kankaku 1.0.2 → 1.2.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 +120 -24
- package/dist/adapters/hub-manager/credentials.js +58 -0
- package/dist/adapters/hub-manager/install.js +93 -14
- package/dist/adapters/hub-manager/port-probe.js +38 -0
- package/dist/adapters/hub-reassign.js +115 -0
- package/dist/adapters/package-versions.js +24 -0
- package/dist/adapters/setup/hub.js +1 -1
- package/dist/adapters/setup/json-writer.js +8 -3
- package/dist/cli.js +100 -20
- package/dist/domain/local-hub-model.js +40 -0
- package/dist/domain/nav-model.js +8 -0
- package/dist/domain/reassign-model.js +101 -0
- package/dist/domain/reassign-picker.js +258 -0
- package/dist/domain/setup-wizard.js +23 -4
- package/dist/domain/version-info.js +5 -0
- package/dist/ports/reassign-actions.js +1 -0
- package/dist/ui/app.js +9 -4
- package/dist/ui/components/table.js +2 -2
- package/dist/ui/layout.js +3 -2
- package/dist/ui/reassign-panel.js +36 -0
- package/dist/ui/setup/wizard-screen.js +31 -4
- package/dist/ui/tasks-screen.js +167 -10
- package/package.json +3 -3
- package/vendor/kankaku-pi/package.json +3 -3
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
import { ALREADY_ASSIGNED, NOT_ON_HUB, UNASSIGNED_LABEL, eligibleForBulk, planReassignment } from "./reassign-model.js";
|
|
2
|
+
/** The id of the "no project" / "no task" option: an empty relation. */
|
|
3
|
+
export const NO_CHOICE = "";
|
|
4
|
+
export function openPicker(input) {
|
|
5
|
+
return { step: "client", index: 0, ...input };
|
|
6
|
+
}
|
|
7
|
+
function clientLabel(name, unassigned) {
|
|
8
|
+
return unassigned === true ? UNASSIGNED_LABEL : name;
|
|
9
|
+
}
|
|
10
|
+
/** The selectable options of a choosing step (`client`, `project`, `task`); empty on every other step. */
|
|
11
|
+
export function pickerOptions(state) {
|
|
12
|
+
const { catalog } = state;
|
|
13
|
+
if (state.step === "client") {
|
|
14
|
+
const active = catalog.clients.filter((client) => client.active);
|
|
15
|
+
const ordered = [...active.filter((client) => client.unassigned !== true), ...active.filter((client) => client.unassigned === true)];
|
|
16
|
+
return ordered.map((client) => ({ id: client.id, label: clientLabel(client.name, client.unassigned) }));
|
|
17
|
+
}
|
|
18
|
+
if (state.step === "project") {
|
|
19
|
+
const projects = catalog.projects.filter((project) => project.active && project.clientId === state.clientId);
|
|
20
|
+
return [...projects.map((project) => ({ id: project.id, label: project.name })), { id: NO_CHOICE, label: "no project" }];
|
|
21
|
+
}
|
|
22
|
+
if (state.step === "task") {
|
|
23
|
+
const tasks = catalog.tasks.filter((task) => task.projectId === state.projectId && task.status !== "done");
|
|
24
|
+
return [...tasks.map((task) => ({ id: task.id, label: task.title })), { id: NO_CHOICE, label: "no task" }];
|
|
25
|
+
}
|
|
26
|
+
return [];
|
|
27
|
+
}
|
|
28
|
+
function plural(count, noun) {
|
|
29
|
+
return `${count} ${noun}${count === 1 ? "" : "s"}`;
|
|
30
|
+
}
|
|
31
|
+
function lines(state) {
|
|
32
|
+
return state.plan?.ok ? state.plan.plan.lines : [];
|
|
33
|
+
}
|
|
34
|
+
function outcomeFor(state, taskId) {
|
|
35
|
+
return state.outcomes?.find((outcome) => outcome.taskId === taskId);
|
|
36
|
+
}
|
|
37
|
+
function skippedGroups(planLines) {
|
|
38
|
+
const groups = [];
|
|
39
|
+
for (const reason of [ALREADY_ASSIGNED, NOT_ON_HUB]) {
|
|
40
|
+
const count = planLines.filter((line) => line.kind === "skipped" && line.reason === reason).length;
|
|
41
|
+
if (count > 0)
|
|
42
|
+
groups.push({ text: `${count} not touched — ${reason}`, tone: "muted" });
|
|
43
|
+
}
|
|
44
|
+
return groups;
|
|
45
|
+
}
|
|
46
|
+
/** The scrolling list of the current step, as display rows. */
|
|
47
|
+
export function pickerRows(state) {
|
|
48
|
+
if (state.step === "review") {
|
|
49
|
+
if (state.plan === undefined || !state.plan.ok)
|
|
50
|
+
return [{ text: `cannot reassign: ${state.plan?.ok === false ? state.plan.error : "no plan"}`, tone: "error" }];
|
|
51
|
+
const planLines = lines(state);
|
|
52
|
+
const rows = [];
|
|
53
|
+
for (const line of planLines) {
|
|
54
|
+
if (line.kind === "reassign")
|
|
55
|
+
rows.push({ text: `${line.from} → ${line.to} · ${line.label}` });
|
|
56
|
+
else if (line.kind === "unchanged")
|
|
57
|
+
rows.push({ text: `already ${line.at} (unchanged) · ${line.label}`, tone: "muted" });
|
|
58
|
+
}
|
|
59
|
+
return [...rows, ...skippedGroups(planLines)];
|
|
60
|
+
}
|
|
61
|
+
if (state.step === "result" || state.step === "applying") {
|
|
62
|
+
const planLines = lines(state);
|
|
63
|
+
const rows = [];
|
|
64
|
+
for (const line of planLines) {
|
|
65
|
+
if (line.kind === "unchanged")
|
|
66
|
+
rows.push({ text: `unchanged · ${line.label}`, tone: "muted" });
|
|
67
|
+
if (line.kind !== "reassign")
|
|
68
|
+
continue;
|
|
69
|
+
if (state.step === "applying") {
|
|
70
|
+
rows.push({ text: `sending · ${line.label}` });
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
const outcome = outcomeFor(state, line.taskId);
|
|
74
|
+
if (outcome === undefined)
|
|
75
|
+
rows.push({ text: `failed: no answer from the hub · ${line.label}`, tone: "error" });
|
|
76
|
+
else if (outcome.status === "reassigned")
|
|
77
|
+
rows.push({ text: `reassigned · ${line.label}` });
|
|
78
|
+
else
|
|
79
|
+
rows.push({ text: `failed: ${outcome.reason} · ${line.label}`, tone: "error" });
|
|
80
|
+
}
|
|
81
|
+
return [...rows, ...skippedGroups(planLines)];
|
|
82
|
+
}
|
|
83
|
+
return pickerOptions(state).map((option) => ({ text: option.label }));
|
|
84
|
+
}
|
|
85
|
+
/** How many lines the current step's list has (what `moveSelection` clamps to). */
|
|
86
|
+
export function pickerRowCount(state) {
|
|
87
|
+
return pickerRows(state).length;
|
|
88
|
+
}
|
|
89
|
+
function clamp(value, count) {
|
|
90
|
+
return Math.min(Math.max(value, 0), Math.max(count - 1, 0));
|
|
91
|
+
}
|
|
92
|
+
/** Move the selection by `delta` lines, clamped to the list (never wraps). */
|
|
93
|
+
export function moveSelection(state, delta) {
|
|
94
|
+
return { ...state, index: clamp(state.index + delta, pickerRowCount(state)) };
|
|
95
|
+
}
|
|
96
|
+
export function jumpSelection(state, where) {
|
|
97
|
+
return { ...state, index: where === "first" ? 0 : clamp(Number.MAX_SAFE_INTEGER, pickerRowCount(state)) };
|
|
98
|
+
}
|
|
99
|
+
function withPlan(state) {
|
|
100
|
+
const selection = {
|
|
101
|
+
clientId: state.clientId ?? "",
|
|
102
|
+
...(state.projectId !== undefined ? { projectId: state.projectId } : {}),
|
|
103
|
+
...(state.hubTaskId !== undefined ? { hubTaskId: state.hubTaskId } : {}),
|
|
104
|
+
};
|
|
105
|
+
const plan = planReassignment({ rows: state.rows, tasks: state.targets, catalog: state.catalog, selection, mode: state.mode });
|
|
106
|
+
return { ...state, step: "review", index: 0, plan };
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* `enter`: take the selected option and go to the next step, or apply from
|
|
110
|
+
* review. Returns `undefined` when the picker closes (from `result`); an
|
|
111
|
+
* empty option list, a rejected plan and the `applying` step leave the
|
|
112
|
+
* state as it is.
|
|
113
|
+
*/
|
|
114
|
+
export function advance(state) {
|
|
115
|
+
if (state.step === "result")
|
|
116
|
+
return undefined;
|
|
117
|
+
if (state.step === "applying")
|
|
118
|
+
return state;
|
|
119
|
+
if (state.step === "review") {
|
|
120
|
+
if (state.plan === undefined || !state.plan.ok)
|
|
121
|
+
return state;
|
|
122
|
+
const hasChanges = state.plan.plan.lines.some((line) => line.kind === "reassign");
|
|
123
|
+
return hasChanges ? { ...state, step: "applying", index: 0 } : { ...state, step: "result", index: 0, outcomes: [] };
|
|
124
|
+
}
|
|
125
|
+
const option = pickerOptions(state)[state.index];
|
|
126
|
+
if (option === undefined)
|
|
127
|
+
return state;
|
|
128
|
+
if (state.step === "client")
|
|
129
|
+
return { ...state, step: "project", index: 0, clientId: option.id };
|
|
130
|
+
if (state.step === "project") {
|
|
131
|
+
if (option.id === NO_CHOICE)
|
|
132
|
+
return withPlan({ ...state, projectId: undefined });
|
|
133
|
+
return { ...state, step: "task", index: 0, projectId: option.id };
|
|
134
|
+
}
|
|
135
|
+
return withPlan({ ...state, hubTaskId: option.id === NO_CHOICE ? undefined : option.id });
|
|
136
|
+
}
|
|
137
|
+
function without(state, ...keys) {
|
|
138
|
+
const copy = { ...state };
|
|
139
|
+
for (const key of keys)
|
|
140
|
+
delete copy[key];
|
|
141
|
+
return copy;
|
|
142
|
+
}
|
|
143
|
+
/** Select the option `id` in the list of `state` (used to land on the previous choice when going back). */
|
|
144
|
+
function selecting(state, id) {
|
|
145
|
+
const index = pickerOptions(state).findIndex((option) => option.id === (id ?? NO_CHOICE));
|
|
146
|
+
return { ...state, index: Math.max(index, 0) };
|
|
147
|
+
}
|
|
148
|
+
/** `esc`/`←`: go back one step, restoring the previous choice; returns `undefined` when the picker closes (from `client` or `result`). */
|
|
149
|
+
export function back(state) {
|
|
150
|
+
switch (state.step) {
|
|
151
|
+
case "client":
|
|
152
|
+
case "result":
|
|
153
|
+
return undefined;
|
|
154
|
+
case "applying":
|
|
155
|
+
return state;
|
|
156
|
+
case "project":
|
|
157
|
+
return selecting(without({ ...state, step: "client" }, "clientId", "projectId", "hubTaskId", "plan"), state.clientId);
|
|
158
|
+
case "task":
|
|
159
|
+
return selecting(without({ ...state, step: "project" }, "projectId", "hubTaskId", "plan"), state.projectId);
|
|
160
|
+
case "review":
|
|
161
|
+
return state.projectId !== undefined
|
|
162
|
+
? selecting(without({ ...state, step: "task" }, "plan"), state.hubTaskId)
|
|
163
|
+
: selecting(without({ ...state, step: "project" }, "projectId", "hubTaskId", "plan"), undefined);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
/** The hub answered every PATCH: show the result. */
|
|
167
|
+
export function finishApplying(state, outcomes) {
|
|
168
|
+
return { ...state, step: "result", index: 0, outcomes };
|
|
169
|
+
}
|
|
170
|
+
const STEP_TITLES = {
|
|
171
|
+
client: "Client",
|
|
172
|
+
project: "Project",
|
|
173
|
+
task: "Task",
|
|
174
|
+
review: "Review",
|
|
175
|
+
applying: "Applying",
|
|
176
|
+
result: "Result",
|
|
177
|
+
};
|
|
178
|
+
export function pickerTitle(state) {
|
|
179
|
+
return `Reassign · ${STEP_TITLES[state.step]}`;
|
|
180
|
+
}
|
|
181
|
+
function subject(state) {
|
|
182
|
+
if (state.mode === "single")
|
|
183
|
+
return `"${state.targets[0]?.label ?? ""}"`;
|
|
184
|
+
return plural(eligibleForBulk(state.targets, state.rows, state.catalog).length, "unassigned task");
|
|
185
|
+
}
|
|
186
|
+
function chosenClientName(state) {
|
|
187
|
+
const client = state.catalog.clients.find((candidate) => candidate.id === state.clientId);
|
|
188
|
+
return client === undefined ? "unknown client" : clientLabel(client.name, client.unassigned);
|
|
189
|
+
}
|
|
190
|
+
/** The two fixed lines above the list: what is being reassigned and what to do or what happened. Always two entries so the layout never shifts. */
|
|
191
|
+
export function pickerHeading(state) {
|
|
192
|
+
switch (state.step) {
|
|
193
|
+
case "client":
|
|
194
|
+
return [`Reassign ${subject(state)}`, "Choose a client"];
|
|
195
|
+
case "project":
|
|
196
|
+
return [`Reassign ${subject(state)}`, `${chosenClientName(state)} — choose a project`];
|
|
197
|
+
case "task": {
|
|
198
|
+
const project = state.catalog.projects.find((candidate) => candidate.id === state.projectId);
|
|
199
|
+
return [`Reassign ${subject(state)}`, `${chosenClientName(state)} · ${project?.name ?? "unknown project"} — choose a task`];
|
|
200
|
+
}
|
|
201
|
+
case "review": {
|
|
202
|
+
if (state.plan === undefined || !state.plan.ok)
|
|
203
|
+
return ["Cannot reassign", ""];
|
|
204
|
+
const planLines = state.plan.plan.lines;
|
|
205
|
+
const changes = planLines.filter((line) => line.kind === "reassign").length;
|
|
206
|
+
const unchanged = planLines.filter((line) => line.kind === "unchanged").length;
|
|
207
|
+
const skipped = planLines.filter((line) => line.kind === "skipped").length;
|
|
208
|
+
const detail = [unchanged > 0 ? `${unchanged} unchanged` : "", skipped > 0 ? `${skipped} not touched` : ""].filter((part) => part !== "").join(" · ");
|
|
209
|
+
return [changes > 0 ? `Move ${plural(changes, "task")} to ${state.plan.plan.destination}` : "Nothing to change", detail];
|
|
210
|
+
}
|
|
211
|
+
case "applying":
|
|
212
|
+
return ["Applying…", "sending the changes to the hub"];
|
|
213
|
+
case "result": {
|
|
214
|
+
const planLines = lines(state);
|
|
215
|
+
const outcomes = state.outcomes ?? [];
|
|
216
|
+
const done = outcomes.filter((outcome) => outcome.status === "reassigned").length;
|
|
217
|
+
const failed = planLines.filter((line) => line.kind === "reassign").length - done;
|
|
218
|
+
const unchanged = planLines.filter((line) => line.kind === "unchanged").length;
|
|
219
|
+
const skipped = planLines.filter((line) => line.kind === "skipped").length;
|
|
220
|
+
const headline = [done > 0 ? `${done} reassigned` : "", unchanged > 0 ? `${unchanged} unchanged` : "", failed > 0 ? `${failed} failed` : ""].filter((part) => part !== "").join(" · ");
|
|
221
|
+
return [headline === "" ? "Nothing changed" : headline, skipped > 0 ? `${skipped} not touched` : ""];
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
/** The footer key hints for the current step. */
|
|
226
|
+
export function pickerHints(state) {
|
|
227
|
+
switch (state.step) {
|
|
228
|
+
case "client":
|
|
229
|
+
return [
|
|
230
|
+
{ key: "↑↓", label: "move" },
|
|
231
|
+
{ key: "enter", label: "next" },
|
|
232
|
+
{ key: "esc", label: "close" },
|
|
233
|
+
];
|
|
234
|
+
case "project":
|
|
235
|
+
case "task":
|
|
236
|
+
return [
|
|
237
|
+
{ key: "↑↓", label: "move" },
|
|
238
|
+
{ key: "enter", label: "next" },
|
|
239
|
+
{ key: "esc", label: "back" },
|
|
240
|
+
];
|
|
241
|
+
case "review":
|
|
242
|
+
if (state.plan === undefined || !state.plan.ok)
|
|
243
|
+
return [{ key: "esc", label: "back" }];
|
|
244
|
+
return [
|
|
245
|
+
{ key: "↑↓", label: "scroll" },
|
|
246
|
+
{ key: "enter", label: "apply" },
|
|
247
|
+
{ key: "esc", label: "back" },
|
|
248
|
+
];
|
|
249
|
+
case "applying":
|
|
250
|
+
return [{ key: "…", label: "applying, please wait" }];
|
|
251
|
+
case "result":
|
|
252
|
+
return [
|
|
253
|
+
{ key: "↑↓", label: "scroll" },
|
|
254
|
+
{ key: "enter", label: "done" },
|
|
255
|
+
{ key: "esc", label: "done" },
|
|
256
|
+
];
|
|
257
|
+
}
|
|
258
|
+
}
|
|
@@ -5,8 +5,9 @@
|
|
|
5
5
|
* callers (`src/ui/setup/*`, `src/cli.tsx`) read the real files and pass
|
|
6
6
|
* plain facts in via `WizardFacts`, exactly like `domain/setup-plan.ts`.
|
|
7
7
|
*/
|
|
8
|
+
import { shouldWriteCredentials } from "./local-hub-model.js";
|
|
8
9
|
import { detectAgents } from "./setup-plan.js";
|
|
9
|
-
/** The local hub's
|
|
10
|
+
/** The local hub's default port, where the wizard's port field starts looking for a free one - mirrors `hub-manager/install.ts#DEFAULT_HUB_PORT`. */
|
|
10
11
|
export const DEFAULT_HUB_PORT = 8090;
|
|
11
12
|
/**
|
|
12
13
|
* Shorten `path` for display by replacing a leading `homeDir` with `~`.
|
|
@@ -31,7 +32,7 @@ function clearError(errors, key) {
|
|
|
31
32
|
delete rest[key];
|
|
32
33
|
return rest;
|
|
33
34
|
}
|
|
34
|
-
const HUB_ERROR_KEYS = ["url", "email", "password", "ownerEmail", "ownerPassword"];
|
|
35
|
+
const HUB_ERROR_KEYS = ["url", "email", "password", "ownerEmail", "ownerPassword", "port"];
|
|
35
36
|
function clearHubErrors(errors) {
|
|
36
37
|
return HUB_ERROR_KEYS.reduce((acc, key) => clearError(acc, key), errors);
|
|
37
38
|
}
|
|
@@ -50,6 +51,7 @@ export function createWizardState(facts) {
|
|
|
50
51
|
password: facts.hub.password ?? "",
|
|
51
52
|
ownerEmail: "",
|
|
52
53
|
ownerPassword: "",
|
|
54
|
+
port: String(facts.hub.localPort ?? DEFAULT_HUB_PORT),
|
|
53
55
|
},
|
|
54
56
|
roots: facts.roots.current ?? facts.roots.defaultRoots,
|
|
55
57
|
errors: {},
|
|
@@ -68,6 +70,17 @@ export function setHubMode(state, mode) {
|
|
|
68
70
|
export function setHubField(state, field, value) {
|
|
69
71
|
return { ...state, hub: { ...state.hub, [field]: value }, errors: clearError(state.errors, field) };
|
|
70
72
|
}
|
|
73
|
+
/** The local hub port as a number when `text` is digits only within 1024-65535, otherwise `undefined`. */
|
|
74
|
+
export function parseHubPort(text) {
|
|
75
|
+
if (!/^\d+$/.test(text))
|
|
76
|
+
return undefined;
|
|
77
|
+
const port = Number(text);
|
|
78
|
+
return port >= 1024 && port <= 65535 ? port : undefined;
|
|
79
|
+
}
|
|
80
|
+
/** Flags `port` as occupied (`port N is in use`), found by the probe the Hub step runs before advancing; editing the port field clears it. */
|
|
81
|
+
export function setHubPortInUse(state, port) {
|
|
82
|
+
return { ...state, errors: { ...state.errors, port: `port ${port} is in use` } };
|
|
83
|
+
}
|
|
71
84
|
export function setHubHealth(state, ok) {
|
|
72
85
|
return { ...state, hub: { ...state.hub, healthOk: ok } };
|
|
73
86
|
}
|
|
@@ -96,6 +109,8 @@ function validateHub(hub) {
|
|
|
96
109
|
errors.ownerEmail = "enter a valid email";
|
|
97
110
|
if (hub.ownerPassword.trim() === "")
|
|
98
111
|
errors.ownerPassword = "enter a password";
|
|
112
|
+
if (parseHubPort(hub.port) === undefined)
|
|
113
|
+
errors.port = "enter a port between 1024 and 65535";
|
|
99
114
|
return errors;
|
|
100
115
|
}
|
|
101
116
|
return {};
|
|
@@ -140,8 +155,12 @@ export function planFromWizard(state, facts) {
|
|
|
140
155
|
}
|
|
141
156
|
}
|
|
142
157
|
else if (state.hub.mode === "local") {
|
|
143
|
-
const url = `http://127.0.0.1:${DEFAULT_HUB_PORT}`;
|
|
144
|
-
|
|
158
|
+
const url = `http://127.0.0.1:${parseHubPort(state.hub.port) ?? DEFAULT_HUB_PORT}`;
|
|
159
|
+
const existingUrl = facts.hub.credentialsPresent ? facts.hub.url : undefined;
|
|
160
|
+
const note = shouldWriteCredentials(existingUrl, url)
|
|
161
|
+
? "sync credentials → the local hub"
|
|
162
|
+
: `sync credentials stay on ${existingUrl} (switch later with kankaku hub use)`;
|
|
163
|
+
actions.push({ kind: "install-local-hub", file: url, label: `install a local hub at ${url}`, note });
|
|
145
164
|
}
|
|
146
165
|
const sameRoots = facts.roots.current !== undefined && arraysEqual(facts.roots.current, state.roots);
|
|
147
166
|
if (!sameRoots) {
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
/** Pure text for `kankaku --version`. No I/O: `adapters/package-versions.ts` reads the versions. */
|
|
2
|
+
/** `kankaku <version>`, then one indented line per carried package: its version, or `not found`. */
|
|
3
|
+
export function formatVersionLines(ownVersion, carried) {
|
|
4
|
+
return [`kankaku ${ownVersion}`, ...carried.map(({ name, version }) => ` ${name} ${version ?? "not found"}`)];
|
|
5
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/ui/app.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { useState } from "react";
|
|
3
3
|
import { useApp, useInput } from "ink";
|
|
4
|
-
import { INITIAL_NAV_STATE, clearProjectFilter, focusMain, focusSidebar, moveSidebar, openProjectInTasks, screenForKey, switchScreen, } from "../domain/nav-model.js";
|
|
4
|
+
import { INITIAL_NAV_STATE, clearProjectFilter, focusMain, focusSidebar, moveSidebar, openProjectInTasks, screenForKey, setModal, switchScreen, } from "../domain/nav-model.js";
|
|
5
5
|
import { DashboardScreen } from "./dashboard-screen.js";
|
|
6
6
|
import { TasksScreen } from "./tasks-screen.js";
|
|
7
7
|
import { CatalogScreen } from "./catalog-screen.js";
|
|
@@ -23,9 +23,12 @@ import { SetupWizard } from "./setup/wizard-screen.js";
|
|
|
23
23
|
* to it, keeping focus on main; Tasks' own `esc` handling (gated the same
|
|
24
24
|
* way as every other screen key, by its `focused` prop) clears that
|
|
25
25
|
* filter — this hook only predicts whether that will happen, to decide
|
|
26
|
-
* whether it should also move focus.
|
|
26
|
+
* whether it should also move focus. While the Tasks screen has its
|
|
27
|
+
* reassignment picker open (`NavState.modal`, reported through its
|
|
28
|
+
* `onModalChange`), only `q` is handled here: `esc`, `←`, `Tab` and `1`-`4`
|
|
29
|
+
* belong to the picker, so it can never be left half-way by accident.
|
|
27
30
|
*/
|
|
28
|
-
export function App({ roots, version, loadToday, loadTasks, catalog, sync, dashboardActions, wizard, startInWizard }) {
|
|
31
|
+
export function App({ roots, version, loadToday, loadTasks, catalog, sync, dashboardActions, wizard, startInWizard, reassign }) {
|
|
29
32
|
const { exit } = useApp();
|
|
30
33
|
const [nav, setNav] = useState(INITIAL_NAV_STATE);
|
|
31
34
|
const [showWizard, setShowWizard] = useState(startInWizard === true && wizard !== undefined);
|
|
@@ -34,6 +37,8 @@ export function App({ roots, version, loadToday, loadTasks, catalog, sync, dashb
|
|
|
34
37
|
exit();
|
|
35
38
|
return;
|
|
36
39
|
}
|
|
40
|
+
if (nav.modal === true)
|
|
41
|
+
return;
|
|
37
42
|
const nextScreen = screenForKey(input);
|
|
38
43
|
if (nextScreen !== undefined) {
|
|
39
44
|
setNav((state) => switchScreen(state, nextScreen));
|
|
@@ -70,5 +75,5 @@ export function App({ roots, version, loadToday, loadTasks, catalog, sync, dashb
|
|
|
70
75
|
if (showWizard && wizard) {
|
|
71
76
|
return _jsx(SetupWizard, { facts: wizard.facts, actions: wizard.actions, onDone: () => setShowWizard(false), onQuit: () => exit(), version: version });
|
|
72
77
|
}
|
|
73
|
-
return (_jsxs(_Fragment, { children: [nav.screen === "dashboard" && (_jsx(DashboardScreen, { load: loadToday, actions: dashboardActions, roots: roots, version: version, focused: focused, onOpenProject: (project) => setNav((state) => openProjectInTasks(state, project)) })), nav.screen === "tasks" && (_jsx(TasksScreen, { load: loadTasks, roots: roots, version: version, focused: focused, ...(nav.projectFilter !== undefined ? { projectFilter: nav.projectFilter } : {}), onClearFilter: () => setNav((state) => clearProjectFilter(state)) })), nav.screen === "catalog" && _jsx(CatalogScreen, { ...catalog, roots: roots, version: version, focused: focused }), nav.screen === "sync" && _jsx(SyncScreen, { ...sync, roots: roots, version: version, focused: focused })] }));
|
|
78
|
+
return (_jsxs(_Fragment, { children: [nav.screen === "dashboard" && (_jsx(DashboardScreen, { load: loadToday, actions: dashboardActions, roots: roots, version: version, focused: focused, onOpenProject: (project) => setNav((state) => openProjectInTasks(state, project)) })), nav.screen === "tasks" && (_jsx(TasksScreen, { load: loadTasks, roots: roots, version: version, focused: focused, ...(nav.projectFilter !== undefined ? { projectFilter: nav.projectFilter } : {}), onClearFilter: () => setNav((state) => clearProjectFilter(state)), onModalChange: (open) => setNav((state) => (state.modal === true) === open ? state : setModal(state, open)), ...(reassign !== undefined ? { reassign } : {}) })), nav.screen === "catalog" && _jsx(CatalogScreen, { ...catalog, roots: roots, version: version, focused: focused }), nav.screen === "sync" && _jsx(SyncScreen, { ...sync, roots: roots, version: version, focused: focused })] }));
|
|
74
79
|
}
|
|
@@ -39,7 +39,7 @@ function rowText(columns, values) {
|
|
|
39
39
|
* than its column truncates with `…`; `emptyText` renders instead of the
|
|
40
40
|
* body when `rows` is empty.
|
|
41
41
|
*/
|
|
42
|
-
export function Table({ columns, rows, rowKey, cell, selectedIndex, emptyText = "no rows", maxRows }) {
|
|
42
|
+
export function Table({ columns, rows, rowKey, cell, selectedIndex, rowColor, emptyText = "no rows", maxRows }) {
|
|
43
43
|
const theme = useTheme();
|
|
44
44
|
const startRef = useRef(0);
|
|
45
45
|
const windowed = maxRows !== undefined && rows.length > maxRows;
|
|
@@ -50,6 +50,6 @@ export function Table({ columns, rows, rowKey, cell, selectedIndex, emptyText =
|
|
|
50
50
|
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { color: theme.muted, children: ` ${rowText(columns, (column) => column.header)}` }), rows.length === 0 ? (_jsx(Text, { color: theme.muted, children: emptyText })) : (_jsxs(_Fragment, { children: [window !== undefined && window.hiddenAbove > 0 && (_jsx(Text, { color: theme.muted, children: ` ↑ ${window.hiddenAbove} more` }, "__indicator-above__")), visibleRows.map((row, index) => {
|
|
51
51
|
const actualIndex = (window?.start ?? 0) + index;
|
|
52
52
|
const selected = actualIndex === selectedIndex;
|
|
53
|
-
return (_jsx(Text, { backgroundColor: selected ? theme.selectionBg : undefined, children: `${selected ? "› " : " "}${rowText(columns, (column) => cell(row, column.key))}` }, rowKey(row)));
|
|
53
|
+
return (_jsx(Text, { color: rowColor?.(row), backgroundColor: selected ? theme.selectionBg : undefined, children: `${selected ? "› " : " "}${rowText(columns, (column) => cell(row, column.key))}` }, rowKey(row)));
|
|
54
54
|
}), window !== undefined && window.hiddenBelow > 0 && (_jsx(Text, { color: theme.muted, children: ` ↓ ${window.hiddenBelow} more` }, "__indicator-below__"))] }))] }));
|
|
55
55
|
}
|
package/dist/ui/layout.js
CHANGED
|
@@ -39,7 +39,8 @@ function TabStrip({ items, activeId }) {
|
|
|
39
39
|
* own panels and scrolling lists to the actual `mainWidth`/`mainHeight`
|
|
40
40
|
* in whichever mode is active.
|
|
41
41
|
*/
|
|
42
|
-
export function Layout({ columns, rows, headerLeft, headerRight, sidebarItems, activeId, sidebarStats, keyHints, focus = "sidebar", children }) {
|
|
42
|
+
export function Layout({ columns, rows, headerLeft, headerRight, sidebarItems, activeId, sidebarStats, keyHints, footerNote, focus = "sidebar", children }) {
|
|
43
|
+
const theme = useTheme();
|
|
43
44
|
const { stdout } = useStdout();
|
|
44
45
|
const width = columns ?? stdout?.columns ?? 80;
|
|
45
46
|
const height = rows ?? stdout?.rows ?? DEFAULT_ROWS;
|
|
@@ -50,5 +51,5 @@ export function Layout({ columns, rows, headerLeft, headerRight, sidebarItems, a
|
|
|
50
51
|
const chromeRows = mode === "collapsed" ? TAB_STRIP_ROWS : mode === "stacked" ? sidebarBlockRows(sidebarItems.length, sidebarStats.length) : 0;
|
|
51
52
|
const mainHeight = Math.max(middleRows - chromeRows, 0);
|
|
52
53
|
const content = children({ mainWidth, mainHeight, mode });
|
|
53
|
-
return (_jsxs(Box, { flexDirection: "column", width: width, height: height, children: [_jsx(HeaderBar, { left: headerLeft, right: headerRight, width: width }), mode === "collapsed" && (_jsxs(Box, { flexDirection: "column", flexGrow: 1, minHeight: 0, children: [_jsx(TabStrip, { items: sidebarItems, activeId: activeId }), _jsx(Box, { flexDirection: "column", flexGrow: 1, minHeight: 0, overflow: "hidden", children: content })] })), mode === "stacked" && (_jsxs(Box, { flexDirection: "column", flexGrow: 1, minHeight: 0, children: [_jsx(Sidebar, { items: sidebarItems, activeId: activeId, stats: sidebarStats, width: width, focused: sidebarFocused }), _jsx(Box, { flexDirection: "column", flexGrow: 1, minHeight: 0, overflow: "hidden", children: content })] })), mode === "wide" && (_jsxs(Box, { flexDirection: "row", flexGrow: 1, minHeight: 0, alignItems: "stretch", children: [_jsx(Sidebar, { items: sidebarItems, activeId: activeId, stats: sidebarStats, width: SIDEBAR_WIDTH, focused: sidebarFocused }), _jsx(Box, { flexDirection: "column", flexGrow: 1, minHeight: 0, overflow: "hidden", children: content })] })), _jsx(KeyHints, { hints: keyHints })] }));
|
|
54
|
+
return (_jsxs(Box, { flexDirection: "column", width: width, height: height, children: [_jsx(HeaderBar, { left: headerLeft, right: headerRight, width: width }), mode === "collapsed" && (_jsxs(Box, { flexDirection: "column", flexGrow: 1, minHeight: 0, children: [_jsx(TabStrip, { items: sidebarItems, activeId: activeId }), _jsx(Box, { flexDirection: "column", flexGrow: 1, minHeight: 0, overflow: "hidden", children: content })] })), mode === "stacked" && (_jsxs(Box, { flexDirection: "column", flexGrow: 1, minHeight: 0, children: [_jsx(Sidebar, { items: sidebarItems, activeId: activeId, stats: sidebarStats, width: width, focused: sidebarFocused }), _jsx(Box, { flexDirection: "column", flexGrow: 1, minHeight: 0, overflow: "hidden", children: content })] })), mode === "wide" && (_jsxs(Box, { flexDirection: "row", flexGrow: 1, minHeight: 0, alignItems: "stretch", children: [_jsx(Sidebar, { items: sidebarItems, activeId: activeId, stats: sidebarStats, width: SIDEBAR_WIDTH, focused: sidebarFocused }), _jsx(Box, { flexDirection: "column", flexGrow: 1, minHeight: 0, overflow: "hidden", children: content })] })), footerNote !== undefined ? _jsx(Text, { color: theme.warn, children: footerNote }) : _jsx(KeyHints, { hints: keyHints })] }));
|
|
54
55
|
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Text } from "ink";
|
|
3
|
+
import { pickerHeading, pickerRows, pickerTitle } from "../domain/reassign-picker.js";
|
|
4
|
+
import { Panel } from "./components/panel.js";
|
|
5
|
+
import { Table } from "./components/table.js";
|
|
6
|
+
import { useTheme } from "./theme.js";
|
|
7
|
+
/** Rows the panel's own chrome takes (top border line + bottom border) plus the two heading lines and the table's header line. */
|
|
8
|
+
const PICKER_FIXED_ROWS = 2 + 2 + 1;
|
|
9
|
+
/** Columns the panel border (2) and the table's `› ` marker (2) take from the panel width. */
|
|
10
|
+
const PICKER_FIXED_COLUMNS = 4;
|
|
11
|
+
const COLUMN_HEADERS = {
|
|
12
|
+
client: "client",
|
|
13
|
+
project: "project",
|
|
14
|
+
task: "task",
|
|
15
|
+
review: "change",
|
|
16
|
+
applying: "sending",
|
|
17
|
+
result: "result",
|
|
18
|
+
};
|
|
19
|
+
/** How many list lines fit a picker panel of `height` rows (at least one, so a tiny terminal still tracks the selection). */
|
|
20
|
+
export function pickerMaxRows(height) {
|
|
21
|
+
return Math.max(height - PICKER_FIXED_ROWS, 1);
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* The reassignment picker, drawn as one fixed-size `Panel` over the Tasks
|
|
25
|
+
* screen's content zone: two heading lines, then the current step's list
|
|
26
|
+
* in a windowed `Table` (the same scrolling and `↑ N more`/`↓ N more`
|
|
27
|
+
* indicators as every other list). The panel's `height` is fixed by the
|
|
28
|
+
* caller, so a long list scrolls inside it and never grows the frame.
|
|
29
|
+
*/
|
|
30
|
+
export function ReassignPanel({ state, width, height }) {
|
|
31
|
+
const theme = useTheme();
|
|
32
|
+
const [headline, detail] = pickerHeading(state);
|
|
33
|
+
const rows = pickerRows(state).map((entry, index) => ({ ...entry, key: String(index) }));
|
|
34
|
+
const textWidth = Math.max(width - PICKER_FIXED_COLUMNS, 1);
|
|
35
|
+
return (_jsxs(Panel, { title: pickerTitle(state), width: width, height: height, active: true, children: [_jsx(Text, { wrap: "truncate-end", children: headline }), _jsx(Text, { color: theme.muted, wrap: "truncate-end", children: detail === "" ? " " : detail }), _jsx(Table, { columns: [{ key: "text", header: COLUMN_HEADERS[state.step], width: textWidth }], rows: rows, rowKey: (row) => row.key, cell: (row) => row.text, rowColor: (row) => (row.tone === "error" ? theme.error : row.tone === "muted" ? theme.muted : undefined), selectedIndex: state.index, emptyText: "nothing to choose from", maxRows: pickerMaxRows(height) }, state.step)] }));
|
|
36
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { useEffect, useRef, useState } from "react";
|
|
3
3
|
import { Box, Text, useInput, useStdout } from "ink";
|
|
4
|
-
import { DEFAULT_HUB_PORT, applyResult, back, createWizardState, hintsForStep, next, planFromWizard, setHubField, setHubHealth, setHubMode, setRoots, shortenHome, toggleAgent, } from "../../domain/setup-wizard.js";
|
|
4
|
+
import { DEFAULT_HUB_PORT, applyResult, back, createWizardState, hintsForStep, next, parseHubPort, planFromWizard, setHubField, setHubHealth, setHubMode, setHubPortInUse, setRoots, shortenHome, toggleAgent, } from "../../domain/setup-wizard.js";
|
|
5
5
|
import { HeaderBar } from "../components/header-bar.js";
|
|
6
6
|
import { KeyHints } from "../components/key-hints.js";
|
|
7
7
|
import { Panel } from "../components/panel.js";
|
|
@@ -111,11 +111,27 @@ export function SetupWizard({ facts, actions, onDone, onQuit, version, columns,
|
|
|
111
111
|
const [healthChecking, setHealthChecking] = useState(false);
|
|
112
112
|
const [rootsText, setRootsText] = useState(() => state.roots.join(", "));
|
|
113
113
|
const applyStartedRef = useRef(false);
|
|
114
|
+
const portTouchedRef = useRef(false);
|
|
114
115
|
useEffect(() => {
|
|
115
116
|
if (state.step === "hub")
|
|
116
117
|
setHubFocus(0);
|
|
117
118
|
}, [state.step]);
|
|
118
|
-
|
|
119
|
+
// Prefill the local hub's port with the first free one from the default upwards, unless an install already recorded its own or the user got there first.
|
|
120
|
+
useEffect(() => {
|
|
121
|
+
if (facts.hub.localPort !== undefined)
|
|
122
|
+
return;
|
|
123
|
+
let cancelled = false;
|
|
124
|
+
void actions.suggestPort(DEFAULT_HUB_PORT).then((port) => {
|
|
125
|
+
if (cancelled || port === undefined || portTouchedRef.current)
|
|
126
|
+
return;
|
|
127
|
+
setState((s) => setHubField(s, "port", String(port)));
|
|
128
|
+
});
|
|
129
|
+
return () => {
|
|
130
|
+
cancelled = true;
|
|
131
|
+
};
|
|
132
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
133
|
+
}, []);
|
|
134
|
+
const hubFieldCount = state.hub.mode === "existing" ? 4 : state.hub.mode === "local" ? 4 : 1;
|
|
119
135
|
const textInputFocused = state.step === "roots" || (state.step === "hub" && hubFocus >= 1 && hubFocus < hubFieldCount);
|
|
120
136
|
useEffect(() => {
|
|
121
137
|
if (state.step !== "apply" || applyStartedRef.current)
|
|
@@ -140,6 +156,14 @@ export function SetupWizard({ facts, actions, onDone, onQuit, version, columns,
|
|
|
140
156
|
onDone();
|
|
141
157
|
return;
|
|
142
158
|
}
|
|
159
|
+
if (state.step === "hub" && state.hub.mode === "local") {
|
|
160
|
+
// A valid port is probed before advancing; an existing install's own port is ours and is never probed.
|
|
161
|
+
const port = parseHubPort(state.hub.port);
|
|
162
|
+
if (port !== undefined && port !== facts.hub.localPort) {
|
|
163
|
+
void actions.isPortFree(port).then((free) => setState((s) => (free ? next(s, facts) : setHubPortInUse(s, port))));
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
143
167
|
setState((s) => next(s, facts));
|
|
144
168
|
return;
|
|
145
169
|
}
|
|
@@ -172,8 +196,11 @@ export function SetupWizard({ facts, actions, onDone, onQuit, version, columns,
|
|
|
172
196
|
const height = rows ?? stdout?.rows ?? DEFAULT_ROWS;
|
|
173
197
|
const mainWidth = width;
|
|
174
198
|
const mainHeight = Math.max(height - HEADER_ROWS - FOOTER_ROWS, 0);
|
|
175
|
-
return (_jsxs(Box, { flexDirection: "column", width: width, height: height, children: [_jsx(HeaderBar, { left: `>_ kankaku setup${version ? ` ${version}` : ""}`, width: width }), _jsx(Box, { flexDirection: "column", flexGrow: 1, minHeight: 0, children: _jsxs(Panel, { title: panelTitle(state), width: mainWidth, height: mainHeight, active: true, children: [state.step === "agents" && (_jsx(Checklist, { items: agentChecklistItems(state.agents, state.selected, facts.homeDir), cursor: agentCursor, onToggle: (id) => setState((s) => toggleAgent(s, id)), onMove: (delta) => setAgentCursor((c) => Math.min(Math.max(c + delta, 0), state.agents.length - 1)), focused: true })), state.step === "hub" && (_jsxs(Box, { flexDirection: "column", children: [_jsx(Radio, { options: HUB_MODE_OPTIONS, value: state.hub.mode, onChange: (mode) => setState((s) => setHubMode(s, mode)), focused: hubFocus === 0 }), state.hub.mode === "existing" && (_jsxs(Box, { flexDirection: "column", children: [["url", "email", "password"].map((field, index) => (_jsxs(Box, { flexDirection: "row", children: [_jsx(Text, { children: `${field}: ` }), _jsx(TextInput, { value: state.hub[field], onChange: (value) => setState((s) => setHubField(s, field, value)), masked: field === "password", focused: hubFocus === index + 1 })] }, field))), state.errors.url && _jsx(Text, { color: theme.error, children: state.errors.url }), state.errors.email && _jsx(Text, { color: theme.error, children: state.errors.email }), state.errors.password && _jsx(Text, { color: theme.error, children: state.errors.password }), _jsx(Text, { children: `[ check ] c ${healthLine(state.hub.healthOk, healthChecking)}` })] })), state.hub.mode === "local" && (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { dimColor: true, children: `will install and run at http://127.0.0.1:${DEFAULT_HUB_PORT}` }), _jsxs(Box, { flexDirection: "row", children: [_jsx(Text, { children: "owner email: " }), _jsx(TextInput, { value: state.hub.ownerEmail, onChange: (value) => setState((s) => setHubField(s, "ownerEmail", value)), focused: hubFocus === 1 })] }), _jsxs(Box, { flexDirection: "row", children: [_jsx(Text, { children: "owner password: " }), _jsx(TextInput, { value: state.hub.ownerPassword, onChange: (value) => setState((s) => setHubField(s, "ownerPassword", value)), masked: true, focused: hubFocus === 2 })] }),
|
|
199
|
+
return (_jsxs(Box, { flexDirection: "column", width: width, height: height, children: [_jsx(HeaderBar, { left: `>_ kankaku setup${version ? ` ${version}` : ""}`, width: width }), _jsx(Box, { flexDirection: "column", flexGrow: 1, minHeight: 0, children: _jsxs(Panel, { title: panelTitle(state), width: mainWidth, height: mainHeight, active: true, children: [state.step === "agents" && (_jsx(Checklist, { items: agentChecklistItems(state.agents, state.selected, facts.homeDir), cursor: agentCursor, onToggle: (id) => setState((s) => toggleAgent(s, id)), onMove: (delta) => setAgentCursor((c) => Math.min(Math.max(c + delta, 0), state.agents.length - 1)), focused: true })), state.step === "hub" && (_jsxs(Box, { flexDirection: "column", children: [_jsx(Radio, { options: HUB_MODE_OPTIONS, value: state.hub.mode, onChange: (mode) => setState((s) => setHubMode(s, mode)), focused: hubFocus === 0 }), state.hub.mode === "existing" && (_jsxs(Box, { flexDirection: "column", children: [["url", "email", "password"].map((field, index) => (_jsxs(Box, { flexDirection: "row", children: [_jsx(Text, { children: `${field}: ` }), _jsx(TextInput, { value: state.hub[field], onChange: (value) => setState((s) => setHubField(s, field, value)), masked: field === "password", focused: hubFocus === index + 1 })] }, field))), state.errors.url && _jsx(Text, { color: theme.error, children: state.errors.url }), state.errors.email && _jsx(Text, { color: theme.error, children: state.errors.email }), state.errors.password && _jsx(Text, { color: theme.error, children: state.errors.password }), _jsx(Text, { children: `[ check ] c ${healthLine(state.hub.healthOk, healthChecking)}` })] })), state.hub.mode === "local" && (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { dimColor: true, children: `will install and run at http://127.0.0.1:${parseHubPort(state.hub.port) ?? DEFAULT_HUB_PORT}` }), _jsxs(Box, { flexDirection: "row", children: [_jsx(Text, { children: "owner email: " }), _jsx(TextInput, { value: state.hub.ownerEmail, onChange: (value) => setState((s) => setHubField(s, "ownerEmail", value)), focused: hubFocus === 1 })] }), _jsxs(Box, { flexDirection: "row", children: [_jsx(Text, { children: "owner password: " }), _jsx(TextInput, { value: state.hub.ownerPassword, onChange: (value) => setState((s) => setHubField(s, "ownerPassword", value)), masked: true, focused: hubFocus === 2 })] }), _jsxs(Box, { flexDirection: "row", children: [_jsx(Text, { children: "port: " }), _jsx(TextInput, { value: state.hub.port, onChange: (value) => {
|
|
200
|
+
portTouchedRef.current = true;
|
|
201
|
+
setState((s) => setHubField(s, "port", value.replace(/\D/g, "")));
|
|
202
|
+
}, focused: hubFocus === 3 })] }), state.errors.ownerEmail && _jsx(Text, { color: theme.error, children: state.errors.ownerEmail }), state.errors.ownerPassword && _jsx(Text, { color: theme.error, children: state.errors.ownerPassword }), state.errors.port && _jsx(Text, { color: theme.error, children: state.errors.port })] }))] })), state.step === "roots" && (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { children: "project roots (comma-separated):" }), _jsx(TextInput, { value: rootsText, onChange: (value) => {
|
|
176
203
|
setRootsText(value);
|
|
177
204
|
setState((s) => setRoots(s, value));
|
|
178
|
-
}, focused: true }), state.errors.roots && _jsx(Text, { color: theme.error, children: state.errors.roots })] })), state.step === "review" && (_jsx(Table, { columns: reviewColumns(mainWidth), rows: state.plan, rowKey: (row) => `${row.kind}-${row.file}`, cell: reviewCell, emptyText: "nothing to change" })), state.step === "apply" && (_jsxs(Box, { flexDirection: "column", children: [state.results.map((result, index) => (_jsx(Text, { children: applyResultLine(result) }, `${result.action.kind}-${index}`))), state.results.length < state.plan.length && _jsx(Text, { dimColor: true, children: "applying\u2026" })] })), state.step === "done" && (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { children: `setup complete: ${state.results.length} change(s) applied` }), _jsx(Text, { dimColor: true, children: "enter open dashboard" })] }))] }) }), _jsx(KeyHints, { hints: hints })] }));
|
|
205
|
+
}, focused: true }), state.errors.roots && _jsx(Text, { color: theme.error, children: state.errors.roots })] })), state.step === "review" && (_jsxs(Box, { flexDirection: "column", children: [_jsx(Table, { columns: reviewColumns(mainWidth), rows: state.plan, rowKey: (row) => `${row.kind}-${row.file}`, cell: reviewCell, emptyText: "nothing to change" }), state.plan.map((action) => action.note !== undefined && _jsx(Text, { children: action.note }, `note-${action.kind}-${action.file}`))] })), state.step === "apply" && (_jsxs(Box, { flexDirection: "column", children: [state.results.map((result, index) => (_jsx(Text, { children: applyResultLine(result) }, `${result.action.kind}-${index}`))), state.results.length < state.plan.length && _jsx(Text, { dimColor: true, children: "applying\u2026" })] })), state.step === "done" && (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { children: `setup complete: ${state.results.length} change(s) applied` }), _jsx(Text, { dimColor: true, children: "enter open dashboard" })] }))] }) }), _jsx(KeyHints, { hints: hints })] }));
|
|
179
206
|
}
|