skillmux 0.1.1 → 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.
@@ -0,0 +1,1790 @@
1
+ import {
2
+ ManifestValidationError,
3
+ buildEmptyManifest,
4
+ collectDoctorIssues,
5
+ dedupeAndSortIssues,
6
+ discoverAgents,
7
+ formatValidationIssues,
8
+ isPathInside,
9
+ manifestSchema,
10
+ resolveSkillmuxHome,
11
+ runAdopt,
12
+ runDisable,
13
+ runEnable,
14
+ runRemove,
15
+ runScan,
16
+ scanAgentSkills
17
+ } from "./chunk-DBEVDI27.js";
18
+
19
+ // src/tui/launch-tui.tsx
20
+ import { render } from "ink";
21
+
22
+ // src/tui/app.tsx
23
+ import { readFileSync } from "fs";
24
+ import { Text as Text9, useApp, useInput } from "ink";
25
+ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
26
+
27
+ // src/tui/load-dashboard-state.ts
28
+ import { homedir } from "os";
29
+
30
+ // src/manifest/read-manifest-snapshot.ts
31
+ import * as fs from "fs/promises";
32
+ import { join, resolve } from "path";
33
+ function normalizeHomePath(home) {
34
+ const resolvedHome = resolve(home);
35
+ return process.platform === "win32" ? resolvedHome.toLowerCase() : resolvedHome;
36
+ }
37
+ async function readManifestSnapshot(home) {
38
+ const manifestPath = join(home, "manifest.json");
39
+ try {
40
+ const contents = await fs.readFile(manifestPath, "utf8");
41
+ const parsed = manifestSchema.safeParse(JSON.parse(contents));
42
+ if (!parsed.success) {
43
+ throw new ManifestValidationError(
44
+ `Invalid manifest at ${manifestPath}: ${formatValidationIssues(parsed.error)}`
45
+ );
46
+ }
47
+ if (normalizeHomePath(parsed.data.skillmuxHome) !== normalizeHomePath(home)) {
48
+ throw new ManifestValidationError(
49
+ `Invalid manifest at ${manifestPath}: skillmuxHome must match ${home}`
50
+ );
51
+ }
52
+ return { manifest: parsed.data, exists: true };
53
+ } catch (error) {
54
+ if (typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT") {
55
+ return { manifest: buildEmptyManifest(home), exists: false };
56
+ }
57
+ if (error instanceof SyntaxError) {
58
+ throw new ManifestValidationError(
59
+ `Invalid manifest at ${manifestPath}: malformed JSON`
60
+ );
61
+ }
62
+ throw error;
63
+ }
64
+ }
65
+
66
+ // src/tui/dashboard-model.ts
67
+ var emptyCounts = () => ({
68
+ enabledCount: 0,
69
+ disabledCount: 0,
70
+ unmanagedCount: 0,
71
+ issueCount: 0
72
+ });
73
+ function sortById(values) {
74
+ return [...values].sort((left, right) => left.id.localeCompare(right.id));
75
+ }
76
+ function getSelectedAgentId(agents, requestedAgentId) {
77
+ if (requestedAgentId !== void 0) {
78
+ const requestedAgent = agents.find((agent) => agent.id === requestedAgentId);
79
+ if (requestedAgent !== void 0) {
80
+ return requestedAgent.id;
81
+ }
82
+ }
83
+ const availableAgent = agents.find(
84
+ (agent) => agent.exists && agent.supportedOnPlatform
85
+ );
86
+ return availableAgent?.id ?? agents[0]?.id ?? null;
87
+ }
88
+ function findEnabledActivation(manifest, skillId, agentId) {
89
+ return manifest.activations.find(
90
+ (activation) => activation.skillId === skillId && activation.agentId === agentId && activation.state === "enabled"
91
+ );
92
+ }
93
+ function findAnyActivation(manifest, skillId, agentId) {
94
+ return manifest.activations.find(
95
+ (activation) => activation.skillId === skillId && activation.agentId === agentId
96
+ );
97
+ }
98
+ function buildManagedSkillRow(manifest, skill, agentId) {
99
+ const enabledActivation = findEnabledActivation(manifest, skill.id, agentId);
100
+ if (enabledActivation !== void 0) {
101
+ return {
102
+ id: skill.id,
103
+ kind: "enabled",
104
+ marker: "\u25CF",
105
+ skillId: skill.id,
106
+ name: skill.name,
107
+ path: skill.path,
108
+ agentId,
109
+ activationLinkPath: enabledActivation.linkPath
110
+ };
111
+ }
112
+ return {
113
+ id: skill.id,
114
+ kind: "disabled",
115
+ marker: "\u25CB",
116
+ skillId: skill.id,
117
+ name: skill.name,
118
+ path: skill.path,
119
+ agentId,
120
+ activationLinkPath: findAnyActivation(manifest, skill.id, agentId)?.linkPath ?? null
121
+ };
122
+ }
123
+ function isAdoptableEntry(entry) {
124
+ return entry.kind === "unmanaged-directory" || entry.kind === "unmanaged-link";
125
+ }
126
+ function buildUnmanagedRows(entries, agentId) {
127
+ return entries.filter((entry) => entry.agentId === agentId).filter(isAdoptableEntry).sort(
128
+ (left, right) => `${left.skillName}:${left.path}`.localeCompare(
129
+ `${right.skillName}:${right.path}`
130
+ )
131
+ ).map((entry) => ({
132
+ id: `unmanaged:${entry.skillName}`,
133
+ kind: "unmanaged",
134
+ marker: "?",
135
+ skillName: entry.skillName,
136
+ name: entry.skillName,
137
+ path: entry.path,
138
+ agentId,
139
+ entryKind: entry.kind,
140
+ targetPath: entry.targetPath
141
+ }));
142
+ }
143
+ function relatedAgentIdsForIssue(issue, agents) {
144
+ if (issue.path === void 0) {
145
+ return [];
146
+ }
147
+ const issuePath = issue.path;
148
+ return agents.filter((agent) => isPathInside(agent.absoluteSkillsDirectoryPath, issuePath)).map((agent) => agent.id);
149
+ }
150
+ function buildIssueId(issue, agentId) {
151
+ return `issue:${agentId}:${issue.code}:${issue.path ?? issue.message}`;
152
+ }
153
+ function buildIssueRows(issues, agents, agentId) {
154
+ return issues.filter((issue) => {
155
+ const relatedAgentIds = relatedAgentIdsForIssue(issue, agents);
156
+ return relatedAgentIds.includes(agentId);
157
+ }).map((issue) => ({
158
+ id: buildIssueId(issue, agentId),
159
+ kind: "issue",
160
+ marker: "!",
161
+ issueCode: issue.code,
162
+ severity: issue.severity,
163
+ message: issue.message,
164
+ path: issue.path ?? null,
165
+ agentId
166
+ }));
167
+ }
168
+ function buildSkillRowsForAgent(input, agentId) {
169
+ const managedRows = sortById(Object.values(input.manifest.skills)).map(
170
+ (skill) => buildManagedSkillRow(input.manifest, skill, agentId)
171
+ );
172
+ const unmanagedRows = buildUnmanagedRows(input.entries, agentId);
173
+ const issueRows = buildIssueRows(input.issues, input.agents, agentId);
174
+ return [...managedRows, ...unmanagedRows, ...issueRows];
175
+ }
176
+ function countsForRows(rows) {
177
+ const counts = emptyCounts();
178
+ for (const row of rows) {
179
+ if (row.kind === "enabled") {
180
+ counts.enabledCount += 1;
181
+ } else if (row.kind === "disabled") {
182
+ counts.disabledCount += 1;
183
+ } else if (row.kind === "unmanaged") {
184
+ counts.unmanagedCount += 1;
185
+ } else {
186
+ counts.issueCount += 1;
187
+ }
188
+ }
189
+ return counts;
190
+ }
191
+ function countActivationsForAgent(manifest, agentId) {
192
+ return manifest.activations.filter((activation) => activation.agentId === agentId).length;
193
+ }
194
+ function buildAgentRows(input) {
195
+ return sortById(input.agents).map((agent) => {
196
+ const counts = countsForRows(buildSkillRowsForAgent(input, agent.id));
197
+ return {
198
+ id: agent.id,
199
+ name: agent.stableName,
200
+ path: agent.absoluteSkillsDirectoryPath,
201
+ discovery: agent.discovery,
202
+ exists: agent.exists,
203
+ supported: agent.supportedOnPlatform,
204
+ activationCount: countActivationsForAgent(input.manifest, agent.id),
205
+ ...counts
206
+ };
207
+ });
208
+ }
209
+ function buildDashboardModel(input) {
210
+ const sortedAgents = sortById(input.agents);
211
+ const selectedAgentId = getSelectedAgentId(
212
+ sortedAgents,
213
+ input.selectedAgentId
214
+ );
215
+ const skills = selectedAgentId === null ? [] : buildSkillRowsForAgent({ ...input, agents: sortedAgents }, selectedAgentId);
216
+ const selectedSkillId = skills.some((row) => row.id === input.selectedSkillId) ? input.selectedSkillId : skills[0]?.id ?? null;
217
+ return {
218
+ agents: buildAgentRows({ ...input, agents: sortedAgents }),
219
+ skills,
220
+ selectedAgentId,
221
+ selectedSkillId,
222
+ lastScanAt: input.manifest.lastScan.at,
223
+ issueCount: input.issues.length
224
+ };
225
+ }
226
+
227
+ // src/tui/load-dashboard-state.ts
228
+ async function loadDashboardState(options = {}) {
229
+ const homeDir = options.homeDir ?? homedir();
230
+ const resolvedSkillmuxHome = resolveSkillmuxHome(homeDir).skillmuxHome;
231
+ const skillmuxHome = options.skillmuxHome ?? resolvedSkillmuxHome;
232
+ const { manifest } = await readManifestSnapshot(skillmuxHome);
233
+ const agents = await discoverAgents({
234
+ homeDir,
235
+ platform: options.platform,
236
+ skillmuxHome
237
+ });
238
+ const scanResults = [];
239
+ for (const agent of agents) {
240
+ scanResults.push(await scanAgentSkills(agent, skillmuxHome));
241
+ }
242
+ const entries = scanResults.flatMap((result) => result.entries);
243
+ const scanIssues = scanResults.flatMap((result) => result.issues);
244
+ const doctorIssues = await collectDoctorIssues({ manifest, agents, entries });
245
+ const issues = dedupeAndSortIssues([...scanIssues, ...doctorIssues]);
246
+ return buildDashboardModel({
247
+ manifest,
248
+ agents,
249
+ entries,
250
+ issues,
251
+ selectedAgentId: options.selectedAgentId,
252
+ selectedSkillId: options.selectedSkillId
253
+ });
254
+ }
255
+
256
+ // src/tui/actions.ts
257
+ var defaultServices = {
258
+ runEnable,
259
+ runDisable,
260
+ runAdopt,
261
+ runRemove,
262
+ runScan,
263
+ reload: loadDashboardState
264
+ };
265
+ function stripTrailingNewlines(output) {
266
+ return output.replace(/[\r\n]+$/u, "");
267
+ }
268
+ function actionLabel(action) {
269
+ return action.charAt(0).toUpperCase() + action.slice(1);
270
+ }
271
+ function errorReason(error) {
272
+ const message = error instanceof Error ? error.message : String(error);
273
+ const firstLine = message.split(/\r?\n/u)[0]?.trim();
274
+ return firstLine === void 0 || firstLine.length === 0 ? "Unknown error" : firstLine;
275
+ }
276
+ function reloadOptions(input) {
277
+ return {
278
+ homeDir: input.homeDir,
279
+ skillmuxHome: input.skillmuxHome,
280
+ platform: input.platform,
281
+ selectedAgentId: input.model.selectedAgentId ?? void 0,
282
+ selectedSkillId: input.model.selectedSkillId ?? void 0
283
+ };
284
+ }
285
+ async function reloadAfterCommand(input, services, output) {
286
+ return {
287
+ model: await services.reload(reloadOptions(input)),
288
+ statusMessage: stripTrailingNewlines(output)
289
+ };
290
+ }
291
+ function refusal(model, statusMessage) {
292
+ return { model, statusMessage };
293
+ }
294
+ function resolveSelectedSkill(model) {
295
+ if (model.selectedSkillId === null) {
296
+ return null;
297
+ }
298
+ return model.skills.find((row) => row.id === model.selectedSkillId) ?? null;
299
+ }
300
+ function resolveSelectedAgent(model) {
301
+ if (model.selectedAgentId === null) {
302
+ return null;
303
+ }
304
+ return model.agents.find((row) => row.id === model.selectedAgentId) ?? null;
305
+ }
306
+ async function dispatchTuiAction(input) {
307
+ const services = { ...defaultServices, ...input.services };
308
+ try {
309
+ if (input.action === "scan") {
310
+ const result2 = await services.runScan({
311
+ homeDir: input.homeDir,
312
+ skillmuxHome: input.skillmuxHome,
313
+ platform: input.platform
314
+ });
315
+ return reloadAfterCommand(input, services, result2.output);
316
+ }
317
+ if (input.action === "adopt-all") {
318
+ const selectedAgent = resolveSelectedAgent(input.model);
319
+ if (selectedAgent === null) {
320
+ return refusal(input.model, "Select an agent first");
321
+ }
322
+ if (selectedAgent.unmanagedCount <= 0) {
323
+ return refusal(input.model, "No unmanaged skills to adopt for this agent");
324
+ }
325
+ const result2 = await services.runAdopt({
326
+ homeDir: input.homeDir,
327
+ skillmuxHome: input.skillmuxHome,
328
+ agent: selectedAgent.id
329
+ });
330
+ return reloadAfterCommand(input, services, result2.output);
331
+ }
332
+ const selectedSkill = resolveSelectedSkill(input.model);
333
+ if (selectedSkill === null) {
334
+ return refusal(input.model, "Select a skill first");
335
+ }
336
+ if (input.action === "toggle") {
337
+ if (selectedSkill.kind === "enabled") {
338
+ const result2 = await services.runDisable({
339
+ homeDir: input.homeDir,
340
+ skillmuxHome: input.skillmuxHome,
341
+ skill: selectedSkill.skillId,
342
+ agent: selectedSkill.agentId
343
+ });
344
+ return reloadAfterCommand(input, services, result2.output);
345
+ }
346
+ if (selectedSkill.kind === "disabled") {
347
+ const result2 = await services.runEnable({
348
+ homeDir: input.homeDir,
349
+ skillmuxHome: input.skillmuxHome,
350
+ skill: selectedSkill.skillId,
351
+ agent: selectedSkill.agentId
352
+ });
353
+ return reloadAfterCommand(input, services, result2.output);
354
+ }
355
+ return refusal(input.model, "Toggle is only available for managed rows");
356
+ }
357
+ if (input.action === "adopt") {
358
+ if (selectedSkill.kind !== "unmanaged") {
359
+ return refusal(input.model, "Adopt is only available for unmanaged rows");
360
+ }
361
+ const result2 = await services.runAdopt({
362
+ homeDir: input.homeDir,
363
+ skillmuxHome: input.skillmuxHome,
364
+ agent: selectedSkill.agentId,
365
+ skill: selectedSkill.skillName
366
+ });
367
+ return reloadAfterCommand(input, services, result2.output);
368
+ }
369
+ if (selectedSkill.kind === "enabled") {
370
+ return refusal(input.model, "Disable this skill before removing it");
371
+ }
372
+ if (selectedSkill.kind !== "disabled") {
373
+ return refusal(input.model, "Remove is only available for disabled rows");
374
+ }
375
+ const result = await services.runRemove({
376
+ homeDir: input.homeDir,
377
+ skillmuxHome: input.skillmuxHome,
378
+ skill: selectedSkill.skillId
379
+ });
380
+ return reloadAfterCommand(input, services, result.output);
381
+ } catch (error) {
382
+ return {
383
+ model: input.model,
384
+ statusMessage: `${actionLabel(input.action)} failed: ${errorReason(error)}`
385
+ };
386
+ }
387
+ }
388
+
389
+ // src/tui/components/Dashboard.tsx
390
+ import { Box as Box8, Text as Text8 } from "ink";
391
+
392
+ // src/tui/state.ts
393
+ var focusOrder = ["agents", "skills"];
394
+ function clampCursor(cursor, rowCount) {
395
+ if (rowCount <= 0) {
396
+ return 0;
397
+ }
398
+ return Math.min(Math.max(cursor, 0), rowCount - 1);
399
+ }
400
+ function normalizeQuery(query) {
401
+ return query.trim().toLocaleLowerCase();
402
+ }
403
+ function includesQuery(values, query) {
404
+ const normalizedQuery = normalizeQuery(query);
405
+ if (normalizedQuery.length === 0) {
406
+ return true;
407
+ }
408
+ return values.some(
409
+ (value) => (value ?? "").toLocaleLowerCase().includes(normalizedQuery)
410
+ );
411
+ }
412
+ function isRelevantAgentRow(row, selectedAgentId) {
413
+ return row.id === selectedAgentId || row.exists || (row.activationCount ?? 0) > 0 || row.enabledCount > 0 || row.unmanagedCount > 0 || row.issueCount > 0;
414
+ }
415
+ function skillMatchesQuery(row, query) {
416
+ if (row.kind === "issue") {
417
+ return includesQuery(
418
+ [row.id, row.issueCode, row.message, row.path, row.agentId],
419
+ query
420
+ );
421
+ }
422
+ if (row.kind === "unmanaged") {
423
+ return includesQuery(
424
+ [row.id, row.skillName, row.name, row.path, row.agentId],
425
+ query
426
+ );
427
+ }
428
+ return includesQuery([row.id, row.skillId, row.name, row.path, row.agentId], query);
429
+ }
430
+ function replaceModelSelection(state, selection) {
431
+ return {
432
+ ...state,
433
+ model: {
434
+ ...state.model,
435
+ ...selection
436
+ }
437
+ };
438
+ }
439
+ function clearTransientIntent(state) {
440
+ return {
441
+ ...state,
442
+ pendingAction: null,
443
+ statusMessage: null
444
+ };
445
+ }
446
+ function restoreSearchSelection(state) {
447
+ if (state.search === null) {
448
+ return state;
449
+ }
450
+ const previousSelection = state.search.previousSelection;
451
+ return {
452
+ ...state,
453
+ model: {
454
+ ...state.model,
455
+ selectedAgentId: previousSelection.selectedAgentId,
456
+ selectedSkillId: previousSelection.selectedSkillId
457
+ },
458
+ agentCursor: previousSelection.agentCursor,
459
+ skillCursor: previousSelection.skillCursor,
460
+ pendingAgentId: previousSelection.pendingAgentId
461
+ };
462
+ }
463
+ function searchHasVisibleResults(state) {
464
+ if (state.search === null) {
465
+ return false;
466
+ }
467
+ return state.search.panel === "agents" ? getVisibleAgents(state).length > 0 : getVisibleSkills(state).length > 0;
468
+ }
469
+ function selectedAgentIndex(model) {
470
+ if (model.selectedAgentId === null) {
471
+ return 0;
472
+ }
473
+ const index = model.agents.findIndex((row) => row.id === model.selectedAgentId);
474
+ return index < 0 ? 0 : index;
475
+ }
476
+ function selectedSkillIndex(model) {
477
+ if (model.selectedSkillId === null) {
478
+ return 0;
479
+ }
480
+ const index = model.skills.findIndex((row) => row.id === model.selectedSkillId);
481
+ return index < 0 ? 0 : index;
482
+ }
483
+ function selectedAgentSkill(skills, agentId) {
484
+ if (agentId === null) {
485
+ return null;
486
+ }
487
+ return skills.find((row) => row.agentId === agentId) ?? null;
488
+ }
489
+ function selectedAgentRow(state) {
490
+ if (state.model.selectedAgentId === null) {
491
+ return null;
492
+ }
493
+ return state.model.agents.find((row) => row.id === state.model.selectedAgentId) ?? null;
494
+ }
495
+ function moveFocus(focus, direction) {
496
+ const currentIndex = focusOrder.indexOf(focus);
497
+ const nextIndex = (currentIndex + direction + focusOrder.length) % focusOrder.length;
498
+ return focusOrder[nextIndex] ?? "agents";
499
+ }
500
+ function stateWithAgentCursor(state, cursor) {
501
+ const visibleAgents = getVisibleAgents(state);
502
+ const agentCursor = clampCursor(cursor, visibleAgents.length);
503
+ const selectedAgent = visibleAgents[agentCursor] ?? null;
504
+ const selectedSkill = selectedAgentSkill(state.model.skills, selectedAgent?.id ?? null);
505
+ const previousAgentId = state.model.selectedAgentId;
506
+ const selectedAgentId = selectedAgent?.id ?? null;
507
+ const model = {
508
+ ...state.model,
509
+ selectedAgentId,
510
+ selectedSkillId: selectedSkill?.id ?? null
511
+ };
512
+ const skillCursor = selectedSkill === null ? 0 : clampCursor(
513
+ getVisibleSkills({ ...state, model }).findIndex(
514
+ (row) => row.id === selectedSkill.id
515
+ ),
516
+ getVisibleSkills({ ...state, model }).length
517
+ );
518
+ return {
519
+ ...state,
520
+ model,
521
+ agentCursor,
522
+ skillCursor,
523
+ pendingAgentId: selectedAgentId !== null && selectedAgentId !== previousAgentId ? selectedAgentId : state.pendingAgentId
524
+ };
525
+ }
526
+ function stateWithSkillCursor(state, cursor) {
527
+ const visibleSkills = getVisibleSkills(state);
528
+ const skillCursor = clampCursor(cursor, visibleSkills.length);
529
+ const selectedSkill = visibleSkills[skillCursor] ?? null;
530
+ return replaceModelSelection(
531
+ {
532
+ ...state,
533
+ skillCursor
534
+ },
535
+ {
536
+ selectedAgentId: state.model.selectedAgentId,
537
+ selectedSkillId: selectedSkill?.id ?? null
538
+ }
539
+ );
540
+ }
541
+ function moveCursor(state, cursor) {
542
+ if (state.focus === "agents") {
543
+ return stateWithAgentCursor(state, cursor);
544
+ }
545
+ if (state.focus === "skills") {
546
+ return stateWithSkillCursor(state, cursor);
547
+ }
548
+ return state;
549
+ }
550
+ function isModalBackgroundEvent(event) {
551
+ return event.type === "focus-next" || event.type === "focus-previous" || event.type === "next-row" || event.type === "previous-row" || event.type === "first-row" || event.type === "last-row" || event.type === "open-search" || event.type === "search-query-changed" || event.type === "open-help" || event.type === "request-adopt" || event.type === "request-adopt-all" || event.type === "request-remove" || event.type === "request-toggle" || event.type === "request-scan" || event.type === "clear-pending-action";
552
+ }
553
+ function getVisibleAgents(state) {
554
+ if (state.search?.panel !== "agents") {
555
+ return state.model.agents.filter(
556
+ (row) => isRelevantAgentRow(row, state.model.selectedAgentId)
557
+ );
558
+ }
559
+ return state.model.agents.filter(
560
+ (row) => includesQuery([row.id, row.name, row.path, row.discovery], state.search?.query ?? "")
561
+ );
562
+ }
563
+ function getVisibleSkills(state) {
564
+ if (state.model.selectedAgentId === null) {
565
+ return [];
566
+ }
567
+ const agentSkills = state.model.skills.filter((row) => row.agentId === state.model.selectedAgentId);
568
+ if (state.search?.panel !== "skills") {
569
+ return agentSkills;
570
+ }
571
+ return agentSkills.filter((row) => skillMatchesQuery(row, state.search?.query ?? ""));
572
+ }
573
+ function getSelectedSkill(state) {
574
+ if (state.model.selectedSkillId === null) {
575
+ return null;
576
+ }
577
+ return state.model.skills.find((row) => row.id === state.model.selectedSkillId) ?? null;
578
+ }
579
+ function getAvailableActions(state) {
580
+ const selectedSkill = getSelectedSkill(state);
581
+ const selectedAgent = selectedAgentRow(state);
582
+ const canAcceptActions = state.modal === null && !state.busy;
583
+ const hasFocusedSkill = canAcceptActions && state.focus === "skills";
584
+ return {
585
+ toggle: hasFocusedSkill && (selectedSkill?.kind === "enabled" || selectedSkill?.kind === "disabled"),
586
+ adopt: hasFocusedSkill && selectedSkill?.kind === "unmanaged",
587
+ adoptAll: canAcceptActions && (selectedAgent?.unmanagedCount ?? 0) > 0,
588
+ remove: hasFocusedSkill && selectedSkill?.kind === "disabled",
589
+ scan: canAcceptActions,
590
+ help: canAcceptActions
591
+ };
592
+ }
593
+ function consumeActionIntent(state) {
594
+ return {
595
+ state: {
596
+ ...state,
597
+ pendingAction: null
598
+ },
599
+ action: state.pendingAction
600
+ };
601
+ }
602
+ function consumeAgentSelectionIntent(state) {
603
+ return {
604
+ state: {
605
+ ...state,
606
+ pendingAgentId: null
607
+ },
608
+ agentId: state.pendingAgentId
609
+ };
610
+ }
611
+ function createInitialTuiState(model) {
612
+ const state = {
613
+ model,
614
+ focus: "agents",
615
+ agentCursor: selectedAgentIndex(model),
616
+ skillCursor: selectedSkillIndex(model),
617
+ search: null,
618
+ statusMessage: null,
619
+ modal: null,
620
+ busy: false,
621
+ pendingAction: null,
622
+ pendingAgentId: null
623
+ };
624
+ return {
625
+ ...state,
626
+ agentCursor: clampCursor(state.agentCursor, getVisibleAgents(state).length),
627
+ skillCursor: clampCursor(state.skillCursor, getVisibleSkills(state).length)
628
+ };
629
+ }
630
+ function updateTuiState(state, event) {
631
+ if (state.modal !== null) {
632
+ if (event.type === "close") {
633
+ return {
634
+ ...state,
635
+ modal: null,
636
+ pendingAction: null
637
+ };
638
+ }
639
+ if (event.type === "set-busy") {
640
+ return {
641
+ ...state,
642
+ busy: event.busy
643
+ };
644
+ }
645
+ if (event.type === "set-status") {
646
+ return {
647
+ ...state,
648
+ statusMessage: event.message
649
+ };
650
+ }
651
+ if (isModalBackgroundEvent(event)) {
652
+ return state;
653
+ }
654
+ }
655
+ if (state.busy) {
656
+ if (event.type === "set-busy") {
657
+ return {
658
+ ...state,
659
+ busy: event.busy
660
+ };
661
+ }
662
+ if (event.type === "set-status") {
663
+ return {
664
+ ...state,
665
+ statusMessage: event.message
666
+ };
667
+ }
668
+ if (event.type === "request-toggle" || event.type === "request-adopt" || event.type === "request-remove" || event.type === "request-scan" || event.type === "open-help") {
669
+ return {
670
+ ...state,
671
+ pendingAction: null
672
+ };
673
+ }
674
+ }
675
+ const readyState = clearTransientIntent(state);
676
+ if (event.type === "focus-next") {
677
+ return {
678
+ ...readyState,
679
+ focus: moveFocus(state.focus, 1)
680
+ };
681
+ }
682
+ if (event.type === "focus-previous") {
683
+ return {
684
+ ...readyState,
685
+ focus: moveFocus(state.focus, -1)
686
+ };
687
+ }
688
+ if (event.type === "next-row") {
689
+ return moveCursor(
690
+ readyState,
691
+ state.focus === "agents" ? state.agentCursor + 1 : state.skillCursor + 1
692
+ );
693
+ }
694
+ if (event.type === "previous-row") {
695
+ return moveCursor(
696
+ readyState,
697
+ state.focus === "agents" ? state.agentCursor - 1 : state.skillCursor - 1
698
+ );
699
+ }
700
+ if (event.type === "first-row") {
701
+ return moveCursor(readyState, 0);
702
+ }
703
+ if (event.type === "last-row") {
704
+ const rowCount = state.focus === "agents" ? getVisibleAgents(state).length : getVisibleSkills(state).length;
705
+ return moveCursor(readyState, rowCount - 1);
706
+ }
707
+ if (event.type === "open-search") {
708
+ if (state.focus !== "agents" && state.focus !== "skills") {
709
+ return {
710
+ ...readyState,
711
+ statusMessage: "Search is available for agents and skills"
712
+ };
713
+ }
714
+ return {
715
+ ...readyState,
716
+ search: {
717
+ panel: state.focus,
718
+ query: "",
719
+ previousSelection: {
720
+ selectedAgentId: state.model.selectedAgentId,
721
+ selectedSkillId: state.model.selectedSkillId,
722
+ agentCursor: state.agentCursor,
723
+ skillCursor: state.skillCursor,
724
+ pendingAgentId: state.pendingAgentId
725
+ }
726
+ }
727
+ };
728
+ }
729
+ if (event.type === "search-query-changed") {
730
+ if (state.search === null) {
731
+ return readyState;
732
+ }
733
+ const searchedState = {
734
+ ...readyState,
735
+ search: {
736
+ ...state.search,
737
+ query: event.query
738
+ }
739
+ };
740
+ const rowCount = searchedState.search.panel === "agents" ? getVisibleAgents(searchedState).length : getVisibleSkills(searchedState).length;
741
+ return searchedState.search.panel === "agents" ? stateWithAgentCursor(searchedState, clampCursor(searchedState.agentCursor, rowCount)) : stateWithSkillCursor(searchedState, clampCursor(searchedState.skillCursor, rowCount));
742
+ }
743
+ if (event.type === "close") {
744
+ if (state.search !== null) {
745
+ return {
746
+ ...restoreSearchSelection(readyState),
747
+ search: null
748
+ };
749
+ }
750
+ if (state.modal !== null) {
751
+ return {
752
+ ...readyState,
753
+ modal: null
754
+ };
755
+ }
756
+ return readyState;
757
+ }
758
+ if (event.type === "submit-search") {
759
+ if (state.search !== null) {
760
+ if (!searchHasVisibleResults(state)) {
761
+ return {
762
+ ...restoreSearchSelection(readyState),
763
+ search: null
764
+ };
765
+ }
766
+ return {
767
+ ...readyState,
768
+ search: null
769
+ };
770
+ }
771
+ return readyState;
772
+ }
773
+ if (event.type === "open-help") {
774
+ return {
775
+ ...readyState,
776
+ modal: { kind: "help" }
777
+ };
778
+ }
779
+ if (event.type === "request-adopt") {
780
+ if (state.focus !== "skills") {
781
+ return readyState;
782
+ }
783
+ const selectedSkill = getSelectedSkill(state);
784
+ if (selectedSkill?.kind !== "unmanaged") {
785
+ return {
786
+ ...readyState,
787
+ statusMessage: "Adopt is only available for unmanaged rows"
788
+ };
789
+ }
790
+ return {
791
+ ...readyState,
792
+ modal: {
793
+ kind: "confirm-adopt",
794
+ skillId: selectedSkill.skillName,
795
+ agentId: selectedSkill.agentId
796
+ }
797
+ };
798
+ }
799
+ if (event.type === "request-adopt-all") {
800
+ const selectedAgent = selectedAgentRow(state);
801
+ if (selectedAgent === null) {
802
+ return {
803
+ ...readyState,
804
+ statusMessage: "Select an agent first"
805
+ };
806
+ }
807
+ if (selectedAgent.unmanagedCount <= 0) {
808
+ return {
809
+ ...readyState,
810
+ statusMessage: "No unmanaged skills to adopt for this agent"
811
+ };
812
+ }
813
+ return {
814
+ ...readyState,
815
+ modal: {
816
+ kind: "confirm-adopt-all",
817
+ agentId: selectedAgent.id,
818
+ unmanagedCount: selectedAgent.unmanagedCount
819
+ }
820
+ };
821
+ }
822
+ if (event.type === "request-remove") {
823
+ if (state.focus !== "skills") {
824
+ return readyState;
825
+ }
826
+ const selectedSkill = getSelectedSkill(state);
827
+ if (selectedSkill?.kind !== "disabled") {
828
+ return {
829
+ ...readyState,
830
+ statusMessage: selectedSkill?.kind === "enabled" ? "Disable this skill before removing it" : "Remove is only available for disabled rows"
831
+ };
832
+ }
833
+ return {
834
+ ...readyState,
835
+ modal: {
836
+ kind: "confirm-remove",
837
+ skillId: selectedSkill.skillId
838
+ }
839
+ };
840
+ }
841
+ if (event.type === "request-toggle") {
842
+ if (state.focus !== "skills") {
843
+ return readyState;
844
+ }
845
+ const selectedSkill = getSelectedSkill(state);
846
+ if (selectedSkill?.kind !== "enabled" && selectedSkill?.kind !== "disabled") {
847
+ return {
848
+ ...readyState,
849
+ statusMessage: "Toggle is only available for managed rows"
850
+ };
851
+ }
852
+ return {
853
+ ...readyState,
854
+ pendingAction: "toggle"
855
+ };
856
+ }
857
+ if (event.type === "request-scan") {
858
+ return {
859
+ ...readyState,
860
+ pendingAction: "scan"
861
+ };
862
+ }
863
+ if (event.type === "set-busy") {
864
+ return {
865
+ ...readyState,
866
+ busy: event.busy
867
+ };
868
+ }
869
+ if (event.type === "set-status") {
870
+ return {
871
+ ...readyState,
872
+ statusMessage: event.message
873
+ };
874
+ }
875
+ return readyState;
876
+ }
877
+
878
+ // src/tui/components/AgentList.tsx
879
+ import { Box, Text } from "ink";
880
+ import { jsx, jsxs } from "react/jsx-runtime";
881
+ function statusMarker(agent) {
882
+ if (!agent.supported) {
883
+ return "!";
884
+ }
885
+ if (!agent.exists) {
886
+ return "?";
887
+ }
888
+ return "*";
889
+ }
890
+ function statusColor(agent) {
891
+ if (!agent.supported) {
892
+ return "red";
893
+ }
894
+ if (!agent.exists || agent.issueCount > 0) {
895
+ return "yellow";
896
+ }
897
+ return "green";
898
+ }
899
+ function AgentList({
900
+ agents,
901
+ selectedAgentId,
902
+ focused,
903
+ searchQuery,
904
+ width = 24,
905
+ height = 18
906
+ }) {
907
+ const emptyMessage = searchQuery !== void 0 && searchQuery.trim().length > 0 ? "No matching agents" : "No agents found";
908
+ return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", width, height, children: [
909
+ /* @__PURE__ */ jsx(Text, { bold: true, color: focused ? "cyan" : void 0, children: "Agents" }),
910
+ agents.length === 0 ? /* @__PURE__ */ jsx(Text, { dimColor: true, children: emptyMessage }) : agents.map((agent) => {
911
+ const selected = agent.id === selectedAgentId;
912
+ const selectionPrefix = selected ? ">" : " ";
913
+ return /* @__PURE__ */ jsxs(Text, { inverse: selected, children: [
914
+ /* @__PURE__ */ jsx(Text, { color: statusColor(agent), children: statusMarker(agent) }),
915
+ /* @__PURE__ */ jsxs(Text, { children: [
916
+ selectionPrefix,
917
+ " ",
918
+ agent.name
919
+ ] })
920
+ ] }, agent.id);
921
+ })
922
+ ] });
923
+ }
924
+
925
+ // src/tui/components/ConfirmDialog.tsx
926
+ import { Box as Box2, Text as Text2 } from "ink";
927
+ import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
928
+ var confirmDialogHeight = 4;
929
+ function confirmationText(modal) {
930
+ if (modal.kind === "confirm-adopt") {
931
+ return `Adopt ${modal.skillId} for ${modal.agentId}?`;
932
+ }
933
+ if (modal.kind === "confirm-adopt-all") {
934
+ return `Adopt all unmanaged skills for ${modal.agentId}?`;
935
+ }
936
+ return `Remove ${modal.skillId} from SkillMux?`;
937
+ }
938
+ function confirmationDetails(modal) {
939
+ if (modal.kind !== "confirm-adopt-all") {
940
+ return null;
941
+ }
942
+ return `${modal.unmanagedCount} unmanaged skills will be moved under SkillMux management.`;
943
+ }
944
+ function ConfirmDialog({ modal }) {
945
+ return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", height: confirmDialogHeight, children: [
946
+ /* @__PURE__ */ jsx2(Text2, { bold: true, color: modal.kind === "confirm-remove" ? "yellow" : "cyan", children: "Confirm" }),
947
+ /* @__PURE__ */ jsx2(Text2, { children: confirmationText(modal) }),
948
+ confirmationDetails(modal) === null ? null : /* @__PURE__ */ jsx2(Text2, { children: confirmationDetails(modal) }),
949
+ /* @__PURE__ */ jsx2(Text2, { children: "[y] confirm [Esc] cancel" })
950
+ ] });
951
+ }
952
+
953
+ // src/tui/components/DetailPane.tsx
954
+ import { Box as Box3, Text as Text3 } from "ink";
955
+ import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
956
+ function compactPath(value, maxLength) {
957
+ if (value.length <= maxLength) {
958
+ return value;
959
+ }
960
+ const separator = value.includes("\\") ? "\\" : "/";
961
+ const parts = value.split(/[\\/]+/).filter((part) => part.length > 0);
962
+ let suffix = parts.at(-1) ?? value;
963
+ for (let index = parts.length - 2; index >= 0; index -= 1) {
964
+ const candidate = `${parts[index]}${separator}${suffix}`;
965
+ if (`...${separator}${candidate}`.length > maxLength) {
966
+ break;
967
+ }
968
+ suffix = candidate;
969
+ }
970
+ const shortened = `...${separator}${suffix}`;
971
+ if (shortened.length <= maxLength) {
972
+ return shortened;
973
+ }
974
+ if (maxLength <= 3) {
975
+ return ".".repeat(maxLength);
976
+ }
977
+ return `...${suffix.slice(-(maxLength - 3))}`;
978
+ }
979
+ function detailLines(skill) {
980
+ if (skill.kind === "enabled") {
981
+ return [
982
+ { label: "Name", value: skill.name, compact: false },
983
+ { label: "Status", value: "enabled", compact: false },
984
+ { label: "Store", value: skill.path, compact: true },
985
+ { label: "Link", value: skill.activationLinkPath, compact: true }
986
+ ];
987
+ }
988
+ if (skill.kind === "disabled") {
989
+ return [
990
+ { label: "Name", value: skill.name, compact: false },
991
+ { label: "Status", value: "disabled", compact: false },
992
+ { label: "Store", value: skill.path, compact: true },
993
+ {
994
+ label: "Link",
995
+ value: skill.activationLinkPath ?? "not linked",
996
+ compact: skill.activationLinkPath !== null
997
+ }
998
+ ];
999
+ }
1000
+ if (skill.kind === "unmanaged") {
1001
+ return [
1002
+ { label: "Name", value: skill.name, compact: false },
1003
+ { label: "Status", value: "unmanaged", compact: false },
1004
+ { label: "Entry", value: skill.entryKind, compact: false },
1005
+ { label: "Path", value: skill.path, compact: true }
1006
+ ];
1007
+ }
1008
+ return [
1009
+ { label: "Status", value: "issue", compact: false },
1010
+ { label: "Code", value: skill.issueCode, compact: false },
1011
+ { label: "Severity", value: skill.severity, compact: false },
1012
+ { label: "Message", value: skill.message, compact: false },
1013
+ { label: "Path", value: skill.path ?? "none", compact: skill.path !== null }
1014
+ ];
1015
+ }
1016
+ function DetailPane({
1017
+ selectedAgent,
1018
+ selectedSkill,
1019
+ focused: _focused,
1020
+ loadingAgentName = null,
1021
+ width = 28,
1022
+ height = 18
1023
+ }) {
1024
+ return /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", width, height, children: [
1025
+ /* @__PURE__ */ jsx3(Text3, { bold: true, children: "Detail" }),
1026
+ selectedAgent === null ? /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: "Select an agent" }) : /* @__PURE__ */ jsxs3(Text3, { dimColor: true, children: [
1027
+ "Agent: ",
1028
+ selectedAgent.name
1029
+ ] }),
1030
+ selectedSkill === null ? loadingAgentName !== null ? /* @__PURE__ */ jsxs3(Text3, { dimColor: true, children: [
1031
+ "Loading details for ",
1032
+ loadingAgentName,
1033
+ "..."
1034
+ ] }) : /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: "Select a skill row" }) : detailLines(selectedSkill).map(({ label, value, compact }) => {
1035
+ const valueWidth = Math.max(width - (label.length + 2), 8);
1036
+ const renderedValue = compact ? compactPath(value, valueWidth) : value;
1037
+ return /* @__PURE__ */ jsxs3(Text3, { children: [
1038
+ /* @__PURE__ */ jsxs3(Text3, { dimColor: true, children: [
1039
+ label,
1040
+ ": "
1041
+ ] }),
1042
+ /* @__PURE__ */ jsx3(Text3, { children: renderedValue })
1043
+ ] }, label);
1044
+ })
1045
+ ] });
1046
+ }
1047
+
1048
+ // src/tui/components/Footer.tsx
1049
+ import { Box as Box4, Text as Text4 } from "ink";
1050
+ import { Fragment, jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
1051
+ var agentLegend = "Agent icons: * ready yellow * issues ? missing ! unsupported";
1052
+ var skillLegend = "Skill markers: \u25CF enabled \u25CB disabled ? unmanaged ! issue";
1053
+ function Footer({ actions, search }) {
1054
+ const shortcuts = [
1055
+ "[Left/Right]focus",
1056
+ actions.toggle ? "[Space]toggle" : null,
1057
+ actions.adopt ? "[a]adopt" : null,
1058
+ actions.adoptAll ? "[Shift+A]adopt all" : null,
1059
+ actions.remove ? "[r]remove" : null,
1060
+ actions.scan ? "[s]scan" : null,
1061
+ actions.help ? "[?]help" : null,
1062
+ "[q]quit"
1063
+ ].filter((shortcut) => shortcut !== null);
1064
+ return /* @__PURE__ */ jsx4(Box4, { flexDirection: "column", height: 3, children: search === null ? /* @__PURE__ */ jsxs4(Fragment, { children: [
1065
+ /* @__PURE__ */ jsx4(Text4, { children: shortcuts.join(" ") }),
1066
+ /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: agentLegend }),
1067
+ /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: skillLegend })
1068
+ ] }) : /* @__PURE__ */ jsxs4(Text4, { children: [
1069
+ /* @__PURE__ */ jsx4(Text4, { color: "cyan", children: "/" }),
1070
+ /* @__PURE__ */ jsx4(Text4, { children: search.query }),
1071
+ /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: " [Enter]keep [Esc]cancel" })
1072
+ ] }) });
1073
+ }
1074
+
1075
+ // src/tui/components/HelpOverlay.tsx
1076
+ import { Box as Box5, Text as Text5 } from "ink";
1077
+ import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
1078
+ function HelpOverlay() {
1079
+ return /* @__PURE__ */ jsxs5(Box5, { flexDirection: "column", height: 8, children: [
1080
+ /* @__PURE__ */ jsx5(Text5, { bold: true, children: "Help" }),
1081
+ /* @__PURE__ */ jsxs5(Text5, { children: [
1082
+ /* @__PURE__ */ jsx5(Text5, { bold: true, children: "Navigation" }),
1083
+ /* @__PURE__ */ jsx5(Text5, { children: ": Left/Right switch panels, j/k or Up/Down move, g/G jump." })
1084
+ ] }),
1085
+ /* @__PURE__ */ jsxs5(Text5, { children: [
1086
+ /* @__PURE__ */ jsx5(Text5, { bold: true, children: "Actions" }),
1087
+ /* @__PURE__ */ jsx5(Text5, { children: ": Space toggles, a adopts, Shift+A current-agent bulk adopt, r removes, s scans." })
1088
+ ] }),
1089
+ /* @__PURE__ */ jsxs5(Text5, { children: [
1090
+ /* @__PURE__ */ jsx5(Text5, { bold: true, children: "Search" }),
1091
+ /* @__PURE__ */ jsx5(Text5, { children: ": / filters the focused list, Enter keeps the result, Esc cancels." })
1092
+ ] }),
1093
+ /* @__PURE__ */ jsxs5(Text5, { children: [
1094
+ /* @__PURE__ */ jsx5(Text5, { bold: true, children: "Agent icons" }),
1095
+ /* @__PURE__ */ jsx5(Text5, { children: ": * ready, yellow * issues, ? missing, ! unsupported." })
1096
+ ] }),
1097
+ /* @__PURE__ */ jsxs5(Text5, { children: [
1098
+ /* @__PURE__ */ jsx5(Text5, { bold: true, children: "Skill markers" }),
1099
+ /* @__PURE__ */ jsx5(Text5, { children: ": \u25CF enabled, \u25CB disabled, ? unmanaged, ! issue." })
1100
+ ] }),
1101
+ /* @__PURE__ */ jsxs5(Text5, { children: [
1102
+ /* @__PURE__ */ jsx5(Text5, { bold: true, children: "Safety" }),
1103
+ /* @__PURE__ */ jsx5(Text5, { children: ": Toggle, adopt, remove, and scan can update SkillMux state and agent links." })
1104
+ ] })
1105
+ ] });
1106
+ }
1107
+
1108
+ // src/tui/components/SkillList.tsx
1109
+ import { Box as Box6, Text as Text6 } from "ink";
1110
+ import { jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
1111
+ function markerColor(skill) {
1112
+ if (skill.kind === "enabled") {
1113
+ return "green";
1114
+ }
1115
+ if (skill.kind === "issue") {
1116
+ return skill.severity === "error" ? "red" : "yellow";
1117
+ }
1118
+ if (skill.kind === "unmanaged") {
1119
+ return "yellow";
1120
+ }
1121
+ return "gray";
1122
+ }
1123
+ function skillLabel(skill) {
1124
+ if (skill.kind === "issue") {
1125
+ return skill.issueCode;
1126
+ }
1127
+ return skill.name;
1128
+ }
1129
+ function SkillList({
1130
+ agentId,
1131
+ skills,
1132
+ selectedSkillId,
1133
+ focused,
1134
+ searchQuery,
1135
+ loadingAgentName = null,
1136
+ width = 28,
1137
+ height = 18
1138
+ }) {
1139
+ const emptyMessage = loadingAgentName !== null ? `Loading skills for ${loadingAgentName}...` : agentId === null ? "Select an agent" : searchQuery !== void 0 && searchQuery.trim().length > 0 ? "No matching skills" : "No skills for this agent";
1140
+ return /* @__PURE__ */ jsxs6(Box6, { flexDirection: "column", width, height, children: [
1141
+ /* @__PURE__ */ jsxs6(Text6, { bold: true, color: focused ? "cyan" : void 0, children: [
1142
+ "Skills for ",
1143
+ agentId ?? "none"
1144
+ ] }),
1145
+ skills.length === 0 ? /* @__PURE__ */ jsx6(Text6, { dimColor: true, children: emptyMessage }) : skills.map((skill) => {
1146
+ const selected = skill.id === selectedSkillId;
1147
+ return /* @__PURE__ */ jsxs6(Text6, { inverse: focused && selected, children: [
1148
+ /* @__PURE__ */ jsx6(Text6, { color: markerColor(skill), children: skill.marker }),
1149
+ /* @__PURE__ */ jsxs6(Text6, { children: [
1150
+ " ",
1151
+ skillLabel(skill)
1152
+ ] })
1153
+ ] }, skill.id);
1154
+ })
1155
+ ] });
1156
+ }
1157
+
1158
+ // src/tui/components/StatusLine.tsx
1159
+ import { Box as Box7, Text as Text7 } from "ink";
1160
+ import { jsx as jsx7 } from "react/jsx-runtime";
1161
+ function StatusLine({
1162
+ busy,
1163
+ statusMessage,
1164
+ lastScanAt,
1165
+ issueCount
1166
+ }) {
1167
+ const message = statusMessage ?? (busy ? "scanning..." : `Last scan: ${lastScanAt ?? "never"} | issues: ${issueCount}`);
1168
+ return /* @__PURE__ */ jsx7(Box7, { height: 1, children: /* @__PURE__ */ jsx7(Text7, { color: busy ? "cyan" : void 0, children: message }) });
1169
+ }
1170
+
1171
+ // src/tui/components/Dashboard.tsx
1172
+ import { jsx as jsx8, jsxs as jsxs7 } from "react/jsx-runtime";
1173
+ var minimumWidth = 80;
1174
+ var minimumHeight = 24;
1175
+ var agentRatio = 0.26;
1176
+ var skillRatio = 0.3;
1177
+ var detailRatio = 0.44;
1178
+ var agentMinimumWidth = 20;
1179
+ var skillMinimumWidth = 24;
1180
+ var detailMinimumWidth = 28;
1181
+ function paneWidths(width) {
1182
+ const agentWidth = Math.max(agentMinimumWidth, Math.round(width * agentRatio));
1183
+ const skillWidth = Math.max(skillMinimumWidth, Math.round(width * skillRatio));
1184
+ const detailWidth = Math.max(
1185
+ detailMinimumWidth,
1186
+ Math.round(width * detailRatio)
1187
+ );
1188
+ const widthDelta = width - (agentWidth + skillWidth + detailWidth);
1189
+ if (widthDelta === 0) {
1190
+ return { agentWidth, skillWidth, detailWidth };
1191
+ }
1192
+ return {
1193
+ agentWidth,
1194
+ skillWidth,
1195
+ detailWidth: detailWidth + widthDelta
1196
+ };
1197
+ }
1198
+ function Dashboard({
1199
+ state,
1200
+ width,
1201
+ height
1202
+ }) {
1203
+ if (width < minimumWidth || height < minimumHeight) {
1204
+ return /* @__PURE__ */ jsx8(
1205
+ Box8,
1206
+ {
1207
+ flexDirection: "column",
1208
+ width,
1209
+ height,
1210
+ justifyContent: "center",
1211
+ alignItems: "center",
1212
+ children: /* @__PURE__ */ jsx8(Text8, { children: "Terminal too small. Resize to at least 80x24." })
1213
+ }
1214
+ );
1215
+ }
1216
+ const visibleAgents = getVisibleAgents(state);
1217
+ const visibleSkills = getVisibleSkills(state);
1218
+ const selectedAgent = state.model.agents.find((agent) => agent.id === state.model.selectedAgentId) ?? null;
1219
+ const selectedSkill = getSelectedSkill(state);
1220
+ const loadingAgentName = state.pendingAgentId ?? (state.busy && state.statusMessage === "loading agent..." ? state.model.selectedAgentId : null);
1221
+ const loadingAgent = loadingAgentName === null ? null : state.model.agents.find((agent) => agent.id === loadingAgentName) ?? null;
1222
+ const actions = getAvailableActions(state);
1223
+ const footerHeight = 3;
1224
+ const overlayHeight = state.modal?.kind === "help" ? 8 : state.modal?.kind === "confirm-adopt" || state.modal?.kind === "confirm-adopt-all" || state.modal?.kind === "confirm-remove" ? confirmDialogHeight : 0;
1225
+ const bodyHeight = Math.max(height - 1 - footerHeight - overlayHeight, 0);
1226
+ const { agentWidth, skillWidth, detailWidth } = paneWidths(width);
1227
+ return /* @__PURE__ */ jsxs7(Box8, { flexDirection: "column", width, height, children: [
1228
+ /* @__PURE__ */ jsx8(
1229
+ StatusLine,
1230
+ {
1231
+ busy: state.busy,
1232
+ statusMessage: state.statusMessage,
1233
+ lastScanAt: state.model.lastScanAt,
1234
+ issueCount: state.model.issueCount
1235
+ }
1236
+ ),
1237
+ /* @__PURE__ */ jsxs7(Box8, { flexDirection: "row", width, height: bodyHeight, children: [
1238
+ /* @__PURE__ */ jsx8(
1239
+ AgentList,
1240
+ {
1241
+ agents: visibleAgents,
1242
+ selectedAgentId: state.model.selectedAgentId,
1243
+ focused: state.focus === "agents",
1244
+ searchQuery: state.search?.panel === "agents" ? state.search.query : void 0,
1245
+ width: agentWidth,
1246
+ height: bodyHeight
1247
+ }
1248
+ ),
1249
+ /* @__PURE__ */ jsx8(
1250
+ SkillList,
1251
+ {
1252
+ agentId: state.model.selectedAgentId,
1253
+ skills: visibleSkills,
1254
+ selectedSkillId: state.model.selectedSkillId,
1255
+ focused: state.focus === "skills",
1256
+ searchQuery: state.search?.panel === "skills" ? state.search.query : void 0,
1257
+ loadingAgentName: loadingAgent?.name ?? null,
1258
+ width: skillWidth,
1259
+ height: bodyHeight
1260
+ }
1261
+ ),
1262
+ /* @__PURE__ */ jsx8(
1263
+ DetailPane,
1264
+ {
1265
+ selectedAgent,
1266
+ selectedSkill,
1267
+ focused: state.focus === "detail",
1268
+ loadingAgentName: loadingAgent?.name ?? null,
1269
+ width: detailWidth,
1270
+ height: bodyHeight
1271
+ }
1272
+ )
1273
+ ] }),
1274
+ state.modal?.kind === "help" ? /* @__PURE__ */ jsx8(HelpOverlay, {}) : null,
1275
+ state.modal?.kind === "confirm-adopt" || state.modal?.kind === "confirm-adopt-all" || state.modal?.kind === "confirm-remove" ? /* @__PURE__ */ jsx8(ConfirmDialog, { modal: state.modal }) : null,
1276
+ state.modal === null ? /* @__PURE__ */ jsx8(Footer, { actions, search: state.search }) : /* @__PURE__ */ jsx8(Box8, { height: 3 })
1277
+ ] });
1278
+ }
1279
+
1280
+ // src/tui/app.tsx
1281
+ import { jsx as jsx9 } from "react/jsx-runtime";
1282
+ var defaultServices2 = {
1283
+ loadDashboardState,
1284
+ dispatchTuiAction
1285
+ };
1286
+ function errorReason2(error) {
1287
+ const message = error instanceof Error ? error.message : String(error);
1288
+ const firstLine = message.split(/\r?\n/u)[0]?.trim();
1289
+ return firstLine === void 0 || firstLine.length === 0 ? "Unknown error" : firstLine;
1290
+ }
1291
+ function loadOptions(props, selectedAgentId, selectedSkillId) {
1292
+ return {
1293
+ homeDir: props.homeDir,
1294
+ skillmuxHome: props.skillmuxHome,
1295
+ platform: props.platform,
1296
+ selectedAgentId,
1297
+ selectedSkillId
1298
+ };
1299
+ }
1300
+ function replaceStateModel(previous, model, statusMessage) {
1301
+ const next = createInitialTuiState(model);
1302
+ return {
1303
+ ...next,
1304
+ focus: previous.focus,
1305
+ search: previous.search,
1306
+ statusMessage,
1307
+ modal: null,
1308
+ busy: false
1309
+ };
1310
+ }
1311
+ function isTextInput(input) {
1312
+ return input.length > 0 && !/[\u0000-\u001F\u007F]/u.test(input);
1313
+ }
1314
+ function parseBridgedSize(value) {
1315
+ try {
1316
+ const parsed = JSON.parse(value);
1317
+ if (typeof parsed.columns === "number" && typeof parsed.rows === "number" && Number.isFinite(parsed.columns) && Number.isFinite(parsed.rows)) {
1318
+ return {
1319
+ columns: parsed.columns,
1320
+ rows: parsed.rows
1321
+ };
1322
+ }
1323
+ } catch {
1324
+ return null;
1325
+ }
1326
+ return null;
1327
+ }
1328
+ function liveTerminalSize() {
1329
+ return {
1330
+ columns: process.stdout.columns ?? 80,
1331
+ rows: process.stdout.rows ?? 24
1332
+ };
1333
+ }
1334
+ function readSizeFile(path) {
1335
+ try {
1336
+ return parseBridgedSize(readFileSync(path, "utf8"));
1337
+ } catch {
1338
+ return null;
1339
+ }
1340
+ }
1341
+ function useBridgedTerminalSize(path) {
1342
+ const [size, setSize] = useState(
1343
+ () => path === null ? null : readSizeFile(path)
1344
+ );
1345
+ useEffect(() => {
1346
+ if (path === null) {
1347
+ setSize(null);
1348
+ return;
1349
+ }
1350
+ let cancelled = false;
1351
+ const refresh = () => {
1352
+ if (cancelled) {
1353
+ return;
1354
+ }
1355
+ const nextSize = readSizeFile(path);
1356
+ setSize(
1357
+ (current) => nextSize === null ? current : current !== null && current.columns === nextSize.columns && current.rows === nextSize.rows ? current : nextSize
1358
+ );
1359
+ };
1360
+ refresh();
1361
+ const timer = setInterval(refresh, 50);
1362
+ return () => {
1363
+ cancelled = true;
1364
+ clearInterval(timer);
1365
+ };
1366
+ }, [path]);
1367
+ return size;
1368
+ }
1369
+ function LiveDashboardViewport({
1370
+ state,
1371
+ terminalWidth,
1372
+ terminalHeight
1373
+ }) {
1374
+ return /* @__PURE__ */ jsx9(
1375
+ Dashboard,
1376
+ {
1377
+ state,
1378
+ width: terminalWidth ?? liveTerminalSize().columns,
1379
+ height: terminalHeight ?? liveTerminalSize().rows
1380
+ }
1381
+ );
1382
+ }
1383
+ function BridgedDashboardViewport({
1384
+ state,
1385
+ terminalWidth,
1386
+ terminalHeight,
1387
+ bridgePath
1388
+ }) {
1389
+ const bridgedTerminalSize = useBridgedTerminalSize(bridgePath ?? null);
1390
+ const fallbackSize = liveTerminalSize();
1391
+ return /* @__PURE__ */ jsx9(
1392
+ Dashboard,
1393
+ {
1394
+ state,
1395
+ width: terminalWidth ?? bridgedTerminalSize?.columns ?? fallbackSize.columns,
1396
+ height: terminalHeight ?? bridgedTerminalSize?.rows ?? fallbackSize.rows
1397
+ }
1398
+ );
1399
+ }
1400
+ function App({
1401
+ homeDir,
1402
+ skillmuxHome,
1403
+ platform,
1404
+ terminalWidth,
1405
+ terminalHeight,
1406
+ services: serviceOverrides
1407
+ }) {
1408
+ const { exit } = useApp();
1409
+ const [state, setState] = useState(null);
1410
+ const [loadError, setLoadError] = useState(null);
1411
+ const requestSequence = useRef(0);
1412
+ const latestRequest = useRef(0);
1413
+ const activeActionRequest = useRef(null);
1414
+ const stableModel = useRef(null);
1415
+ const services = useMemo(
1416
+ () => ({ ...defaultServices2, ...serviceOverrides }),
1417
+ [serviceOverrides]
1418
+ );
1419
+ const sizeBridgePath = process.env.SKILLMUX_TUI_PTY_SIZE_FILE?.trim() ?? null;
1420
+ const sizeBridgeEnabled = sizeBridgePath !== null && sizeBridgePath.length > 0;
1421
+ const beginRequest = useCallback(() => {
1422
+ requestSequence.current += 1;
1423
+ latestRequest.current = requestSequence.current;
1424
+ return requestSequence.current;
1425
+ }, []);
1426
+ const isLatestRequest = useCallback((requestId) => {
1427
+ return latestRequest.current === requestId;
1428
+ }, []);
1429
+ const startBusyState = useCallback(
1430
+ (baseState, action) => updateTuiState(
1431
+ updateTuiState(baseState, { type: "set-busy", busy: true }),
1432
+ {
1433
+ type: "set-status",
1434
+ message: action === "scan" ? "scanning..." : "working..."
1435
+ }
1436
+ ),
1437
+ []
1438
+ );
1439
+ useEffect(() => {
1440
+ let cancelled = false;
1441
+ services.loadDashboardState(loadOptions({ homeDir, skillmuxHome, platform })).then((model) => {
1442
+ if (!cancelled) {
1443
+ stableModel.current = model;
1444
+ setState(createInitialTuiState(model));
1445
+ setLoadError(null);
1446
+ }
1447
+ }).catch((error) => {
1448
+ if (!cancelled) {
1449
+ setLoadError(`Failed to load dashboard: ${errorReason2(error)}`);
1450
+ }
1451
+ });
1452
+ return () => {
1453
+ cancelled = true;
1454
+ };
1455
+ }, [homeDir, platform, services, skillmuxHome]);
1456
+ const runAction = useCallback(
1457
+ (action, model, baseState) => {
1458
+ if (activeActionRequest.current !== null) {
1459
+ return;
1460
+ }
1461
+ const requestId = beginRequest();
1462
+ activeActionRequest.current = requestId;
1463
+ setState(
1464
+ (current) => current === null ? current : startBusyState(baseState ?? current, action)
1465
+ );
1466
+ services.dispatchTuiAction({
1467
+ action,
1468
+ model,
1469
+ homeDir,
1470
+ skillmuxHome,
1471
+ platform
1472
+ }).then((result) => {
1473
+ if (!isLatestRequest(requestId)) {
1474
+ return;
1475
+ }
1476
+ stableModel.current = result.model;
1477
+ setState(
1478
+ (current) => current === null ? current : replaceStateModel(current, result.model, result.statusMessage)
1479
+ );
1480
+ }).catch((error) => {
1481
+ if (!isLatestRequest(requestId)) {
1482
+ return;
1483
+ }
1484
+ setState(
1485
+ (current) => current === null ? current : updateTuiState(
1486
+ updateTuiState(current, { type: "set-busy", busy: false }),
1487
+ {
1488
+ type: "set-status",
1489
+ message: `Action failed: ${errorReason2(error)}`
1490
+ }
1491
+ )
1492
+ );
1493
+ }).finally(() => {
1494
+ if (activeActionRequest.current === requestId) {
1495
+ activeActionRequest.current = null;
1496
+ }
1497
+ });
1498
+ },
1499
+ [
1500
+ beginRequest,
1501
+ homeDir,
1502
+ isLatestRequest,
1503
+ platform,
1504
+ services,
1505
+ skillmuxHome,
1506
+ startBusyState
1507
+ ]
1508
+ );
1509
+ const reloadAgent = useCallback(
1510
+ (agentId) => {
1511
+ const requestId = beginRequest();
1512
+ setState(
1513
+ (current) => current === null ? current : updateTuiState(
1514
+ updateTuiState(current, { type: "set-busy", busy: true }),
1515
+ { type: "set-status", message: "loading agent..." }
1516
+ )
1517
+ );
1518
+ services.loadDashboardState(loadOptions({ homeDir, skillmuxHome, platform }, agentId)).then((model) => {
1519
+ if (!isLatestRequest(requestId)) {
1520
+ return;
1521
+ }
1522
+ stableModel.current = model;
1523
+ setState(
1524
+ (current) => current === null ? current : replaceStateModel(current, model, null)
1525
+ );
1526
+ }).catch((error) => {
1527
+ if (!isLatestRequest(requestId)) {
1528
+ return;
1529
+ }
1530
+ setState(
1531
+ (current) => current === null ? current : stableModel.current === null ? updateTuiState(
1532
+ updateTuiState(current, { type: "set-busy", busy: false }),
1533
+ {
1534
+ type: "set-status",
1535
+ message: `Load failed: ${errorReason2(error)}`
1536
+ }
1537
+ ) : replaceStateModel(
1538
+ current,
1539
+ stableModel.current,
1540
+ `Load failed: ${errorReason2(error)}`
1541
+ )
1542
+ );
1543
+ });
1544
+ },
1545
+ [beginRequest, homeDir, isLatestRequest, platform, services, skillmuxHome]
1546
+ );
1547
+ useEffect(() => {
1548
+ if (state?.pendingAction === null || state?.pendingAction === void 0) {
1549
+ return;
1550
+ }
1551
+ const consumed = consumeActionIntent(state);
1552
+ setState(consumed.state);
1553
+ if (consumed.action !== null) {
1554
+ runAction(consumed.action, consumed.state.model);
1555
+ }
1556
+ }, [runAction, state]);
1557
+ useEffect(() => {
1558
+ if (state?.pendingAgentId === null || state?.pendingAgentId === void 0) {
1559
+ return;
1560
+ }
1561
+ const consumed = consumeAgentSelectionIntent(state);
1562
+ setState(consumed.state);
1563
+ if (consumed.agentId !== null) {
1564
+ reloadAgent(consumed.agentId);
1565
+ }
1566
+ }, [reloadAgent, state]);
1567
+ useInput((input, key) => {
1568
+ if (key.ctrl && input === "c") {
1569
+ exit();
1570
+ return;
1571
+ }
1572
+ if (state === null) {
1573
+ if (input === "q") {
1574
+ exit();
1575
+ }
1576
+ return;
1577
+ }
1578
+ if (state.search !== null) {
1579
+ if (key.escape) {
1580
+ setState(updateTuiState(state, { type: "close" }));
1581
+ return;
1582
+ }
1583
+ if (key.return) {
1584
+ setState(updateTuiState(state, { type: "submit-search" }));
1585
+ return;
1586
+ }
1587
+ if (key.backspace || key.delete) {
1588
+ setState(
1589
+ updateTuiState(state, {
1590
+ type: "search-query-changed",
1591
+ query: state.search.query.slice(0, -1)
1592
+ })
1593
+ );
1594
+ return;
1595
+ }
1596
+ if (!key.ctrl && !key.meta && isTextInput(input)) {
1597
+ setState(
1598
+ updateTuiState(state, {
1599
+ type: "search-query-changed",
1600
+ query: `${state.search.query}${input}`
1601
+ })
1602
+ );
1603
+ }
1604
+ return;
1605
+ }
1606
+ if (state.modal !== null) {
1607
+ if (key.escape) {
1608
+ setState(updateTuiState(state, { type: "close" }));
1609
+ return;
1610
+ }
1611
+ if (input === "q") {
1612
+ exit();
1613
+ return;
1614
+ }
1615
+ if (activeActionRequest.current !== null) {
1616
+ return;
1617
+ }
1618
+ if (input.toLocaleLowerCase() === "y" && state.modal.kind === "confirm-adopt") {
1619
+ const closedState = updateTuiState(state, { type: "close" });
1620
+ runAction("adopt", closedState.model, closedState);
1621
+ return;
1622
+ }
1623
+ if (input.toLocaleLowerCase() === "y" && state.modal.kind === "confirm-adopt-all") {
1624
+ const closedState = updateTuiState(state, { type: "close" });
1625
+ runAction("adopt-all", closedState.model, closedState);
1626
+ return;
1627
+ }
1628
+ if (input.toLocaleLowerCase() === "y" && state.modal.kind === "confirm-remove") {
1629
+ const closedState = updateTuiState(state, { type: "close" });
1630
+ runAction("remove", closedState.model, closedState);
1631
+ return;
1632
+ }
1633
+ return;
1634
+ }
1635
+ if (input === "q") {
1636
+ exit();
1637
+ return;
1638
+ }
1639
+ if (activeActionRequest.current !== null) {
1640
+ return;
1641
+ }
1642
+ if (key.rightArrow) {
1643
+ setState(
1644
+ updateTuiState(state, {
1645
+ type: "focus-next"
1646
+ })
1647
+ );
1648
+ return;
1649
+ }
1650
+ if (key.leftArrow) {
1651
+ setState(
1652
+ updateTuiState(state, {
1653
+ type: "focus-previous"
1654
+ })
1655
+ );
1656
+ return;
1657
+ }
1658
+ if (key.downArrow || input === "j") {
1659
+ setState(updateTuiState(state, { type: "next-row" }));
1660
+ return;
1661
+ }
1662
+ if (key.upArrow || input === "k") {
1663
+ setState(updateTuiState(state, { type: "previous-row" }));
1664
+ return;
1665
+ }
1666
+ if (input === "g") {
1667
+ setState(updateTuiState(state, { type: "first-row" }));
1668
+ return;
1669
+ }
1670
+ if (input === "G") {
1671
+ setState(updateTuiState(state, { type: "last-row" }));
1672
+ return;
1673
+ }
1674
+ if (input === "/") {
1675
+ setState(updateTuiState(state, { type: "open-search" }));
1676
+ return;
1677
+ }
1678
+ if (input === "?") {
1679
+ setState(updateTuiState(state, { type: "open-help" }));
1680
+ return;
1681
+ }
1682
+ if (key.escape) {
1683
+ setState(updateTuiState(state, { type: "close" }));
1684
+ return;
1685
+ }
1686
+ if (input === " ") {
1687
+ setState(updateTuiState(state, { type: "request-toggle" }));
1688
+ return;
1689
+ }
1690
+ if (input === "A" || key.shift && input === "a") {
1691
+ setState(updateTuiState(state, { type: "request-adopt-all" }));
1692
+ return;
1693
+ }
1694
+ if (input === "a") {
1695
+ setState(updateTuiState(state, { type: "request-adopt" }));
1696
+ return;
1697
+ }
1698
+ if (input === "r") {
1699
+ setState(updateTuiState(state, { type: "request-remove" }));
1700
+ return;
1701
+ }
1702
+ if (input === "s") {
1703
+ setState(updateTuiState(state, { type: "request-scan" }));
1704
+ }
1705
+ });
1706
+ if (loadError !== null) {
1707
+ return /* @__PURE__ */ jsx9(Text9, { color: "red", children: loadError });
1708
+ }
1709
+ if (state === null) {
1710
+ return /* @__PURE__ */ jsx9(Text9, { children: "loading dashboard..." });
1711
+ }
1712
+ return sizeBridgeEnabled ? /* @__PURE__ */ jsx9(
1713
+ BridgedDashboardViewport,
1714
+ {
1715
+ state,
1716
+ terminalWidth,
1717
+ terminalHeight,
1718
+ bridgePath: sizeBridgePath
1719
+ }
1720
+ ) : /* @__PURE__ */ jsx9(
1721
+ LiveDashboardViewport,
1722
+ {
1723
+ state,
1724
+ terminalWidth,
1725
+ terminalHeight
1726
+ }
1727
+ );
1728
+ }
1729
+
1730
+ // src/tui/launch-tui.tsx
1731
+ import { jsx as jsx10 } from "react/jsx-runtime";
1732
+ var alternateScreenEnter = "\x1B[?1049h";
1733
+ var alternateScreenExit = "\x1B[?1049l";
1734
+ var cursorHide = "\x1B[?25l";
1735
+ var cursorShow = "\x1B[?25h";
1736
+ var lifecycleTraceEnabled = process.env.SKILLMUX_TUI_PTY_TRACE === "1";
1737
+ async function launchTui(options = {}) {
1738
+ let failure;
1739
+ let instance = null;
1740
+ let sigintRequested = false;
1741
+ const handleSigint = () => {
1742
+ sigintRequested = true;
1743
+ instance?.unmount();
1744
+ };
1745
+ try {
1746
+ process.once("SIGINT", handleSigint);
1747
+ process.stdout.write(alternateScreenEnter);
1748
+ process.stdout.write(cursorHide);
1749
+ await writeLifecycleTrace("alt-screen-enter");
1750
+ instance = render(/* @__PURE__ */ jsx10(App, { ...options }));
1751
+ if (sigintRequested) {
1752
+ instance.unmount();
1753
+ }
1754
+ await instance.waitUntilExit();
1755
+ await writeLifecycleTrace("session-exit-clean");
1756
+ } catch (error) {
1757
+ failure = error;
1758
+ } finally {
1759
+ process.removeListener("SIGINT", handleSigint);
1760
+ failure = await runCleanup(failure, () => writeLifecycleTrace("alt-screen-exit"));
1761
+ failure = await runCleanup(failure, () => process.stdout.write(alternateScreenExit));
1762
+ failure = await runCleanup(failure, () => process.stdout.write(cursorShow));
1763
+ }
1764
+ if (failure !== void 0) {
1765
+ throw failure;
1766
+ }
1767
+ }
1768
+ function writeLifecycleTrace(stage) {
1769
+ if (!lifecycleTraceEnabled) {
1770
+ return Promise.resolve();
1771
+ }
1772
+ process.stderr.write(`[skillmux:${stage}]
1773
+ `);
1774
+ return stage === "session-exit-clean" ? sleep(0) : Promise.resolve();
1775
+ }
1776
+ async function runCleanup(failure, cleanup) {
1777
+ try {
1778
+ await cleanup();
1779
+ return failure;
1780
+ } catch (error) {
1781
+ return failure === void 0 ? error : failure;
1782
+ }
1783
+ }
1784
+ function sleep(ms) {
1785
+ return new Promise((resolve2) => setTimeout(resolve2, ms));
1786
+ }
1787
+ export {
1788
+ launchTui
1789
+ };
1790
+ //# sourceMappingURL=launch-tui-PHWJPIQZ.js.map