skillmux 0.1.3 → 0.1.4

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,3188 @@
1
+ import {
2
+ ManifestValidationError,
3
+ buildEmptyManifest,
4
+ collectDoctorIssues,
5
+ dedupeAndSortIssues,
6
+ discoverAgents,
7
+ formatValidationIssues,
8
+ isPathInside,
9
+ loadUserConfig,
10
+ manifestSchema,
11
+ normalizeAgentId,
12
+ resolveSkillmuxHome,
13
+ runAdopt,
14
+ runConfigAddAgent,
15
+ runConfigRemoveAgent,
16
+ runConfigUpdateAgent,
17
+ runDisable,
18
+ runDoctor,
19
+ runEnable,
20
+ runImport,
21
+ runRemove,
22
+ runScan,
23
+ scanAgentSkills
24
+ } from "./chunk-EEIOA7GC.js";
25
+
26
+ // src/tui/launch-tui.tsx
27
+ import { render } from "ink";
28
+
29
+ // src/tui/app.tsx
30
+ import { readFileSync } from "fs";
31
+ import { Text as Text11, useApp, useInput } from "ink";
32
+ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
33
+
34
+ // src/tui/forms.ts
35
+ function cloneValues(values) {
36
+ return Object.fromEntries(
37
+ Object.entries(values).map(([key, value]) => [
38
+ key,
39
+ Array.isArray(value) ? [...value] : value
40
+ ])
41
+ );
42
+ }
43
+ function isDirty(values, initialValues) {
44
+ return JSON.stringify(values) !== JSON.stringify(initialValues);
45
+ }
46
+ function trimOrEmpty(value) {
47
+ return (value ?? "").trim();
48
+ }
49
+ function normalizePlatformList(value) {
50
+ const platforms = value.map((entry) => entry.trim().toLowerCase()).filter((entry) => entry.length > 0);
51
+ return platforms.length === 0 ? void 0 : [...new Set(platforms)];
52
+ }
53
+ function updateFormField(form, field, value) {
54
+ const nextValues = {
55
+ ...form.values,
56
+ [field]: value
57
+ };
58
+ return {
59
+ values: nextValues,
60
+ initialValues: form.initialValues,
61
+ dirty: isDirty(nextValues, form.initialValues),
62
+ error: null
63
+ };
64
+ }
65
+ function buildConfigAddAgentDefaults() {
66
+ return {
67
+ id: "",
68
+ root: "root",
69
+ skills: "skills",
70
+ name: "",
71
+ platforms: [],
72
+ disabledByDefault: false
73
+ };
74
+ }
75
+ function buildConfigUpdateAgentDefaults() {
76
+ return {
77
+ root: "",
78
+ skills: "skills",
79
+ name: "",
80
+ platforms: [],
81
+ enabledByDefault: false,
82
+ disabledByDefault: false,
83
+ preserveEnabledByDefault: false
84
+ };
85
+ }
86
+ function buildImportDefaults() {
87
+ return {
88
+ sourcePath: "",
89
+ skillName: ""
90
+ };
91
+ }
92
+ function normalizeConfigAddAgentValues(values) {
93
+ return {
94
+ id: trimOrEmpty(values.id),
95
+ root: trimOrEmpty(values.root),
96
+ skills: trimOrEmpty(values.skills) || void 0,
97
+ name: trimOrEmpty(values.name) || void 0,
98
+ platforms: normalizePlatformList(values.platforms),
99
+ disabledByDefault: values.disabledByDefault ? true : void 0
100
+ };
101
+ }
102
+ function normalizeConfigUpdateAgentValues(values, agentId) {
103
+ const enabledByDefault = values.enabledByDefault ? true : void 0;
104
+ const disabledByDefault = values.disabledByDefault ? true : void 0;
105
+ return {
106
+ id: agentId,
107
+ root: trimOrEmpty(values.root) || void 0,
108
+ skills: trimOrEmpty(values.skills) || void 0,
109
+ name: trimOrEmpty(values.name) || void 0,
110
+ platforms: normalizePlatformList(values.platforms),
111
+ ...values.preserveEnabledByDefault ? {} : {
112
+ enabledByDefault,
113
+ disabledByDefault
114
+ }
115
+ };
116
+ }
117
+ function normalizeImportValues(values) {
118
+ return {
119
+ sourcePath: trimOrEmpty(values.sourcePath),
120
+ skillName: trimOrEmpty(values.skillName)
121
+ };
122
+ }
123
+ function createConfigAddAgentForm(values = {}) {
124
+ const initialValues = {
125
+ ...buildConfigAddAgentDefaults(),
126
+ ...values
127
+ };
128
+ return {
129
+ values: cloneValues(initialValues),
130
+ initialValues: cloneValues(initialValues),
131
+ dirty: false,
132
+ error: null
133
+ };
134
+ }
135
+ function updateConfigAddAgentFormField(form, field, value) {
136
+ return updateFormField(form, field, value);
137
+ }
138
+ function validateConfigAddAgentForm(form) {
139
+ const values = normalizeConfigAddAgentValues(form.values);
140
+ if (values.id.length === 0 || values.root.length === 0) {
141
+ return "Agent id and root are required";
142
+ }
143
+ return null;
144
+ }
145
+ function buildRunConfigAddAgentOptions(form) {
146
+ return normalizeConfigAddAgentValues(form.values);
147
+ }
148
+ function normalizeRunConfigAddAgentOptions(input) {
149
+ return {
150
+ id: trimOrEmpty(input.id),
151
+ root: trimOrEmpty(input.root),
152
+ skills: input.skills === void 0 ? void 0 : trimOrEmpty(input.skills),
153
+ name: input.name === void 0 ? void 0 : trimOrEmpty(input.name),
154
+ platforms: input.platforms === void 0 ? void 0 : [...new Set(input.platforms.map((entry) => entry.trim().toLowerCase()).filter((entry) => entry.length > 0))],
155
+ disabledByDefault: input.disabledByDefault === true ? true : void 0
156
+ };
157
+ }
158
+ function createConfigUpdateAgentForm(values = {}) {
159
+ const initialValues = {
160
+ ...buildConfigUpdateAgentDefaults(),
161
+ ...values
162
+ };
163
+ return {
164
+ values: cloneValues(initialValues),
165
+ initialValues: cloneValues(initialValues),
166
+ dirty: false,
167
+ error: null
168
+ };
169
+ }
170
+ function updateConfigUpdateAgentFormField(form, field, value) {
171
+ const next = field === "enabledByDefault" && value === true ? updateFormField(
172
+ updateFormField(form, field, value),
173
+ "disabledByDefault",
174
+ false
175
+ ) : field === "disabledByDefault" && value === true ? updateFormField(updateFormField(form, field, value), "enabledByDefault", false) : updateFormField(form, field, value);
176
+ return {
177
+ ...next,
178
+ values: {
179
+ ...next.values,
180
+ enabledByDefault: field === "disabledByDefault" && value === true ? false : next.values.enabledByDefault,
181
+ disabledByDefault: field === "enabledByDefault" && value === true ? false : next.values.disabledByDefault,
182
+ preserveEnabledByDefault: field === "enabledByDefault" || field === "disabledByDefault" ? false : next.values.preserveEnabledByDefault
183
+ }
184
+ };
185
+ }
186
+ function validateConfigUpdateAgentForm(form) {
187
+ const values = normalizeConfigUpdateAgentValues(form.values, "agent");
188
+ if (values.root !== void 0 && values.root.length === 0) {
189
+ return "Root path cannot be empty";
190
+ }
191
+ if (form.values.enabledByDefault && form.values.disabledByDefault) {
192
+ return "Enabled by default and disabled by default cannot both be set";
193
+ }
194
+ return null;
195
+ }
196
+ function createConfigUpdateAgentFormFromSeed(seed) {
197
+ return createConfigUpdateAgentForm({
198
+ root: seed.homeRelativeRootPath ?? "",
199
+ skills: seed.skillsDirectoryPath ?? "",
200
+ name: seed.stableName ?? "",
201
+ platforms: [...seed.supportedPlatforms ?? []],
202
+ enabledByDefault: seed.overrideEnabledByDefault === true,
203
+ disabledByDefault: seed.overrideEnabledByDefault === false,
204
+ preserveEnabledByDefault: seed.overrideEnabledByDefault === void 0
205
+ });
206
+ }
207
+ function buildRunConfigUpdateAgentOptions(form, agentId) {
208
+ const values = normalizeConfigUpdateAgentValues(form.values, agentId);
209
+ return {
210
+ id: values.id,
211
+ ...values.root === void 0 ? {} : { root: values.root },
212
+ ...values.skills === void 0 ? {} : { skills: values.skills },
213
+ ...values.name === void 0 ? {} : { name: values.name },
214
+ ...values.platforms === void 0 ? {} : { platforms: values.platforms },
215
+ ...values.enabledByDefault === void 0 ? {} : { enabledByDefault: true },
216
+ ...values.disabledByDefault === void 0 ? {} : { disabledByDefault: true }
217
+ };
218
+ }
219
+ function normalizeRunConfigUpdateAgentOptions(input) {
220
+ return {
221
+ id: trimOrEmpty(input.id),
222
+ ...input.root === void 0 ? {} : { root: trimOrEmpty(input.root) },
223
+ ...input.skills === void 0 ? {} : { skills: trimOrEmpty(input.skills) },
224
+ ...input.name === void 0 ? {} : { name: trimOrEmpty(input.name) },
225
+ ...input.platforms === void 0 ? {} : {
226
+ platforms: [...new Set(input.platforms.map((entry) => entry.trim().toLowerCase()))]
227
+ },
228
+ ...input.enabledByDefault === true ? { enabledByDefault: true } : {},
229
+ ...input.disabledByDefault === true ? { disabledByDefault: true } : {}
230
+ };
231
+ }
232
+ function createImportSkillForm(values = {}) {
233
+ const initialValues = {
234
+ ...buildImportDefaults(),
235
+ ...values
236
+ };
237
+ return {
238
+ values: cloneValues(initialValues),
239
+ initialValues: cloneValues(initialValues),
240
+ dirty: false,
241
+ error: null
242
+ };
243
+ }
244
+ function updateImportSkillFormField(form, field, value) {
245
+ return updateFormField(form, field, value);
246
+ }
247
+ function validateImportSkillForm(form) {
248
+ const values = normalizeImportValues(form.values);
249
+ if (values.sourcePath.length === 0 || values.skillName.length === 0) {
250
+ return "Skill name and source path are required";
251
+ }
252
+ return null;
253
+ }
254
+ function buildRunImportOptions(form) {
255
+ return normalizeImportValues(form.values);
256
+ }
257
+
258
+ // src/tui/load-dashboard-state.ts
259
+ import { homedir } from "os";
260
+
261
+ // src/manifest/read-manifest-snapshot.ts
262
+ import * as fs from "fs/promises";
263
+ import { join, resolve } from "path";
264
+ function normalizeHomePath(home) {
265
+ const resolvedHome = resolve(home);
266
+ return process.platform === "win32" ? resolvedHome.toLowerCase() : resolvedHome;
267
+ }
268
+ async function readManifestSnapshot(home) {
269
+ const manifestPath = join(home, "manifest.json");
270
+ try {
271
+ const contents = await fs.readFile(manifestPath, "utf8");
272
+ const parsed = manifestSchema.safeParse(JSON.parse(contents));
273
+ if (!parsed.success) {
274
+ throw new ManifestValidationError(
275
+ `Invalid manifest at ${manifestPath}: ${formatValidationIssues(parsed.error)}`
276
+ );
277
+ }
278
+ if (normalizeHomePath(parsed.data.skillmuxHome) !== normalizeHomePath(home)) {
279
+ throw new ManifestValidationError(
280
+ `Invalid manifest at ${manifestPath}: skillmuxHome must match ${home}`
281
+ );
282
+ }
283
+ return { manifest: parsed.data, exists: true };
284
+ } catch (error) {
285
+ if (typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT") {
286
+ return { manifest: buildEmptyManifest(home), exists: false };
287
+ }
288
+ if (error instanceof SyntaxError) {
289
+ throw new ManifestValidationError(
290
+ `Invalid manifest at ${manifestPath}: malformed JSON`
291
+ );
292
+ }
293
+ throw error;
294
+ }
295
+ }
296
+
297
+ // src/tui/dashboard-model.ts
298
+ var emptyCounts = () => ({
299
+ enabledCount: 0,
300
+ disabledCount: 0,
301
+ unmanagedCount: 0,
302
+ issueCount: 0
303
+ });
304
+ function sortById(values) {
305
+ return [...values].sort((left, right) => left.id.localeCompare(right.id));
306
+ }
307
+ function getSelectedAgentId(agents, requestedAgentId) {
308
+ if (requestedAgentId !== void 0) {
309
+ const requestedAgent = agents.find((agent) => agent.id === requestedAgentId);
310
+ if (requestedAgent !== void 0) {
311
+ return requestedAgent.id;
312
+ }
313
+ }
314
+ const availableAgent = agents.find(
315
+ (agent) => agent.exists && agent.supportedOnPlatform
316
+ );
317
+ return availableAgent?.id ?? agents[0]?.id ?? null;
318
+ }
319
+ function findEnabledActivation(manifest, skillId, agentId) {
320
+ return manifest.activations.find(
321
+ (activation) => activation.skillId === skillId && activation.agentId === agentId && activation.state === "enabled"
322
+ );
323
+ }
324
+ function findAnyActivation(manifest, skillId, agentId) {
325
+ return manifest.activations.find(
326
+ (activation) => activation.skillId === skillId && activation.agentId === agentId
327
+ );
328
+ }
329
+ function buildManagedSkillRow(manifest, skill, agentId) {
330
+ const enabledActivation = findEnabledActivation(manifest, skill.id, agentId);
331
+ if (enabledActivation !== void 0) {
332
+ return {
333
+ id: skill.id,
334
+ kind: "enabled",
335
+ marker: "\u25CF",
336
+ skillId: skill.id,
337
+ name: skill.name,
338
+ path: skill.path,
339
+ agentId,
340
+ activationLinkPath: enabledActivation.linkPath
341
+ };
342
+ }
343
+ return {
344
+ id: skill.id,
345
+ kind: "disabled",
346
+ marker: "\u25CB",
347
+ skillId: skill.id,
348
+ name: skill.name,
349
+ path: skill.path,
350
+ agentId,
351
+ activationLinkPath: findAnyActivation(manifest, skill.id, agentId)?.linkPath ?? null
352
+ };
353
+ }
354
+ function isAdoptableEntry(entry) {
355
+ return entry.kind === "unmanaged-directory" || entry.kind === "unmanaged-link";
356
+ }
357
+ function buildUnmanagedRows(entries, agentId) {
358
+ return entries.filter((entry) => entry.agentId === agentId).filter(isAdoptableEntry).sort(
359
+ (left, right) => `${left.skillName}:${left.path}`.localeCompare(
360
+ `${right.skillName}:${right.path}`
361
+ )
362
+ ).map((entry) => ({
363
+ id: `unmanaged:${entry.skillName}`,
364
+ kind: "unmanaged",
365
+ marker: "?",
366
+ skillName: entry.skillName,
367
+ name: entry.skillName,
368
+ path: entry.path,
369
+ agentId,
370
+ entryKind: entry.kind,
371
+ targetPath: entry.targetPath
372
+ }));
373
+ }
374
+ function relatedAgentIdsForIssue(issue, agents) {
375
+ if (issue.path === void 0) {
376
+ return [];
377
+ }
378
+ const issuePath2 = issue.path;
379
+ return agents.filter((agent) => isPathInside(agent.absoluteSkillsDirectoryPath, issuePath2)).map((agent) => agent.id);
380
+ }
381
+ function buildIssueId(issue, agentId) {
382
+ return `issue:${agentId}:${issue.code}:${issue.path ?? issue.message}`;
383
+ }
384
+ function buildIssueRows(issues, agents, agentId) {
385
+ return issues.filter((issue) => {
386
+ const relatedAgentIds = relatedAgentIdsForIssue(issue, agents);
387
+ return relatedAgentIds.includes(agentId);
388
+ }).map((issue) => ({
389
+ id: buildIssueId(issue, agentId),
390
+ kind: "issue",
391
+ marker: "!",
392
+ issueCode: issue.code,
393
+ severity: issue.severity,
394
+ message: issue.message,
395
+ path: issue.path ?? null,
396
+ agentId
397
+ }));
398
+ }
399
+ function buildSkillRowsForAgent(input, agentId) {
400
+ const managedRows = sortById(Object.values(input.manifest.skills)).map(
401
+ (skill) => buildManagedSkillRow(input.manifest, skill, agentId)
402
+ );
403
+ const unmanagedRows = buildUnmanagedRows(input.entries, agentId);
404
+ const issueRows = buildIssueRows(input.issues, input.agents, agentId);
405
+ return [...managedRows, ...unmanagedRows, ...issueRows];
406
+ }
407
+ function countsForRows(rows) {
408
+ const counts = emptyCounts();
409
+ for (const row of rows) {
410
+ if (row.kind === "enabled") {
411
+ counts.enabledCount += 1;
412
+ } else if (row.kind === "disabled") {
413
+ counts.disabledCount += 1;
414
+ } else if (row.kind === "unmanaged") {
415
+ counts.unmanagedCount += 1;
416
+ } else {
417
+ counts.issueCount += 1;
418
+ }
419
+ }
420
+ return counts;
421
+ }
422
+ function countActivationsForAgent(manifest, agentId) {
423
+ return manifest.activations.filter((activation) => activation.agentId === agentId).length;
424
+ }
425
+ function hasUserOverride(configuredAgentIds, agentId) {
426
+ return configuredAgentIds.has(agentId);
427
+ }
428
+ function buildAgentRows(input) {
429
+ const configuredAgentIds = new Set(input.configuredAgentIds ?? []);
430
+ return sortById(input.agents).map((agent) => {
431
+ const counts = countsForRows(buildSkillRowsForAgent(input, agent.id));
432
+ const userOverride = hasUserOverride(configuredAgentIds, agent.id);
433
+ const agentOverride = input.agentOverrides?.[agent.id];
434
+ return {
435
+ id: agent.id,
436
+ name: agent.stableName,
437
+ stableName: agent.stableName,
438
+ path: agent.absoluteSkillsDirectoryPath,
439
+ homeRelativeRootPath: agent.homeRelativeRootPath,
440
+ skillsDirectoryPath: agent.skillsDirectoryPath,
441
+ supportedPlatforms: [...agent.supportedPlatforms],
442
+ enabledByDefault: agent.enabledByDefault,
443
+ ...agentOverride?.stableName === void 0 ? {} : { overrideStableName: agentOverride.stableName },
444
+ ...agentOverride?.homeRelativeRootPath === void 0 ? {} : { overrideHomeRelativeRootPath: agentOverride.homeRelativeRootPath },
445
+ ...agentOverride?.skillsDirectoryPath === void 0 ? {} : { overrideSkillsDirectoryPath: agentOverride.skillsDirectoryPath },
446
+ ...agentOverride?.supportedPlatforms === void 0 ? {} : { overrideSupportedPlatforms: [...agentOverride.supportedPlatforms] },
447
+ ...agentOverride?.enabledByDefault === void 0 ? {} : { overrideEnabledByDefault: agentOverride.enabledByDefault },
448
+ discovery: agent.discovery,
449
+ exists: agent.exists,
450
+ supported: agent.supportedOnPlatform,
451
+ hasUserOverride: userOverride,
452
+ canEditOverride: userOverride,
453
+ canRemoveOverride: userOverride,
454
+ activationCount: countActivationsForAgent(input.manifest, agent.id),
455
+ ...counts
456
+ };
457
+ });
458
+ }
459
+ function buildDashboardModel(input) {
460
+ const sortedAgents = sortById(input.agents);
461
+ const selectedAgentId = getSelectedAgentId(
462
+ sortedAgents,
463
+ input.selectedAgentId
464
+ );
465
+ const skills = selectedAgentId === null ? [] : buildSkillRowsForAgent({ ...input, agents: sortedAgents }, selectedAgentId);
466
+ const selectedSkillId = skills.some((row) => row.id === input.selectedSkillId) ? input.selectedSkillId : skills[0]?.id ?? null;
467
+ return {
468
+ agents: buildAgentRows({ ...input, agents: sortedAgents }),
469
+ skills,
470
+ selectedAgentId,
471
+ selectedSkillId,
472
+ lastScanAt: input.manifest.lastScan.at,
473
+ issueCount: input.issues.length
474
+ };
475
+ }
476
+
477
+ // src/tui/load-dashboard-state.ts
478
+ async function loadDashboardState(options = {}) {
479
+ const homeDir = options.homeDir ?? homedir();
480
+ const resolvedSkillmuxHome = resolveSkillmuxHome(homeDir).skillmuxHome;
481
+ const skillmuxHome = options.skillmuxHome ?? resolvedSkillmuxHome;
482
+ const userConfig = await loadUserConfig(skillmuxHome);
483
+ const { manifest } = await readManifestSnapshot(skillmuxHome);
484
+ const agents = await discoverAgents({
485
+ homeDir,
486
+ platform: options.platform,
487
+ skillmuxHome
488
+ });
489
+ const scanResults = [];
490
+ for (const agent of agents) {
491
+ scanResults.push(await scanAgentSkills(agent, skillmuxHome));
492
+ }
493
+ const entries = scanResults.flatMap((result) => result.entries);
494
+ const scanIssues = scanResults.flatMap((result) => result.issues);
495
+ const doctorIssues = await collectDoctorIssues({ manifest, agents, entries });
496
+ const issues = dedupeAndSortIssues([...scanIssues, ...doctorIssues]);
497
+ return buildDashboardModel({
498
+ manifest,
499
+ agents,
500
+ entries,
501
+ issues,
502
+ configuredAgentIds: Object.keys(userConfig.agents),
503
+ agentOverrides: userConfig.agents,
504
+ selectedAgentId: options.selectedAgentId,
505
+ selectedSkillId: options.selectedSkillId
506
+ });
507
+ }
508
+
509
+ // src/tui/actions.ts
510
+ var defaultServices = {
511
+ runEnable,
512
+ runDisable,
513
+ runAdopt,
514
+ runConfigAddAgent,
515
+ runConfigUpdateAgent,
516
+ runConfigRemoveAgent,
517
+ runImport,
518
+ runDoctor,
519
+ runRemove,
520
+ runScan,
521
+ reload: loadDashboardState
522
+ };
523
+ function stripTrailingNewlines(output) {
524
+ return output.replace(/[\r\n]+$/u, "");
525
+ }
526
+ function actionLabel(action) {
527
+ return action.charAt(0).toUpperCase() + action.slice(1);
528
+ }
529
+ function commandLabel(kind) {
530
+ if (kind === "config-add-agent") {
531
+ return "Config add agent";
532
+ }
533
+ if (kind === "config-update-agent") {
534
+ return "Config update agent";
535
+ }
536
+ if (kind === "config-remove-agent") {
537
+ return "Config remove agent";
538
+ }
539
+ if (kind === "import-skill") {
540
+ return "Import skill";
541
+ }
542
+ return "Doctor";
543
+ }
544
+ function errorReason(error) {
545
+ const message = error instanceof Error ? error.message : String(error);
546
+ const firstLine = message.split(/\r?\n/u)[0]?.trim();
547
+ return firstLine === void 0 || firstLine.length === 0 ? "Unknown error" : firstLine;
548
+ }
549
+ function reloadOptions(input, selection = {}) {
550
+ const hasSelectedAgentId = Object.prototype.hasOwnProperty.call(
551
+ selection,
552
+ "selectedAgentId"
553
+ );
554
+ const hasSelectedSkillId = Object.prototype.hasOwnProperty.call(
555
+ selection,
556
+ "selectedSkillId"
557
+ );
558
+ return {
559
+ homeDir: input.homeDir,
560
+ skillmuxHome: input.skillmuxHome,
561
+ platform: input.platform,
562
+ selectedAgentId: hasSelectedAgentId ? selection.selectedAgentId ?? void 0 : input.model.selectedAgentId ?? void 0,
563
+ selectedSkillId: hasSelectedSkillId ? selection.selectedSkillId ?? void 0 : input.model.selectedSkillId ?? void 0
564
+ };
565
+ }
566
+ async function reloadAfterCommand(input, services, output, selection = {}) {
567
+ return {
568
+ model: await services.reload(reloadOptions(input, selection)),
569
+ statusMessage: stripTrailingNewlines(output)
570
+ };
571
+ }
572
+ function refusal(model, statusMessage) {
573
+ return { model, statusMessage };
574
+ }
575
+ function resolveSelectedSkill(model) {
576
+ if (model.selectedSkillId === null) {
577
+ return null;
578
+ }
579
+ return model.skills.find((row) => row.id === model.selectedSkillId) ?? null;
580
+ }
581
+ function resolveSelectedAgent(model) {
582
+ if (model.selectedAgentId === null) {
583
+ return null;
584
+ }
585
+ return model.agents.find((row) => row.id === model.selectedAgentId) ?? null;
586
+ }
587
+ function isPendingCommand(action) {
588
+ return typeof action === "object";
589
+ }
590
+ async function dispatchTuiAction(input) {
591
+ const services = { ...defaultServices, ...input.services };
592
+ try {
593
+ if (isPendingCommand(input.action)) {
594
+ if (input.action.kind === "config-add-agent") {
595
+ const normalizedInput = normalizeRunConfigAddAgentOptions(input.action.input);
596
+ const result3 = await services.runConfigAddAgent({
597
+ homeDir: input.homeDir,
598
+ skillmuxHome: input.skillmuxHome,
599
+ ...normalizedInput
600
+ });
601
+ return reloadAfterCommand(
602
+ input,
603
+ services,
604
+ result3.output,
605
+ {
606
+ selectedAgentId: result3.agentId ?? normalizeAgentId(normalizedInput.id),
607
+ selectedSkillId: void 0
608
+ }
609
+ );
610
+ }
611
+ if (input.action.kind === "config-update-agent") {
612
+ const normalizedInput = normalizeRunConfigUpdateAgentOptions(input.action.input);
613
+ const result3 = await services.runConfigUpdateAgent({
614
+ homeDir: input.homeDir,
615
+ skillmuxHome: input.skillmuxHome,
616
+ ...normalizedInput
617
+ });
618
+ return reloadAfterCommand(
619
+ input,
620
+ services,
621
+ result3.output,
622
+ {
623
+ selectedAgentId: result3.agentId ?? normalizedInput.id,
624
+ selectedSkillId: void 0
625
+ }
626
+ );
627
+ }
628
+ if (input.action.kind === "config-remove-agent") {
629
+ const result3 = await services.runConfigRemoveAgent({
630
+ homeDir: input.homeDir,
631
+ skillmuxHome: input.skillmuxHome,
632
+ ...input.action.input
633
+ });
634
+ return reloadAfterCommand(
635
+ input,
636
+ services,
637
+ result3.output,
638
+ {
639
+ selectedAgentId: null,
640
+ selectedSkillId: void 0
641
+ }
642
+ );
643
+ }
644
+ if (input.action.kind === "import-skill") {
645
+ const result3 = await services.runImport({
646
+ homeDir: input.homeDir,
647
+ skillmuxHome: input.skillmuxHome,
648
+ ...input.action.input
649
+ });
650
+ return reloadAfterCommand(input, services, result3.output);
651
+ }
652
+ const result2 = await services.runDoctor({
653
+ homeDir: input.homeDir,
654
+ skillmuxHome: input.skillmuxHome,
655
+ platform: input.platform
656
+ });
657
+ return {
658
+ model: input.model,
659
+ statusMessage: stripTrailingNewlines(result2.output),
660
+ doctor: result2
661
+ };
662
+ }
663
+ if (input.action === "scan") {
664
+ const result2 = await services.runScan({
665
+ homeDir: input.homeDir,
666
+ skillmuxHome: input.skillmuxHome,
667
+ platform: input.platform
668
+ });
669
+ return reloadAfterCommand(input, services, result2.output);
670
+ }
671
+ if (input.action === "adopt-all") {
672
+ const selectedAgent = resolveSelectedAgent(input.model);
673
+ if (selectedAgent === null) {
674
+ return refusal(input.model, "Select an agent first");
675
+ }
676
+ if (selectedAgent.unmanagedCount <= 0) {
677
+ return refusal(input.model, "No unmanaged skills to adopt for this agent");
678
+ }
679
+ const result2 = await services.runAdopt({
680
+ homeDir: input.homeDir,
681
+ skillmuxHome: input.skillmuxHome,
682
+ agent: selectedAgent.id
683
+ });
684
+ return reloadAfterCommand(input, services, result2.output);
685
+ }
686
+ const selectedSkill = resolveSelectedSkill(input.model);
687
+ if (selectedSkill === null) {
688
+ return refusal(input.model, "Select a skill first");
689
+ }
690
+ if (input.action === "toggle") {
691
+ if (selectedSkill.kind === "enabled") {
692
+ const result2 = await services.runDisable({
693
+ homeDir: input.homeDir,
694
+ skillmuxHome: input.skillmuxHome,
695
+ skill: selectedSkill.skillId,
696
+ agent: selectedSkill.agentId
697
+ });
698
+ return reloadAfterCommand(input, services, result2.output);
699
+ }
700
+ if (selectedSkill.kind === "disabled") {
701
+ const result2 = await services.runEnable({
702
+ homeDir: input.homeDir,
703
+ skillmuxHome: input.skillmuxHome,
704
+ skill: selectedSkill.skillId,
705
+ agent: selectedSkill.agentId
706
+ });
707
+ return reloadAfterCommand(input, services, result2.output);
708
+ }
709
+ return refusal(input.model, "Toggle is only available for managed rows");
710
+ }
711
+ if (input.action === "adopt") {
712
+ if (selectedSkill.kind !== "unmanaged") {
713
+ return refusal(input.model, "Adopt is only available for unmanaged rows");
714
+ }
715
+ const result2 = await services.runAdopt({
716
+ homeDir: input.homeDir,
717
+ skillmuxHome: input.skillmuxHome,
718
+ agent: selectedSkill.agentId,
719
+ skill: selectedSkill.skillName
720
+ });
721
+ return reloadAfterCommand(input, services, result2.output);
722
+ }
723
+ if (selectedSkill.kind === "enabled") {
724
+ return refusal(input.model, "Disable this skill before removing it");
725
+ }
726
+ if (selectedSkill.kind !== "disabled") {
727
+ return refusal(input.model, "Remove is only available for disabled rows");
728
+ }
729
+ const result = await services.runRemove({
730
+ homeDir: input.homeDir,
731
+ skillmuxHome: input.skillmuxHome,
732
+ skill: selectedSkill.skillId
733
+ });
734
+ return reloadAfterCommand(input, services, result.output);
735
+ } catch (error) {
736
+ const label = isPendingCommand(input.action) ? commandLabel(input.action.kind) : actionLabel(input.action);
737
+ return {
738
+ model: input.model,
739
+ statusMessage: `${label} failed: ${errorReason(error)}`,
740
+ commandSucceeded: false
741
+ };
742
+ }
743
+ }
744
+
745
+ // src/tui/components/Dashboard.tsx
746
+ import { Box as Box10, Text as Text10 } from "ink";
747
+
748
+ // src/tui/state.ts
749
+ var focusOrder = ["agents", "skills"];
750
+ function clampCursor(cursor, rowCount) {
751
+ if (rowCount <= 0) {
752
+ return 0;
753
+ }
754
+ return Math.min(Math.max(cursor, 0), rowCount - 1);
755
+ }
756
+ function normalizeQuery(query) {
757
+ return query.trim().toLocaleLowerCase();
758
+ }
759
+ function includesQuery(values, query) {
760
+ const normalizedQuery = normalizeQuery(query);
761
+ if (normalizedQuery.length === 0) {
762
+ return true;
763
+ }
764
+ return values.some(
765
+ (value) => (value ?? "").toLocaleLowerCase().includes(normalizedQuery)
766
+ );
767
+ }
768
+ function isRelevantAgentRow(row, selectedAgentId) {
769
+ return row.id === selectedAgentId || row.hasUserOverride === true || row.exists || (row.activationCount ?? 0) > 0 || row.enabledCount > 0 || row.unmanagedCount > 0 || row.issueCount > 0;
770
+ }
771
+ function skillMatchesQuery(row, query) {
772
+ if (row.kind === "issue") {
773
+ return includesQuery(
774
+ [row.id, row.issueCode, row.message, row.path, row.agentId],
775
+ query
776
+ );
777
+ }
778
+ if (row.kind === "unmanaged") {
779
+ return includesQuery(
780
+ [row.id, row.skillName, row.name, row.path, row.agentId],
781
+ query
782
+ );
783
+ }
784
+ return includesQuery([row.id, row.skillId, row.name, row.path, row.agentId], query);
785
+ }
786
+ function replaceModelSelection(state, selection) {
787
+ return {
788
+ ...state,
789
+ model: {
790
+ ...state.model,
791
+ ...selection
792
+ }
793
+ };
794
+ }
795
+ function clearTransientIntent(state) {
796
+ return {
797
+ ...state,
798
+ pendingAction: null,
799
+ pendingCommand: null,
800
+ statusMessage: null
801
+ };
802
+ }
803
+ function isFormModal(modal) {
804
+ return modal?.kind === "add-agent" || modal?.kind === "edit-agent" || modal?.kind === "import";
805
+ }
806
+ function restoreDismissedForm(modal) {
807
+ if (modal.kind !== "confirm-discard-dirty-form") {
808
+ return modal;
809
+ }
810
+ return modal.modal;
811
+ }
812
+ function restoreSearchSelection(state) {
813
+ if (state.search === null) {
814
+ return state;
815
+ }
816
+ const previousSelection = state.search.previousSelection;
817
+ return {
818
+ ...state,
819
+ model: {
820
+ ...state.model,
821
+ selectedAgentId: previousSelection.selectedAgentId,
822
+ selectedSkillId: previousSelection.selectedSkillId
823
+ },
824
+ agentCursor: previousSelection.agentCursor,
825
+ skillCursor: previousSelection.skillCursor,
826
+ pendingAgentId: previousSelection.pendingAgentId
827
+ };
828
+ }
829
+ function searchHasVisibleResults(state) {
830
+ if (state.search === null) {
831
+ return false;
832
+ }
833
+ return state.search.panel === "agents" ? getVisibleAgents(state).length > 0 : getVisibleSkills(state).length > 0;
834
+ }
835
+ function selectedAgentIndex(model) {
836
+ if (model.selectedAgentId === null) {
837
+ return 0;
838
+ }
839
+ const index = model.agents.findIndex((row) => row.id === model.selectedAgentId);
840
+ return index < 0 ? 0 : index;
841
+ }
842
+ function selectedSkillIndex(model) {
843
+ if (model.selectedSkillId === null) {
844
+ return 0;
845
+ }
846
+ const index = model.skills.findIndex((row) => row.id === model.selectedSkillId);
847
+ return index < 0 ? 0 : index;
848
+ }
849
+ function selectedAgentSkill(skills, agentId) {
850
+ if (agentId === null) {
851
+ return null;
852
+ }
853
+ return skills.find((row) => row.agentId === agentId) ?? null;
854
+ }
855
+ function selectedAgentRow(state) {
856
+ if (state.model.selectedAgentId === null) {
857
+ return null;
858
+ }
859
+ return state.model.agents.find((row) => row.id === state.model.selectedAgentId) ?? null;
860
+ }
861
+ function moveFocus(focus, direction) {
862
+ const currentIndex = focusOrder.indexOf(focus);
863
+ const nextIndex = (currentIndex + direction + focusOrder.length) % focusOrder.length;
864
+ return focusOrder[nextIndex] ?? "agents";
865
+ }
866
+ function stateWithAgentCursor(state, cursor) {
867
+ const visibleAgents = getVisibleAgents(state);
868
+ const agentCursor = clampCursor(cursor, visibleAgents.length);
869
+ const selectedAgent = visibleAgents[agentCursor] ?? null;
870
+ const selectedSkill = selectedAgentSkill(state.model.skills, selectedAgent?.id ?? null);
871
+ const previousAgentId = state.model.selectedAgentId;
872
+ const selectedAgentId = selectedAgent?.id ?? null;
873
+ const model = {
874
+ ...state.model,
875
+ selectedAgentId,
876
+ selectedSkillId: selectedSkill?.id ?? null
877
+ };
878
+ const skillCursor = selectedSkill === null ? 0 : clampCursor(
879
+ getVisibleSkills({ ...state, model }).findIndex(
880
+ (row) => row.id === selectedSkill.id
881
+ ),
882
+ getVisibleSkills({ ...state, model }).length
883
+ );
884
+ return {
885
+ ...state,
886
+ model,
887
+ agentCursor,
888
+ skillCursor,
889
+ pendingAgentId: selectedAgentId !== null && selectedAgentId !== previousAgentId ? selectedAgentId : state.pendingAgentId
890
+ };
891
+ }
892
+ function stateWithSkillCursor(state, cursor) {
893
+ const visibleSkills = getVisibleSkills(state);
894
+ const skillCursor = clampCursor(cursor, visibleSkills.length);
895
+ const selectedSkill = visibleSkills[skillCursor] ?? null;
896
+ return replaceModelSelection(
897
+ {
898
+ ...state,
899
+ skillCursor
900
+ },
901
+ {
902
+ selectedAgentId: state.model.selectedAgentId,
903
+ selectedSkillId: selectedSkill?.id ?? null
904
+ }
905
+ );
906
+ }
907
+ function moveCursor(state, cursor) {
908
+ if (state.focus === "agents") {
909
+ return stateWithAgentCursor(state, cursor);
910
+ }
911
+ if (state.focus === "skills") {
912
+ return stateWithSkillCursor(state, cursor);
913
+ }
914
+ return state;
915
+ }
916
+ function isModalBackgroundEvent(event) {
917
+ 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 === "open-add-agent" || event.type === "open-edit-agent" || event.type === "open-remove-agent" || event.type === "open-import" || event.type === "open-doctor" || event.type === "open-discard-dirty-form" || event.type === "submit-remove-agent" || 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";
918
+ }
919
+ function getVisibleAgents(state) {
920
+ if (state.search?.panel !== "agents") {
921
+ return state.model.agents.filter(
922
+ (row) => isRelevantAgentRow(row, state.model.selectedAgentId)
923
+ );
924
+ }
925
+ return state.model.agents.filter(
926
+ (row) => includesQuery([row.id, row.name, row.path, row.discovery], state.search?.query ?? "")
927
+ );
928
+ }
929
+ function getVisibleSkills(state) {
930
+ if (state.model.selectedAgentId === null) {
931
+ return [];
932
+ }
933
+ const agentSkills = state.model.skills.filter((row) => row.agentId === state.model.selectedAgentId);
934
+ if (state.search?.panel !== "skills") {
935
+ return agentSkills;
936
+ }
937
+ return agentSkills.filter((row) => skillMatchesQuery(row, state.search?.query ?? ""));
938
+ }
939
+ function getSelectedSkill(state) {
940
+ if (state.model.selectedSkillId === null) {
941
+ return null;
942
+ }
943
+ return state.model.skills.find((row) => row.id === state.model.selectedSkillId) ?? null;
944
+ }
945
+ function getAvailableActions(state) {
946
+ const selectedSkill = getSelectedSkill(state);
947
+ const selectedAgent = selectedAgentRow(state);
948
+ const canAcceptActions = state.modal === null && !state.busy;
949
+ const hasFocusedSkill = canAcceptActions && state.focus === "skills";
950
+ const canEditSelectedAgent = selectedAgent?.canEditOverride === true;
951
+ const canRemoveSelectedAgent = selectedAgent?.canRemoveOverride === true;
952
+ return {
953
+ addAgent: canAcceptActions,
954
+ editAgent: canAcceptActions && canEditSelectedAgent,
955
+ removeAgent: canAcceptActions && canRemoveSelectedAgent,
956
+ importSkill: canAcceptActions,
957
+ doctor: canAcceptActions,
958
+ toggle: hasFocusedSkill && (selectedSkill?.kind === "enabled" || selectedSkill?.kind === "disabled"),
959
+ adopt: hasFocusedSkill && selectedSkill?.kind === "unmanaged",
960
+ adoptAll: canAcceptActions && (selectedAgent?.unmanagedCount ?? 0) > 0,
961
+ remove: hasFocusedSkill && selectedSkill?.kind === "disabled",
962
+ scan: canAcceptActions,
963
+ help: canAcceptActions
964
+ };
965
+ }
966
+ function consumeActionIntent(state) {
967
+ return {
968
+ state: {
969
+ ...state,
970
+ pendingAction: null
971
+ },
972
+ action: state.pendingAction
973
+ };
974
+ }
975
+ function consumePendingCommandIntent(state) {
976
+ return {
977
+ state: {
978
+ ...state,
979
+ pendingCommand: null
980
+ },
981
+ command: state.pendingCommand
982
+ };
983
+ }
984
+ function consumeAgentSelectionIntent(state) {
985
+ return {
986
+ state: {
987
+ ...state,
988
+ pendingAgentId: null
989
+ },
990
+ agentId: state.pendingAgentId
991
+ };
992
+ }
993
+ function createInitialTuiState(model) {
994
+ const state = {
995
+ model,
996
+ focus: "agents",
997
+ agentCursor: selectedAgentIndex(model),
998
+ skillCursor: selectedSkillIndex(model),
999
+ search: null,
1000
+ statusMessage: null,
1001
+ modal: null,
1002
+ busy: false,
1003
+ pendingAction: null,
1004
+ pendingCommand: null,
1005
+ pendingAgentId: null
1006
+ };
1007
+ return {
1008
+ ...state,
1009
+ agentCursor: clampCursor(state.agentCursor, getVisibleAgents(state).length),
1010
+ skillCursor: clampCursor(state.skillCursor, getVisibleSkills(state).length)
1011
+ };
1012
+ }
1013
+ function updateTuiState(state, event) {
1014
+ if (state.modal !== null) {
1015
+ if (event.type === "confirm-discard-dirty-form") {
1016
+ if (state.modal.kind !== "confirm-discard-dirty-form") {
1017
+ return state;
1018
+ }
1019
+ return {
1020
+ ...state,
1021
+ modal: null,
1022
+ pendingCommand: null
1023
+ };
1024
+ }
1025
+ if (event.type === "close") {
1026
+ if (state.modal.kind === "confirm-discard-dirty-form") {
1027
+ return {
1028
+ ...state,
1029
+ modal: restoreDismissedForm(state.modal)
1030
+ };
1031
+ }
1032
+ if (isFormModal(state.modal) && state.modal.form.dirty) {
1033
+ return {
1034
+ ...state,
1035
+ modal: {
1036
+ kind: "confirm-discard-dirty-form",
1037
+ modal: state.modal
1038
+ }
1039
+ };
1040
+ }
1041
+ return {
1042
+ ...state,
1043
+ modal: null,
1044
+ pendingAction: null,
1045
+ pendingCommand: null
1046
+ };
1047
+ }
1048
+ if (event.type === "set-busy") {
1049
+ return {
1050
+ ...state,
1051
+ busy: event.busy
1052
+ };
1053
+ }
1054
+ if (event.type === "set-status") {
1055
+ return {
1056
+ ...state,
1057
+ statusMessage: event.message
1058
+ };
1059
+ }
1060
+ if (state.modal.kind === "doctor") {
1061
+ if (event.type === "doctor-result-loaded") {
1062
+ return {
1063
+ ...state,
1064
+ modal: {
1065
+ kind: "doctor",
1066
+ status: "ready",
1067
+ report: event.report
1068
+ }
1069
+ };
1070
+ }
1071
+ if (event.type === "doctor-result-failed") {
1072
+ return {
1073
+ ...state,
1074
+ modal: {
1075
+ kind: "doctor",
1076
+ status: "error",
1077
+ errorMessage: event.errorMessage
1078
+ }
1079
+ };
1080
+ }
1081
+ }
1082
+ if (state.modal.kind === "add-agent") {
1083
+ if (event.type === "add-agent-form-field-changed") {
1084
+ return {
1085
+ ...state,
1086
+ modal: {
1087
+ ...state.modal,
1088
+ form: updateConfigAddAgentFormField(
1089
+ state.modal.form,
1090
+ event.field,
1091
+ event.value
1092
+ )
1093
+ }
1094
+ };
1095
+ }
1096
+ if (event.type === "submit-add-agent-form") {
1097
+ const error = validateConfigAddAgentForm(state.modal.form);
1098
+ if (error !== null) {
1099
+ return {
1100
+ ...state,
1101
+ modal: {
1102
+ ...state.modal,
1103
+ form: {
1104
+ ...state.modal.form,
1105
+ error
1106
+ }
1107
+ }
1108
+ };
1109
+ }
1110
+ return {
1111
+ ...state,
1112
+ pendingCommand: {
1113
+ kind: "config-add-agent",
1114
+ input: buildRunConfigAddAgentOptions(state.modal.form)
1115
+ },
1116
+ modal: {
1117
+ ...state.modal,
1118
+ form: {
1119
+ ...state.modal.form,
1120
+ error: null
1121
+ }
1122
+ }
1123
+ };
1124
+ }
1125
+ }
1126
+ if (state.modal.kind === "edit-agent") {
1127
+ if (event.type === "edit-agent-form-field-changed") {
1128
+ return {
1129
+ ...state,
1130
+ modal: {
1131
+ ...state.modal,
1132
+ form: updateConfigUpdateAgentFormField(
1133
+ state.modal.form,
1134
+ event.field,
1135
+ event.value
1136
+ )
1137
+ }
1138
+ };
1139
+ }
1140
+ if (event.type === "submit-edit-agent-form") {
1141
+ const error = validateConfigUpdateAgentForm(state.modal.form);
1142
+ if (error !== null) {
1143
+ return {
1144
+ ...state,
1145
+ modal: {
1146
+ ...state.modal,
1147
+ form: {
1148
+ ...state.modal.form,
1149
+ error
1150
+ }
1151
+ }
1152
+ };
1153
+ }
1154
+ return {
1155
+ ...state,
1156
+ pendingCommand: {
1157
+ kind: "config-update-agent",
1158
+ input: buildRunConfigUpdateAgentOptions(state.modal.form, state.modal.agentId)
1159
+ },
1160
+ modal: {
1161
+ ...state.modal,
1162
+ form: {
1163
+ ...state.modal.form,
1164
+ error: null
1165
+ }
1166
+ }
1167
+ };
1168
+ }
1169
+ }
1170
+ if (state.modal.kind === "confirm-remove-agent") {
1171
+ if (event.type === "submit-remove-agent") {
1172
+ return {
1173
+ ...state,
1174
+ pendingCommand: {
1175
+ kind: "config-remove-agent",
1176
+ input: { id: state.modal.agentId }
1177
+ }
1178
+ };
1179
+ }
1180
+ }
1181
+ if (state.modal.kind === "import") {
1182
+ if (event.type === "import-form-field-changed") {
1183
+ return {
1184
+ ...state,
1185
+ modal: {
1186
+ ...state.modal,
1187
+ form: updateImportSkillFormField(
1188
+ state.modal.form,
1189
+ event.field,
1190
+ event.value
1191
+ )
1192
+ }
1193
+ };
1194
+ }
1195
+ if (event.type === "submit-import-form") {
1196
+ const error = validateImportSkillForm(state.modal.form);
1197
+ if (error !== null) {
1198
+ return {
1199
+ ...state,
1200
+ modal: {
1201
+ ...state.modal,
1202
+ form: {
1203
+ ...state.modal.form,
1204
+ error
1205
+ }
1206
+ }
1207
+ };
1208
+ }
1209
+ return {
1210
+ ...state,
1211
+ pendingCommand: {
1212
+ kind: "import-skill",
1213
+ input: buildRunImportOptions(state.modal.form)
1214
+ },
1215
+ modal: {
1216
+ ...state.modal,
1217
+ form: {
1218
+ ...state.modal.form,
1219
+ error: null
1220
+ }
1221
+ }
1222
+ };
1223
+ }
1224
+ }
1225
+ if (isModalBackgroundEvent(event)) {
1226
+ return state;
1227
+ }
1228
+ }
1229
+ if (state.busy) {
1230
+ if (event.type === "set-busy") {
1231
+ return {
1232
+ ...state,
1233
+ busy: event.busy
1234
+ };
1235
+ }
1236
+ if (event.type === "set-status") {
1237
+ return {
1238
+ ...state,
1239
+ statusMessage: event.message
1240
+ };
1241
+ }
1242
+ if (event.type === "request-toggle" || event.type === "request-adopt" || event.type === "request-remove" || event.type === "request-scan" || event.type === "open-help" || event.type === "open-add-agent" || event.type === "open-edit-agent" || event.type === "open-remove-agent" || event.type === "open-import" || event.type === "open-doctor" || event.type === "open-discard-dirty-form") {
1243
+ return {
1244
+ ...state,
1245
+ pendingAction: null
1246
+ };
1247
+ }
1248
+ }
1249
+ const readyState = clearTransientIntent(state);
1250
+ if (event.type === "focus-next") {
1251
+ return {
1252
+ ...readyState,
1253
+ focus: moveFocus(state.focus, 1)
1254
+ };
1255
+ }
1256
+ if (event.type === "focus-previous") {
1257
+ return {
1258
+ ...readyState,
1259
+ focus: moveFocus(state.focus, -1)
1260
+ };
1261
+ }
1262
+ if (event.type === "next-row") {
1263
+ return moveCursor(
1264
+ readyState,
1265
+ state.focus === "agents" ? state.agentCursor + 1 : state.skillCursor + 1
1266
+ );
1267
+ }
1268
+ if (event.type === "previous-row") {
1269
+ return moveCursor(
1270
+ readyState,
1271
+ state.focus === "agents" ? state.agentCursor - 1 : state.skillCursor - 1
1272
+ );
1273
+ }
1274
+ if (event.type === "first-row") {
1275
+ return moveCursor(readyState, 0);
1276
+ }
1277
+ if (event.type === "last-row") {
1278
+ const rowCount = state.focus === "agents" ? getVisibleAgents(state).length : getVisibleSkills(state).length;
1279
+ return moveCursor(readyState, rowCount - 1);
1280
+ }
1281
+ if (event.type === "open-search") {
1282
+ if (state.focus !== "agents" && state.focus !== "skills") {
1283
+ return {
1284
+ ...readyState,
1285
+ statusMessage: "Search is available for agents and skills"
1286
+ };
1287
+ }
1288
+ return {
1289
+ ...readyState,
1290
+ search: {
1291
+ panel: state.focus,
1292
+ query: "",
1293
+ previousSelection: {
1294
+ selectedAgentId: state.model.selectedAgentId,
1295
+ selectedSkillId: state.model.selectedSkillId,
1296
+ agentCursor: state.agentCursor,
1297
+ skillCursor: state.skillCursor,
1298
+ pendingAgentId: state.pendingAgentId
1299
+ }
1300
+ }
1301
+ };
1302
+ }
1303
+ if (event.type === "search-query-changed") {
1304
+ if (state.search === null) {
1305
+ return readyState;
1306
+ }
1307
+ const searchedState = {
1308
+ ...readyState,
1309
+ search: {
1310
+ ...state.search,
1311
+ query: event.query
1312
+ }
1313
+ };
1314
+ const rowCount = searchedState.search.panel === "agents" ? getVisibleAgents(searchedState).length : getVisibleSkills(searchedState).length;
1315
+ return searchedState.search.panel === "agents" ? stateWithAgentCursor(searchedState, clampCursor(searchedState.agentCursor, rowCount)) : stateWithSkillCursor(searchedState, clampCursor(searchedState.skillCursor, rowCount));
1316
+ }
1317
+ if (event.type === "close") {
1318
+ if (state.search !== null) {
1319
+ return {
1320
+ ...restoreSearchSelection(readyState),
1321
+ search: null
1322
+ };
1323
+ }
1324
+ if (state.modal !== null) {
1325
+ return {
1326
+ ...readyState,
1327
+ modal: null
1328
+ };
1329
+ }
1330
+ return readyState;
1331
+ }
1332
+ if (event.type === "submit-search") {
1333
+ if (state.search !== null) {
1334
+ if (!searchHasVisibleResults(state)) {
1335
+ return {
1336
+ ...restoreSearchSelection(readyState),
1337
+ search: null
1338
+ };
1339
+ }
1340
+ return {
1341
+ ...readyState,
1342
+ search: null
1343
+ };
1344
+ }
1345
+ return readyState;
1346
+ }
1347
+ if (event.type === "open-help") {
1348
+ return {
1349
+ ...readyState,
1350
+ modal: { kind: "help" }
1351
+ };
1352
+ }
1353
+ if (event.type === "open-add-agent") {
1354
+ return {
1355
+ ...readyState,
1356
+ modal: {
1357
+ kind: "add-agent",
1358
+ form: createConfigAddAgentForm()
1359
+ }
1360
+ };
1361
+ }
1362
+ if (event.type === "open-edit-agent") {
1363
+ const selectedAgent = selectedAgentRow(state);
1364
+ if (selectedAgent?.canEditOverride !== true) {
1365
+ return {
1366
+ ...readyState,
1367
+ statusMessage: "Select an agent override first"
1368
+ };
1369
+ }
1370
+ return {
1371
+ ...readyState,
1372
+ modal: {
1373
+ kind: "edit-agent",
1374
+ agentId: selectedAgent.id,
1375
+ form: createConfigUpdateAgentFormFromSeed({
1376
+ id: selectedAgent.id,
1377
+ stableName: selectedAgent.overrideStableName,
1378
+ homeRelativeRootPath: selectedAgent.overrideHomeRelativeRootPath,
1379
+ skillsDirectoryPath: selectedAgent.overrideSkillsDirectoryPath,
1380
+ supportedPlatforms: selectedAgent.overrideSupportedPlatforms,
1381
+ overrideEnabledByDefault: selectedAgent.overrideEnabledByDefault
1382
+ })
1383
+ }
1384
+ };
1385
+ }
1386
+ if (event.type === "open-remove-agent") {
1387
+ const selectedAgent = selectedAgentRow(state);
1388
+ if (selectedAgent?.canRemoveOverride !== true) {
1389
+ return {
1390
+ ...readyState,
1391
+ statusMessage: "Select an agent override first"
1392
+ };
1393
+ }
1394
+ return {
1395
+ ...readyState,
1396
+ modal: { kind: "confirm-remove-agent", agentId: selectedAgent.id }
1397
+ };
1398
+ }
1399
+ if (event.type === "open-import") {
1400
+ return {
1401
+ ...readyState,
1402
+ modal: {
1403
+ kind: "import",
1404
+ form: createImportSkillForm()
1405
+ }
1406
+ };
1407
+ }
1408
+ if (event.type === "open-doctor") {
1409
+ return {
1410
+ ...readyState,
1411
+ modal: {
1412
+ kind: "doctor",
1413
+ status: "loading"
1414
+ },
1415
+ pendingCommand: {
1416
+ kind: "doctor"
1417
+ }
1418
+ };
1419
+ }
1420
+ if (event.type === "open-discard-dirty-form") {
1421
+ return {
1422
+ ...readyState,
1423
+ modal: {
1424
+ kind: "confirm-discard-dirty-form",
1425
+ modal: {
1426
+ kind: "add-agent",
1427
+ form: createConfigAddAgentForm()
1428
+ }
1429
+ }
1430
+ };
1431
+ }
1432
+ if (event.type === "request-adopt") {
1433
+ if (state.focus !== "skills") {
1434
+ return readyState;
1435
+ }
1436
+ const selectedSkill = getSelectedSkill(state);
1437
+ if (selectedSkill?.kind !== "unmanaged") {
1438
+ return {
1439
+ ...readyState,
1440
+ statusMessage: "Adopt is only available for unmanaged rows"
1441
+ };
1442
+ }
1443
+ return {
1444
+ ...readyState,
1445
+ modal: {
1446
+ kind: "confirm-adopt",
1447
+ skillId: selectedSkill.skillName,
1448
+ agentId: selectedSkill.agentId
1449
+ }
1450
+ };
1451
+ }
1452
+ if (event.type === "request-adopt-all") {
1453
+ const selectedAgent = selectedAgentRow(state);
1454
+ if (selectedAgent === null) {
1455
+ return {
1456
+ ...readyState,
1457
+ statusMessage: "Select an agent first"
1458
+ };
1459
+ }
1460
+ if (selectedAgent.unmanagedCount <= 0) {
1461
+ return {
1462
+ ...readyState,
1463
+ statusMessage: "No unmanaged skills to adopt for this agent"
1464
+ };
1465
+ }
1466
+ return {
1467
+ ...readyState,
1468
+ modal: {
1469
+ kind: "confirm-adopt-all",
1470
+ agentId: selectedAgent.id,
1471
+ unmanagedCount: selectedAgent.unmanagedCount
1472
+ }
1473
+ };
1474
+ }
1475
+ if (event.type === "request-remove") {
1476
+ if (state.focus !== "skills") {
1477
+ return readyState;
1478
+ }
1479
+ const selectedSkill = getSelectedSkill(state);
1480
+ if (selectedSkill?.kind !== "disabled") {
1481
+ return {
1482
+ ...readyState,
1483
+ statusMessage: selectedSkill?.kind === "enabled" ? "Disable this skill before removing it" : "Remove is only available for disabled rows"
1484
+ };
1485
+ }
1486
+ return {
1487
+ ...readyState,
1488
+ modal: {
1489
+ kind: "confirm-remove",
1490
+ skillId: selectedSkill.skillId
1491
+ }
1492
+ };
1493
+ }
1494
+ if (event.type === "request-toggle") {
1495
+ if (state.focus !== "skills") {
1496
+ return readyState;
1497
+ }
1498
+ const selectedSkill = getSelectedSkill(state);
1499
+ if (selectedSkill?.kind !== "enabled" && selectedSkill?.kind !== "disabled") {
1500
+ return {
1501
+ ...readyState,
1502
+ statusMessage: "Toggle is only available for managed rows"
1503
+ };
1504
+ }
1505
+ return {
1506
+ ...readyState,
1507
+ pendingAction: "toggle"
1508
+ };
1509
+ }
1510
+ if (event.type === "request-scan") {
1511
+ return {
1512
+ ...readyState,
1513
+ pendingAction: "scan"
1514
+ };
1515
+ }
1516
+ if (event.type === "set-busy") {
1517
+ return {
1518
+ ...readyState,
1519
+ busy: event.busy
1520
+ };
1521
+ }
1522
+ if (event.type === "set-status") {
1523
+ return {
1524
+ ...readyState,
1525
+ statusMessage: event.message
1526
+ };
1527
+ }
1528
+ return readyState;
1529
+ }
1530
+
1531
+ // src/tui/components/AgentList.tsx
1532
+ import { Box, Text } from "ink";
1533
+ import { jsx, jsxs } from "react/jsx-runtime";
1534
+ function statusMarker(agent) {
1535
+ if (!agent.supported) {
1536
+ return "!";
1537
+ }
1538
+ if (!agent.exists) {
1539
+ return "?";
1540
+ }
1541
+ return "*";
1542
+ }
1543
+ function statusColor(agent) {
1544
+ if (!agent.supported) {
1545
+ return "red";
1546
+ }
1547
+ if (!agent.exists || agent.issueCount > 0) {
1548
+ return "yellow";
1549
+ }
1550
+ return "green";
1551
+ }
1552
+ function AgentList({
1553
+ agents,
1554
+ selectedAgentId,
1555
+ focused,
1556
+ searchQuery,
1557
+ width = 24,
1558
+ height = 18
1559
+ }) {
1560
+ const emptyMessage = searchQuery !== void 0 && searchQuery.trim().length > 0 ? "No matching agents" : "No agents found";
1561
+ return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", width, height, children: [
1562
+ /* @__PURE__ */ jsx(Text, { bold: true, color: focused ? "cyan" : void 0, children: "Agents" }),
1563
+ agents.length === 0 ? /* @__PURE__ */ jsx(Text, { dimColor: true, children: emptyMessage }) : agents.map((agent) => {
1564
+ const selected = agent.id === selectedAgentId;
1565
+ const selectionPrefix = selected ? ">" : " ";
1566
+ return /* @__PURE__ */ jsxs(Text, { inverse: selected, children: [
1567
+ /* @__PURE__ */ jsx(Text, { color: statusColor(agent), children: statusMarker(agent) }),
1568
+ /* @__PURE__ */ jsxs(Text, { children: [
1569
+ selectionPrefix,
1570
+ " ",
1571
+ agent.name
1572
+ ] })
1573
+ ] }, agent.id);
1574
+ })
1575
+ ] });
1576
+ }
1577
+
1578
+ // src/tui/components/ConfirmDialog.tsx
1579
+ import { Box as Box2, Text as Text2 } from "ink";
1580
+ import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
1581
+ var confirmDialogHeight = 4;
1582
+ function confirmationText(modal) {
1583
+ if (modal.kind === "confirm-adopt") {
1584
+ return `Adopt ${modal.skillId} for ${modal.agentId}?`;
1585
+ }
1586
+ if (modal.kind === "confirm-adopt-all") {
1587
+ return `Adopt all unmanaged skills for ${modal.agentId}?`;
1588
+ }
1589
+ if (modal.kind === "confirm-remove-agent") {
1590
+ return `Remove agent override for ${modal.agentId}?`;
1591
+ }
1592
+ if (modal.kind === "confirm-discard-dirty-form") {
1593
+ return "Discard unsaved changes?";
1594
+ }
1595
+ return `Remove ${modal.skillId} from SkillMux?`;
1596
+ }
1597
+ function confirmationDetails(modal) {
1598
+ if (modal.kind !== "confirm-adopt-all") {
1599
+ if (modal.kind === "confirm-remove-agent") {
1600
+ return "This will remove the selected agent override from SkillMux.";
1601
+ }
1602
+ if (modal.kind === "confirm-discard-dirty-form") {
1603
+ return "This will close the form and discard the current changes.";
1604
+ }
1605
+ return null;
1606
+ }
1607
+ return `${modal.unmanagedCount} unmanaged skills will be moved under SkillMux management.`;
1608
+ }
1609
+ function ConfirmDialog({ modal }) {
1610
+ return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", height: confirmDialogHeight, children: [
1611
+ /* @__PURE__ */ jsx2(
1612
+ Text2,
1613
+ {
1614
+ bold: true,
1615
+ color: modal.kind === "confirm-remove" || modal.kind === "confirm-remove-agent" ? "yellow" : "cyan",
1616
+ children: "Confirm"
1617
+ }
1618
+ ),
1619
+ /* @__PURE__ */ jsx2(Text2, { children: confirmationText(modal) }),
1620
+ confirmationDetails(modal) === null ? null : /* @__PURE__ */ jsx2(Text2, { children: confirmationDetails(modal) }),
1621
+ /* @__PURE__ */ jsx2(Text2, { children: "[y] confirm [Esc] cancel" })
1622
+ ] });
1623
+ }
1624
+
1625
+ // src/tui/components/DoctorDialog.tsx
1626
+ import { Box as Box3, Text as Text3 } from "ink";
1627
+ import { Fragment, jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
1628
+ function issueLabel(issue) {
1629
+ return `${issue.severity} ${issue.code}`;
1630
+ }
1631
+ function issuePath(issue) {
1632
+ return issue.path ?? null;
1633
+ }
1634
+ function visibleIssueCount(height) {
1635
+ return Math.max(height - 5, 1);
1636
+ }
1637
+ function DoctorDialog({
1638
+ modal,
1639
+ scrollOffset = 0,
1640
+ width = 72,
1641
+ height = 14
1642
+ }) {
1643
+ if (modal.status === "loading") {
1644
+ return /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", width, height, children: [
1645
+ /* @__PURE__ */ jsx3(Text3, { bold: true, children: "Doctor" }),
1646
+ /* @__PURE__ */ jsx3(Text3, { children: "Loading doctor diagnostics..." }),
1647
+ /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: "[Esc] close" })
1648
+ ] });
1649
+ }
1650
+ if (modal.status === "error") {
1651
+ return /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", width, height, children: [
1652
+ /* @__PURE__ */ jsx3(Text3, { bold: true, children: "Doctor" }),
1653
+ /* @__PURE__ */ jsx3(Text3, { color: "red", children: modal.errorMessage }),
1654
+ /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: "[Esc] close" })
1655
+ ] });
1656
+ }
1657
+ const issues = modal.report.issues;
1658
+ const maxIssues = visibleIssueCount(height);
1659
+ const maxOffset = Math.max(issues.length - maxIssues, 0);
1660
+ const clampedOffset = Math.min(Math.max(scrollOffset, 0), maxOffset);
1661
+ const visibleIssues = issues.slice(clampedOffset, clampedOffset + maxIssues);
1662
+ return /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", width, height, children: [
1663
+ /* @__PURE__ */ jsx3(Text3, { bold: true, children: "Doctor" }),
1664
+ /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: issues.length === 0 ? "No doctor issues found." : `${issues.length} issue(s) found` }),
1665
+ issues.length === 0 ? null : /* @__PURE__ */ jsxs3(Fragment, { children: [
1666
+ visibleIssues.map((issue) => /* @__PURE__ */ jsxs3(Text3, { children: [
1667
+ /* @__PURE__ */ jsx3(Text3, { color: issue.severity === "error" ? "red" : "yellow", children: "! " }),
1668
+ /* @__PURE__ */ jsxs3(Text3, { children: [
1669
+ issueLabel(issue),
1670
+ " - ",
1671
+ issue.message,
1672
+ issuePath(issue) === null ? "" : ` - ${issuePath(issue)}`
1673
+ ] })
1674
+ ] }, `${issue.code}:${issue.path ?? issue.message}`)),
1675
+ issues.length > visibleIssues.length ? /* @__PURE__ */ jsxs3(Text3, { dimColor: true, children: [
1676
+ "Showing ",
1677
+ clampedOffset + 1,
1678
+ "-",
1679
+ clampedOffset + visibleIssues.length,
1680
+ " of ",
1681
+ issues.length
1682
+ ] }) : null
1683
+ ] }),
1684
+ /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: "[Up/Down] scroll [Esc] close" })
1685
+ ] });
1686
+ }
1687
+
1688
+ // src/tui/components/DetailPane.tsx
1689
+ import { Box as Box4, Text as Text4 } from "ink";
1690
+ import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
1691
+ function compactPath(value, maxLength) {
1692
+ if (value.length <= maxLength) {
1693
+ return value;
1694
+ }
1695
+ const separator = value.includes("\\") ? "\\" : "/";
1696
+ const parts = value.split(/[\\/]+/).filter((part) => part.length > 0);
1697
+ let suffix = parts.at(-1) ?? value;
1698
+ for (let index = parts.length - 2; index >= 0; index -= 1) {
1699
+ const candidate = `${parts[index]}${separator}${suffix}`;
1700
+ if (`...${separator}${candidate}`.length > maxLength) {
1701
+ break;
1702
+ }
1703
+ suffix = candidate;
1704
+ }
1705
+ const shortened = `...${separator}${suffix}`;
1706
+ if (shortened.length <= maxLength) {
1707
+ return shortened;
1708
+ }
1709
+ if (maxLength <= 3) {
1710
+ return ".".repeat(maxLength);
1711
+ }
1712
+ return `...${suffix.slice(-(maxLength - 3))}`;
1713
+ }
1714
+ function detailLines(skill) {
1715
+ if (skill.kind === "enabled") {
1716
+ return [
1717
+ { label: "Name", value: skill.name, compact: false },
1718
+ { label: "Status", value: "enabled", compact: false },
1719
+ { label: "Store", value: skill.path, compact: true },
1720
+ { label: "Link", value: skill.activationLinkPath, compact: true }
1721
+ ];
1722
+ }
1723
+ if (skill.kind === "disabled") {
1724
+ return [
1725
+ { label: "Name", value: skill.name, compact: false },
1726
+ { label: "Status", value: "disabled", compact: false },
1727
+ { label: "Store", value: skill.path, compact: true },
1728
+ {
1729
+ label: "Link",
1730
+ value: skill.activationLinkPath ?? "not linked",
1731
+ compact: skill.activationLinkPath !== null
1732
+ }
1733
+ ];
1734
+ }
1735
+ if (skill.kind === "unmanaged") {
1736
+ return [
1737
+ { label: "Name", value: skill.name, compact: false },
1738
+ { label: "Status", value: "unmanaged", compact: false },
1739
+ { label: "Entry", value: skill.entryKind, compact: false },
1740
+ { label: "Path", value: skill.path, compact: true }
1741
+ ];
1742
+ }
1743
+ return [
1744
+ { label: "Status", value: "issue", compact: false },
1745
+ { label: "Code", value: skill.issueCode, compact: false },
1746
+ { label: "Severity", value: skill.severity, compact: false },
1747
+ { label: "Message", value: skill.message, compact: false },
1748
+ { label: "Path", value: skill.path ?? "none", compact: skill.path !== null }
1749
+ ];
1750
+ }
1751
+ function DetailPane({
1752
+ selectedAgent,
1753
+ selectedSkill,
1754
+ focused: _focused,
1755
+ loadingAgentName = null,
1756
+ width = 28,
1757
+ height = 18
1758
+ }) {
1759
+ return /* @__PURE__ */ jsxs4(Box4, { flexDirection: "column", width, height, children: [
1760
+ /* @__PURE__ */ jsx4(Text4, { bold: true, children: "Detail" }),
1761
+ selectedAgent === null ? /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: "Select an agent" }) : /* @__PURE__ */ jsxs4(Text4, { dimColor: true, children: [
1762
+ "Agent: ",
1763
+ selectedAgent.name
1764
+ ] }),
1765
+ selectedSkill === null ? loadingAgentName !== null ? /* @__PURE__ */ jsxs4(Text4, { dimColor: true, children: [
1766
+ "Loading details for ",
1767
+ loadingAgentName,
1768
+ "..."
1769
+ ] }) : /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: "Select a skill row" }) : detailLines(selectedSkill).map(({ label, value, compact }) => {
1770
+ const valueWidth = Math.max(width - (label.length + 2), 8);
1771
+ const renderedValue = compact ? compactPath(value, valueWidth) : value;
1772
+ return /* @__PURE__ */ jsxs4(Text4, { children: [
1773
+ /* @__PURE__ */ jsxs4(Text4, { dimColor: true, children: [
1774
+ label,
1775
+ ": "
1776
+ ] }),
1777
+ /* @__PURE__ */ jsx4(Text4, { children: renderedValue })
1778
+ ] }, label);
1779
+ })
1780
+ ] });
1781
+ }
1782
+
1783
+ // src/tui/components/Footer.tsx
1784
+ import { Box as Box5, Text as Text5 } from "ink";
1785
+ import { Fragment as Fragment2, jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
1786
+ var agentLegend = "Agent icons: * ready yellow * issues ? missing ! unsupported";
1787
+ var skillLegend = "Skill markers: \u25CF enabled \u25CB disabled ? unmanaged ! issue";
1788
+ function Footer({ actions, search }) {
1789
+ const shortcuts = [
1790
+ actions.addAgent ? "[n]add agent" : null,
1791
+ actions.editAgent ? "[e]edit agent" : null,
1792
+ actions.removeAgent ? "[X]remove agent" : null,
1793
+ actions.importSkill ? "[i]import" : null,
1794
+ actions.doctor ? "[d]doctor" : null,
1795
+ "[Left/Right]focus",
1796
+ actions.toggle ? "[Space]toggle" : null,
1797
+ actions.adopt ? "[a]adopt" : null,
1798
+ actions.adoptAll ? "[Shift+A]adopt all" : null,
1799
+ actions.remove ? "[r]remove" : null,
1800
+ actions.scan ? "[s]scan" : null,
1801
+ actions.help ? "[?]help" : null,
1802
+ "[q]quit"
1803
+ ].filter((shortcut) => shortcut !== null);
1804
+ return /* @__PURE__ */ jsx5(Box5, { flexDirection: "column", height: 3, children: search === null ? /* @__PURE__ */ jsxs5(Fragment2, { children: [
1805
+ /* @__PURE__ */ jsx5(Text5, { children: shortcuts.join(" ") }),
1806
+ /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: agentLegend }),
1807
+ /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: skillLegend })
1808
+ ] }) : /* @__PURE__ */ jsxs5(Text5, { children: [
1809
+ /* @__PURE__ */ jsx5(Text5, { color: "cyan", children: "/" }),
1810
+ /* @__PURE__ */ jsx5(Text5, { children: search.query }),
1811
+ /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: " [Enter]keep [Esc]cancel" })
1812
+ ] }) });
1813
+ }
1814
+
1815
+ // src/tui/components/HelpOverlay.tsx
1816
+ import { Box as Box6, Text as Text6 } from "ink";
1817
+ import { jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
1818
+ function HelpOverlay() {
1819
+ return /* @__PURE__ */ jsxs6(Box6, { flexDirection: "column", height: 8, children: [
1820
+ /* @__PURE__ */ jsx6(Text6, { bold: true, children: "Help" }),
1821
+ /* @__PURE__ */ jsxs6(Text6, { children: [
1822
+ /* @__PURE__ */ jsx6(Text6, { bold: true, children: "Navigation" }),
1823
+ /* @__PURE__ */ jsx6(Text6, { children: ": Left/Right switch panels, j/k or Up/Down move, g/G jump." })
1824
+ ] }),
1825
+ /* @__PURE__ */ jsxs6(Text6, { children: [
1826
+ /* @__PURE__ */ jsx6(Text6, { bold: true, children: "Actions" }),
1827
+ /* @__PURE__ */ jsx6(Text6, { children: ": Space toggles, a adopts, Shift+A current-agent bulk adopt, r removes, s scans, n add agent, e edit selected override, X remove selected override, i import, d doctor." })
1828
+ ] }),
1829
+ /* @__PURE__ */ jsxs6(Text6, { children: [
1830
+ /* @__PURE__ */ jsx6(Text6, { bold: true, children: "Search" }),
1831
+ /* @__PURE__ */ jsx6(Text6, { children: ": / filters the focused list, Enter keeps the result, Esc cancels." })
1832
+ ] }),
1833
+ /* @__PURE__ */ jsxs6(Text6, { children: [
1834
+ /* @__PURE__ */ jsx6(Text6, { bold: true, children: "Agent icons" }),
1835
+ /* @__PURE__ */ jsx6(Text6, { children: ": * ready, yellow * issues, ? missing, ! unsupported." })
1836
+ ] }),
1837
+ /* @__PURE__ */ jsxs6(Text6, { children: [
1838
+ /* @__PURE__ */ jsx6(Text6, { bold: true, children: "Skill markers" }),
1839
+ /* @__PURE__ */ jsx6(Text6, { children: ": \u25CF enabled, \u25CB disabled, ? unmanaged, ! issue." })
1840
+ ] }),
1841
+ /* @__PURE__ */ jsxs6(Text6, { children: [
1842
+ /* @__PURE__ */ jsx6(Text6, { bold: true, children: "Safety" }),
1843
+ /* @__PURE__ */ jsx6(Text6, { children: ": Toggle, adopt, remove, and scan can update SkillMux state and agent links." })
1844
+ ] })
1845
+ ] });
1846
+ }
1847
+
1848
+ // src/tui/components/FormDialog.tsx
1849
+ import { Box as Box7, Text as Text7 } from "ink";
1850
+ import { jsx as jsx7, jsxs as jsxs7 } from "react/jsx-runtime";
1851
+ var platformOptions = ["win32", "linux", "darwin"];
1852
+ function checkbox(value) {
1853
+ return value ? "[x]" : "[ ]";
1854
+ }
1855
+ function renderTextField(label, value, active) {
1856
+ return /* @__PURE__ */ jsxs7(Text7, { inverse: active, children: [
1857
+ /* @__PURE__ */ jsxs7(Text7, { bold: true, children: [
1858
+ label,
1859
+ ": "
1860
+ ] }),
1861
+ /* @__PURE__ */ jsx7(Text7, { children: value.length > 0 ? value : " " })
1862
+ ] }, label);
1863
+ }
1864
+ function renderBooleanField(label, value, active) {
1865
+ return /* @__PURE__ */ jsxs7(Text7, { inverse: active, children: [
1866
+ /* @__PURE__ */ jsxs7(Text7, { bold: true, children: [
1867
+ label,
1868
+ ": "
1869
+ ] }),
1870
+ /* @__PURE__ */ jsx7(Text7, { children: checkbox(value) })
1871
+ ] }, label);
1872
+ }
1873
+ function renderPlatformField(selectedPlatforms, activePlatformIndex, active) {
1874
+ return platformOptions.map((platform, index) => {
1875
+ const selected = selectedPlatforms.includes(platform);
1876
+ const isCurrent = active && index === activePlatformIndex;
1877
+ return /* @__PURE__ */ jsxs7(Text7, { inverse: isCurrent, children: [
1878
+ /* @__PURE__ */ jsx7(Text7, { children: isCurrent ? "> " : " " }),
1879
+ /* @__PURE__ */ jsxs7(Text7, { children: [
1880
+ checkbox(selected),
1881
+ " "
1882
+ ] }),
1883
+ /* @__PURE__ */ jsx7(Text7, { children: platform })
1884
+ ] }, platform);
1885
+ });
1886
+ }
1887
+ function FormDialog({
1888
+ modal,
1889
+ fieldIndex = 0,
1890
+ platformIndex = 0,
1891
+ width = 72,
1892
+ height = 14
1893
+ }) {
1894
+ const activeField = fieldIndex;
1895
+ const submitFieldIndex = modal.kind === "import" ? 2 : 6;
1896
+ if (modal.kind === "import") {
1897
+ return /* @__PURE__ */ jsxs7(Box7, { flexDirection: "column", width, height, children: [
1898
+ /* @__PURE__ */ jsx7(Text7, { bold: true, children: "Import skill" }),
1899
+ modal.form.error === null ? null : /* @__PURE__ */ jsx7(Text7, { color: "red", children: modal.form.error }),
1900
+ renderTextField(
1901
+ "Source path",
1902
+ modal.form.values.sourcePath,
1903
+ activeField === 0
1904
+ ),
1905
+ renderTextField(
1906
+ "Skill name",
1907
+ modal.form.values.skillName,
1908
+ activeField === 1
1909
+ ),
1910
+ /* @__PURE__ */ jsx7(Text7, { inverse: activeField === submitFieldIndex, children: "Submit" }),
1911
+ /* @__PURE__ */ jsx7(Text7, { dimColor: true, children: "[Up/Down] move [Enter] submit selected row [Esc] cancel" })
1912
+ ] });
1913
+ }
1914
+ const title = modal.kind === "add-agent" ? "Add agent" : `Edit agent ${modal.agentId}`;
1915
+ const fields = modal.kind === "add-agent" ? [
1916
+ renderTextField("Agent id", modal.form.values.id, activeField === 0),
1917
+ renderTextField("Root path", modal.form.values.root, activeField === 1),
1918
+ renderTextField("Skills path", modal.form.values.skills, activeField === 2),
1919
+ renderTextField("Display name", modal.form.values.name, activeField === 3)
1920
+ ] : [
1921
+ renderTextField("Root path", modal.form.values.root, activeField === 0),
1922
+ renderTextField("Skills path", modal.form.values.skills, activeField === 1),
1923
+ renderTextField("Display name", modal.form.values.name, activeField === 2)
1924
+ ];
1925
+ const platformFieldIndex = modal.kind === "add-agent" ? 4 : 3;
1926
+ const booleanFieldIndex = modal.kind === "add-agent" ? 5 : 4;
1927
+ const platformLines = renderPlatformField(
1928
+ modal.form.values.platforms,
1929
+ platformIndex,
1930
+ activeField === platformFieldIndex
1931
+ );
1932
+ const booleanLabel = modal.kind === "add-agent" ? "Disabled by default" : "Enabled by default";
1933
+ const booleanValue = modal.kind === "add-agent" ? modal.form.values.disabledByDefault : modal.form.values.enabledByDefault;
1934
+ const secondaryBooleanLabel = modal.kind === "add-agent" ? null : "Disabled by default";
1935
+ return /* @__PURE__ */ jsxs7(Box7, { flexDirection: "column", width, height, children: [
1936
+ /* @__PURE__ */ jsx7(Text7, { bold: true, children: title }),
1937
+ modal.form.error === null ? null : /* @__PURE__ */ jsx7(Text7, { color: "red", children: modal.form.error }),
1938
+ fields,
1939
+ /* @__PURE__ */ jsx7(Text7, { bold: true, inverse: activeField === platformFieldIndex, children: "Platforms" }),
1940
+ platformLines,
1941
+ renderBooleanField(
1942
+ booleanLabel,
1943
+ booleanValue,
1944
+ activeField === booleanFieldIndex
1945
+ ),
1946
+ secondaryBooleanLabel === null ? null : renderBooleanField(
1947
+ secondaryBooleanLabel,
1948
+ modal.form.values.disabledByDefault,
1949
+ activeField === booleanFieldIndex + 1
1950
+ ),
1951
+ /* @__PURE__ */ jsx7(Text7, { inverse: activeField === submitFieldIndex, children: "Submit" }),
1952
+ modal.kind === "edit-agent" ? /* @__PURE__ */ jsx7(Text7, { dimColor: true, children: "Leaving both defaults unchecked preserves the current setting." }) : null,
1953
+ /* @__PURE__ */ jsx7(Text7, { dimColor: true, children: "[Up/Down] move [Enter] submit selected row [Esc] cancel" })
1954
+ ] });
1955
+ }
1956
+
1957
+ // src/tui/components/SkillList.tsx
1958
+ import { Box as Box8, Text as Text8 } from "ink";
1959
+ import { jsx as jsx8, jsxs as jsxs8 } from "react/jsx-runtime";
1960
+ function markerColor(skill) {
1961
+ if (skill.kind === "enabled") {
1962
+ return "green";
1963
+ }
1964
+ if (skill.kind === "issue") {
1965
+ return skill.severity === "error" ? "red" : "yellow";
1966
+ }
1967
+ if (skill.kind === "unmanaged") {
1968
+ return "yellow";
1969
+ }
1970
+ return "gray";
1971
+ }
1972
+ function skillLabel(skill) {
1973
+ if (skill.kind === "issue") {
1974
+ return skill.issueCode;
1975
+ }
1976
+ return skill.name;
1977
+ }
1978
+ function SkillList({
1979
+ agentId,
1980
+ skills,
1981
+ selectedSkillId,
1982
+ focused,
1983
+ searchQuery,
1984
+ loadingAgentName = null,
1985
+ width = 28,
1986
+ height = 18
1987
+ }) {
1988
+ 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";
1989
+ return /* @__PURE__ */ jsxs8(Box8, { flexDirection: "column", width, height, children: [
1990
+ /* @__PURE__ */ jsxs8(Text8, { bold: true, color: focused ? "cyan" : void 0, children: [
1991
+ "Skills for ",
1992
+ agentId ?? "none"
1993
+ ] }),
1994
+ skills.length === 0 ? /* @__PURE__ */ jsx8(Text8, { dimColor: true, children: emptyMessage }) : skills.map((skill) => {
1995
+ const selected = skill.id === selectedSkillId;
1996
+ return /* @__PURE__ */ jsxs8(Text8, { inverse: focused && selected, children: [
1997
+ /* @__PURE__ */ jsx8(Text8, { color: markerColor(skill), children: skill.marker }),
1998
+ /* @__PURE__ */ jsxs8(Text8, { children: [
1999
+ " ",
2000
+ skillLabel(skill)
2001
+ ] })
2002
+ ] }, skill.id);
2003
+ })
2004
+ ] });
2005
+ }
2006
+
2007
+ // src/tui/components/StatusLine.tsx
2008
+ import { Box as Box9, Text as Text9 } from "ink";
2009
+ import { jsx as jsx9 } from "react/jsx-runtime";
2010
+ function StatusLine({
2011
+ busy,
2012
+ statusMessage,
2013
+ lastScanAt,
2014
+ issueCount
2015
+ }) {
2016
+ const message = statusMessage ?? (busy ? "scanning..." : `Last scan: ${lastScanAt ?? "never"} | issues: ${issueCount}`);
2017
+ return /* @__PURE__ */ jsx9(Box9, { height: 1, children: /* @__PURE__ */ jsx9(Text9, { color: busy ? "cyan" : void 0, children: message }) });
2018
+ }
2019
+
2020
+ // src/tui/components/Dashboard.tsx
2021
+ import { jsx as jsx10, jsxs as jsxs9 } from "react/jsx-runtime";
2022
+ var minimumWidth = 80;
2023
+ var minimumHeight = 24;
2024
+ var agentRatio = 0.26;
2025
+ var skillRatio = 0.3;
2026
+ var detailRatio = 0.44;
2027
+ var agentMinimumWidth = 20;
2028
+ var skillMinimumWidth = 24;
2029
+ var detailMinimumWidth = 28;
2030
+ var largeModalWidth = 72;
2031
+ var largeModalHeight = 14;
2032
+ function paneWidths(width) {
2033
+ const agentWidth = Math.max(agentMinimumWidth, Math.round(width * agentRatio));
2034
+ const skillWidth = Math.max(skillMinimumWidth, Math.round(width * skillRatio));
2035
+ const detailWidth = Math.max(
2036
+ detailMinimumWidth,
2037
+ Math.round(width * detailRatio)
2038
+ );
2039
+ const widthDelta = width - (agentWidth + skillWidth + detailWidth);
2040
+ if (widthDelta === 0) {
2041
+ return { agentWidth, skillWidth, detailWidth };
2042
+ }
2043
+ return {
2044
+ agentWidth,
2045
+ skillWidth,
2046
+ detailWidth: detailWidth + widthDelta
2047
+ };
2048
+ }
2049
+ function Dashboard({
2050
+ state,
2051
+ width,
2052
+ height,
2053
+ modalInteraction
2054
+ }) {
2055
+ const interaction = modalInteraction ?? {
2056
+ fieldIndex: 0,
2057
+ platformIndex: 0,
2058
+ doctorScrollOffset: 0
2059
+ };
2060
+ if (width < minimumWidth || height < minimumHeight) {
2061
+ return /* @__PURE__ */ jsx10(
2062
+ Box10,
2063
+ {
2064
+ flexDirection: "column",
2065
+ width,
2066
+ height,
2067
+ justifyContent: "center",
2068
+ alignItems: "center",
2069
+ children: /* @__PURE__ */ jsx10(Text10, { children: "Terminal too small. Resize to at least 80x24." })
2070
+ }
2071
+ );
2072
+ }
2073
+ const visibleAgents = getVisibleAgents(state);
2074
+ const visibleSkills = getVisibleSkills(state);
2075
+ const selectedAgent = state.model.agents.find((agent) => agent.id === state.model.selectedAgentId) ?? null;
2076
+ const selectedSkill = getSelectedSkill(state);
2077
+ const loadingAgentName = state.pendingAgentId ?? (state.busy && state.statusMessage === "loading agent..." ? state.model.selectedAgentId : null);
2078
+ const loadingAgent = loadingAgentName === null ? null : state.model.agents.find((agent) => agent.id === loadingAgentName) ?? null;
2079
+ const actions = getAvailableActions(state);
2080
+ const footerHeight = 3;
2081
+ const largeModal = state.modal?.kind === "add-agent" || state.modal?.kind === "edit-agent" || state.modal?.kind === "import" || state.modal?.kind === "doctor" || state.modal?.kind === "confirm-remove-agent";
2082
+ const overlayHeight = largeModal ? 0 : state.modal?.kind === "help" ? 8 : state.modal?.kind === "confirm-adopt" || state.modal?.kind === "confirm-adopt-all" || state.modal?.kind === "confirm-remove" ? confirmDialogHeight : 0;
2083
+ const footerSpace = state.modal === null ? footerHeight : largeModal ? 0 : footerHeight;
2084
+ const bodyHeight = Math.max(height - 1 - footerSpace - overlayHeight, 0);
2085
+ const { agentWidth, skillWidth, detailWidth } = paneWidths(width);
2086
+ const modalWidth = Math.min(width - 4, largeModalWidth);
2087
+ const modalHeight = Math.min(bodyHeight, largeModalHeight);
2088
+ return /* @__PURE__ */ jsxs9(Box10, { flexDirection: "column", width, height, children: [
2089
+ /* @__PURE__ */ jsx10(
2090
+ StatusLine,
2091
+ {
2092
+ busy: state.busy,
2093
+ statusMessage: state.statusMessage,
2094
+ lastScanAt: state.model.lastScanAt,
2095
+ issueCount: state.model.issueCount
2096
+ }
2097
+ ),
2098
+ largeModal ? /* @__PURE__ */ jsx10(
2099
+ Box10,
2100
+ {
2101
+ flexDirection: "column",
2102
+ width,
2103
+ height: bodyHeight,
2104
+ justifyContent: "center",
2105
+ alignItems: "center",
2106
+ children: /* @__PURE__ */ jsx10(Box10, { width: modalWidth, height: modalHeight, children: state.modal?.kind === "add-agent" || state.modal?.kind === "edit-agent" || state.modal?.kind === "import" ? /* @__PURE__ */ jsx10(
2107
+ FormDialog,
2108
+ {
2109
+ modal: state.modal,
2110
+ fieldIndex: interaction.fieldIndex,
2111
+ platformIndex: interaction.platformIndex,
2112
+ width: modalWidth,
2113
+ height: modalHeight
2114
+ }
2115
+ ) : state.modal?.kind === "doctor" ? /* @__PURE__ */ jsx10(
2116
+ DoctorDialog,
2117
+ {
2118
+ modal: state.modal,
2119
+ scrollOffset: interaction.doctorScrollOffset,
2120
+ width: modalWidth,
2121
+ height: modalHeight
2122
+ }
2123
+ ) : state.modal?.kind === "confirm-remove-agent" ? /* @__PURE__ */ jsx10(ConfirmDialog, { modal: state.modal }) : null })
2124
+ }
2125
+ ) : /* @__PURE__ */ jsxs9(Box10, { flexDirection: "row", width, height: bodyHeight, children: [
2126
+ /* @__PURE__ */ jsx10(
2127
+ AgentList,
2128
+ {
2129
+ agents: visibleAgents,
2130
+ selectedAgentId: state.model.selectedAgentId,
2131
+ focused: state.focus === "agents",
2132
+ searchQuery: state.search?.panel === "agents" ? state.search.query : void 0,
2133
+ width: agentWidth,
2134
+ height: bodyHeight
2135
+ }
2136
+ ),
2137
+ /* @__PURE__ */ jsx10(
2138
+ SkillList,
2139
+ {
2140
+ agentId: state.model.selectedAgentId,
2141
+ skills: visibleSkills,
2142
+ selectedSkillId: state.model.selectedSkillId,
2143
+ focused: state.focus === "skills",
2144
+ searchQuery: state.search?.panel === "skills" ? state.search.query : void 0,
2145
+ loadingAgentName: loadingAgent?.name ?? null,
2146
+ width: skillWidth,
2147
+ height: bodyHeight
2148
+ }
2149
+ ),
2150
+ /* @__PURE__ */ jsx10(
2151
+ DetailPane,
2152
+ {
2153
+ selectedAgent,
2154
+ selectedSkill,
2155
+ focused: state.focus === "detail",
2156
+ loadingAgentName: loadingAgent?.name ?? null,
2157
+ width: detailWidth,
2158
+ height: bodyHeight
2159
+ }
2160
+ )
2161
+ ] }),
2162
+ state.modal?.kind === "help" ? /* @__PURE__ */ jsx10(HelpOverlay, {}) : null,
2163
+ state.modal?.kind === "confirm-adopt" || state.modal?.kind === "confirm-adopt-all" || state.modal?.kind === "confirm-remove" || state.modal?.kind === "confirm-discard-dirty-form" ? /* @__PURE__ */ jsx10(ConfirmDialog, { modal: state.modal }) : null,
2164
+ state.modal === null ? /* @__PURE__ */ jsx10(Footer, { actions, search: state.search }) : largeModal ? null : /* @__PURE__ */ jsx10(Box10, { height: 3 })
2165
+ ] });
2166
+ }
2167
+
2168
+ // src/tui/app.tsx
2169
+ import { jsx as jsx11 } from "react/jsx-runtime";
2170
+ var defaultServices2 = {
2171
+ loadDashboardState,
2172
+ dispatchTuiAction
2173
+ };
2174
+ var addAgentFieldOrder = [
2175
+ "id",
2176
+ "root",
2177
+ "skills",
2178
+ "name",
2179
+ "platforms",
2180
+ "disabledByDefault"
2181
+ ];
2182
+ var editAgentFieldOrder = [
2183
+ "root",
2184
+ "skills",
2185
+ "name",
2186
+ "platforms",
2187
+ "enabledByDefault",
2188
+ "disabledByDefault"
2189
+ ];
2190
+ var importFieldOrder = ["sourcePath", "skillName"];
2191
+ var platformOptions2 = ["win32", "linux", "darwin"];
2192
+ function clampIndex(index, count) {
2193
+ if (count <= 0) {
2194
+ return 0;
2195
+ }
2196
+ return (index % count + count) % count;
2197
+ }
2198
+ function nextText(value, input) {
2199
+ return `${value}${input}`;
2200
+ }
2201
+ function trimLast(value) {
2202
+ return value.slice(0, Math.max(value.length - 1, 0));
2203
+ }
2204
+ function togglePlatform(values, platform) {
2205
+ return values.includes(platform) ? values.filter((entry) => entry !== platform) : [...values, platform];
2206
+ }
2207
+ function commandFailureMessage(error) {
2208
+ return `Action failed: ${errorReason2(error)}`;
2209
+ }
2210
+ function restoreFailedFormModal(modal, message) {
2211
+ switch (modal.kind) {
2212
+ case "add-agent":
2213
+ return {
2214
+ kind: "add-agent",
2215
+ form: {
2216
+ ...modal.form,
2217
+ error: message
2218
+ }
2219
+ };
2220
+ case "edit-agent":
2221
+ return {
2222
+ kind: "edit-agent",
2223
+ agentId: modal.agentId,
2224
+ form: {
2225
+ ...modal.form,
2226
+ error: message
2227
+ }
2228
+ };
2229
+ case "import":
2230
+ return {
2231
+ kind: "import",
2232
+ form: {
2233
+ ...modal.form,
2234
+ error: message
2235
+ }
2236
+ };
2237
+ }
2238
+ }
2239
+ function errorReason2(error) {
2240
+ const message = error instanceof Error ? error.message : String(error);
2241
+ const firstLine = message.split(/\r?\n/u)[0]?.trim();
2242
+ return firstLine === void 0 || firstLine.length === 0 ? "Unknown error" : firstLine;
2243
+ }
2244
+ function loadOptions(props, selectedAgentId, selectedSkillId) {
2245
+ return {
2246
+ homeDir: props.homeDir,
2247
+ skillmuxHome: props.skillmuxHome,
2248
+ platform: props.platform,
2249
+ selectedAgentId,
2250
+ selectedSkillId
2251
+ };
2252
+ }
2253
+ function replaceStateModel(previous, model, statusMessage) {
2254
+ const next = createInitialTuiState(model);
2255
+ return {
2256
+ ...next,
2257
+ focus: previous.focus,
2258
+ search: previous.search,
2259
+ statusMessage,
2260
+ modal: null,
2261
+ busy: false
2262
+ };
2263
+ }
2264
+ function isTextInput(input) {
2265
+ return input.length > 0 && !/[\u0000-\u001F\u007F]/u.test(input);
2266
+ }
2267
+ function parseBridgedSize(value) {
2268
+ try {
2269
+ const parsed = JSON.parse(value);
2270
+ if (typeof parsed.columns === "number" && typeof parsed.rows === "number" && Number.isFinite(parsed.columns) && Number.isFinite(parsed.rows)) {
2271
+ return {
2272
+ columns: parsed.columns,
2273
+ rows: parsed.rows
2274
+ };
2275
+ }
2276
+ } catch {
2277
+ return null;
2278
+ }
2279
+ return null;
2280
+ }
2281
+ function liveTerminalSize() {
2282
+ return {
2283
+ columns: process.stdout.columns ?? 80,
2284
+ rows: process.stdout.rows ?? 24
2285
+ };
2286
+ }
2287
+ function readSizeFile(path) {
2288
+ try {
2289
+ return parseBridgedSize(readFileSync(path, "utf8"));
2290
+ } catch {
2291
+ return null;
2292
+ }
2293
+ }
2294
+ function useBridgedTerminalSize(path) {
2295
+ const [size, setSize] = useState(
2296
+ () => path === null ? null : readSizeFile(path)
2297
+ );
2298
+ useEffect(() => {
2299
+ if (path === null) {
2300
+ setSize(null);
2301
+ return;
2302
+ }
2303
+ let cancelled = false;
2304
+ const refresh = () => {
2305
+ if (cancelled) {
2306
+ return;
2307
+ }
2308
+ const nextSize = readSizeFile(path);
2309
+ setSize(
2310
+ (current) => nextSize === null ? current : current !== null && current.columns === nextSize.columns && current.rows === nextSize.rows ? current : nextSize
2311
+ );
2312
+ };
2313
+ refresh();
2314
+ const timer = setInterval(refresh, 50);
2315
+ return () => {
2316
+ cancelled = true;
2317
+ clearInterval(timer);
2318
+ };
2319
+ }, [path]);
2320
+ return size;
2321
+ }
2322
+ function LiveDashboardViewport({
2323
+ state,
2324
+ terminalWidth,
2325
+ terminalHeight,
2326
+ modalInteraction
2327
+ }) {
2328
+ return /* @__PURE__ */ jsx11(
2329
+ Dashboard,
2330
+ {
2331
+ state,
2332
+ width: terminalWidth ?? liveTerminalSize().columns,
2333
+ height: terminalHeight ?? liveTerminalSize().rows,
2334
+ modalInteraction
2335
+ }
2336
+ );
2337
+ }
2338
+ function BridgedDashboardViewport({
2339
+ state,
2340
+ terminalWidth,
2341
+ terminalHeight,
2342
+ bridgePath,
2343
+ modalInteraction
2344
+ }) {
2345
+ const bridgedTerminalSize = useBridgedTerminalSize(bridgePath ?? null);
2346
+ const fallbackSize = liveTerminalSize();
2347
+ return /* @__PURE__ */ jsx11(
2348
+ Dashboard,
2349
+ {
2350
+ state,
2351
+ width: terminalWidth ?? bridgedTerminalSize?.columns ?? fallbackSize.columns,
2352
+ height: terminalHeight ?? bridgedTerminalSize?.rows ?? fallbackSize.rows,
2353
+ modalInteraction
2354
+ }
2355
+ );
2356
+ }
2357
+ function App({
2358
+ homeDir,
2359
+ skillmuxHome,
2360
+ platform,
2361
+ terminalWidth,
2362
+ terminalHeight,
2363
+ services: serviceOverrides
2364
+ }) {
2365
+ const { exit } = useApp();
2366
+ const [state, setState] = useState(null);
2367
+ const [loadError, setLoadError] = useState(null);
2368
+ const requestSequence = useRef(0);
2369
+ const latestRequest = useRef(0);
2370
+ const activeActionRequest = useRef(null);
2371
+ const stableModel = useRef(null);
2372
+ const [modalInteraction, setModalInteraction] = useState({
2373
+ fieldIndex: 0,
2374
+ platformIndex: 0,
2375
+ doctorScrollOffset: 0
2376
+ });
2377
+ const services = useMemo(
2378
+ () => ({ ...defaultServices2, ...serviceOverrides }),
2379
+ [serviceOverrides]
2380
+ );
2381
+ const sizeBridgePath = process.env.SKILLMUX_TUI_PTY_SIZE_FILE?.trim() ?? null;
2382
+ const sizeBridgeEnabled = sizeBridgePath !== null && sizeBridgePath.length > 0;
2383
+ const modalKind = state?.modal?.kind ?? null;
2384
+ const beginRequest = useCallback(() => {
2385
+ requestSequence.current += 1;
2386
+ latestRequest.current = requestSequence.current;
2387
+ return requestSequence.current;
2388
+ }, []);
2389
+ const isLatestRequest = useCallback((requestId) => {
2390
+ return latestRequest.current === requestId;
2391
+ }, []);
2392
+ const startBusyState = useCallback(
2393
+ (baseState, action) => updateTuiState(
2394
+ updateTuiState(baseState, { type: "set-busy", busy: true }),
2395
+ {
2396
+ type: "set-status",
2397
+ message: action === "scan" ? "scanning..." : "working..."
2398
+ }
2399
+ ),
2400
+ []
2401
+ );
2402
+ useEffect(() => {
2403
+ let cancelled = false;
2404
+ services.loadDashboardState(loadOptions({ homeDir, skillmuxHome, platform })).then((model) => {
2405
+ if (!cancelled) {
2406
+ stableModel.current = model;
2407
+ setState(createInitialTuiState(model));
2408
+ setLoadError(null);
2409
+ }
2410
+ }).catch((error) => {
2411
+ if (!cancelled) {
2412
+ setLoadError(`Failed to load dashboard: ${errorReason2(error)}`);
2413
+ }
2414
+ });
2415
+ return () => {
2416
+ cancelled = true;
2417
+ };
2418
+ }, [homeDir, platform, services, skillmuxHome]);
2419
+ useEffect(() => {
2420
+ setModalInteraction({
2421
+ fieldIndex: 0,
2422
+ platformIndex: 0,
2423
+ doctorScrollOffset: 0
2424
+ });
2425
+ }, [modalKind]);
2426
+ const runAction = useCallback(
2427
+ (action, model, baseState) => {
2428
+ if (activeActionRequest.current !== null) {
2429
+ return;
2430
+ }
2431
+ const requestId = beginRequest();
2432
+ activeActionRequest.current = requestId;
2433
+ setState(
2434
+ (current) => current === null ? current : startBusyState(baseState ?? current, action)
2435
+ );
2436
+ services.dispatchTuiAction({
2437
+ action,
2438
+ model,
2439
+ homeDir,
2440
+ skillmuxHome,
2441
+ platform
2442
+ }).then((result) => {
2443
+ if (!isLatestRequest(requestId)) {
2444
+ return;
2445
+ }
2446
+ stableModel.current = result.model;
2447
+ setState(
2448
+ (current) => current === null ? current : replaceStateModel(current, result.model, result.statusMessage)
2449
+ );
2450
+ }).catch((error) => {
2451
+ if (!isLatestRequest(requestId)) {
2452
+ return;
2453
+ }
2454
+ setState(
2455
+ (current) => current === null ? current : updateTuiState(
2456
+ updateTuiState(current, { type: "set-busy", busy: false }),
2457
+ {
2458
+ type: "set-status",
2459
+ message: `Action failed: ${errorReason2(error)}`
2460
+ }
2461
+ )
2462
+ );
2463
+ }).finally(() => {
2464
+ if (activeActionRequest.current === requestId) {
2465
+ activeActionRequest.current = null;
2466
+ }
2467
+ });
2468
+ },
2469
+ [
2470
+ beginRequest,
2471
+ homeDir,
2472
+ isLatestRequest,
2473
+ platform,
2474
+ services,
2475
+ skillmuxHome,
2476
+ startBusyState
2477
+ ]
2478
+ );
2479
+ const reloadAgent = useCallback(
2480
+ (agentId) => {
2481
+ const requestId = beginRequest();
2482
+ setState(
2483
+ (current) => current === null ? current : updateTuiState(
2484
+ updateTuiState(current, { type: "set-busy", busy: true }),
2485
+ { type: "set-status", message: "loading agent..." }
2486
+ )
2487
+ );
2488
+ services.loadDashboardState(loadOptions({ homeDir, skillmuxHome, platform }, agentId)).then((model) => {
2489
+ if (!isLatestRequest(requestId)) {
2490
+ return;
2491
+ }
2492
+ stableModel.current = model;
2493
+ setState(
2494
+ (current) => current === null ? current : replaceStateModel(current, model, null)
2495
+ );
2496
+ }).catch((error) => {
2497
+ if (!isLatestRequest(requestId)) {
2498
+ return;
2499
+ }
2500
+ setState(
2501
+ (current) => current === null ? current : stableModel.current === null ? updateTuiState(
2502
+ updateTuiState(current, { type: "set-busy", busy: false }),
2503
+ {
2504
+ type: "set-status",
2505
+ message: `Load failed: ${errorReason2(error)}`
2506
+ }
2507
+ ) : replaceStateModel(
2508
+ current,
2509
+ stableModel.current,
2510
+ `Load failed: ${errorReason2(error)}`
2511
+ )
2512
+ );
2513
+ });
2514
+ },
2515
+ [beginRequest, homeDir, isLatestRequest, platform, services, skillmuxHome]
2516
+ );
2517
+ useEffect(() => {
2518
+ if (state?.pendingAction === null || state?.pendingAction === void 0) {
2519
+ return;
2520
+ }
2521
+ const consumed = consumeActionIntent(state);
2522
+ setState(consumed.state);
2523
+ if (consumed.action !== null) {
2524
+ runAction(consumed.action, consumed.state.model);
2525
+ }
2526
+ }, [runAction, state]);
2527
+ useEffect(() => {
2528
+ if (state?.pendingAgentId === null || state?.pendingAgentId === void 0) {
2529
+ return;
2530
+ }
2531
+ const consumed = consumeAgentSelectionIntent(state);
2532
+ setState(consumed.state);
2533
+ if (consumed.agentId !== null) {
2534
+ reloadAgent(consumed.agentId);
2535
+ }
2536
+ }, [reloadAgent, state]);
2537
+ useEffect(() => {
2538
+ if (state?.pendingCommand === null || state?.pendingCommand === void 0) {
2539
+ return;
2540
+ }
2541
+ const consumed = consumePendingCommandIntent(state);
2542
+ setState(consumed.state);
2543
+ if (consumed.command === null) {
2544
+ return;
2545
+ }
2546
+ if (consumed.command.kind === "doctor") {
2547
+ const requestId2 = beginRequest();
2548
+ services.dispatchTuiAction({
2549
+ action: consumed.command,
2550
+ model: consumed.state.model,
2551
+ homeDir,
2552
+ skillmuxHome,
2553
+ platform
2554
+ }).then((result) => {
2555
+ if (!isLatestRequest(requestId2)) {
2556
+ return;
2557
+ }
2558
+ if (result.commandSucceeded === false) {
2559
+ const failureMessage = result.statusMessage;
2560
+ setState(
2561
+ (current) => current === null ? current : updateTuiState(
2562
+ updateTuiState(current, {
2563
+ type: "doctor-result-failed",
2564
+ errorMessage: failureMessage
2565
+ }),
2566
+ { type: "set-status", message: failureMessage }
2567
+ )
2568
+ );
2569
+ return;
2570
+ }
2571
+ const doctorReport = result.doctor;
2572
+ if (doctorReport === void 0) {
2573
+ throw new Error("Doctor report missing");
2574
+ }
2575
+ stableModel.current = result.model;
2576
+ setState(
2577
+ (current) => current === null ? current : updateTuiState(
2578
+ updateTuiState(current, {
2579
+ type: "doctor-result-loaded",
2580
+ report: doctorReport
2581
+ }),
2582
+ { type: "set-status", message: result.statusMessage }
2583
+ )
2584
+ );
2585
+ }).catch((error) => {
2586
+ if (!isLatestRequest(requestId2)) {
2587
+ return;
2588
+ }
2589
+ setState(
2590
+ (current) => current === null ? current : updateTuiState(
2591
+ updateTuiState(current, {
2592
+ type: "doctor-result-failed",
2593
+ errorMessage: `Doctor failed: ${errorReason2(error)}`
2594
+ }),
2595
+ { type: "set-status", message: `Doctor failed: ${errorReason2(error)}` }
2596
+ )
2597
+ );
2598
+ });
2599
+ return;
2600
+ }
2601
+ if (activeActionRequest.current !== null) {
2602
+ return;
2603
+ }
2604
+ const requestId = beginRequest();
2605
+ activeActionRequest.current = requestId;
2606
+ const busyState = {
2607
+ ...consumed.state,
2608
+ modal: null,
2609
+ busy: true,
2610
+ statusMessage: "working..."
2611
+ };
2612
+ setState(busyState);
2613
+ services.dispatchTuiAction({
2614
+ action: consumed.command,
2615
+ model: busyState.model,
2616
+ homeDir,
2617
+ skillmuxHome,
2618
+ platform
2619
+ }).then((result) => {
2620
+ if (!isLatestRequest(requestId)) {
2621
+ return;
2622
+ }
2623
+ if (result.commandSucceeded === false) {
2624
+ const failureMessage = result.statusMessage;
2625
+ if (consumed.command?.kind === "doctor") {
2626
+ setState({
2627
+ ...consumed.state,
2628
+ busy: false,
2629
+ statusMessage: failureMessage,
2630
+ modal: {
2631
+ kind: "doctor",
2632
+ status: "error",
2633
+ errorMessage: failureMessage
2634
+ }
2635
+ });
2636
+ return;
2637
+ }
2638
+ if (consumed.state.modal !== null && "form" in consumed.state.modal) {
2639
+ setState({
2640
+ ...consumed.state,
2641
+ busy: false,
2642
+ statusMessage: failureMessage,
2643
+ modal: restoreFailedFormModal(consumed.state.modal, failureMessage)
2644
+ });
2645
+ return;
2646
+ }
2647
+ setState({
2648
+ ...consumed.state,
2649
+ busy: false,
2650
+ statusMessage: failureMessage
2651
+ });
2652
+ return;
2653
+ }
2654
+ stableModel.current = result.model;
2655
+ setState(
2656
+ (current) => current === null ? current : replaceStateModel(current, result.model, result.statusMessage)
2657
+ );
2658
+ }).catch((error) => {
2659
+ if (!isLatestRequest(requestId)) {
2660
+ return;
2661
+ }
2662
+ const failedModal = consumed.state.modal;
2663
+ const failureMessage = commandFailureMessage(error);
2664
+ if (failedModal === null || failedModal.kind !== "add-agent" && failedModal.kind !== "edit-agent" && failedModal.kind !== "import") {
2665
+ setState({
2666
+ ...consumed.state,
2667
+ busy: false,
2668
+ statusMessage: failureMessage
2669
+ });
2670
+ return;
2671
+ }
2672
+ setState({
2673
+ ...consumed.state,
2674
+ busy: false,
2675
+ statusMessage: failureMessage,
2676
+ modal: restoreFailedFormModal(failedModal, failureMessage)
2677
+ });
2678
+ }).finally(() => {
2679
+ if (activeActionRequest.current === requestId) {
2680
+ activeActionRequest.current = null;
2681
+ }
2682
+ });
2683
+ }, [
2684
+ beginRequest,
2685
+ homeDir,
2686
+ isLatestRequest,
2687
+ platform,
2688
+ services,
2689
+ skillmuxHome,
2690
+ state
2691
+ ]);
2692
+ useInput((input, key) => {
2693
+ if (key.ctrl && input === "c") {
2694
+ exit();
2695
+ return;
2696
+ }
2697
+ if (state === null) {
2698
+ if (input === "q") {
2699
+ exit();
2700
+ }
2701
+ return;
2702
+ }
2703
+ if (state.search !== null) {
2704
+ if (key.escape || input === "\x1B") {
2705
+ setState(updateTuiState(state, { type: "close" }));
2706
+ return;
2707
+ }
2708
+ if (key.return) {
2709
+ setState(updateTuiState(state, { type: "submit-search" }));
2710
+ return;
2711
+ }
2712
+ if (key.backspace || key.delete) {
2713
+ setState(
2714
+ updateTuiState(state, {
2715
+ type: "search-query-changed",
2716
+ query: state.search.query.slice(0, -1)
2717
+ })
2718
+ );
2719
+ return;
2720
+ }
2721
+ if (!key.ctrl && !key.meta && isTextInput(input)) {
2722
+ setState(
2723
+ updateTuiState(state, {
2724
+ type: "search-query-changed",
2725
+ query: `${state.search.query}${input}`
2726
+ })
2727
+ );
2728
+ }
2729
+ return;
2730
+ }
2731
+ if (state.modal !== null) {
2732
+ if (input === "q") {
2733
+ if (state.modal.kind === "confirm-discard-dirty-form" || "form" in state.modal && state.modal.form.dirty) {
2734
+ setState(updateTuiState(state, { type: "close" }));
2735
+ return;
2736
+ }
2737
+ exit();
2738
+ return;
2739
+ }
2740
+ if (activeActionRequest.current !== null) {
2741
+ return;
2742
+ }
2743
+ if (state.modal.kind === "help") {
2744
+ if (key.escape || input === "\x1B") {
2745
+ setState(updateTuiState(state, { type: "close" }));
2746
+ }
2747
+ return;
2748
+ }
2749
+ if (state.modal.kind === "confirm-discard-dirty-form") {
2750
+ if (input.toLocaleLowerCase() === "y") {
2751
+ setState(updateTuiState(state, { type: "confirm-discard-dirty-form" }));
2752
+ return;
2753
+ }
2754
+ if (key.escape || input === "\x1B" || input.toLocaleLowerCase() === "n") {
2755
+ setState(updateTuiState(state, { type: "close" }));
2756
+ }
2757
+ return;
2758
+ }
2759
+ if (state.modal.kind === "confirm-adopt" || state.modal.kind === "confirm-adopt-all" || state.modal.kind === "confirm-remove" || state.modal.kind === "confirm-remove-agent") {
2760
+ if (input.toLocaleLowerCase() === "y") {
2761
+ const closedState = updateTuiState(state, { type: "close" });
2762
+ if (state.modal.kind === "confirm-adopt") {
2763
+ runAction("adopt", closedState.model, closedState);
2764
+ } else if (state.modal.kind === "confirm-adopt-all") {
2765
+ runAction("adopt-all", closedState.model, closedState);
2766
+ } else if (state.modal.kind === "confirm-remove") {
2767
+ runAction("remove", closedState.model, closedState);
2768
+ } else {
2769
+ setState(updateTuiState(state, { type: "submit-remove-agent" }));
2770
+ }
2771
+ return;
2772
+ }
2773
+ if (key.escape || input === "\x1B" || input.toLocaleLowerCase() === "n") {
2774
+ setState(updateTuiState(state, { type: "close" }));
2775
+ }
2776
+ return;
2777
+ }
2778
+ if (state.modal.kind === "doctor") {
2779
+ if (key.escape || input === "\x1B") {
2780
+ setState(updateTuiState(state, { type: "close" }));
2781
+ return;
2782
+ }
2783
+ if (key.downArrow || key.rightArrow || input === "j") {
2784
+ setModalInteraction((current) => ({
2785
+ ...current,
2786
+ doctorScrollOffset: current.doctorScrollOffset + 1
2787
+ }));
2788
+ return;
2789
+ }
2790
+ if (key.upArrow || key.leftArrow || input === "k") {
2791
+ setModalInteraction((current) => ({
2792
+ ...current,
2793
+ doctorScrollOffset: Math.max(current.doctorScrollOffset - 1, 0)
2794
+ }));
2795
+ return;
2796
+ }
2797
+ return;
2798
+ }
2799
+ if (state.modal.kind === "add-agent" || state.modal.kind === "edit-agent" || state.modal.kind === "import") {
2800
+ const fieldOrder = state.modal.kind === "add-agent" ? addAgentFieldOrder : state.modal.kind === "edit-agent" ? editAgentFieldOrder : importFieldOrder;
2801
+ const submitFieldIndex = fieldOrder.length;
2802
+ const currentFieldIndex = clampIndex(
2803
+ modalInteraction.fieldIndex,
2804
+ fieldOrder.length + 1
2805
+ );
2806
+ const currentField = fieldOrder[currentFieldIndex];
2807
+ const moveField = (direction) => {
2808
+ setModalInteraction((current) => ({
2809
+ ...current,
2810
+ fieldIndex: clampIndex(
2811
+ current.fieldIndex + direction,
2812
+ fieldOrder.length + 1
2813
+ ),
2814
+ platformIndex: 0
2815
+ }));
2816
+ };
2817
+ const updateAddAgent = (field, value) => {
2818
+ setState(
2819
+ updateTuiState(state, {
2820
+ type: "add-agent-form-field-changed",
2821
+ field,
2822
+ value
2823
+ })
2824
+ );
2825
+ };
2826
+ const updateEditAgent = (field, value) => {
2827
+ setState(
2828
+ updateTuiState(state, {
2829
+ type: "edit-agent-form-field-changed",
2830
+ field,
2831
+ value
2832
+ })
2833
+ );
2834
+ };
2835
+ const updateImport = (field, value) => {
2836
+ setState(
2837
+ updateTuiState(state, {
2838
+ type: "import-form-field-changed",
2839
+ field,
2840
+ value
2841
+ })
2842
+ );
2843
+ };
2844
+ if (key.escape || input === "\x1B") {
2845
+ setState(updateTuiState(state, { type: "close" }));
2846
+ return;
2847
+ }
2848
+ if (key.return && currentFieldIndex === submitFieldIndex) {
2849
+ if (state.modal.kind === "add-agent") {
2850
+ setState(updateTuiState(state, { type: "submit-add-agent-form" }));
2851
+ } else if (state.modal.kind === "edit-agent") {
2852
+ setState(updateTuiState(state, { type: "submit-edit-agent-form" }));
2853
+ } else {
2854
+ setState(updateTuiState(state, { type: "submit-import-form" }));
2855
+ }
2856
+ return;
2857
+ }
2858
+ if (key.downArrow) {
2859
+ moveField(1);
2860
+ return;
2861
+ }
2862
+ if (key.upArrow) {
2863
+ moveField(-1);
2864
+ return;
2865
+ }
2866
+ if (currentField === void 0) {
2867
+ return;
2868
+ }
2869
+ if (currentField === "platforms") {
2870
+ const platform2 = platformOptions2[modalInteraction.platformIndex] ?? platformOptions2[0];
2871
+ if (key.leftArrow) {
2872
+ setModalInteraction((current) => ({
2873
+ ...current,
2874
+ platformIndex: clampIndex(current.platformIndex - 1, platformOptions2.length)
2875
+ }));
2876
+ return;
2877
+ }
2878
+ if (key.rightArrow) {
2879
+ setModalInteraction((current) => ({
2880
+ ...current,
2881
+ platformIndex: clampIndex(current.platformIndex + 1, platformOptions2.length)
2882
+ }));
2883
+ return;
2884
+ }
2885
+ if (input === " ") {
2886
+ if (state.modal.kind === "add-agent") {
2887
+ updateAddAgent(
2888
+ "platforms",
2889
+ togglePlatform(state.modal.form.values.platforms, platform2)
2890
+ );
2891
+ } else if (state.modal.kind === "edit-agent") {
2892
+ updateEditAgent(
2893
+ "platforms",
2894
+ togglePlatform(state.modal.form.values.platforms, platform2)
2895
+ );
2896
+ }
2897
+ return;
2898
+ }
2899
+ }
2900
+ if (currentField === "disabledByDefault" && state.modal.kind === "add-agent") {
2901
+ if (input === " ") {
2902
+ updateAddAgent("disabledByDefault", !state.modal.form.values.disabledByDefault);
2903
+ return;
2904
+ }
2905
+ }
2906
+ if (state.modal.kind === "edit-agent") {
2907
+ if (currentField === "enabledByDefault") {
2908
+ if (input === " ") {
2909
+ updateEditAgent("enabledByDefault", !state.modal.form.values.enabledByDefault);
2910
+ return;
2911
+ }
2912
+ }
2913
+ if (currentField === "disabledByDefault") {
2914
+ if (input === " ") {
2915
+ updateEditAgent(
2916
+ "disabledByDefault",
2917
+ !state.modal.form.values.disabledByDefault
2918
+ );
2919
+ return;
2920
+ }
2921
+ }
2922
+ }
2923
+ if (isTextInput(input)) {
2924
+ if (state.modal.kind === "add-agent") {
2925
+ if (currentField === "id") {
2926
+ updateAddAgent("id", nextText(state.modal.form.values.id, input));
2927
+ return;
2928
+ }
2929
+ if (currentField === "root") {
2930
+ updateAddAgent("root", nextText(state.modal.form.values.root, input));
2931
+ return;
2932
+ }
2933
+ if (currentField === "skills") {
2934
+ updateAddAgent("skills", nextText(state.modal.form.values.skills, input));
2935
+ return;
2936
+ }
2937
+ if (currentField === "name") {
2938
+ updateAddAgent("name", nextText(state.modal.form.values.name, input));
2939
+ return;
2940
+ }
2941
+ } else if (state.modal.kind === "edit-agent") {
2942
+ if (currentField === "root") {
2943
+ updateEditAgent("root", nextText(state.modal.form.values.root, input));
2944
+ return;
2945
+ }
2946
+ if (currentField === "skills") {
2947
+ updateEditAgent("skills", nextText(state.modal.form.values.skills, input));
2948
+ return;
2949
+ }
2950
+ if (currentField === "name") {
2951
+ updateEditAgent("name", nextText(state.modal.form.values.name, input));
2952
+ return;
2953
+ }
2954
+ } else if (state.modal.kind === "import") {
2955
+ if (currentField === "sourcePath") {
2956
+ updateImport("sourcePath", nextText(state.modal.form.values.sourcePath, input));
2957
+ return;
2958
+ }
2959
+ if (currentField === "skillName") {
2960
+ updateImport("skillName", nextText(state.modal.form.values.skillName, input));
2961
+ return;
2962
+ }
2963
+ }
2964
+ }
2965
+ if (key.backspace || key.delete) {
2966
+ if (state.modal.kind === "add-agent") {
2967
+ if (currentField === "id") {
2968
+ updateAddAgent("id", trimLast(state.modal.form.values.id));
2969
+ return;
2970
+ }
2971
+ if (currentField === "root") {
2972
+ updateAddAgent("root", trimLast(state.modal.form.values.root));
2973
+ return;
2974
+ }
2975
+ if (currentField === "skills") {
2976
+ updateAddAgent("skills", trimLast(state.modal.form.values.skills));
2977
+ return;
2978
+ }
2979
+ if (currentField === "name") {
2980
+ updateAddAgent("name", trimLast(state.modal.form.values.name));
2981
+ return;
2982
+ }
2983
+ } else if (state.modal.kind === "edit-agent") {
2984
+ if (currentField === "root") {
2985
+ updateEditAgent("root", trimLast(state.modal.form.values.root));
2986
+ return;
2987
+ }
2988
+ if (currentField === "skills") {
2989
+ updateEditAgent("skills", trimLast(state.modal.form.values.skills));
2990
+ return;
2991
+ }
2992
+ if (currentField === "name") {
2993
+ updateEditAgent("name", trimLast(state.modal.form.values.name));
2994
+ return;
2995
+ }
2996
+ } else if (state.modal.kind === "import") {
2997
+ if (currentField === "sourcePath") {
2998
+ updateImport("sourcePath", trimLast(state.modal.form.values.sourcePath));
2999
+ return;
3000
+ }
3001
+ if (currentField === "skillName") {
3002
+ updateImport("skillName", trimLast(state.modal.form.values.skillName));
3003
+ return;
3004
+ }
3005
+ }
3006
+ }
3007
+ return;
3008
+ }
3009
+ return;
3010
+ }
3011
+ if (input === "q") {
3012
+ exit();
3013
+ return;
3014
+ }
3015
+ if (activeActionRequest.current !== null) {
3016
+ return;
3017
+ }
3018
+ if (key.rightArrow) {
3019
+ setState(
3020
+ updateTuiState(state, {
3021
+ type: "focus-next"
3022
+ })
3023
+ );
3024
+ return;
3025
+ }
3026
+ if (key.leftArrow) {
3027
+ setState(
3028
+ updateTuiState(state, {
3029
+ type: "focus-previous"
3030
+ })
3031
+ );
3032
+ return;
3033
+ }
3034
+ if (key.downArrow || input === "j") {
3035
+ setState(updateTuiState(state, { type: "next-row" }));
3036
+ return;
3037
+ }
3038
+ if (key.upArrow || input === "k") {
3039
+ setState(updateTuiState(state, { type: "previous-row" }));
3040
+ return;
3041
+ }
3042
+ if (input === "g") {
3043
+ setState(updateTuiState(state, { type: "first-row" }));
3044
+ return;
3045
+ }
3046
+ if (input === "G") {
3047
+ setState(updateTuiState(state, { type: "last-row" }));
3048
+ return;
3049
+ }
3050
+ if (input === "/") {
3051
+ setState(updateTuiState(state, { type: "open-search" }));
3052
+ return;
3053
+ }
3054
+ if (input === "?") {
3055
+ setState(updateTuiState(state, { type: "open-help" }));
3056
+ return;
3057
+ }
3058
+ if (input === "n") {
3059
+ setState(updateTuiState(state, { type: "open-add-agent" }));
3060
+ return;
3061
+ }
3062
+ if (input === "e") {
3063
+ setState(updateTuiState(state, { type: "open-edit-agent" }));
3064
+ return;
3065
+ }
3066
+ if (input === "X" || key.shift && input === "x") {
3067
+ setState(updateTuiState(state, { type: "open-remove-agent" }));
3068
+ return;
3069
+ }
3070
+ if (input === "i") {
3071
+ setState(updateTuiState(state, { type: "open-import" }));
3072
+ return;
3073
+ }
3074
+ if (input === "d") {
3075
+ setState(updateTuiState(state, { type: "open-doctor" }));
3076
+ return;
3077
+ }
3078
+ if (key.escape || input === "\x1B") {
3079
+ setState(updateTuiState(state, { type: "close" }));
3080
+ return;
3081
+ }
3082
+ if (input === " ") {
3083
+ setState(updateTuiState(state, { type: "request-toggle" }));
3084
+ return;
3085
+ }
3086
+ if (input === "A" || key.shift && input === "a") {
3087
+ setState(updateTuiState(state, { type: "request-adopt-all" }));
3088
+ return;
3089
+ }
3090
+ if (input === "a") {
3091
+ setState(updateTuiState(state, { type: "request-adopt" }));
3092
+ return;
3093
+ }
3094
+ if (input === "r") {
3095
+ setState(updateTuiState(state, { type: "request-remove" }));
3096
+ return;
3097
+ }
3098
+ if (input === "s") {
3099
+ setState(updateTuiState(state, { type: "request-scan" }));
3100
+ }
3101
+ });
3102
+ if (loadError !== null) {
3103
+ return /* @__PURE__ */ jsx11(Text11, { color: "red", children: loadError });
3104
+ }
3105
+ if (state === null) {
3106
+ return /* @__PURE__ */ jsx11(Text11, { children: "loading dashboard..." });
3107
+ }
3108
+ return sizeBridgeEnabled ? /* @__PURE__ */ jsx11(
3109
+ BridgedDashboardViewport,
3110
+ {
3111
+ state,
3112
+ terminalWidth,
3113
+ terminalHeight,
3114
+ bridgePath: sizeBridgePath,
3115
+ modalInteraction
3116
+ }
3117
+ ) : /* @__PURE__ */ jsx11(
3118
+ LiveDashboardViewport,
3119
+ {
3120
+ state,
3121
+ terminalWidth,
3122
+ terminalHeight,
3123
+ modalInteraction
3124
+ }
3125
+ );
3126
+ }
3127
+
3128
+ // src/tui/launch-tui.tsx
3129
+ import { jsx as jsx12 } from "react/jsx-runtime";
3130
+ var alternateScreenEnter = "\x1B[?1049h";
3131
+ var alternateScreenExit = "\x1B[?1049l";
3132
+ var cursorHide = "\x1B[?25l";
3133
+ var cursorShow = "\x1B[?25h";
3134
+ var lifecycleTraceEnabled = process.env.SKILLMUX_TUI_PTY_TRACE === "1";
3135
+ async function launchTui(options = {}) {
3136
+ let failure;
3137
+ let instance = null;
3138
+ let sigintRequested = false;
3139
+ const handleSigint = () => {
3140
+ sigintRequested = true;
3141
+ instance?.unmount();
3142
+ };
3143
+ try {
3144
+ process.once("SIGINT", handleSigint);
3145
+ process.stdout.write(alternateScreenEnter);
3146
+ process.stdout.write(cursorHide);
3147
+ await writeLifecycleTrace("alt-screen-enter");
3148
+ instance = render(/* @__PURE__ */ jsx12(App, { ...options }));
3149
+ if (sigintRequested) {
3150
+ instance.unmount();
3151
+ }
3152
+ await instance.waitUntilExit();
3153
+ await writeLifecycleTrace("session-exit-clean");
3154
+ } catch (error) {
3155
+ failure = error;
3156
+ } finally {
3157
+ process.removeListener("SIGINT", handleSigint);
3158
+ failure = await runCleanup(failure, () => writeLifecycleTrace("alt-screen-exit"));
3159
+ failure = await runCleanup(failure, () => process.stdout.write(alternateScreenExit));
3160
+ failure = await runCleanup(failure, () => process.stdout.write(cursorShow));
3161
+ }
3162
+ if (failure !== void 0) {
3163
+ throw failure;
3164
+ }
3165
+ }
3166
+ function writeLifecycleTrace(stage) {
3167
+ if (!lifecycleTraceEnabled) {
3168
+ return Promise.resolve();
3169
+ }
3170
+ process.stderr.write(`[skillmux:${stage}]
3171
+ `);
3172
+ return stage === "session-exit-clean" ? sleep(0) : Promise.resolve();
3173
+ }
3174
+ async function runCleanup(failure, cleanup) {
3175
+ try {
3176
+ await cleanup();
3177
+ return failure;
3178
+ } catch (error) {
3179
+ return failure === void 0 ? error : failure;
3180
+ }
3181
+ }
3182
+ function sleep(ms) {
3183
+ return new Promise((resolve2) => setTimeout(resolve2, ms));
3184
+ }
3185
+ export {
3186
+ launchTui
3187
+ };
3188
+ //# sourceMappingURL=launch-tui-IC7COEGE.js.map