skill-flow 1.0.3 → 1.0.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.
Files changed (84) hide show
  1. package/README.md +40 -3
  2. package/README.zh.md +40 -3
  3. package/dist/adapters/channel-adapters.js +11 -3
  4. package/dist/adapters/channel-adapters.js.map +1 -1
  5. package/dist/cli.js +69 -37
  6. package/dist/cli.js.map +1 -1
  7. package/dist/domain/types.d.ts +54 -1
  8. package/dist/services/config-coordinator.d.ts +38 -0
  9. package/dist/services/config-coordinator.js +81 -0
  10. package/dist/services/config-coordinator.js.map +1 -0
  11. package/dist/services/doctor-service.d.ts +2 -0
  12. package/dist/services/doctor-service.js +62 -0
  13. package/dist/services/doctor-service.js.map +1 -1
  14. package/dist/services/inventory-service.d.ts +3 -1
  15. package/dist/services/inventory-service.js +12 -5
  16. package/dist/services/inventory-service.js.map +1 -1
  17. package/dist/services/skill-flow.d.ts +50 -26
  18. package/dist/services/skill-flow.js +502 -89
  19. package/dist/services/skill-flow.js.map +1 -1
  20. package/dist/services/source-service.d.ts +20 -10
  21. package/dist/services/source-service.js +359 -75
  22. package/dist/services/source-service.js.map +1 -1
  23. package/dist/services/workflow-service.d.ts +2 -2
  24. package/dist/services/workflow-service.js +17 -4
  25. package/dist/services/workflow-service.js.map +1 -1
  26. package/dist/services/workspace-bootstrap-service.d.ts +25 -0
  27. package/dist/services/workspace-bootstrap-service.js +140 -0
  28. package/dist/services/workspace-bootstrap-service.js.map +1 -0
  29. package/dist/state/store.d.ts +16 -0
  30. package/dist/state/store.js +93 -18
  31. package/dist/state/store.js.map +1 -1
  32. package/dist/tests/clawhub.test.d.ts +1 -0
  33. package/dist/tests/clawhub.test.js +63 -0
  34. package/dist/tests/clawhub.test.js.map +1 -0
  35. package/dist/tests/cli-utils.test.d.ts +1 -0
  36. package/dist/tests/cli-utils.test.js +15 -0
  37. package/dist/tests/cli-utils.test.js.map +1 -0
  38. package/dist/tests/config-coordinator.test.d.ts +1 -0
  39. package/dist/tests/config-coordinator.test.js +172 -0
  40. package/dist/tests/config-coordinator.test.js.map +1 -0
  41. package/dist/tests/config-integration.test.d.ts +1 -0
  42. package/dist/tests/config-integration.test.js +238 -0
  43. package/dist/tests/config-integration.test.js.map +1 -0
  44. package/dist/tests/config-ui-utils.test.d.ts +1 -0
  45. package/dist/tests/config-ui-utils.test.js +389 -0
  46. package/dist/tests/config-ui-utils.test.js.map +1 -0
  47. package/dist/tests/find-and-naming-utils.test.d.ts +1 -0
  48. package/dist/tests/find-and-naming-utils.test.js +127 -0
  49. package/dist/tests/find-and-naming-utils.test.js.map +1 -0
  50. package/dist/tests/skill-flow.test.js +334 -881
  51. package/dist/tests/skill-flow.test.js.map +1 -1
  52. package/dist/tests/source-lifecycle.test.d.ts +1 -0
  53. package/dist/tests/source-lifecycle.test.js +605 -0
  54. package/dist/tests/source-lifecycle.test.js.map +1 -0
  55. package/dist/tests/target-definitions.test.d.ts +1 -0
  56. package/dist/tests/target-definitions.test.js +51 -0
  57. package/dist/tests/target-definitions.test.js.map +1 -0
  58. package/dist/tests/test-helpers.d.ts +18 -0
  59. package/dist/tests/test-helpers.js +123 -0
  60. package/dist/tests/test-helpers.js.map +1 -0
  61. package/dist/tui/config-app.d.ts +147 -24
  62. package/dist/tui/config-app.js +1081 -335
  63. package/dist/tui/config-app.js.map +1 -1
  64. package/dist/tui/find-app.d.ts +1 -1
  65. package/dist/tui/find-app.js +36 -4
  66. package/dist/tui/find-app.js.map +1 -1
  67. package/dist/utils/clawhub.d.ts +3 -0
  68. package/dist/utils/clawhub.js +32 -3
  69. package/dist/utils/clawhub.js.map +1 -1
  70. package/dist/utils/cli.d.ts +1 -0
  71. package/dist/utils/cli.js +15 -0
  72. package/dist/utils/cli.js.map +1 -0
  73. package/dist/utils/constants.d.ts +4 -0
  74. package/dist/utils/constants.js +31 -0
  75. package/dist/utils/constants.js.map +1 -1
  76. package/dist/utils/fs.d.ts +5 -0
  77. package/dist/utils/fs.js +52 -1
  78. package/dist/utils/fs.js.map +1 -1
  79. package/dist/utils/naming.d.ts +1 -0
  80. package/dist/utils/naming.js +7 -1
  81. package/dist/utils/naming.js.map +1 -1
  82. package/dist/utils/source-id.js +4 -0
  83. package/dist/utils/source-id.js.map +1 -1
  84. package/package.json +1 -1
@@ -3,12 +3,17 @@ import { useEffect, useRef, useState } from "react";
3
3
  import { Box, Text, useApp, useInput, useStdout } from "ink";
4
4
  import { TARGET_LABELS, TARGET_ORDER } from "../utils/constants.js";
5
5
  import { buildProjectedSkillName, formatGroupLabel, parseGitHubRepo, resolveProjectedSkillNames, } from "../utils/naming.js";
6
- import { countActions } from "../utils/format.js";
7
6
  import { getParentSelectionState, toggleChild, toggleParent, } from "./selection-state.js";
8
7
  const EMPTY_DRAFT = {
9
8
  enabledTargets: [],
10
9
  selectedLeafIds: [],
11
10
  };
