accrete-kit 0.1.1 → 0.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.
Files changed (69) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/README.md +9 -2
  3. package/dist/scaffold/assets/generated/release.json +1 -1
  4. package/dist/scaffold/manifest.json +2 -2
  5. package/dist/ui/LICENSE +21 -0
  6. package/dist/ui/THIRD-PARTY-NOTICES +212 -0
  7. package/dist/ui/bin/accrete-ui.mjs +46 -0
  8. package/dist/ui/page/app.js +49 -0
  9. package/dist/ui/page/index.html +17 -0
  10. package/dist/ui/page/panels/activity.css +80 -0
  11. package/dist/ui/page/panels/activity.js +85 -0
  12. package/dist/ui/page/panels/agents.css +61 -0
  13. package/dist/ui/page/panels/agents.js +173 -0
  14. package/dist/ui/page/panels/objects.css +155 -0
  15. package/dist/ui/page/panels/objects.js +309 -0
  16. package/dist/ui/page/panels/running.css +35 -0
  17. package/dist/ui/page/panels/running.js +159 -0
  18. package/dist/ui/page/style.css +86 -0
  19. package/dist/ui/page/terminal.js +88 -0
  20. package/dist/ui/server/agent-sessions.mjs +600 -0
  21. package/dist/ui/server/auth.mjs +29 -0
  22. package/dist/ui/server/deps.mjs +12 -0
  23. package/dist/ui/server/documents.mjs +285 -0
  24. package/dist/ui/server/events.mjs +18 -0
  25. package/dist/ui/server/processes.mjs +110 -0
  26. package/dist/ui/server/readers/activity.mjs +134 -0
  27. package/dist/ui/server/readers/agents.mjs +45 -0
  28. package/dist/ui/server/readers/objects.mjs +261 -0
  29. package/dist/ui/server/readers/running.mjs +245 -0
  30. package/dist/ui/server/scope.mjs +36 -0
  31. package/dist/ui/server/server.mjs +156 -0
  32. package/dist/ui/server/terminal.mjs +134 -0
  33. package/dist/ui/server/workspace.mjs +26 -0
  34. package/dist/ui/ui.json +3 -0
  35. package/dist/ui/vendor/@xterm/addon-fit/lib/addon-fit.mjs +18 -0
  36. package/dist/ui/vendor/@xterm/addon-unicode11/lib/addon-unicode11.mjs +44 -0
  37. package/dist/ui/vendor/@xterm/addon-webgl/lib/addon-webgl.mjs +101 -0
  38. package/dist/ui/vendor/@xterm/xterm/css/xterm.css +285 -0
  39. package/dist/ui/vendor/@xterm/xterm/lib/xterm.mjs +53 -0
  40. package/dist/ui/vendor/node-pty/lib/eventEmitter2.js +47 -0
  41. package/dist/ui/vendor/node-pty/lib/index.js +52 -0
  42. package/dist/ui/vendor/node-pty/lib/interfaces.js +7 -0
  43. package/dist/ui/vendor/node-pty/lib/terminal.js +190 -0
  44. package/dist/ui/vendor/node-pty/lib/types.js +7 -0
  45. package/dist/ui/vendor/node-pty/lib/unixTerminal.js +346 -0
  46. package/dist/ui/vendor/node-pty/lib/utils.js +39 -0
  47. package/dist/ui/vendor/node-pty/package.json +64 -0
  48. package/dist/ui/vendor/node-pty/prebuilds/darwin-arm64/pty.node +0 -0
  49. package/dist/ui/vendor/node-pty/prebuilds/darwin-arm64/spawn-helper +0 -0
  50. package/dist/ui/vendor/node-pty/prebuilds/darwin-x64/pty.node +0 -0
  51. package/dist/ui/vendor/node-pty/prebuilds/darwin-x64/spawn-helper +0 -0
  52. package/dist/ui/vendor/ws/index.js +22 -0
  53. package/dist/ui/vendor/ws/lib/buffer-util.js +142 -0
  54. package/dist/ui/vendor/ws/lib/constants.js +19 -0
  55. package/dist/ui/vendor/ws/lib/event-target.js +292 -0
  56. package/dist/ui/vendor/ws/lib/extension.js +203 -0
  57. package/dist/ui/vendor/ws/lib/limiter.js +55 -0
  58. package/dist/ui/vendor/ws/lib/permessage-deflate.js +530 -0
  59. package/dist/ui/vendor/ws/lib/receiver.js +743 -0
  60. package/dist/ui/vendor/ws/lib/sender.js +610 -0
  61. package/dist/ui/vendor/ws/lib/stream.js +161 -0
  62. package/dist/ui/vendor/ws/lib/subprotocol.js +62 -0
  63. package/dist/ui/vendor/ws/lib/validation.js +152 -0
  64. package/dist/ui/vendor/ws/lib/websocket-server.js +562 -0
  65. package/dist/ui/vendor/ws/lib/websocket.js +1426 -0
  66. package/dist/ui/vendor/ws/package.json +74 -0
  67. package/lib/cli.mjs +22 -4
  68. package/lib/ui.mjs +24 -0
  69. package/package.json +5 -5
