skillmux 0.1.3 → 0.1.5

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