@theihtisham/agent-shadow-brain 7.1.0 → 7.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +9 -0
- package/README.md +36 -418
- package/dist/brain/auto-update.js +1 -1
- package/dist/brain/brain-portability.js +1 -1
- package/dist/brain/context-assembly.d.ts +58 -0
- package/dist/brain/context-assembly.d.ts.map +1 -0
- package/dist/brain/context-assembly.js +207 -0
- package/dist/brain/context-assembly.js.map +1 -0
- package/dist/brain/feature-settings.d.ts +53 -0
- package/dist/brain/feature-settings.d.ts.map +1 -0
- package/dist/brain/feature-settings.js +232 -0
- package/dist/brain/feature-settings.js.map +1 -0
- package/dist/brain/global-brain.d.ts +1 -0
- package/dist/brain/global-brain.d.ts.map +1 -1
- package/dist/brain/global-brain.js +3 -3
- package/dist/brain/global-brain.js.map +1 -1
- package/dist/brain/mcp-server.d.ts +2 -0
- package/dist/brain/mcp-server.d.ts.map +1 -1
- package/dist/brain/mcp-server.js +47 -5
- package/dist/brain/mcp-server.js.map +1 -1
- package/dist/brain/orchestrator.js +1 -1
- package/dist/brain/subconscious.d.ts +19 -24
- package/dist/brain/subconscious.d.ts.map +1 -1
- package/dist/brain/subconscious.js +77 -218
- package/dist/brain/subconscious.js.map +1 -1
- package/dist/cli.js +4 -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 +54 -0
- package/dist/context-cli.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 +32 -4
- 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 +5 -27
- package/dist/dashboard/v6-dashboard-html.js.map +1 -1
- package/dist/dashboard/workspace-api.d.ts +10 -0
- package/dist/dashboard/workspace-api.d.ts.map +1 -0
- package/dist/dashboard/workspace-api.js +193 -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 +1555 -0
- package/dist/dashboard/workspace-html.js.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/types.d.ts +4 -2
- package/dist/types.d.ts.map +1 -1
- package/docs/WORKSPACE.md +70 -0
- package/docs/assets/context-flow-v7.2.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.2.0.md +24 -0
- package/package.json +19 -121
|
@@ -0,0 +1,1555 @@
|
|
|
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>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;
|
|
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;
|
|
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; white-space: nowrap; }
|
|
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
|
+
.connection { display: inline-flex; gap: 6px; align-items: center; white-space: nowrap; }
|
|
92
|
+
.dot { width: 7px; height: 7px; border-radius: 50%; display: inline-block; background: currentColor; }
|
|
93
|
+
.connection[data-state="connected"] .dot { color: var(--teal); }
|
|
94
|
+
.connection[data-state="error"] .dot { color: var(--bad); }
|
|
95
|
+
.content { max-width: 1510px; margin: 0 auto; padding: 33px 34px 52px; min-width: 0; }
|
|
96
|
+
.page-head { display: flex; justify-content: space-between; align-items: flex-start; gap: 18px; margin-bottom: 24px; }
|
|
97
|
+
.page-head p { margin-top: 8px; }
|
|
98
|
+
.page-head .button { align-self: center; }
|
|
99
|
+
.panel { border: 1px solid var(--line); background: var(--surface); border-radius: var(--r); min-width: 0; }
|
|
100
|
+
.panel-head { display: flex; justify-content: space-between; gap: 14px; align-items: center; padding: 17px 19px; border-bottom: 1px solid var(--line); }
|
|
101
|
+
.panel-head p { margin-top: 3px; }
|
|
102
|
+
.panel-body { padding: 18px 19px; min-width: 0; }
|
|
103
|
+
.stack { display: grid; gap: 17px; align-content: start; min-width: 0; }
|
|
104
|
+
.split { display: grid; grid-template-columns: minmax(0, 1.4fr) minmax(0, 1fr); align-items: start; gap: 18px; }
|
|
105
|
+
.button {
|
|
106
|
+
appearance: none; color: var(--ink); background: var(--surface); border: 1px solid var(--control);
|
|
107
|
+
min-height: 37px; padding: 6px 11px; font: inherit; font-size: .88rem; font-weight: 650; border-radius: 7px;
|
|
108
|
+
display: inline-flex; align-items: center; justify-content: center; gap: 7px; text-align: center; line-height: 1.22;
|
|
109
|
+
text-decoration: none; cursor: pointer; transition: background var(--ease), color var(--ease), border-color var(--ease);
|
|
110
|
+
}
|
|
111
|
+
.button:hover, .button[aria-pressed="true"] { background: var(--hover); }
|
|
112
|
+
.button[aria-pressed="true"] { color: var(--teal); border-color: var(--teal); }
|
|
113
|
+
.button.primary { background: var(--teal-fill); color: var(--accent-ink); border-color: var(--teal-fill); }
|
|
114
|
+
.button.primary:hover { background: var(--teal-hover); border-color: var(--teal-hover); }
|
|
115
|
+
.button.quiet { background: transparent; border-color: transparent; color: var(--teal); }
|
|
116
|
+
.button.quiet:hover { background: var(--hover); }
|
|
117
|
+
.button.icon-button { width: 37px; padding: 0; }
|
|
118
|
+
.button:disabled { opacity: .65; cursor: default; }
|
|
119
|
+
.button:active:not(:disabled) { transform: translateY(1px); }
|
|
120
|
+
.label { display: grid; gap: 5px; font-size: .88rem; font-weight: 630; min-width: 0; }
|
|
121
|
+
:is(input:not([type=checkbox]),select,textarea) {
|
|
122
|
+
min-width: 0; width: 100%; color: var(--ink); background: var(--surface); font: inherit; font-weight: 450;
|
|
123
|
+
border: 1px solid var(--control); border-radius: 7px; padding: 8px 10px; min-height: 38px;
|
|
124
|
+
}
|
|
125
|
+
:is(input,textarea)::placeholder { color: var(--muted); opacity: 1; }
|
|
126
|
+
textarea { resize: vertical; }
|
|
127
|
+
.search-field { position: relative; min-width: 0; }
|
|
128
|
+
.search-field > [data-icon] { position: absolute; bottom: 10px; left: 10px; color: var(--muted); pointer-events: none; line-height: 1; }
|
|
129
|
+
.search-field input { padding-left: 34px; }
|
|
130
|
+
.inline-check { display: flex; align-items: flex-start; gap: 7px; min-height: 28px; }
|
|
131
|
+
.inline-check input { accent-color: var(--teal-fill); margin-top: 4px; }
|
|
132
|
+
.form-actions { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; }
|
|
133
|
+
.filters { display: grid; grid-template-columns: minmax(180px, 1.7fr) repeat(3,minmax(100px,1fr)) auto; gap: 10px; align-items: end; }
|
|
134
|
+
.filter-bar { display: flex; align-items: end; gap: 10px; flex-wrap: wrap; }
|
|
135
|
+
.filter-bar .label { flex: 1 1 155px; }
|
|
136
|
+
.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); }
|
|
137
|
+
.status.error { background: var(--bad-bg); color: var(--bad); border-color: transparent; }
|
|
138
|
+
.status.error .button { color: var(--bad); }
|
|
139
|
+
.status.error .button:hover { background: var(--surface); }
|
|
140
|
+
.blank { display: grid; gap: 6px; padding: 29px 19px; }
|
|
141
|
+
.blank > .icon { color: var(--teal); margin-bottom: 4px; }
|
|
142
|
+
.blank a { justify-self: start; margin-top: 4px; }
|
|
143
|
+
.skeleton { display: grid; gap: 12px; padding: 19px; }
|
|
144
|
+
.skeleton i { width: 82%; height: 12px; display: block; border-radius: 4px; background: var(--skeleton); }
|
|
145
|
+
.skeleton i:nth-child(2) { width: 63%; }
|
|
146
|
+
.skeleton i:nth-child(3) { width: 71%; }
|
|
147
|
+
.tag {
|
|
148
|
+
background: var(--inset); color: var(--muted); padding: 3px 7px; border-radius: 5px;
|
|
149
|
+
font-size: .79rem; font-weight: 600; line-height: 1.3; overflow-wrap: anywhere;
|
|
150
|
+
}
|
|
151
|
+
.tag.project { background: var(--project-bg); color: var(--project); }
|
|
152
|
+
.tag.agent { background: var(--agent-bg); color: var(--agent); }
|
|
153
|
+
.tag.memory { background: var(--memory-bg); color: var(--memory); }
|
|
154
|
+
.tags { display: flex; align-items: center; flex-wrap: wrap; gap: 5px 7px; }
|
|
155
|
+
.summary {
|
|
156
|
+
display: grid; grid-template-columns: repeat(3,minmax(0,1fr)); background: var(--surface);
|
|
157
|
+
border: 1px solid var(--line); border-radius: var(--r); margin-bottom: 18px;
|
|
158
|
+
}
|
|
159
|
+
.summary-item { display: flex; align-items: center; gap: 13px; min-width: 0; padding: 18px 19px; }
|
|
160
|
+
.summary-item + .summary-item { border-left: 1px solid var(--line); }
|
|
161
|
+
.tile-icon { display: grid; place-items: center; width: 37px; height: 37px; border-radius: 7px; flex: none; background: var(--tint); color: var(--teal); }
|
|
162
|
+
.summary dt { color: var(--muted); font-size: .84rem; font-weight: 600; }
|
|
163
|
+
.summary dd { font-size: 1.3rem; font-weight: 700; line-height: 1.12; font-variant-numeric: tabular-nums; margin-top: 4px; }
|
|
164
|
+
.list { list-style: none; padding: 0; }
|
|
165
|
+
.list > li + li { border-top: 1px solid var(--line); }
|
|
166
|
+
.project-row { display: grid; grid-template-columns: minmax(0,1.5fr) minmax(65px,.55fr) auto; align-items: center; gap: 12px; padding: 12px 19px; }
|
|
167
|
+
.project-name { display: flex; gap: 7px; align-items: center; min-width: 0; font-weight: 650; overflow-wrap: anywhere; }
|
|
168
|
+
.project-name:hover { text-decoration: underline; }
|
|
169
|
+
.project-detail { margin: 3px 0 0 22px; }
|
|
170
|
+
.project-count { color: var(--muted); font-size: .81rem; text-align: right; font-variant-numeric: tabular-nums; }
|
|
171
|
+
.project-meter { display: block; width: 100%; height: 5px; margin-top: 4px; border-radius: 3px; overflow: hidden; background: var(--inset); }
|
|
172
|
+
.project-meter span { display: block; height: 100%; background: var(--teal); border-radius: inherit; opacity: .75; }
|
|
173
|
+
.event { display: grid; grid-template-columns: 27px minmax(0,1fr) auto; gap: 9px; align-items: start; padding: 12px 19px; }
|
|
174
|
+
.event .tile-icon { width: 26px; height: 26px; }
|
|
175
|
+
.event strong { display: block; font-size: .89rem; font-weight: 630; overflow-wrap: anywhere; }
|
|
176
|
+
.event time { color: var(--muted); font-size: .77rem; }
|
|
177
|
+
.event-meta { margin-top: 2px; }
|
|
178
|
+
.overview-memory { display: grid; gap: 5px; padding: 12px 19px; }
|
|
179
|
+
.overview-memory strong { overflow-wrap: anywhere; }
|
|
180
|
+
.toolbar-end { margin-left: auto; }
|
|
181
|
+
.list-footer { border-top: 1px solid var(--line); display: flex; justify-content: space-between; align-items: center; gap: 10px; padding: 12px 18px; }
|
|
182
|
+
.memory-row { padding: 14px 19px; display: grid; gap: 8px; }
|
|
183
|
+
.memory-head { display: flex; justify-content: space-between; align-items: start; gap: 10px; }
|
|
184
|
+
.memory-head time { white-space: nowrap; }
|
|
185
|
+
.memory-row p { max-width: 90ch; white-space: pre-wrap; }
|
|
186
|
+
.memory-row details summary { color: var(--teal); font-size: .85rem; font-weight: 650; cursor: pointer; width: fit-content; max-width: 100%; }
|
|
187
|
+
.memory-row details ul { padding-left: 19px; margin-top: 4px; }
|
|
188
|
+
.memory-row details li { overflow-wrap: anywhere; }
|
|
189
|
+
.source { display: flex; align-items: baseline; gap: 6px; color: var(--muted); min-width: 0; }
|
|
190
|
+
.source .icon { align-self: center; }
|
|
191
|
+
.graph-body { display: grid; grid-template-columns: minmax(0, 1fr) minmax(225px, 27%); min-width: 0; }
|
|
192
|
+
.graph-stage { position: relative; min-width: 0; height: 480px; background: var(--canvas); overflow: hidden; }
|
|
193
|
+
#graph-svg { display: block; width: 100%; height: 100%; touch-action: none; cursor: grab; user-select: none; }
|
|
194
|
+
#graph-svg.dragging { cursor: grabbing; }
|
|
195
|
+
.cluster-area { fill: var(--project-bg); fill-opacity: .49; stroke: var(--graph-project); stroke-opacity: .25; }
|
|
196
|
+
.graph-node { color: var(--memory); cursor: pointer; }
|
|
197
|
+
.graph-node.project { color: var(--project); }
|
|
198
|
+
.graph-node.agent { color: var(--agent); }
|
|
199
|
+
.graph-node .node-surface { fill: var(--surface); stroke: var(--line); stroke-width: 1.25; }
|
|
200
|
+
.graph-node.project .node-surface { fill: var(--project-bg); stroke: var(--graph-project); }
|
|
201
|
+
.graph-node.agent .node-surface { fill: var(--agent-bg); stroke: var(--graph-agent); }
|
|
202
|
+
.graph-node.dot-node .node-surface { stroke: var(--memory); }
|
|
203
|
+
.graph-node .node-glyph { fill: currentColor; }
|
|
204
|
+
.graph-node text { fill: var(--ink); font-size: 12px; font-weight: 600; }
|
|
205
|
+
.graph-node .node-count { fill: var(--muted); font-size: 11px; font-weight: 550; }
|
|
206
|
+
.graph-node:is(:hover,:focus-visible,[aria-pressed="true"]) .node-surface { stroke: currentColor; stroke-width: 2.2; }
|
|
207
|
+
.graph-node.dot-node:is(:hover,:focus-visible,[aria-pressed="true"]) .node-surface { fill: var(--memory-bg); }
|
|
208
|
+
.graph-node:focus-visible { outline: none; }
|
|
209
|
+
.graph-node.unrelated { opacity: .34; }
|
|
210
|
+
.graph-edge { fill: none; stroke: var(--graph-line); stroke-width: 1.15; opacity: .53; }
|
|
211
|
+
.graph-edge.project-edge { stroke: var(--graph-project); }
|
|
212
|
+
.graph-edge.agent-edge { stroke: var(--graph-agent); stroke-dasharray: 4 3; opacity: .35; }
|
|
213
|
+
.graph-edge.active { stroke: var(--teal); stroke-width: 2.1; opacity: 1; }
|
|
214
|
+
.graph-edge.agent-edge.active { stroke: var(--agent); }
|
|
215
|
+
.graph-edge.unrelated { opacity: .13; }
|
|
216
|
+
.graph-tooltip { pointer-events: none; }
|
|
217
|
+
.graph-tooltip rect { fill: var(--surface); stroke: var(--memory); }
|
|
218
|
+
.graph-tooltip text { fill: var(--ink); font-size: 12px; font-weight: 650; }
|
|
219
|
+
.graph-tools { position: absolute; bottom: 12px; left: 12px; display: flex; gap: 5px; align-items: center; }
|
|
220
|
+
.graph-tools .button { min-height: 32px; }
|
|
221
|
+
.graph-tools .icon-button { width: 32px; }
|
|
222
|
+
.graph-tools output { padding: 4px 6px; background: var(--surface); color: var(--muted); border-radius: 5px; }
|
|
223
|
+
.graph-inspector { min-width: 0; border-left: 1px solid var(--line); max-height: 480px; overflow-y: auto; }
|
|
224
|
+
.graph-inspector .panel-body { display: grid; gap: 11px; }
|
|
225
|
+
.graph-inspector p { white-space: pre-wrap; }
|
|
226
|
+
.related { display: grid; gap: 5px; }
|
|
227
|
+
.related .button { justify-content: start; overflow-wrap: anywhere; text-align: left; }
|
|
228
|
+
.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; }
|
|
229
|
+
.legend > span { display: inline-flex; align-items: center; gap: 5px; }
|
|
230
|
+
.legend-glyph { display: inline-block; width: 9px; height: 9px; background: currentColor; flex: none; }
|
|
231
|
+
.legend-glyph.project { border-radius: 2px; color: var(--project); }
|
|
232
|
+
.legend-glyph.agent { border-radius: 50%; color: var(--agent); }
|
|
233
|
+
.legend-glyph.memory { width: 7px; height: 7px; transform: rotate(45deg); color: var(--memory); margin-right: 2px; }
|
|
234
|
+
.legend-line { display: inline-block; width: 16px; border-top: 2px solid var(--graph-line); }
|
|
235
|
+
.legend-line.project-edge { border-color: var(--graph-project); }
|
|
236
|
+
.legend-line.agent-edge { border-color: var(--graph-agent); border-top-style: dashed; }
|
|
237
|
+
.graph-list { min-width: 0; max-height: 480px; overflow-y: auto; }
|
|
238
|
+
.graph-list .graph-row { padding: 9px 12px; }
|
|
239
|
+
.graph-list .button { justify-content: flex-start; text-align: left; width: 100%; }
|
|
240
|
+
.graph-row .tag { margin-left: auto; }
|
|
241
|
+
.context-layout { grid-template-columns: minmax(250px,.9fr) minmax(0,1.35fr); }
|
|
242
|
+
.context-config { display: grid; gap: 12px; }
|
|
243
|
+
.context-preview { white-space: pre-wrap; overflow-wrap: anywhere; background: var(--inset); border-radius: 7px; padding: 13px; margin: 0; max-height: 420px; overflow: auto; }
|
|
244
|
+
.context-reasons { display: grid; gap: 10px; }
|
|
245
|
+
.context-reasons summary { cursor: pointer; font-weight: 650; }
|
|
246
|
+
.context-reasons li { padding: 8px 0; }
|
|
247
|
+
.context-reasons li strong { overflow-wrap: anywhere; }
|
|
248
|
+
.context-reasons li p { margin-top: 2px; }
|
|
249
|
+
.settings { max-width: 930px; }
|
|
250
|
+
.setting { display: flex; align-items: center; justify-content: space-between; gap: 15px; padding: 16px 19px; }
|
|
251
|
+
.setting p { margin-top: 4px; }
|
|
252
|
+
.switch { display: inline-flex; align-items: center; justify-content: end; gap: 7px; min-width: 87px; min-height: 36px; border-color: transparent; background: transparent; }
|
|
253
|
+
.switch-track { width: 32px; height: 18px; border-radius: 11px; position: relative; background: var(--control); }
|
|
254
|
+
.switch-track::after {
|
|
255
|
+
content: ""; width: 12px; height: 12px; position: absolute; left: 3px; top: 3px; border-radius: 50%;
|
|
256
|
+
background: white; transition: transform var(--ease);
|
|
257
|
+
}
|
|
258
|
+
.switch[aria-checked="true"] .switch-track { background: var(--teal-fill); }
|
|
259
|
+
.switch[aria-checked="true"] .switch-track::after { transform: translateX(14px); background: var(--accent-ink); }
|
|
260
|
+
.setting select { width: 131px; flex: none; }
|
|
261
|
+
.save-status { padding: 8px 19px; min-height: 35px; border-top: 1px solid var(--line); }
|
|
262
|
+
.save-status[data-error="true"] { color: var(--bad); }
|
|
263
|
+
.notice { margin: 0 0 16px; }
|
|
264
|
+
.entered { animation: enter var(--ease) both; }
|
|
265
|
+
@keyframes enter { from { opacity: .7; transform: translateY(3px); } to { opacity: 1; transform: translateY(0); } }
|
|
266
|
+
html[data-motion="reduce"] *, html[data-motion="reduce"] *::before, html[data-motion="reduce"] *::after { animation-duration: .01ms !important; transition-duration: .01ms !important; }
|
|
267
|
+
@media (prefers-reduced-motion: reduce) {
|
|
268
|
+
*, *::before, *::after { animation-duration: .01ms !important; transition-duration: .01ms !important; }
|
|
269
|
+
}
|
|
270
|
+
@media (max-width: 1140px) {
|
|
271
|
+
:root { --sidebar: 188px; }
|
|
272
|
+
.top, .content { padding-left: 24px; padding-right: 24px; }
|
|
273
|
+
.filters { grid-template-columns: repeat(3,minmax(0,1fr)); }
|
|
274
|
+
.filters .search-field { grid-column: span 2; }
|
|
275
|
+
.filters .button { grid-column: 3; grid-row: 1; }
|
|
276
|
+
.overview-columns { grid-template-columns: minmax(0,1.25fr) minmax(0,1fr); }
|
|
277
|
+
.event { grid-template-columns: 27px minmax(0,1fr); }
|
|
278
|
+
.event time { grid-column: 2; }
|
|
279
|
+
}
|
|
280
|
+
@media (max-width: 900px) {
|
|
281
|
+
:root { --sidebar: 66px; }
|
|
282
|
+
.rail { padding-left: 10px; padding-right: 10px; }
|
|
283
|
+
.rail .brand { justify-content: center; padding: 0; }
|
|
284
|
+
.nav-link { justify-content: center; min-height: 41px; }
|
|
285
|
+
.brand-name, .nav-label, .rail-foot .arrow, .rail-build { display: none; }
|
|
286
|
+
.context-layout { grid-template-columns: minmax(0,1fr); }
|
|
287
|
+
}
|
|
288
|
+
@media (max-width: 690px) {
|
|
289
|
+
:root { --header: 48px; font-size: 14px; }
|
|
290
|
+
.shell { display: block; }
|
|
291
|
+
.rail { z-index: var(--sticky); height: auto; flex-direction: row; align-items: center; gap: 7px; padding: 5px 10px; }
|
|
292
|
+
.rail-nav { display: flex; gap: 3px; }
|
|
293
|
+
.nav-link { padding: 7px 9px; min-height: 35px; }
|
|
294
|
+
.rail-foot { margin: 0 0 0 auto; padding: 0; border: 0; }
|
|
295
|
+
.brand-mark { width: 30px; height: 30px; }
|
|
296
|
+
.top { padding: 7px 13px; }
|
|
297
|
+
.top-actions { gap: 8px; }
|
|
298
|
+
.content { padding: 22px 12px 32px; }
|
|
299
|
+
h1 { font-size: 1.68rem; }
|
|
300
|
+
.page-head { gap: 8px; margin-bottom: 17px; flex-wrap: wrap; }
|
|
301
|
+
.summary { margin-bottom: 13px; }
|
|
302
|
+
.summary-item { gap: 6px; padding: 11px 9px; }
|
|
303
|
+
.summary-item .tile-icon { display: none; }
|
|
304
|
+
.split { grid-template-columns: minmax(0,1fr); gap: 13px; }
|
|
305
|
+
.graph-body { grid-template-columns: minmax(0,1fr); }
|
|
306
|
+
.graph-stage, .graph-list { height: 365px; }
|
|
307
|
+
.graph-inspector { border-left: 0; border-top: 1px solid var(--line); max-height: 340px; }
|
|
308
|
+
.panel-head, .panel-body, .memory-row, .setting { padding-left: 12px; padding-right: 12px; }
|
|
309
|
+
.project-row, .event { padding-left: 12px; padding-right: 12px; }
|
|
310
|
+
.list-footer { flex-wrap: wrap; }
|
|
311
|
+
.graph-mode { margin-left: 0; }
|
|
312
|
+
}
|
|
313
|
+
@media (max-width: 355px) {
|
|
314
|
+
.rail { gap: 3px; }
|
|
315
|
+
.nav-link { padding-left: 7px; padding-right: 7px; }
|
|
316
|
+
.crumb > .muted, .crumb .icon { display: none; }
|
|
317
|
+
.filters { grid-template-columns: repeat(2,minmax(0,1fr)); }
|
|
318
|
+
.filters .search-field { grid-column: 1 / -1; }
|
|
319
|
+
.filters .button { grid-row: auto; grid-column: auto; }
|
|
320
|
+
}
|
|
321
|
+
</style>
|
|
322
|
+
</head>
|
|
323
|
+
<body>
|
|
324
|
+
<a class="skip button" href="#main-content">Skip to content</a>
|
|
325
|
+
<div class="shell">
|
|
326
|
+
<aside class="rail">
|
|
327
|
+
<a class="brand" href="#overview" aria-label="Shadow Brain overview">
|
|
328
|
+
<span class="brand-mark" data-icon="brand"></span><span class="brand-name">Shadow Brain</span>
|
|
329
|
+
</a>
|
|
330
|
+
<nav class="rail-nav" aria-label="Workspace">
|
|
331
|
+
<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>
|
|
332
|
+
<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>
|
|
333
|
+
<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>
|
|
334
|
+
<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>
|
|
335
|
+
<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>
|
|
336
|
+
</nav>
|
|
337
|
+
<div class="rail-foot">
|
|
338
|
+
<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>
|
|
339
|
+
<p class="rail-build" id="rail-version">Local workspace</p>
|
|
340
|
+
</div>
|
|
341
|
+
</aside>
|
|
342
|
+
<div class="main">
|
|
343
|
+
<header class="top">
|
|
344
|
+
<div class="crumb"><span class="muted">Workspace</span><span data-icon="chevron"></span><strong id="crumb">Overview</strong></div>
|
|
345
|
+
<div class="top-actions">
|
|
346
|
+
<span class="connection muted small" id="connection" data-state="loading"><span class="dot"></span><span id="connection-label">Connecting</span></span>
|
|
347
|
+
<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>
|
|
348
|
+
</div>
|
|
349
|
+
</header>
|
|
350
|
+
<main id="main-content" class="content" tabindex="-1">
|
|
351
|
+
<div class="notice" id="workspace-notice" role="status" hidden></div>
|
|
352
|
+
<section id="page-overview" aria-labelledby="title-overview">
|
|
353
|
+
<div class="page-head">
|
|
354
|
+
<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>
|
|
355
|
+
<a href="#context" class="button primary"><span data-icon="context"></span>Assemble context</a>
|
|
356
|
+
</div>
|
|
357
|
+
<div id="overview-body" aria-busy="true"></div>
|
|
358
|
+
</section>
|
|
359
|
+
<section id="page-memories" aria-labelledby="title-memories" hidden>
|
|
360
|
+
<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>
|
|
361
|
+
<div class="stack">
|
|
362
|
+
<form id="memory-form" class="panel panel-body filters" role="search" aria-label="Search memories">
|
|
363
|
+
<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>
|
|
364
|
+
<label class="label" for="memory-project">Project<select id="memory-project"><option value="">All projects</option></select></label>
|
|
365
|
+
<label class="label" for="memory-category">Category<select id="memory-category"><option value="">All categories</option></select></label>
|
|
366
|
+
<label class="label" for="memory-agent">Agent<select id="memory-agent"><option value="">All agents</option></select></label>
|
|
367
|
+
<button class="button primary" id="memory-search" type="submit"><span data-icon="search"></span>Search</button>
|
|
368
|
+
</form>
|
|
369
|
+
<div id="memory-results" class="panel" aria-busy="true"></div>
|
|
370
|
+
</div>
|
|
371
|
+
</section>
|
|
372
|
+
<section id="page-graph" aria-labelledby="title-graph" hidden>
|
|
373
|
+
<div class="page-head"><div><h1 id="title-graph" tabindex="-1">Graph</h1><p class="muted">Explore how stored memories connect to projects and agents.</p></div></div>
|
|
374
|
+
<div class="stack">
|
|
375
|
+
<div class="panel panel-body filter-bar">
|
|
376
|
+
<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>
|
|
377
|
+
<label class="label" for="graph-project">Project<select id="graph-project"><option value="">All projects</option></select></label>
|
|
378
|
+
<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>
|
|
379
|
+
<div class="form-actions toolbar-end graph-mode" role="group" aria-label="Graph view">
|
|
380
|
+
<button type="button" class="button" id="graph-mode-map" aria-pressed="true"><span data-icon="graph"></span>Map</button>
|
|
381
|
+
<button type="button" class="button" id="graph-mode-list" aria-pressed="false"><span data-icon="list"></span>List</button>
|
|
382
|
+
</div>
|
|
383
|
+
</div>
|
|
384
|
+
<div class="panel" id="graph-panel" aria-busy="true">
|
|
385
|
+
<div class="panel-head"><h2>Memory relationships</h2><span id="graph-count" class="muted small" role="status"></span></div>
|
|
386
|
+
<div id="graph-status"></div>
|
|
387
|
+
<div class="graph-body" id="graph-body" hidden>
|
|
388
|
+
<div class="graph-stage" id="graph-stage">
|
|
389
|
+
<svg id="graph-svg" xmlns="http://www.w3.org/2000/svg" role="group" aria-label="Memory relationship map. Use arrow keys to navigate nodes."></svg>
|
|
390
|
+
<div class="graph-tools" role="group" aria-label="Map zoom">
|
|
391
|
+
<button class="button icon-button" type="button" id="graph-out" aria-label="Zoom out" title="Zoom out"><span data-icon="minus"></span></button>
|
|
392
|
+
<button class="button icon-button" type="button" id="graph-in" aria-label="Zoom in" title="Zoom in"><span data-icon="plus"></span></button>
|
|
393
|
+
<button class="button" type="button" id="graph-fit">Fit</button>
|
|
394
|
+
<output id="graph-scale" class="small" aria-label="Zoom level">100%</output>
|
|
395
|
+
</div>
|
|
396
|
+
</div>
|
|
397
|
+
<div id="graph-list" class="graph-list" hidden></div>
|
|
398
|
+
<aside id="graph-inspector" class="graph-inspector" aria-label="Selected node details"></aside>
|
|
399
|
+
</div>
|
|
400
|
+
<div id="graph-legend" class="legend" hidden>
|
|
401
|
+
<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>
|
|
402
|
+
<span><i class="legend-line project-edge"></i> Belongs to project</span><span><i class="legend-line agent-edge"></i> Recorded by agent</span>
|
|
403
|
+
</div>
|
|
404
|
+
</div>
|
|
405
|
+
</div>
|
|
406
|
+
</section>
|
|
407
|
+
<section id="page-context" aria-labelledby="title-context" hidden>
|
|
408
|
+
<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>
|
|
409
|
+
<div class="split context-layout">
|
|
410
|
+
<div class="panel">
|
|
411
|
+
<div class="panel-head"><h2>Task details</h2></div>
|
|
412
|
+
<form class="panel-body context-config" id="context-form">
|
|
413
|
+
<label class="label" for="context-project">Project<select id="context-project" required><option value="">Select a project</option></select></label>
|
|
414
|
+
<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>
|
|
415
|
+
<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>
|
|
416
|
+
<label class="inline-check small" id="context-cross-row" hidden><input type="checkbox" id="context-cross">Include eligible memories from other projects</label>
|
|
417
|
+
<div id="context-gate"></div>
|
|
418
|
+
<div class="form-actions"><button class="button primary" id="context-submit" type="submit" disabled><span data-icon="context"></span>Assemble context</button></div>
|
|
419
|
+
</form>
|
|
420
|
+
</div>
|
|
421
|
+
<div class="panel" id="context-result"><div class="panel-head"><h2>Context preview</h2></div><div id="context-output"></div></div>
|
|
422
|
+
</div>
|
|
423
|
+
</section>
|
|
424
|
+
<section id="page-features" aria-labelledby="title-features" hidden>
|
|
425
|
+
<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>
|
|
426
|
+
<div class="stack settings">
|
|
427
|
+
<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>
|
|
428
|
+
<div class="panel">
|
|
429
|
+
<div class="panel-head"><h2>Display</h2></div>
|
|
430
|
+
<ul class="list">
|
|
431
|
+
<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>
|
|
432
|
+
<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>
|
|
433
|
+
</ul>
|
|
434
|
+
<p id="display-status" class="save-status small muted" role="status" hidden></p>
|
|
435
|
+
</div>
|
|
436
|
+
</div>
|
|
437
|
+
</section>
|
|
438
|
+
</main>
|
|
439
|
+
</div>
|
|
440
|
+
</div>
|
|
441
|
+
<script>
|
|
442
|
+
'use strict';
|
|
443
|
+
|
|
444
|
+
const glyphs = {
|
|
445
|
+
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"/>',
|
|
446
|
+
overview: '<rect x="3.5" y="4" width="17" height="16" rx="2"/><path d="M3.5 9.5h17m-11 0V20"/>',
|
|
447
|
+
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"/>',
|
|
448
|
+
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"/>',
|
|
449
|
+
context: '<path d="M9 8h6m-6 4h6m-6 4h4M6 3h9l4 4v14H6zM15 3v5h4"/>',
|
|
450
|
+
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))"/>',
|
|
451
|
+
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"/>',
|
|
452
|
+
diagonal: '<path d="M6 18 18 6M8 6h10v10"/>',
|
|
453
|
+
chevron: '<path d="m9 6 6 6-6 6"/>',
|
|
454
|
+
left: '<path d="m14 6-6 6 6 6"/>',
|
|
455
|
+
moon: '<path d="M20 15a8.5 8.5 0 0 1-11-11 8.5 8.5 0 1 0 11 11Z"/>',
|
|
456
|
+
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"/>',
|
|
457
|
+
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"/>',
|
|
458
|
+
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"/>',
|
|
459
|
+
activity: '<path d="M3 12h4l3-7 4 14 3-7h4"/>',
|
|
460
|
+
search: '<circle cx="10.5" cy="10.5" r="6"/><path d="m15 15 5 5"/>',
|
|
461
|
+
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"/>',
|
|
462
|
+
list: '<path d="M8 6h12M8 12h12M8 18h12M4 6h.01M4 12h.01M4 18h.01"/>',
|
|
463
|
+
plus: '<path d="M12 5v14M5 12h14"/>',
|
|
464
|
+
minus: '<path d="M5 12h14"/>',
|
|
465
|
+
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"/>',
|
|
466
|
+
warning: '<path d="m12 3 10 17H2L12 3zm0 6v4m0 3h.01"/>',
|
|
467
|
+
refresh: '<path d="M19.5 9a8 8 0 1 0 .3 5.5M20 4v5h-5"/>'
|
|
468
|
+
};
|
|
469
|
+
const viewNames = { overview: 'Overview', memories: 'Memories', graph: 'Graph', context: 'Context', features: 'Features' };
|
|
470
|
+
const definitions = [
|
|
471
|
+
{ id: 'contextAssembly', label: 'Context assembly', description: 'Build a task-specific brief from saved project memories.' },
|
|
472
|
+
{ id: 'crossProjectContext', label: 'Cross-project context', description: 'Allow context assembly to include eligible memories from other projects when requested.' },
|
|
473
|
+
{ id: 'automaticBriefing', label: 'Automatic briefing', description: 'Include a context brief when a supported agent starts work in a project.' },
|
|
474
|
+
{ id: 'liveActivity', label: 'Live activity', description: 'Refresh the workspace activity feed automatically.' }
|
|
475
|
+
];
|
|
476
|
+
const $ = (id) => document.getElementById(id);
|
|
477
|
+
const esc = (value) => String(value ?? '').replace(/[&<>"']/g, (char) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[char]));
|
|
478
|
+
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>';
|
|
479
|
+
const list = (value) => Array.isArray(value) ? value : [];
|
|
480
|
+
const compact = (value) => String(value ?? '').replace(/\s+/g, ' ').trim();
|
|
481
|
+
const shorten = (value, limit) => value.length > limit ? value.slice(0, limit - 1) + '…' : value;
|
|
482
|
+
const number = (value) => Number(value || 0).toLocaleString();
|
|
483
|
+
const countLabel = (value, singular, plural = singular + 's') => number(value) + ' ' + (Number(value) === 1 ? singular : plural);
|
|
484
|
+
const nice = (value) => String(value ?? '').replace(/[-_]/g, ' ').replace(/^./, (char) => char.toUpperCase());
|
|
485
|
+
const tags = (items) => '<div class="tags">' + items.filter(([, value]) => value).map(([kind, value]) =>
|
|
486
|
+
'<span class="tag ' + esc(kind) + '">' + esc(value) + '</span>').join('') + '</div>';
|
|
487
|
+
const DATE = new Intl.DateTimeFormat(undefined, { month: 'short', day: 'numeric', year: 'numeric' });
|
|
488
|
+
const TIME = new Intl.DateTimeFormat(undefined, { month: 'short', day: 'numeric', hour: 'numeric', minute: '2-digit' });
|
|
489
|
+
const state = {
|
|
490
|
+
view: '', projects: [], overviewLoaded: false, overviewPending: null,
|
|
491
|
+
activity: [], activityEnabled: null, activityPending: false, features: null, featurePending: null, featureSaving: false,
|
|
492
|
+
memory: { query: null, request: 0, controller: null, limit: 20, offset: 0 },
|
|
493
|
+
graph: { snapshot: null, project: null, mode: matchMedia('(max-width: 690px)').matches ? 'list' : 'map', selected: '', nodes: [], edges: [], layout: null, zoom: 1, panX: 0, panY: 0, request: 0, controller: null, drag: null },
|
|
494
|
+
context: { pack: null, pending: false, dirty: false }
|
|
495
|
+
};
|
|
496
|
+
|
|
497
|
+
document.querySelectorAll('[data-icon]').forEach((element) => { element.innerHTML = icon(element.dataset.icon); });
|
|
498
|
+
|
|
499
|
+
function dateMarkup(value, full = false) {
|
|
500
|
+
if (!value) return '';
|
|
501
|
+
const date = new Date(value);
|
|
502
|
+
if (!Number.isFinite(date.getTime())) return '';
|
|
503
|
+
const today = new Date();
|
|
504
|
+
const dateText = full || date.getFullYear() !== today.getFullYear() ? DATE.format(date) : TIME.format(date);
|
|
505
|
+
return '<time class="small muted" datetime="' + esc(date.toISOString()) + '" title="' + esc(date.toLocaleString()) + '">' + esc(dateText) + '</time>';
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
function blank(iconName, title, detail, action = '') {
|
|
509
|
+
return '<div class="blank">' + icon(iconName) + '<h3>' + esc(title) + '</h3><p class="small muted">' + esc(detail) + '</p>' + action + '</div>';
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
function skeleton(label) {
|
|
513
|
+
return '<div class="skeleton" role="status"><span class="muted small">' + esc(label) + '</span><i></i><i></i><i></i></div>';
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
function errorMarkup(message, retry) {
|
|
517
|
+
return '<div class="status error" role="alert">' + icon('warning') + '<span>' + esc(message) + '</span>' +
|
|
518
|
+
(retry ? '<button class="button quiet" data-retry="' + retry + '" type="button">Try again</button>' : '') + '</div>';
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
function setBusy(element, isBusy) {
|
|
522
|
+
element.setAttribute('aria-busy', String(isBusy));
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
function connection(value) {
|
|
526
|
+
$('connection').dataset.state = value;
|
|
527
|
+
$('connection-label').textContent = value === 'connected' ? 'Local server' : value === 'error' ? 'Disconnected' : 'Connecting';
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
async function api(path, { body, signal } = {}) {
|
|
531
|
+
let response;
|
|
532
|
+
try {
|
|
533
|
+
response = await fetch('/api/workspace/' + path, {
|
|
534
|
+
headers: body ? { Accept: 'application/json', 'Content-Type': 'application/json' } : { Accept: 'application/json' },
|
|
535
|
+
method: body ? 'POST' : 'GET',
|
|
536
|
+
body: body ? JSON.stringify(body) : undefined,
|
|
537
|
+
cache: 'no-store',
|
|
538
|
+
signal: signal || AbortSignal.timeout(12000)
|
|
539
|
+
});
|
|
540
|
+
} catch (error) {
|
|
541
|
+
if (error.name === 'AbortError') throw error;
|
|
542
|
+
connection('error');
|
|
543
|
+
throw new Error('Could not reach the local Shadow Brain server.');
|
|
544
|
+
}
|
|
545
|
+
connection('connected');
|
|
546
|
+
const data = await response.json().catch(() => null);
|
|
547
|
+
if (!response.ok || !data) {
|
|
548
|
+
const error = new Error(response.ok ? 'The server returned an unreadable response.' : data?.error || 'The server could not complete this request.');
|
|
549
|
+
error.status = response.status;
|
|
550
|
+
throw error;
|
|
551
|
+
}
|
|
552
|
+
return data;
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
function optionSet(id, placeholder, values, preferred) {
|
|
556
|
+
const select = $(id);
|
|
557
|
+
const previous = preferred ?? select.value;
|
|
558
|
+
const choices = values.filter((item) => item && item.id);
|
|
559
|
+
const signature = JSON.stringify(choices);
|
|
560
|
+
if (select.dataset.signature !== signature) {
|
|
561
|
+
select.replaceChildren(new Option(placeholder, ''), ...choices.map((item) => new Option(item.name || item.id, item.id)));
|
|
562
|
+
select.dataset.signature = signature;
|
|
563
|
+
}
|
|
564
|
+
if (previous && Array.from(select.options).some((option) => option.value === previous)) select.value = previous;
|
|
565
|
+
else select.value = '';
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
function queryPath(name, params) {
|
|
569
|
+
const search = new URLSearchParams();
|
|
570
|
+
Object.entries(params).forEach(([key, value]) => { if (value !== '' && value != null) search.set(key, String(value)); });
|
|
571
|
+
return name + (search.size ? '?' + search : '');
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
function routeLink(view, params) {
|
|
575
|
+
return '#' + queryPath(view, params);
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
function renderSummary(totals) {
|
|
579
|
+
return '<dl class="summary">' + [
|
|
580
|
+
['memory', 'Stored memories', totals.memories], ['project', 'Projects', totals.projects], ['agent', 'Contributing agents', totals.agents]
|
|
581
|
+
].map(([image, label, value]) => '<div class="summary-item"><span class="tile-icon">' + icon(image) +
|
|
582
|
+
'</span><div><dt>' + label + '</dt><dd>' + number(value) + '</dd></div></div>').join('') + '</dl>';
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
function renderProjects(projects) {
|
|
586
|
+
if (!projects.length) return blank('project', 'No projects yet', 'Projects appear after an agent stores a memory with a project scope.');
|
|
587
|
+
const max = Math.max(1, ...projects.map((project) => Number(project.memories) || 0));
|
|
588
|
+
return '<ul class="list">' + [...projects].sort((a, b) => b.memories - a.memories).slice(0, 8).map((project) => {
|
|
589
|
+
const size = Math.max(0, Math.min(100, Math.round((Number(project.memories) || 0) / max * 100)));
|
|
590
|
+
const href = routeLink('memories', { project: project.id });
|
|
591
|
+
return '<li class="project-row"><div><a class="project-name" href="' + esc(href) + '">' + icon('project', true) +
|
|
592
|
+
'<span>' + esc(project.name || project.id) + '</span></a>' +
|
|
593
|
+
(project.updatedAt ? '<div class="project-detail">' + dateMarkup(project.updatedAt, true) + '</div>' : '') +
|
|
594
|
+
'</div><div class="project-count">' + countLabel(project.memories, 'memory', 'memories') +
|
|
595
|
+
'<span class="project-meter" aria-hidden="true"><span style="width:' + size + '%"></span></span></div>' +
|
|
596
|
+
'<a class="button icon-button quiet" aria-label="' + esc('View memories for ' + (project.name || project.id)) + '" href="' + esc(href) + '">' + icon('chevron') + '</a></li>';
|
|
597
|
+
}).join('') + '</ul>';
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
function renderOverviewMemories(memories) {
|
|
601
|
+
if (!memories.length) return '';
|
|
602
|
+
return '<div class="panel"><div class="panel-head"><h2>Latest memories</h2><a class="button quiet" href="#memories">Browse ' + icon('chevron', true) +
|
|
603
|
+
'</a></div><ul class="list">' + memories.map((item) => '<li class="overview-memory"><div class="memory-head"><strong>' +
|
|
604
|
+
esc(item.title || nice(item.category) || 'Memory') + '</strong>' + dateMarkup(item.updatedAt, true) + '</div>' +
|
|
605
|
+
tags([['project', item.projectName || item.project], ['agent', item.agent], ['memory', nice(item.category)]]) + '</li>').join('') + '</ul></div>';
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
function renderActivity() {
|
|
609
|
+
const target = $('activity-body');
|
|
610
|
+
if (!target) return;
|
|
611
|
+
const enabled = state.activityEnabled;
|
|
612
|
+
$('activity-status').textContent = enabled === true ? 'Live' : enabled === false ? 'Paused' : '';
|
|
613
|
+
if (enabled === false) {
|
|
614
|
+
target.innerHTML = blank('activity', 'Live activity is paused', 'Turn it back on in Features to see recent events.',
|
|
615
|
+
'<a class="button quiet" href="#features">Open Features ' + icon('chevron', true) + '</a>');
|
|
616
|
+
return;
|
|
617
|
+
}
|
|
618
|
+
if (!state.activity.length) {
|
|
619
|
+
target.innerHTML = blank('activity', 'No recent activity', 'Memory and agent events appear here as work happens.');
|
|
620
|
+
return;
|
|
621
|
+
}
|
|
622
|
+
target.innerHTML = '<ul class="list">' + state.activity.slice(0, 8).map((event) => {
|
|
623
|
+
const image = String(event.type ?? '').includes('memory') ? 'memory' : 'activity';
|
|
624
|
+
const meta = [event.agent, event.project].filter(Boolean).join(' · ');
|
|
625
|
+
return '<li class="event"><span class="tile-icon">' + icon(image, true) + '</span><div><strong>' +
|
|
626
|
+
esc(event.label || nice(event.type) || 'Activity') + '</strong>' +
|
|
627
|
+
(meta ? '<p class="event-meta small muted">' + esc(meta) + '</p>' : '') + '</div>' + dateMarkup(event.timestamp) + '</li>';
|
|
628
|
+
}).join('') + '</ul>';
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
function renderOverview(data) {
|
|
632
|
+
const sample = data.truncated ? '<p class="status small muted notice">Project and agent counts reflect the latest ' +
|
|
633
|
+
number(data.inventoryLimit) + ' stored memories.</p>' : '';
|
|
634
|
+
const pending = data.storageHealth?.status === 'pending' ? '<p class="status small muted notice">The memory store has pending writes.</p>' : '';
|
|
635
|
+
const projectNote = state.projects.length > 8 ? 'Top projects by saved memory count' : 'Saved memories by project';
|
|
636
|
+
$('overview-body').innerHTML = '<div id="overview-refresh"></div>' + pending + sample + renderSummary(data.totals || {}) +
|
|
637
|
+
'<div class="split overview-columns"><div class="stack"><div class="panel"><div class="panel-head"><div><h2>Project inventory</h2>' +
|
|
638
|
+
'<p class="muted small">' + projectNote + '</p></div><a class="button quiet" href="#memories">Browse ' + icon('chevron', true) +
|
|
639
|
+
'</a></div>' + renderProjects(state.projects) + '</div>' + renderOverviewMemories(list(data.recentMemories)) +
|
|
640
|
+
'</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>';
|
|
641
|
+
renderActivity();
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
function syncProjectOptions() {
|
|
645
|
+
const contextProject = $('context-project').value;
|
|
646
|
+
const graphProject = state.view === 'graph' ? routeParams().get('project') : null;
|
|
647
|
+
optionSet('context-project', 'Select a project', state.projects, contextProject || routeParams().get('project'));
|
|
648
|
+
optionSet('graph-project', 'All projects', state.projects, $('graph-project').value || graphProject);
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
async function loadOverview(force = false) {
|
|
652
|
+
if (state.overviewPending) return state.overviewPending;
|
|
653
|
+
if (state.overviewLoaded && !force) return;
|
|
654
|
+
const target = $('overview-body');
|
|
655
|
+
if (!state.overviewLoaded) target.innerHTML = '<div class="panel">' + skeleton('Loading workspace…') + '</div>';
|
|
656
|
+
setBusy(target, true);
|
|
657
|
+
state.overviewPending = (async () => {
|
|
658
|
+
try {
|
|
659
|
+
const data = await api('overview');
|
|
660
|
+
state.overviewLoaded = true;
|
|
661
|
+
state.projects = list(data.projects);
|
|
662
|
+
state.activity = list(data.activity);
|
|
663
|
+
if (typeof data.features?.liveActivity === 'boolean') state.activityEnabled = data.features.liveActivity;
|
|
664
|
+
if (data.version) $('rail-version').textContent = 'Version ' + data.version;
|
|
665
|
+
syncProjectOptions();
|
|
666
|
+
renderOverview(data);
|
|
667
|
+
renderContextControls();
|
|
668
|
+
} catch (error) {
|
|
669
|
+
if (!state.overviewLoaded) target.innerHTML = errorMarkup(error.message, 'overview');
|
|
670
|
+
else $('overview-refresh').innerHTML = '<div class="notice">' + errorMarkup('Could not refresh the workspace. Showing the last loaded overview.', 'overview') + '</div>';
|
|
671
|
+
renderContextControls();
|
|
672
|
+
} finally {
|
|
673
|
+
setBusy(target, false);
|
|
674
|
+
state.overviewPending = null;
|
|
675
|
+
}
|
|
676
|
+
})();
|
|
677
|
+
return state.overviewPending;
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
async function refreshActivity() {
|
|
681
|
+
if (document.hidden || state.view !== 'overview' || state.activityEnabled === false || state.activityPending || !state.overviewLoaded) return;
|
|
682
|
+
state.activityPending = true;
|
|
683
|
+
try {
|
|
684
|
+
const data = await api('activity');
|
|
685
|
+
if (state.features?.features?.liveActivity === false) return;
|
|
686
|
+
state.activityEnabled = data.enabled;
|
|
687
|
+
state.activity = list(data.activity);
|
|
688
|
+
renderActivity();
|
|
689
|
+
} catch {
|
|
690
|
+
const status = $('activity-status');
|
|
691
|
+
if (status) status.textContent = 'Refresh unavailable';
|
|
692
|
+
} finally {
|
|
693
|
+
state.activityPending = false;
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
function sourceMarkup(values) {
|
|
698
|
+
const sources = Array.from(new Set(list(values).filter((source) => typeof source === 'string' && source.trim())));
|
|
699
|
+
if (!sources.length) return '<p class="source small">' + icon('source', true) + '<span>No source recorded</span></p>';
|
|
700
|
+
return sources.map((source) => '<p class="source small">' + icon('source', true) + '<span class="mono">' + esc(source) + '</span></p>').join('');
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
function memoryQuery() {
|
|
704
|
+
return {
|
|
705
|
+
q: $('memory-query').value.trim(),
|
|
706
|
+
project: $('memory-project').value,
|
|
707
|
+
category: $('memory-category').value,
|
|
708
|
+
agent: $('memory-agent').value
|
|
709
|
+
};
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
function renderMemories(data) {
|
|
713
|
+
const target = $('memory-results');
|
|
714
|
+
const items = list(data.items);
|
|
715
|
+
const total = Number.isFinite(data.total) ? data.total : items.length;
|
|
716
|
+
const { offset } = state.memory;
|
|
717
|
+
const activeFilters = Object.values(state.memory.query || {}).some(Boolean);
|
|
718
|
+
const subtitle = state.memory.query?.q ? 'Ranked search results' : 'Most recent first';
|
|
719
|
+
const heading = '<div class="panel-head"><div><h2>' + esc(activeFilters ? 'Results' : 'Recent memories') +
|
|
720
|
+
'</h2><p class="small muted">' + subtitle + '</p></div><span class="small muted" role="status">' + countLabel(total, 'memory', 'memories') + '</span></div>' +
|
|
721
|
+
(data.truncated ? '<p class="panel-body small muted">Filters and counts may be limited to ' + number(data.inventoryLimit) + ' memories.</p>' : '');
|
|
722
|
+
if (!items.length) {
|
|
723
|
+
target.innerHTML = heading + (activeFilters
|
|
724
|
+
? 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>')
|
|
725
|
+
: blank('memory', 'No memories yet', 'Saved memories appear here after an agent stores a lesson or observation.'));
|
|
726
|
+
return;
|
|
727
|
+
}
|
|
728
|
+
target.innerHTML = heading + '<ul class="list">' + items.map((item) => {
|
|
729
|
+
const title = item.title || nice(item.category) || 'Memory';
|
|
730
|
+
const reasons = list(item.reasons);
|
|
731
|
+
const detail = reasons.length ? '<details><summary>Why this matched</summary><ul class="small muted">' +
|
|
732
|
+
reasons.map((reason) => '<li>' + esc(reason) + '</li>').join('') + '</ul></details>' : '';
|
|
733
|
+
const snippet = compact(item.snippet) && compact(item.snippet) !== compact(title) ? '<p>' + esc(item.snippet) + '</p>' : '';
|
|
734
|
+
return '<li class="memory-row"><div class="memory-head"><h3>' + esc(title) + '</h3>' + dateMarkup(item.updatedAt, true) + '</div>' +
|
|
735
|
+
tags([['project', item.projectName || item.project], ['agent', item.agent], ['memory', nice(item.category)]]) + snippet +
|
|
736
|
+
sourceMarkup([...list(item.sources), item.source]) + detail + '</li>';
|
|
737
|
+
}).join('') + '</ul>' + (offset || total > items.length ? '<div class="list-footer"><span class="muted small">' +
|
|
738
|
+
number(offset + 1) + '–' + number(offset + items.length) + ' of ' + number(total) + '</span><div class="form-actions">' +
|
|
739
|
+
'<button type="button" class="button" data-memory-page="prev"' + (offset ? '' : ' disabled') + '>' + icon('left', true) +
|
|
740
|
+
'Previous</button><button type="button" class="button" data-memory-page="next"' + (offset + items.length < total ? '' : ' disabled') +
|
|
741
|
+
'>Next' + icon('chevron', true) + '</button></div></div>' : '');
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
async function loadMemories(query, offset = 0) {
|
|
745
|
+
const target = $('memory-results');
|
|
746
|
+
const store = state.memory;
|
|
747
|
+
const seq = ++store.request;
|
|
748
|
+
store.controller?.abort();
|
|
749
|
+
const controller = new AbortController();
|
|
750
|
+
store.controller = controller;
|
|
751
|
+
store.query = query;
|
|
752
|
+
store.offset = offset;
|
|
753
|
+
setBusy(target, true);
|
|
754
|
+
target.innerHTML = skeleton('Loading memories…');
|
|
755
|
+
try {
|
|
756
|
+
const data = await api(queryPath('memories', { ...query, limit: store.limit, offset }), {
|
|
757
|
+
signal: AbortSignal.any([controller.signal, AbortSignal.timeout(12000)])
|
|
758
|
+
});
|
|
759
|
+
if (seq !== store.request) return;
|
|
760
|
+
const facets = data.facets || {};
|
|
761
|
+
const toOptions = (values) => list(values).map((value) => ({ id: value, name: nice(value) }));
|
|
762
|
+
const projects = list(facets.projects);
|
|
763
|
+
optionSet('memory-project', 'All projects', projects.length ? projects : state.projects, query.project);
|
|
764
|
+
optionSet('memory-category', 'All categories', toOptions(facets.categories), query.category);
|
|
765
|
+
optionSet('memory-agent', 'All agents', toOptions(facets.agents), query.agent);
|
|
766
|
+
renderMemories(data);
|
|
767
|
+
} catch (error) {
|
|
768
|
+
if (seq === store.request && error.name !== 'AbortError') target.innerHTML = errorMarkup(error.message, 'memories');
|
|
769
|
+
} finally {
|
|
770
|
+
if (seq === store.request) setBusy(target, false);
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
const graphKinds = { project: 'Project', agent: 'Agent', memory: 'Memory' };
|
|
775
|
+
const graphEdgeLabels = { 'belongs-to': 'Belongs to project', 'recorded-by': 'Recorded by agent' };
|
|
776
|
+
const graphEdgeClass = { 'belongs-to': ' project-edge', 'recorded-by': ' agent-edge' };
|
|
777
|
+
|
|
778
|
+
function graphNeighbors(edges, id) {
|
|
779
|
+
return edges.filter((edge) => edge.source === id || edge.target === id).map((edge) => ({
|
|
780
|
+
id: edge.source === id ? edge.target : edge.source,
|
|
781
|
+
edge
|
|
782
|
+
}));
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
function graphPillGrid(positions, nodes, width, top, pitch, nodeWidth = pitch - 18) {
|
|
786
|
+
const columns = Math.max(1, Math.floor((width - 28) / pitch));
|
|
787
|
+
for (let index = 0; index < nodes.length; index += columns) {
|
|
788
|
+
const row = nodes.slice(index, index + columns);
|
|
789
|
+
const offset = (width - row.length * pitch) / 2 + (pitch - nodeWidth) / 2;
|
|
790
|
+
row.forEach((node, column) => {
|
|
791
|
+
positions.set(node.id, { x: offset + column * pitch, y: top + Math.floor(index / columns) * 45, width: nodeWidth, height: 33 });
|
|
792
|
+
});
|
|
793
|
+
}
|
|
794
|
+
return Math.ceil(nodes.length / columns) * 45;
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
function projectCluster(hub, memories) {
|
|
798
|
+
const rings = [];
|
|
799
|
+
for (let start = 0, ring = 0; start < memories.length; ring++) {
|
|
800
|
+
const capacity = 13 + ring * 8;
|
|
801
|
+
const members = memories.slice(start, start + capacity);
|
|
802
|
+
rings.push({ members, rx: 86 + ring * 32, ry: 61 + ring * 29 });
|
|
803
|
+
start += members.length;
|
|
804
|
+
}
|
|
805
|
+
const outer = rings.at(-1);
|
|
806
|
+
return { hub, rings, count: memories.length, rx: (outer?.rx || 75) + 17, ry: (outer?.ry || 36) + 17 };
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
function packClusters(clusters, viewport, agentBands) {
|
|
810
|
+
let best;
|
|
811
|
+
for (let columns = 1; columns <= Math.min(clusters.length, 8); columns++) {
|
|
812
|
+
const rows = [];
|
|
813
|
+
for (let index = 0; index < clusters.length; index += columns) rows.push(clusters.slice(index, index + columns));
|
|
814
|
+
const rowWidths = rows.map((row) => row.reduce((sum, cluster) => sum + cluster.rx * 2 + 19, 0));
|
|
815
|
+
const rowHeights = rows.map((row) => Math.max(...row.map((cluster) => cluster.ry)) * 2 + 20);
|
|
816
|
+
const width = Math.max(agentBands.width, 36 + Math.max(...rowWidths));
|
|
817
|
+
const height = 46 + agentBands.height + rowHeights.reduce((sum, value) => sum + value, 0);
|
|
818
|
+
const scale = Math.min(viewport.width / width, (viewport.height - 45) / height);
|
|
819
|
+
if (!best || scale > best.scale) best = { rows, rowWidths, rowHeights, width, height, scale };
|
|
820
|
+
}
|
|
821
|
+
return best;
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
function graphLayout(nodes, edges) {
|
|
825
|
+
const positions = new Map();
|
|
826
|
+
const clusters = [];
|
|
827
|
+
const order = (a, b) => String(a.label || '').localeCompare(String(b.label || '')) || String(a.id).localeCompare(String(b.id));
|
|
828
|
+
const projects = nodes.filter((node) => node.kind === 'project').sort(order);
|
|
829
|
+
const agents = nodes.filter((node) => node.kind === 'agent').sort(order);
|
|
830
|
+
const memories = nodes.filter((node) => node.kind === 'memory').sort(order);
|
|
831
|
+
const byId = new Map(memories.map((node) => [node.id, node]));
|
|
832
|
+
const expanded = state.graph.project && projects.length === 1 && memories.length <= 25;
|
|
833
|
+
if (!projects.length || !memories.length || expanded) {
|
|
834
|
+
const width = Math.max(450, Math.min(5, Math.ceil(Math.sqrt(memories.length * 1.1))) * 184 + 30);
|
|
835
|
+
let height = 28;
|
|
836
|
+
height += graphPillGrid(positions, projects, width, height, 178, 155);
|
|
837
|
+
if (expanded) height += 14;
|
|
838
|
+
height += graphPillGrid(positions, memories, width, height, 184);
|
|
839
|
+
if (expanded) height += 14;
|
|
840
|
+
height += graphPillGrid(positions, agents, width, height, 140, 118);
|
|
841
|
+
return { positions, clusters, width, height: height + 18 };
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
const groupMap = new Map(projects.map((node) => [node.id, []]));
|
|
845
|
+
const grouped = new Set();
|
|
846
|
+
for (const edge of edges) {
|
|
847
|
+
if (edge.kind !== 'belongs-to') continue;
|
|
848
|
+
const memory = byId.get(edge.target);
|
|
849
|
+
const group = groupMap.get(edge.source);
|
|
850
|
+
if (!memory || !group || grouped.has(memory.id)) continue;
|
|
851
|
+
group.push(memory);
|
|
852
|
+
grouped.add(memory.id);
|
|
853
|
+
}
|
|
854
|
+
const groups = projects.map((node) => projectCluster(node, groupMap.get(node.id).sort(order)));
|
|
855
|
+
const ungrouped = memories.filter((node) => !grouped.has(node.id));
|
|
856
|
+
const split = Math.ceil(agents.length / 2);
|
|
857
|
+
const topAgents = agents.slice(0, split);
|
|
858
|
+
const bottomAgents = agents.slice(split);
|
|
859
|
+
const agentWidth = Math.min(split, 6) * 132 + 28;
|
|
860
|
+
const agentHeight = (Math.ceil(split / 6) + Math.ceil(bottomAgents.length / 6)) * 45;
|
|
861
|
+
const packed = packClusters(groups, graphViewport(), { width: agentWidth, height: agentHeight });
|
|
862
|
+
const { width, rows, rowWidths, rowHeights } = packed;
|
|
863
|
+
let y = 14 + graphPillGrid(positions, topAgents, width, 14, 132, 112) + 18;
|
|
864
|
+
rows.forEach((row, rowIndex) => {
|
|
865
|
+
let x = (width - rowWidths[rowIndex]) / 2 + 9.5;
|
|
866
|
+
for (const group of row) {
|
|
867
|
+
const cx = x + group.rx;
|
|
868
|
+
const cy = y + rowHeights[rowIndex] / 2;
|
|
869
|
+
clusters.push({ cx, cy, rx: group.rx, ry: group.ry });
|
|
870
|
+
positions.set(group.hub.id, { x: cx - 66, y: cy - 21, width: 132, height: 42, count: group.count });
|
|
871
|
+
group.rings.forEach(({ members, rx, ry }, ringIndex) => {
|
|
872
|
+
members.forEach((node, index) => {
|
|
873
|
+
const angle = (index + (ringIndex % 2 ? .5 : 0)) * Math.PI * 2 / members.length - Math.PI / 2;
|
|
874
|
+
positions.set(node.id, { x: cx + Math.cos(angle) * rx - 11, y: cy + Math.sin(angle) * ry - 11, width: 22, height: 22, dot: true });
|
|
875
|
+
});
|
|
876
|
+
});
|
|
877
|
+
x += group.rx * 2 + 19;
|
|
878
|
+
}
|
|
879
|
+
y += rowHeights[rowIndex];
|
|
880
|
+
});
|
|
881
|
+
y += graphPillGrid(positions, ungrouped, width, y + 3, 184);
|
|
882
|
+
y += graphPillGrid(positions, bottomAgents, width, y + 8, 132, 112);
|
|
883
|
+
return { positions, clusters, width, height: y + 16 };
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
function graphEdgePath(edge, layout) {
|
|
887
|
+
const a = layout.positions.get(edge.source);
|
|
888
|
+
const b = layout.positions.get(edge.target);
|
|
889
|
+
if (!a || !b) return '';
|
|
890
|
+
const ax = a.x + a.width / 2;
|
|
891
|
+
const ay = a.y + a.height / 2;
|
|
892
|
+
const bx = b.x + b.width / 2;
|
|
893
|
+
const by = b.y + b.height / 2;
|
|
894
|
+
const start = 'M' + ax + ' ' + ay;
|
|
895
|
+
if (edge.kind === 'belongs-to') return start + ' L' + bx + ' ' + by;
|
|
896
|
+
const bend = (ay + by) / 2;
|
|
897
|
+
return start + ' C' + ax + ' ' + bend + ' ' + bx + ' ' + bend + ' ' + bx + ' ' + by;
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
function graphNodeMarkup(node, position, tabIndex) {
|
|
901
|
+
const { x, y, width, height } = position;
|
|
902
|
+
const label = compact(node.label || node.id);
|
|
903
|
+
const hasCount = Number.isFinite(position.count);
|
|
904
|
+
const mark = node.kind === 'project' ? '<rect class="node-glyph" x="11" y="' + (hasCount ? 10 : 12) + '" width="8" height="8" rx="1"/>'
|
|
905
|
+
: 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"/>';
|
|
906
|
+
const display = shorten(label, Math.floor((width - 30) / 6.1));
|
|
907
|
+
const shape = position.dot
|
|
908
|
+
? '<circle class="node-surface" cx="11" cy="11" r="9.5"/><path class="node-glyph" d="m11 7 4 4-4 4-4-4z"/>'
|
|
909
|
+
: '<rect class="node-surface" width="' + width + '" height="' + height + '" rx="6"/>' + mark +
|
|
910
|
+
'<text x="26" y="' + (hasCount ? 18 : 21) + '">' + esc(display) + '</text>' +
|
|
911
|
+
(hasCount ? '<text class="node-count" x="26" y="33">' + countLabel(position.count, 'memory', 'memories') + '</text>' : '');
|
|
912
|
+
return '<g class="graph-node ' + node.kind + (position.dot ? ' dot-node' : '') + '" role="button" aria-label="' + esc(graphKinds[node.kind] + ': ' + label) +
|
|
913
|
+
'" data-node="' + esc(node.id) + '" aria-pressed="false" tabindex="' + tabIndex + '" transform="translate(' + x + ' ' + y + ')"><title>' +
|
|
914
|
+
esc(label) + '</title>' + shape + '</g>';
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
function renderGraphList() {
|
|
918
|
+
$('graph-list').innerHTML = '<ul class="list">' + state.graph.nodes.map((node) => '<li class="graph-row"><button type="button" class="button" data-node="' +
|
|
919
|
+
esc(node.id) + '" aria-pressed="false">' + icon(node.kind, true) + '<span>' + esc(node.label || node.id) +
|
|
920
|
+
'</span><span class="tag ' + node.kind + '">' + graphKinds[node.kind] + '</span></button></li>').join('') + '</ul>';
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
function renderGraphMap() {
|
|
924
|
+
const graph = state.graph;
|
|
925
|
+
const layout = graph.layout = graphLayout(graph.nodes, graph.edges);
|
|
926
|
+
const svg = $('graph-svg');
|
|
927
|
+
const clusters = layout.clusters.map(({ cx, cy, rx, ry }) => '<ellipse class="cluster-area" cx="' + cx + '" cy="' + cy +
|
|
928
|
+
'" rx="' + rx + '" ry="' + ry + '"/>').join('');
|
|
929
|
+
const edges = graph.edges.map((edge, i) => '<path class="graph-edge' + (graphEdgeClass[edge.kind] || '') +
|
|
930
|
+
'" data-edge="' + i + '" d="' + graphEdgePath(edge, layout) + '"/>').join('');
|
|
931
|
+
const nodes = graph.nodes.map((node, i) => graphNodeMarkup(node, layout.positions.get(node.id), i === 0 ? 0 : -1)).join('');
|
|
932
|
+
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></defs>' +
|
|
933
|
+
'<rect width="100%" height="100%" fill="url(#map-dots)"/><g id="graph-transform"><g aria-hidden="true">' + clusters + edges + '</g>' +
|
|
934
|
+
nodes + '<g class="graph-tooltip" id="graph-tooltip" aria-hidden="true"></g></g>';
|
|
935
|
+
fitGraph();
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
function graphViewport() {
|
|
939
|
+
const rect = $('graph-svg').getBoundingClientRect();
|
|
940
|
+
return { width: Math.max(rect.width, 1), height: Math.max(rect.height, 1) };
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
function updateGraphTransform() {
|
|
944
|
+
const graph = state.graph;
|
|
945
|
+
const transform = $('graph-transform');
|
|
946
|
+
if (!transform || graph.mode !== 'map') return;
|
|
947
|
+
const { width, height } = graphViewport();
|
|
948
|
+
$('graph-svg').setAttribute('viewBox', '0 0 ' + width + ' ' + height);
|
|
949
|
+
transform.setAttribute('transform', 'translate(' + graph.panX + ' ' + graph.panY + ') scale(' + graph.zoom + ')');
|
|
950
|
+
$('graph-scale').textContent = Math.round(graph.zoom * 100) + '%';
|
|
951
|
+
$('graph-out').disabled = graph.zoom <= .06;
|
|
952
|
+
$('graph-in').disabled = graph.zoom >= 2.75;
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
function fitGraph() {
|
|
956
|
+
const graph = state.graph;
|
|
957
|
+
if (!graph.layout || graph.mode !== 'map' || $('graph-body').hidden || state.view !== 'graph') return;
|
|
958
|
+
const { width, height } = graphViewport();
|
|
959
|
+
const { width: mapWidth, height: mapHeight } = graph.layout;
|
|
960
|
+
const widthScale = Math.max(1, width - 24) / mapWidth;
|
|
961
|
+
const heightScale = Math.max(1, height - 54) / mapHeight;
|
|
962
|
+
const zoom = Math.min(1.35, widthScale, heightScale);
|
|
963
|
+
graph.zoom = zoom;
|
|
964
|
+
graph.panX = (width - mapWidth * zoom) / 2;
|
|
965
|
+
graph.panY = Math.max(0, (height - 38 - mapHeight * zoom) / 2);
|
|
966
|
+
updateGraphTransform();
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
function zoomGraph(factor, anchor) {
|
|
970
|
+
const graph = state.graph;
|
|
971
|
+
if (!graph.layout) return;
|
|
972
|
+
const viewport = graphViewport();
|
|
973
|
+
const center = anchor || { x: viewport.width / 2, y: viewport.height / 2 };
|
|
974
|
+
const previous = graph.zoom;
|
|
975
|
+
const target = Math.min(2.75, Math.max(Math.min(.06, previous), previous * factor));
|
|
976
|
+
const ratio = target / previous;
|
|
977
|
+
graph.panX = center.x - (center.x - graph.panX) * ratio;
|
|
978
|
+
graph.panY = center.y - (center.y - graph.panY) * ratio;
|
|
979
|
+
graph.zoom = target;
|
|
980
|
+
updateGraphTransform();
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
function centerGraphNode(id) {
|
|
984
|
+
const graph = state.graph;
|
|
985
|
+
const position = graph.layout?.positions.get(id);
|
|
986
|
+
if (!position || graph.mode !== 'map') return;
|
|
987
|
+
const { width, height } = graphViewport();
|
|
988
|
+
const zoom = Math.max(.8, graph.zoom);
|
|
989
|
+
graph.zoom = zoom;
|
|
990
|
+
graph.panX = width / 2 - (position.x + position.width / 2) * zoom;
|
|
991
|
+
graph.panY = (height - 38) / 2 - (position.y + position.height / 2) * zoom;
|
|
992
|
+
updateGraphTransform();
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
function renderGraphTooltip(id) {
|
|
996
|
+
const graph = state.graph;
|
|
997
|
+
const tooltip = $('graph-tooltip');
|
|
998
|
+
if (!tooltip) return;
|
|
999
|
+
const point = graph.layout.positions.get(id);
|
|
1000
|
+
const node = graph.nodes.find((item) => item.id === id);
|
|
1001
|
+
if (!point?.dot || !node) {
|
|
1002
|
+
tooltip.replaceChildren();
|
|
1003
|
+
return;
|
|
1004
|
+
}
|
|
1005
|
+
const label = shorten(compact(node.label || node.id), 48);
|
|
1006
|
+
const width = Math.max(70, label.length * 6.7 + 18);
|
|
1007
|
+
const x = Math.min(graph.layout.width - width - 2, Math.max(2, point.x + 11 - width / 2));
|
|
1008
|
+
const y = Math.max(1, point.y - 32);
|
|
1009
|
+
tooltip.setAttribute('transform', 'translate(' + x + ' ' + y + ')');
|
|
1010
|
+
tooltip.innerHTML = '<rect width="' + width + '" height="26" rx="5"/><text x="9" y="17">' + esc(label) + '</text>';
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
function renderGraphInspector() {
|
|
1014
|
+
const graph = state.graph;
|
|
1015
|
+
const all = new Map(list(graph.snapshot?.nodes).map((node) => [node.id, node]));
|
|
1016
|
+
const selected = all.get(graph.selected);
|
|
1017
|
+
const inspector = $('graph-inspector');
|
|
1018
|
+
if (!selected) {
|
|
1019
|
+
inspector.innerHTML = '<div class="panel-head"><h3>Node details</h3></div>' +
|
|
1020
|
+
blank('graph', 'Select a node', 'See its stored details and follow its recorded connections.');
|
|
1021
|
+
return;
|
|
1022
|
+
}
|
|
1023
|
+
const adjacent = graphNeighbors(list(graph.snapshot.edges), selected.id).filter((item) => all.has(item.id));
|
|
1024
|
+
const connected = adjacent.length ? '<details open><summary>' + countLabel(adjacent.length, 'connection') + '</summary><div class="related">' +
|
|
1025
|
+
adjacent.map(({ id, edge }) => {
|
|
1026
|
+
const node = all.get(id);
|
|
1027
|
+
return '<button type="button" class="button quiet" data-related="' + esc(id) + '" title="' +
|
|
1028
|
+
esc(graphEdgeLabels[edge.kind] || nice(edge.kind) || 'Connection') + '">' + icon(node.kind, true) + '<span>' +
|
|
1029
|
+
esc(node.label || node.id) + '</span></button>';
|
|
1030
|
+
}).join('') + '</div></details>' : '<p class="muted small">No connections in this view.</p>';
|
|
1031
|
+
const project = state.projects.find((item) => item.id === selected.project)?.name || selected.project;
|
|
1032
|
+
const snippet = compact(selected.snippet) && compact(selected.snippet) !== compact(selected.label)
|
|
1033
|
+
? '<p class="small">' + esc(selected.snippet) + '</p>' : '';
|
|
1034
|
+
inspector.innerHTML = '<div class="panel-head"><h3>Node details</h3></div><div class="panel-body">' +
|
|
1035
|
+
tags([[selected.kind, graphKinds[selected.kind]], ['memory', nice(selected.category)]]) + '<h3>' + esc(selected.label || selected.id) + '</h3>' +
|
|
1036
|
+
(project && selected.kind !== 'project' ? '<p class="muted small">' + esc(project) + '</p>' : '') + snippet +
|
|
1037
|
+
(selected.kind === 'memory' ? sourceMarkup([...list(selected.sources), selected.source]) : '') + connected + '</div>';
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
function selectGraphNode(id, { center = false, focus = false } = {}) {
|
|
1041
|
+
const graph = state.graph;
|
|
1042
|
+
graph.selected = id;
|
|
1043
|
+
const connected = new Set(graphNeighbors(graph.edges, id).map((item) => item.id));
|
|
1044
|
+
connected.add(id);
|
|
1045
|
+
const focusable = $('graph-svg').querySelector('[data-node][tabindex="0"]')?.dataset.node;
|
|
1046
|
+
const tabStop = id || focusable || graph.nodes[0]?.id;
|
|
1047
|
+
$('graph-body').querySelectorAll('[data-node]').forEach((element) => {
|
|
1048
|
+
const nodeId = element.dataset.node;
|
|
1049
|
+
element.setAttribute('aria-pressed', String(id === nodeId));
|
|
1050
|
+
if (element instanceof SVGElement) {
|
|
1051
|
+
element.classList.toggle('unrelated', !!id && !connected.has(nodeId));
|
|
1052
|
+
element.setAttribute('tabindex', nodeId === tabStop ? '0' : '-1');
|
|
1053
|
+
}
|
|
1054
|
+
});
|
|
1055
|
+
if (graph.mode === 'map') {
|
|
1056
|
+
$('graph-svg').querySelectorAll('[data-edge]').forEach((element) => {
|
|
1057
|
+
const edge = graph.edges[Number(element.dataset.edge)];
|
|
1058
|
+
const active = edge.source === id || edge.target === id;
|
|
1059
|
+
element.classList.toggle('active', active);
|
|
1060
|
+
element.classList.toggle('unrelated', !!id && !active);
|
|
1061
|
+
});
|
|
1062
|
+
renderGraphTooltip(id);
|
|
1063
|
+
}
|
|
1064
|
+
renderGraphInspector();
|
|
1065
|
+
if (center) centerGraphNode(id);
|
|
1066
|
+
if (focus) {
|
|
1067
|
+
const target = graph.mode === 'map' ? $('graph-svg') : $('graph-list');
|
|
1068
|
+
target.querySelector('[data-node="' + CSS.escape(id) + '"]')?.focus();
|
|
1069
|
+
}
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
function renderGraph() {
|
|
1073
|
+
const graph = state.graph;
|
|
1074
|
+
if (!graph.snapshot) return;
|
|
1075
|
+
const search = $('graph-query').value.trim().toLowerCase();
|
|
1076
|
+
const kind = $('graph-kind').value;
|
|
1077
|
+
const nodes = list(graph.snapshot.nodes).filter((node) => Object.hasOwn(graphKinds, node.kind));
|
|
1078
|
+
const matches = nodes.filter((node) => (!kind || node.kind === kind) &&
|
|
1079
|
+
(!search || [node.label, node.snippet, node.project, node.category].filter(Boolean).join(' ').toLowerCase().includes(search)));
|
|
1080
|
+
const ids = new Set(matches.map((node) => node.id));
|
|
1081
|
+
if (search && !kind) {
|
|
1082
|
+
matches.forEach((node) => graphNeighbors(list(graph.snapshot.edges), node.id).forEach((item) => ids.add(item.id)));
|
|
1083
|
+
}
|
|
1084
|
+
graph.nodes = nodes.filter((node) => ids.has(node.id));
|
|
1085
|
+
graph.edges = list(graph.snapshot.edges).filter((edge) => ids.has(edge.source) && ids.has(edge.target));
|
|
1086
|
+
if (graph.selected && !ids.has(graph.selected)) graph.selected = '';
|
|
1087
|
+
const matchingCount = search && matches.length !== graph.nodes.length ? countLabel(matches.length, 'match', 'matches') + ' · ' : '';
|
|
1088
|
+
$('graph-count').textContent = matchingCount + countLabel(graph.nodes.length, 'node') + ' · ' + countLabel(graph.edges.length, 'connection');
|
|
1089
|
+
$('graph-body').hidden = !graph.nodes.length;
|
|
1090
|
+
$('graph-legend').hidden = !graph.nodes.length;
|
|
1091
|
+
const note = graph.snapshot.truncated ? '<div class="panel-body small muted">Showing ' +
|
|
1092
|
+
number(graph.snapshot.shownMemories) + ' memories in this snapshot. Filter by project to explore a narrower set.</div>' : '';
|
|
1093
|
+
$('graph-status').innerHTML = graph.nodes.length ? note : blank('graph', search || kind ? 'No matching nodes' : 'No relationships yet',
|
|
1094
|
+
search || kind ? 'Change the name or node type filter.' : 'The graph fills in as project and agent memories are stored.');
|
|
1095
|
+
if (!graph.nodes.length) return;
|
|
1096
|
+
$('graph-stage').hidden = graph.mode !== 'map';
|
|
1097
|
+
$('graph-list').hidden = graph.mode !== 'list';
|
|
1098
|
+
$('graph-mode-map').setAttribute('aria-pressed', String(graph.mode === 'map'));
|
|
1099
|
+
$('graph-mode-list').setAttribute('aria-pressed', String(graph.mode === 'list'));
|
|
1100
|
+
renderGraphList();
|
|
1101
|
+
if (graph.mode === 'map') renderGraphMap();
|
|
1102
|
+
selectGraphNode(graph.selected);
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1105
|
+
async function loadGraph(project, force = false) {
|
|
1106
|
+
const graph = state.graph;
|
|
1107
|
+
if (!force && graph.snapshot && graph.project === project) {
|
|
1108
|
+
renderGraph();
|
|
1109
|
+
return;
|
|
1110
|
+
}
|
|
1111
|
+
const seq = ++graph.request;
|
|
1112
|
+
graph.controller?.abort();
|
|
1113
|
+
const controller = new AbortController();
|
|
1114
|
+
graph.controller = controller;
|
|
1115
|
+
graph.snapshot = null;
|
|
1116
|
+
graph.project = project;
|
|
1117
|
+
graph.selected = '';
|
|
1118
|
+
setBusy($('graph-panel'), true);
|
|
1119
|
+
$('graph-status').innerHTML = skeleton('Loading relationships…');
|
|
1120
|
+
$('graph-count').textContent = '';
|
|
1121
|
+
$('graph-body').hidden = true;
|
|
1122
|
+
$('graph-legend').hidden = true;
|
|
1123
|
+
try {
|
|
1124
|
+
const snapshot = await api(queryPath('graph', { project }), { signal: AbortSignal.any([controller.signal, AbortSignal.timeout(12000)]) });
|
|
1125
|
+
if (seq !== graph.request) return;
|
|
1126
|
+
graph.snapshot = snapshot;
|
|
1127
|
+
renderGraph();
|
|
1128
|
+
} catch (error) {
|
|
1129
|
+
if (seq === graph.request && error.name !== 'AbortError') $('graph-status').innerHTML = errorMarkup(error.message, 'graph');
|
|
1130
|
+
} finally {
|
|
1131
|
+
if (seq === graph.request) setBusy($('graph-panel'), false);
|
|
1132
|
+
}
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1135
|
+
function renderContextControls() {
|
|
1136
|
+
const input = $('context-cross');
|
|
1137
|
+
const toggle = state.features?.features;
|
|
1138
|
+
const canCross = toggle?.contextAssembly === true && toggle?.crossProjectContext === true;
|
|
1139
|
+
$('context-cross-row').hidden = !canCross;
|
|
1140
|
+
if (!canCross) input.checked = false;
|
|
1141
|
+
$('context-submit').disabled = !toggle?.contextAssembly || !state.projects.length || state.context.pending;
|
|
1142
|
+
const gate = $('context-gate');
|
|
1143
|
+
if (!toggle && state.featurePending) {
|
|
1144
|
+
gate.innerHTML = '<p class="muted small" role="status">Checking feature settings…</p>';
|
|
1145
|
+
} else if (!toggle) {
|
|
1146
|
+
gate.innerHTML = errorMarkup('Could not check context assembly settings.', 'features');
|
|
1147
|
+
} else if (!toggle.contextAssembly) {
|
|
1148
|
+
gate.innerHTML = '<p class="muted small">Context assembly is turned off. <a href="#features">Manage in Features</a></p>';
|
|
1149
|
+
} else if (!state.overviewLoaded && state.overviewPending) {
|
|
1150
|
+
gate.innerHTML = '<p class="muted small" role="status">Loading projects…</p>';
|
|
1151
|
+
} else if (!state.overviewLoaded) {
|
|
1152
|
+
gate.innerHTML = errorMarkup('Could not load projects.', 'overview');
|
|
1153
|
+
} else if (!state.projects.length) {
|
|
1154
|
+
gate.innerHTML = '<p class="muted small">Store a project memory before assembling context.</p>';
|
|
1155
|
+
} else {
|
|
1156
|
+
gate.replaceChildren();
|
|
1157
|
+
}
|
|
1158
|
+
}
|
|
1159
|
+
|
|
1160
|
+
function contextReasonList(items, format) {
|
|
1161
|
+
return '<ul class="list">' + items.map(format).join('') + '</ul>';
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1164
|
+
function renderContext() {
|
|
1165
|
+
const output = $('context-output');
|
|
1166
|
+
const { pack, dirty } = state.context;
|
|
1167
|
+
if (!pack) {
|
|
1168
|
+
output.innerHTML = blank('context', 'Ready for a task', 'Choose a project and describe the work to see the context and its sources here.');
|
|
1169
|
+
return;
|
|
1170
|
+
}
|
|
1171
|
+
const entries = list(pack.entries);
|
|
1172
|
+
const excluded = list(pack.excluded);
|
|
1173
|
+
const budget = pack.budget || {};
|
|
1174
|
+
const warnings = list(pack.warnings);
|
|
1175
|
+
const allowed = state.features?.features?.contextAssembly === true;
|
|
1176
|
+
const canCopy = allowed && !dirty && !!pack.markdown && pack.enabled && entries.length > 0;
|
|
1177
|
+
const warningMarkup = warnings.map((warning) => '<p class="small muted">' + esc(warning) + '</p>').join('');
|
|
1178
|
+
const selection = contextReasonList(entries, (entry) => {
|
|
1179
|
+
const title = shorten(compact(entry.excerpt), 120) || nice(entry.category) || entry.id;
|
|
1180
|
+
const reason = list(entry.reasons).map((detail) => '<p class="small muted">' + esc(detail) + '</p>').join('');
|
|
1181
|
+
return '<li><strong>' + esc(title) + '</strong><div class="small muted">' + esc([entry.projectName, entry.agentTool, nice(entry.category)].filter(Boolean).join(' · ')) +
|
|
1182
|
+
'</div>' + reason + sourceMarkup(entry.sources) + '</li>';
|
|
1183
|
+
});
|
|
1184
|
+
const reasonLabels = {
|
|
1185
|
+
duplicate: 'Matches a memory already considered.',
|
|
1186
|
+
budget: 'Would exceed the selected budget.',
|
|
1187
|
+
limit: 'Beyond the brief’s memory limit.',
|
|
1188
|
+
empty: 'Contains no memory text.',
|
|
1189
|
+
lookback: 'Falls outside the selected time range.'
|
|
1190
|
+
};
|
|
1191
|
+
const rejections = contextReasonList(excluded, (entry) => '<li><strong class="mono">' + esc(entry.id) +
|
|
1192
|
+
'</strong><p class="small muted">' + esc(reasonLabels[entry.reason] || nice(entry.reason)) + '</p></li>');
|
|
1193
|
+
const scope = pack.scope === 'cross-project' ? 'Cross-project' : 'Selected project';
|
|
1194
|
+
output.innerHTML = '<div class="panel-body stack">' + (dirty ? '<p class="status small" role="status">Task details changed. Assemble again to update this preview.</p>' : '') +
|
|
1195
|
+
'<div class="form-actions">' + tags([['project', scope], ['memory', number(budget.estimatedTokens) + ' / ' + number(budget.tokenBudget) + ' estimated tokens']]) +
|
|
1196
|
+
(canCopy ? '<button class="button toolbar-end" id="context-copy" type="button">' + icon('copy') + 'Copy brief</button>' : '') + '</div>' +
|
|
1197
|
+
warningMarkup +
|
|
1198
|
+
(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>' :
|
|
1199
|
+
blank('context', pack.enabled ? 'No memories fit this task' : 'Context assembly is turned off', pack.enabled ? 'Try a more specific task or a larger budget.' : 'Enable it in Features to build a brief.')) +
|
|
1200
|
+
'<div class="context-reasons">' +
|
|
1201
|
+
(entries.length ? '<details open><summary>Included · ' + number(entries.length) + '</summary>' + selection + '</details>' : '') +
|
|
1202
|
+
(excluded.length ? '<details><summary>Excluded · ' + number(excluded.length) + '</summary>' + rejections + '</details>' : '') +
|
|
1203
|
+
'</div><p id="copy-status" class="small muted" role="status" hidden></p></div>';
|
|
1204
|
+
}
|
|
1205
|
+
|
|
1206
|
+
async function assembleContext() {
|
|
1207
|
+
const store = state.context;
|
|
1208
|
+
const toggle = state.features?.features;
|
|
1209
|
+
const query = $('context-task').value.trim();
|
|
1210
|
+
const projectId = $('context-project').value;
|
|
1211
|
+
const tokenBudget = Number($('context-budget').value);
|
|
1212
|
+
if (store.pending || !toggle?.contextAssembly || !projectId) return;
|
|
1213
|
+
if (!query) {
|
|
1214
|
+
$('context-task').setCustomValidity('Enter a task.');
|
|
1215
|
+
$('context-task').reportValidity();
|
|
1216
|
+
return;
|
|
1217
|
+
}
|
|
1218
|
+
const body = { query, projectId, tokenBudget, includeCrossProject: toggle.crossProjectContext && $('context-cross').checked };
|
|
1219
|
+
store.pending = true;
|
|
1220
|
+
store.dirty = false;
|
|
1221
|
+
renderContextControls();
|
|
1222
|
+
setBusy($('context-result'), true);
|
|
1223
|
+
$('context-output').innerHTML = skeleton('Assembling context…');
|
|
1224
|
+
try {
|
|
1225
|
+
store.pack = await api('context', { body });
|
|
1226
|
+
renderContext();
|
|
1227
|
+
} catch (error) {
|
|
1228
|
+
store.pack = null;
|
|
1229
|
+
$('context-output').innerHTML = errorMarkup(error.message, 'context');
|
|
1230
|
+
} finally {
|
|
1231
|
+
store.pending = false;
|
|
1232
|
+
setBusy($('context-result'), false);
|
|
1233
|
+
renderContextControls();
|
|
1234
|
+
}
|
|
1235
|
+
}
|
|
1236
|
+
|
|
1237
|
+
async function copyContext() {
|
|
1238
|
+
const store = state.context;
|
|
1239
|
+
const status = $('copy-status');
|
|
1240
|
+
if (!store.pack?.markdown || store.dirty || !status) return;
|
|
1241
|
+
try {
|
|
1242
|
+
await navigator.clipboard.writeText(store.pack.markdown);
|
|
1243
|
+
status.textContent = 'Brief copied.';
|
|
1244
|
+
} catch {
|
|
1245
|
+
status.textContent = 'Clipboard access is unavailable. Select and copy the preview text.';
|
|
1246
|
+
}
|
|
1247
|
+
status.hidden = false;
|
|
1248
|
+
}
|
|
1249
|
+
|
|
1250
|
+
function renderFeatures() {
|
|
1251
|
+
const target = $('feature-list');
|
|
1252
|
+
if (!state.features) return;
|
|
1253
|
+
const current = state.features.features;
|
|
1254
|
+
const overrides = new Map(list(state.features.definitions).map((item) => [item.id, item]));
|
|
1255
|
+
target.innerHTML = '<ul class="list">' + definitions.map((definition) => {
|
|
1256
|
+
const display = overrides.get(definition.id) || definition;
|
|
1257
|
+
const on = current[definition.id] === true;
|
|
1258
|
+
return '<li class="setting"><div><h3 id="label-' + definition.id + '">' + esc(display.label) + '</h3><p class="muted small" id="desc-' +
|
|
1259
|
+
definition.id + '">' + esc(display.description) + '</p></div><button type="button" class="button switch" role="switch" aria-checked="' +
|
|
1260
|
+
on + '" aria-labelledby="label-' + definition.id + '" aria-describedby="desc-' + definition.id + '" data-feature="' + definition.id + '"' +
|
|
1261
|
+
(state.featureSaving ? ' disabled' : '') + '><span aria-hidden="true">' + (on ? 'On' : 'Off') + '</span><span class="switch-track" aria-hidden="true"></span></button></li>';
|
|
1262
|
+
}).join('') + '</ul>';
|
|
1263
|
+
}
|
|
1264
|
+
|
|
1265
|
+
function applyFeatures(snapshot) {
|
|
1266
|
+
const before = state.features?.features;
|
|
1267
|
+
const current = snapshot.features;
|
|
1268
|
+
if (!current || !Number.isSafeInteger(snapshot.revision) || !definitions.every(({ id }) => typeof current[id] === 'boolean')) {
|
|
1269
|
+
throw new Error('The server returned unreadable feature settings.');
|
|
1270
|
+
}
|
|
1271
|
+
if (state.features && snapshot.revision < state.features.revision) return;
|
|
1272
|
+
state.features = snapshot;
|
|
1273
|
+
state.activityEnabled = current.liveActivity;
|
|
1274
|
+
if (before?.crossProjectContext && !current.crossProjectContext && state.context.pack?.scope === 'cross-project') state.context.dirty = true;
|
|
1275
|
+
renderFeatures();
|
|
1276
|
+
renderActivity();
|
|
1277
|
+
renderContextControls();
|
|
1278
|
+
if (state.context.pack && !state.context.pending) renderContext();
|
|
1279
|
+
if (current.liveActivity && before?.liveActivity !== true) void refreshActivity();
|
|
1280
|
+
}
|
|
1281
|
+
|
|
1282
|
+
function featureStatus(message, error = false) {
|
|
1283
|
+
const target = $('feature-status');
|
|
1284
|
+
target.textContent = message;
|
|
1285
|
+
target.dataset.error = String(error);
|
|
1286
|
+
}
|
|
1287
|
+
|
|
1288
|
+
async function loadFeatures(force = false) {
|
|
1289
|
+
if (state.featurePending) return state.featurePending;
|
|
1290
|
+
if (state.features && !force) return;
|
|
1291
|
+
const target = $('feature-list');
|
|
1292
|
+
if (!state.features) target.innerHTML = skeleton('Loading feature settings…');
|
|
1293
|
+
setBusy(target, true);
|
|
1294
|
+
state.featurePending = (async () => {
|
|
1295
|
+
try {
|
|
1296
|
+
applyFeatures(await api('features'));
|
|
1297
|
+
return true;
|
|
1298
|
+
} catch (error) {
|
|
1299
|
+
if (!state.features) target.innerHTML = errorMarkup(error.message, 'features');
|
|
1300
|
+
else if (!state.featureSaving) featureStatus(error.message, true);
|
|
1301
|
+
return false;
|
|
1302
|
+
} finally {
|
|
1303
|
+
state.featurePending = null;
|
|
1304
|
+
setBusy(target, false);
|
|
1305
|
+
renderContextControls();
|
|
1306
|
+
}
|
|
1307
|
+
})();
|
|
1308
|
+
renderContextControls();
|
|
1309
|
+
return state.featurePending;
|
|
1310
|
+
}
|
|
1311
|
+
|
|
1312
|
+
async function toggleFeature(key) {
|
|
1313
|
+
if (state.featureSaving || !state.features || !definitions.some((definition) => definition.id === key)) return;
|
|
1314
|
+
state.featureSaving = true;
|
|
1315
|
+
$('feature-list').querySelectorAll('[data-feature]').forEach((button) => { button.disabled = true; });
|
|
1316
|
+
featureStatus('Saving…');
|
|
1317
|
+
try {
|
|
1318
|
+
const body = { patch: { [key]: !state.features.features[key] }, expectedRevision: state.features.revision };
|
|
1319
|
+
applyFeatures(await api('features', { body }));
|
|
1320
|
+
featureStatus('Saved.');
|
|
1321
|
+
} catch (error) {
|
|
1322
|
+
if (error.status === 409) {
|
|
1323
|
+
const refreshed = await loadFeatures(true);
|
|
1324
|
+
featureStatus(refreshed ? 'Settings changed elsewhere. Review the current settings and try again.' :
|
|
1325
|
+
'Settings changed elsewhere. Reload Features to see the current settings.', true);
|
|
1326
|
+
} else {
|
|
1327
|
+
featureStatus(error.message, true);
|
|
1328
|
+
}
|
|
1329
|
+
} finally {
|
|
1330
|
+
state.featureSaving = false;
|
|
1331
|
+
renderFeatures();
|
|
1332
|
+
if (state.view === 'features') $('feature-list').querySelector('[data-feature="' + key + '"]')?.focus();
|
|
1333
|
+
}
|
|
1334
|
+
}
|
|
1335
|
+
|
|
1336
|
+
function setPreference(key, value, announce = true) {
|
|
1337
|
+
document.documentElement.dataset[key] = value;
|
|
1338
|
+
let persisted = true;
|
|
1339
|
+
if (announce) {
|
|
1340
|
+
try {
|
|
1341
|
+
localStorage.setItem('shadow-brain:workspace:' + key, value);
|
|
1342
|
+
} catch { persisted = false; }
|
|
1343
|
+
const status = $('display-status');
|
|
1344
|
+
status.hidden = false;
|
|
1345
|
+
status.textContent = persisted ? 'Display preference saved on this device.' : 'Display preference applied for this session.';
|
|
1346
|
+
}
|
|
1347
|
+
$('theme-setting').value = document.documentElement.dataset.theme;
|
|
1348
|
+
$('motion-setting').value = document.documentElement.dataset.motion;
|
|
1349
|
+
const dark = document.documentElement.dataset.theme === 'dark';
|
|
1350
|
+
const label = dark ? 'Switch to light theme' : 'Switch to dark theme';
|
|
1351
|
+
const button = $('theme-toggle');
|
|
1352
|
+
button.innerHTML = icon(dark ? 'sun' : 'moon');
|
|
1353
|
+
button.title = label;
|
|
1354
|
+
button.setAttribute('aria-label', label);
|
|
1355
|
+
if (announce && !persisted && state.view !== 'features') {
|
|
1356
|
+
$('workspace-notice').hidden = false;
|
|
1357
|
+
$('workspace-notice').textContent = 'Display preference applied for this session.';
|
|
1358
|
+
}
|
|
1359
|
+
}
|
|
1360
|
+
|
|
1361
|
+
function routeParams() {
|
|
1362
|
+
return new URLSearchParams(location.hash.split('?')[1] || '');
|
|
1363
|
+
}
|
|
1364
|
+
|
|
1365
|
+
function changeView(moveFocus = false) {
|
|
1366
|
+
const hash = location.hash.slice(1).split('?')[0];
|
|
1367
|
+
const view = Object.hasOwn(viewNames, hash) ? hash : 'overview';
|
|
1368
|
+
const previous = state.view;
|
|
1369
|
+
state.view = view;
|
|
1370
|
+
$('crumb').textContent = viewNames[view];
|
|
1371
|
+
document.title = viewNames[view] + ' · Shadow Brain';
|
|
1372
|
+
for (const name of Object.keys(viewNames)) {
|
|
1373
|
+
const section = $('page-' + name);
|
|
1374
|
+
section.hidden = name !== view;
|
|
1375
|
+
section.classList.remove('entered');
|
|
1376
|
+
const link = document.querySelector('[data-view="' + name + '"]');
|
|
1377
|
+
if (name === view) link.setAttribute('aria-current', 'page');
|
|
1378
|
+
else link.removeAttribute('aria-current');
|
|
1379
|
+
}
|
|
1380
|
+
if (previous !== view) $('page-' + view).classList.add('entered');
|
|
1381
|
+
if (moveFocus) {
|
|
1382
|
+
window.scrollTo(0, 0);
|
|
1383
|
+
$('title-' + view).focus({ preventScroll: true });
|
|
1384
|
+
}
|
|
1385
|
+
const params = routeParams();
|
|
1386
|
+
const project = params.get('project') || '';
|
|
1387
|
+
switch (view) {
|
|
1388
|
+
case 'overview':
|
|
1389
|
+
void loadOverview(previous !== '' && previous !== 'overview');
|
|
1390
|
+
void loadFeatures();
|
|
1391
|
+
break;
|
|
1392
|
+
case 'memories':
|
|
1393
|
+
if (project) optionSet('memory-project', 'All projects', state.projects, project);
|
|
1394
|
+
void loadMemories({ ...memoryQuery(), project: project || $('memory-project').value });
|
|
1395
|
+
break;
|
|
1396
|
+
case 'graph':
|
|
1397
|
+
void loadOverview();
|
|
1398
|
+
if (project) optionSet('graph-project', 'All projects', state.projects, project);
|
|
1399
|
+
void loadGraph(project || $('graph-project').value, previous !== '' && previous !== 'graph');
|
|
1400
|
+
break;
|
|
1401
|
+
case 'context':
|
|
1402
|
+
void loadOverview();
|
|
1403
|
+
void loadFeatures(previous !== '');
|
|
1404
|
+
renderContextControls();
|
|
1405
|
+
break;
|
|
1406
|
+
case 'features':
|
|
1407
|
+
featureStatus('');
|
|
1408
|
+
void loadFeatures(true);
|
|
1409
|
+
break;
|
|
1410
|
+
}
|
|
1411
|
+
}
|
|
1412
|
+
|
|
1413
|
+
function graphSelectFromList(id, fromInspector) {
|
|
1414
|
+
if (!state.graph.nodes.some((node) => node.id === id)) {
|
|
1415
|
+
$('graph-query').value = '';
|
|
1416
|
+
$('graph-kind').value = '';
|
|
1417
|
+
renderGraph();
|
|
1418
|
+
}
|
|
1419
|
+
selectGraphNode(id, { center: fromInspector, focus: fromInspector });
|
|
1420
|
+
}
|
|
1421
|
+
|
|
1422
|
+
function handleGraphKeyboard(event) {
|
|
1423
|
+
const target = event.target.closest('[data-node]');
|
|
1424
|
+
if (!target) return;
|
|
1425
|
+
if (event.key === 'Enter' || event.key === ' ') {
|
|
1426
|
+
event.preventDefault();
|
|
1427
|
+
selectGraphNode(target.dataset.node, { center: true });
|
|
1428
|
+
return;
|
|
1429
|
+
}
|
|
1430
|
+
if (!['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight', 'Home', 'End'].includes(event.key)) return;
|
|
1431
|
+
event.preventDefault();
|
|
1432
|
+
const graph = state.graph;
|
|
1433
|
+
const { positions } = graph.layout;
|
|
1434
|
+
const points = [...positions.entries()].map(([id, point]) => ({ id, x: point.x + point.width / 2, y: point.y + point.height / 2 }));
|
|
1435
|
+
let next;
|
|
1436
|
+
if (event.key === 'Home' || event.key === 'End') {
|
|
1437
|
+
points.sort((a, b) => a.y - b.y || a.x - b.x);
|
|
1438
|
+
next = event.key === 'Home' ? points[0] : points.at(-1);
|
|
1439
|
+
} else {
|
|
1440
|
+
const current = points.find((point) => point.id === target.dataset.node);
|
|
1441
|
+
const horizontal = event.key === 'ArrowLeft' || event.key === 'ArrowRight';
|
|
1442
|
+
const sign = event.key === 'ArrowLeft' || event.key === 'ArrowUp' ? -1 : 1;
|
|
1443
|
+
const axis = horizontal ? 'x' : 'y';
|
|
1444
|
+
const other = horizontal ? 'y' : 'x';
|
|
1445
|
+
const score = (point) => Math.abs(point[axis] - current[axis]) + Math.abs(point[other] - current[other]) * 1.4;
|
|
1446
|
+
next = points.filter((point) => (point[axis] - current[axis]) * sign > 3).sort((a, b) => score(a) - score(b))[0];
|
|
1447
|
+
}
|
|
1448
|
+
if (next) selectGraphNode(next.id, { center: true, focus: true });
|
|
1449
|
+
}
|
|
1450
|
+
|
|
1451
|
+
$('theme-toggle').addEventListener('click', () => setPreference('theme', document.documentElement.dataset.theme === 'dark' ? 'light' : 'dark'));
|
|
1452
|
+
$('theme-setting').addEventListener('change', (event) => setPreference('theme', event.target.value));
|
|
1453
|
+
$('motion-setting').addEventListener('change', (event) => setPreference('motion', event.target.value));
|
|
1454
|
+
$('memory-form').addEventListener('submit', (event) => { event.preventDefault(); void loadMemories(memoryQuery()); });
|
|
1455
|
+
['memory-project', 'memory-category', 'memory-agent'].forEach((id) => $(id).addEventListener('change', () => { void loadMemories(memoryQuery()); }));
|
|
1456
|
+
$('graph-query').addEventListener('input', renderGraph);
|
|
1457
|
+
$('graph-kind').addEventListener('change', renderGraph);
|
|
1458
|
+
$('graph-project').addEventListener('change', (event) => { void loadGraph(event.target.value); });
|
|
1459
|
+
['map', 'list'].forEach((mode) => $('graph-mode-' + mode).addEventListener('click', () => {
|
|
1460
|
+
state.graph.mode = mode;
|
|
1461
|
+
$('graph-mode-map').setAttribute('aria-pressed', String(mode === 'map'));
|
|
1462
|
+
$('graph-mode-list').setAttribute('aria-pressed', String(mode === 'list'));
|
|
1463
|
+
renderGraph();
|
|
1464
|
+
}));
|
|
1465
|
+
$('graph-fit').addEventListener('click', () => fitGraph());
|
|
1466
|
+
$('graph-in').addEventListener('click', () => zoomGraph(1.35));
|
|
1467
|
+
$('graph-out').addEventListener('click', () => zoomGraph(1 / 1.35));
|
|
1468
|
+
$('graph-svg').addEventListener('keydown', handleGraphKeyboard);
|
|
1469
|
+
$('graph-svg').addEventListener('focusin', (event) => {
|
|
1470
|
+
const node = event.target.closest('[data-node]');
|
|
1471
|
+
if (node) renderGraphTooltip(node.dataset.node);
|
|
1472
|
+
});
|
|
1473
|
+
$('graph-svg').addEventListener('focusout', () => renderGraphTooltip(state.graph.selected));
|
|
1474
|
+
$('graph-svg').addEventListener('pointerdown', (event) => {
|
|
1475
|
+
if (event.button !== 0 || !event.isPrimary || event.target.closest('[data-node]')) return;
|
|
1476
|
+
const graph = state.graph;
|
|
1477
|
+
graph.drag = { id: event.pointerId, x: event.clientX - graph.panX, y: event.clientY - graph.panY };
|
|
1478
|
+
$('graph-svg').setPointerCapture(event.pointerId);
|
|
1479
|
+
$('graph-svg').classList.add('dragging');
|
|
1480
|
+
});
|
|
1481
|
+
$('graph-svg').addEventListener('pointermove', (event) => {
|
|
1482
|
+
const graph = state.graph;
|
|
1483
|
+
if (graph.drag?.id !== event.pointerId) return;
|
|
1484
|
+
graph.panX = event.clientX - graph.drag.x;
|
|
1485
|
+
graph.panY = event.clientY - graph.drag.y;
|
|
1486
|
+
updateGraphTransform();
|
|
1487
|
+
});
|
|
1488
|
+
$('graph-svg').addEventListener('lostpointercapture', () => {
|
|
1489
|
+
state.graph.drag = null;
|
|
1490
|
+
$('graph-svg').classList.remove('dragging');
|
|
1491
|
+
});
|
|
1492
|
+
window.addEventListener('resize', () => {
|
|
1493
|
+
if (state.view === 'graph' && state.graph.mode === 'map' && state.graph.nodes.length) {
|
|
1494
|
+
renderGraphMap();
|
|
1495
|
+
selectGraphNode(state.graph.selected);
|
|
1496
|
+
}
|
|
1497
|
+
});
|
|
1498
|
+
|
|
1499
|
+
$('context-form').addEventListener('submit', (event) => { event.preventDefault(); void assembleContext(); });
|
|
1500
|
+
$('context-form').addEventListener('input', () => {
|
|
1501
|
+
$('context-task').setCustomValidity('');
|
|
1502
|
+
const store = state.context;
|
|
1503
|
+
if (store.pack || store.pending) {
|
|
1504
|
+
if (!store.dirty) {
|
|
1505
|
+
store.dirty = true;
|
|
1506
|
+
if (!store.pending) renderContext();
|
|
1507
|
+
}
|
|
1508
|
+
}
|
|
1509
|
+
});
|
|
1510
|
+
|
|
1511
|
+
document.addEventListener('click', (event) => {
|
|
1512
|
+
const element = event.target.closest('[data-retry],[data-memory-clear],[data-memory-page],[data-node],[data-related],[data-feature],#context-copy,.skip');
|
|
1513
|
+
if (!element) return;
|
|
1514
|
+
const data = element.dataset;
|
|
1515
|
+
if (data.retry) {
|
|
1516
|
+
switch (data.retry) {
|
|
1517
|
+
case 'overview': void loadOverview(true); break;
|
|
1518
|
+
case 'memories': void loadMemories(state.memory.query || memoryQuery(), state.memory.offset); break;
|
|
1519
|
+
case 'graph': void loadGraph(state.graph.project, true); break;
|
|
1520
|
+
case 'context': $('context-form').requestSubmit(); break;
|
|
1521
|
+
case 'features': void loadFeatures(true); break;
|
|
1522
|
+
}
|
|
1523
|
+
} else if (Object.hasOwn(data, 'memoryClear')) {
|
|
1524
|
+
['memory-query', 'memory-project', 'memory-category', 'memory-agent'].forEach((id) => { $(id).value = ''; });
|
|
1525
|
+
$('memory-query').focus();
|
|
1526
|
+
void loadMemories(memoryQuery());
|
|
1527
|
+
} else if (data.memoryPage) {
|
|
1528
|
+
const { query, offset, limit } = state.memory;
|
|
1529
|
+
const step = data.memoryPage === 'next' ? limit : -limit;
|
|
1530
|
+
$('memory-search').focus();
|
|
1531
|
+
void loadMemories(query || memoryQuery(), Math.max(0, offset + step));
|
|
1532
|
+
} else if (data.node) {
|
|
1533
|
+
selectGraphNode(data.node);
|
|
1534
|
+
} else if (data.related) {
|
|
1535
|
+
graphSelectFromList(data.related, true);
|
|
1536
|
+
} else if (data.feature) {
|
|
1537
|
+
void toggleFeature(data.feature);
|
|
1538
|
+
} else if (element.id === 'context-copy') {
|
|
1539
|
+
void copyContext();
|
|
1540
|
+
} else if (element.classList.contains('skip')) {
|
|
1541
|
+
event.preventDefault();
|
|
1542
|
+
$('main-content').focus();
|
|
1543
|
+
}
|
|
1544
|
+
});
|
|
1545
|
+
|
|
1546
|
+
window.addEventListener('hashchange', () => changeView(true));
|
|
1547
|
+
document.addEventListener('visibilitychange', () => { if (!document.hidden) void refreshActivity(); });
|
|
1548
|
+
setInterval(() => { void refreshActivity(); }, 10000);
|
|
1549
|
+
setPreference('theme', document.documentElement.dataset.theme, false);
|
|
1550
|
+
renderContext();
|
|
1551
|
+
changeView();
|
|
1552
|
+
</script>
|
|
1553
|
+
</body>
|
|
1554
|
+
</html>`;
|
|
1555
|
+
//# sourceMappingURL=workspace-html.js.map
|