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.
- package/README.md +40 -3
- package/README.zh.md +40 -3
- package/dist/adapters/channel-adapters.js +11 -3
- package/dist/adapters/channel-adapters.js.map +1 -1
- package/dist/cli.js +69 -37
- package/dist/cli.js.map +1 -1
- package/dist/domain/types.d.ts +54 -1
- package/dist/services/config-coordinator.d.ts +38 -0
- package/dist/services/config-coordinator.js +81 -0
- package/dist/services/config-coordinator.js.map +1 -0
- package/dist/services/doctor-service.d.ts +2 -0
- package/dist/services/doctor-service.js +62 -0
- package/dist/services/doctor-service.js.map +1 -1
- package/dist/services/inventory-service.d.ts +3 -1
- package/dist/services/inventory-service.js +12 -5
- package/dist/services/inventory-service.js.map +1 -1
- package/dist/services/skill-flow.d.ts +50 -26
- package/dist/services/skill-flow.js +502 -89
- package/dist/services/skill-flow.js.map +1 -1
- package/dist/services/source-service.d.ts +20 -10
- package/dist/services/source-service.js +359 -75
- package/dist/services/source-service.js.map +1 -1
- package/dist/services/workflow-service.d.ts +2 -2
- package/dist/services/workflow-service.js +17 -4
- package/dist/services/workflow-service.js.map +1 -1
- package/dist/services/workspace-bootstrap-service.d.ts +25 -0
- package/dist/services/workspace-bootstrap-service.js +140 -0
- package/dist/services/workspace-bootstrap-service.js.map +1 -0
- package/dist/state/store.d.ts +16 -0
- package/dist/state/store.js +93 -18
- package/dist/state/store.js.map +1 -1
- package/dist/tests/clawhub.test.d.ts +1 -0
- package/dist/tests/clawhub.test.js +63 -0
- package/dist/tests/clawhub.test.js.map +1 -0
- package/dist/tests/cli-utils.test.d.ts +1 -0
- package/dist/tests/cli-utils.test.js +15 -0
- package/dist/tests/cli-utils.test.js.map +1 -0
- package/dist/tests/config-coordinator.test.d.ts +1 -0
- package/dist/tests/config-coordinator.test.js +172 -0
- package/dist/tests/config-coordinator.test.js.map +1 -0
- package/dist/tests/config-integration.test.d.ts +1 -0
- package/dist/tests/config-integration.test.js +238 -0
- package/dist/tests/config-integration.test.js.map +1 -0
- package/dist/tests/config-ui-utils.test.d.ts +1 -0
- package/dist/tests/config-ui-utils.test.js +389 -0
- package/dist/tests/config-ui-utils.test.js.map +1 -0
- package/dist/tests/find-and-naming-utils.test.d.ts +1 -0
- package/dist/tests/find-and-naming-utils.test.js +127 -0
- package/dist/tests/find-and-naming-utils.test.js.map +1 -0
- package/dist/tests/skill-flow.test.js +334 -881
- package/dist/tests/skill-flow.test.js.map +1 -1
- package/dist/tests/source-lifecycle.test.d.ts +1 -0
- package/dist/tests/source-lifecycle.test.js +605 -0
- package/dist/tests/source-lifecycle.test.js.map +1 -0
- package/dist/tests/target-definitions.test.d.ts +1 -0
- package/dist/tests/target-definitions.test.js +51 -0
- package/dist/tests/target-definitions.test.js.map +1 -0
- package/dist/tests/test-helpers.d.ts +18 -0
- package/dist/tests/test-helpers.js +123 -0
- package/dist/tests/test-helpers.js.map +1 -0
- package/dist/tui/config-app.d.ts +147 -24
- package/dist/tui/config-app.js +1081 -335
- package/dist/tui/config-app.js.map +1 -1
- package/dist/tui/find-app.d.ts +1 -1
- package/dist/tui/find-app.js +36 -4
- package/dist/tui/find-app.js.map +1 -1
- package/dist/utils/clawhub.d.ts +3 -0
- package/dist/utils/clawhub.js +32 -3
- package/dist/utils/clawhub.js.map +1 -1
- package/dist/utils/cli.d.ts +1 -0
- package/dist/utils/cli.js +15 -0
- package/dist/utils/cli.js.map +1 -0
- package/dist/utils/constants.d.ts +4 -0
- package/dist/utils/constants.js +31 -0
- package/dist/utils/constants.js.map +1 -1
- package/dist/utils/fs.d.ts +5 -0
- package/dist/utils/fs.js +52 -1
- package/dist/utils/fs.js.map +1 -1
- package/dist/utils/naming.d.ts +1 -0
- package/dist/utils/naming.js +7 -1
- package/dist/utils/naming.js.map +1 -1
- package/dist/utils/source-id.js +4 -0
- package/dist/utils/source-id.js.map +1 -1
- package/package.json +1 -1
package/dist/tui/config-app.js
CHANGED
|
@@ -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
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
return
|
|
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 (
|
|
42
|
-
|
|
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
|
|
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
|
|
51
|
-
const
|
|
52
|
-
|
|
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
|
|
76
|
-
|
|
77
|
-
|
|
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 === "
|
|
80
|
-
return
|
|
110
|
+
if (updateState.phase === "failed") {
|
|
111
|
+
return { kind: "update-failed", label: "Update Failed", color: "red" };
|
|
81
112
|
}
|
|
82
|
-
if (phase === "
|
|
83
|
-
return "
|
|
113
|
+
if (saveState.phase === "saving") {
|
|
114
|
+
return { kind: "saving", label: "Saving", color: "cyan" };
|
|
84
115
|
}
|
|
85
|
-
if (phase === "
|
|
86
|
-
return
|
|
116
|
+
if (saveState.phase === "failed") {
|
|
117
|
+
return { kind: "failed", label: "Failed", color: "red" };
|
|
87
118
|
}
|
|
88
|
-
|
|
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
|
|
91
|
-
const
|
|
92
|
-
|
|
93
|
-
|
|
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 (
|
|
96
|
-
|
|
136
|
+
if (width >= 100) {
|
|
137
|
+
parts.push(`Changes: ${changeCount}`);
|
|
97
138
|
}
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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
|
-
|
|
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
|
|
168
|
+
return "detail.actions";
|
|
108
169
|
}
|
|
109
|
-
export function
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
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 (
|
|
115
|
-
|
|
116
|
-
|
|
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 (
|
|
119
|
-
|
|
120
|
-
|
|
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 (
|
|
123
|
-
|
|
124
|
-
return parts.join(" · ");
|
|
222
|
+
if (actionCursor > 0) {
|
|
223
|
+
return { focus, agentCursor, skillCursor, actionCursor: actionCursor - 1 };
|
|
125
224
|
}
|
|
126
|
-
if (
|
|
127
|
-
|
|
128
|
-
|
|
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 (
|
|
131
|
-
|
|
132
|
-
|
|
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
|
-
|
|
135
|
-
|
|
136
|
-
|
|
241
|
+
return { focus, agentCursor, skillCursor, actionCursor };
|
|
242
|
+
}
|
|
243
|
+
export function getNextSelectionIndexAfterDelete(currentIndex, nextCount) {
|
|
244
|
+
if (nextCount <= 0) {
|
|
245
|
+
return -1;
|
|
137
246
|
}
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
259
|
-
const
|
|
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
|
|
268
|
-
const
|
|
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 =
|
|
273
|
-
const
|
|
274
|
-
|
|
275
|
-
|
|
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
|
|
280
|
-
const
|
|
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
|
|
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
|
|
310
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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 (!
|
|
681
|
+
if (!activeSummary || !canEditSelected) {
|
|
369
682
|
return;
|
|
370
683
|
}
|
|
371
|
-
const sourceId =
|
|
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
|
|
398
|
-
if (!
|
|
710
|
+
const handleUpdate = () => {
|
|
711
|
+
if (!activeSummary || !selectedGroup || !canRunActions) {
|
|
399
712
|
return;
|
|
400
713
|
}
|
|
401
|
-
const sourceId =
|
|
402
|
-
const
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
}
|
|
411
|
-
|
|
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
|
-
|
|
415
|
-
|
|
416
|
-
loading: false,
|
|
417
|
-
requestId: nextRequestId,
|
|
741
|
+
phase: "updating",
|
|
742
|
+
message: `updating ${selectedGroup.title}...`,
|
|
418
743
|
},
|
|
419
744
|
}));
|
|
420
|
-
|
|
421
|
-
|
|
745
|
+
void app.updateSources(sourceIds).then(async (result) => {
|
|
746
|
+
if ((updateRequestIds.current[sourceId] ?? 0) !== requestId) {
|
|
747
|
+
return;
|
|
748
|
+
}
|
|
422
749
|
if (!result.ok) {
|
|
423
|
-
|
|
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
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
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:
|
|
448
|
-
? `saved ${appliedChangeCount} changes`
|
|
449
|
-
: "saved with no changes",
|
|
790
|
+
message: "saved",
|
|
450
791
|
},
|
|
451
792
|
}));
|
|
452
|
-
|
|
453
|
-
|
|
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
|
-
|
|
456
|
-
|
|
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 (!
|
|
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
|
|
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
|
-
|
|
490
|
-
|
|
491
|
-
|
|
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
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
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
|
|
927
|
+
if (key.leftArrow && focus !== "groups") {
|
|
928
|
+
setFocus("groups");
|
|
929
|
+
return;
|
|
930
|
+
}
|
|
931
|
+
if (focus === "groups") {
|
|
519
932
|
if (key.downArrow) {
|
|
520
|
-
|
|
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
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
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
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
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 (
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
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 (
|
|
595
|
-
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { bold: true, children: "No skills groups yet" }), _jsx(Text, { children: "
|
|
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
|
|
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 -
|
|
601
|
-
const [groupsWidth,
|
|
1046
|
+
const paneHeight = Math.max(12, terminalRows - 3);
|
|
1047
|
+
const [groupsWidth, detailWidth] = getPaneWidths(terminalColumns);
|
|
602
1048
|
const bodyRowCount = getPaneViewportCount(paneHeight);
|
|
603
|
-
const
|
|
604
|
-
|
|
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")}
|
|
609
|
-
active: focus === "
|
|
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 === "
|
|
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
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
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: "
|
|
638
|
-
text:
|
|
639
|
-
active:
|
|
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
|
-
...
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
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
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
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
|
-
|
|
675
|
-
|
|
676
|
-
|
|
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
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
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
|
|
688
|
-
if (
|
|
689
|
-
return "
|
|
1308
|
+
function buildPreviewLabel({ blockedCount, changeCount, errorMessage, loading, }) {
|
|
1309
|
+
if (loading) {
|
|
1310
|
+
return "planning...";
|
|
690
1311
|
}
|
|
691
|
-
if (
|
|
692
|
-
return "
|
|
1312
|
+
if (errorMessage) {
|
|
1313
|
+
return "failed";
|
|
1314
|
+
}
|
|
1315
|
+
if (blockedCount > 0 && changeCount > 0) {
|
|
1316
|
+
return `${changeCount} changes, ${blockedCount} blocked`;
|
|
693
1317
|
}
|
|
694
|
-
if (
|
|
695
|
-
return
|
|
1318
|
+
if (blockedCount > 0) {
|
|
1319
|
+
return `${blockedCount} blocked`;
|
|
696
1320
|
}
|
|
697
|
-
return
|
|
1321
|
+
return `${changeCount} changes`;
|
|
698
1322
|
}
|
|
699
|
-
function
|
|
700
|
-
|
|
701
|
-
|
|
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
|
-
|
|
704
|
-
|
|
1337
|
+
for (const failedBootMessage of failedBootMessages) {
|
|
1338
|
+
alerts.push({ level: "error", message: `Boot issue: ${failedBootMessage}` });
|
|
705
1339
|
}
|
|
706
|
-
|
|
707
|
-
|
|
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
|
-
|
|
710
|
-
|
|
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
|
|
1453
|
+
return rows;
|
|
713
1454
|
}
|
|
714
|
-
function
|
|
715
|
-
|
|
716
|
-
|
|
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
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
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
|
-
|
|
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
|
}
|