11
+ const EMPTY_CONFIG_GROUP = {
12
+ id: "__empty__",
13
+ title: "",
14
+ kind: "source",
15
+ summaries: [],
16
+ };
12
17
  const EMPTY_PREVIEW = {
13
18
  actions: [],
14
19
  blockedCount: 0,
@@ -17,6 +22,8 @@ const EMPTY_PREVIEW = {
17
22
  requestId: 0,
18
23
  };
19
24
  const PANE_CHROME_ROWS = 5;
25
+ const UPDATED_FEEDBACK_MS = 1_200;
26
+ const CLAWHUB_GROUP_ID = "__clawhub_skills__";
20
27
  export function normalizeDraft(draft) {
21
28
  return {
22
29
  enabledTargets: [...draft.enabledTargets].sort(),
@@ -28,120 +35,291 @@ export function draftsEqual(left, right) {
28
35
  const nextRight = normalizeDraft(right);
29
36
  return JSON.stringify(nextLeft) === JSON.stringify(nextRight);
30
37
  }
31
- export function getSaveDisplayPhase(savePhase, isDirty) {
32
- if (savePhase === "saving") {
33
- return "saving";
34
- }
35
- if (savePhase === "failed") {
36
- return "failed";
37
- }
38
- if (isDirty) {
39
- return "dirty";
38
+ export function buildDraftsFromSummaries(summaries) {
39
+ return Object.fromEntries(summaries.map((summary) => {
40
+ const enabledTargets = Object.entries(summary.bindings.targets)
41
+ .filter(([, value]) => value?.enabled)
42
+ .map(([target]) => target);
43
+ const selectedLeafIds = [
44
+ ...new Set(enabledTargets.flatMap((target) => summary.bindings.targets[target]?.leafIds ?? [])),
45
+ ];
46
+ return [summary.source.id, normalizeDraft({ enabledTargets, selectedLeafIds })];
47
+ }));
48
+ }
49
+ export function buildConfigGroups(summaries) {
50
+ const clawhubSummaries = summaries.filter((summary) => summary.source.kind === "clawhub");
51
+ const groups = [];
52
+ let clawhubGroupInserted = false;
53
+ for (const summary of summaries) {
54
+ if (summary.source.kind === "clawhub") {
55
+ if (!clawhubGroupInserted && clawhubSummaries.length > 0) {
56
+ groups.push({
57
+ id: CLAWHUB_GROUP_ID,
58
+ title: "ClawHub Skills",
59
+ kind: "clawhub",
60
+ summaries: clawhubSummaries,
61
+ });
62
+ clawhubGroupInserted = true;
63
+ }
64
+ continue;
65
+ }
66
+ groups.push({
67
+ id: summary.source.id,
68
+ title: formatGroupLabel(summary.source),
69
+ kind: "source",
70
+ summaries: [summary],
71
+ });
40
72
  }
41
- if (savePhase === "saved") {
42
- return "saved";
73
+ if (!clawhubGroupInserted && clawhubSummaries.length > 0) {
74
+ groups.push({
75
+ id: CLAWHUB_GROUP_ID,
76
+ title: "ClawHub Skills",
77
+ kind: "clawhub",
78
+ summaries: clawhubSummaries,
79
+ });
43
80
  }
44
- return "clean";
81
+ return groups;
82
+ }
83
+ export function buildConfigGroupSkillRows(group) {
84
+ return group.summaries.flatMap((summary) => summary.leafs.map((leaf) => ({
85
+ summary,
86
+ leaf,
87
+ })));
45
88
  }
46
89
  export function getPaneViewportCount(paneHeight, reservedRows = 0) {
47
90
  return Math.max(1, paneHeight - PANE_CHROME_ROWS - reservedRows);
48
91
  }
49
92
  export function getPaneWidths(terminalColumns) {
50
- const defaultWidths = [34, 52, 42];
51
- const minWidths = [22, 30, 22];
52
- const gapColumns = 2;
53
- const available = Math.max(74, terminalColumns - gapColumns);
54
- const defaultTotal = defaultWidths.reduce((sum, width) => sum + width, 0);
55
- if (available >= defaultTotal) {
56
- return defaultWidths;
57
- }
58
- const minTotal = minWidths.reduce((sum, width) => sum + width, 0);
59
- if (available <= minTotal) {
60
- const left = Math.max(18, Math.floor((available * 22) / minTotal));
61
- const middle = Math.max(24, Math.floor((available * 30) / minTotal));
62
- const right = Math.max(18, available - left - middle);
63
- return [left, middle, right];
64
- }
65
- const extra = available - minTotal;
66
- const flexTotal = defaultTotal - minTotal;
67
- const left = minWidths[0] + Math.floor((extra * (defaultWidths[0] - minWidths[0])) / flexTotal);
68
- const middle = minWidths[1] + Math.floor((extra * (defaultWidths[1] - minWidths[1])) / flexTotal);
69
- const right = available - left - middle;
70
- return [left, middle, right];
93
+ const available = Math.max(56, terminalColumns - 1);
94
+ const left = Math.max(20, Math.min(30, Math.floor(available * 0.28)));
95
+ return [left, Math.max(32, available - left)];
71
96
  }
72
97
  export function getActionChangeCount(actions) {
73
98
  return actions.filter((action) => action.kind !== "noop").length;
74
99
  }
75
- export function buildSaveLabel(phase, changeCount) {
76
- if (phase === "saving") {
77
- return "Save · SAVING...";
100
+ export function getGroupSelectedLeafCount({ drafts, group, }) {
101
+ return group.summaries.reduce((count, summary) => count + (drafts[summary.source.id]?.selectedLeafIds.length ?? 0), 0);
102
+ }
103
+ export function getStatusDisplay({ deleteState, isSelectedDelete, saveState, updateState, }) {
104
+ if (isSelectedDelete && deleteState.phase === "deleting") {
105
+ return { kind: "deleting", label: "Deleting", color: "yellow" };
106
+ }
107
+ if (updateState.phase === "updating") {
108
+ return { kind: "updating", label: "Updating", color: "cyan" };
78
109
  }
79
- if (phase === "saved") {
80
- return changeCount > 0 ? `Save · SAVED · ${changeCount} changes` : "Save · SAVED";
110
+ if (updateState.phase === "failed") {
111
+ return { kind: "update-failed", label: "Update Failed", color: "red" };
81
112
  }
82
- if (phase === "failed") {
83
- return "Save · FAILED";
113
+ if (saveState.phase === "saving") {
114
+ return { kind: "saving", label: "Saving", color: "cyan" };
84
115
  }
85
- if (phase === "dirty") {
86
- return changeCount > 0 ? `Save · DIRTY · ${changeCount} changes` : "Save · DIRTY";
116
+ if (saveState.phase === "failed") {
117
+ return { kind: "failed", label: "Failed", color: "red" };
87
118
  }
88
- return "Save";
119
+ if (updateState.phase === "updated") {
120
+ return { kind: "updated", label: "Updated", color: "green" };
121
+ }
122
+ if (saveState.phase === "saved") {
123
+ return { kind: "saved", label: "Saved", color: "green" };
124
+ }
125
+ return { kind: "clean", label: "Clean", color: "gray" };
89
126
  }
90
- export function buildCommandBar({ changeCount, focus, saveFocused, savePhase, }) {
91
- const backHint = focus === "groups" ? "Esc/q exit" : "Esc/q back";
92
- if (focus === "groups") {
93
- return `Enter inspect skills Tab switch pane Up/Down move ${backHint}`;
127
+ export function buildTopBar({ width, isDirty, changeCount, showDelete, statusLabel, }) {
128
+ const parts = [
129
+ "Skill Flow",
130
+ "[u] Update",
131
+ `Dirty: ${isDirty ? "Yes" : "No"}`,
132
+ ];
133
+ if (showDelete) {
134
+ parts.push("[d] Delete");
94
135
  }
95
- if (focus === "skills") {
96
- return `Space toggle skill Enter inspect agents Tab switch pane Up/Down move ${backHint}`;
136
+ if (width >= 100) {
137
+ parts.push(`Changes: ${changeCount}`);
97
138
  }
98
- if (saveFocused) {
99
- if (savePhase === "saving") {
100
- return `Enter wait for save Tab switch pane Up/Down move ${backHint}`;
101
- }
102
- if (changeCount > 0) {
103
- return `Enter save ${changeCount} changes Tab switch pane Up/Down move ${backHint}`;
139
+ parts.push(`Status: ${statusLabel}`);
140
+ return parts.join(" ");
141
+ }
142
+ export function prioritizeAlerts(alerts) {
143
+ const seen = new Set();
144
+ const priority = {
145
+ error: 0,
146
+ blocked: 1,
147
+ warning: 2,
148
+ };
149
+ return alerts
150
+ .filter((alert) => {
151
+ const key = `${alert.level}:${alert.message}`;
152
+ if (seen.has(key)) {
153
+ return false;
104
154
  }
105
- return `Enter save current state Tab switch pane Up/Down move ${backHint}`;
155
+ seen.add(key);
156
+ return true;
157
+ })
158
+ .sort((left, right) => priority[left.level] - priority[right.level])
159
+ .slice(0, 2);
160
+ }
161
+ export function getInitialDetailFocus({ hasAgents, hasSkills, }) {
162
+ if (hasAgents) {
163
+ return "detail.agents";
164
+ }
165
+ if (hasSkills) {
166
+ return "detail.skills";
106
167
  }
107
- return `Space toggle agent Enter move to save Tab switch pane Up/Down move ${backHint}`;
168
+ return "detail.actions";
108
169
  }
109
- export function buildContextBar({ blockedCount, changeCount, previewError, previewLoading, savePhase, saveMessage, selectedLeafName, selectedLeafWarnings, skippedLeafs, sourceLabel, }) {
110
- const parts = [sourceLabel];
111
- if (selectedLeafName) {
112
- parts.push(`skill ${selectedLeafName}`);
170
+ export function moveDetailFocus({ actionCursor, actionCount, agentCount, agentCursor, direction, focus, skillCount, skillCursor, }) {
171
+ if (focus === "detail.agents") {
172
+ if (direction === -1) {
173
+ if (agentCount > 0 && agentCursor > 0) {
174
+ return { focus, agentCursor: agentCursor - 1, skillCursor, actionCursor };
175
+ }
176
+ return { focus, agentCursor, skillCursor, actionCursor };
177
+ }
178
+ if (agentCount > 0 && agentCursor < agentCount - 1) {
179
+ return { focus, agentCursor: agentCursor + 1, skillCursor, actionCursor };
180
+ }
181
+ if (skillCount > 0) {
182
+ return { focus: "detail.skills", agentCursor, skillCursor: 0, actionCursor };
183
+ }
184
+ return {
185
+ focus: "detail.actions",
186
+ agentCursor,
187
+ skillCursor,
188
+ actionCursor: Math.min(actionCursor, Math.max(0, actionCount - 1)),
189
+ };
113
190
  }
114
- if (savePhase === "saving") {
115
- parts.push("saving changes...");
116
- return parts.join(" · ");
191
+ if (focus === "detail.skills") {
192
+ if (direction === -1) {
193
+ if (skillCount > 0 && skillCursor > 0) {
194
+ return { focus, agentCursor, skillCursor: skillCursor - 1, actionCursor };
195
+ }
196
+ if (agentCount > 0) {
197
+ return {
198
+ focus: "detail.agents",
199
+ agentCursor: Math.max(0, agentCount - 1),
200
+ skillCursor,
201
+ actionCursor,
202
+ };
203
+ }
204
+ return { focus, agentCursor, skillCursor, actionCursor };
205
+ }
206
+ if (skillCount > 0 && skillCursor < skillCount - 1) {
207
+ return { focus, agentCursor, skillCursor: skillCursor + 1, actionCursor };
208
+ }
209
+ return {
210
+ focus: "detail.actions",
211
+ agentCursor,
212
+ skillCursor,
213
+ actionCursor: Math.min(actionCursor, Math.max(0, actionCount - 1)),
214
+ };
117
215
  }
118
- if (savePhase === "failed") {
119
- parts.push(saveMessage ?? "save failed");
120
- return parts.join(" · ");
216
+ if (direction === 1) {
217
+ if (actionCursor < actionCount - 1) {
218
+ return { focus, agentCursor, skillCursor, actionCursor: actionCursor + 1 };
219
+ }
220
+ return { focus, agentCursor, skillCursor, actionCursor };
121
221
  }
122
- if (savePhase === "saved") {
123
- parts.push(saveMessage ?? "saved");
124
- return parts.join(" · ");
222
+ if (actionCursor > 0) {
223
+ return { focus, agentCursor, skillCursor, actionCursor: actionCursor - 1 };
125
224
  }
126
- if (previewLoading) {
127
- parts.push("planning changes...");
128
- return parts.join(" · ");
225
+ if (skillCount > 0) {
226
+ return {
227
+ focus: "detail.skills",
228
+ agentCursor,
229
+ skillCursor: Math.max(0, skillCount - 1),
230
+ actionCursor,
231
+ };
129
232
  }
130
- if (previewError) {
131
- parts.push(`preview failed: ${previewError}`);
132
- return parts.join(" · ");
233
+ if (agentCount > 0) {
234
+ return {
235
+ focus: "detail.agents",
236
+ agentCursor: Math.max(0, agentCount - 1),
237
+ skillCursor,
238
+ actionCursor,
239
+ };
133
240
  }
134
- if (selectedLeafWarnings.length > 0) {
135
- parts.push(`warning: ${selectedLeafWarnings[0]}`);
136
- return parts.join(" · ");
241
+ return { focus, agentCursor, skillCursor, actionCursor };
242
+ }
243
+ export function getNextSelectionIndexAfterDelete(currentIndex, nextCount) {
244
+ if (nextCount <= 0) {
245
+ return -1;
137
246
  }
138
- if (skippedLeafs > 0) {
139
- parts.push(`skipped ${skippedLeafs} invalid or duplicate skills`);
140
- return parts.join(" · ");
247
+ return Math.min(currentIndex, nextCount - 1);
248
+ }
249
+ export function captureFocusSnapshot({ actionCursor, agentCursor, availableTargets, focus, groupId, selectedGroupIndex, selectedSummary, skillCursor, }) {
250
+ return {
251
+ focus,
252
+ groupIndex: selectedGroupIndex,
253
+ groupId,
254
+ sourceId: selectedSummary?.source.id,
255
+ agentTarget: agentCursor > 0 ? availableTargets[agentCursor - 1] : undefined,
256
+ skillId: selectedSummary && skillCursor > 0
257
+ ? selectedSummary.leafs[skillCursor - 1]?.id
258
+ : undefined,
259
+ action: actionCursor === 1 ? "delete" : "update",
260
+ };
261
+ }
262
+ export function reconcileFocusAfterReload({ availableTargets, nextGroups, snapshot, }) {
263
+ const selectedGroupIndex = nextGroups.findIndex((group) => group.id === snapshot.groupId);
264
+ const fallbackGroupIndex = Math.min(snapshot.groupIndex, Math.max(0, nextGroups.length - 1));
265
+ const resolvedGroupIndex = nextGroups.length === 0
266
+ ? -1
267
+ : selectedGroupIndex >= 0 && nextGroups[selectedGroupIndex]
268
+ ? selectedGroupIndex
269
+ : fallbackGroupIndex;
270
+ const group = resolvedGroupIndex >= 0 ? nextGroups[resolvedGroupIndex] : undefined;
271
+ const skillRows = group ? buildConfigGroupSkillRows(group) : [];
272
+ const hasAgents = availableTargets.length > 0;
273
+ const hasSkills = skillRows.length > 0;
274
+ let focus = snapshot.focus;
275
+ let agentCursor = 0;
276
+ let skillCursor = 0;
277
+ let actionCursor = snapshot.action === "delete" && group?.kind !== "clawhub" ? 1 : 0;
278
+ if (focus === "detail.agents") {
279
+ if (hasAgents) {
280
+ const nextAgentIndex = snapshot.agentTarget
281
+ ? availableTargets.indexOf(snapshot.agentTarget)
282
+ : -1;
283
+ agentCursor = nextAgentIndex >= 0 ? nextAgentIndex + 1 : 0;
284
+ }
285
+ else {
286
+ focus = getInitialDetailFocus({ hasAgents, hasSkills });
287
+ }
288
+ }
289
+ if (focus === "detail.skills") {
290
+ if (hasSkills) {
291
+ const nextSkillIndex = snapshot.sourceId && snapshot.skillId
292
+ ? skillRows.findIndex((row) => row.summary.source.id === snapshot.sourceId && row.leaf.id === snapshot.skillId)
293
+ : -1;
294
+ skillCursor = nextSkillIndex >= 0 ? nextSkillIndex + 1 : 0;
295
+ }
296
+ else {
297
+ focus = getInitialDetailFocus({ hasAgents, hasSkills });
298
+ }
299
+ }
300
+ if (focus === "detail.actions") {
301
+ focus = "detail.actions";
302
+ }
303
+ return {
304
+ actionCursor,
305
+ agentCursor,
306
+ focus: resolvedGroupIndex >= 0 ? focus : "groups",
307
+ groupCursor: resolvedGroupIndex,
308
+ selectedGroupIndex: resolvedGroupIndex,
309
+ skillCursor,
310
+ };
311
+ }
312
+ export function getRequestedAction({ actionCursor, canDelete, focus, input, keyReturn, }) {
313
+ if (input === "u") {
314
+ return "update";
315
+ }
316
+ if (input === "d" && canDelete) {
317
+ return "delete";
318
+ }
319
+ if (focus === "detail.actions" && keyReturn) {
320
+ return actionCursor === 1 && canDelete ? "delete" : "update";
141
321
  }
142
- parts.push(`changes ${changeCount}`);
143
- parts.push(`blocked ${blockedCount}`);
144
- return parts.join(" · ");
322
+ return undefined;
145
323
  }
146
324
  export function buildProjectionWarningMap({ drafts, summaries, sourceId, }) {
147
325
  const currentDraft = drafts[sourceId] ?? EMPTY_DRAFT;
@@ -251,70 +429,143 @@ function buildProjectionNameMap({ drafts, summaries, sourceId, }) {
251
429
  })),
252
430
  ]);
253
431
  }
254
- export function ConfigApp({ app, availableTargets, summaries, initialDrafts, }) {
432
+ export function buildScrollableRows(items, cursorIndex, visibleCount, keyPrefix = "scroll", reserveHintSlots = false) {
433
+ const safeVisibleCount = Math.max(1, visibleCount);
434
+ const needsHints = items.length > safeVisibleCount;
435
+ const hasHintSlots = needsHints || (reserveHintSlots && safeVisibleCount >= 3);
436
+ const adjustedVisibleCount = hasHintSlots ? Math.max(1, safeVisibleCount - 2) : safeVisibleCount;
437
+ const windowed = getWindowedRows(items, cursorIndex, adjustedVisibleCount);
438
+ const hasUp = windowed.start > 0;
439
+ const hasDown = windowed.end < items.length;
440
+ const rows = [];
441
+ if (hasHintSlots) {
442
+ rows.push({
443
+ key: `__scroll_up__:${keyPrefix}`,
444
+ text: hasUp ? "↑ more" : "",
445
+ active: false,
446
+ color: "gray",
447
+ });
448
+ }
449
+ rows.push(...windowed.rows);
450
+ if (hasHintSlots) {
451
+ rows.push({
452
+ key: `__scroll_down__:${keyPrefix}`,
453
+ text: hasDown ? "↓ more" : "",
454
+ active: false,
455
+ color: "gray",
456
+ });
457
+ }
458
+ return {
459
+ rows,
460
+ start: windowed.start,
461
+ end: windowed.end,
462
+ };
463
+ }
464
+ export function ConfigApp({ app, availableTargets, summaries, initialDrafts, bootStatus, }) {
255
465
  const { exit } = useApp();
256
466
  const { stdout } = useStdout();
257
467
  const previewRequestIds = useRef({});
258
- const [selectedGroupIndex, setSelectedGroupIndex] = useState(summaries.length > 0 ? 0 : -1);
259
- const [groupCursor, setGroupCursor] = useState(summaries.length > 0 ? 0 : -1);
468
+ const saveRequestIds = useRef({});
469
+ const updateRequestIds = useRef({});
470
+ const updatedTimers = useRef({});
471
+ const [summaryList, setSummaryList] = useState(summaries);
472
+ const groupViews = buildConfigGroups(summaryList);
473
+ const [selectedGroupIndex, setSelectedGroupIndex] = useState(groupViews.length > 0 ? 0 : -1);
474
+ const [groupCursor, setGroupCursor] = useState(groupViews.length > 0 ? 0 : -1);
260
475
  const [focus, setFocus] = useState("groups");
261
476
  const [skillCursor, setSkillCursor] = useState(0);
262
477
  const [targetCursor, setTargetCursor] = useState(0);
478
+ const [actionCursor, setActionCursor] = useState(0);
263
479
  const [drafts, setDrafts] = useState(initialDrafts);
264
480
  const [savedDrafts, setSavedDrafts] = useState(initialDrafts);
265
481
  const [previewBySourceId, setPreviewBySourceId] = useState({});
266
482
  const [saveStateBySourceId, setSaveStateBySourceId] = useState({});
267
- const selectedSummary = summaries[selectedGroupIndex];
268
- const selectedSourceId = selectedSummary?.source.id ?? "";
483
+ const [updateStateBySourceId, setUpdateStateBySourceId] = useState({});
484
+ const [deleteState, setDeleteState] = useState({
485
+ phase: "idle",
486
+ sourceId: undefined,
487
+ message: undefined,
488
+ });
489
+ const selectedGroup = groupViews[selectedGroupIndex] ?? EMPTY_CONFIG_GROUP;
490
+ const selectedSkillRows = buildConfigGroupSkillRows(selectedGroup);
491
+ const selectedSkillRow = skillCursor > 0 ? selectedSkillRows[skillCursor - 1] : undefined;
492
+ const activeSummary = selectedSkillRow?.summary ?? selectedGroup.summaries[0];
493
+ const selectedSourceId = activeSummary?.source.id ?? "";
269
494
  const selectedDraft = drafts[selectedSourceId] ?? EMPTY_DRAFT;
270
495
  const savedDraft = savedDrafts[selectedSourceId] ?? EMPTY_DRAFT;
271
496
  const isDirty = !draftsEqual(selectedDraft, savedDraft);
272
- const leafIds = selectedSummary?.leafs.map((leaf) => leaf.id) ?? [];
273
- const treeState = {
274
- allLeafIds: leafIds,
275
- selectedLeafIds: selectedDraft.selectedLeafIds,
276
- };
277
- const parentSelectionState = getParentSelectionState(treeState);
497
+ const leafIds = activeSummary?.leafs.map((leaf) => leaf.id) ?? [];
498
+ const groupSelectedLeafCount = getGroupSelectedLeafCount({
499
+ drafts,
500
+ group: selectedGroup,
501
+ });
278
502
  const visibleTargets = availableTargets;
279
- const targetSelectableCount = visibleTargets.length > 0 ? visibleTargets.length + 1 : 0;
280
- const targetSaveCursor = targetSelectableCount;
503
+ const agentInteractiveCount = visibleTargets.length > 0 ? visibleTargets.length + 1 : 0;
504
+ const skillInteractiveCount = selectedSkillRows.length > 0 ? selectedSkillRows.length + 1 : 0;
505
+ const treeState = selectedGroup.kind === "clawhub"
506
+ ? {
507
+ allLeafIds: selectedSkillRows.map((row) => row.leaf.id),
508
+ selectedLeafIds: selectedGroup.summaries.flatMap((summary) => drafts[summary.source.id]?.selectedLeafIds ?? []),
509
+ }
510
+ : {
511
+ allLeafIds: leafIds,
512
+ selectedLeafIds: selectedDraft.selectedLeafIds,
513
+ };
514
+ const parentSelectionState = getParentSelectionState(treeState);
281
515
  const visibleEnabledTargets = visibleTargets.filter((target) => selectedDraft.enabledTargets.includes(target));
282
516
  const allTargetsSelected = visibleTargets.length > 0 && visibleEnabledTargets.length === visibleTargets.length;
283
517
  const projectionWarningsByLeafId = buildProjectionWarningMap({
284
518
  drafts,
285
- summaries,
519
+ summaries: summaryList,
286
520
  sourceId: selectedSourceId,
287
521
  });
288
522
  const projectedNamesByLeafId = buildProjectionNameMap({
289
523
  drafts,
290
- summaries,
524
+ summaries: summaryList,
291
525
  sourceId: selectedSourceId,
292
526
  });
293
- const selectedLeaf = selectedSummary && skillCursor > 0
294
- ? selectedSummary.leafs[skillCursor - 1]
295
- : undefined;
296
- const selectedLeafWarnings = selectedLeaf
297
- ? [
298
- ...selectedLeaf.metadataWarnings,
299
- ...(projectionWarningsByLeafId[selectedLeaf.id] ?? []),
300
- ]
301
- : [];
527
+ const failedBootBySourceId = new Map(bootStatus.failedSources.map((item) => [item.sourceId, item.message]));
302
528
  const previewState = previewBySourceId[selectedSourceId] ?? EMPTY_PREVIEW;
303
- const actionCounts = countActions(previewState.actions);
304
529
  const changeCount = getActionChangeCount(previewState.actions);
305
530
  const saveState = saveStateBySourceId[selectedSourceId] ?? {
306
531
  phase: "idle",
307
532
  message: undefined,
308
533
  };
309
- const savePhase = getSaveDisplayPhase(saveState.phase, isDirty);
310
- const skippedLeafs = selectedSummary?.lock?.invalidLeafs.length ?? 0;
534
+ const updateState = updateStateBySourceId[selectedSourceId] ?? {
535
+ phase: "idle",
536
+ message: undefined,
537
+ };
538
+ const isSelectedDelete = deleteState.sourceId === selectedSourceId;
539
+ const canDelete = selectedGroup.kind === "clawhub" ? focus === "detail.skills" && skillCursor > 0 : true;
540
+ const showDeleteAction = selectedGroup.kind !== "clawhub";
541
+ const actionCount = showDeleteAction ? 2 : 1;
542
+ const statusDisplay = getStatusDisplay({
543
+ deleteState,
544
+ isSelectedDelete,
545
+ saveState,
546
+ updateState,
547
+ });
548
+ const canEditSelected = activeSummary !== undefined &&
549
+ updateState.phase !== "updating" &&
550
+ deleteState.phase !== "deleting";
551
+ const canRunActions = activeSummary !== undefined &&
552
+ saveState.phase !== "saving" &&
553
+ updateState.phase !== "updating" &&
554
+ deleteState.phase !== "deleting";
311
555
  useEffect(() => {
312
- if (!selectedSummary) {
556
+ return () => {
557
+ for (const timer of Object.values(updatedTimers.current)) {
558
+ if (timer) {
559
+ clearTimeout(timer);
560
+ }
561
+ }
562
+ };
563
+ }, []);
564
+ useEffect(() => {
565
+ if (!activeSummary) {
313
566
  return;
314
567
  }
315
- // draft -> preview request id -> latest preview wins
316
- // save -> explicit phase -> dirty/saving/saved/failed
317
- const sourceId = selectedSummary.source.id;
568
+ const sourceId = activeSummary.source.id;
318
569
  const requestId = (previewRequestIds.current[sourceId] ?? 0) + 1;
319
570
  previewRequestIds.current[sourceId] = requestId;
320
571
  setPreviewBySourceId((current) => ({
@@ -363,12 +614,74 @@ export function ConfigApp({ app, availableTargets, summaries, initialDrafts, })
363
614
  return () => {
364
615
  disposed = true;
365
616
  };
366
- }, [app, selectedDraft, selectedSummary]);
617
+ }, [app, activeSummary, selectedDraft]);
618
+ useEffect(() => {
619
+ if (!activeSummary || draftsEqual(selectedDraft, savedDraft)) {
620
+ return;
621
+ }
622
+ if (saveState.phase === "saving" || saveState.phase === "failed") {
623
+ return;
624
+ }
625
+ const sourceId = activeSummary.source.id;
626
+ const requestId = (saveRequestIds.current[sourceId] ?? 0) + 1;
627
+ saveRequestIds.current[sourceId] = requestId;
628
+ const draftToSave = normalizeDraft(selectedDraft);
629
+ setSaveStateBySourceId((current) => ({
630
+ ...current,
631
+ [sourceId]: {
632
+ phase: "saving",
633
+ message: "saving changes...",
634
+ },
635
+ }));
636
+ void app.applyDraft(sourceId, draftToSave).then((result) => {
637
+ setSaveStateBySourceId((current) => {
638
+ if ((saveRequestIds.current[sourceId] ?? 0) !== requestId) {
639
+ return current;
640
+ }
641
+ if (!result.ok) {
642
+ return {
643
+ ...current,
644
+ [sourceId]: {
645
+ phase: "failed",
646
+ message: firstErrorMessage(result),
647
+ },
648
+ };
649
+ }
650
+ const appliedDraft = normalizeDraft(result.data.draft);
651
+ setDrafts((draftsCurrent) => ({
652
+ ...draftsCurrent,
653
+ [sourceId]: appliedDraft,
654
+ }));
655
+ setSavedDrafts((savedCurrent) => ({
656
+ ...savedCurrent,
657
+ [sourceId]: appliedDraft,
658
+ }));
659
+ setPreviewBySourceId((previewCurrent) => ({
660
+ ...previewCurrent,
661
+ [sourceId]: {
662
+ actions: result.data.actions,
663
+ blockedCount: result.data.actions.filter((action) => action.kind === "blocked")
664
+ .length,
665
+ errorMessage: undefined,
666
+ loading: false,
667
+ requestId: previewRequestIds.current[sourceId] ?? 0,
668
+ },
669
+ }));
670
+ return {
671
+ ...current,
672
+ [sourceId]: {
673
+ phase: "saved",
674
+ message: "saved",
675
+ },
676
+ };
677
+ });
678
+ });
679
+ }, [app, saveState.phase, savedDraft, selectedDraft, activeSummary]);
367
680
  const updateSelectedDraft = (updater) => {
368
- if (!selectedSummary) {
681
+ if (!activeSummary || !canEditSelected) {
369
682
  return;
370
683
  }
371
- const sourceId = selectedSummary.source.id;
684
+ const sourceId = activeSummary.source.id;
372
685
  setDrafts((current) => {
373
686
  const currentDraft = current[sourceId] ?? EMPTY_DRAFT;
374
687
  const nextDraft = normalizeDraft(updater(currentDraft));
@@ -394,76 +707,173 @@ export function ConfigApp({ app, availableTargets, summaries, initialDrafts, })
394
707
  };
395
708
  });
396
709
  };
397
- const handleSave = () => {
398
- if (!selectedSummary || savePhase === "saving") {
710
+ const handleUpdate = () => {
711
+ if (!activeSummary || !selectedGroup || !canRunActions) {
399
712
  return;
400
713
  }
401
- const sourceId = selectedSummary.source.id;
402
- const nextRequestId = (previewRequestIds.current[sourceId] ?? 0) + 1;
403
- previewRequestIds.current[sourceId] = nextRequestId;
404
- setSaveStateBySourceId((current) => ({
405
- ...current,
406
- [sourceId]: {
407
- phase: "saving",
408
- message: "saving changes...",
409
- },
410
- }));
411
- setPreviewBySourceId((current) => ({
714
+ const sourceId = activeSummary.source.id;
715
+ const sourceIds = selectedGroup.kind === "clawhub"
716
+ ? selectedGroup.summaries.map((summary) => summary.source.id)
717
+ : [sourceId];
718
+ const requestId = (updateRequestIds.current[sourceId] ?? 0) + 1;
719
+ updateRequestIds.current[sourceId] = requestId;
720
+ for (const id of sourceIds) {
721
+ previewRequestIds.current[id] = (previewRequestIds.current[id] ?? 0) + 1;
722
+ saveRequestIds.current[id] = (saveRequestIds.current[id] ?? 0) + 1;
723
+ }
724
+ if (updatedTimers.current[sourceId]) {
725
+ clearTimeout(updatedTimers.current[sourceId]);
726
+ updatedTimers.current[sourceId] = undefined;
727
+ }
728
+ const snapshot = captureFocusSnapshot({
729
+ actionCursor,
730
+ agentCursor: targetCursor,
731
+ availableTargets,
732
+ focus,
733
+ groupId: selectedGroup.id,
734
+ selectedGroupIndex,
735
+ selectedSummary: activeSummary,
736
+ skillCursor,
737
+ });
738
+ setUpdateStateBySourceId((current) => ({
412
739
  ...current,
413
740
  [sourceId]: {
414
- ...(current[sourceId] ?? EMPTY_PREVIEW),
415
- errorMessage: undefined,
416
- loading: false,
417
- requestId: nextRequestId,
741
+ phase: "updating",
742
+ message: `updating ${selectedGroup.title}...`,
418
743
  },
419
744
  }));
420
- const draftToSave = normalizeDraft(selectedDraft);
421
- void app.applyDraft(sourceId, draftToSave).then((result) => {
745
+ void app.updateSources(sourceIds).then(async (result) => {
746
+ if ((updateRequestIds.current[sourceId] ?? 0) !== requestId) {
747
+ return;
748
+ }
422
749
  if (!result.ok) {
423
- const message = firstErrorMessage(result);
424
- setSaveStateBySourceId((current) => ({
750
+ setUpdateStateBySourceId((current) => ({
425
751
  ...current,
426
752
  [sourceId]: {
427
753
  phase: "failed",
428
- message,
754
+ message: firstErrorMessage(result),
429
755
  },
430
756
  }));
431
757
  return;
432
758
  }
433
- const appliedDraft = normalizeDraft(result.data.draft);
434
- const appliedChangeCount = getActionChangeCount(result.data.actions);
435
- setDrafts((current) => ({
436
- ...current,
437
- [sourceId]: appliedDraft,
438
- }));
439
- setSavedDrafts((current) => ({
440
- ...current,
441
- [sourceId]: appliedDraft,
442
- }));
759
+ const configResult = await app.getConfigData();
760
+ if ((updateRequestIds.current[sourceId] ?? 0) !== requestId) {
761
+ return;
762
+ }
763
+ if (!configResult.ok) {
764
+ setUpdateStateBySourceId((current) => ({
765
+ ...current,
766
+ [sourceId]: {
767
+ phase: "failed",
768
+ message: firstErrorMessage(configResult),
769
+ },
770
+ }));
771
+ return;
772
+ }
773
+ const nextSummaries = configResult.data.summaries;
774
+ const nextDrafts = buildDraftsFromSummaries(nextSummaries);
775
+ const nextIds = new Set(nextSummaries.map((summary) => summary.source.id));
776
+ const nextGroups = buildConfigGroups(nextSummaries);
777
+ const nextFocusState = reconcileFocusAfterReload({
778
+ availableTargets,
779
+ nextGroups,
780
+ snapshot,
781
+ });
782
+ setSummaryList(nextSummaries);
783
+ setDrafts(nextDrafts);
784
+ setSavedDrafts(nextDrafts);
785
+ setPreviewBySourceId((current) => pruneSourceMap(current, nextIds));
443
786
  setSaveStateBySourceId((current) => ({
444
- ...current,
787
+ ...pruneSourceMap(current, nextIds),
445
788
  [sourceId]: {
446
789
  phase: "saved",
447
- message: appliedChangeCount > 0
448
- ? `saved ${appliedChangeCount} changes`
449
- : "saved with no changes",
790
+ message: "saved",
450
791
  },
451
792
  }));
452
- setPreviewBySourceId((current) => ({
453
- ...current,
793
+ setSelectedGroupIndex(nextFocusState.selectedGroupIndex);
794
+ setGroupCursor(nextFocusState.groupCursor);
795
+ setFocus(nextFocusState.focus);
796
+ setTargetCursor(nextFocusState.agentCursor);
797
+ setSkillCursor(nextFocusState.skillCursor);
798
+ setActionCursor(nextFocusState.actionCursor);
799
+ setUpdateStateBySourceId((current) => ({
800
+ ...pruneSourceMap(current, nextIds),
454
801
  [sourceId]: {
455
- actions: result.data.actions,
456
- blockedCount: result.data.actions.filter((action) => action.kind === "blocked")
457
- .length,
458
- errorMessage: undefined,
459
- loading: false,
460
- requestId: nextRequestId,
802
+ phase: "updated",
803
+ message: "updated",
461
804
  },
462
805
  }));
806
+ updatedTimers.current[sourceId] = setTimeout(() => {
807
+ if ((updateRequestIds.current[sourceId] ?? 0) !== requestId) {
808
+ return;
809
+ }
810
+ setUpdateStateBySourceId((current) => {
811
+ const state = current[sourceId];
812
+ if (!state || state.phase !== "updated") {
813
+ return current;
814
+ }
815
+ return {
816
+ ...current,
817
+ [sourceId]: {
818
+ phase: "idle",
819
+ message: undefined,
820
+ },
821
+ };
822
+ });
823
+ updatedTimers.current[sourceId] = undefined;
824
+ }, UPDATED_FEEDBACK_MS);
825
+ });
826
+ };
827
+ const handleDelete = () => {
828
+ if (!activeSummary || !canRunActions) {
829
+ return;
830
+ }
831
+ const sourceId = activeSummary.source.id;
832
+ previewRequestIds.current[sourceId] = (previewRequestIds.current[sourceId] ?? 0) + 1;
833
+ saveRequestIds.current[sourceId] = (saveRequestIds.current[sourceId] ?? 0) + 1;
834
+ updateRequestIds.current[sourceId] = (updateRequestIds.current[sourceId] ?? 0) + 1;
835
+ if (updatedTimers.current[sourceId]) {
836
+ clearTimeout(updatedTimers.current[sourceId]);
837
+ updatedTimers.current[sourceId] = undefined;
838
+ }
839
+ setDeleteState({
840
+ phase: "deleting",
841
+ sourceId,
842
+ message: `deleting ${formatGroupLabel(activeSummary.source)}...`,
843
+ });
844
+ void app.uninstall([sourceId]).then((result) => {
845
+ if (!result.ok) {
846
+ setDeleteState({
847
+ phase: "failed",
848
+ sourceId,
849
+ message: firstErrorMessage(result),
850
+ });
851
+ return;
852
+ }
853
+ const nextSummaries = summaryList.filter((summary) => summary.source.id !== sourceId);
854
+ const nextGroups = buildConfigGroups(nextSummaries);
855
+ const nextSelectedGroupIndex = getNextSelectionIndexAfterDelete(selectedGroupIndex, nextGroups.length);
856
+ setSummaryList(nextSummaries);
857
+ setDrafts((current) => removeSourceFromMap(current, sourceId));
858
+ setSavedDrafts((current) => removeSourceFromMap(current, sourceId));
859
+ setPreviewBySourceId((current) => removeSourceFromMap(current, sourceId));
860
+ setSaveStateBySourceId((current) => removeSourceFromMap(current, sourceId));
861
+ setUpdateStateBySourceId((current) => removeSourceFromMap(current, sourceId));
862
+ setDeleteState({
863
+ phase: "idle",
864
+ sourceId: undefined,
865
+ message: undefined,
866
+ });
867
+ setSelectedGroupIndex(nextSelectedGroupIndex);
868
+ setGroupCursor(nextSelectedGroupIndex);
869
+ setFocus("groups");
870
+ setTargetCursor(0);
871
+ setSkillCursor(0);
872
+ setActionCursor(0);
463
873
  });
464
874
  };
465
875
  useInput((input, key) => {
466
- if (!selectedSummary) {
876
+ if (!activeSummary) {
467
877
  if (input === "q" || key.escape || (input === "c" && key.ctrl)) {
468
878
  exit();
469
879
  }
@@ -473,12 +883,23 @@ export function ConfigApp({ app, availableTargets, summaries, initialDrafts, })
473
883
  exit();
474
884
  return;
475
885
  }
886
+ const requestedAction = getRequestedAction({
887
+ actionCursor,
888
+ canDelete,
889
+ focus,
890
+ input,
891
+ keyReturn: Boolean(key.return),
892
+ });
893
+ if (requestedAction === "update") {
894
+ handleUpdate();
895
+ return;
896
+ }
897
+ if (requestedAction === "delete") {
898
+ handleDelete();
899
+ return;
900
+ }
476
901
  if (input === "q" || key.escape) {
477
- if (focus === "targets") {
478
- setFocus("skills");
479
- return;
480
- }
481
- if (focus === "skills") {
902
+ if (focus !== "groups") {
482
903
  setFocus("groups");
483
904
  return;
484
905
  }
@@ -486,73 +907,65 @@ export function ConfigApp({ app, availableTargets, summaries, initialDrafts, })
486
907
  return;
487
908
  }
488
909
  if (key.tab) {
489
- const cycle = ["groups", "skills", "targets"];
490
- const currentIndex = cycle.indexOf(focus);
491
- setFocus(cycle[(currentIndex + 1) % cycle.length] ?? "groups");
910
+ if (focus === "groups") {
911
+ setFocus(getInitialDetailFocus({
912
+ hasAgents: agentInteractiveCount > 0,
913
+ hasSkills: skillInteractiveCount > 0,
914
+ }));
915
+ return;
916
+ }
917
+ setFocus("groups");
492
918
  return;
493
919
  }
494
- if (focus === "groups") {
495
- if (key.downArrow) {
496
- setGroupCursor((current) => {
497
- const next = Math.min(current + 1, Math.max(0, summaries.length - 1));
498
- setSelectedGroupIndex(next);
499
- setSkillCursor(0);
500
- setTargetCursor(0);
501
- return next;
502
- });
503
- }
504
- if (key.upArrow) {
505
- setGroupCursor((current) => {
506
- const next = Math.max(current - 1, 0);
507
- setSelectedGroupIndex(next);
508
- setSkillCursor(0);
509
- setTargetCursor(0);
510
- return next;
511
- });
512
- }
513
- if (key.return) {
514
- setFocus("skills");
515
- }
920
+ if (key.rightArrow && focus === "groups") {
921
+ setFocus(getInitialDetailFocus({
922
+ hasAgents: agentInteractiveCount > 0,
923
+ hasSkills: skillInteractiveCount > 0,
924
+ }));
516
925
  return;
517
926
  }
518
- if (focus === "skills") {
927
+ if (key.leftArrow && focus !== "groups") {
928
+ setFocus("groups");
929
+ return;
930
+ }
931
+ if (focus === "groups") {
519
932
  if (key.downArrow) {
520
- setSkillCursor((current) => Math.min(current + 1, leafIds.length));
933
+ const next = Math.min(groupCursor + 1, Math.max(0, groupViews.length - 1));
934
+ setGroupCursor(next);
935
+ setSelectedGroupIndex(next);
936
+ setTargetCursor(0);
937
+ setSkillCursor(0);
938
+ setActionCursor(0);
521
939
  }
522
940
  if (key.upArrow) {
523
- setSkillCursor((current) => Math.max(current - 1, 0));
524
- }
525
- if (input === " ") {
526
- updateSelectedDraft((currentDraft) => {
527
- const baseState = {
528
- allLeafIds: leafIds,
529
- selectedLeafIds: currentDraft.selectedLeafIds,
530
- };
531
- const nextState = skillCursor === 0
532
- ? toggleParent(baseState)
533
- : toggleChild(baseState, leafIds[skillCursor - 1]);
534
- return {
535
- ...currentDraft,
536
- selectedLeafIds: nextState.selectedLeafIds,
537
- };
538
- });
539
- }
540
- if (key.return) {
541
- setFocus("targets");
941
+ const next = Math.max(groupCursor - 1, 0);
942
+ setGroupCursor(next);
943
+ setSelectedGroupIndex(next);
944
+ setTargetCursor(0);
945
+ setSkillCursor(0);
946
+ setActionCursor(0);
542
947
  }
543
948
  return;
544
949
  }
545
- if (key.downArrow) {
546
- setTargetCursor((current) => Math.min(current + 1, targetSaveCursor));
547
- }
548
- if (key.upArrow) {
549
- setTargetCursor((current) => Math.max(current - 1, 0));
950
+ if (key.downArrow || key.upArrow) {
951
+ const next = moveDetailFocus({
952
+ actionCursor,
953
+ actionCount,
954
+ agentCount: agentInteractiveCount,
955
+ agentCursor: targetCursor,
956
+ direction: key.downArrow ? 1 : -1,
957
+ focus: focus,
958
+ skillCount: skillInteractiveCount,
959
+ skillCursor,
960
+ });
961
+ setFocus(next.focus);
962
+ setTargetCursor(next.agentCursor);
963
+ setSkillCursor(next.skillCursor);
964
+ setActionCursor(next.actionCursor);
965
+ return;
550
966
  }
551
- if (input === " ") {
967
+ if (focus === "detail.agents" && input === " " && agentInteractiveCount > 0) {
552
968
  updateSelectedDraft((currentDraft) => {
553
- if (visibleTargets.length === 0 || targetCursor === targetSaveCursor) {
554
- return currentDraft;
555
- }
556
969
  if (targetCursor === 0) {
557
970
  const enabledTargets = new Set(currentDraft.enabledTargets);
558
971
  const nextSelectAll = !visibleTargets.every((target) => enabledTargets.has(target));
@@ -570,6 +983,9 @@ export function ConfigApp({ app, availableTargets, summaries, initialDrafts, })
570
983
  };
571
984
  }
572
985
  const target = visibleTargets[targetCursor - 1];
986
+ if (!target) {
987
+ return currentDraft;
988
+ }
573
989
  const enabledTargets = new Set(currentDraft.enabledTargets);
574
990
  if (enabledTargets.has(target)) {
575
991
  enabledTargets.delete(target);
@@ -582,38 +998,85 @@ export function ConfigApp({ app, availableTargets, summaries, initialDrafts, })
582
998
  enabledTargets: TARGET_ORDER.filter((item) => enabledTargets.has(item)),
583
999
  };
584
1000
  });
1001
+ return;
585
1002
  }
586
- if (key.return) {
587
- if (targetCursor === targetSaveCursor) {
588
- handleSave();
589
- return;
590
- }
591
- setTargetCursor(targetSaveCursor);
1003
+ if (focus === "detail.skills" && input === " " && skillInteractiveCount > 0) {
1004
+ updateSelectedDraft((currentDraft) => {
1005
+ if (selectedGroup.kind === "clawhub") {
1006
+ if (skillCursor === 0) {
1007
+ return currentDraft;
1008
+ }
1009
+ const row = selectedSkillRows[skillCursor - 1];
1010
+ if (!row) {
1011
+ return currentDraft;
1012
+ }
1013
+ if (row.summary.source.id !== selectedSourceId) {
1014
+ return currentDraft;
1015
+ }
1016
+ const baseState = {
1017
+ allLeafIds: row.summary.leafs.map((leaf) => leaf.id),
1018
+ selectedLeafIds: currentDraft.selectedLeafIds,
1019
+ };
1020
+ const nextState = toggleChild(baseState, row.leaf.id);
1021
+ return {
1022
+ ...currentDraft,
1023
+ selectedLeafIds: nextState.selectedLeafIds,
1024
+ };
1025
+ }
1026
+ const baseState = {
1027
+ allLeafIds: leafIds,
1028
+ selectedLeafIds: currentDraft.selectedLeafIds,
1029
+ };
1030
+ const nextState = skillCursor === 0
1031
+ ? toggleParent(baseState)
1032
+ : toggleChild(baseState, leafIds[skillCursor - 1]);
1033
+ return {
1034
+ ...currentDraft,
1035
+ selectedLeafIds: nextState.selectedLeafIds,
1036
+ };
1037
+ });
592
1038
  }
593
1039
  });
594
- if (summaries.length === 0) {
595
- return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { bold: true, children: "No skills 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." })] }));
1040
+ if (groupViews.length === 0) {
1041
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { bold: true, children: "No skills groups yet" }), _jsx(Text, { children: "Run skill-flow add <source> to install your first group." }), _jsx(Text, { dimColor: true, children: deleteState.message ?? "Press q or esc to exit." })] }));
596
1042
  }
597
- const activeSummary = selectedSummary;
1043
+ const renderSummary = activeSummary;
598
1044
  const terminalRows = stdout.rows ?? 24;
599
1045
  const terminalColumns = stdout.columns ?? 120;
600
- const paneHeight = Math.max(12, terminalRows - 4);
601
- const [groupsWidth, skillsWidth, targetsWidth] = getPaneWidths(terminalColumns);
1046
+ const paneHeight = Math.max(12, terminalRows - 3);
1047
+ const [groupsWidth, detailWidth] = getPaneWidths(terminalColumns);
602
1048
  const bodyRowCount = getPaneViewportCount(paneHeight);
603
- const targetListRowCount = Math.max(1, bodyRowCount - 1);
604
- const targetItems = visibleTargets.length > 0
1049
+ const groupRows = buildScrollableRows(groupViews.map((group, index) => {
1050
+ const isCursor = groupCursor === index;
1051
+ const isSelected = selectedGroupIndex === index;
1052
+ return {
1053
+ key: group.id,
1054
+ text: `${group.title}${group.kind === "clawhub"
1055
+ ? group.summaries.some((summary) => failedBootBySourceId.has(summary.source.id))
1056
+ ? " !"
1057
+ : ""
1058
+ : failedBootBySourceId.has(group.summaries[0]?.source.id ?? "")
1059
+ ? " !"
1060
+ : ""}`,
1061
+ active: focus === "groups" && isCursor,
1062
+ activeColor: "cyan",
1063
+ bold: isSelected,
1064
+ color: isSelected ? "white" : "gray",
1065
+ };
1066
+ }), Math.max(0, groupCursor), bodyRowCount, "groups");
1067
+ const agentRows = visibleTargets.length > 0
605
1068
  ? [
606
1069
  {
607
1070
  key: "__all_targets__",
608
- text: `${selectionMarker(allTargetsSelected ? "full" : visibleEnabledTargets.length > 0 ? "partial" : "empty")} all agents`,
609
- active: focus === "targets" && targetCursor === 0,
1071
+ text: `${selectionMarker(allTargetsSelected ? "full" : visibleEnabledTargets.length > 0 ? "partial" : "empty")} All Agents`,
1072
+ active: focus === "detail.agents" && targetCursor === 0,
610
1073
  bold: true,
611
1074
  color: undefined,
612
1075
  },
613
1076
  ...visibleTargets.map((target, index) => ({
614
1077
  key: target,
615
1078
  text: `${selectionMarker(selectedDraft.enabledTargets.includes(target) ? "full" : "empty")} ${TARGET_LABELS[target]}`,
616
- active: focus === "targets" && targetCursor === index + 1,
1079
+ active: focus === "detail.agents" && targetCursor === index + 1,
617
1080
  color: "gray",
618
1081
  })),
619
1082
  ]
@@ -622,109 +1085,399 @@ export function ConfigApp({ app, availableTargets, summaries, initialDrafts, })
622
1085
  key: "__no_targets__",
623
1086
  text: "No detected agent targets",
624
1087
  active: false,
625
- bold: false,
626
1088
  color: "gray",
627
1089
  },
628
1090
  ];
629
- const groupRows = getWindowedRows(summaries.map((summary, index) => ({
630
- key: summary.source.id,
631
- text: `${formatGroupLabel(summary.source)} ${formatGroupSaveState(getSaveDisplayPhase((saveStateBySourceId[summary.source.id]?.phase ?? "idle"), !draftsEqual(drafts[summary.source.id] ?? EMPTY_DRAFT, savedDrafts[summary.source.id] ?? EMPTY_DRAFT)))}`,
632
- active: focus === "groups" && groupCursor === index,
633
- color: getGroupStateColor(getSaveDisplayPhase((saveStateBySourceId[summary.source.id]?.phase ?? "idle"), !draftsEqual(drafts[summary.source.id] ?? EMPTY_DRAFT, savedDrafts[summary.source.id] ?? EMPTY_DRAFT))),
634
- })), Math.max(0, groupCursor), bodyRowCount);
635
- const skillRows = getWindowedRows([
1091
+ const skillRows = selectedSkillRows.length > 0
1092
+ ? [
1093
+ {
1094
+ key: "__all__",
1095
+ text: `${selectionMarker(parentSelectionState)} All Skills`,
1096
+ active: focus === "detail.skills" && skillCursor === 0,
1097
+ bold: true,
1098
+ color: undefined,
1099
+ },
1100
+ ...selectedSkillRows.map((row, index) => {
1101
+ const rowDraft = drafts[row.summary.source.id] ?? EMPTY_DRAFT;
1102
+ const rowSelected = rowDraft.selectedLeafIds.includes(row.leaf.id);
1103
+ const warnings = [
1104
+ ...row.leaf.metadataWarnings,
1105
+ ...(row.summary.source.id === selectedSourceId
1106
+ ? (projectionWarningsByLeafId[row.leaf.id] ?? [])
1107
+ : []),
1108
+ ];
1109
+ const inlineWarning = warnings[0] ? ` (${warnings[0]})` : "";
1110
+ const projectedLabel = row.summary.source.id === selectedSourceId && rowSelected
1111
+ ? (projectedNamesByLeafId.get(row.leaf.id) ?? row.leaf.linkName)
1112
+ : row.leaf.linkName;
1113
+ const label = selectedGroup.kind === "clawhub"
1114
+ ? `${projectedLabel} · ${formatGroupLabel(row.summary.source)}`
1115
+ : projectedLabel;
1116
+ return {
1117
+ key: row.leaf.id,
1118
+ text: `${selectionMarker(rowSelected ? "full" : "empty")} ${label}${inlineWarning}`,
1119
+ active: focus === "detail.skills" && skillCursor === index + 1,
1120
+ color: warnings.length > 0 ? "yellow" : "gray",
1121
+ };
1122
+ }),
1123
+ ]
1124
+ : [
1125
+ {
1126
+ key: "__no_skills__",
1127
+ text: "No skills in this group",
1128
+ active: false,
1129
+ color: "gray",
1130
+ },
1131
+ ];
1132
+ const selectedGroupBootMessages = selectedGroup
1133
+ ? selectedGroup.kind === "clawhub"
1134
+ ? selectedGroup.summaries
1135
+ .map((summary) => failedBootBySourceId.get(summary.source.id))
1136
+ .filter((message) => Boolean(message))
1137
+ : [failedBootBySourceId.get(selectedSourceId)].filter((message) => Boolean(message))
1138
+ : [];
1139
+ const alerts = prioritizeAlerts(buildAlerts({
1140
+ deleteState,
1141
+ failedBootMessages: selectedGroupBootMessages,
1142
+ isSelectedDelete,
1143
+ previewState,
1144
+ projectionWarningsByLeafId,
1145
+ saveState,
1146
+ selectedDraft,
1147
+ selectedSummary: renderSummary,
1148
+ updateState,
1149
+ }));
1150
+ const previewLabel = buildPreviewLabel({
1151
+ blockedCount: previewState.blockedCount,
1152
+ changeCount,
1153
+ errorMessage: previewState.errorMessage,
1154
+ loading: previewState.loading,
1155
+ });
1156
+ const bootLabel = selectedGroupBootMessages.length > 0 ? "PARTIAL" : "OK";
1157
+ const metadataRows = buildDetailMetadataRows({
1158
+ alerts,
1159
+ detailWidth,
1160
+ group: selectedGroup,
1161
+ summary: renderSummary,
1162
+ });
1163
+ const actionRows = buildActionRows({
1164
+ actionCursor,
1165
+ canRunActions,
1166
+ deleteState,
1167
+ focus,
1168
+ isSelectedDelete,
1169
+ showDeleteAction,
1170
+ updateState,
1171
+ });
1172
+ const fixedRows = metadataRows.length +
1173
+ 4 +
1174
+ actionRows.length;
1175
+ const sectionBudget = Math.max(2, bodyRowCount - fixedRows);
1176
+ const hasAgentSection = visibleTargets.length > 0;
1177
+ const hasSkillSection = selectedSkillRows.length > 0;
1178
+ const agentBudget = hasAgentSection && hasSkillSection
1179
+ ? Math.min(Math.max(1, sectionBudget - 1), Math.max(4, Math.floor(sectionBudget * 0.4)))
1180
+ : sectionBudget;
1181
+ const skillBudget = hasAgentSection && hasSkillSection ? Math.max(1, sectionBudget - agentBudget) : sectionBudget;
1182
+ const visibleAgentRows = buildScrollableRows(agentRows, Math.min(targetCursor, Math.max(0, agentRows.length - 1)), Math.max(1, agentBudget), "agents", true);
1183
+ const visibleSkillRows = buildScrollableRows(skillRows, Math.min(skillCursor, Math.max(0, skillRows.length - 1)), Math.max(1, skillBudget), "skills", true);
1184
+ const filledSkillRows = [...visibleSkillRows.rows];
1185
+ const skillPaddingCount = Math.max(0, skillBudget - filledSkillRows.length);
1186
+ for (let index = 0; index < skillPaddingCount; index += 1) {
1187
+ filledSkillRows.push({
1188
+ key: `__skills_fill__:${index}`,
1189
+ text: "",
1190
+ active: false,
1191
+ color: undefined,
1192
+ });
1193
+ }
1194
+ const detailRows = [
1195
+ ...metadataRows,
636
1196
  {
637
- key: "__all__",
638
- text: `${selectionMarker(parentSelectionState)} all skills`,
639
- active: focus === "skills" && skillCursor === 0,
1197
+ key: "__agents_gap__",
1198
+ text: "",
1199
+ active: false,
1200
+ color: undefined,
1201
+ },
1202
+ {
1203
+ key: "__agents_header__",
1204
+ text: `Apply to Agents (${visibleEnabledTargets.length}/${visibleTargets.length})`,
1205
+ active: false,
640
1206
  bold: true,
641
1207
  color: undefined,
642
1208
  },
643
- ...activeSummary.leafs.map((leaf, index) => ({
644
- key: leaf.id,
645
- text: `${selectionMarker(selectedDraft.selectedLeafIds.includes(leaf.id) ? "full" : "empty")} ${selectedDraft.selectedLeafIds.includes(leaf.id)
646
- ? (projectedNamesByLeafId.get(leaf.id) ?? leaf.linkName)
647
- : leaf.linkName}`,
648
- active: focus === "skills" && skillCursor === index + 1,
649
- color: leaf.metadataWarnings.length > 0 || (projectionWarningsByLeafId[leaf.id]?.length ?? 0) > 0
650
- ? "yellow"
651
- : "gray",
652
- })),
653
- ], skillCursor, bodyRowCount);
654
- const targetRows = getWindowedRows(targetItems, Math.min(targetCursor, Math.max(0, targetItems.length - 1)), targetListRowCount);
655
- const saveRow = buildSaveRow(focus === "targets" && targetCursor === targetSaveCursor, savePhase, changeCount);
656
- const contextBar = buildContextBar({
657
- blockedCount: previewState.blockedCount,
658
- changeCount,
659
- previewError: previewState.errorMessage,
660
- previewLoading: previewState.loading,
661
- savePhase,
662
- saveMessage: saveState.message,
663
- selectedLeafName: selectedLeaf?.linkName,
664
- selectedLeafWarnings,
665
- skippedLeafs,
666
- sourceLabel: formatGroupLabel(activeSummary.source),
1209
+ ...visibleAgentRows.rows,
1210
+ {
1211
+ key: "__skills_gap__",
1212
+ text: "",
1213
+ active: false,
1214
+ color: undefined,
1215
+ },
1216
+ {
1217
+ key: "__skills_header__",
1218
+ text: `Included Skills (${selectedGroup.kind === "clawhub" ? groupSelectedLeafCount : selectedDraft.selectedLeafIds.length}/${selectedSkillRows.length})`,
1219
+ active: false,
1220
+ bold: true,
1221
+ color: undefined,
1222
+ },
1223
+ ...filledSkillRows,
1224
+ ];
1225
+ return (_jsxs(Box, { flexDirection: "column", height: terminalRows, children: [_jsx(Text, { color: statusDisplay.color, wrap: "truncate-end", children: buildTopBar({
1226
+ width: terminalColumns,
1227
+ isDirty,
1228
+ changeCount,
1229
+ showDelete: canDelete,
1230
+ statusLabel: statusDisplay.label,
1231
+ }) }), _jsxs(Box, { children: [_jsx(Pane, { active: focus === "groups", footer: `${groupViews.length} groups`, gapAfter: true, height: paneHeight, title: "Skills Groups", width: groupsWidth, children: renderPaneRows(groupRows.rows, bodyRowCount, groupsWidth) }), _jsx(Pane, { active: focus !== "groups", footer: buildCommandBar(focus), height: paneHeight, title: "Group Detail", width: detailWidth, children: renderPaneRows(detailRows, bodyRowCount, detailWidth, actionRows) })] }), _jsx(Text, { dimColor: true, wrap: "truncate-end", children: buildFooterHints(focus, canDelete) })] }));
1232
+ }
1233
+ export function ConfigBootstrapApp({ app }) {
1234
+ const { exit } = useApp();
1235
+ const { stdout } = useStdout();
1236
+ const [state, setState] = useState({
1237
+ phase: "loading",
1238
+ logs: ["Booting config..."],
667
1239
  });
668
- const commandBar = buildCommandBar({
669
- changeCount,
670
- focus,
671
- saveFocused: focus === "targets" && targetCursor === targetSaveCursor,
672
- savePhase,
1240
+ useEffect(() => {
1241
+ let cancelled = false;
1242
+ void app.configCoordinator.bootstrapWorkspaceState((event) => {
1243
+ if (cancelled) {
1244
+ return;
1245
+ }
1246
+ setState((current) => {
1247
+ const nextLogs = [...current.logs, event.message].slice(-6);
1248
+ return {
1249
+ ...current,
1250
+ logs: nextLogs,
1251
+ };
1252
+ });
1253
+ }).then((result) => {
1254
+ if (cancelled) {
1255
+ return;
1256
+ }
1257
+ if (!result.ok) {
1258
+ setState((current) => ({
1259
+ phase: "error",
1260
+ logs: current.logs,
1261
+ message: firstErrorMessage(result),
1262
+ }));
1263
+ return;
1264
+ }
1265
+ setState((current) => ({
1266
+ phase: "ready",
1267
+ logs: current.logs,
1268
+ availableTargets: result.data.availableTargets,
1269
+ summaries: result.data.summaries,
1270
+ initialDrafts: result.data.initialDrafts,
1271
+ audit: result.data.audit,
1272
+ bootStatus: result.data.bootStatus,
1273
+ }));
1274
+ });
1275
+ return () => {
1276
+ cancelled = true;
1277
+ };
1278
+ }, [app]);
1279
+ useInput((input, key) => {
1280
+ if (state.phase === "ready") {
1281
+ return;
1282
+ }
1283
+ if (input === "q" || key.escape || (input === "c" && key.ctrl)) {
1284
+ exit();
1285
+ }
673
1286
  });
674
- 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
675
- ? `${visibleEnabledTargets.length}/${visibleTargets.length} targets`
676
- : "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 })] }));
1287
+ if (state.phase === "ready") {
1288
+ return (_jsx(ConfigApp, { app: app, availableTargets: state.availableTargets, summaries: state.summaries, initialDrafts: state.initialDrafts, bootStatus: state.bootStatus }));
1289
+ }
1290
+ const rows = stdout.rows ?? 24;
1291
+ const bootLogs = state.logs.slice(-4);
1292
+ return (_jsxs(Box, { flexDirection: "column", height: rows, children: [_jsxs(Box, { flexGrow: 1, flexDirection: "column", children: [_jsx(Text, { bold: true, children: "Skill Flow Config" }), _jsx(Text, { color: "gray", children: state.phase === "loading"
1293
+ ? "Checking groups, skills, targets, and current paths..."
1294
+ : "Bootstrap failed" }), state.phase === "error" ? _jsx(Text, { color: "red", children: state.message }) : null] }), _jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { bold: true, children: "BOOT LOG" }), bootLogs.map((log) => (_jsx(Text, { color: "gray", children: log }, log))), _jsx(Text, { color: "gray", children: "Press q or Esc to exit." })] })] }));
677
1295
  }
678
- function buildSaveRow(active, phase, changeCount) {
679
- return {
680
- key: "__save__",
681
- text: buildSaveLabel(phase, changeCount),
682
- active,
683
- bold: true,
684
- color: getSaveColor(phase),
685
- };
1296
+ function buildSaveStatusLabel(saveState) {
1297
+ if (saveState.phase === "saving") {
1298
+ return "Saving";
1299
+ }
1300
+ if (saveState.phase === "saved") {
1301
+ return "Saved";
1302
+ }
1303
+ if (saveState.phase === "failed") {
1304
+ return "Failed";
1305
+ }
1306
+ return "Clean";
686
1307
  }
687
- function getSaveColor(phase) {
688
- if (phase === "failed") {
689
- return "red";
1308
+ function buildPreviewLabel({ blockedCount, changeCount, errorMessage, loading, }) {
1309
+ if (loading) {
1310
+ return "planning...";
690
1311
  }
691
- if (phase === "dirty") {
692
- return "yellow";
1312
+ if (errorMessage) {
1313
+ return "failed";
1314
+ }
1315
+ if (blockedCount > 0 && changeCount > 0) {
1316
+ return `${changeCount} changes, ${blockedCount} blocked`;
693
1317
  }
694
- if (phase === "saving") {
695
- return "cyan";
1318
+ if (blockedCount > 0) {
1319
+ return `${blockedCount} blocked`;
696
1320
  }
697
- return "green";
1321
+ return `${changeCount} changes`;
698
1322
  }
699
- function getGroupStateColor(phase) {
700
- if (phase === "failed") {
701
- return "red";
1323
+ function buildAlerts({ deleteState, failedBootMessages, isSelectedDelete, previewState, projectionWarningsByLeafId, saveState, selectedDraft, selectedSummary, updateState, }) {
1324
+ const alerts = [];
1325
+ if (updateState.phase === "failed" && updateState.message) {
1326
+ alerts.push({ level: "error", message: `Update failed: ${updateState.message}` });
1327
+ }
1328
+ if (isSelectedDelete && deleteState.phase === "failed" && deleteState.message) {
1329
+ alerts.push({ level: "error", message: `Delete failed: ${deleteState.message}` });
1330
+ }
1331
+ if (saveState.phase === "failed" && saveState.message) {
1332
+ alerts.push({ level: "error", message: `Save failed: ${saveState.message}` });
1333
+ }
1334
+ if (previewState.errorMessage) {
1335
+ alerts.push({ level: "error", message: `Preview failed: ${previewState.errorMessage}` });
702
1336
  }
703
- if (phase === "dirty") {
704
- return "yellow";
1337
+ for (const failedBootMessage of failedBootMessages) {
1338
+ alerts.push({ level: "error", message: `Boot issue: ${failedBootMessage}` });
705
1339
  }
706
- if (phase === "saving") {
707
- return "cyan";
1340
+ for (const action of previewState.actions) {
1341
+ if (action.kind === "blocked" && action.reason) {
1342
+ alerts.push({ level: "blocked", message: action.reason });
1343
+ }
1344
+ }
1345
+ for (const leaf of selectedSummary.leafs) {
1346
+ if (!selectedDraft.selectedLeafIds.includes(leaf.id)) {
1347
+ continue;
1348
+ }
1349
+ for (const warning of leaf.metadataWarnings) {
1350
+ alerts.push({ level: "warning", message: warning });
1351
+ }
1352
+ for (const warning of projectionWarningsByLeafId[leaf.id] ?? []) {
1353
+ alerts.push({ level: "warning", message: warning });
1354
+ }
1355
+ }
1356
+ if ((selectedSummary.lock?.invalidLeafs.length ?? 0) > 0) {
1357
+ alerts.push({
1358
+ level: "warning",
1359
+ message: `${selectedSummary.lock?.invalidLeafs.length ?? 0} invalid skill entries skipped`,
1360
+ });
1361
+ }
1362
+ return alerts;
1363
+ }
1364
+ export function buildDetailMetadataRows({ alerts, detailWidth, group, summary, }) {
1365
+ const sourceSummary = group.kind === "clawhub"
1366
+ ? `Sources: ${group.summaries.length} clawhub source${group.summaries.length === 1 ? "" : "s"}`
1367
+ : `Source: ${summary.source.locator}`;
1368
+ const rows = [
1369
+ {
1370
+ key: "__title__",
1371
+ text: group.title,
1372
+ active: false,
1373
+ bold: true,
1374
+ color: undefined,
1375
+ },
1376
+ {
1377
+ key: "__source__",
1378
+ text: fitPaneLine(sourceSummary, getPaneInnerWidth(detailWidth) - 2),
1379
+ active: false,
1380
+ color: "gray",
1381
+ },
1382
+ ];
1383
+ if (group.kind === "clawhub") {
1384
+ rows.push({
1385
+ key: "__focused_source__",
1386
+ text: fitPaneLine(`Focused Source: ${formatGroupLabel(summary.source)}`, getPaneInnerWidth(detailWidth) - 2),
1387
+ active: false,
1388
+ color: "gray",
1389
+ });
1390
+ }
1391
+ if (alerts.length > 0) {
1392
+ rows.push({
1393
+ key: "__alerts__",
1394
+ text: "Alerts",
1395
+ active: false,
1396
+ bold: true,
1397
+ color: undefined,
1398
+ });
1399
+ alerts.forEach((alert, index) => {
1400
+ rows.push({
1401
+ key: `__alert__:${index}`,
1402
+ text: `! ${alert.message}`,
1403
+ active: false,
1404
+ color: alert.level === "error"
1405
+ ? "red"
1406
+ : alert.level === "blocked"
1407
+ ? "yellow"
1408
+ : "gray",
1409
+ });
1410
+ });
708
1411
  }
709
- if (phase === "saved") {
710
- return "green";
1412
+ return rows;
1413
+ }
1414
+ export function buildActionRows({ actionCursor, canRunActions, deleteState, focus, isSelectedDelete, showDeleteAction, updateState, }) {
1415
+ const updateText = updateState.phase === "updating"
1416
+ ? "Update · UPDATING..."
1417
+ : updateState.phase === "failed"
1418
+ ? "Update · FAILED"
1419
+ : "Update";
1420
+ const rows = [
1421
+ {
1422
+ key: "__actions_separator__",
1423
+ text: "────────────────────────",
1424
+ active: false,
1425
+ color: "gray",
1426
+ },
1427
+ {
1428
+ key: "__action_update__",
1429
+ text: `[${updateText}]`,
1430
+ active: focus === "detail.actions" && actionCursor === 0,
1431
+ color: canRunActions || updateState.phase !== "idle" ? undefined : "gray",
1432
+ bold: true,
1433
+ },
1434
+ ];
1435
+ if (showDeleteAction) {
1436
+ const deleteText = isSelectedDelete && deleteState.phase === "deleting"
1437
+ ? "Delete · DELETING..."
1438
+ : isSelectedDelete && deleteState.phase === "failed"
1439
+ ? "Delete · FAILED"
1440
+ : "Delete";
1441
+ rows.push({
1442
+ key: "__action_delete__",
1443
+ text: `[${deleteText}]`,
1444
+ active: focus === "detail.actions" && actionCursor === 1,
1445
+ color: isSelectedDelete && deleteState.phase === "failed"
1446
+ ? "red"
1447
+ : canRunActions || (isSelectedDelete && deleteState.phase !== "idle")
1448
+ ? "red"
1449
+ : "gray",
1450
+ bold: true,
1451
+ });
711
1452
  }
712
- return "green";
1453
+ return rows;
713
1454
  }
714
- function buildPaneFooter(start, end, total, summary) {
715
- const overflow = formatOverflow(start, end, total);
716
- return `${overflow} · ${summary}`;
1455
+ function buildCommandBar(focus) {
1456
+ if (focus === "groups") {
1457
+ return "[Tab/→] Edit";
1458
+ }
1459
+ if (focus === "detail.actions") {
1460
+ return "[Enter] Action";
1461
+ }
1462
+ return "[Space] Toggle";
717
1463
  }
718
- function formatOverflow(start, end, total) {
719
- const above = start;
720
- const below = Math.max(0, total - end);
721
- if (above === 0 && below === 0) {
722
- return "top / bottom";
1464
+ function buildFooterHints(focus, canDelete) {
1465
+ if (focus === "groups") {
1466
+ return canDelete
1467
+ ? "[↑↓] Move [Tab/→] Switch pane [u] Update [d] Delete [q] Exit"
1468
+ : "[↑↓] Move [Tab/→] Switch pane [u] Update [q] Exit";
723
1469
  }
724
- return `${above > 0 ? `${above} above` : "top"} / ${below > 0 ? `${below} below` : "bottom"}`;
1470
+ if (focus === "detail.actions") {
1471
+ return canDelete
1472
+ ? "[↑↓] Move [Enter] Action [Tab/←/Esc] Back [u] Update [d] Delete"
1473
+ : "[↑↓] Move [Enter] Action [Tab/←/Esc] Back [u] Update";
1474
+ }
1475
+ return canDelete
1476
+ ? "[↑↓] Move [Space] Toggle [Tab/←/Esc] Back [u] Update [d] Delete"
1477
+ : "[↑↓] Move [Space] Toggle [Tab/←/Esc] Back [u] Update";
725
1478
  }
726
1479
  function RowText({ row, width }) {
727
- const color = row.active ? "cyan" : row.color;
1480
+ const color = row.active ? row.activeColor ?? "cyan" : row.color;
728
1481
  const prefix = row.active ? "> " : " ";
729
1482
  const contentWidth = Math.max(1, getPaneInnerWidth(width) - prefix.length);
730
1483
  const content = fitPaneLine(row.text, contentWidth);
@@ -768,21 +1521,6 @@ function selectionMarker(state) {
768
1521
  function getExactDuplicateKey(linkName, name, description) {
769
1522
  return `${linkName}\n${name}\n${description}`;
770
1523
  }
771
- function formatGroupSaveState(phase) {
772
- if (phase === "dirty") {
773
- return "DIRTY";
774
- }
775
- if (phase === "saving") {
776
- return "SAVING";
777
- }
778
- if (phase === "saved") {
779
- return "SAVED";
780
- }
781
- if (phase === "failed") {
782
- return "FAILED";
783
- }
784
- return "SAVED";
785
- }
786
1524
  function getWindowedRows(items, cursorIndex, visibleCount) {
787
1525
  const safeVisibleCount = Math.max(1, visibleCount);
788
1526
  const maxStart = Math.max(0, items.length - safeVisibleCount);
@@ -794,6 +1532,14 @@ function getWindowedRows(items, cursorIndex, visibleCount) {
794
1532
  end,
795
1533
  };
796
1534
  }
1535
+ function pruneSourceMap(sourceMap, allowedIds) {
1536
+ return Object.fromEntries(Object.entries(sourceMap).filter(([sourceId]) => allowedIds.has(sourceId)));
1537
+ }
1538
+ function removeSourceFromMap(sourceMap, sourceId) {
1539
+ const next = { ...sourceMap };
1540
+ delete next[sourceId];
1541
+ return next;
1542
+ }
797
1543
  function firstErrorMessage(result) {
798
1544
  return result.errors[0]?.message ?? "operation failed";
799
1545
  }