graphlin 0.1.1 → 0.1.2
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/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/package.json +2 -1
- package/plugin.json +1 -1
- package/runtime/daemon/dashboard-info.mjs +175 -0
- package/runtime/daemon/server.mjs +9 -1
- package/runtime/web/app.js +196 -19
- package/runtime/web/index.html +23 -0
- package/runtime/web/style.css +18 -0
- package/scripts/control.mjs +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "graphlin",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "Live architecture and activity diagrams from observable coding-agent work.",
|
|
5
5
|
"private": false,
|
|
6
6
|
"type": "module",
|
|
@@ -76,6 +76,7 @@
|
|
|
76
76
|
"./runtime/core/tool-discovery.mjs",
|
|
77
77
|
"./runtime/daemon/auth.mjs",
|
|
78
78
|
"./runtime/daemon/connection-info.mjs",
|
|
79
|
+
"./runtime/daemon/dashboard-info.mjs",
|
|
79
80
|
"./runtime/daemon/demo.mjs",
|
|
80
81
|
"./runtime/daemon/diagnostics.mjs",
|
|
81
82
|
"./runtime/daemon/export.mjs",
|
package/plugin.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json",
|
|
3
3
|
"name": "graphlin",
|
|
4
|
-
"version": "0.1.
|
|
4
|
+
"version": "0.1.2",
|
|
5
5
|
"description": "Local architecture and activity diagrams from observable coding-agent work.",
|
|
6
6
|
"extensions": {
|
|
7
7
|
"com.openai": {
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import { execFile } from 'node:child_process';
|
|
2
|
+
import { readFile } from 'node:fs/promises';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
|
|
5
|
+
const REGISTRY = 'https://registry.npmjs.org/graphlin/latest';
|
|
6
|
+
const CACHE_MS = 30 * 60 * 1000;
|
|
7
|
+
const MAX_REGISTRY_BYTES = 32 * 1024;
|
|
8
|
+
const MAX_COMMAND_BYTES = 8192;
|
|
9
|
+
const PACKAGE = new URL('../../package.json', import.meta.url);
|
|
10
|
+
const CONTROLS = /[\u0000-\u001f\u007f-\u009f\u2028\u2029\u202a-\u202e\u2066-\u2069]/;
|
|
11
|
+
const INSTRUCTIONS = [
|
|
12
|
+
'Press Ctrl+C in the terminal running Graphlin to stop the viewer.',
|
|
13
|
+
'Run the update command to restart the viewer in this project with the same Graphlin data directory.',
|
|
14
|
+
'Start a new Claude Code or Codex agent session after the viewer restarts.',
|
|
15
|
+
];
|
|
16
|
+
const DEMO_INSTRUCTIONS = [
|
|
17
|
+
'Press Ctrl+C in the terminal running Graphlin to stop the demo viewer.',
|
|
18
|
+
'Run the update command to restart the offline demo with the same Graphlin data directory.',
|
|
19
|
+
];
|
|
20
|
+
|
|
21
|
+
const quote = value => `'${value.replaceAll("'", "'\"'\"'")}'`;
|
|
22
|
+
function absolute(value) {
|
|
23
|
+
if (typeof value !== 'string' || !path.isAbsolute(value) ||
|
|
24
|
+
Buffer.byteLength(value) > 4096 || CONTROLS.test(value)) throw new Error('dashboard_info_unavailable');
|
|
25
|
+
return path.resolve(value);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function stableVersion(value) {
|
|
29
|
+
if (typeof value !== 'string' || value.length > 80) return null;
|
|
30
|
+
const match = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/.exec(value);
|
|
31
|
+
return match ? match.slice(1, 4).map(part => BigInt(part)) : null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// null means unsupported/invalid, never "already latest". Build metadata does
|
|
35
|
+
// not affect stable SemVer precedence; numeric components are not lexical.
|
|
36
|
+
export function compareStableVersions(first, second) {
|
|
37
|
+
const a = stableVersion(first), b = stableVersion(second);
|
|
38
|
+
if (!a || !b) return null;
|
|
39
|
+
for (let index = 0; index < 3; index++) {
|
|
40
|
+
if (a[index] !== b[index]) return a[index] > b[index] ? 1 : -1;
|
|
41
|
+
}
|
|
42
|
+
return 0;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async function runningVersion(read) {
|
|
46
|
+
try {
|
|
47
|
+
const text = await read(PACKAGE, 'utf8');
|
|
48
|
+
if (Buffer.byteLength(text) > 64 * 1024) return null;
|
|
49
|
+
const metadata = JSON.parse(text);
|
|
50
|
+
return metadata.name === 'graphlin' && stableVersion(metadata.version) ? metadata.version : null;
|
|
51
|
+
} catch { return null; }
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function branchInfo(projectRoot, execute) {
|
|
55
|
+
return new Promise(resolve => {
|
|
56
|
+
const unavailable = () => resolve({ status: 'unavailable' });
|
|
57
|
+
try {
|
|
58
|
+
execute('git', ['-c', 'core.fsmonitor=false', 'symbolic-ref', '--quiet', '--short', 'HEAD'], {
|
|
59
|
+
cwd: projectRoot, shell: false, timeout: 750, killSignal: 'SIGKILL',
|
|
60
|
+
maxBuffer: 4096, encoding: 'utf8',
|
|
61
|
+
// Do not inherit credentials, Git overrides, or global config. This
|
|
62
|
+
// local ref lookup never runs a shell, hook, installer or remote.
|
|
63
|
+
env: {
|
|
64
|
+
PATH: process.env.PATH || '/usr/bin:/bin', LC_ALL: 'C',
|
|
65
|
+
GIT_CONFIG_NOSYSTEM: '1', GIT_CONFIG_GLOBAL: '/dev/null',
|
|
66
|
+
GIT_OPTIONAL_LOCKS: '0', GIT_TERMINAL_PROMPT: '0',
|
|
67
|
+
},
|
|
68
|
+
}, (error, stdout, stderr) => {
|
|
69
|
+
if (error) {
|
|
70
|
+
if (error.code === 1 && !error.killed) return resolve({ status: 'detached' });
|
|
71
|
+
if (error.code === 128 && !error.killed && typeof stderr === 'string' &&
|
|
72
|
+
stderr.includes('not a git repository')) return resolve({ status: 'not_git' });
|
|
73
|
+
return unavailable();
|
|
74
|
+
}
|
|
75
|
+
const name = typeof stdout === 'string' ? stdout.replace(/\r?\n$/, '') : '';
|
|
76
|
+
if (!name || Buffer.byteLength(name) > 1024 || CONTROLS.test(name)) return unavailable();
|
|
77
|
+
resolve({ status: 'branch', name });
|
|
78
|
+
});
|
|
79
|
+
} catch { unavailable(); }
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async function registryVersion(fetchImpl, timeoutMs) {
|
|
84
|
+
const controller = new AbortController();
|
|
85
|
+
let timer;
|
|
86
|
+
try {
|
|
87
|
+
return await Promise.race([
|
|
88
|
+
(async () => {
|
|
89
|
+
// Fixed destination and headers: project paths, branch names, daemon
|
|
90
|
+
// state, credentials and registry overrides never enter this request.
|
|
91
|
+
const response = await fetchImpl(REGISTRY, {
|
|
92
|
+
method: 'GET', headers: { Accept: 'application/json' },
|
|
93
|
+
redirect: 'error', credentials: 'omit', referrerPolicy: 'no-referrer',
|
|
94
|
+
signal: controller.signal,
|
|
95
|
+
});
|
|
96
|
+
if (!response.ok || Number(response.headers.get('content-length')) > MAX_REGISTRY_BYTES ||
|
|
97
|
+
!response.body) throw new Error('registry_unavailable');
|
|
98
|
+
const reader = response.body.getReader();
|
|
99
|
+
const chunks = [];
|
|
100
|
+
let length = 0;
|
|
101
|
+
try {
|
|
102
|
+
while (true) {
|
|
103
|
+
const { done, value } = await reader.read();
|
|
104
|
+
if (done) break;
|
|
105
|
+
length += value.byteLength;
|
|
106
|
+
if (length > MAX_REGISTRY_BYTES) throw new Error('registry_unavailable');
|
|
107
|
+
chunks.push(Buffer.from(value));
|
|
108
|
+
}
|
|
109
|
+
} finally {
|
|
110
|
+
void reader.cancel().catch(() => {});
|
|
111
|
+
reader.releaseLock();
|
|
112
|
+
}
|
|
113
|
+
const metadata = JSON.parse(Buffer.concat(chunks, length).toString('utf8'));
|
|
114
|
+
if (metadata.name !== 'graphlin' || !stableVersion(metadata.version)) throw new Error('registry_unavailable');
|
|
115
|
+
return metadata.version;
|
|
116
|
+
})(),
|
|
117
|
+
new Promise((_, reject) => {
|
|
118
|
+
timer = setTimeout(() => {
|
|
119
|
+
controller.abort();
|
|
120
|
+
reject(new Error('registry_unavailable'));
|
|
121
|
+
}, timeoutMs);
|
|
122
|
+
}),
|
|
123
|
+
]);
|
|
124
|
+
} catch {
|
|
125
|
+
// Raw network errors and response bodies may contain private content.
|
|
126
|
+
return null;
|
|
127
|
+
} finally {
|
|
128
|
+
clearTimeout(timer);
|
|
129
|
+
controller.abort();
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Trusted startup context only; dependencies are injectable for offline tests.
|
|
134
|
+
// Creating the provider performs no registry request or Git subprocess.
|
|
135
|
+
export function createDashboardInfoProvider({ projectRoot, dataDir, mode = 'live' }, {
|
|
136
|
+
fetch: fetchImpl = globalThis.fetch, execFile: execute = execFile,
|
|
137
|
+
readFile: read = readFile, now = Date.now, timeoutMs = 1500,
|
|
138
|
+
} = {}) {
|
|
139
|
+
const version = runningVersion(read);
|
|
140
|
+
let cached, pending;
|
|
141
|
+
async function latestVersion() {
|
|
142
|
+
if (cached && now() < cached.expiresAt) return cached.latest;
|
|
143
|
+
if (!pending) {
|
|
144
|
+
pending = registryVersion(fetchImpl, timeoutMs).then(latest => {
|
|
145
|
+
cached = { latest, expiresAt: now() + CACHE_MS };
|
|
146
|
+
return latest;
|
|
147
|
+
}).finally(() => { pending = null; });
|
|
148
|
+
}
|
|
149
|
+
return pending;
|
|
150
|
+
}
|
|
151
|
+
return async () => {
|
|
152
|
+
// Metadata that cannot be displayed safely disables this optional endpoint,
|
|
153
|
+
// never startup, capture, or the rest of the viewer.
|
|
154
|
+
if (!['live', 'demo'].includes(mode)) throw new Error('dashboard_info_unavailable');
|
|
155
|
+
const root = absolute(projectRoot), directory = absolute(dataDir);
|
|
156
|
+
const demo = mode === 'demo';
|
|
157
|
+
const command = `${demo ? '' : `cd ${quote(root)} && `}GRAPHLIN_DATA_DIR=${quote(directory)} npx --yes graphlin@latest${demo ? ' demo' : ''}`;
|
|
158
|
+
// Never truncate a shell argument or offer instructions without a usable
|
|
159
|
+
// command. Quoting can expand otherwise valid paths beyond the UI limit.
|
|
160
|
+
const guide = Buffer.byteLength(command) <= MAX_COMMAND_BYTES
|
|
161
|
+
? { command, instructions: [...(demo ? DEMO_INSTRUCTIONS : INSTRUCTIONS)] } : {};
|
|
162
|
+
const [current, branch, latest] = await Promise.all([
|
|
163
|
+
version, branchInfo(root, execute), latestVersion(),
|
|
164
|
+
]);
|
|
165
|
+
if (!current) throw new Error('dashboard_info_unavailable');
|
|
166
|
+
const comparison = compareStableVersions(latest, current);
|
|
167
|
+
return {
|
|
168
|
+
projectRoot: root, mode, branch, version: current,
|
|
169
|
+
update: {
|
|
170
|
+
status: comparison === null ? 'unavailable' : comparison > 0 ? 'available' : 'current',
|
|
171
|
+
current, ...(latest ? { latest } : {}), ...guide,
|
|
172
|
+
},
|
|
173
|
+
};
|
|
174
|
+
};
|
|
175
|
+
}
|
|
@@ -10,6 +10,7 @@ import { createAuth } from './auth.mjs';
|
|
|
10
10
|
import { createPersistence } from './persistence.mjs';
|
|
11
11
|
import { exportSnapshot } from './export.mjs';
|
|
12
12
|
import { createDiagnostics } from './diagnostics.mjs';
|
|
13
|
+
import { createDashboardInfoProvider } from './dashboard-info.mjs';
|
|
13
14
|
|
|
14
15
|
const WEB = new URL('../web/', import.meta.url);
|
|
15
16
|
const assets = new Map([
|
|
@@ -52,10 +53,13 @@ async function bodyJSON(req) {
|
|
|
52
53
|
}
|
|
53
54
|
|
|
54
55
|
export async function startServer({ projectRoot, dataDir, policy: policyOptions,
|
|
55
|
-
decisionService, apiKey: configuredKey, mode = 'live', port = 0 } = {}) {
|
|
56
|
+
decisionService, apiKey: configuredKey, mode = 'live', port = 0, dashboardInfoDependencies } = {}) {
|
|
56
57
|
if (!Number.isInteger(port) || port < 0 || port > 65535) throw runtimeError('invalid_port');
|
|
57
58
|
if (!['live', 'demo'].includes(mode)) throw runtimeError('invalid_mode');
|
|
58
59
|
const paths = await projectPaths(projectRoot, dataDir, { create: true });
|
|
60
|
+
const dashboardInfo = createDashboardInfoProvider({
|
|
61
|
+
projectRoot: paths.projectRoot, dataDir: paths.dataDir, mode,
|
|
62
|
+
}, dashboardInfoDependencies);
|
|
59
63
|
const lock = await acquireLock(paths);
|
|
60
64
|
let finished;
|
|
61
65
|
const whenClosed = new Promise(resolve => { finished = resolve; });
|
|
@@ -157,6 +161,10 @@ export async function startServer({ projectRoot, dataDir, policy: policyOptions,
|
|
|
157
161
|
return;
|
|
158
162
|
}
|
|
159
163
|
if (!auth.authorized(req)) return json(res, 401, { error: 'authentication_required' });
|
|
164
|
+
if (req.method === 'GET' && req.url === '/api/about') {
|
|
165
|
+
try { return json(res, 200, await dashboardInfo()); }
|
|
166
|
+
catch { return json(res, 503, { error: 'dashboard_info_unavailable' }); }
|
|
167
|
+
}
|
|
160
168
|
if (req.method === 'GET' && req.url === '/api/diagnostics') {
|
|
161
169
|
return json(res, 200, diagnostics.snapshot());
|
|
162
170
|
}
|
package/runtime/web/app.js
CHANGED
|
@@ -157,6 +157,23 @@ export function liveNodeChanges(previous, next, eligible) {
|
|
|
157
157
|
};
|
|
158
158
|
}
|
|
159
159
|
|
|
160
|
+
export function filterDiagram(graph, query) {
|
|
161
|
+
if (!query) return graph;
|
|
162
|
+
const needle = query.toLowerCase();
|
|
163
|
+
const nodes = graph.nodes.filter(node => node.label.toLowerCase().includes(needle));
|
|
164
|
+
const visible = new Set(nodes.map(node => node.id));
|
|
165
|
+
return { ...graph, nodes, edges: graph.edges.filter(edge => visible.has(edge.source) && visible.has(edge.target)) };
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function isSearchTypingTarget(target) {
|
|
169
|
+
for (let element = target; element; element = element.parentElement) {
|
|
170
|
+
if (['input', 'textarea', 'select', 'dialog'].includes(element.tagName?.toLowerCase()) ||
|
|
171
|
+
element.isContentEditable || ['textbox', 'combobox', 'searchbox'].includes(element.getAttribute?.('role')) ||
|
|
172
|
+
['true', '', 'plaintext-only'].includes(element.getAttribute?.('contenteditable'))) return true;
|
|
173
|
+
}
|
|
174
|
+
return false;
|
|
175
|
+
}
|
|
176
|
+
|
|
160
177
|
function nodeTitleWidth(shapeName) {
|
|
161
178
|
return { queue: 132, component: 142, parallelogram: 144, diamond: 140 }[shapeName] || 158;
|
|
162
179
|
}
|
|
@@ -816,6 +833,94 @@ export function friendlyProjectName(projectRoot) {
|
|
|
816
833
|
return safeText(projectRoot, 4096).replace(/\/+$/, '').split('/').at(-1)?.slice(0, 120) || 'Local project';
|
|
817
834
|
}
|
|
818
835
|
|
|
836
|
+
export function startDashboardInfo({ load = signal => request('/api/about', { signal }) } = {}) {
|
|
837
|
+
const $ = id => document.getElementById(id);
|
|
838
|
+
let closed = false, controller = null, timer = null, pending = null, command = '';
|
|
839
|
+
function render(info) {
|
|
840
|
+
if (!record(info) || typeof info.projectRoot !== 'string' ||
|
|
841
|
+
!/^\d+\.\d+\.\d+(?:-[A-Za-z0-9.-]+)?(?:\+[A-Za-z0-9.-]+)?$/.test(info.version ?? '')) {
|
|
842
|
+
throw new Error('invalid_dashboard_info');
|
|
843
|
+
}
|
|
844
|
+
$('project-path').textContent = safeText(info.projectRoot, 4096) || 'Path unavailable';
|
|
845
|
+
$('graphlin-version').textContent = info.version;
|
|
846
|
+
$('version-update-steps').textContent = info.mode === 'demo'
|
|
847
|
+
? 'Press Ctrl+C in the demo terminal, then run this command to restart the updated offline demo.'
|
|
848
|
+
: 'Press Ctrl+C in the viewer terminal, then run this command. Start a new Claude Code or Codex session after setup finishes.';
|
|
849
|
+
const branch = info.branch;
|
|
850
|
+
$('project-branch').textContent = branch?.status === 'branch'
|
|
851
|
+
? safeText(branch.name, 1024) || 'Branch unavailable'
|
|
852
|
+
: branch?.status === 'detached' ? `Detached HEAD${branch.commit ? ` · ${safeText(branch.commit, 12)}` : ''}`
|
|
853
|
+
: branch?.status === 'not_git' ? 'Not a Git repository' : 'Branch unavailable';
|
|
854
|
+
const latest = typeof info.update?.latest === 'string' &&
|
|
855
|
+
/^\d+\.\d+\.\d+$/.test(info.update.latest) ? info.update.latest : null;
|
|
856
|
+
const available = info.update?.status === 'available' && latest;
|
|
857
|
+
$('version-update-status').textContent = available ? `Graphlin ${latest} is available`
|
|
858
|
+
: info.update?.status === 'current' ? 'No newer release found'
|
|
859
|
+
: 'Update check unavailable';
|
|
860
|
+
const nextCommand = info.update?.command;
|
|
861
|
+
command = available && typeof nextCommand === 'string' && nextCommand.trim() &&
|
|
862
|
+
nextCommand.length <= 8192 && !/[\u0000-\u001f\u007f-\u009f\u202a-\u202e\u2066-\u2069]/.test(nextCommand)
|
|
863
|
+
? nextCommand : '';
|
|
864
|
+
$('version-update-guide').hidden = !command;
|
|
865
|
+
if ($('version-update-command').textContent !== command) {
|
|
866
|
+
$('version-update-command').textContent = command;
|
|
867
|
+
$('version-update-copy-status').textContent = '';
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
function refresh() {
|
|
871
|
+
if (closed) return Promise.resolve();
|
|
872
|
+
if (pending) return pending;
|
|
873
|
+
clearTimeout(timer);
|
|
874
|
+
controller = new AbortController();
|
|
875
|
+
pending = (async () => {
|
|
876
|
+
try {
|
|
877
|
+
await Promise.resolve();
|
|
878
|
+
if (closed) return;
|
|
879
|
+
const info = await load(controller.signal);
|
|
880
|
+
if (!closed) render(info);
|
|
881
|
+
} catch {
|
|
882
|
+
if (!closed) {
|
|
883
|
+
$('project-branch').textContent = 'Branch unavailable';
|
|
884
|
+
if ($('project-path').textContent === 'Checking…') $('project-path').textContent = 'Path unavailable';
|
|
885
|
+
if ($('graphlin-version').textContent === 'Checking…') $('graphlin-version').textContent = 'Version unavailable';
|
|
886
|
+
$('version-update-status').textContent = 'Update check unavailable';
|
|
887
|
+
$('version-update-guide').hidden = true;
|
|
888
|
+
$('version-update-command').textContent = '';
|
|
889
|
+
command = '';
|
|
890
|
+
}
|
|
891
|
+
} finally {
|
|
892
|
+
pending = null;
|
|
893
|
+
controller = null;
|
|
894
|
+
if (!closed) timer = setTimeout(() => { void refresh(); }, 30000);
|
|
895
|
+
}
|
|
896
|
+
})();
|
|
897
|
+
return pending;
|
|
898
|
+
}
|
|
899
|
+
const onCopy = async () => {
|
|
900
|
+
if (closed || !command) return;
|
|
901
|
+
const copied = command;
|
|
902
|
+
try {
|
|
903
|
+
await window.navigator.clipboard.writeText(copied);
|
|
904
|
+
if (!closed && command === copied) $('version-update-copy-status').textContent = 'Copied';
|
|
905
|
+
} catch {
|
|
906
|
+
if (!closed && command === copied) {
|
|
907
|
+
$('version-update-copy-status').textContent = 'Select the command and copy it manually.';
|
|
908
|
+
$('version-update-command').focus();
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
};
|
|
912
|
+
$('version-update-copy').addEventListener('click', onCopy);
|
|
913
|
+
return {
|
|
914
|
+
refresh,
|
|
915
|
+
close() {
|
|
916
|
+
closed = true;
|
|
917
|
+
controller?.abort();
|
|
918
|
+
clearTimeout(timer);
|
|
919
|
+
$('version-update-copy').removeEventListener('click', onCopy);
|
|
920
|
+
},
|
|
921
|
+
};
|
|
922
|
+
}
|
|
923
|
+
|
|
819
924
|
export const ORIENTATION_PROMPT = 'Orient yourself in this project: read its main files and explain how the components connect.';
|
|
820
925
|
|
|
821
926
|
// Observations are not an installation or trust audit. Activity (including
|
|
@@ -1450,12 +1555,13 @@ export function startViewer() {
|
|
|
1450
1555
|
connection: 'connecting', busy: false, exporting: false, epoch: 0, connectEpoch: 0,
|
|
1451
1556
|
viewport: null, fitBounds: null, zoom: 1, followFit: true, lastGraphSignature: '', inspectorSignature: '',
|
|
1452
1557
|
nodeElements: new Map(), edgeElements: new Map(), activityElements: new Map(),
|
|
1453
|
-
views: new Map(), viewKey: null, view: null, displayGraph: null,
|
|
1454
|
-
effects: new Map(), motionReady: false, movement: null, closed: false, projectName: '',
|
|
1558
|
+
views: new Map(), viewKey: null, view: null, displayGraph: null, searchQuery: '',
|
|
1559
|
+
effects: new Map(), liveReady: false, motionReady: false, movement: null, closed: false, projectName: '',
|
|
1455
1560
|
};
|
|
1456
1561
|
const motionPreference = window.matchMedia?.('(prefers-reduced-motion: reduce)');
|
|
1457
1562
|
const sketches = createSketchCache();
|
|
1458
1563
|
const detailSketches = createSketchCache(sketchDetails);
|
|
1564
|
+
const dashboardInfo = startDashboardInfo();
|
|
1459
1565
|
const connectionDialog = startConnectionDialog({ onInfo: info => {
|
|
1460
1566
|
state.projectName = friendlyProjectName(info.projectRoot);
|
|
1461
1567
|
renderStatus();
|
|
@@ -1524,7 +1630,7 @@ export function startViewer() {
|
|
|
1524
1630
|
$('onboarding-action').dataset.action = progress.next.action;
|
|
1525
1631
|
// Preserve a manual text selection while live snapshots arrive.
|
|
1526
1632
|
if ($('orientation-prompt').textContent !== ORIENTATION_PROMPT) $('orientation-prompt').textContent = ORIENTATION_PROMPT;
|
|
1527
|
-
$('orientation').hidden = Boolean(state.replayFrame || state.snapshot?.mode === 'demo' || state.snapshot?.mode === 'replay');
|
|
1633
|
+
$('orientation').hidden = Boolean(state.searchQuery || state.replayFrame || state.snapshot?.mode === 'demo' || state.snapshot?.mode === 'replay');
|
|
1528
1634
|
}
|
|
1529
1635
|
function currentGraph() { return state.replayFrame?.graph || state.snapshot?.graph; }
|
|
1530
1636
|
function applyTheme() {
|
|
@@ -1537,6 +1643,7 @@ export function startViewer() {
|
|
|
1537
1643
|
if (state.viewKey !== key) {
|
|
1538
1644
|
finishPan();
|
|
1539
1645
|
clearMotion();
|
|
1646
|
+
state.liveReady = false;
|
|
1540
1647
|
state.motionReady = false;
|
|
1541
1648
|
let view = state.views.get(key);
|
|
1542
1649
|
if (!view) view = createPresentation();
|
|
@@ -1580,14 +1687,14 @@ export function startViewer() {
|
|
|
1580
1687
|
state.effects.set(id, effect);
|
|
1581
1688
|
target.addEventListener('animationend', finish);
|
|
1582
1689
|
}
|
|
1583
|
-
function cancelMovement() {
|
|
1690
|
+
function cancelMovement({ fit = true } = {}) {
|
|
1584
1691
|
const movement = state.movement;
|
|
1585
1692
|
if (!movement) return;
|
|
1586
1693
|
state.movement = null;
|
|
1587
1694
|
window.cancelAnimationFrame?.(movement.frame);
|
|
1588
1695
|
clearTimeout(movement.timer);
|
|
1589
1696
|
if (state.displayGraph) paintGeometry(state.displayGraph);
|
|
1590
|
-
if (movement.fitAfter) fitCamera(movement.fitAfter);
|
|
1697
|
+
if (fit && movement.fitAfter) fitCamera(movement.fitAfter);
|
|
1591
1698
|
}
|
|
1592
1699
|
function clearMotion() {
|
|
1593
1700
|
cancelMovement();
|
|
@@ -1595,10 +1702,11 @@ export function startViewer() {
|
|
|
1595
1702
|
}
|
|
1596
1703
|
function resetMotionBaseline() {
|
|
1597
1704
|
finishPan();
|
|
1705
|
+
state.liveReady = false;
|
|
1598
1706
|
state.motionReady = false;
|
|
1599
1707
|
clearMotion();
|
|
1600
1708
|
}
|
|
1601
|
-
function animateChanges(changes, before) {
|
|
1709
|
+
function animateChanges(changes, before, focusNodeId) {
|
|
1602
1710
|
// Re-addition always cancels a removal, even when motion is suppressed.
|
|
1603
1711
|
for (const node of currentGraph().nodes) if (state.effects.get(node.id)?.element) finishEffect(node.id);
|
|
1604
1712
|
if (!motionAllowed()) return;
|
|
@@ -1627,7 +1735,7 @@ export function startViewer() {
|
|
|
1627
1735
|
removals.push(decoration);
|
|
1628
1736
|
}
|
|
1629
1737
|
if (removals.length) {
|
|
1630
|
-
fitCamera(state.movement?.fitDuring || graphBounds(state.displayGraph));
|
|
1738
|
+
if (!focusNodeId) fitCamera(state.movement?.fitDuring || graphBounds(state.displayGraph));
|
|
1631
1739
|
$('empty-canvas').hidden = true;
|
|
1632
1740
|
$('effects-layer').append(...removals);
|
|
1633
1741
|
}
|
|
@@ -1677,15 +1785,21 @@ export function startViewer() {
|
|
|
1677
1785
|
const eligible = streamed && state.motionReady && !switched && !state.replayFrame &&
|
|
1678
1786
|
snapshot.mode !== 'replay' && state.snapshot?.mode !== 'replay' && motionAllowed();
|
|
1679
1787
|
const changes = liveNodeChanges(state.snapshot?.graph, snapshot.graph, eligible);
|
|
1788
|
+
// A live baseline is independent of animation preferences. Reconnects and
|
|
1789
|
+
// initial/session snapshots establish it without focusing an old arrival.
|
|
1790
|
+
const live = streamed && state.liveReady && !switched && !state.replayFrame &&
|
|
1791
|
+
snapshot.mode !== 'replay' && state.snapshot?.mode !== 'replay';
|
|
1792
|
+
const focusNodeId = liveNodeChanges(state.snapshot?.graph, snapshot.graph, live).added.at(-1);
|
|
1680
1793
|
const before = state.displayGraph;
|
|
1681
|
-
cancelMovement();
|
|
1794
|
+
cancelMovement({ fit: !focusNodeId });
|
|
1682
1795
|
if (switched) resetView();
|
|
1683
1796
|
state.snapshot = snapshot;
|
|
1684
1797
|
state.epoch += 1;
|
|
1685
1798
|
state.frames = historyFrames(snapshot);
|
|
1686
1799
|
state.replayFrame = reconcileReplayFrame(state.frames, state.replayFrame);
|
|
1687
|
-
render();
|
|
1688
|
-
animateChanges(changes, before);
|
|
1800
|
+
render({ focusNodeId });
|
|
1801
|
+
animateChanges(changes, before, focusNodeId);
|
|
1802
|
+
state.liveReady = streamed && !state.replayFrame && snapshot.mode !== 'replay';
|
|
1689
1803
|
state.motionReady = streamed && !state.replayFrame && snapshot.mode !== 'replay' && motionAllowed();
|
|
1690
1804
|
$('updated-at').textContent = `Snapshot received ${formatTime(Date.now())}`;
|
|
1691
1805
|
}
|
|
@@ -1869,6 +1983,23 @@ export function startViewer() {
|
|
|
1869
1983
|
cancelMovement();
|
|
1870
1984
|
fitCamera(graphBounds(graph));
|
|
1871
1985
|
}
|
|
1986
|
+
function focusNode(node, bounds) {
|
|
1987
|
+
finishPan();
|
|
1988
|
+
const zoom = Math.max(.5, state.zoom);
|
|
1989
|
+
const scale = state.zoom / zoom;
|
|
1990
|
+
const width = state.viewport.width * scale;
|
|
1991
|
+
const height = state.viewport.height * scale;
|
|
1992
|
+
state.viewport = {
|
|
1993
|
+
x: node.x + NODE_WIDTH / 2 - width / 2,
|
|
1994
|
+
y: node.y + NODE_HEIGHT / 2 - height / 2,
|
|
1995
|
+
width, height,
|
|
1996
|
+
};
|
|
1997
|
+
state.zoom = zoom;
|
|
1998
|
+
state.fitBounds = bounds;
|
|
1999
|
+
// Removal cleanup must not replace arrival focus with a later fit-all.
|
|
2000
|
+
state.followFit = false;
|
|
2001
|
+
setViewBox();
|
|
2002
|
+
}
|
|
1872
2003
|
function zoom(factor, anchor = { x: .5, y: .5 }) {
|
|
1873
2004
|
if (!state.viewport || !state.fitBounds) return;
|
|
1874
2005
|
cancelMovement();
|
|
@@ -1885,20 +2016,23 @@ export function startViewer() {
|
|
|
1885
2016
|
state.followFit = false;
|
|
1886
2017
|
setViewBox();
|
|
1887
2018
|
}
|
|
1888
|
-
function renderGraph({ forceFit = false } = {}) {
|
|
2019
|
+
function renderGraph({ forceFit = false, focusNodeId } = {}) {
|
|
1889
2020
|
const canonical = currentGraph();
|
|
1890
2021
|
if (!canonical) return;
|
|
1891
2022
|
const view = presentation();
|
|
1892
2023
|
applyTheme();
|
|
1893
|
-
|
|
2024
|
+
// Search only projects visibility; layout, evidence and live arrival
|
|
2025
|
+
// detection retain the complete source graph.
|
|
2026
|
+
const graph = filterDiagram(projectPresentation(canonical, view), state.searchQuery);
|
|
1894
2027
|
state.displayGraph = graph;
|
|
1895
2028
|
const routes = graphEdgeRoutes(graph);
|
|
1896
2029
|
const bounds = graphBounds(graph, routes);
|
|
1897
2030
|
const signature = cameraGraphSignature(graph, view.algorithm);
|
|
1898
|
-
//
|
|
1899
|
-
//
|
|
1900
|
-
|
|
1901
|
-
if (
|
|
2031
|
+
// Focus the final projected position before inserting newcomers. Other
|
|
2032
|
+
// diagram changes retain fit-all; metadata-only updates keep the camera.
|
|
2033
|
+
const newest = graph.nodes.find(node => node.id === focusNodeId);
|
|
2034
|
+
if (newest && state.viewport && !forceFit) focusNode(newest, bounds);
|
|
2035
|
+
else if (forceFit || !state.viewport || signature !== state.lastGraphSignature) fitCamera(bounds);
|
|
1902
2036
|
state.lastGraphSignature = signature;
|
|
1903
2037
|
$('layout').value = view.algorithm;
|
|
1904
2038
|
$('auto-arrange').checked = view.auto;
|
|
@@ -1966,6 +2100,7 @@ export function startViewer() {
|
|
|
1966
2100
|
center,
|
|
1967
2101
|
svgElement('rect', { class: 'selection-ring', x: -7, y: -7, width: NODE_WIDTH + 14, height: NODE_HEIGHT + 14, rx: 14 }),
|
|
1968
2102
|
);
|
|
2103
|
+
group.setAttribute('transform', `translate(${node.x} ${node.y})`);
|
|
1969
2104
|
state.nodeElements.set(node.id, group);
|
|
1970
2105
|
$('node-layer').append(group);
|
|
1971
2106
|
}
|
|
@@ -2009,6 +2144,9 @@ export function startViewer() {
|
|
|
2009
2144
|
$('diagram-title').textContent = `${state.replayFrame ? 'Historical' : 'Live'} architecture, revision ${graph.revision}`;
|
|
2010
2145
|
$('diagram-desc').textContent = `${graph.nodes.length} components and ${graph.edges.length} relationships. Code interpretation does not establish runtime connectivity. Use Tab and Enter to inspect a component or relationship. With the diagram focused, use plus and minus to zoom, arrow keys to pan, and 0 to fit.`;
|
|
2011
2146
|
$('graph-count').textContent = `${graph.nodes.length} components · ${graph.edges.length} relationships`;
|
|
2147
|
+
$('diagram-search-status').textContent = state.searchQuery
|
|
2148
|
+
? `${graph.nodes.length} of ${canonical.nodes.length} components shown` : '';
|
|
2149
|
+
$('diagram-search-clear').hidden = !state.searchQuery;
|
|
2012
2150
|
$('empty-canvas').hidden = graph.nodes.length > 0 || removalBounds().length > 0;
|
|
2013
2151
|
const classifier = state.snapshot.paused ? 'paused' : state.snapshot.status.classifier;
|
|
2014
2152
|
const emptyMessages = {
|
|
@@ -2018,7 +2156,9 @@ export function startViewer() {
|
|
|
2018
2156
|
unavailable: ['Waiting for classification.', 'The classifier is unavailable. Safe activity continues below; supported architecture will appear when classification recovers.'],
|
|
2019
2157
|
timeout: ['Evidence needs another moment.', 'Classification exceeded its deadline. Activity still appears below, and no unsupported components are added.'],
|
|
2020
2158
|
};
|
|
2021
|
-
const message = state.
|
|
2159
|
+
const message = state.searchQuery
|
|
2160
|
+
? ['No matching components.', `No labels contain “${state.searchQuery}”. Try another search or press Esc to restore the diagram.`]
|
|
2161
|
+
: state.replayFrame
|
|
2022
2162
|
? ['No components in this revision.', 'Move through the recent revisions or return to Live to follow the current map.']
|
|
2023
2163
|
: emptyMessages[classifier] || ['Your architecture starts here.', 'Work in a connected agent session. Components appear when approved evidence supports them; activity can arrive first.'];
|
|
2024
2164
|
$('empty-title').textContent = message[0];
|
|
@@ -2305,10 +2445,10 @@ export function startViewer() {
|
|
|
2305
2445
|
$('activity-empty').hidden = events.length > 0;
|
|
2306
2446
|
$('activity-list').hidden = events.length === 0;
|
|
2307
2447
|
}
|
|
2308
|
-
function render() {
|
|
2448
|
+
function render(graphOptions) {
|
|
2309
2449
|
renderStatus();
|
|
2310
2450
|
renderOnboarding();
|
|
2311
|
-
renderGraph();
|
|
2451
|
+
renderGraph(graphOptions);
|
|
2312
2452
|
renderInspector();
|
|
2313
2453
|
renderHistory();
|
|
2314
2454
|
renderActivity();
|
|
@@ -2384,6 +2524,7 @@ export function startViewer() {
|
|
|
2384
2524
|
connection('reconnecting');
|
|
2385
2525
|
error('The live connection was lost. Displaying the last received snapshot while the viewer reconnects.');
|
|
2386
2526
|
});
|
|
2527
|
+
void dashboardInfo.refresh();
|
|
2387
2528
|
// Optional authenticated metadata must not hold up the event stream or
|
|
2388
2529
|
// turn an older server's missing endpoint into a connection failure.
|
|
2389
2530
|
const controller = new AbortController();
|
|
@@ -2392,6 +2533,7 @@ export function startViewer() {
|
|
|
2392
2533
|
const info = normalizeConnectionInfo(await request('/api/connection-info', { signal: controller.signal }));
|
|
2393
2534
|
if (!state.closed && attempt === state.connectEpoch) {
|
|
2394
2535
|
state.projectName = friendlyProjectName(info.projectRoot);
|
|
2536
|
+
$('project-path').textContent = info.projectRoot;
|
|
2395
2537
|
renderStatus();
|
|
2396
2538
|
}
|
|
2397
2539
|
} catch { /* The project ID remains a usable fallback. */ }
|
|
@@ -2455,6 +2597,37 @@ export function startViewer() {
|
|
|
2455
2597
|
$('onboarding-action').addEventListener('click', onOnboardingAction);
|
|
2456
2598
|
$('orientation-copy').addEventListener('click', onCopyOrientation);
|
|
2457
2599
|
$('activity-toggle').addEventListener('click', onToggleActivity);
|
|
2600
|
+
const onSearchInput = () => {
|
|
2601
|
+
if (state.closed || state.searchQuery === $('diagram-search').value) return;
|
|
2602
|
+
state.searchQuery = $('diagram-search').value;
|
|
2603
|
+
// Filtering is not a source change: cancel existing decoration/movement
|
|
2604
|
+
// and render directly, without changing the snapshot arrival baseline.
|
|
2605
|
+
finishPan();
|
|
2606
|
+
clearMotion();
|
|
2607
|
+
renderOnboarding();
|
|
2608
|
+
renderGraph({ forceFit: true });
|
|
2609
|
+
};
|
|
2610
|
+
const clearSearch = () => {
|
|
2611
|
+
$('diagram-search').value = '';
|
|
2612
|
+
onSearchInput();
|
|
2613
|
+
$('diagram-search').focus({ preventScroll: true });
|
|
2614
|
+
};
|
|
2615
|
+
const onSearchKeyDown = event => {
|
|
2616
|
+
if (state.closed || event.defaultPrevented || event.isComposing || event.ctrlKey || event.metaKey || event.altKey ||
|
|
2617
|
+
$('diagnostics-dialog').open || $('connection-dialog').open || document.querySelector?.('dialog[open], [role="dialog"][aria-modal="true"]')) return;
|
|
2618
|
+
const target = event.target || document.activeElement;
|
|
2619
|
+
if (target !== $('diagram-search') && isSearchTypingTarget(target)) return;
|
|
2620
|
+
if (event.key === '/' && target !== $('diagram-search')) {
|
|
2621
|
+
event.preventDefault();
|
|
2622
|
+
$('diagram-search').focus({ preventScroll: true });
|
|
2623
|
+
} else if (event.key === 'Escape' && state.searchQuery) {
|
|
2624
|
+
event.preventDefault();
|
|
2625
|
+
clearSearch();
|
|
2626
|
+
}
|
|
2627
|
+
};
|
|
2628
|
+
$('diagram-search').addEventListener('input', onSearchInput);
|
|
2629
|
+
$('diagram-search-clear').addEventListener('click', clearSearch);
|
|
2630
|
+
window.addEventListener('keydown', onSearchKeyDown);
|
|
2458
2631
|
$('pause').addEventListener('click', () => control(state.snapshot?.paused ? 'resume' : 'pause'));
|
|
2459
2632
|
$('session').addEventListener('change', () => control('session', $('session').value));
|
|
2460
2633
|
$('export').addEventListener('click', exportJSON);
|
|
@@ -2595,9 +2768,13 @@ export function startViewer() {
|
|
|
2595
2768
|
state.closed = true;
|
|
2596
2769
|
projectController?.abort();
|
|
2597
2770
|
projectController = null;
|
|
2771
|
+
dashboardInfo.close();
|
|
2598
2772
|
$('onboarding-action').removeEventListener('click', onOnboardingAction);
|
|
2599
2773
|
$('orientation-copy').removeEventListener('click', onCopyOrientation);
|
|
2600
2774
|
$('activity-toggle').removeEventListener('click', onToggleActivity);
|
|
2775
|
+
$('diagram-search').removeEventListener('input', onSearchInput);
|
|
2776
|
+
$('diagram-search-clear').removeEventListener('click', clearSearch);
|
|
2777
|
+
window.removeEventListener?.('keydown', onSearchKeyDown);
|
|
2601
2778
|
$('architecture').removeEventListener('wheel', onDiagramWheel);
|
|
2602
2779
|
connectionDialog.dispose();
|
|
2603
2780
|
diagnosticsDialog.dispose();
|
package/runtime/web/index.html
CHANGED
|
@@ -32,6 +32,22 @@
|
|
|
32
32
|
<p id="project-label" class="project-label">Local project</p>
|
|
33
33
|
</div>
|
|
34
34
|
|
|
35
|
+
<section class="project-details" aria-label="Project and Graphlin version">
|
|
36
|
+
<dl>
|
|
37
|
+
<div><dt>Branch</dt><dd id="project-branch">Checking…</dd></div>
|
|
38
|
+
<div class="project-path"><dt>Path</dt><dd id="project-path">Checking…</dd></div>
|
|
39
|
+
<div><dt>Graphlin</dt><dd id="graphlin-version">Checking…</dd></div>
|
|
40
|
+
</dl>
|
|
41
|
+
<p id="version-update-status" role="status">Checking for updates…</p>
|
|
42
|
+
<details id="version-update-guide" hidden>
|
|
43
|
+
<summary>How to update</summary>
|
|
44
|
+
<p id="version-update-steps">Press Ctrl+C in the viewer terminal, then run this command. Start a new Claude Code or Codex session after setup finishes.</p>
|
|
45
|
+
<pre id="version-update-command" tabindex="0"></pre>
|
|
46
|
+
<button id="version-update-copy" type="button">Copy update command</button>
|
|
47
|
+
<span id="version-update-copy-status" role="status"></span>
|
|
48
|
+
</details>
|
|
49
|
+
</section>
|
|
50
|
+
|
|
35
51
|
<div id="demo-banner" class="notice demo-notice" role="status" hidden>
|
|
36
52
|
<strong>Demo session</strong>
|
|
37
53
|
<span>Generated project and fixture classifier answers. This is an offline illustration, not a live Jev evaluation.</span>
|
|
@@ -107,6 +123,13 @@
|
|
|
107
123
|
<span class="screen-reader" id="theme-note">Colors follow component types. Themes change appearance only.</span>
|
|
108
124
|
<span id="layout-note">Arranges when components or connections change.</span>
|
|
109
125
|
</div>
|
|
126
|
+
<div class="diagram-search-bar" role="search" aria-label="Diagram search">
|
|
127
|
+
<label for="diagram-search">Find components</label>
|
|
128
|
+
<input id="diagram-search" type="search" maxlength="200" autocomplete="off" spellcheck="false" placeholder="Search labels…" aria-controls="architecture" aria-describedby="diagram-search-hint" aria-keyshortcuts="/">
|
|
129
|
+
<button id="diagram-search-clear" type="button" hidden>Clear search</button>
|
|
130
|
+
<span id="diagram-search-hint">/ to search · Esc to clear</span>
|
|
131
|
+
<span id="diagram-search-status" role="status" aria-live="polite" aria-atomic="true"></span>
|
|
132
|
+
</div>
|
|
110
133
|
<div class="diagram-stage" id="diagram-stage">
|
|
111
134
|
<svg id="architecture" viewBox="0 0 920 510" role="group" tabindex="0" aria-labelledby="diagram-title diagram-desc">
|
|
112
135
|
<title id="diagram-title">Architecture diagram</title>
|
package/runtime/web/style.css
CHANGED
|
@@ -171,6 +171,22 @@ h1 { font-size: clamp(27px, 2.3vw, 36px); letter-spacing: -1px; font-weight: 650
|
|
|
171
171
|
.zoom-controls button { border: 0; background: transparent; min-width: 31px; min-height: 32px; padding: 2px 8px; font-size: 17px; }
|
|
172
172
|
.zoom-controls #fit { border: 1px solid var(--line); font-size: 11px; margin-left: 5px; min-width: 39px; }
|
|
173
173
|
.layout-toolbar { display: flex; flex-wrap: wrap; align-items: center; gap: 8px 12px; padding: 12px 21px 4px; font-size: 11px; }
|
|
174
|
+
.project-details { margin: 0 0 18px; padding: 12px 16px; border: 1px solid var(--line); border-radius: 8px; background: var(--white); font-size: 12px; }
|
|
175
|
+
.project-details dl { display: flex; flex-wrap: wrap; gap: 12px 24px; margin: 0; }
|
|
176
|
+
.project-details dl > div { min-width: 0; }
|
|
177
|
+
.project-details dt { color: var(--muted); font-size: 10px; margin-bottom: 4px; }
|
|
178
|
+
.project-details dd { margin: 0; overflow-wrap: anywhere; }
|
|
179
|
+
.project-details .project-path { flex: 1 1 240px; }
|
|
180
|
+
.project-details > p { margin: 10px 0 0; color: var(--muted); font-size: 11px; }
|
|
181
|
+
.project-details details { margin-top: 10px; }
|
|
182
|
+
.project-details summary { cursor: pointer; font-weight: 600; }
|
|
183
|
+
.project-details pre { white-space: pre-wrap; overflow-wrap: anywhere; padding: 10px; background: var(--paper); }
|
|
184
|
+
.project-details button { min-height: 36px; font-size: 11px; }
|
|
185
|
+
.diagram-search-bar { display: flex; flex-wrap: wrap; align-items: center; gap: 8px 12px; padding: 12px 21px 4px; font-size: 11px; color: var(--muted); }
|
|
186
|
+
.diagram-search-bar input { flex: 1 1 160px; min-width: 0; min-height: 40px; padding: 8px 10px; border: 1px solid var(--line); border-radius: 5px; background: var(--white); color: var(--ink); }
|
|
187
|
+
.diagram-search-bar input::placeholder { color: var(--muted); opacity: 1; }
|
|
188
|
+
.diagram-search-bar button { min-height: 40px; font-size: 11px; }
|
|
189
|
+
#diagram-search-status:empty { display: none; }
|
|
174
190
|
.layout-toolbar label { display: flex; align-items: center; gap: 8px; color: var(--muted); }
|
|
175
191
|
.layout-toolbar select, .shape-picker select { color: var(--ink); background: var(--white); border: 1px solid var(--line); border-radius: 5px; padding: 7px 25px 7px 9px; font: inherit; min-height: 34px; }
|
|
176
192
|
.layout-toolbar button { font-size: 11px; min-height: 34px; padding: 5px 12px; }
|
|
@@ -490,6 +506,7 @@ footer span { margin: 0 8px; color: #91a7c3; }
|
|
|
490
506
|
#replay-note { grid-column: 2; margin-top: -9px; text-align: left; }
|
|
491
507
|
.drawing-toolbar { padding: 15px 15px 0; }
|
|
492
508
|
.layout-toolbar { padding-right: 15px; padding-left: 15px; }
|
|
509
|
+
.diagram-search-bar { padding-right: 15px; padding-left: 15px; }
|
|
493
510
|
.drawing-heading { gap: 3px 10px; }
|
|
494
511
|
.drawing-footer { padding-right: 15px; padding-left: 15px; }
|
|
495
512
|
.diagram-note { padding-left: 15px; }
|
|
@@ -558,6 +575,7 @@ footer span { margin: 0 8px; color: #91a7c3; }
|
|
|
558
575
|
.coverage-status { flex-direction: row; text-align: left; gap: 5px 14px; font-size: 10px; max-width: none; }
|
|
559
576
|
.drawing-toolbar { padding: 10px 10px 0; gap: 7px; align-items: start; }
|
|
560
577
|
.layout-toolbar { padding: 10px 12px 3px; gap: 9px; }
|
|
578
|
+
.diagram-search-bar { padding: 10px 12px 3px; gap: 8px; }
|
|
561
579
|
.layout-toolbar select, .layout-toolbar button { min-height: 40px; }
|
|
562
580
|
.auto-arrange { min-height: 30px; }
|
|
563
581
|
.shape-key { padding-right: 12px; padding-left: 12px; }
|
package/scripts/control.mjs
CHANGED
|
@@ -39,7 +39,7 @@ async function handle(line) {
|
|
|
39
39
|
const supported = ['2024-11-05', '2025-03-26', '2025-06-18'];
|
|
40
40
|
return result({ protocolVersion: supported.includes(message.params?.protocolVersion) ? message.params.protocolVersion : '2025-06-18',
|
|
41
41
|
capabilities: { tools: { listChanged: false } },
|
|
42
|
-
serverInfo: { name: 'graphlin', version: '0.1.
|
|
42
|
+
serverInfo: { name: 'graphlin', version: '0.1.2' },
|
|
43
43
|
instructions: 'Controls only. Passive host hooks provide observations when separately activated. No drawing calls after each action.' });
|
|
44
44
|
}
|
|
45
45
|
if (message.method === 'ping') return result({});
|