skill-flow 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (87) hide show
  1. package/LICENSE +190 -0
  2. package/README.md +108 -0
  3. package/README.zh.md +108 -0
  4. package/dist/adapters/channel-adapters.d.ts +8 -0
  5. package/dist/adapters/channel-adapters.js +56 -0
  6. package/dist/adapters/channel-adapters.js.map +1 -0
  7. package/dist/cli.d.ts +2 -0
  8. package/dist/cli.js +118 -0
  9. package/dist/cli.js.map +1 -0
  10. package/dist/domain/types.d.ts +133 -0
  11. package/dist/domain/types.js +2 -0
  12. package/dist/domain/types.js.map +1 -0
  13. package/dist/services/deployment-applier.d.ts +6 -0
  14. package/dist/services/deployment-applier.js +54 -0
  15. package/dist/services/deployment-applier.js.map +1 -0
  16. package/dist/services/deployment-planner.d.ts +11 -0
  17. package/dist/services/deployment-planner.js +179 -0
  18. package/dist/services/deployment-planner.js.map +1 -0
  19. package/dist/services/doctor-service.d.ts +5 -0
  20. package/dist/services/doctor-service.js +129 -0
  21. package/dist/services/doctor-service.js.map +1 -0
  22. package/dist/services/inventory-service.d.ts +14 -0
  23. package/dist/services/inventory-service.js +186 -0
  24. package/dist/services/inventory-service.js.map +1 -0
  25. package/dist/services/skill-flow.d.ts +60 -0
  26. package/dist/services/skill-flow.js +260 -0
  27. package/dist/services/skill-flow.js.map +1 -0
  28. package/dist/services/source-service.d.ts +35 -0
  29. package/dist/services/source-service.js +270 -0
  30. package/dist/services/source-service.js.map +1 -0
  31. package/dist/services/workflow-service.d.ts +5 -0
  32. package/dist/services/workflow-service.js +32 -0
  33. package/dist/services/workflow-service.js.map +1 -0
  34. package/dist/state/store.d.ts +14 -0
  35. package/dist/state/store.js +59 -0
  36. package/dist/state/store.js.map +1 -0
  37. package/dist/tests/skill-flow.test.d.ts +1 -0
  38. package/dist/tests/skill-flow.test.js +926 -0
  39. package/dist/tests/skill-flow.test.js.map +1 -0
  40. package/dist/tui/config-app.d.ts +47 -0
  41. package/dist/tui/config-app.js +732 -0
  42. package/dist/tui/config-app.js.map +1 -0
  43. package/dist/tui/selection-state.d.ts +8 -0
  44. package/dist/tui/selection-state.js +32 -0
  45. package/dist/tui/selection-state.js.map +1 -0
  46. package/dist/utils/constants.d.ts +19 -0
  47. package/dist/utils/constants.js +164 -0
  48. package/dist/utils/constants.js.map +1 -0
  49. package/dist/utils/format.d.ts +6 -0
  50. package/dist/utils/format.js +45 -0
  51. package/dist/utils/format.js.map +1 -0
  52. package/dist/utils/fs.d.ts +10 -0
  53. package/dist/utils/fs.js +89 -0
  54. package/dist/utils/fs.js.map +1 -0
  55. package/dist/utils/git.d.ts +3 -0
  56. package/dist/utils/git.js +12 -0
  57. package/dist/utils/git.js.map +1 -0
  58. package/dist/utils/result.d.ts +4 -0
  59. package/dist/utils/result.js +15 -0
  60. package/dist/utils/result.js.map +1 -0
  61. package/dist/utils/source-id.d.ts +2 -0
  62. package/dist/utils/source-id.js +16 -0
  63. package/dist/utils/source-id.js.map +1 -0
  64. package/img/img-1.jpg +0 -0
  65. package/package.json +39 -0
  66. package/src/adapters/channel-adapters.ts +75 -0
  67. package/src/cli.tsx +147 -0
  68. package/src/domain/types.ts +175 -0
  69. package/src/services/deployment-applier.ts +81 -0
  70. package/src/services/deployment-planner.ts +259 -0
  71. package/src/services/doctor-service.ts +156 -0
  72. package/src/services/inventory-service.ts +251 -0
  73. package/src/services/skill-flow.ts +381 -0
  74. package/src/services/source-service.ts +427 -0
  75. package/src/services/workflow-service.ts +56 -0
  76. package/src/state/store.ts +68 -0
  77. package/src/tests/skill-flow.test.ts +1184 -0
  78. package/src/tui/config-app.tsx +1094 -0
  79. package/src/tui/selection-state.ts +45 -0
  80. package/src/utils/constants.ts +201 -0
  81. package/src/utils/format.ts +59 -0
  82. package/src/utils/fs.ts +102 -0
  83. package/src/utils/git.ts +16 -0
  84. package/src/utils/result.ts +23 -0
  85. package/src/utils/source-id.ts +19 -0
  86. package/tsconfig.json +22 -0
  87. package/vitest.config.ts +8 -0
