@alavida/agentpack 0.1.2 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -1
- package/bin/intent.js +20 -0
- package/package.json +11 -5
- package/skills/agentpack-cli/SKILL.md +4 -1
- package/skills/authoring-skillgraphs-from-knowledge/SKILL.md +148 -0
- package/skills/authoring-skillgraphs-from-knowledge/references/authored-metadata.md +6 -0
- package/skills/developing-and-testing-skills/SKILL.md +109 -0
- package/skills/developing-and-testing-skills/references/local-workbench.md +7 -0
- package/skills/getting-started-skillgraphs/SKILL.md +115 -0
- package/skills/getting-started-skillgraphs/references/command-routing.md +7 -0
- package/skills/identifying-skill-opportunities/SKILL.md +119 -0
- package/skills/identifying-skill-opportunities/references/capability-boundaries.md +6 -0
- package/skills/maintaining-skillgraph-freshness/SKILL.md +110 -0
- package/skills/repairing-broken-skill-or-plugin-state/SKILL.md +112 -0
- package/skills/repairing-broken-skill-or-plugin-state/references/diagnostic-flows.md +6 -0
- package/skills/shipping-production-plugins-and-packages/SKILL.md +123 -0
- package/skills/shipping-production-plugins-and-packages/references/plugin-delivery.md +6 -0
- package/src/application/skills/build-skill-workbench-model.js +194 -0
- package/src/application/skills/run-skill-workbench-action.js +23 -0
- package/src/application/skills/start-skill-dev-workbench.js +192 -0
- package/src/cli.js +1 -1
- package/src/commands/skills.js +7 -1
- package/src/dashboard/App.jsx +343 -0
- package/src/dashboard/components/Breadcrumbs.jsx +45 -0
- package/src/dashboard/components/ControlStrip.jsx +153 -0
- package/src/dashboard/components/InspectorPanel.jsx +203 -0
- package/src/dashboard/components/SkillGraph.jsx +567 -0
- package/src/dashboard/components/Tooltip.jsx +111 -0
- package/src/dashboard/dist/dashboard.js +26692 -0
- package/src/dashboard/index.html +81 -0
- package/src/dashboard/lib/api.js +19 -0
- package/src/dashboard/lib/router.js +15 -0
- package/src/dashboard/main.jsx +4 -0
- package/src/domain/plugins/load-plugin-definition.js +163 -0
- package/src/domain/plugins/plugin-diagnostic-error.js +18 -0
- package/src/domain/plugins/plugin-requirements.js +15 -0
- package/src/domain/skills/skill-graph.js +1 -0
- package/src/infrastructure/runtime/open-browser.js +20 -0
- package/src/infrastructure/runtime/skill-dev-workbench-server.js +96 -0
- package/src/infrastructure/runtime/watch-skill-workbench.js +68 -0
- package/src/lib/plugins.js +19 -28
- package/src/lib/skills.js +60 -12
- package/src/utils/errors.js +33 -1
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import { existsSync, readdirSync, readFileSync } from 'node:fs';
|
|
2
|
+
import { dirname, join } from 'node:path';
|
|
3
|
+
import { parseSkillFrontmatterFile, readPackageMetadata, normalizeDisplayPath } from '../../domain/skills/skill-model.js';
|
|
4
|
+
import { readBuildState, compareRecordedSources } from '../../domain/skills/skill-provenance.js';
|
|
5
|
+
import { buildSkillGraph, buildSkillStatusMap } from '../../domain/skills/skill-graph.js';
|
|
6
|
+
import { buildTransitiveSkillWorkbenchModel } from './build-skill-workbench-model.js';
|
|
7
|
+
import { startSkillDevWorkbenchServer } from '../../infrastructure/runtime/skill-dev-workbench-server.js';
|
|
8
|
+
import { openBrowser } from '../../infrastructure/runtime/open-browser.js';
|
|
9
|
+
import { watchSkillWorkbench } from '../../infrastructure/runtime/watch-skill-workbench.js';
|
|
10
|
+
import { runSkillWorkbenchAction } from './run-skill-workbench-action.js';
|
|
11
|
+
|
|
12
|
+
function listPackagedSkillDirs(repoRoot) {
|
|
13
|
+
const stack = [repoRoot];
|
|
14
|
+
const results = [];
|
|
15
|
+
|
|
16
|
+
while (stack.length > 0) {
|
|
17
|
+
const current = stack.pop();
|
|
18
|
+
let entries = [];
|
|
19
|
+
try {
|
|
20
|
+
entries = readdirSync(current, { withFileTypes: true });
|
|
21
|
+
} catch {
|
|
22
|
+
continue;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
let hasSkillFile = false;
|
|
26
|
+
let hasPackageFile = false;
|
|
27
|
+
|
|
28
|
+
for (const entry of entries) {
|
|
29
|
+
if (entry.name === '.git' || entry.name === 'node_modules' || entry.name === '.agentpack') continue;
|
|
30
|
+
const fullPath = join(current, entry.name);
|
|
31
|
+
|
|
32
|
+
if (entry.isDirectory()) {
|
|
33
|
+
stack.push(fullPath);
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (entry.name === 'SKILL.md') hasSkillFile = true;
|
|
38
|
+
if (entry.name === 'package.json') hasPackageFile = true;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (hasSkillFile && hasPackageFile) {
|
|
42
|
+
results.push(current);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return results.sort();
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function findPackageDirByName(repoRoot, packageName) {
|
|
50
|
+
const stack = [repoRoot];
|
|
51
|
+
|
|
52
|
+
while (stack.length > 0) {
|
|
53
|
+
const current = stack.pop();
|
|
54
|
+
let entries = [];
|
|
55
|
+
try {
|
|
56
|
+
entries = readdirSync(current, { withFileTypes: true });
|
|
57
|
+
} catch {
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
for (const entry of entries) {
|
|
62
|
+
if (entry.name === '.git' || entry.name === 'node_modules') continue;
|
|
63
|
+
const fullPath = join(current, entry.name);
|
|
64
|
+
|
|
65
|
+
if (entry.isDirectory()) {
|
|
66
|
+
stack.push(fullPath);
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (entry.name !== 'package.json') continue;
|
|
71
|
+
|
|
72
|
+
try {
|
|
73
|
+
const pkg = JSON.parse(readFileSync(fullPath, 'utf-8'));
|
|
74
|
+
if (pkg.name === packageName) return dirname(fullPath);
|
|
75
|
+
} catch {
|
|
76
|
+
// skip
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const nodeModulesPath = join(repoRoot, 'node_modules', ...packageName.split('/'));
|
|
82
|
+
if (existsSync(join(nodeModulesPath, 'SKILL.md'))) return nodeModulesPath;
|
|
83
|
+
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function buildModelForSkill(repoRoot, targetPackageName) {
|
|
88
|
+
const packageDirs = listPackagedSkillDirs(repoRoot);
|
|
89
|
+
const skillGraph = buildSkillGraph(repoRoot, packageDirs, {
|
|
90
|
+
parseSkillFrontmatterFile,
|
|
91
|
+
readPackageMetadata,
|
|
92
|
+
findPackageDirByName: (root, name) => findPackageDirByName(root, name),
|
|
93
|
+
normalizeDisplayPath,
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
const buildState = readBuildState(repoRoot);
|
|
97
|
+
const staleSkills = new Set();
|
|
98
|
+
const changedSources = new Set(); // track individual changed source paths
|
|
99
|
+
|
|
100
|
+
for (const [packageName, record] of Object.entries(buildState.skills || buildState)) {
|
|
101
|
+
if (typeof record !== 'object' || !record.sources) continue;
|
|
102
|
+
try {
|
|
103
|
+
const changes = compareRecordedSources(repoRoot, record);
|
|
104
|
+
if (changes.length > 0) {
|
|
105
|
+
staleSkills.add(packageName);
|
|
106
|
+
for (const change of changes) changedSources.add(change.path);
|
|
107
|
+
}
|
|
108
|
+
} catch {
|
|
109
|
+
// skip
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const statusMap = buildSkillStatusMap(skillGraph, staleSkills);
|
|
114
|
+
|
|
115
|
+
function resolveSkillSources(packageName) {
|
|
116
|
+
const graphNode = skillGraph.get(packageName);
|
|
117
|
+
if (!graphNode) return [];
|
|
118
|
+
|
|
119
|
+
const skillFilePath = join(repoRoot, graphNode.skillFile);
|
|
120
|
+
try {
|
|
121
|
+
const metadata = parseSkillFrontmatterFile(skillFilePath);
|
|
122
|
+
return metadata.sources || [];
|
|
123
|
+
} catch {
|
|
124
|
+
return [];
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function resolveSkillRequires(packageName) {
|
|
129
|
+
const graphNode = skillGraph.get(packageName);
|
|
130
|
+
if (!graphNode) return [];
|
|
131
|
+
|
|
132
|
+
const skillFilePath = join(repoRoot, graphNode.skillFile);
|
|
133
|
+
try {
|
|
134
|
+
const metadata = parseSkillFrontmatterFile(skillFilePath);
|
|
135
|
+
return metadata.requires || [];
|
|
136
|
+
} catch {
|
|
137
|
+
return [];
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return buildTransitiveSkillWorkbenchModel({
|
|
142
|
+
repoRoot,
|
|
143
|
+
targetPackageName,
|
|
144
|
+
skillGraph,
|
|
145
|
+
statusMap,
|
|
146
|
+
changedSources,
|
|
147
|
+
resolveSkillSources,
|
|
148
|
+
resolveSkillRequires,
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export async function startSkillDevWorkbench({
|
|
153
|
+
repoRoot,
|
|
154
|
+
skillDir,
|
|
155
|
+
open = true,
|
|
156
|
+
disableBrowser = false,
|
|
157
|
+
}) {
|
|
158
|
+
const packageMetadata = readPackageMetadata(skillDir);
|
|
159
|
+
const defaultSkill = packageMetadata.packageName;
|
|
160
|
+
|
|
161
|
+
const server = await startSkillDevWorkbenchServer({
|
|
162
|
+
buildModel: (skillPackageName) => buildModelForSkill(repoRoot, skillPackageName),
|
|
163
|
+
defaultSkill,
|
|
164
|
+
onAction(action) {
|
|
165
|
+
return runSkillWorkbenchAction(action, {
|
|
166
|
+
cwd: repoRoot,
|
|
167
|
+
target: skillDir,
|
|
168
|
+
packageName: defaultSkill,
|
|
169
|
+
});
|
|
170
|
+
},
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
const watcher = watchSkillWorkbench(repoRoot, skillDir, () => {
|
|
174
|
+
// Model is rebuilt on each request, so no cache to invalidate
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
if (open && !disableBrowser) {
|
|
178
|
+
openBrowser(server.url);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
return {
|
|
182
|
+
url: server.url,
|
|
183
|
+
port: server.port,
|
|
184
|
+
refresh() {
|
|
185
|
+
// no-op: models are built on demand per request
|
|
186
|
+
},
|
|
187
|
+
close() {
|
|
188
|
+
watcher.close();
|
|
189
|
+
server.close();
|
|
190
|
+
},
|
|
191
|
+
};
|
|
192
|
+
}
|
package/src/cli.js
CHANGED
|
@@ -68,7 +68,7 @@ export function run(argv) {
|
|
|
68
68
|
if (err instanceof AgentpackError) {
|
|
69
69
|
const opts = program.opts?.() || {};
|
|
70
70
|
if (opts.json) {
|
|
71
|
-
output.json(
|
|
71
|
+
output.json(err.toJSON());
|
|
72
72
|
} else {
|
|
73
73
|
output.error(formatError(err));
|
|
74
74
|
}
|
package/src/commands/skills.js
CHANGED
|
@@ -26,11 +26,13 @@ export function skillsCommand() {
|
|
|
26
26
|
.command('dev')
|
|
27
27
|
.description('Link one local packaged skill for local Claude and agent discovery')
|
|
28
28
|
.option('--no-sync', 'Skip syncing managed package dependencies from requires')
|
|
29
|
+
.option('--no-dashboard', 'Skip starting the local skill development workbench')
|
|
29
30
|
.argument('<target>', 'Packaged skill directory or SKILL.md path')
|
|
30
|
-
.action((target, opts, command) => {
|
|
31
|
+
.action(async (target, opts, command) => {
|
|
31
32
|
const globalOpts = command.optsWithGlobals();
|
|
32
33
|
const session = startSkillDev(target, {
|
|
33
34
|
sync: opts.sync,
|
|
35
|
+
dashboard: opts.dashboard,
|
|
34
36
|
onStart(result) {
|
|
35
37
|
if (globalOpts.json) {
|
|
36
38
|
output.json(result);
|
|
@@ -45,6 +47,9 @@ export function skillsCommand() {
|
|
|
45
47
|
for (const link of result.links) {
|
|
46
48
|
output.write(`Linked: ${link}`);
|
|
47
49
|
}
|
|
50
|
+
if (result.workbench?.enabled) {
|
|
51
|
+
output.write(`Workbench URL: ${result.workbench.url}`);
|
|
52
|
+
}
|
|
48
53
|
output.write('Note: if your current agent session was already running, start a fresh session to pick up newly linked skills.');
|
|
49
54
|
if (result.unresolved.length > 0) {
|
|
50
55
|
output.write('Unresolved Dependencies:');
|
|
@@ -72,6 +77,7 @@ export function skillsCommand() {
|
|
|
72
77
|
|
|
73
78
|
process.once('SIGTERM', stop);
|
|
74
79
|
process.once('SIGINT', stop);
|
|
80
|
+
await session.ready;
|
|
75
81
|
});
|
|
76
82
|
|
|
77
83
|
cmd
|
|
@@ -0,0 +1,343 @@
|
|
|
1
|
+
import { useEffect, useLayoutEffect, useState, useCallback, useRef } from 'react';
|
|
2
|
+
import { fetchWorkbenchModel, runWorkbenchAction } from './lib/api.js';
|
|
3
|
+
import { getSkillFromHash, setSkillHash, onHashChange } from './lib/router.js';
|
|
4
|
+
import { SkillGraph } from './components/SkillGraph.jsx';
|
|
5
|
+
import { InspectorPanel } from './components/InspectorPanel.jsx';
|
|
6
|
+
import { Breadcrumbs } from './components/Breadcrumbs.jsx';
|
|
7
|
+
import { ControlStrip } from './components/ControlStrip.jsx';
|
|
8
|
+
import { Tooltip } from './components/Tooltip.jsx';
|
|
9
|
+
|
|
10
|
+
export function App() {
|
|
11
|
+
const [model, setModel] = useState(null);
|
|
12
|
+
const [selectedId, setSelectedId] = useState(null);
|
|
13
|
+
const [error, setError] = useState(null);
|
|
14
|
+
const [loading, setLoading] = useState(true);
|
|
15
|
+
const [busyAction, setBusyAction] = useState(null);
|
|
16
|
+
const [labelsVisible, setLabelsVisible] = useState(true);
|
|
17
|
+
const [knowledgeVisible, setKnowledgeVisible] = useState(true);
|
|
18
|
+
const [resetZoomSignal, setResetZoomSignal] = useState(0);
|
|
19
|
+
const [lightMode, setLightMode] = useState(false);
|
|
20
|
+
const [trail, setTrail] = useState([]);
|
|
21
|
+
const [tooltipNode, setTooltipNode] = useState(null);
|
|
22
|
+
const [tooltipPos, setTooltipPos] = useState(null);
|
|
23
|
+
const inspectedNode = model?.nodes.find((n) => n.id === selectedId) || null;
|
|
24
|
+
|
|
25
|
+
useLayoutEffect(() => {
|
|
26
|
+
document.documentElement.setAttribute('data-theme', lightMode ? 'light' : 'dark');
|
|
27
|
+
}, [lightMode]);
|
|
28
|
+
|
|
29
|
+
const loadModel = useCallback(async (skillPackageName) => {
|
|
30
|
+
try {
|
|
31
|
+
setLoading(true);
|
|
32
|
+
setError(null);
|
|
33
|
+
const nextModel = await fetchWorkbenchModel(skillPackageName);
|
|
34
|
+
setModel(nextModel);
|
|
35
|
+
setSelectedId(null);
|
|
36
|
+
return nextModel;
|
|
37
|
+
} catch (err) {
|
|
38
|
+
setError(err.message);
|
|
39
|
+
return null;
|
|
40
|
+
} finally {
|
|
41
|
+
setLoading(false);
|
|
42
|
+
}
|
|
43
|
+
}, []);
|
|
44
|
+
|
|
45
|
+
// Initial load
|
|
46
|
+
useEffect(() => {
|
|
47
|
+
const hashSkill = getSkillFromHash();
|
|
48
|
+
loadModel(hashSkill).then((m) => {
|
|
49
|
+
if (m) {
|
|
50
|
+
const entry = { packageName: m.selected.id, name: m.selected.name };
|
|
51
|
+
if (hashSkill) {
|
|
52
|
+
setTrail([entry]);
|
|
53
|
+
} else {
|
|
54
|
+
setTrail([entry]);
|
|
55
|
+
setSkillHash(m.selected.id);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
}, [loadModel]);
|
|
60
|
+
|
|
61
|
+
// Hash change listener
|
|
62
|
+
useEffect(() => {
|
|
63
|
+
onHashChange((skillPackageName) => {
|
|
64
|
+
if (skillPackageName) {
|
|
65
|
+
loadModel(skillPackageName).then((m) => {
|
|
66
|
+
if (m) {
|
|
67
|
+
setTrail((prev) => {
|
|
68
|
+
const existingIndex = prev.findIndex((e) => e.packageName === skillPackageName);
|
|
69
|
+
if (existingIndex >= 0) {
|
|
70
|
+
return prev.slice(0, existingIndex + 1);
|
|
71
|
+
}
|
|
72
|
+
return [...prev, { packageName: m.selected.id, name: m.selected.name }];
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
}, [loadModel]);
|
|
79
|
+
|
|
80
|
+
function navigateToSkill(packageName) {
|
|
81
|
+
setSkillHash(packageName);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function handleBreadcrumbNavigate(packageName, index) {
|
|
85
|
+
setTrail((prev) => prev.slice(0, index + 1));
|
|
86
|
+
setSkillHash(packageName);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async function handleAction(action) {
|
|
90
|
+
if (action === 'reset-zoom') {
|
|
91
|
+
setResetZoomSignal((s) => s + 1);
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (action === 'refresh') {
|
|
96
|
+
const hashSkill = getSkillFromHash();
|
|
97
|
+
setBusyAction('refresh');
|
|
98
|
+
await loadModel(hashSkill);
|
|
99
|
+
setBusyAction(null);
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
try {
|
|
104
|
+
setBusyAction(action);
|
|
105
|
+
await runWorkbenchAction(action);
|
|
106
|
+
} catch (err) {
|
|
107
|
+
setError(err.message);
|
|
108
|
+
} finally {
|
|
109
|
+
setBusyAction(null);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const handleHover = useCallback((node, pos) => {
|
|
114
|
+
setTooltipNode(node);
|
|
115
|
+
setTooltipPos(pos);
|
|
116
|
+
}, []);
|
|
117
|
+
|
|
118
|
+
const handleHoverEnd = useCallback(() => {
|
|
119
|
+
setTooltipNode(null);
|
|
120
|
+
setTooltipPos(null);
|
|
121
|
+
}, []);
|
|
122
|
+
|
|
123
|
+
const handleGraphClick = useCallback((nodeId) => {
|
|
124
|
+
setSelectedId((prev) => prev === nodeId ? null : nodeId);
|
|
125
|
+
}, []);
|
|
126
|
+
|
|
127
|
+
return (
|
|
128
|
+
<>
|
|
129
|
+
{/* Header */}
|
|
130
|
+
<header style={{
|
|
131
|
+
padding: '20px 40px 0',
|
|
132
|
+
display: 'flex',
|
|
133
|
+
justifyContent: 'space-between',
|
|
134
|
+
alignItems: 'flex-start',
|
|
135
|
+
flexShrink: 0,
|
|
136
|
+
}}>
|
|
137
|
+
<div>
|
|
138
|
+
<div style={{
|
|
139
|
+
fontFamily: 'var(--font-mono)',
|
|
140
|
+
fontVariant: 'small-caps',
|
|
141
|
+
textTransform: 'uppercase',
|
|
142
|
+
letterSpacing: 3,
|
|
143
|
+
fontSize: 9,
|
|
144
|
+
color: 'var(--text-dim)',
|
|
145
|
+
marginBottom: 4,
|
|
146
|
+
}}>
|
|
147
|
+
Agentpack
|
|
148
|
+
</div>
|
|
149
|
+
<div style={{
|
|
150
|
+
fontFamily: 'var(--font-body)',
|
|
151
|
+
fontSize: 28,
|
|
152
|
+
fontWeight: 400,
|
|
153
|
+
fontStyle: 'italic',
|
|
154
|
+
color: 'var(--text)',
|
|
155
|
+
}}>
|
|
156
|
+
Skill Graph
|
|
157
|
+
</div>
|
|
158
|
+
<hr style={{
|
|
159
|
+
width: 36,
|
|
160
|
+
height: 1,
|
|
161
|
+
background: 'var(--status-current)',
|
|
162
|
+
border: 'none',
|
|
163
|
+
marginTop: 10,
|
|
164
|
+
}} />
|
|
165
|
+
</div>
|
|
166
|
+
<div style={{
|
|
167
|
+
display: 'flex',
|
|
168
|
+
gap: 20,
|
|
169
|
+
alignItems: 'center',
|
|
170
|
+
}}>
|
|
171
|
+
<LegendItem color="var(--edge-provenance)" label="Source" shape="diamond" />
|
|
172
|
+
<LegendItem color="#c45454" label="Changed" shape="diamond" />
|
|
173
|
+
<LegendItem color="var(--status-current)" label="Current" shape="dot" />
|
|
174
|
+
<LegendItem color="var(--status-stale)" label="Stale" shape="dot" />
|
|
175
|
+
<LegendItem color="var(--status-affected)" label="Affected" shape="ring" />
|
|
176
|
+
<LegendItem color="var(--edge-requires)" label="Requires" shape="line" />
|
|
177
|
+
<LegendItem color="var(--edge-provenance)" label="Provenance" shape="dashed" />
|
|
178
|
+
</div>
|
|
179
|
+
</header>
|
|
180
|
+
|
|
181
|
+
{/* Breadcrumbs */}
|
|
182
|
+
<Breadcrumbs trail={trail} onNavigate={handleBreadcrumbNavigate} />
|
|
183
|
+
|
|
184
|
+
{/* Main area */}
|
|
185
|
+
<div style={{ flex: 1, position: 'relative', minHeight: 0 }}>
|
|
186
|
+
{loading && !model && (
|
|
187
|
+
<div style={{
|
|
188
|
+
display: 'flex',
|
|
189
|
+
alignItems: 'center',
|
|
190
|
+
justifyContent: 'center',
|
|
191
|
+
height: '100%',
|
|
192
|
+
fontFamily: 'var(--font-mono)',
|
|
193
|
+
fontSize: 12,
|
|
194
|
+
color: 'var(--text-dim)',
|
|
195
|
+
}}>
|
|
196
|
+
Loading...
|
|
197
|
+
</div>
|
|
198
|
+
)}
|
|
199
|
+
|
|
200
|
+
{error && (
|
|
201
|
+
<div style={{
|
|
202
|
+
display: 'flex',
|
|
203
|
+
flexDirection: 'column',
|
|
204
|
+
alignItems: 'center',
|
|
205
|
+
justifyContent: 'center',
|
|
206
|
+
height: '100%',
|
|
207
|
+
gap: 16,
|
|
208
|
+
}}>
|
|
209
|
+
<div style={{
|
|
210
|
+
fontFamily: 'var(--font-body)',
|
|
211
|
+
fontSize: 18,
|
|
212
|
+
fontStyle: 'italic',
|
|
213
|
+
color: 'var(--status-stale)',
|
|
214
|
+
}}>
|
|
215
|
+
{error}
|
|
216
|
+
</div>
|
|
217
|
+
<button
|
|
218
|
+
type="button"
|
|
219
|
+
onClick={() => handleAction('refresh')}
|
|
220
|
+
style={{
|
|
221
|
+
fontFamily: 'var(--font-mono)',
|
|
222
|
+
fontSize: 11,
|
|
223
|
+
background: 'var(--surface)',
|
|
224
|
+
border: '1px solid var(--border-bright)',
|
|
225
|
+
color: 'var(--text-dim)',
|
|
226
|
+
padding: '8px 16px',
|
|
227
|
+
cursor: 'pointer',
|
|
228
|
+
letterSpacing: '0.04em',
|
|
229
|
+
}}
|
|
230
|
+
>
|
|
231
|
+
Retry
|
|
232
|
+
</button>
|
|
233
|
+
</div>
|
|
234
|
+
)}
|
|
235
|
+
|
|
236
|
+
{model && !error && (
|
|
237
|
+
<SkillGraph
|
|
238
|
+
model={model}
|
|
239
|
+
selectedId={selectedId}
|
|
240
|
+
onSelect={handleGraphClick}
|
|
241
|
+
onHover={handleHover}
|
|
242
|
+
onHoverEnd={handleHoverEnd}
|
|
243
|
+
labelsVisible={labelsVisible}
|
|
244
|
+
knowledgeVisible={knowledgeVisible}
|
|
245
|
+
resetZoomSignal={resetZoomSignal}
|
|
246
|
+
/>
|
|
247
|
+
)}
|
|
248
|
+
|
|
249
|
+
{model && model.nodes.length === 1 && model.nodes[0].type === 'skill' && (
|
|
250
|
+
<div style={{
|
|
251
|
+
position: 'absolute',
|
|
252
|
+
bottom: 80,
|
|
253
|
+
left: '50%',
|
|
254
|
+
transform: 'translateX(-50%)',
|
|
255
|
+
fontFamily: 'var(--font-body)',
|
|
256
|
+
fontSize: 14,
|
|
257
|
+
fontStyle: 'italic',
|
|
258
|
+
color: 'var(--text-faint)',
|
|
259
|
+
}}>
|
|
260
|
+
No dependencies or sources found.
|
|
261
|
+
</div>
|
|
262
|
+
)}
|
|
263
|
+
</div>
|
|
264
|
+
|
|
265
|
+
{/* Tooltip */}
|
|
266
|
+
<Tooltip node={tooltipNode} position={tooltipPos} />
|
|
267
|
+
|
|
268
|
+
{/* Inspector Panel */}
|
|
269
|
+
<InspectorPanel
|
|
270
|
+
node={inspectedNode}
|
|
271
|
+
onClose={() => setSelectedId(null)}
|
|
272
|
+
onNavigate={navigateToSkill}
|
|
273
|
+
/>
|
|
274
|
+
|
|
275
|
+
{/* Control Strip */}
|
|
276
|
+
<ControlStrip
|
|
277
|
+
onAction={handleAction}
|
|
278
|
+
busyAction={busyAction}
|
|
279
|
+
labelsVisible={labelsVisible}
|
|
280
|
+
onToggleLabels={() => setLabelsVisible((v) => !v)}
|
|
281
|
+
knowledgeVisible={knowledgeVisible}
|
|
282
|
+
onToggleKnowledge={() => setKnowledgeVisible((v) => !v)}
|
|
283
|
+
lightMode={lightMode}
|
|
284
|
+
onToggleTheme={() => setLightMode((v) => !v)}
|
|
285
|
+
/>
|
|
286
|
+
</>
|
|
287
|
+
);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function LegendItem({ color, label, shape }) {
|
|
291
|
+
return (
|
|
292
|
+
<div style={{
|
|
293
|
+
display: 'flex',
|
|
294
|
+
alignItems: 'center',
|
|
295
|
+
gap: 6,
|
|
296
|
+
fontFamily: 'var(--font-mono)',
|
|
297
|
+
fontSize: 10,
|
|
298
|
+
color: 'var(--text-dim)',
|
|
299
|
+
letterSpacing: '0.04em',
|
|
300
|
+
}}>
|
|
301
|
+
{shape === 'dot' && (
|
|
302
|
+
<div style={{
|
|
303
|
+
width: 10,
|
|
304
|
+
height: 10,
|
|
305
|
+
borderRadius: '50%',
|
|
306
|
+
background: color,
|
|
307
|
+
}} />
|
|
308
|
+
)}
|
|
309
|
+
{shape === 'ring' && (
|
|
310
|
+
<div style={{
|
|
311
|
+
width: 10,
|
|
312
|
+
height: 10,
|
|
313
|
+
borderRadius: '50%',
|
|
314
|
+
background: 'transparent',
|
|
315
|
+
border: `1.5px solid ${color}`,
|
|
316
|
+
}} />
|
|
317
|
+
)}
|
|
318
|
+
{shape === 'diamond' && (
|
|
319
|
+
<svg width="12" height="12" viewBox="-6 -6 12 12" style={{ display: 'block' }}>
|
|
320
|
+
<path d="M0,-5 L5,0 L0,5 L-5,0 Z" fill={color} opacity={0.6} />
|
|
321
|
+
</svg>
|
|
322
|
+
)}
|
|
323
|
+
{shape === 'line' && (
|
|
324
|
+
<div style={{
|
|
325
|
+
width: 20,
|
|
326
|
+
height: 2,
|
|
327
|
+
borderRadius: 1,
|
|
328
|
+
background: color,
|
|
329
|
+
opacity: 0.6,
|
|
330
|
+
}} />
|
|
331
|
+
)}
|
|
332
|
+
{shape === 'dashed' && (
|
|
333
|
+
<div style={{
|
|
334
|
+
width: 20,
|
|
335
|
+
height: 0,
|
|
336
|
+
borderTop: `2px dashed ${color}`,
|
|
337
|
+
opacity: 0.5,
|
|
338
|
+
}} />
|
|
339
|
+
)}
|
|
340
|
+
{label}
|
|
341
|
+
</div>
|
|
342
|
+
);
|
|
343
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
export function Breadcrumbs({ trail, onNavigate }) {
|
|
2
|
+
if (!trail || trail.length === 0) return null;
|
|
3
|
+
|
|
4
|
+
return (
|
|
5
|
+
<nav style={{
|
|
6
|
+
display: 'flex',
|
|
7
|
+
alignItems: 'center',
|
|
8
|
+
gap: 6,
|
|
9
|
+
fontFamily: 'var(--font-mono)',
|
|
10
|
+
fontSize: 11,
|
|
11
|
+
letterSpacing: '0.02em',
|
|
12
|
+
color: 'var(--text-dim)',
|
|
13
|
+
padding: '0 40px',
|
|
14
|
+
height: 32,
|
|
15
|
+
borderBottom: '1px solid var(--border)',
|
|
16
|
+
flexShrink: 0,
|
|
17
|
+
}}>
|
|
18
|
+
{trail.map((entry, index) => {
|
|
19
|
+
const isLast = index === trail.length - 1;
|
|
20
|
+
return (
|
|
21
|
+
<span key={entry.packageName} style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
|
|
22
|
+
{index > 0 && <span style={{ color: 'var(--text-faint)' }}>›</span>}
|
|
23
|
+
{isLast ? (
|
|
24
|
+
<span style={{ color: 'var(--text)' }}>
|
|
25
|
+
{entry.name || entry.packageName}
|
|
26
|
+
</span>
|
|
27
|
+
) : (
|
|
28
|
+
<span
|
|
29
|
+
onClick={() => onNavigate(entry.packageName, index)}
|
|
30
|
+
style={{
|
|
31
|
+
cursor: 'pointer',
|
|
32
|
+
transition: 'color 200ms ease',
|
|
33
|
+
}}
|
|
34
|
+
onMouseEnter={(e) => { e.target.style.color = 'var(--text)'; }}
|
|
35
|
+
onMouseLeave={(e) => { e.target.style.color = 'var(--text-dim)'; }}
|
|
36
|
+
>
|
|
37
|
+
{entry.name || entry.packageName}
|
|
38
|
+
</span>
|
|
39
|
+
)}
|
|
40
|
+
</span>
|
|
41
|
+
);
|
|
42
|
+
})}
|
|
43
|
+
</nav>
|
|
44
|
+
);
|
|
45
|
+
}
|