@tacuchi/agent-workflow-cli 25.3.1 → 25.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/application/cut-intent-ledger.js +164 -0
- package/dist/application/cut-intent-ledger.js.map +1 -0
- package/dist/application/doctor/repair-runner.js +35 -2
- package/dist/application/doctor/repair-runner.js.map +1 -1
- package/dist/application/export-service.js +8 -1
- package/dist/application/export-service.js.map +1 -1
- package/dist/application/history-publications.js +76 -0
- package/dist/application/history-publications.js.map +1 -0
- package/dist/application/lineage-service.js +119 -0
- package/dist/application/lineage-service.js.map +1 -0
- package/dist/application/paths-service.js +8 -0
- package/dist/application/paths-service.js.map +1 -1
- package/dist/application/persist-service.js +9 -3
- package/dist/application/persist-service.js.map +1 -1
- package/dist/application/release-pass-ledger.js +285 -0
- package/dist/application/release-pass-ledger.js.map +1 -0
- package/dist/application/resume-service.js +9 -5
- package/dist/application/resume-service.js.map +1 -1
- package/dist/application/retirement/prepare.js +2 -2
- package/dist/application/retirement/prepare.js.map +1 -1
- package/dist/application/retirement/resolve.js.map +1 -1
- package/dist/application/self/install-plugin-skills-git.js +28 -1
- package/dist/application/self/install-plugin-skills-git.js.map +1 -1
- package/dist/application/self/skills-apply.js +623 -0
- package/dist/application/self/skills-apply.js.map +1 -0
- package/dist/application/self/skills-catalog.js +35 -0
- package/dist/application/self/skills-catalog.js.map +1 -0
- package/dist/application/self/skills-change.js +479 -0
- package/dist/application/self/skills-change.js.map +1 -0
- package/dist/application/self/skills-discovery.js +275 -0
- package/dist/application/self/skills-discovery.js.map +1 -0
- package/dist/application/self/skills-manager.js +69 -499
- package/dist/application/self/skills-manager.js.map +1 -1
- package/dist/application/self/skills-payload.js +566 -0
- package/dist/application/self/skills-payload.js.map +1 -0
- package/dist/application/self/skills-registry.js +31 -11
- package/dist/application/self/skills-registry.js.map +1 -1
- package/dist/application/session-resolver.js +8 -2
- package/dist/application/session-resolver.js.map +1 -1
- package/dist/application/{retirement/graph.js → workline-graph.js} +18 -12
- package/dist/application/workline-graph.js.map +1 -0
- package/dist/application/workline-index-service.js +200 -16
- package/dist/application/workline-index-service.js.map +1 -1
- package/dist/application/workspace-materialization-service.js +40 -4
- package/dist/application/workspace-materialization-service.js.map +1 -1
- package/dist/application/worktree-service.js +276 -6
- package/dist/application/worktree-service.js.map +1 -1
- package/dist/cli/commands/cut-intent.js +176 -0
- package/dist/cli/commands/cut-intent.js.map +1 -0
- package/dist/cli/commands/index.js +10 -0
- package/dist/cli/commands/index.js.map +1 -1
- package/dist/cli/commands/release-pass.js +192 -0
- package/dist/cli/commands/release-pass.js.map +1 -0
- package/dist/cli/commands/resume.js +3 -0
- package/dist/cli/commands/resume.js.map +1 -1
- package/dist/cli/commands/status.js +7 -0
- package/dist/cli/commands/status.js.map +1 -1
- package/dist/cli/commands/worktree.js +19 -5
- package/dist/cli/commands/worktree.js.map +1 -1
- package/dist/cli/help-groups.js +11 -0
- package/dist/cli/help-groups.js.map +1 -1
- package/dist/cli/main.js +27 -3
- package/dist/cli/main.js.map +1 -1
- package/dist/cli/tui/data/recommended-skills.js +363 -9
- package/dist/cli/tui/data/recommended-skills.js.map +1 -1
- package/dist/cli/tui/tabs/skills-tab.js +448 -204
- package/dist/cli/tui/tabs/skills-tab.js.map +1 -1
- package/dist/domain/cut-intent.js +79 -0
- package/dist/domain/cut-intent.js.map +1 -0
- package/dist/domain/doctor/operations.js +3 -1
- package/dist/domain/doctor/operations.js.map +1 -1
- package/dist/domain/flow/authority.js +8 -0
- package/dist/domain/flow/authority.js.map +1 -1
- package/dist/domain/release-pass.js +95 -0
- package/dist/domain/release-pass.js.map +1 -0
- package/dist/runtime/namespace-resolver.js +3 -10
- package/dist/runtime/namespace-resolver.js.map +1 -1
- package/dist/runtime/workline-marker.js +70 -0
- package/dist/runtime/workline-marker.js.map +1 -0
- package/package.json +1 -1
- package/skills/w/roles/README.md +2 -0
- package/dist/application/retirement/graph.js.map +0 -1
|
@@ -1,16 +1,26 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
// [Skills] —
|
|
3
|
-
// (installed → unmanaged → registered → recommended), detail
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
2
|
+
// [Skills] — external-skills manager (skills.sh model): one list with badges
|
|
3
|
+
// (installed → unmanaged → registered → recommended), a detail that states
|
|
4
|
+
// INSTALLATION and RECOMMENDATION as separate facts, and one journey for every
|
|
5
|
+
// change: selection → preview → applying → result
|
|
6
|
+
// (DES-001@r7 / SCR-001@r3).
|
|
7
|
+
//
|
|
8
|
+
// Since Spec 043 there is no shortcut that mutates: every action prepares a
|
|
9
|
+
// proposal, the preview shows the WHOLE set with its destinations, `Back` is
|
|
10
|
+
// where the focus starts, and `Apply` sends that proposal's own digest as the
|
|
11
|
+
// approval. The result is a view — per destination, with its verification
|
|
12
|
+
// separate — and not a toast that scrolls a partial failure away.
|
|
13
|
+
//
|
|
14
|
+
// The list opens PROJECTED to the recommended catalog and `t` toggles to every
|
|
7
15
|
// detected skill (SPEC 019) — a view mode, not a second data source.
|
|
8
|
-
// Backed by skills-manager
|
|
9
|
-
// [Workline]
|
|
16
|
+
// Backed by skills-manager (reads), skills-change (prepare) and skills-apply
|
|
17
|
+
// (apply/recover); the `w` bundle administration lives in [Workline].
|
|
10
18
|
import { Box, Text, useInput, useStdout } from "ink";
|
|
11
19
|
import { useCallback, useMemo, useRef, useState } from "react";
|
|
12
20
|
import { formatTuiEvent } from "../../../application/logging/log-events.js";
|
|
13
|
-
import {
|
|
21
|
+
import { applySkillChange, readSkillJournal, recoverSkillJournal, } from "../../../application/self/skills-apply.js";
|
|
22
|
+
import { prepareSkillChange, } from "../../../application/self/skills-change.js";
|
|
23
|
+
import { REPLICA_HOST_KEYS, REPLICA_HOST_LABELS, canonicalSkillsRoot, listSkills, resolveSkillSource, } from "../../../application/self/skills-manager.js";
|
|
14
24
|
import { ConfirmBanner } from "../components/confirm-banner.js";
|
|
15
25
|
import { DetailPanel } from "../components/detail-panel.js";
|
|
16
26
|
import { InputPrompt } from "../components/input-prompt.js";
|
|
@@ -19,7 +29,7 @@ import { notificationStackRows } from "../components/notification-stack.js";
|
|
|
19
29
|
import { PageHead } from "../components/page-head.js";
|
|
20
30
|
import { QuickActions } from "../components/quick-actions.js";
|
|
21
31
|
import { SectionHead } from "../components/section-head.js";
|
|
22
|
-
import { RECOMMENDED_SKILLS } from "../data/recommended-skills.js";
|
|
32
|
+
import { RECOMMENDED_SKILLS, SKILL_CATALOG } from "../data/recommended-skills.js";
|
|
23
33
|
import { useLockWhile } from "../input-lock.js";
|
|
24
34
|
import { useNotificationItems } from "../notification-center.js";
|
|
25
35
|
import { rowWidth } from "../row-width.js";
|
|
@@ -27,8 +37,6 @@ import { colors, icons } from "../theme.js";
|
|
|
27
37
|
import { useListDetailKeys } from "../use-list-detail-keys.js";
|
|
28
38
|
import { useListWindow, windowRangeHint } from "../use-list-window.js";
|
|
29
39
|
import { useOnMount } from "../use-on-mount.js";
|
|
30
|
-
// Derived from the engine's own replica list — the four places that used to
|
|
31
|
-
// spell "Claude, Gemini" by hand went stale the moment a replica host changed.
|
|
32
40
|
const REPLICA_LABELS = REPLICA_HOST_LABELS.join(", ");
|
|
33
41
|
// Rows the chrome consumes around the skills list; the window (useListWindow)
|
|
34
42
|
// gets what remains of the viewport:
|
|
@@ -38,10 +46,28 @@ const REPLICA_LABELS = REPLICA_HOST_LABELS.join(", ");
|
|
|
38
46
|
// this tab 6 = PageHead 2 (row + marginBottom) · SectionHead 1 ·
|
|
39
47
|
// QuickActions 3 (marginTop + rule + keys)
|
|
40
48
|
const SKILLS_LIST_RESERVED_ROWS = 22;
|
|
41
|
-
// The `filtered` mode's projection: the
|
|
42
|
-
//
|
|
49
|
+
// The `filtered` mode's projection: the names of the HABITUAL set, whatever
|
|
50
|
+
// their status — a withdrawn entry leaves it and stays reachable under `all`.
|
|
43
51
|
const RECOMMENDED_NAMES = new Set(RECOMMENDED_SKILLS.map((s) => s.name));
|
|
44
|
-
|
|
52
|
+
// What `Recommendation` reads for each verdict, and which ones earn a badge in
|
|
53
|
+
// the list: `keep`/`conditional` are the ordinary cases and stay quiet, while
|
|
54
|
+
// the three that ask for a decision are visible without opening the detail.
|
|
55
|
+
const DISPOSITION_LABEL = {
|
|
56
|
+
keep: "keep",
|
|
57
|
+
conditional: "conditional",
|
|
58
|
+
repair: "repair/replace",
|
|
59
|
+
candidate: "candidate",
|
|
60
|
+
withdrawn: "withdrawn",
|
|
61
|
+
};
|
|
62
|
+
const BADGED_DISPOSITIONS = {
|
|
63
|
+
repair: "warn",
|
|
64
|
+
candidate: "info",
|
|
65
|
+
withdrawn: "warn",
|
|
66
|
+
};
|
|
67
|
+
// Said, never inferred: a field the review did not record is unknown, and
|
|
68
|
+
// unknown is not "compatible".
|
|
69
|
+
const UNKNOWN = "unknown";
|
|
70
|
+
/** Rows visible in the current mode: the whole list, or just the habitual set's. */
|
|
45
71
|
function projectSkills(all, showAll) {
|
|
46
72
|
return showAll ? all : all.filter((s) => RECOMMENDED_NAMES.has(s.name));
|
|
47
73
|
}
|
|
@@ -51,30 +77,26 @@ const STATUS_GLYPH = {
|
|
|
51
77
|
registered: { glyph: "◇", active: false, tone: "dim" },
|
|
52
78
|
recommended: { glyph: "·", active: false, tone: "info" },
|
|
53
79
|
};
|
|
80
|
+
/** The colour each per-destination status reads in: `applied` is the only one
|
|
81
|
+
* that means "done", and `failed` must never look like it. */
|
|
82
|
+
const RESULT_COLOR = {
|
|
83
|
+
applied: colors.ok,
|
|
84
|
+
pending: colors.warn,
|
|
85
|
+
failed: colors.err,
|
|
86
|
+
unchanged: colors.dim,
|
|
87
|
+
restored: colors.accent,
|
|
88
|
+
};
|
|
54
89
|
export function SkillsTab({ ctx, isActive, onToast }) {
|
|
55
90
|
const [items, setItems] = useState([]);
|
|
56
91
|
const [mode, setMode] = useState({ kind: "list" });
|
|
57
|
-
// View mode, local and unpersisted: every mount opens on the
|
|
92
|
+
// View mode, local and unpersisted: every mount opens on the catalog (SPEC 019).
|
|
58
93
|
const [showAll, setShowAll] = useState(false);
|
|
59
|
-
// Mirror of the VISIBLE rows to preserve the selection BY NAME when they
|
|
60
|
-
// change — a refresh re-orders the list (installed→registered→recommended)
|
|
61
|
-
// and the toggle re-projects it, so a numeric cursor would land on another
|
|
62
|
-
// skill. Assigned during render, so a callback reads the rows the user is
|
|
63
|
-
// actually looking at.
|
|
64
94
|
const visibleRef = useRef([]);
|
|
65
95
|
const { stdout } = useStdout();
|
|
66
|
-
// Rows the active mode shows. `items` keeps the FULL list so the PageHead
|
|
67
|
-
// totals stay global — only the section's rows follow the mode.
|
|
68
96
|
const visible = useMemo(() => projectSkills(items, showAll), [items, showAll]);
|
|
69
97
|
visibleRef.current = visible;
|
|
70
98
|
useLockWhile(mode.kind !== "list" && mode.kind !== "detail");
|
|
71
|
-
// `detailActions` needs `current` (→ the hook's cursor), so its length
|
|
72
|
-
// reaches the hook one render late via this ref. Safe: the list cannot
|
|
73
|
-
// change while the detail is open, so the value is always fresh by the
|
|
74
|
-
// time the detail phase reads it.
|
|
75
99
|
const actionsLenRef = useRef(0);
|
|
76
|
-
// Shared list/detail/confirm keys (↑↓ · ⏎ · esc · a add · y/n). The
|
|
77
|
-
// callbacks close over consts declared below — they only run on keystrokes.
|
|
78
100
|
const { cursor, setCursor, actionCursor } = useListDetailKeys({
|
|
79
101
|
isActive,
|
|
80
102
|
phase: mode.kind === "list" || mode.kind === "detail"
|
|
@@ -84,7 +106,10 @@ export function SkillsTab({ ctx, isActive, onToast }) {
|
|
|
84
106
|
: "off",
|
|
85
107
|
listLen: visible.length,
|
|
86
108
|
actionsLen: actionsLenRef.current,
|
|
87
|
-
onAdd: () => setMode({
|
|
109
|
+
onAdd: () => setMode({
|
|
110
|
+
kind: "wizard-source",
|
|
111
|
+
intent: { operation: "install", source: "", withdraw: [] },
|
|
112
|
+
}),
|
|
88
113
|
onOpenDetail: () => setMode({ kind: "detail" }),
|
|
89
114
|
onCloseDetail: () => setMode({ kind: "list" }),
|
|
90
115
|
onRunAction: (i) => {
|
|
@@ -97,26 +122,11 @@ export function SkillsTab({ ctx, isActive, onToast }) {
|
|
|
97
122
|
return;
|
|
98
123
|
if (!yes)
|
|
99
124
|
return setMode({ kind: "detail" });
|
|
100
|
-
|
|
101
|
-
if (mode.action === "uninstall") {
|
|
102
|
-
void runAction(`uninstalling ${name}…`, `Uninstalled · ${name}`, () => uninstallSkill(ctx, name));
|
|
103
|
-
}
|
|
104
|
-
else {
|
|
105
|
-
void runAction(`removing ${name}…`, `Removed · ${name}`, () => removeSkill(ctx, name));
|
|
106
|
-
}
|
|
125
|
+
void prepare({ operation: mode.action, name: current.name }, `preparing ${mode.action} of ${current.name}…`);
|
|
107
126
|
},
|
|
108
127
|
});
|
|
109
|
-
// Windowed slice of the skills list (shared hook): renders only the rows
|
|
110
|
-
// that fit the viewport, following the cursor at the edges. Non-TTY
|
|
111
|
-
// (unknown height) → the whole list renders, as before. The window derives
|
|
112
|
-
// from the cursor only — the by-name selection across refresh is untouched.
|
|
113
|
-
// The NotificationStack height joins the reservation so a visible banner
|
|
114
|
-
// can't clip the active row.
|
|
115
128
|
const notifItems = useNotificationItems();
|
|
116
129
|
const listWindow = useListWindow(visible.length, cursor, SKILLS_LIST_RESERVED_ROWS + notificationStackRows(notifItems));
|
|
117
|
-
// Re-anchors the cursor when the visible rows change (refresh or mode
|
|
118
|
-
// toggle): the same skill stays selected if it survives, otherwise the
|
|
119
|
-
// cursor clamps to the new list.
|
|
120
130
|
const reanchorCursor = useCallback((next) => {
|
|
121
131
|
setCursor((c) => {
|
|
122
132
|
const prevName = visibleRef.current[c]?.name;
|
|
@@ -126,7 +136,7 @@ export function SkillsTab({ ctx, isActive, onToast }) {
|
|
|
126
136
|
}, [setCursor]);
|
|
127
137
|
const refresh = useCallback(async () => {
|
|
128
138
|
try {
|
|
129
|
-
const next = await listSkills(ctx,
|
|
139
|
+
const next = await listSkills(ctx, SKILL_CATALOG);
|
|
130
140
|
reanchorCursor(projectSkills(next, showAll));
|
|
131
141
|
setItems(next);
|
|
132
142
|
}
|
|
@@ -134,72 +144,183 @@ export function SkillsTab({ ctx, isActive, onToast }) {
|
|
|
134
144
|
onToast?.({ tone: "err", title: "Error loading skills", body: err.message });
|
|
135
145
|
}
|
|
136
146
|
}, [ctx, onToast, reanchorCursor, showAll]);
|
|
137
|
-
|
|
147
|
+
// On mount the tab also asks whether a previous run left an operation
|
|
148
|
+
// half-applied: finding it is what makes the journal worth writing.
|
|
149
|
+
useOnMount(() => {
|
|
150
|
+
void (async () => {
|
|
151
|
+
await refresh();
|
|
152
|
+
const journal = await readSkillJournal(ctx);
|
|
153
|
+
if (journal !== null)
|
|
154
|
+
setMode({ kind: "recovery", journal, cursor: 0 });
|
|
155
|
+
})();
|
|
156
|
+
});
|
|
138
157
|
const current = visible[cursor] ?? null;
|
|
139
158
|
const installedCount = items.filter((s) => s.status === "installed").length;
|
|
140
159
|
const unmanagedCount = items.filter((s) => s.status === "unmanaged").length;
|
|
141
160
|
const registeredCount = items.filter((s) => s.status === "registered").length;
|
|
142
161
|
const recommendedCount = items.filter((s) => s.status === "recommended").length;
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
162
|
+
/**
|
|
163
|
+
* Prepares a change and shows its preview. Nothing is applied here: this is
|
|
164
|
+
* the only door to a mutation, and it opens onto a preview.
|
|
165
|
+
*/
|
|
166
|
+
const prepare = useCallback(async (request, label, intent) => {
|
|
167
|
+
setMode({ kind: "busy", label });
|
|
168
|
+
try {
|
|
169
|
+
const outcome = await prepareSkillChange(ctx, request);
|
|
170
|
+
if (outcome.status === "rejected") {
|
|
171
|
+
onToast?.({
|
|
172
|
+
tone: "err",
|
|
173
|
+
title: "Change refused",
|
|
174
|
+
body: `${outcome.rejection.code}: ${outcome.rejection.message}`,
|
|
175
|
+
});
|
|
176
|
+
setMode({ kind: "list" });
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
if (outcome.status === "needs-choice") {
|
|
180
|
+
setMode({
|
|
181
|
+
kind: "selection",
|
|
182
|
+
intent: intent ?? {
|
|
183
|
+
operation: "install",
|
|
184
|
+
source: request.source ?? "",
|
|
185
|
+
withdraw: request.withdraw ?? [],
|
|
186
|
+
},
|
|
187
|
+
inventory: outcome.inventory,
|
|
188
|
+
candidates: outcome.candidates,
|
|
189
|
+
chosen: [],
|
|
190
|
+
cursor: 0,
|
|
191
|
+
note: null,
|
|
192
|
+
});
|
|
193
|
+
await outcome.release();
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
if (outcome.status === "needs-expansion") {
|
|
197
|
+
await outcome.release();
|
|
198
|
+
setMode(expansionSelection(request, outcome.expansions, intent));
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
setMode({
|
|
202
|
+
kind: "preview",
|
|
203
|
+
prepared: { proposal: outcome.proposal, release: outcome.release },
|
|
204
|
+
// `Back` is where the focus starts: Apply is an explicit move.
|
|
205
|
+
cursor: 0,
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
catch (err) {
|
|
209
|
+
onToast?.({ tone: "err", title: "Error", body: err.message });
|
|
210
|
+
setMode({ kind: "list" });
|
|
211
|
+
}
|
|
212
|
+
}, [ctx, onToast]);
|
|
213
|
+
/** Re-opens the selection with the expansion the payload requires. */
|
|
214
|
+
const expansionSelection = useCallback((request, expansions, intent) => {
|
|
215
|
+
const chosen = [...(request.paths ?? [])];
|
|
216
|
+
const candidates = expansions.map((expansion) => ({
|
|
217
|
+
path: expansion.path,
|
|
218
|
+
name: expansion.name,
|
|
219
|
+
directory: expansion.path.split("/").pop() ?? expansion.name,
|
|
220
|
+
}));
|
|
221
|
+
return {
|
|
222
|
+
kind: "selection",
|
|
223
|
+
intent: intent ?? {
|
|
224
|
+
operation: request.operation,
|
|
225
|
+
source: request.source ?? "",
|
|
226
|
+
withdraw: request.withdraw ?? [],
|
|
227
|
+
},
|
|
228
|
+
inventory: {
|
|
229
|
+
source: request.source ?? "",
|
|
230
|
+
kind: "git",
|
|
231
|
+
requestedRef: null,
|
|
232
|
+
resolvedRef: null,
|
|
233
|
+
candidates,
|
|
234
|
+
truncated: false,
|
|
235
|
+
limits: null,
|
|
236
|
+
},
|
|
237
|
+
candidates,
|
|
238
|
+
chosen,
|
|
239
|
+
cursor: 0,
|
|
240
|
+
note: expansions.map((expansion) => expansion.reason).join(" · "),
|
|
241
|
+
};
|
|
242
|
+
}, []);
|
|
243
|
+
const applyPrepared = useCallback(async (prepared) => {
|
|
244
|
+
setMode({ kind: "applying", label: `applying ${prepared.proposal.operation}…` });
|
|
245
|
+
try {
|
|
246
|
+
const outcome = await applySkillChange(ctx, prepared.proposal, prepared.proposal.digest);
|
|
247
|
+
if (outcome.status === "refused") {
|
|
248
|
+
onToast?.({
|
|
249
|
+
tone: "err",
|
|
250
|
+
title: "Apply refused",
|
|
251
|
+
body: `${outcome.refusal.code}: ${outcome.refusal.message}`,
|
|
252
|
+
});
|
|
253
|
+
setMode({ kind: "list" });
|
|
254
|
+
}
|
|
255
|
+
else {
|
|
256
|
+
void ctx.logger?.info(formatTuiEvent(`skills ${prepared.proposal.operation}`, "ok"));
|
|
257
|
+
setMode({ kind: "result", result: outcome.result });
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
catch (err) {
|
|
261
|
+
onToast?.({ tone: "err", title: "Error", body: err.message });
|
|
262
|
+
setMode({ kind: "list" });
|
|
263
|
+
}
|
|
264
|
+
finally {
|
|
265
|
+
await prepared.release();
|
|
266
|
+
await refresh();
|
|
267
|
+
}
|
|
268
|
+
}, [ctx, onToast, refresh]);
|
|
269
|
+
// Detail actions per status. `unmanaged` (outside the registry) is not
|
|
270
|
+
// operable: the ownership guard rejects it, so the row is informational.
|
|
149
271
|
const detailActions = useMemo(() => {
|
|
150
272
|
if (!current || current.status === "unmanaged")
|
|
151
273
|
return [];
|
|
152
|
-
if (current.status === "recommended") {
|
|
153
|
-
|
|
274
|
+
if (current.status === "recommended" || current.status === "registered") {
|
|
275
|
+
const entries = [
|
|
154
276
|
{
|
|
155
277
|
id: "install",
|
|
156
278
|
action: {
|
|
157
279
|
name: "Install",
|
|
158
|
-
description: `
|
|
280
|
+
description: `Prepare, preview, then materialize (canonical + ${REPLICA_LABELS}).`,
|
|
159
281
|
},
|
|
160
282
|
},
|
|
161
283
|
];
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
return [
|
|
165
|
-
{
|
|
166
|
-
id: "install",
|
|
167
|
-
action: {
|
|
168
|
-
name: "Install",
|
|
169
|
-
description: `Materialize canonical + host replicas (${REPLICA_LABELS}).`,
|
|
170
|
-
},
|
|
171
|
-
},
|
|
172
|
-
{
|
|
284
|
+
if (current.status === "registered") {
|
|
285
|
+
entries.push({
|
|
173
286
|
id: "remove",
|
|
174
287
|
action: { name: "Remove", description: "Drop from the registry.", danger: true },
|
|
175
|
-
}
|
|
176
|
-
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
return entries;
|
|
177
291
|
}
|
|
178
|
-
// The engine's canonical classifier (isAbsolute covers Windows paths like
|
|
179
|
-
// C:\… that a startsWith("/") would misclassify as git).
|
|
180
292
|
const resolved = resolveSkillSource(current.source, current.ref);
|
|
181
293
|
const gitSource = !("error" in resolved) && resolved.kind === "git";
|
|
294
|
+
const proposed = current.curation?.proposedSource;
|
|
182
295
|
return [
|
|
183
296
|
...(gitSource
|
|
184
297
|
? [
|
|
185
298
|
{
|
|
186
299
|
id: "update",
|
|
300
|
+
action: { name: "Update", description: "Re-fetch the registered ref." },
|
|
301
|
+
},
|
|
302
|
+
]
|
|
303
|
+
: []),
|
|
304
|
+
...(proposed !== undefined
|
|
305
|
+
? [
|
|
306
|
+
{
|
|
307
|
+
id: "replace",
|
|
187
308
|
action: {
|
|
188
|
-
name: "
|
|
189
|
-
description:
|
|
309
|
+
name: "Change source",
|
|
310
|
+
description: `Prepare from ${proposed} and show the differences.`,
|
|
190
311
|
},
|
|
191
312
|
},
|
|
192
313
|
]
|
|
193
314
|
: []),
|
|
194
315
|
{
|
|
195
|
-
id: "
|
|
196
|
-
action: { name: "
|
|
316
|
+
id: "repair",
|
|
317
|
+
action: { name: "Repair", description: "Rebuild the host replicas from the canonical." },
|
|
197
318
|
},
|
|
198
319
|
{
|
|
199
320
|
id: "uninstall",
|
|
200
321
|
action: {
|
|
201
322
|
name: "Uninstall",
|
|
202
|
-
description: "Delete canonical +
|
|
323
|
+
description: "Delete canonical + replicas; keeps the registration.",
|
|
203
324
|
danger: true,
|
|
204
325
|
},
|
|
205
326
|
},
|
|
@@ -214,68 +335,21 @@ export function SkillsTab({ ctx, isActive, onToast }) {
|
|
|
214
335
|
];
|
|
215
336
|
}, [current]);
|
|
216
337
|
actionsLenRef.current = detailActions.length;
|
|
217
|
-
// TUI surface is EN (SPEC 007); the engine's summaries (ES) go in the body.
|
|
218
|
-
const runAction = useCallback(async (label, successTitle, op) => {
|
|
219
|
-
setMode({ kind: "busy", label });
|
|
220
|
-
try {
|
|
221
|
-
const result = await op();
|
|
222
|
-
if (result.ok) {
|
|
223
|
-
onToast?.({ tone: "ok", title: successTitle, body: result.data?.summary ?? "" });
|
|
224
|
-
if (result.data?.warning) {
|
|
225
|
-
onToast?.({ tone: "info", title: "Notice", body: result.data.warning });
|
|
226
|
-
}
|
|
227
|
-
void ctx.logger?.info(formatTuiEvent(`skill-manager ${label}`, "ok"));
|
|
228
|
-
}
|
|
229
|
-
else {
|
|
230
|
-
onToast?.({
|
|
231
|
-
tone: "err",
|
|
232
|
-
title: "Operation refused",
|
|
233
|
-
body: result.error?.message ?? "",
|
|
234
|
-
});
|
|
235
|
-
}
|
|
236
|
-
}
|
|
237
|
-
catch (err) {
|
|
238
|
-
onToast?.({ tone: "err", title: "Error", body: err.message });
|
|
239
|
-
}
|
|
240
|
-
await refresh();
|
|
241
|
-
setMode({ kind: "list" });
|
|
242
|
-
}, [ctx, onToast, refresh]);
|
|
243
338
|
const triggerAction = useCallback((id) => {
|
|
244
339
|
if (!current)
|
|
245
340
|
return;
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
if (current.status === "recommended") {
|
|
250
|
-
// Register + install in one step (the seed already carries the source).
|
|
251
|
-
void runAction(`installing ${name}…`, `Installed · ${name}`, async () => {
|
|
252
|
-
const reg = await registerSkill(ctx, { source: current.source, pick: name });
|
|
253
|
-
if (!reg.ok)
|
|
254
|
-
return reg;
|
|
255
|
-
return installSkill(ctx, name);
|
|
256
|
-
});
|
|
257
|
-
}
|
|
258
|
-
else {
|
|
259
|
-
void runAction(`installing ${name}…`, `Installed · ${name}`, () => installSkill(ctx, name));
|
|
260
|
-
}
|
|
261
|
-
return;
|
|
262
|
-
case "update":
|
|
263
|
-
void runAction(`updating ${name}…`, `Updated · ${name}`, () => updateSkill(ctx, name));
|
|
264
|
-
return;
|
|
265
|
-
case "reinstall":
|
|
266
|
-
void runAction(`reinstalling ${name}…`, `Reinstalled · ${name}`, () => reinstallSkill(ctx, name));
|
|
267
|
-
return;
|
|
268
|
-
case "uninstall":
|
|
269
|
-
setMode({ kind: "confirm", action: "uninstall" });
|
|
270
|
-
return;
|
|
271
|
-
case "remove":
|
|
272
|
-
setMode({ kind: "confirm", action: "remove" });
|
|
273
|
-
return;
|
|
341
|
+
if (id === "uninstall" || id === "remove") {
|
|
342
|
+
setMode({ kind: "confirm", action: id });
|
|
343
|
+
return;
|
|
274
344
|
}
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
345
|
+
const request = acquiringRequest(id, current);
|
|
346
|
+
if (request === null)
|
|
347
|
+
return;
|
|
348
|
+
void prepare(request, `preparing ${id} of ${current.name}…`, request.operation === "replace" && request.source !== undefined
|
|
349
|
+
? { operation: "replace", source: request.source, withdraw: [current.name] }
|
|
350
|
+
: undefined);
|
|
351
|
+
}, [current, prepare]);
|
|
352
|
+
// input — `t` toggles the list mode (catalog only ↔ every detected skill).
|
|
279
353
|
useInput((input) => {
|
|
280
354
|
if (!isActive || (mode.kind !== "list" && mode.kind !== "detail"))
|
|
281
355
|
return;
|
|
@@ -284,88 +358,112 @@ export function SkillsTab({ ctx, isActive, onToast }) {
|
|
|
284
358
|
reanchorCursor(projectSkills(items, !showAll));
|
|
285
359
|
setShowAll((v) => !v);
|
|
286
360
|
}, { isActive });
|
|
287
|
-
// input —
|
|
361
|
+
// input — the source prompt
|
|
288
362
|
useInput((_input, key) => {
|
|
289
363
|
if (!isActive || mode.kind !== "wizard-source")
|
|
290
364
|
return;
|
|
291
365
|
if (key.escape)
|
|
292
366
|
setMode({ kind: "list" });
|
|
293
367
|
}, { isActive });
|
|
294
|
-
// input —
|
|
295
|
-
useInput((
|
|
296
|
-
if (!isActive || mode.kind !== "
|
|
297
|
-
return;
|
|
298
|
-
if (key.escape) {
|
|
299
|
-
setMode({ kind: "list" });
|
|
368
|
+
// input — selection (↑↓ · space toggles · ⏎ continue · esc cancel)
|
|
369
|
+
useInput((input, key) => {
|
|
370
|
+
if (!isActive || mode.kind !== "selection")
|
|
300
371
|
return;
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
372
|
+
if (key.return) {
|
|
373
|
+
// An empty set keeps the selection with its explanation: continuing
|
|
374
|
+
// with nothing chosen would prepare a proposal about nothing.
|
|
375
|
+
if (mode.chosen.length === 0) {
|
|
376
|
+
return void setMode({ ...mode, note: "Choose at least one skill with [space]." });
|
|
377
|
+
}
|
|
378
|
+
void prepare({
|
|
379
|
+
operation: mode.intent.operation,
|
|
380
|
+
source: mode.intent.source,
|
|
381
|
+
paths: mode.chosen,
|
|
382
|
+
...(mode.intent.withdraw.length > 0 ? { withdraw: mode.intent.withdraw } : {}),
|
|
383
|
+
}, `preparing ${mode.chosen.length} skill(s)…`, mode.intent);
|
|
304
384
|
return;
|
|
305
385
|
}
|
|
306
|
-
|
|
307
|
-
|
|
386
|
+
const next = selectionAfterKey(mode, input, key);
|
|
387
|
+
if (next !== null)
|
|
388
|
+
setMode(next);
|
|
389
|
+
}, { isActive });
|
|
390
|
+
// input — preview (↑↓ Back/Apply · ⏎ run the focused one · esc back)
|
|
391
|
+
useInput((_input, key) => {
|
|
392
|
+
if (!isActive || mode.kind !== "preview")
|
|
308
393
|
return;
|
|
394
|
+
if (key.escape) {
|
|
395
|
+
void mode.prepared.release();
|
|
396
|
+
return void setMode({ kind: "list" });
|
|
309
397
|
}
|
|
398
|
+
if (key.upArrow)
|
|
399
|
+
return void setMode({ ...mode, cursor: 0 });
|
|
400
|
+
if (key.downArrow)
|
|
401
|
+
return void setMode({ ...mode, cursor: 1 });
|
|
310
402
|
if (key.return) {
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
setMode({ kind: "
|
|
403
|
+
if (mode.cursor === 0) {
|
|
404
|
+
void mode.prepared.release();
|
|
405
|
+
setMode({ kind: "list" });
|
|
406
|
+
return;
|
|
407
|
+
}
|
|
408
|
+
void applyPrepared(mode.prepared);
|
|
314
409
|
}
|
|
315
410
|
}, { isActive });
|
|
316
|
-
// input —
|
|
317
|
-
useInput((
|
|
318
|
-
if (!isActive || mode.kind !== "
|
|
411
|
+
// input — result: any of the two obvious keys returns to the list
|
|
412
|
+
useInput((_input, key) => {
|
|
413
|
+
if (!isActive || mode.kind !== "result")
|
|
319
414
|
return;
|
|
320
|
-
if (key.escape)
|
|
415
|
+
if (key.escape || key.return)
|
|
321
416
|
setMode({ kind: "list" });
|
|
417
|
+
}, { isActive });
|
|
418
|
+
// input — recovery. A screen with no way out is a trap: `esc` leaves the
|
|
419
|
+
// journal pending and the offer comes back next time the tab opens. What it
|
|
420
|
+
// must never do is let the tab be used as if nothing were unfinished —
|
|
421
|
+
// hence the explicit note on the panel.
|
|
422
|
+
useInput((input, key) => {
|
|
423
|
+
if (!isActive || mode.kind !== "recovery")
|
|
322
424
|
return;
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
if (
|
|
326
|
-
void
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
if (!reg.ok)
|
|
333
|
-
return reg;
|
|
334
|
-
return installSkill(ctx, pick);
|
|
335
|
-
});
|
|
336
|
-
}
|
|
425
|
+
if (key.escape)
|
|
426
|
+
return void setMode({ kind: "list" });
|
|
427
|
+
if (key.upArrow)
|
|
428
|
+
return void setMode({ ...mode, cursor: 0 });
|
|
429
|
+
if (key.downArrow)
|
|
430
|
+
return void setMode({ ...mode, cursor: 1 });
|
|
431
|
+
const choice = recoveryChoice(input, key.return, mode.cursor);
|
|
432
|
+
if (choice !== null)
|
|
433
|
+
void resolveJournal(choice);
|
|
337
434
|
}, { isActive });
|
|
338
|
-
const
|
|
339
|
-
setMode({ kind: "
|
|
435
|
+
const resolveJournal = useCallback(async (choice) => {
|
|
436
|
+
setMode({ kind: "applying", label: `resolving the pending operation (${choice})…` });
|
|
340
437
|
try {
|
|
341
|
-
const
|
|
342
|
-
if (
|
|
343
|
-
onToast?.({
|
|
438
|
+
const outcome = await recoverSkillJournal(ctx, choice);
|
|
439
|
+
if (outcome.status === "refused") {
|
|
440
|
+
onToast?.({
|
|
441
|
+
tone: "err",
|
|
442
|
+
title: "Recovery refused",
|
|
443
|
+
body: `${outcome.refusal.code}: ${outcome.refusal.message}`,
|
|
444
|
+
});
|
|
344
445
|
setMode({ kind: "list" });
|
|
345
|
-
return;
|
|
346
446
|
}
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
? { kind: "wizard-warning", source, pick: single }
|
|
351
|
-
: { kind: "wizard-pick", source, candidates, cursor: 0 });
|
|
447
|
+
else {
|
|
448
|
+
setMode({ kind: "result", result: outcome.result });
|
|
449
|
+
}
|
|
352
450
|
}
|
|
353
451
|
catch (err) {
|
|
354
452
|
onToast?.({ tone: "err", title: "Error", body: err.message });
|
|
355
453
|
setMode({ kind: "list" });
|
|
356
454
|
}
|
|
357
|
-
|
|
455
|
+
finally {
|
|
456
|
+
await refresh();
|
|
457
|
+
}
|
|
458
|
+
}, [ctx, onToast, refresh]);
|
|
358
459
|
const overlayVisible = mode.kind !== "list";
|
|
359
460
|
const home = ctx.env.homeDir();
|
|
360
|
-
// Visible-range indicator for the SectionHead hint slot — only when the
|
|
361
|
-
// window hides rows (consumes no extra terminal row); it keeps priority over
|
|
362
|
-
// the mode hint, which QuickActions announces anyway.
|
|
363
461
|
const listRangeHint = windowRangeHint(listWindow, visible.length);
|
|
364
462
|
const modeHint = showAll ? "all skills · t show recommended" : "recommended only · t show all";
|
|
365
463
|
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(PageHead, { title: "Skills", count: {
|
|
366
464
|
label: `${installedCount} installed${unmanagedCount > 0 ? ` · ${unmanagedCount} unmanaged` : ""} · ${registeredCount} registered · ${recommendedCount} recommended`,
|
|
367
465
|
tone: installedCount > 0 ? "accent" : "warn",
|
|
368
|
-
}, action: _jsx(Text, { color: colors.mute, children: "~/.agents/skills + host replicas" }) }), _jsx(SectionHead, { label: "Skills", count: visible.length, hint: listRangeHint ?? modeHint, ...(mode.kind
|
|
466
|
+
}, action: _jsx(Text, { color: colors.mute, children: "~/.agents/skills + host replicas" }) }), _jsx(SectionHead, { label: "Skills", count: visible.length, hint: listRangeHint ?? modeHint, ...(mode.kind === "wizard-source" || mode.kind === "selection"
|
|
369
467
|
? { rightAction: "esc cancel" }
|
|
370
468
|
: mode.kind === "detail" || mode.kind === "confirm"
|
|
371
469
|
? { rightAction: "esc to close detail" }
|
|
@@ -373,18 +471,18 @@ export function SkillsTab({ ctx, isActive, onToast }) {
|
|
|
373
471
|
const glyph = STATUS_GLYPH[s.status];
|
|
374
472
|
return (_jsx(ListRow, { icon: glyph.glyph, iconActive: glyph.active, title: s.name, subtitle: s.status === "unmanaged" && s.source === ""
|
|
375
473
|
? "outside the registry"
|
|
376
|
-
: `${s.source}${s.ref ? ` #${s.ref}` : ""}`, meta: s
|
|
474
|
+
: `${s.source}${s.ref ? ` #${s.ref}` : ""}`, meta: rowMeta(s), state: { label: s.status, tone: glyph.tone }, chevron: true, active: listWindow.start + i === cursor, dimmed: overlayVisible && mode.kind !== "detail" && mode.kind !== "confirm", widthHint: rowWidth(stdout?.columns, overlayVisible) }, s.name));
|
|
377
475
|
}), mode.kind === "wizard-source" ? (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsx(SectionHead, { label: "Add skill", hint: "Step 1 \u00B7 Source", rightAction: "\u23CE inspect \u00B7 esc cancel" }), _jsx(Box, { marginLeft: 2, children: _jsx(InputPrompt, { message: "source (owner/repo \u00B7 git URL \u00B7 absolute path):", onSubmit: (value) => {
|
|
378
476
|
const source = value.trim();
|
|
379
477
|
if (!source) {
|
|
380
478
|
setMode({ kind: "list" });
|
|
381
479
|
return;
|
|
382
480
|
}
|
|
383
|
-
void
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
481
|
+
void prepare({ operation: "install", source }, "inspecting source…", {
|
|
482
|
+
...mode.intent,
|
|
483
|
+
source,
|
|
484
|
+
});
|
|
485
|
+
}, isActive: isActive }) })] })) : null, mode.kind === "selection" ? (_jsx(SelectionPanel, { mode: mode, columns: stdout?.columns })) : null, mode.kind === "preview" ? (_jsx(PreviewPanel, { proposal: mode.prepared.proposal, cursor: mode.cursor, home: home })) : null, mode.kind === "result" ? _jsx(ResultPanel, { result: mode.result, home: home }) : null, mode.kind === "recovery" ? (_jsx(RecoveryPanel, { journal: mode.journal, cursor: mode.cursor })) : null, mode.kind === "busy" || mode.kind === "applying" ? (_jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: colors.warn, children: [icons.spinner, " ", mode.label] }) })) : null] }), current && (mode.kind === "detail" || mode.kind === "confirm") ? (_jsx(DetailPanel, { bordered: true, header: { name: current.name, meta: detailMeta(current, home) }, statePill: {
|
|
388
486
|
label: current.status,
|
|
389
487
|
tone: current.status === "installed"
|
|
390
488
|
? "ok"
|
|
@@ -392,29 +490,175 @@ export function SkillsTab({ ctx, isActive, onToast }) {
|
|
|
392
490
|
? "warn"
|
|
393
491
|
: "dim",
|
|
394
492
|
}, actions: detailActions.map((a) => a.action), focusedAction: actionCursor, banner: mode.kind === "confirm" ? (_jsx(ConfirmBanner, { title: `× ${mode.action === "uninstall" ? "Uninstall" : "Remove"} ${current.name}?`, body: mode.action === "uninstall"
|
|
395
|
-
? "
|
|
396
|
-
:
|
|
397
|
-
? "Uninstalls and drops the registration. A recommended skill returns to the recommended list."
|
|
398
|
-
: "Drops the registration. A recommended skill returns to the recommended list." })) : null })) : null] }), _jsx(Box, { marginTop: 1, children: _jsx(QuickActions, { actions: [
|
|
493
|
+
? "Prepares the removal of canonical + replicas; the registration stays. You still approve the preview."
|
|
494
|
+
: "Prepares the removal and drops the registration. You still approve the preview." })) : null })) : null] }), _jsx(Box, { marginTop: 1, children: _jsx(QuickActions, { actions: [
|
|
399
495
|
{ key: "a", label: "add skill" },
|
|
400
496
|
{ key: "t", label: showAll ? "show recommended" : "show all" },
|
|
401
497
|
] }) })] }));
|
|
402
498
|
}
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
499
|
+
/** Explicit selection: every eligible skill by name AND location. */
|
|
500
|
+
function SelectionPanel({ mode, columns, }) {
|
|
501
|
+
return (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsx(SectionHead, { label: "Select skills", hint: `${mode.chosen.length} of ${mode.candidates.length} chosen`, rightAction: "space toggle \u00B7 \u23CE continue \u00B7 esc cancel" }), mode.note !== null ? (_jsx(Box, { marginLeft: 2, children: _jsx(Text, { color: colors.warn, children: mode.note }) })) : null, mode.inventory.truncated ? (_jsx(Box, { marginLeft: 2, children: _jsx(Text, { color: colors.warn, children: "The source walk hit its limit: this list is a floor, not every skill." }) })) : null, _jsx(Box, { flexDirection: "column", children: mode.candidates.map((candidate, i) => (_jsx(ListRow, { icon: mode.chosen.includes(candidate.path) ? "◆" : "·", iconActive: mode.chosen.includes(candidate.path), title: candidate.name, subtitle: candidate.path === "" ? "(source root)" : candidate.path, meta: candidate.name === candidate.directory
|
|
502
|
+
? []
|
|
503
|
+
: [{ label: `dir ${candidate.directory}`, tone: "dim" }], state: mode.chosen.includes(candidate.path)
|
|
504
|
+
? { label: "chosen", tone: "ok" }
|
|
505
|
+
: { label: "available", tone: "dim" }, active: mode.cursor === i, widthHint: rowWidth(columns, true) }, candidate.path))) })] }));
|
|
506
|
+
}
|
|
507
|
+
/** The whole change before Apply: set, sources, destinations and effects. */
|
|
508
|
+
function PreviewPanel({ proposal, cursor, home, }) {
|
|
509
|
+
const short = (path) => path.replace(home, "~");
|
|
510
|
+
return (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsx(SectionHead, { label: `Proposed changes · ${proposal.operation}`, hint: `${proposal.additions.length} added · ${proposal.withdrawals.length} withdrawn`, rightAction: "\u2191\u2193 choose \u00B7 \u23CE run \u00B7 esc back" }), _jsxs(Box, { flexDirection: "column", marginLeft: 2, children: [proposal.additions.map((skill) => (_jsxs(Text, { color: colors.text, children: ["+ ", skill.name, _jsxs(Text, { color: colors.dim, children: [" ", "\u00B7 ", skill.provenance.source, skill.path === "" ? "" : `/${skill.path}`, skill.provenance.resolvedRef === null
|
|
511
|
+
? ""
|
|
512
|
+
: ` @ ${skill.provenance.resolvedRef.slice(0, 12)}`, " · ", skill.files.length, " files", skill.manifests.length > 1 ? ` · ${skill.manifests.length} SKILL.md` : ""] })] }, skill.name))), proposal.withdrawals.map((name) => (_jsxs(Text, { color: colors.warn, children: ["\u2212 ", name, " ", _jsx(Text, { color: colors.dim, children: "\u00B7 managed installation retired" })] }, name))), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { color: colors.mute, children: "DESTINATIONS" }), proposal.destinations.map((destination) => (_jsxs(Text, { color: colors.dim, children: [destination.action.padEnd(9), " ", destination.host.padEnd(8), " ", short(destination.location), destination.shared ? " · shared" : "", destination.ownership === "foreign" ? " · foreign, preserved" : ""] }, `${destination.host}-${destination.location}`)))] }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsxs(Text, { color: colors.mute, children: ["EFFECTS \u00B7 ", proposal.effects.join(", ")] }), proposal.notes.map((note) => (_jsxs(Text, { color: colors.faint, children: ["\u00B7 ", note] }, note)))] }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsxs(Text, { color: cursor === 0 ? colors.bright : colors.dim, children: [cursor === 0 ? icons.focusBar : " ", " Back"] }), _jsxs(Text, { color: cursor === 1 ? colors.err : colors.dim, children: [cursor === 1 ? icons.focusBar : " ", " Apply"] })] })] })] }));
|
|
513
|
+
}
|
|
514
|
+
/** Per destination: what happened, and — separately — what was checked. */
|
|
515
|
+
function ResultPanel({ result, home }) {
|
|
516
|
+
return (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsx(SectionHead, { label: `Result · ${result.operation}`, hint: result.summary, rightAction: "\u23CE back to the list" }), _jsxs(Box, { flexDirection: "column", marginLeft: 2, children: [result.destinations.map((destination) => (_jsxs(Text, { color: colors.dim, children: [_jsx(Text, { color: RESULT_COLOR[destination.status], children: destination.status.padEnd(10) }), destination.host.padEnd(8), " ", destination.location.replace(home, "~"), destination.verification === null
|
|
517
|
+
? " · not checked"
|
|
518
|
+
: ` · ${destination.verification.passed ? "verified" : "CHECK FAILED"}: ${destination.verification.checked.replace(home, "~")}`, destination.detail === undefined ? "" : ` · ${destination.detail}`] }, destination.location))), result.recovery !== null ? (_jsx(Box, { marginTop: 1, children: _jsx(Text, { color: colors.warn, children: result.recovery.action }) })) : null, result.cleanup !== null ? (_jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: colors.faint, children: ["Backups pending removal: ", result.cleanup.pending.join(", ")] }) })) : null, _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: colors.faint, children: "Applied is not \"the host loaded it\": what is checked above is the bytes this manager wrote in the locations it manages." }) })] })] }));
|
|
519
|
+
}
|
|
520
|
+
/** An operation a previous run left pending: put it back, or accept and retry. */
|
|
521
|
+
function RecoveryPanel({ journal, cursor }) {
|
|
522
|
+
return (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsx(SectionHead, { label: "Unfinished operation", hint: `${journal.operation} · ${journal.destinations.length} destination(s)`, rightAction: "\u2191\u2193 choose \u00B7 \u23CE run \u00B7 esc later" }), _jsxs(Box, { flexDirection: "column", marginLeft: 2, children: [_jsx(Text, { color: colors.warn, children: "A previous run did not finish. Its backups are kept, so the previous state can come back." }), journal.destinations.map((destination) => (_jsxs(Text, { color: colors.dim, children: [destination.status.padEnd(10), " ", destination.location] }, destination.location))), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsxs(Text, { color: cursor === 0 ? colors.bright : colors.dim, children: [cursor === 0 ? icons.focusBar : " ", " [r] Restore the previous state"] }), _jsxs(Text, { color: cursor === 1 ? colors.bright : colors.dim, children: [cursor === 1 ? icons.focusBar : " ", " [k] Keep the current state and start over"] })] }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: colors.faint, children: "Neither repeats an effect: a new attempt needs a new proposal. `esc` leaves it unfinished \u2014 the backups stay and this offer returns next time." }) })] })] }));
|
|
523
|
+
}
|
|
524
|
+
/** Which way out of a pending operation the keystroke asks for. */
|
|
525
|
+
function recoveryChoice(input, entered, cursor) {
|
|
526
|
+
if (input === "r" || input === "R")
|
|
527
|
+
return "restore";
|
|
528
|
+
if (input === "k" || input === "K")
|
|
529
|
+
return "discard";
|
|
530
|
+
if (!entered)
|
|
531
|
+
return null;
|
|
532
|
+
return cursor === 0 ? "restore" : "discard";
|
|
533
|
+
}
|
|
534
|
+
/** Navigation and toggling inside the selection — the state, not the effect. */
|
|
535
|
+
function selectionAfterKey(mode, input, key) {
|
|
536
|
+
if (key.escape)
|
|
537
|
+
return { kind: "list" };
|
|
538
|
+
if (key.upArrow)
|
|
539
|
+
return { ...mode, cursor: Math.max(0, mode.cursor - 1) };
|
|
540
|
+
if (key.downArrow) {
|
|
541
|
+
return { ...mode, cursor: Math.min(mode.candidates.length - 1, mode.cursor + 1) };
|
|
542
|
+
}
|
|
543
|
+
if (input !== " ")
|
|
544
|
+
return null;
|
|
545
|
+
const path = mode.candidates[mode.cursor]?.path;
|
|
546
|
+
if (path === undefined)
|
|
547
|
+
return null;
|
|
548
|
+
return {
|
|
549
|
+
...mode,
|
|
550
|
+
chosen: mode.chosen.includes(path)
|
|
551
|
+
? mode.chosen.filter((p) => p !== path)
|
|
552
|
+
: [...mode.chosen, path],
|
|
553
|
+
};
|
|
554
|
+
}
|
|
555
|
+
/**
|
|
556
|
+
* The request an acquiring action sends to the preparer.
|
|
557
|
+
*
|
|
558
|
+
* The path the catalog reviewed is what makes a NESTED skill reachable; a name
|
|
559
|
+
* is the fallback, and it only resolves while it is unambiguous.
|
|
560
|
+
*/
|
|
561
|
+
function acquiringRequest(id, item) {
|
|
562
|
+
const curation = item.curation;
|
|
563
|
+
const where = curation?.path !== undefined ? { paths: [curation.path] } : { pick: item.name };
|
|
564
|
+
// The registered ref travels with EVERY acquisition, not just Update: a row
|
|
565
|
+
// registered at `#v2` that installs from the default branch would silently
|
|
566
|
+
// drop the pin — the entry is rewritten from what the acquisition resolved.
|
|
567
|
+
const pinned = item.ref !== undefined ? { ref: item.ref } : {};
|
|
568
|
+
if (id === "repair")
|
|
569
|
+
return { operation: "repair", name: item.name };
|
|
570
|
+
if (id === "install")
|
|
571
|
+
return { operation: "install", source: item.source, ...pinned, ...where };
|
|
572
|
+
if (id === "update") {
|
|
573
|
+
return { operation: "update", source: item.source, ...pinned, ...where };
|
|
574
|
+
}
|
|
575
|
+
// An explicit source change: the previous installation is retired in the
|
|
576
|
+
// SAME proposal that brings the new one.
|
|
577
|
+
const source = curation?.proposedSource;
|
|
578
|
+
if (source === undefined)
|
|
579
|
+
return null;
|
|
580
|
+
return { operation: "replace", source, withdraw: [item.name], ...where };
|
|
581
|
+
}
|
|
582
|
+
/** Badges the row carries: the degraded replica, plus a verdict that asks for
|
|
583
|
+
* a decision (an ordinary keep/conditional row stays quiet). */
|
|
584
|
+
function rowMeta(item) {
|
|
585
|
+
const meta = [];
|
|
586
|
+
if (item.mode === "copy")
|
|
587
|
+
meta.push({ label: "copy", tone: "warn" });
|
|
588
|
+
const disposition = item.curation?.disposition;
|
|
589
|
+
const tone = disposition ? BADGED_DISPOSITIONS[disposition] : undefined;
|
|
590
|
+
if (disposition && tone)
|
|
591
|
+
meta.push({ label: DISPOSITION_LABEL[disposition], tone });
|
|
592
|
+
return meta;
|
|
593
|
+
}
|
|
594
|
+
/** Replica line of an installed/registered/unmanaged row. */
|
|
595
|
+
function replicaLine(item) {
|
|
596
|
+
return [
|
|
412
597
|
`agents ${item.replicas.agents ? "✓" : "·"}`,
|
|
598
|
+
// `item.mode` describes the FIRST replica host's materialization (symlink
|
|
599
|
+
// vs copy) — the suffix stays on that one, and the host names come from
|
|
600
|
+
// the engine's list instead of being spelled here.
|
|
413
601
|
...REPLICA_HOST_KEYS.map((key, i) => `${key} ${item.replicas[key] ? "✓" : "·"}${i === 0 && item.mode === "copy" ? " (copy)" : ""}`),
|
|
414
602
|
].join(" · ");
|
|
603
|
+
}
|
|
604
|
+
/** Short form of an inspected revision — enough to compare, never a claim
|
|
605
|
+
* about what an acquisition would resolve. */
|
|
606
|
+
function shortRef(ref) {
|
|
607
|
+
return ref.length > 12 ? ref.slice(0, 12) : ref;
|
|
608
|
+
}
|
|
609
|
+
/** The reviewed verdict, or the honest absence of one. */
|
|
610
|
+
function recommendationLines(curation) {
|
|
611
|
+
if (curation === undefined) {
|
|
612
|
+
return [
|
|
613
|
+
"Recommendation: not in the reviewed catalog",
|
|
614
|
+
`Use when: ${UNKNOWN}`,
|
|
615
|
+
`Known limits: ${UNKNOWN}`,
|
|
616
|
+
];
|
|
617
|
+
}
|
|
618
|
+
const verdict = DISPOSITION_LABEL[curation.disposition];
|
|
619
|
+
return [
|
|
620
|
+
`Recommendation: ${curation.reason ? `${verdict} — ${curation.reason}` : verdict}`,
|
|
621
|
+
`Use when: ${curation.useWhen ?? UNKNOWN}`,
|
|
622
|
+
`Known limits: ${curation.knownLimits ?? UNKNOWN}`,
|
|
623
|
+
];
|
|
624
|
+
}
|
|
625
|
+
/** Where the bytes come from — and, when they differ, where a repair would
|
|
626
|
+
* take them. A divergence is shown, never resolved silently. */
|
|
627
|
+
function sourceLines(item, curation) {
|
|
628
|
+
const source = `${item.source === "" ? UNKNOWN : item.source}${item.ref ? ` #${item.ref}` : ""}`;
|
|
629
|
+
const proposed = curation?.proposedSource;
|
|
630
|
+
const lines = [proposed ? `Installed source: ${source}` : `Source: ${source}`];
|
|
631
|
+
if (proposed)
|
|
632
|
+
lines.push(`Proposed source: ${proposed}`);
|
|
633
|
+
if (curation?.path)
|
|
634
|
+
lines.push(`Path in source: ${curation.path}`);
|
|
635
|
+
if (curation?.skillName && curation.skillName !== item.name) {
|
|
636
|
+
lines.push(`Catalog name: ${item.name}`, `Skill name: ${curation.skillName}`);
|
|
637
|
+
}
|
|
638
|
+
if (curation?.reviewedRef)
|
|
639
|
+
lines.push(`Reviewed rev: ${shortRef(curation.reviewedRef)}`);
|
|
640
|
+
if (curation?.evidence)
|
|
641
|
+
lines.push(`Evidence: ${curation.evidence}`);
|
|
642
|
+
return lines;
|
|
643
|
+
}
|
|
644
|
+
/**
|
|
645
|
+
* Detail body: installation and recommendation as SEPARATE facts (Spec 043 ·
|
|
646
|
+
* DES-001@r7 / SCR-001@r3#detail). Reading it changes nothing.
|
|
647
|
+
*/
|
|
648
|
+
function detailMeta(item, home) {
|
|
649
|
+
const lines = [];
|
|
650
|
+
if (item.description)
|
|
651
|
+
lines.push(item.description, "");
|
|
652
|
+
lines.push(item.status === "recommended"
|
|
653
|
+
? "Status: recommended · not installed"
|
|
654
|
+
: `Status: ${item.status} · ${replicaLine(item)}`);
|
|
655
|
+
lines.push(...recommendationLines(item.curation), ...sourceLines(item, item.curation));
|
|
656
|
+
if (item.status !== "recommended") {
|
|
657
|
+
lines.push(`${canonicalSkillsRoot(home)}/${item.name}`.replace(home, "~"));
|
|
658
|
+
}
|
|
415
659
|
if (item.status === "unmanaged") {
|
|
416
|
-
|
|
660
|
+
lines.push("Installed outside the registry (e.g. skills.sh) — not operable from here.");
|
|
417
661
|
}
|
|
418
|
-
return
|
|
662
|
+
return lines.join("\n");
|
|
419
663
|
}
|
|
420
664
|
//# sourceMappingURL=skills-tab.js.map
|