@@ -0,0 +1,173 @@
1
+ // The agents panel: which sessions exist for this Workspace and what each is doing.
2
+ // Liveness is worded as the evidence for it. Transcript text is untrusted, so
3
+ // everything goes in through textContent.
4
+
5
+ function duration(ms) {
6
+ const s = Math.max(0, Math.round(ms / 1000));
7
+ if (s < 60) return `${s}s`;
8
+ const m = Math.floor(s / 60);
9
+ if (m < 60) return `${m}m`;
10
+ const h = Math.floor(m / 60);
11
+ if (h < 48) return `${h}h ${m % 60}m`;
12
+ return `${Math.floor(h / 24)}d`;
13
+ }
14
+
15
+ function time(value) {
16
+ return typeof value === 'number' ? value : Date.parse(value);
17
+ }
18
+
19
+ // A span that keeps its age current; see the ticker in mount.
20
+ function ago(at) {
21
+ const span = document.createElement('span');
22
+ span.dataset.since = time(at);
23
+ span.textContent = `${duration(Date.now() - time(at))} ago`;
24
+ return span;
25
+ }
26
+
27
+ function node(tag, className, ...parts) {
28
+ const element = document.createElement(tag);
29
+ if (className) element.className = className;
30
+ element.append(...parts.filter((part) => part !== null && part !== undefined));
31
+ return element;
32
+ }
33
+
34
+ function shortPath(path, state) {
35
+ if (!path) return 'unknown';
36
+ return path.startsWith(`${state.home}/`) ? `~${path.slice(state.home.length)}` : path;
37
+ }
38
+
39
+ function programOf(command) {
40
+ if (!command) return null;
41
+ const [program, ...rest] = command.split(/\s+/);
42
+ const name = program.split('/').pop();
43
+ return rest[0] && !rest[0].startsWith('-') ? `${name} ${rest[0]}` : name;
44
+ }
45
+
46
+ function evidence(session) {
47
+ const parts = [];
48
+ const { process: proc, sessionFile: file } = session;
49
+ if (proc?.via === 'open rollout') {
50
+ parts.push(`process ${proc.pid} (${programOf(proc.command) ?? 'unknown'}) has this rollout open, written `);
51
+ } else if (proc) {
52
+ const says = file?.status ? `, session file says ${file.status}` : '';
53
+ parts.push(`process ${proc.pid} present${says}, `);
54
+ parts.push(session.mtimeMs ? 'transcript written ' : 'no transcript yet');
55
+ } else if (file) {
56
+ parts.push(`session file names process ${file.pid}, which is not running, last written `);
57
+ } else if (session.missingParent) {
58
+ parts.push('no rollout found for this thread, only its subagents; ');
59
+ parts.push('no process found');
60
+ } else {
61
+ parts.push('no process found, last written ');
62
+ }
63
+ if (session.mtimeMs) parts.push(ago(session.mtimeMs));
64
+ if (session.turnEndedAt) parts.push(', turn ended ', ago(session.turnEndedAt));
65
+ return parts;
66
+ }
67
+
68
+ function toolLine(tool) {
69
+ if (!tool) return node('div', 'tool quiet', 'last tool unknown');
70
+ const summary = node('code', null, tool.summary ?? 'unknown');
71
+ summary.title = tool.summary ?? '';
72
+ const when = node('span', 'quiet', tool.pending ? 'no result yet' : null);
73
+ if (tool.at && Number.isFinite(Date.parse(tool.at))) when.append(tool.pending ? ', ' : '', ago(tool.at));
74
+ return node('div', 'tool', node('span', 'tool-name', tool.name ?? 'unknown'), summary, when);
75
+ }
76
+
77
+ function subagentRow(child, parent, state) {
78
+ const label = [child.kind, child.nickname].filter(Boolean).join(' ') || 'subagent';
79
+ const facts = [];
80
+ if (child.process) facts.push(`rollout open in process ${child.process.pid}, `);
81
+ if (child.mtimeMs) facts.push('written ', ago(child.mtimeMs));
82
+ if (child.turnEndedAt) facts.push(', turn ended ', ago(child.turnEndedAt));
83
+ return node(
84
+ 'div',
85
+ 'subagent',
86
+ node('div', 'row', node('span', 'kind', label), ' ', node('span', 'title', child.title ?? 'unknown')),
87
+ child.cwd && child.cwd !== parent.cwd ? node('div', 'cwd', shortPath(child.cwd, state)) : null,
88
+ node('div', 'evidence', ...facts),
89
+ toolLine(child.tool),
90
+ );
91
+ }
92
+
93
+ function subagentsBlock(session, state, opened) {
94
+ const { total, active, shown } = session.subagents;
95
+ if (total === 0) return null;
96
+ const summary = `${total} subagent${total === 1 ? '' : 's'}, ${active} written in the last 10 min`;
97
+ const details = node('details', 'subagents', node('summary', null, summary));
98
+ details.open = opened.get(session.id) ?? active > 0;
99
+ details.addEventListener('toggle', () => opened.set(session.id, details.open));
100
+ for (const child of shown) details.append(subagentRow(child, session, state));
101
+ if (total > shown.length) details.append(node('div', 'quiet', `and ${total - shown.length} older`));
102
+ return details;
103
+ }
104
+
105
+ function sessionRow(session, state, opened) {
106
+ const title = session.title ?? `unknown title, ${session.id.slice(0, 8)}`;
107
+ return node(
108
+ 'article',
109
+ session.process ? 'session has-process' : 'session',
110
+ node('div', 'row', node('span', 'agent', session.agent), ' ', node('span', 'title', title)),
111
+ node('div', 'cwd', shortPath(session.cwd, state)),
112
+ node('div', 'evidence', ...evidence(session)),
113
+ toolLine(session.tool),
114
+ subagentsBlock(session, state, opened),
115
+ );
116
+ }
117
+
118
+ function footer(state) {
119
+ const { claude, codex } = state.sources;
120
+ const lines = [`Read ${claude.transcripts} Claude transcripts, ${claude.sessionFiles} session files, ${codex.rollouts} Codex rollouts`];
121
+ if (claude.noCwd) lines.push(`no working directory found in ${claude.noCwd} of the transcripts`);
122
+ if (codex.notUnderstood) lines.push(`${codex.notUnderstood} of the rollouts not understood`);
123
+ if (codex.scanning) lines.push('still indexing Codex rollouts');
124
+ return node('div', 'quiet foot', lines.join('; '));
125
+ }
126
+
127
+ function render(el, state, opened) {
128
+ const parts = [];
129
+ if (!state.loading) {
130
+ const withProcess = state.sessions.filter((session) => session.process).length;
131
+ const count = state.sessions.length + (state.hiddenSessions ?? 0);
132
+ parts.push(node('div', 'quiet head', `${count} session${count === 1 ? '' : 's'}, ${withProcess} with a process. Scope: ${state.scope.map((path) => shortPath(path, state)).join(', ')}`));
133
+ for (const session of state.sessions) parts.push(sessionRow(session, state, opened));
134
+ if (state.hiddenSessions) parts.push(node('div', 'quiet', `and ${state.hiddenSessions} older sessions`));
135
+ for (const proc of state.unattached) {
136
+ parts.push(
137
+ node('div', 'unattached', `process ${proc.pid} (${programOf(proc.command)}) is in ${shortPath(proc.cwd, state)}; no session file or open rollout ties it to a session`),
138
+ );
139
+ }
140
+ parts.push(footer(state));
141
+ } else {
142
+ parts.push(node('div', 'quiet head', 'Reading sessions.'));
143
+ }
144
+ el.replaceChildren(...parts);
145
+ }
146
+
147
+ export function mount(el, events) {
148
+ const opened = new Map(); // session id -> whether its subagents are shown
149
+ let waiting = null;
150
+
151
+ // Do not pull text out from under someone selecting it.
152
+ const selecting = () => {
153
+ const selection = getSelection();
154
+ return selection && !selection.isCollapsed && el.contains(selection.anchorNode);
155
+ };
156
+
157
+ events.addEventListener('agents', (event) => {
158
+ waiting = JSON.parse(event.data);
159
+ if (selecting()) return;
160
+ render(el, waiting, opened);
161
+ waiting = null;
162
+ });
163
+
164
+ setInterval(() => {
165
+ if (waiting && !selecting()) {
166
+ render(el, waiting, opened);
167
+ waiting = null;
168
+ }
169
+ for (const span of el.querySelectorAll('[data-since]')) {
170
+ span.textContent = `${duration(Date.now() - Number(span.dataset.since))} ago`;
171
+ }
172
+ }, 5000);
173
+ }
@@ -0,0 +1,155 @@
1
+ /* Each panel scrolls itself, so moving between tabs keeps each one's place. */
2
+ #panels > #panel-objects {
3
+ height: 100%;
4
+ overflow: auto;
5
+ padding: 0;
6
+ }
7
+
8
+ #panel-objects .head {
9
+ position: sticky;
10
+ top: 0;
11
+ z-index: 1;
12
+ background: var(--bg);
13
+ border-bottom: 1px solid var(--border);
14
+ }
15
+ #panel-objects .types {
16
+ display: flex;
17
+ flex-wrap: wrap;
18
+ gap: 2px;
19
+ padding: 6px 8px 4px;
20
+ }
21
+ #panel-objects .type {
22
+ padding: 2px 6px;
23
+ border: 0;
24
+ border-radius: 3px;
25
+ background: none;
26
+ color: var(--muted);
27
+ font: inherit;
28
+ cursor: pointer;
29
+ }
30
+ #panel-objects .type:hover { color: var(--fg); }
31
+ #panel-objects .type.current {
32
+ background: var(--bg-raised);
33
+ color: var(--fg);
34
+ }
35
+ #panel-objects .count { font-variant-numeric: tabular-nums; opacity: 0.8; }
36
+
37
+ #panel-objects .notice {
38
+ padding: 0 12px 5px;
39
+ font-size: 12px;
40
+ color: var(--muted);
41
+ }
42
+ #panel-objects .notice:empty { display: none; }
43
+ #panel-objects .behind {
44
+ margin: 2px 0 4px;
45
+ padding: 3px 0 3px 8px;
46
+ border-left: 2px solid var(--accent);
47
+ color: var(--fg);
48
+ }
49
+ #panel-objects .sample {
50
+ color: var(--muted);
51
+ overflow-wrap: anywhere;
52
+ }
53
+ #panel-objects code {
54
+ font: 12px ui-monospace, Menlo, monospace;
55
+ color: var(--fg);
56
+ user-select: all;
57
+ }
58
+
59
+ #panel-objects .body { padding: 8px 8px 16px; }
60
+ #panel-objects .empty {
61
+ padding: 4px 4px;
62
+ color: var(--muted);
63
+ }
64
+
65
+ #panel-objects .rows { display: flex; flex-direction: column; }
66
+ #panel-objects .row {
67
+ display: flex;
68
+ flex-wrap: wrap;
69
+ align-items: baseline;
70
+ column-gap: 8px;
71
+ width: 100%;
72
+ padding: 2px 4px;
73
+ border: 0;
74
+ border-radius: 3px;
75
+ background: none;
76
+ color: inherit;
77
+ font: inherit;
78
+ text-align: left;
79
+ cursor: pointer;
80
+ }
81
+ #panel-objects button.row:hover { background: var(--bg-raised); }
82
+ #panel-objects .row.plain { cursor: default; }
83
+ #panel-objects .primary {
84
+ min-width: 0;
85
+ overflow-wrap: anywhere;
86
+ }
87
+ #panel-objects .row.absent .primary { color: var(--muted); }
88
+ #panel-objects .id,
89
+ #panel-objects .facts,
90
+ #panel-objects .status,
91
+ #panel-objects .time,
92
+ #panel-objects .tag,
93
+ #panel-objects .detail {
94
+ color: var(--muted);
95
+ font-size: 12px;
96
+ overflow-wrap: anywhere;
97
+ }
98
+ #panel-objects .time {
99
+ margin-left: auto;
100
+ font-variant-numeric: tabular-nums;
101
+ white-space: nowrap;
102
+ }
103
+ #panel-objects .tag { font-style: italic; }
104
+
105
+ #panel-objects .more,
106
+ #panel-objects .back {
107
+ margin: 4px 0;
108
+ padding: 2px 4px;
109
+ border: 0;
110
+ background: none;
111
+ color: var(--accent);
112
+ font: inherit;
113
+ font-size: 12px;
114
+ cursor: pointer;
115
+ }
116
+ #panel-objects .more:hover,
117
+ #panel-objects .back:hover { text-decoration: underline; }
118
+
119
+ #panel-objects h2 {
120
+ margin: 4px 4px 2px;
121
+ font-size: 14px;
122
+ font-weight: 600;
123
+ overflow-wrap: anywhere;
124
+ }
125
+ #panel-objects h2 .kind {
126
+ color: var(--muted);
127
+ font-weight: 400;
128
+ }
129
+ #panel-objects .meta {
130
+ margin: 0 4px;
131
+ color: var(--muted);
132
+ font-size: 12px;
133
+ overflow-wrap: anywhere;
134
+ }
135
+ #panel-objects .body .behind { margin: 4px; font-size: 12px; }
136
+
137
+ #panel-objects .relation { margin-top: 12px; }
138
+ #panel-objects h3 {
139
+ margin: 0 4px 2px;
140
+ color: var(--muted);
141
+ font-size: 11px;
142
+ font-weight: 600;
143
+ letter-spacing: 0.04em;
144
+ text-transform: uppercase;
145
+ }
146
+ #panel-objects .document {
147
+ margin: 4px;
148
+ padding: 8px;
149
+ border: 1px solid var(--border);
150
+ border-radius: 3px;
151
+ background: var(--bg-raised);
152
+ font: 12px/1.45 ui-monospace, Menlo, monospace;
153
+ white-space: pre-wrap;
154
+ overflow-wrap: anywhere;
155
+ }
@@ -0,0 +1,309 @@
1
+ // The objects panel: browse the types, open a whole document, follow its relations.
2
+ // Workspace text is untrusted, so everything goes in through text nodes.
3
+ const TYPES = [
4
+ ['model', 'Models', 'Model'],
5
+ ['iteration', 'Iterations', 'Iteration'],
6
+ ['tool', 'Tools', 'Tool'],
7
+ ['data', 'Data', 'Data'],
8
+ ['result', 'Results', 'Result'],
9
+ ['memory', 'Memory', 'Memory'],
10
+ ];
11
+ const SINGULAR = Object.fromEntries(TYPES.map(([kind, , one]) => [kind, one]));
12
+ const labelOf = (kind) => TYPES.find(([type]) => type === kind)?.[1];
13
+ const PAGE = 100;
14
+ const RELATION_PAGE = 50;
15
+ const REFRESH = '.accrete/bin/accrete index refresh';
16
+
17
+ function h(tag, props, ...children) {
18
+ const node = Object.assign(document.createElement(tag), props);
19
+ node.append(...children.filter((child) => child !== null && child !== undefined));
20
+ return node;
21
+ }
22
+
23
+ function when(iso) {
24
+ const date = new Date(iso);
25
+ if (Number.isNaN(date.getTime())) return String(iso);
26
+ const time = date.toLocaleTimeString([], { hour12: false });
27
+ return date.toDateString() === new Date().toDateString() ? time : `${date.toLocaleDateString('en-CA')} ${time.slice(0, 5)}`;
28
+ }
29
+
30
+ const shortRevision = (revision) => (revision.length > 12 ? revision.slice(0, 8) : revision);
31
+
32
+ async function fetchData(rest) {
33
+ const response = await fetch(`/data/objects/${rest}`);
34
+ if (!response.ok) throw new Error(`${response.status}`);
35
+ return response.json();
36
+ }
37
+
38
+ export function mount(el, events) {
39
+ const types = h('div', { className: 'types' });
40
+ const notice = h('div', { className: 'notice' });
41
+ const body = h('div', { className: 'body' });
42
+ el.replaceChildren(h('div', { className: 'head' }, types, notice), body);
43
+
44
+ let state = null;
45
+ let view = null; // { type: 'list', kind, items, total } or { type: 'object', id, data }
46
+ let back = []; // Views to return to, each with its scroll position.
47
+ let token = 0; // Only the latest request may draw.
48
+
49
+ function renderTypes() {
50
+ types.replaceChildren(
51
+ ...TYPES.map(([kind, label]) =>
52
+ h(
53
+ 'button',
54
+ {
55
+ className: view?.kind === kind && view.type === 'list' ? 'type current' : 'type',
56
+ onclick: () => {
57
+ back = [];
58
+ showList(kind);
59
+ },
60
+ },
61
+ label,
62
+ ' ',
63
+ h('span', { className: 'count', textContent: state?.counts[kind] ?? 0 }),
64
+ ),
65
+ ),
66
+ );
67
+ }
68
+
69
+ function samples(group, label) {
70
+ if (!group.count) return [];
71
+ const more = group.count > group.sample.length ? ` and ${group.count - group.sample.length} more` : '';
72
+ return [h('div', { className: 'sample', textContent: `${label}: ${group.sample.join(', ')}${more}` })];
73
+ }
74
+
75
+ function renderNotice() {
76
+ const parts = [];
77
+ if (state.install) {
78
+ parts.push(
79
+ h('div', {}, 'No Kernel on this host. Documents list and open. Run ', h('code', { textContent: state.install }), ' in the terminal.'),
80
+ state.kernel_error ? h('div', { className: 'sample', textContent: state.kernel_error }) : null,
81
+ );
82
+ }
83
+ const index = state.index;
84
+ if (index.error) {
85
+ parts.push(h('div', {}, 'Could not read the index: ', index.error));
86
+ } else if (!index.present) {
87
+ if (!state.install) {
88
+ parts.push(h('div', {}, 'No index on this host. Documents list and open. Relations appear after ', h('code', { textContent: REFRESH }), ' in the terminal.'));
89
+ }
90
+ } else {
91
+ const behind = index.changed.count + index.missing.count + index.gone.count;
92
+ if (behind) {
93
+ const counts = [
94
+ index.changed.count && `${index.changed.count} changed since`,
95
+ index.missing.count && `${index.missing.count} not in it`,
96
+ index.gone.count && `${index.gone.count} in it but gone from disk`,
97
+ ].filter(Boolean);
98
+ parts.push(
99
+ h(
100
+ 'div',
101
+ { className: 'behind' },
102
+ h('div', {}, `The index is behind the documents: ${counts.join(', ')}. Relations may be out of date. Run `, h('code', { textContent: REFRESH }), ' in the terminal.'),
103
+ ...samples(index.changed, 'Changed'),
104
+ ...samples(index.missing, 'Not in the index'),
105
+ ...samples(index.gone, 'Gone from disk'),
106
+ ),
107
+ );
108
+ }
109
+ parts.push(h('div', { className: 'refreshed', textContent: `Index refreshed ${when(index.refreshed_at)}, ${index.object_count} objects` }));
110
+ }
111
+ notice.replaceChildren(...parts);
112
+ }
113
+
114
+ function row(item) {
115
+ return h(
116
+ 'button',
117
+ { className: 'row', onclick: () => openObject(item.id) },
118
+ h('span', { className: 'primary', textContent: item.name ?? item.id }),
119
+ item.name && item.name !== item.id ? h('span', { className: 'id', textContent: item.id }) : null,
120
+ item.problem ? h('span', { className: 'tag', textContent: item.problem }) : null,
121
+ item.status ? h('span', { className: 'status', textContent: item.status }) : null,
122
+ item.time ? h('span', { className: 'time', textContent: when(item.time) }) : null,
123
+ );
124
+ }
125
+
126
+ function renderList() {
127
+ const nodes = view.items.length
128
+ ? [h('div', { className: 'rows' }, ...view.items.map(row))]
129
+ : [h('div', { className: 'empty', textContent: `No ${labelOf(view.kind)} in this Workspace.` })];
130
+ const left = view.total - view.items.length;
131
+ if (left > 0) nodes.push(h('button', { className: 'more', textContent: `${left} more`, onclick: loadMore }));
132
+ body.replaceChildren(...nodes);
133
+ }
134
+
135
+ async function loadMore() {
136
+ const mine = ++token;
137
+ const page = await fetchData(`list/${view.kind}/${view.items.length}/${PAGE}`);
138
+ if (mine !== token) return;
139
+ view.items.push(...page.items);
140
+ view.total = page.total;
141
+ renderList();
142
+ }
143
+
144
+ async function showList(kind) {
145
+ view = { type: 'list', kind, items: [], total: 0 };
146
+ renderTypes();
147
+ await refresh();
148
+ el.scrollTop = 0;
149
+ }
150
+
151
+ function relationRow(relation) {
152
+ const facts = [
153
+ relation.direction,
154
+ relation.role,
155
+ relation.commit ? `commit ${shortRevision(relation.commit)}` : null,
156
+ relation.revision ? `${relation.kind === 'tool' || relation.section === 'Results' ? 'revision' : 'starts at'} ${shortRevision(relation.revision)}` : null,
157
+ ].filter(Boolean);
158
+ const notes = [];
159
+ if (relation.id === null) notes.push(h('span', { className: 'detail', textContent: relation.detail ?? '' }));
160
+ if (!relation.present && relation.id !== null) notes.push(h('span', { className: 'tag', textContent: 'not present here' }));
161
+ if (relation.commit_available === false) notes.push(h('span', { className: 'tag', textContent: 'commit not present here' }));
162
+ if (relation.commit_available === null) notes.push(h('span', { className: 'tag', textContent: 'no checkout here' }));
163
+ const title = [relation.commit, relation.revision].filter(Boolean).join(' ');
164
+ const content = [
165
+ h('span', { className: 'primary', textContent: relation.name ?? relation.id ?? '(no id)' }),
166
+ relation.name && relation.name !== relation.id ? h('span', { className: 'id', textContent: relation.id }) : null,
167
+ facts.length ? h('span', { className: 'facts', textContent: facts.join(' · '), title }) : null,
168
+ ...notes,
169
+ ];
170
+ if (relation.id === null) return h('div', { className: 'row plain' }, ...content);
171
+ return h('button', { className: relation.present ? 'row' : 'row absent', onclick: () => openObject(relation.id) }, ...content);
172
+ }
173
+
174
+ function relationSections(relations) {
175
+ const sections = new Map();
176
+ for (const relation of relations) {
177
+ if (!sections.has(relation.section)) sections.set(relation.section, []);
178
+ sections.get(relation.section).push(relation);
179
+ }
180
+ return [...sections].map(([section, rows]) => {
181
+ const list = h('div', { className: 'rows' });
182
+ const node = h('div', { className: 'relation' }, h('h3', { textContent: section }), list);
183
+ let shown = 0;
184
+ const more = h('button', { className: 'more' });
185
+ const showMore = () => {
186
+ const next = rows.slice(shown, shown + RELATION_PAGE);
187
+ list.append(...next.map(relationRow));
188
+ shown += next.length;
189
+ more.textContent = `${rows.length - shown} more`;
190
+ if (shown >= rows.length) more.remove();
191
+ };
192
+ more.onclick = showMore;
193
+ node.append(more);
194
+ showMore();
195
+ return node;
196
+ });
197
+ }
198
+
199
+ function renderRelations(data) {
200
+ if (data.relations_error) return [h('div', { className: 'empty', textContent: `Could not read relations from the index: ${data.relations_error}` })];
201
+ if (!data.relations) return [h('div', { className: 'empty', textContent: 'Relations come from the index, which is not on this host.' })];
202
+ if (!data.relations.length) return [h('div', { className: 'empty', textContent: 'No relations in the index.' })];
203
+ return relationSections(data.relations);
204
+ }
205
+
206
+ function renderObject() {
207
+ const { data, id } = view;
208
+ const previous = back.at(-1);
209
+ const backTo = previous?.type === 'object' ? previous.data?.name ?? previous.id : labelOf(previous?.kind ?? data.kind) ?? 'Objects';
210
+ const backButton = h('button', { className: 'back', textContent: `← ${backTo}`, onclick: goBack });
211
+ if (!data.present) {
212
+ body.replaceChildren(
213
+ backButton,
214
+ h('h2', { textContent: id }),
215
+ h('div', { className: 'meta', textContent: 'Not present on this host. Other objects name it, and it may exist elsewhere.' }),
216
+ ...renderRelations(data),
217
+ );
218
+ return;
219
+ }
220
+ const times = [
221
+ data.created_at && `created ${when(data.created_at)}`,
222
+ data.updated_at && `updated ${when(data.updated_at)}`,
223
+ data.authored_at && `authored ${when(data.authored_at)}`,
224
+ ].filter(Boolean);
225
+ const meta = [
226
+ h('div', { className: 'meta', textContent: [data.id, data.status].filter(Boolean).join(' · ') }),
227
+ h('div', { className: 'meta', textContent: data.rel }),
228
+ h('div', { className: 'meta', title: data.revision, textContent: [`revision ${shortRevision(data.revision)}`, ...times].join(' · ') }),
229
+ ];
230
+ for (const [key, value] of Object.entries(data.binding ?? {})) {
231
+ meta.push(h('div', { className: 'meta', textContent: `${key} on this host: ${typeof value === 'string' ? value : JSON.stringify(value)}` }));
232
+ }
233
+ if (data.kind === 'model' && !data.binding) meta.push(h('div', { className: 'meta', textContent: 'No checkout bound on this host.' }));
234
+ if (data.problem) meta.push(h('div', { className: 'behind', textContent: `This document: ${data.problem}.` }));
235
+ if (data.in_index === false) {
236
+ meta.push(h('div', { className: 'behind', textContent: 'This document is not in the index.' }));
237
+ } else if (data.in_index && data.index_revision !== data.revision) {
238
+ meta.push(h('div', { className: 'behind', textContent: `The index has revision ${shortRevision(data.index_revision)} of this document.` }));
239
+ }
240
+ body.replaceChildren(
241
+ backButton,
242
+ h('h2', {}, h('span', { className: 'kind', textContent: SINGULAR[data.kind] }), ' ', data.name ?? data.id),
243
+ ...meta,
244
+ ...renderRelations(data),
245
+ h('div', { className: 'relation' }, h('h3', { textContent: 'Document' }), h('pre', { className: 'document', textContent: data.text })),
246
+ );
247
+ }
248
+
249
+ function remember() {
250
+ if (view) back.push({ ...view, scroll: el.scrollTop });
251
+ }
252
+
253
+ async function openObject(id) {
254
+ remember();
255
+ view = { type: 'object', id, data: null };
256
+ renderTypes();
257
+ await refresh();
258
+ el.scrollTop = 0;
259
+ }
260
+
261
+ async function goBack() {
262
+ const previous = back.pop();
263
+ if (!previous) return showList(view.data?.kind ?? 'model');
264
+ view = previous;
265
+ renderTypes();
266
+ if (view.type === 'list') renderList();
267
+ else renderObject();
268
+ el.scrollTop = previous.scroll;
269
+ await refresh();
270
+ }
271
+
272
+ // Refetch what is on screen, and redraw only if it changed, so scrolling and any
273
+ // text selection survive a change elsewhere in the Workspace.
274
+ async function refresh() {
275
+ const mine = ++token;
276
+ const current = view;
277
+ try {
278
+ if (current.type === 'list') {
279
+ const page = await fetchData(`list/${current.kind}/0/${Math.max(current.items.length, PAGE)}`);
280
+ if (mine !== token) return;
281
+ const same = current.drawn && JSON.stringify(page.items) === JSON.stringify(current.items) && page.total === current.total;
282
+ Object.assign(current, { items: page.items, total: page.total, drawn: true });
283
+ if (!same) renderList();
284
+ } else {
285
+ const data = await fetchData(`object/${encodeURIComponent(current.id)}`);
286
+ if (mine !== token) return;
287
+ const same = current.data && JSON.stringify(data) === JSON.stringify(current.data);
288
+ current.data = data;
289
+ if (!same) renderObject();
290
+ }
291
+ } catch (err) {
292
+ if (mine === token) body.replaceChildren(h('div', { className: 'empty', textContent: `Could not read this from the server: ${err.message}` }));
293
+ }
294
+ }
295
+
296
+ events.addEventListener('objects', (event) => {
297
+ state = JSON.parse(event.data);
298
+ if (!view) view = { type: 'list', kind: TYPES.find(([kind]) => state.counts[kind])?.[0] ?? 'model', items: [], total: 0 };
299
+ renderTypes();
300
+ renderNotice();
301
+ refresh();
302
+ });
303
+
304
+ // Activity asks for an object here, since the panels are separate modules.
305
+ document.addEventListener('accrete:open-object', (event) => {
306
+ document.querySelector('#tabs [data-panel="objects"]')?.click();
307
+ openObject(event.detail.id);
308
+ });
309
+ }
@@ -0,0 +1,35 @@
1
+ /* Styles for the running panel. Scope them under #panel-running. */
2
+ #panel-running .head { margin-bottom: 8px; }
3
+ #panel-running .quiet { color: var(--muted); }
4
+
5
+ #panel-running .tree {
6
+ padding: 8px 0;
7
+ border-top: 1px solid var(--border);
8
+ }
9
+ #panel-running .process { min-width: 0; }
10
+ #panel-running .process .process {
11
+ margin: 4px 0 0 4px;
12
+ padding-left: 8px;
13
+ border-left: 1px solid var(--border);
14
+ }
15
+ #panel-running .command {
16
+ overflow: hidden;
17
+ white-space: nowrap;
18
+ text-overflow: ellipsis;
19
+ font: 12px ui-monospace, Menlo, monospace;
20
+ }
21
+ #panel-running .evidence {
22
+ color: var(--muted);
23
+ font-size: 12px;
24
+ overflow-wrap: anywhere;
25
+ }
26
+ #panel-running .file { margin-top: 6px; }
27
+ #panel-running .path { color: var(--fg); }
28
+ #panel-running pre {
29
+ margin: 2px 0 0;
30
+ padding: 4px 6px;
31
+ overflow-x: auto;
32
+ background: var(--bg-raised);
33
+ border: 1px solid var(--border);
34
+ font: 12px/1.4 ui-monospace, Menlo, monospace;
35
+ }