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.
- package/CHANGELOG.md +9 -0
- package/README.md +9 -2
- package/dist/scaffold/assets/generated/release.json +1 -1
- package/dist/scaffold/manifest.json +2 -2
- package/dist/ui/LICENSE +21 -0
- package/dist/ui/THIRD-PARTY-NOTICES +212 -0
- package/dist/ui/bin/accrete-ui.mjs +46 -0
- package/dist/ui/page/app.js +49 -0
- package/dist/ui/page/index.html +17 -0
- package/dist/ui/page/panels/activity.css +80 -0
- package/dist/ui/page/panels/activity.js +85 -0
- package/dist/ui/page/panels/agents.css +61 -0
- package/dist/ui/page/panels/agents.js +173 -0
- package/dist/ui/page/panels/objects.css +155 -0
- package/dist/ui/page/panels/objects.js +309 -0
- package/dist/ui/page/panels/running.css +35 -0
- package/dist/ui/page/panels/running.js +159 -0
- package/dist/ui/page/style.css +86 -0
- package/dist/ui/page/terminal.js +88 -0
- package/dist/ui/server/agent-sessions.mjs +600 -0
- package/dist/ui/server/auth.mjs +29 -0
- package/dist/ui/server/deps.mjs +12 -0
- package/dist/ui/server/documents.mjs +285 -0
- package/dist/ui/server/events.mjs +18 -0
- package/dist/ui/server/processes.mjs +110 -0
- package/dist/ui/server/readers/activity.mjs +134 -0
- package/dist/ui/server/readers/agents.mjs +45 -0
- package/dist/ui/server/readers/objects.mjs +261 -0
- package/dist/ui/server/readers/running.mjs +245 -0
- package/dist/ui/server/scope.mjs +36 -0
- package/dist/ui/server/server.mjs +156 -0
- package/dist/ui/server/terminal.mjs +134 -0
- package/dist/ui/server/workspace.mjs +26 -0
- package/dist/ui/ui.json +3 -0
- package/dist/ui/vendor/@xterm/addon-fit/lib/addon-fit.mjs +18 -0
- package/dist/ui/vendor/@xterm/addon-unicode11/lib/addon-unicode11.mjs +44 -0
- package/dist/ui/vendor/@xterm/addon-webgl/lib/addon-webgl.mjs +101 -0
- package/dist/ui/vendor/@xterm/xterm/css/xterm.css +285 -0
- package/dist/ui/vendor/@xterm/xterm/lib/xterm.mjs +53 -0
- package/dist/ui/vendor/node-pty/lib/eventEmitter2.js +47 -0
- package/dist/ui/vendor/node-pty/lib/index.js +52 -0
- package/dist/ui/vendor/node-pty/lib/interfaces.js +7 -0
- package/dist/ui/vendor/node-pty/lib/terminal.js +190 -0
- package/dist/ui/vendor/node-pty/lib/types.js +7 -0
- package/dist/ui/vendor/node-pty/lib/unixTerminal.js +346 -0
- package/dist/ui/vendor/node-pty/lib/utils.js +39 -0
- package/dist/ui/vendor/node-pty/package.json +64 -0
- package/dist/ui/vendor/node-pty/prebuilds/darwin-arm64/pty.node +0 -0
- package/dist/ui/vendor/node-pty/prebuilds/darwin-arm64/spawn-helper +0 -0
- package/dist/ui/vendor/node-pty/prebuilds/darwin-x64/pty.node +0 -0
- package/dist/ui/vendor/node-pty/prebuilds/darwin-x64/spawn-helper +0 -0
- package/dist/ui/vendor/ws/index.js +22 -0
- package/dist/ui/vendor/ws/lib/buffer-util.js +142 -0
- package/dist/ui/vendor/ws/lib/constants.js +19 -0
- package/dist/ui/vendor/ws/lib/event-target.js +292 -0
- package/dist/ui/vendor/ws/lib/extension.js +203 -0
- package/dist/ui/vendor/ws/lib/limiter.js +55 -0
- package/dist/ui/vendor/ws/lib/permessage-deflate.js +530 -0
- package/dist/ui/vendor/ws/lib/receiver.js +743 -0
- package/dist/ui/vendor/ws/lib/sender.js +610 -0
- package/dist/ui/vendor/ws/lib/stream.js +161 -0
- package/dist/ui/vendor/ws/lib/subprotocol.js +62 -0
- package/dist/ui/vendor/ws/lib/validation.js +152 -0
- package/dist/ui/vendor/ws/lib/websocket-server.js +562 -0
- package/dist/ui/vendor/ws/lib/websocket.js +1426 -0
- package/dist/ui/vendor/ws/package.json +74 -0
- package/lib/cli.mjs +22 -4
- package/lib/ui.mjs +24 -0
- package/package.json +5 -5
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
// The running panel: what the process table and open files show, nothing more.
|
|
2
|
+
// Log text is untrusted, so everything goes in through textContent.
|
|
3
|
+
|
|
4
|
+
function duration(ms) {
|
|
5
|
+
const s = Math.max(0, Math.round(ms / 1000));
|
|
6
|
+
if (s < 60) return `${s}s`;
|
|
7
|
+
const m = Math.floor(s / 60);
|
|
8
|
+
if (m < 60) return `${m}m`;
|
|
9
|
+
const h = Math.floor(m / 60);
|
|
10
|
+
if (h < 48) return `${h}h ${m % 60}m`;
|
|
11
|
+
return `${Math.floor(h / 24)}d ${h % 24}h`;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function bytes(n) {
|
|
15
|
+
if (n < 1024) return `${n} B`;
|
|
16
|
+
if (n < 1048576) return `${Math.round(n / 1024)} KB`;
|
|
17
|
+
if (n < 1073741824) return `${(n / 1048576).toFixed(1)} MB`;
|
|
18
|
+
return `${(n / 1073741824).toFixed(2)} GB`;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function memory(mb) {
|
|
22
|
+
return mb < 1024 ? `${mb} MB` : `${(mb / 1024).toFixed(1)} GB`;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// A span that keeps its age current; see the ticker in mount.
|
|
26
|
+
function since(at) {
|
|
27
|
+
const span = document.createElement('span');
|
|
28
|
+
span.dataset.since = at;
|
|
29
|
+
span.textContent = duration(Date.now() - at);
|
|
30
|
+
return span;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function node(tag, className, ...parts) {
|
|
34
|
+
const element = document.createElement(tag);
|
|
35
|
+
if (className) element.className = className;
|
|
36
|
+
element.append(...parts.filter((part) => part !== null && part !== undefined));
|
|
37
|
+
return element;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function displayPath(path, state) {
|
|
41
|
+
const base = state.scope.find((dir) => path.startsWith(`${dir}/`));
|
|
42
|
+
if (base) return path.slice(base.length + 1);
|
|
43
|
+
return path.startsWith(`${state.home}/`) ? `~${path.slice(state.home.length)}` : path;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function baseName(path) {
|
|
47
|
+
return path.split('/').pop();
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function processEvidence(proc, state) {
|
|
51
|
+
const parts = [];
|
|
52
|
+
if (proc.pids) {
|
|
53
|
+
const listed = proc.pids.slice(0, 4).join(', ');
|
|
54
|
+
parts.push(`${proc.pids.length} alike, pids ${listed}${proc.pids.length > 4 ? ', ...' : ''}, oldest running `, since(proc.startedAt), `, ${memory(proc.rssMb)} together`);
|
|
55
|
+
return parts;
|
|
56
|
+
}
|
|
57
|
+
parts.push(`pid ${proc.pid}, running `, since(proc.startedAt), `, ${proc.cpu}% CPU, ${memory(proc.rssMb)}`);
|
|
58
|
+
const newest = state.files[proc.writes[0]];
|
|
59
|
+
if (newest) {
|
|
60
|
+
const others = proc.writes.length > 1 ? ` and ${proc.writes.length - 1} more` : '';
|
|
61
|
+
parts.push(`, writing ${baseName(newest.path)}${others}, last write `, since(newest.mtimeMs), ' ago');
|
|
62
|
+
}
|
|
63
|
+
return parts;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// The program's name is what reads; the whole path is on hover.
|
|
67
|
+
function shortCommand(command) {
|
|
68
|
+
const [program, ...args] = command.split(' ');
|
|
69
|
+
const name = program.split('/').pop().replace(/^-/, '');
|
|
70
|
+
return [program.startsWith('-') ? `-${name}` : name, ...args].join(' ');
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function processRow(proc, state) {
|
|
74
|
+
const command = node('div', 'command', shortCommand(proc.command));
|
|
75
|
+
command.title = proc.command;
|
|
76
|
+
return node(
|
|
77
|
+
'div',
|
|
78
|
+
'process',
|
|
79
|
+
command,
|
|
80
|
+
node('div', 'evidence', ...processEvidence(proc, state)),
|
|
81
|
+
...proc.children.map((child) => processRow(child, state)),
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function growth(file) {
|
|
86
|
+
if (file.overSeconds < 5) return null;
|
|
87
|
+
if (file.grew === 0) return `, no growth in ${file.overSeconds}s`;
|
|
88
|
+
return `, ${file.grew > 0 ? '+' : '-'}${bytes(Math.abs(file.grew))} in ${file.overSeconds}s`;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function fileBlock(file, state) {
|
|
92
|
+
const facts = [`${bytes(file.size)}`];
|
|
93
|
+
if (file.binary) facts.push(', binary');
|
|
94
|
+
if (file.unknown) facts.push(', could not read');
|
|
95
|
+
const heading = node(
|
|
96
|
+
'div',
|
|
97
|
+
'evidence',
|
|
98
|
+
node('span', 'path', displayPath(file.path, state)),
|
|
99
|
+
` ${facts.join('')}${growth(file) ?? ''}, last write `,
|
|
100
|
+
since(file.mtimeMs),
|
|
101
|
+
' ago',
|
|
102
|
+
);
|
|
103
|
+
const block = node('div', 'file', heading);
|
|
104
|
+
if (file.lines?.length) block.append(node('pre', null, file.lines.join('\n')));
|
|
105
|
+
return block;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function filesOf(proc, seen, out = []) {
|
|
109
|
+
for (const path of proc.writes) {
|
|
110
|
+
if (!seen.has(path)) {
|
|
111
|
+
seen.add(path);
|
|
112
|
+
out.push(path);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
for (const child of proc.children) filesOf(child, seen, out);
|
|
116
|
+
return out;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function render(el, state) {
|
|
120
|
+
if (state.loading) {
|
|
121
|
+
el.replaceChildren(node('div', 'quiet head', 'Reading the process table.'));
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
const scope = state.scope.map((path) => (path.startsWith(`${state.home}/`) ? `~${path.slice(state.home.length)}` : path)).join(', ');
|
|
125
|
+
const parts = [node('div', 'quiet head', `${state.roots.length ? '' : 'Nothing running. '}Scope: ${scope}`)];
|
|
126
|
+
const seen = new Set();
|
|
127
|
+
for (const root of state.roots) {
|
|
128
|
+
const paths = filesOf(root, seen);
|
|
129
|
+
parts.push(node('section', 'tree', processRow(root, state), ...paths.map((path) => fileBlock(state.files[path], state))));
|
|
130
|
+
}
|
|
131
|
+
el.replaceChildren(...parts);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export function mount(el, events) {
|
|
135
|
+
let waiting = null;
|
|
136
|
+
|
|
137
|
+
// Do not pull text out from under someone selecting it.
|
|
138
|
+
const selecting = () => {
|
|
139
|
+
const selection = getSelection();
|
|
140
|
+
return selection && !selection.isCollapsed && el.contains(selection.anchorNode);
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
events.addEventListener('running', (event) => {
|
|
144
|
+
waiting = JSON.parse(event.data);
|
|
145
|
+
if (selecting()) return;
|
|
146
|
+
render(el, waiting);
|
|
147
|
+
waiting = null;
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
setInterval(() => {
|
|
151
|
+
if (waiting && !selecting()) {
|
|
152
|
+
render(el, waiting);
|
|
153
|
+
waiting = null;
|
|
154
|
+
}
|
|
155
|
+
for (const span of el.querySelectorAll('[data-since]')) {
|
|
156
|
+
span.textContent = duration(Date.now() - Number(span.dataset.since));
|
|
157
|
+
}
|
|
158
|
+
}, 5000);
|
|
159
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
:root {
|
|
2
|
+
color-scheme: dark;
|
|
3
|
+
--bg: #16181c;
|
|
4
|
+
--bg-raised: #1e2127;
|
|
5
|
+
--fg: #d8dbe0;
|
|
6
|
+
--muted: #8b919b;
|
|
7
|
+
--border: #2e323a;
|
|
8
|
+
--accent: #7aa2f7;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/* The panels follow the system; the terminal stays dark, as its programs expect. */
|
|
12
|
+
@media (prefers-color-scheme: light) {
|
|
13
|
+
:root {
|
|
14
|
+
color-scheme: light;
|
|
15
|
+
--bg: #f7f7f5;
|
|
16
|
+
--bg-raised: #eeeeeb;
|
|
17
|
+
--fg: #1f2328;
|
|
18
|
+
--muted: #646b75;
|
|
19
|
+
--border: #d8d9d5;
|
|
20
|
+
--accent: #2f5fd0;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
* { box-sizing: border-box; }
|
|
25
|
+
|
|
26
|
+
html, body {
|
|
27
|
+
height: 100%;
|
|
28
|
+
margin: 0;
|
|
29
|
+
background: var(--bg);
|
|
30
|
+
color: var(--fg);
|
|
31
|
+
font: 13px/1.45 system-ui, sans-serif;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
body {
|
|
35
|
+
display: grid;
|
|
36
|
+
grid-template-columns: minmax(0, 3fr) minmax(280px, 2fr);
|
|
37
|
+
height: 100vh;
|
|
38
|
+
overflow: hidden;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
#terminal {
|
|
42
|
+
min-width: 0;
|
|
43
|
+
min-height: 0;
|
|
44
|
+
background: #101114;
|
|
45
|
+
}
|
|
46
|
+
/* The padding belongs on .xterm because the fit addon only subtracts it from there. */
|
|
47
|
+
#terminal .xterm {
|
|
48
|
+
height: 100%;
|
|
49
|
+
padding: 6px 0 6px 8px;
|
|
50
|
+
}
|
|
51
|
+
#terminal .xterm-viewport { background-color: #101114; }
|
|
52
|
+
|
|
53
|
+
aside {
|
|
54
|
+
display: flex;
|
|
55
|
+
flex-direction: column;
|
|
56
|
+
min-width: 0;
|
|
57
|
+
min-height: 0;
|
|
58
|
+
border-left: 1px solid var(--border);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
#tabs {
|
|
62
|
+
display: flex;
|
|
63
|
+
flex: none;
|
|
64
|
+
border-bottom: 1px solid var(--border);
|
|
65
|
+
background: var(--bg-raised);
|
|
66
|
+
}
|
|
67
|
+
#tabs button {
|
|
68
|
+
padding: 8px 14px;
|
|
69
|
+
border: 0;
|
|
70
|
+
border-bottom: 2px solid transparent;
|
|
71
|
+
background: none;
|
|
72
|
+
color: var(--muted);
|
|
73
|
+
font: inherit;
|
|
74
|
+
cursor: pointer;
|
|
75
|
+
}
|
|
76
|
+
#tabs button[aria-selected="true"] {
|
|
77
|
+
color: var(--fg);
|
|
78
|
+
border-bottom-color: var(--accent);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
#panels {
|
|
82
|
+
flex: 1;
|
|
83
|
+
min-height: 0;
|
|
84
|
+
overflow: auto;
|
|
85
|
+
}
|
|
86
|
+
#panels > section { padding: 12px; }
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { Terminal } from '/vendor/xterm.mjs';
|
|
2
|
+
import { FitAddon } from '/vendor/addon-fit.mjs';
|
|
3
|
+
import { WebglAddon } from '/vendor/addon-webgl.mjs';
|
|
4
|
+
import { Unicode11Addon } from '/vendor/addon-unicode11.mjs';
|
|
5
|
+
|
|
6
|
+
// OSC 52: programs and tmux set the clipboard this way. Writing only; never reads.
|
|
7
|
+
function writeClipboard(data) {
|
|
8
|
+
const base64 = data.split(';')[1];
|
|
9
|
+
if (base64 && base64 !== '?') {
|
|
10
|
+
const text = new TextDecoder().decode(Uint8Array.from(atob(base64), (c) => c.charCodeAt(0)));
|
|
11
|
+
navigator.clipboard.writeText(text).catch(() => {});
|
|
12
|
+
}
|
|
13
|
+
return true;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function startTerminal(el) {
|
|
17
|
+
const term = new Terminal({
|
|
18
|
+
fontFamily: 'ui-monospace, "SF Mono", Menlo, Monaco, monospace',
|
|
19
|
+
fontSize: 13,
|
|
20
|
+
scrollback: 10000,
|
|
21
|
+
macOptionClickForcesSelection: true, // Option+drag selects in the browser even when a program holds the mouse
|
|
22
|
+
allowProposedApi: true, // for unicode11
|
|
23
|
+
theme: { background: '#101114' },
|
|
24
|
+
});
|
|
25
|
+
const fit = new FitAddon();
|
|
26
|
+
term.loadAddon(fit);
|
|
27
|
+
term.loadAddon(new Unicode11Addon());
|
|
28
|
+
term.unicode.activeVersion = '11'; // the default table makes emoji one cell wide, which tmux and agents do not
|
|
29
|
+
term.parser.registerOscHandler(52, writeClipboard);
|
|
30
|
+
term.open(el);
|
|
31
|
+
try {
|
|
32
|
+
const webgl = new WebglAddon();
|
|
33
|
+
webgl.onContextLoss(() => webgl.dispose());
|
|
34
|
+
term.loadAddon(webgl);
|
|
35
|
+
} catch {
|
|
36
|
+
// The DOM renderer takes over.
|
|
37
|
+
}
|
|
38
|
+
fit.fit();
|
|
39
|
+
window.term = term; // a handle for the browser console
|
|
40
|
+
|
|
41
|
+
const ws = new WebSocket(`ws://${location.host}/terminal`);
|
|
42
|
+
ws.binaryType = 'arraybuffer';
|
|
43
|
+
const send = (data) => ws.readyState === WebSocket.OPEN && ws.send(data);
|
|
44
|
+
const sendSize = () => send(JSON.stringify({ cols: term.cols, rows: term.rows }));
|
|
45
|
+
|
|
46
|
+
const encoder = new TextEncoder();
|
|
47
|
+
term.onData((data) => send(encoder.encode(data)));
|
|
48
|
+
term.onBinary((data) => send(Uint8Array.from(data, (c) => c.charCodeAt(0))));
|
|
49
|
+
term.onResize(sendSize);
|
|
50
|
+
|
|
51
|
+
// xterm sends a bare CR for Shift+Enter, the same as Enter, which submits in an
|
|
52
|
+
// agent. ESC CR is what Claude Code and Codex both take as a newline, in or out of tmux.
|
|
53
|
+
term.attachCustomKeyEventHandler((event) => {
|
|
54
|
+
if (event.key !== 'Enter' || !event.shiftKey || event.altKey || event.ctrlKey || event.metaKey) return true;
|
|
55
|
+
event.preventDefault();
|
|
56
|
+
if (event.type === 'keydown') send(encoder.encode('\x1b\r'));
|
|
57
|
+
return false;
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
// Tell the server what xterm has parsed, so it can pause the PTY while this page is behind.
|
|
61
|
+
let parsed = 0;
|
|
62
|
+
const writeOutput = (bytes) => {
|
|
63
|
+
term.write(bytes, () => {
|
|
64
|
+
parsed += bytes.length;
|
|
65
|
+
if (parsed < 32 * 1024) return;
|
|
66
|
+
send(JSON.stringify({ ack: parsed }));
|
|
67
|
+
parsed = 0;
|
|
68
|
+
});
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
ws.onopen = sendSize;
|
|
72
|
+
ws.onmessage = (event) => {
|
|
73
|
+
if (typeof event.data === 'string') {
|
|
74
|
+
// Replayed output. Any terminal queries in it were answered the first time,
|
|
75
|
+
// so xterm's automatic replies must not reach the shell again.
|
|
76
|
+
term.options.disableStdin = true;
|
|
77
|
+
term.write(event.data, () => {
|
|
78
|
+
term.options.disableStdin = false;
|
|
79
|
+
});
|
|
80
|
+
} else {
|
|
81
|
+
writeOutput(new Uint8Array(event.data));
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
ws.onclose = () => term.write('\r\n\x1b[2m[disconnected; reload to reattach]\x1b[0m\r\n');
|
|
85
|
+
|
|
86
|
+
new ResizeObserver(() => fit.fit()).observe(el);
|
|
87
|
+
term.focus();
|
|
88
|
+
}
|