@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.
Files changed (43) hide show
  1. package/README.md +11 -1
  2. package/bin/intent.js +20 -0
  3. package/package.json +11 -5
  4. package/skills/agentpack-cli/SKILL.md +4 -1
  5. package/skills/authoring-skillgraphs-from-knowledge/SKILL.md +148 -0
  6. package/skills/authoring-skillgraphs-from-knowledge/references/authored-metadata.md +6 -0
  7. package/skills/developing-and-testing-skills/SKILL.md +109 -0
  8. package/skills/developing-and-testing-skills/references/local-workbench.md +7 -0
  9. package/skills/getting-started-skillgraphs/SKILL.md +115 -0
  10. package/skills/getting-started-skillgraphs/references/command-routing.md +7 -0
  11. package/skills/identifying-skill-opportunities/SKILL.md +119 -0
  12. package/skills/identifying-skill-opportunities/references/capability-boundaries.md +6 -0
  13. package/skills/maintaining-skillgraph-freshness/SKILL.md +110 -0
  14. package/skills/repairing-broken-skill-or-plugin-state/SKILL.md +112 -0
  15. package/skills/repairing-broken-skill-or-plugin-state/references/diagnostic-flows.md +6 -0
  16. package/skills/shipping-production-plugins-and-packages/SKILL.md +123 -0
  17. package/skills/shipping-production-plugins-and-packages/references/plugin-delivery.md +6 -0
  18. package/src/application/skills/build-skill-workbench-model.js +194 -0
  19. package/src/application/skills/run-skill-workbench-action.js +23 -0
  20. package/src/application/skills/start-skill-dev-workbench.js +192 -0
  21. package/src/cli.js +1 -1
  22. package/src/commands/skills.js +7 -1
  23. package/src/dashboard/App.jsx +343 -0
  24. package/src/dashboard/components/Breadcrumbs.jsx +45 -0
  25. package/src/dashboard/components/ControlStrip.jsx +153 -0
  26. package/src/dashboard/components/InspectorPanel.jsx +203 -0
  27. package/src/dashboard/components/SkillGraph.jsx +567 -0
  28. package/src/dashboard/components/Tooltip.jsx +111 -0
  29. package/src/dashboard/dist/dashboard.js +26692 -0
  30. package/src/dashboard/index.html +81 -0
  31. package/src/dashboard/lib/api.js +19 -0
  32. package/src/dashboard/lib/router.js +15 -0
  33. package/src/dashboard/main.jsx +4 -0
  34. package/src/domain/plugins/load-plugin-definition.js +163 -0
  35. package/src/domain/plugins/plugin-diagnostic-error.js +18 -0
  36. package/src/domain/plugins/plugin-requirements.js +15 -0
  37. package/src/domain/skills/skill-graph.js +1 -0
  38. package/src/infrastructure/runtime/open-browser.js +20 -0
  39. package/src/infrastructure/runtime/skill-dev-workbench-server.js +96 -0
  40. package/src/infrastructure/runtime/watch-skill-workbench.js +68 -0
  41. package/src/lib/plugins.js +19 -28
  42. package/src/lib/skills.js +60 -12
  43. 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({ error: err.code, message: err.message });
71
+ output.json(err.toJSON());
72
72
  } else {
73
73
  output.error(formatError(err));
74
74
  }
@@ -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
+ }