@@ -0,0 +1,732 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useEffect, useRef, useState } from "react";
3
+ import { Box, Text, useApp, useInput, useStdout } from "ink";
4
+ import { TARGET_LABELS, TARGET_ORDER } from "../utils/constants.js";
5
+ import { countActions } from "../utils/format.js";
6
+ import { getParentSelectionState, toggleChild, toggleParent, } from "./selection-state.js";
7
+ const EMPTY_DRAFT = {
8
+ enabledTargets: [],
9
+ selectedLeafIds: [],
10
+ };
11
+ const EMPTY_PREVIEW = {
12
+ actions: [],
13
+ blockedCount: 0,
14
+ errorMessage: undefined,
15
+ loading: false,
16
+ requestId: 0,
17
+ };
18
+ const PANE_CHROME_ROWS = 5;
19
+ export function normalizeDraft(draft) {
20
+ return {
21
+ enabledTargets: [...draft.enabledTargets].sort(),
22
+ selectedLeafIds: [...draft.selectedLeafIds].sort(),
23
+ };
24
+ }
25
+ export function draftsEqual(left, right) {
26
+ const nextLeft = normalizeDraft(left);
27
+ const nextRight = normalizeDraft(right);
28
+ return JSON.stringify(nextLeft) === JSON.stringify(nextRight);
29
+ }
30
+ export function getSaveDisplayPhase(savePhase, isDirty) {
31
+ if (savePhase === "saving") {
32
+ return "saving";
33
+ }
34
+ if (savePhase === "failed") {
35
+ return "failed";
36
+ }
37
+ if (isDirty) {
38
+ return "dirty";
39
+ }
40
+ if (savePhase === "saved") {
41
+ return "saved";
42
+ }
43
+ return "clean";
44
+ }
45
+ export function getPaneViewportCount(paneHeight, reservedRows = 0) {
46
+ return Math.max(1, paneHeight - PANE_CHROME_ROWS - reservedRows);
47
+ }
48
+ export function getPaneWidths(terminalColumns) {
49
+ const defaultWidths = [34, 52, 42];
50
+ const minWidths = [22, 30, 22];
51
+ const gapColumns = 2;
52
+ const available = Math.max(74, terminalColumns - gapColumns);
53
+ const defaultTotal = defaultWidths.reduce((sum, width) => sum + width, 0);
54
+ if (available >= defaultTotal) {
55
+ return defaultWidths;
56
+ }
57
+ const minTotal = minWidths.reduce((sum, width) => sum + width, 0);
58
+ if (available <= minTotal) {
59
+ const left = Math.max(18, Math.floor((available * 22) / minTotal));
60
+ const middle = Math.max(24, Math.floor((available * 30) / minTotal));
61
+ const right = Math.max(18, available - left - middle);
62
+ return [left, middle, right];
63
+ }
64
+ const extra = available - minTotal;
65
+ const flexTotal = defaultTotal - minTotal;
66
+ const left = minWidths[0] + Math.floor((extra * (defaultWidths[0] - minWidths[0])) / flexTotal);
67
+ const middle = minWidths[1] + Math.floor((extra * (defaultWidths[1] - minWidths[1])) / flexTotal);
68
+ const right = available - left - middle;
69
+ return [left, middle, right];
70
+ }
71
+ export function getActionChangeCount(actions) {
72
+ return actions.filter((action) => action.kind !== "noop").length;
73
+ }
74
+ export function buildSaveLabel(phase, changeCount) {
75
+ if (phase === "saving") {
76
+ return "Save · SAVING...";
77
+ }
78
+ if (phase === "saved") {
79
+ return changeCount > 0 ? `Save · SAVED · ${changeCount} changes` : "Save · SAVED";
80
+ }
81
+ if (phase === "failed") {
82
+ return "Save · FAILED";
83
+ }
84
+ if (phase === "dirty") {
85
+ return changeCount > 0 ? `Save · DIRTY · ${changeCount} changes` : "Save · DIRTY";
86
+ }
87
+ return "Save";
88
+ }
89
+ export function buildCommandBar({ changeCount, focus, saveFocused, savePhase, }) {
90
+ const backHint = focus === "groups" ? "Esc/q exit" : "Esc/q back";
91
+ if (focus === "groups") {
92
+ return `Enter inspect skills Tab switch pane Up/Down move ${backHint}`;
93
+ }
94
+ if (focus === "skills") {
95
+ return `Space toggle skill Enter inspect agents Tab switch pane Up/Down move ${backHint}`;
96
+ }
97
+ if (saveFocused) {
98
+ if (savePhase === "saving") {
99
+ return `Enter wait for save Tab switch pane Up/Down move ${backHint}`;
100
+ }
101
+ if (changeCount > 0) {
102
+ return `Enter save ${changeCount} changes Tab switch pane Up/Down move ${backHint}`;
103
+ }
104
+ return `Enter save current state Tab switch pane Up/Down move ${backHint}`;
105
+ }
106
+ return `Space toggle agent Enter move to save Tab switch pane Up/Down move ${backHint}`;
107
+ }
108
+ export function buildContextBar({ blockedCount, changeCount, previewError, previewLoading, savePhase, saveMessage, selectedLeafName, selectedLeafWarnings, skippedLeafs, sourceId, }) {
109
+ const parts = [sourceId];
110
+ if (selectedLeafName) {
111
+ parts.push(`skill ${selectedLeafName}`);
112
+ }
113
+ if (savePhase === "saving") {
114
+ parts.push("saving changes...");
115
+ return parts.join(" · ");
116
+ }
117
+ if (savePhase === "failed") {
118
+ parts.push(saveMessage ?? "save failed");
119
+ return parts.join(" · ");
120
+ }
121
+ if (savePhase === "saved") {
122
+ parts.push(saveMessage ?? "saved");
123
+ return parts.join(" · ");
124
+ }
125
+ if (previewLoading) {
126
+ parts.push("planning changes...");
127
+ return parts.join(" · ");
128
+ }
129
+ if (previewError) {
130
+ parts.push(`preview failed: ${previewError}`);
131
+ return parts.join(" · ");
132
+ }
133
+ if (selectedLeafWarnings.length > 0) {
134
+ parts.push(`warning: ${selectedLeafWarnings[0]}`);
135
+ return parts.join(" · ");
136
+ }
137
+ if (skippedLeafs > 0) {
138
+ parts.push(`skipped ${skippedLeafs} invalid or duplicate skills`);
139
+ return parts.join(" · ");
140
+ }
141
+ parts.push(`changes ${changeCount}`);
142
+ parts.push(`blocked ${blockedCount}`);
143
+ return parts.join(" · ");
144
+ }
145
+ export function buildProjectionWarningMap({ drafts, summaries, sourceId, }) {
146
+ const currentDraft = drafts[sourceId] ?? EMPTY_DRAFT;
147
+ const currentSummary = summaries.find((summary) => summary.source.id === sourceId);
148
+ if (!currentSummary || currentDraft.enabledTargets.length === 0) {
149
+ return {};
150
+ }
151
+ const currentSelectedLeafIds = new Set(currentDraft.selectedLeafIds);
152
+ const currentEnabledTargets = new Set(currentDraft.enabledTargets);
153
+ const otherSelectedLeafs = summaries.flatMap((summary) => {
154
+ if (summary.source.id === sourceId) {
155
+ return [];
156
+ }
157
+ const otherDraft = drafts[summary.source.id] ?? EMPTY_DRAFT;
158
+ const hasTargetOverlap = otherDraft.enabledTargets.some((target) => currentEnabledTargets.has(target));
159
+ if (!hasTargetOverlap) {
160
+ return [];
161
+ }
162
+ return otherDraft.selectedLeafIds
163
+ .map((leafId) => summary.leafs.find((leaf) => leaf.id === leafId))
164
+ .filter((leaf) => Boolean(leaf))
165
+ .map((leaf) => ({
166
+ sourceId: summary.source.id,
167
+ leaf,
168
+ exactKey: getExactDuplicateKey(leaf.linkName, leaf.name, leaf.description),
169
+ }));
170
+ });
171
+ const warningsByLeafId = {};
172
+ for (const leaf of currentSummary.leafs) {
173
+ if (!currentSelectedLeafIds.has(leaf.id)) {
174
+ continue;
175
+ }
176
+ const exactKey = getExactDuplicateKey(leaf.linkName, leaf.name, leaf.description);
177
+ const exactDuplicate = otherSelectedLeafs.find((candidate) => candidate.exactKey === exactKey);
178
+ if (exactDuplicate) {
179
+ warningsByLeafId[leaf.id] = [
180
+ `identical skill already selected in ${exactDuplicate.sourceId}, this one will be skipped`,
181
+ ];
182
+ continue;
183
+ }
184
+ const renameConflict = otherSelectedLeafs.find((candidate) => candidate.leaf.linkName === leaf.linkName);
185
+ if (renameConflict) {
186
+ warningsByLeafId[leaf.id] = [
187
+ `conflicts with ${renameConflict.sourceId}, will deploy as ${sourceId}-${leaf.linkName}`,
188
+ ];
189
+ }
190
+ }
191
+ return warningsByLeafId;
192
+ }
193
+ export function ConfigApp({ app, availableTargets, summaries, initialDrafts, }) {
194
+ const { exit } = useApp();
195
+ const { stdout } = useStdout();
196
+ const previewRequestIds = useRef({});
197
+ const [selectedGroupIndex, setSelectedGroupIndex] = useState(summaries.length > 0 ? 0 : -1);
198
+ const [groupCursor, setGroupCursor] = useState(summaries.length > 0 ? 0 : -1);
199
+ const [focus, setFocus] = useState("groups");
200
+ const [skillCursor, setSkillCursor] = useState(0);
201
+ const [targetCursor, setTargetCursor] = useState(0);
202
+ const [drafts, setDrafts] = useState(initialDrafts);
203
+ const [savedDrafts, setSavedDrafts] = useState(initialDrafts);
204
+ const [previewBySourceId, setPreviewBySourceId] = useState({});
205
+ const [saveStateBySourceId, setSaveStateBySourceId] = useState({});
206
+ const selectedSummary = summaries[selectedGroupIndex];
207
+ const selectedSourceId = selectedSummary?.source.id ?? "";
208
+ const selectedDraft = drafts[selectedSourceId] ?? EMPTY_DRAFT;
209
+ const savedDraft = savedDrafts[selectedSourceId] ?? EMPTY_DRAFT;
210
+ const isDirty = !draftsEqual(selectedDraft, savedDraft);
211
+ const leafIds = selectedSummary?.leafs.map((leaf) => leaf.id) ?? [];
212
+ const treeState = {
213
+ allLeafIds: leafIds,
214
+ selectedLeafIds: selectedDraft.selectedLeafIds,
215
+ };
216
+ const parentSelectionState = getParentSelectionState(treeState);
217
+ const visibleTargets = availableTargets;
218
+ const targetSelectableCount = visibleTargets.length > 0 ? visibleTargets.length + 1 : 0;
219
+ const targetSaveCursor = targetSelectableCount;
220
+ const visibleEnabledTargets = visibleTargets.filter((target) => selectedDraft.enabledTargets.includes(target));
221
+ const allTargetsSelected = visibleTargets.length > 0 && visibleEnabledTargets.length === visibleTargets.length;
222
+ const projectionWarningsByLeafId = buildProjectionWarningMap({
223
+ drafts,
224
+ summaries,
225
+ sourceId: selectedSourceId,
226
+ });
227
+ const selectedLeaf = selectedSummary && skillCursor > 0
228
+ ? selectedSummary.leafs[skillCursor - 1]
229
+ : undefined;
230
+ const selectedLeafWarnings = selectedLeaf
231
+ ? [
232
+ ...selectedLeaf.metadataWarnings,
233
+ ...(projectionWarningsByLeafId[selectedLeaf.id] ?? []),
234
+ ]
235
+ : [];
236
+ const previewState = previewBySourceId[selectedSourceId] ?? EMPTY_PREVIEW;
237
+ const actionCounts = countActions(previewState.actions);
238
+ const changeCount = getActionChangeCount(previewState.actions);
239
+ const saveState = saveStateBySourceId[selectedSourceId] ?? {
240
+ phase: "idle",
241
+ message: undefined,
242
+ };
243
+ const savePhase = getSaveDisplayPhase(saveState.phase, isDirty);
244
+ const skippedLeafs = selectedSummary?.lock?.invalidLeafs.length ?? 0;
245
+ useEffect(() => {
246
+ if (!selectedSummary) {
247
+ return;
248
+ }
249
+ // draft -> preview request id -> latest preview wins
250
+ // save -> explicit phase -> dirty/saving/saved/failed
251
+ const sourceId = selectedSummary.source.id;
252
+ const requestId = (previewRequestIds.current[sourceId] ?? 0) + 1;
253
+ previewRequestIds.current[sourceId] = requestId;
254
+ setPreviewBySourceId((current) => ({
255
+ ...current,
256
+ [sourceId]: {
257
+ ...(current[sourceId] ?? EMPTY_PREVIEW),
258
+ errorMessage: undefined,
259
+ loading: true,
260
+ requestId,
261
+ },
262
+ }));
263
+ let disposed = false;
264
+ void app.previewDraft(sourceId, selectedDraft).then((result) => {
265
+ if (disposed) {
266
+ return;
267
+ }
268
+ setPreviewBySourceId((current) => {
269
+ const currentState = current[sourceId] ?? EMPTY_PREVIEW;
270
+ if (currentState.requestId !== requestId) {
271
+ return current;
272
+ }
273
+ if (!result.ok) {
274
+ return {
275
+ ...current,
276
+ [sourceId]: {
277
+ actions: [],
278
+ blockedCount: 0,
279
+ errorMessage: firstErrorMessage(result),
280
+ loading: false,
281
+ requestId,
282
+ },
283
+ };
284
+ }
285
+ return {
286
+ ...current,
287
+ [sourceId]: {
288
+ actions: result.data.plan.actions,
289
+ blockedCount: result.data.plan.blocked.length,
290
+ errorMessage: undefined,
291
+ loading: false,
292
+ requestId,
293
+ },
294
+ };
295
+ });
296
+ });
297
+ return () => {
298
+ disposed = true;
299
+ };
300
+ }, [app, selectedDraft, selectedSummary]);
301
+ const updateSelectedDraft = (updater) => {
302
+ if (!selectedSummary) {
303
+ return;
304
+ }
305
+ const sourceId = selectedSummary.source.id;
306
+ setDrafts((current) => {
307
+ const currentDraft = current[sourceId] ?? EMPTY_DRAFT;
308
+ const nextDraft = normalizeDraft(updater(currentDraft));
309
+ if (draftsEqual(currentDraft, nextDraft)) {
310
+ return current;
311
+ }
312
+ return {
313
+ ...current,
314
+ [sourceId]: nextDraft,
315
+ };
316
+ });
317
+ setSaveStateBySourceId((current) => {
318
+ const state = current[sourceId];
319
+ if (!state || state.phase === "idle") {
320
+ return current;
321
+ }
322
+ return {
323
+ ...current,
324
+ [sourceId]: {
325
+ phase: "idle",
326
+ message: undefined,
327
+ },
328
+ };
329
+ });
330
+ };
331
+ const handleSave = () => {
332
+ if (!selectedSummary || savePhase === "saving") {
333
+ return;
334
+ }
335
+ const sourceId = selectedSummary.source.id;
336
+ const nextRequestId = (previewRequestIds.current[sourceId] ?? 0) + 1;
337
+ previewRequestIds.current[sourceId] = nextRequestId;
338
+ setSaveStateBySourceId((current) => ({
339
+ ...current,
340
+ [sourceId]: {
341
+ phase: "saving",
342
+ message: "saving changes...",
343
+ },
344
+ }));
345
+ setPreviewBySourceId((current) => ({
346
+ ...current,
347
+ [sourceId]: {
348
+ ...(current[sourceId] ?? EMPTY_PREVIEW),
349
+ errorMessage: undefined,
350
+ loading: false,
351
+ requestId: nextRequestId,
352
+ },
353
+ }));
354
+ const draftToSave = normalizeDraft(selectedDraft);
355
+ void app.applyDraft(sourceId, draftToSave).then((result) => {
356
+ if (!result.ok) {
357
+ const message = firstErrorMessage(result);
358
+ setSaveStateBySourceId((current) => ({
359
+ ...current,
360
+ [sourceId]: {
361
+ phase: "failed",
362
+ message,
363
+ },
364
+ }));
365
+ return;
366
+ }
367
+ const appliedDraft = normalizeDraft(result.data.draft);
368
+ const appliedChangeCount = getActionChangeCount(result.data.actions);
369
+ setDrafts((current) => ({
370
+ ...current,
371
+ [sourceId]: appliedDraft,
372
+ }));
373
+ setSavedDrafts((current) => ({
374
+ ...current,
375
+ [sourceId]: appliedDraft,
376
+ }));
377
+ setSaveStateBySourceId((current) => ({
378
+ ...current,
379
+ [sourceId]: {
380
+ phase: "saved",
381
+ message: appliedChangeCount > 0
382
+ ? `saved ${appliedChangeCount} changes`
383
+ : "saved with no changes",
384
+ },
385
+ }));
386
+ setPreviewBySourceId((current) => ({
387
+ ...current,
388
+ [sourceId]: {
389
+ actions: result.data.actions,
390
+ blockedCount: result.data.actions.filter((action) => action.kind === "blocked")
391
+ .length,
392
+ errorMessage: undefined,
393
+ loading: false,
394
+ requestId: nextRequestId,
395
+ },
396
+ }));
397
+ });
398
+ };
399
+ useInput((input, key) => {
400
+ if (!selectedSummary) {
401
+ if (input === "q" || key.escape || (input === "c" && key.ctrl)) {
402
+ exit();
403
+ }
404
+ return;
405
+ }
406
+ if (input === "c" && key.ctrl) {
407
+ exit();
408
+ return;
409
+ }
410
+ if (input === "q" || key.escape) {
411
+ if (focus === "targets") {
412
+ setFocus("skills");
413
+ return;
414
+ }
415
+ if (focus === "skills") {
416
+ setFocus("groups");
417
+ return;
418
+ }
419
+ exit();
420
+ return;
421
+ }
422
+ if (key.tab) {
423
+ const cycle = ["groups", "skills", "targets"];
424
+ const currentIndex = cycle.indexOf(focus);
425
+ setFocus(cycle[(currentIndex + 1) % cycle.length] ?? "groups");
426
+ return;
427
+ }
428
+ if (focus === "groups") {
429
+ if (key.downArrow) {
430
+ setGroupCursor((current) => {
431
+ const next = Math.min(current + 1, Math.max(0, summaries.length - 1));
432
+ setSelectedGroupIndex(next);
433
+ setSkillCursor(0);
434
+ setTargetCursor(0);
435
+ return next;
436
+ });
437
+ }
438
+ if (key.upArrow) {
439
+ setGroupCursor((current) => {
440
+ const next = Math.max(current - 1, 0);
441
+ setSelectedGroupIndex(next);
442
+ setSkillCursor(0);
443
+ setTargetCursor(0);
444
+ return next;
445
+ });
446
+ }
447
+ if (key.return) {
448
+ setFocus("skills");
449
+ }
450
+ return;
451
+ }
452
+ if (focus === "skills") {
453
+ if (key.downArrow) {
454
+ setSkillCursor((current) => Math.min(current + 1, leafIds.length));
455
+ }
456
+ if (key.upArrow) {
457
+ setSkillCursor((current) => Math.max(current - 1, 0));
458
+ }
459
+ if (input === " ") {
460
+ updateSelectedDraft((currentDraft) => {
461
+ const baseState = {
462
+ allLeafIds: leafIds,
463
+ selectedLeafIds: currentDraft.selectedLeafIds,
464
+ };
465
+ const nextState = skillCursor === 0
466
+ ? toggleParent(baseState)
467
+ : toggleChild(baseState, leafIds[skillCursor - 1]);
468
+ return {
469
+ ...currentDraft,
470
+ selectedLeafIds: nextState.selectedLeafIds,
471
+ };
472
+ });
473
+ }
474
+ if (key.return) {
475
+ setFocus("targets");
476
+ }
477
+ return;
478
+ }
479
+ if (key.downArrow) {
480
+ setTargetCursor((current) => Math.min(current + 1, targetSaveCursor));
481
+ }
482
+ if (key.upArrow) {
483
+ setTargetCursor((current) => Math.max(current - 1, 0));
484
+ }
485
+ if (input === " ") {
486
+ updateSelectedDraft((currentDraft) => {
487
+ if (visibleTargets.length === 0 || targetCursor === targetSaveCursor) {
488
+ return currentDraft;
489
+ }
490
+ if (targetCursor === 0) {
491
+ const enabledTargets = new Set(currentDraft.enabledTargets);
492
+ const nextSelectAll = !visibleTargets.every((target) => enabledTargets.has(target));
493
+ for (const target of visibleTargets) {
494
+ if (nextSelectAll) {
495
+ enabledTargets.add(target);
496
+ }
497
+ else {
498
+ enabledTargets.delete(target);
499
+ }
500
+ }
501
+ return {
502
+ ...currentDraft,
503
+ enabledTargets: TARGET_ORDER.filter((target) => enabledTargets.has(target)),
504
+ };
505
+ }
506
+ const target = visibleTargets[targetCursor - 1];
507
+ const enabledTargets = new Set(currentDraft.enabledTargets);
508
+ if (enabledTargets.has(target)) {
509
+ enabledTargets.delete(target);
510
+ }
511
+ else {
512
+ enabledTargets.add(target);
513
+ }
514
+ return {
515
+ ...currentDraft,
516
+ enabledTargets: TARGET_ORDER.filter((item) => enabledTargets.has(item)),
517
+ };
518
+ });
519
+ }
520
+ if (key.return) {
521
+ if (targetCursor === targetSaveCursor) {
522
+ handleSave();
523
+ return;
524
+ }
525
+ setTargetCursor(targetSaveCursor);
526
+ }
527
+ });
528
+ if (summaries.length === 0) {
529
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { bold: true, children: "No workflow groups yet" }), _jsx(Text, { children: "Add a Git source to discover a grouped set of related skills." }), _jsx(Text, { dimColor: true, children: "Press q or esc to exit." })] }));
530
+ }
531
+ const activeSummary = selectedSummary;
532
+ const terminalRows = stdout.rows ?? 24;
533
+ const terminalColumns = stdout.columns ?? 120;
534
+ const paneHeight = Math.max(12, terminalRows - 4);
535
+ const [groupsWidth, skillsWidth, targetsWidth] = getPaneWidths(terminalColumns);
536
+ const bodyRowCount = getPaneViewportCount(paneHeight);
537
+ const targetListRowCount = Math.max(1, bodyRowCount - 1);
538
+ const targetItems = visibleTargets.length > 0
539
+ ? [
540
+ {
541
+ key: "__all_targets__",
542
+ text: `${selectionMarker(allTargetsSelected ? "full" : visibleEnabledTargets.length > 0 ? "partial" : "empty")} all agents`,
543
+ active: focus === "targets" && targetCursor === 0,
544
+ bold: true,
545
+ color: undefined,
546
+ },
547
+ ...visibleTargets.map((target, index) => ({
548
+ key: target,
549
+ text: `${selectionMarker(selectedDraft.enabledTargets.includes(target) ? "full" : "empty")} ${TARGET_LABELS[target]}`,
550
+ active: focus === "targets" && targetCursor === index + 1,
551
+ color: "gray",
552
+ })),
553
+ ]
554
+ : [
555
+ {
556
+ key: "__no_targets__",
557
+ text: "No detected agent targets",
558
+ active: false,
559
+ bold: false,
560
+ color: "gray",
561
+ },
562
+ ];
563
+ const groupRows = getWindowedRows(summaries.map((summary, index) => ({
564
+ key: summary.source.id,
565
+ text: `${summary.source.id} ${formatGroupSaveState(getSaveDisplayPhase((saveStateBySourceId[summary.source.id]?.phase ?? "idle"), !draftsEqual(drafts[summary.source.id] ?? EMPTY_DRAFT, savedDrafts[summary.source.id] ?? EMPTY_DRAFT)))}`,
566
+ active: focus === "groups" && groupCursor === index,
567
+ color: getGroupStateColor(getSaveDisplayPhase((saveStateBySourceId[summary.source.id]?.phase ?? "idle"), !draftsEqual(drafts[summary.source.id] ?? EMPTY_DRAFT, savedDrafts[summary.source.id] ?? EMPTY_DRAFT))),
568
+ })), Math.max(0, groupCursor), bodyRowCount);
569
+ const skillRows = getWindowedRows([
570
+ {
571
+ key: "__all__",
572
+ text: `${selectionMarker(parentSelectionState)} all skills`,
573
+ active: focus === "skills" && skillCursor === 0,
574
+ bold: true,
575
+ color: undefined,
576
+ },
577
+ ...activeSummary.leafs.map((leaf, index) => ({
578
+ key: leaf.id,
579
+ text: `${selectionMarker(selectedDraft.selectedLeafIds.includes(leaf.id) ? "full" : "empty")} ${leaf.linkName}`,
580
+ active: focus === "skills" && skillCursor === index + 1,
581
+ color: leaf.metadataWarnings.length > 0 || (projectionWarningsByLeafId[leaf.id]?.length ?? 0) > 0
582
+ ? "yellow"
583
+ : "gray",
584
+ })),
585
+ ], skillCursor, bodyRowCount);
586
+ const targetRows = getWindowedRows(targetItems, Math.min(targetCursor, Math.max(0, targetItems.length - 1)), targetListRowCount);
587
+ const saveRow = buildSaveRow(focus === "targets" && targetCursor === targetSaveCursor, savePhase, changeCount);
588
+ const contextBar = buildContextBar({
589
+ blockedCount: previewState.blockedCount,
590
+ changeCount,
591
+ previewError: previewState.errorMessage,
592
+ previewLoading: previewState.loading,
593
+ savePhase,
594
+ saveMessage: saveState.message,
595
+ selectedLeafName: selectedLeaf?.linkName,
596
+ selectedLeafWarnings,
597
+ skippedLeafs,
598
+ sourceId: selectedSourceId,
599
+ });
600
+ const commandBar = buildCommandBar({
601
+ changeCount,
602
+ focus,
603
+ saveFocused: focus === "targets" && targetCursor === targetSaveCursor,
604
+ savePhase,
605
+ });
606
+ return (_jsxs(Box, { flexDirection: "column", height: terminalRows, children: [_jsxs(Box, { children: [_jsx(Pane, { active: focus === "groups", footer: buildPaneFooter(groupRows.start, groupRows.end, summaries.length, `group ${selectedGroupIndex + 1}/${summaries.length}`), gapAfter: true, height: paneHeight, title: "WORKFLOW GROUPS", width: groupsWidth, children: renderPaneRows(groupRows.rows, bodyRowCount, groupsWidth) }), _jsx(Pane, { active: focus === "skills", footer: buildPaneFooter(skillRows.start, skillRows.end, activeSummary.leafs.length + 1, `${selectedDraft.selectedLeafIds.length}/${leafIds.length} selected`), gapAfter: true, height: paneHeight, title: "GROUP DETAIL", width: skillsWidth, children: renderPaneRows(skillRows.rows, bodyRowCount, skillsWidth) }), _jsx(Pane, { active: focus === "targets", footer: buildPaneFooter(targetRows.start, targetRows.end, targetItems.length, visibleTargets.length > 0
607
+ ? `${visibleEnabledTargets.length}/${visibleTargets.length} targets`
608
+ : "no detected targets"), height: paneHeight, title: "AGENT PROJECTION", width: targetsWidth, children: renderPaneRows(targetRows.rows, bodyRowCount, targetsWidth, [saveRow]) })] }), _jsx(Text, { dimColor: true, wrap: "truncate-middle", children: contextBar }), _jsx(Text, { dimColor: true, wrap: "truncate-middle", children: commandBar })] }));
609
+ }
610
+ function buildSaveRow(active, phase, changeCount) {
611
+ return {
612
+ key: "__save__",
613
+ text: buildSaveLabel(phase, changeCount),
614
+ active,
615
+ bold: true,
616
+ color: getSaveColor(phase),
617
+ };
618
+ }
619
+ function getSaveColor(phase) {
620
+ if (phase === "failed") {
621
+ return "red";
622
+ }
623
+ if (phase === "dirty") {
624
+ return "yellow";
625
+ }
626
+ if (phase === "saving") {
627
+ return "cyan";
628
+ }
629
+ return "green";
630
+ }
631
+ function getGroupStateColor(phase) {
632
+ if (phase === "failed") {
633
+ return "red";
634
+ }
635
+ if (phase === "dirty") {
636
+ return "yellow";
637
+ }
638
+ if (phase === "saving") {
639
+ return "cyan";
640
+ }
641
+ if (phase === "saved") {
642
+ return "green";
643
+ }
644
+ return "green";
645
+ }
646
+ function buildPaneFooter(start, end, total, summary) {
647
+ const overflow = formatOverflow(start, end, total);
648
+ return `${overflow} · ${summary}`;
649
+ }
650
+ function formatOverflow(start, end, total) {
651
+ const above = start;
652
+ const below = Math.max(0, total - end);
653
+ if (above === 0 && below === 0) {
654
+ return "top / bottom";
655
+ }
656
+ return `${above > 0 ? `${above} above` : "top"} / ${below > 0 ? `${below} below` : "bottom"}`;
657
+ }
658
+ function RowText({ row, width }) {
659
+ const color = row.active ? "cyan" : row.color;
660
+ const prefix = row.active ? "> " : " ";
661
+ const contentWidth = Math.max(1, getPaneInnerWidth(width) - prefix.length);
662
+ const content = fitPaneLine(row.text, contentWidth);
663
+ return (_jsxs(Text, { wrap: "truncate-end", ...(color ? { color } : {}), ...(row.bold ? { bold: true } : {}), children: [prefix, content] }));
664
+ }
665
+ function Pane({ title, active, width, children, height, footer, gapAfter = false, }) {
666
+ return (_jsxs(Box, { flexDirection: "column", width: width, height: height, marginRight: gapAfter ? 1 : 0, paddingX: 1, borderStyle: "round", borderColor: active ? "cyan" : "gray", children: [_jsx(Text, { bold: true, wrap: "truncate-end", children: fitPaneLine(title, getPaneInnerWidth(width)) }), _jsx(Text, { children: " " }), children, _jsx(Text, { dimColor: true, wrap: "truncate-middle", children: fitPaneLine(footer, getPaneInnerWidth(width)) })] }));
667
+ }
668
+ function renderPaneRows(rows, bodyRowCount, paneWidth, tailRows = []) {
669
+ const items = rows.map((row) => (_jsx(RowText, { row: row, width: paneWidth }, row.key)));
670
+ const blankCount = Math.max(0, bodyRowCount - rows.length - tailRows.length);
671
+ for (let index = 0; index < blankCount; index += 1) {
672
+ items.push(_jsx(Text, { children: " " }, `__blank__:${index}`));
673
+ }
674
+ for (const row of tailRows) {
675
+ items.push(_jsx(RowText, { row: row, width: paneWidth }, row.key));
676
+ }
677
+ return items;
678
+ }
679
+ function fitPaneLine(text, width) {
680
+ if (text.length <= width) {
681
+ return text.padEnd(width, " ");
682
+ }
683
+ if (width <= 1) {
684
+ return "…";
685
+ }
686
+ return `${text.slice(0, width - 1)}…`;
687
+ }
688
+ function getPaneInnerWidth(width) {
689
+ return Math.max(1, width - 4);
690
+ }
691
+ function selectionMarker(state) {
692
+ if (state === "full") {
693
+ return "[x]";
694
+ }
695
+ if (state === "partial") {
696
+ return "[-]";
697
+ }
698
+ return "[ ]";
699
+ }
700
+ function getExactDuplicateKey(linkName, name, description) {
701
+ return `${linkName}\n${name}\n${description}`;
702
+ }
703
+ function formatGroupSaveState(phase) {
704
+ if (phase === "dirty") {
705
+ return "DIRTY";
706
+ }
707
+ if (phase === "saving") {
708
+ return "SAVING";
709
+ }
710
+ if (phase === "saved") {
711
+ return "SAVED";
712
+ }
713
+ if (phase === "failed") {
714
+ return "FAILED";
715
+ }
716
+ return "SAVED";
717
+ }
718
+ function getWindowedRows(items, cursorIndex, visibleCount) {
719
+ const safeVisibleCount = Math.max(1, visibleCount);
720
+ const maxStart = Math.max(0, items.length - safeVisibleCount);
721
+ const start = Math.min(Math.max(0, cursorIndex - Math.floor(safeVisibleCount / 2)), maxStart);
722
+ const end = Math.min(items.length, start + safeVisibleCount);
723
+ return {
724
+ rows: items.slice(start, end),
725
+ start,
726
+ end,
727
+ };
728
+ }
729
+ function firstErrorMessage(result) {
730
+ return result.errors[0]?.message ?? "operation failed";
731
+ }
732
+ //# sourceMappingURL=config-app.js.map