@sealant/mend 0.15.0 → 0.15.2
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/dist/dashboard-model.js +95 -0
- package/dist/dashboard.js +322 -49
- package/dist/main.js +2 -1
- package/dist/shared.js +6 -0
- package/dist/tui-theme.js +5 -0
- package/package.json +1 -1
package/dist/dashboard-model.js
CHANGED
|
@@ -200,3 +200,98 @@ export const foldGroupStatus = (group) => {
|
|
|
200
200
|
return "idle";
|
|
201
201
|
return statuses[0] ?? "idle";
|
|
202
202
|
};
|
|
203
|
+
// ─── attach + verb helpers ──────────────────────────────────────────────────
|
|
204
|
+
/**
|
|
205
|
+
* Where "get me in" should land when the session has no live agent terminal:
|
|
206
|
+
* the newest LIVE shell, so repeated attaches rejoin the same one instead of
|
|
207
|
+
* stacking a fresh bash per attempt (the five-orphan-shells failure mode).
|
|
208
|
+
* Null = nothing attachable; opening a new shell is then honest.
|
|
209
|
+
*/
|
|
210
|
+
export const liveShellOf = (processes) => processes.findLast((process) => process.kind === "shell" && process.exitedAt === null) ?? null;
|
|
211
|
+
/**
|
|
212
|
+
* The optimistic stop: the row settles AND its live process/service fact
|
|
213
|
+
* lines drop in the same paint — the server's refetch only confirms.
|
|
214
|
+
*/
|
|
215
|
+
export const markSessionStopped = (data, sessionId) => {
|
|
216
|
+
const processesBySession = new Map(data.processesBySession);
|
|
217
|
+
processesBySession.delete(sessionId);
|
|
218
|
+
const servicesBySession = new Map(data.servicesBySession);
|
|
219
|
+
servicesBySession.delete(sessionId);
|
|
220
|
+
return {
|
|
221
|
+
...mapWorkbenchSessions(data, (session) => session.id === sessionId ? { ...session, status: "stopped" } : session),
|
|
222
|
+
processesBySession,
|
|
223
|
+
servicesBySession,
|
|
224
|
+
};
|
|
225
|
+
};
|
|
226
|
+
/** The optimistic removal: the whole group leaves the list before the server answers. */
|
|
227
|
+
export const removeWorktreeGroup = (data, projectId, group) => {
|
|
228
|
+
const memberIds = new Set(group.sessions.map((item) => item.session.id));
|
|
229
|
+
const detail = data.details.get(projectId);
|
|
230
|
+
if (detail === undefined)
|
|
231
|
+
return data;
|
|
232
|
+
const details = new Map(data.details);
|
|
233
|
+
details.set(projectId, {
|
|
234
|
+
...detail,
|
|
235
|
+
sessions: detail.sessions.filter((session) => !memberIds.has(session.id)),
|
|
236
|
+
...(detail.worktrees === undefined
|
|
237
|
+
? {}
|
|
238
|
+
: { worktrees: detail.worktrees.filter((worktree) => worktree.id !== group.id) }),
|
|
239
|
+
});
|
|
240
|
+
return { ...data, details };
|
|
241
|
+
};
|
|
242
|
+
/**
|
|
243
|
+
* Subsequence fuzzy score, fzf-flavored: every query character must appear in
|
|
244
|
+
* order; consecutive hits and segment starts (after `/`, `-`, `_`, `.`) score
|
|
245
|
+
* higher; earlier matches beat later ones. Null = no match. Case-insensitive.
|
|
246
|
+
*/
|
|
247
|
+
export const fuzzyScore = (query, candidate) => {
|
|
248
|
+
if (query === "")
|
|
249
|
+
return 0;
|
|
250
|
+
const q = query.toLowerCase();
|
|
251
|
+
const c = candidate.toLowerCase();
|
|
252
|
+
let score = 0;
|
|
253
|
+
let last = -1;
|
|
254
|
+
for (const char of q) {
|
|
255
|
+
const at = c.indexOf(char, last + 1);
|
|
256
|
+
if (at === -1)
|
|
257
|
+
return null;
|
|
258
|
+
if (at === last + 1)
|
|
259
|
+
score += 3;
|
|
260
|
+
else if (at === 0 || "/-_.".includes(c[at - 1] ?? ""))
|
|
261
|
+
score += 2;
|
|
262
|
+
else
|
|
263
|
+
score += 1;
|
|
264
|
+
score -= (at - last - 1) * 0.01;
|
|
265
|
+
last = at;
|
|
266
|
+
}
|
|
267
|
+
return score;
|
|
268
|
+
};
|
|
269
|
+
/**
|
|
270
|
+
* The picker's rows for one query: fuzzy-filtered, best score first, ties by
|
|
271
|
+
* most recent commit; an empty query lists everything, default branch on top.
|
|
272
|
+
*/
|
|
273
|
+
export const filterBranches = (branches, query) => {
|
|
274
|
+
const scored = branches.flatMap((branch) => {
|
|
275
|
+
const score = fuzzyScore(query, branch.name);
|
|
276
|
+
return score === null ? [] : [{ branch, score }];
|
|
277
|
+
});
|
|
278
|
+
return scored
|
|
279
|
+
.toSorted((a, b) => {
|
|
280
|
+
if (query === "" && a.branch.isDefault !== b.branch.isDefault) {
|
|
281
|
+
return a.branch.isDefault ? -1 : 1;
|
|
282
|
+
}
|
|
283
|
+
if (a.score !== b.score)
|
|
284
|
+
return b.score - a.score;
|
|
285
|
+
return b.branch.committedAt.localeCompare(a.branch.committedAt);
|
|
286
|
+
})
|
|
287
|
+
.map((entry) => entry.branch);
|
|
288
|
+
};
|
|
289
|
+
/** Commit the base step: the highlighted branch (default branch = null base). */
|
|
290
|
+
export const advanceFromBase = (current) => {
|
|
291
|
+
const chosen = filterBranches(current.branches ?? [], current.query)[current.baseIndex];
|
|
292
|
+
return {
|
|
293
|
+
...current,
|
|
294
|
+
base: chosen === undefined || chosen.isDefault ? null : chosen.name,
|
|
295
|
+
step: "harness",
|
|
296
|
+
};
|
|
297
|
+
};
|
package/dist/dashboard.js
CHANGED
|
@@ -3,12 +3,12 @@ import { CliRenderEvents, createCliRenderer } from "@opentui/core";
|
|
|
3
3
|
import { createRoot, useKeyboard, useRenderer, useTerminalDimensions } from "@opentui/react";
|
|
4
4
|
import { QueryClient, QueryClientProvider, useMutation, useQuery, useQueryClient, } from "@tanstack/react-query";
|
|
5
5
|
import { useEffect, useRef, useState } from "react";
|
|
6
|
-
import { deriveHarnesses, deriveProjects, deriveRows, deriveWorktrees, foldGroupStatus, labelIsIdentity, rowKeyOf, worktreeDisplayName, fetchWorkbench, mapWorkbenchSessions, prependSession, removeSession, replaceSession, WORKBENCH_KEY, } from "./dashboard-model.js";
|
|
6
|
+
import { deriveHarnesses, deriveProjects, advanceFromBase, deriveRows, deriveWorktrees, filterBranches, foldGroupStatus, labelIsIdentity, rowKeyOf, worktreeDisplayName, fetchWorkbench, liveShellOf, mapWorkbenchSessions, markSessionStopped, removeWorktreeGroup, prependSession, removeSession, replaceSession, WORKBENCH_KEY, } from "./dashboard-model.js";
|
|
7
7
|
import { reviewTargetForSession } from "./review-workflow.js";
|
|
8
8
|
import { ReviewScreen } from "./review.js";
|
|
9
9
|
import { cwdFacts, HARNESS_COMMANDS, isPendingId, LIVE_STATUSES, matchProjectByCwd, normalizeProjectName, pendingId, } from "./shared.js";
|
|
10
10
|
import { openUrl } from "./terminal.js";
|
|
11
|
-
import { COBALT, FAINT, INK, INK_2, MUTED, RED, RULE, WASH } from "./tui-theme.js";
|
|
11
|
+
import { COBALT, FAINT, INK, INK_2, MUTED, RED, RULE, SURFACE, WASH } from "./tui-theme.js";
|
|
12
12
|
// Near-mono on purpose: a status is a word, and only an observed failure
|
|
13
13
|
// earns color. Live states read at full ink; settled ones recede.
|
|
14
14
|
const STATUS_COLOR = {
|
|
@@ -155,15 +155,24 @@ const App = ({ ctx, onQuit }) => {
|
|
|
155
155
|
const [editing, setEditing] = useState(null);
|
|
156
156
|
/** Session id a stop is armed against; the second press fires it. */
|
|
157
157
|
const [stopArmed, setStopArmed] = useState(null);
|
|
158
|
-
/**
|
|
159
|
-
|
|
160
|
-
|
|
158
|
+
/**
|
|
159
|
+
* The whole worktree creation is ONE modal: name, base (fuzzy over the
|
|
160
|
+
* project's branches), harness — every step visible, the active one
|
|
161
|
+
* expanded. Enter advances, esc steps back (cancels from the name).
|
|
162
|
+
*/
|
|
163
|
+
const [creating, setCreating] = useState(null);
|
|
164
|
+
/**
|
|
165
|
+
* The cwd is a git repo the store doesn't know: offer to adopt it, once
|
|
166
|
+
* per dashboard run. Enter adopts, esc declines and stays quiet.
|
|
167
|
+
*/
|
|
168
|
+
const [adoptOffer, setAdoptOffer] = useState(null);
|
|
169
|
+
const adoptOfferDecided = useRef(false);
|
|
161
170
|
const [reviewing, setReviewing] = useState(null);
|
|
162
171
|
/** Set synchronously around attach/launch so keystrokes can't double-fire. */
|
|
163
172
|
const lockRef = useRef(false);
|
|
164
173
|
/** Latest modal state for mutation callbacks — closures there go stale. */
|
|
165
174
|
const modalRef = useRef(false);
|
|
166
|
-
modalRef.current = editing !== null || reviewing !== null || picker !== null;
|
|
175
|
+
modalRef.current = editing !== null || reviewing !== null || picker !== null || creating !== null;
|
|
167
176
|
const { width: terminalCols, height: terminalRows } = useTerminalDimensions();
|
|
168
177
|
const showProjectsPane = terminalCols >= 72;
|
|
169
178
|
const showDetail = terminalRows >= 18;
|
|
@@ -197,6 +206,26 @@ const App = ({ ctx, onQuit }) => {
|
|
|
197
206
|
const selectSession = (id) => setSessionKey(`s:${id}`);
|
|
198
207
|
const say = (text) => setStatus({ text, at: Date.now() });
|
|
199
208
|
const refetch = () => void queryClient.invalidateQueries({ queryKey: WORKBENCH_KEY });
|
|
209
|
+
// A repo underfoot that the store has never met: raise the adopt offer the
|
|
210
|
+
// first time the workbench answers and no project matches the cwd.
|
|
211
|
+
useEffect(() => {
|
|
212
|
+
if (data === undefined || adoptOfferDecided.current)
|
|
213
|
+
return;
|
|
214
|
+
if (homeProject !== undefined)
|
|
215
|
+
return;
|
|
216
|
+
const facts = cwdFacts(ctx.cwd);
|
|
217
|
+
if (facts.repoRoot === null)
|
|
218
|
+
return;
|
|
219
|
+
adoptOfferDecided.current = true;
|
|
220
|
+
setAdoptOffer({
|
|
221
|
+
root: facts.repoRoot,
|
|
222
|
+
source: facts.originUrl ?? facts.repoRoot,
|
|
223
|
+
name: normalizeProjectName(facts.repoRoot.split("/").at(-1) ?? "project"),
|
|
224
|
+
modeIndex: 0,
|
|
225
|
+
});
|
|
226
|
+
// homeProject/data identity is enough; ctx.cwd never changes in a run.
|
|
227
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
228
|
+
}, [data, homeProject]);
|
|
200
229
|
// Keep each pane's selection on screen — the one imperative escape hatch.
|
|
201
230
|
const projectScrollRef = useRef(null);
|
|
202
231
|
const sessionScrollRef = useRef(null);
|
|
@@ -227,11 +256,21 @@ const App = ({ ctx, onQuit }) => {
|
|
|
227
256
|
outcome = await ctx.attachTty(session.id, session.harness);
|
|
228
257
|
if (outcome === "unavailable") {
|
|
229
258
|
// A live session whose terminal ended (idle: a workspace held open,
|
|
230
|
-
// no PTY behind it). Enter still means "get me in" —
|
|
231
|
-
//
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
259
|
+
// no PTY behind it). Enter still means "get me in" — REJOIN the shell
|
|
260
|
+
// already holding the workspace when one is live; only open a fresh
|
|
261
|
+
// one when nothing is attachable (stacking a new bash per attempt is
|
|
262
|
+
// how a session ends up held open by orphan shells).
|
|
263
|
+
const detail = await ctx.api("GET", `/sessions/${session.id}`);
|
|
264
|
+
const existing = liveShellOf(detail.processes);
|
|
265
|
+
if (existing !== null) {
|
|
266
|
+
process.stdout.write(`no live terminal — rejoining the open shell\n\n`);
|
|
267
|
+
outcome = await ctx.attachTty(session.id, "shell", existing.id);
|
|
268
|
+
}
|
|
269
|
+
else {
|
|
270
|
+
const shell = await ctx.api("POST", `/sessions/${session.id}/shell`);
|
|
271
|
+
process.stdout.write(`no live terminal — opened a shell in the workspace\n\n`);
|
|
272
|
+
outcome = await ctx.attachTty(session.id, "shell", shell.id);
|
|
273
|
+
}
|
|
235
274
|
}
|
|
236
275
|
}
|
|
237
276
|
catch (error) {
|
|
@@ -283,7 +322,7 @@ const App = ({ ctx, onQuit }) => {
|
|
|
283
322
|
harness: vars.harness,
|
|
284
323
|
label: null,
|
|
285
324
|
name: vars.name,
|
|
286
|
-
base:
|
|
325
|
+
base: vars.base,
|
|
287
326
|
});
|
|
288
327
|
await ctx.api("POST", `/sessions/${session.id}/launch`, { argv });
|
|
289
328
|
return session;
|
|
@@ -296,7 +335,7 @@ const App = ({ ctx, onQuit }) => {
|
|
|
296
335
|
label: null,
|
|
297
336
|
branch: vars.name === null ? "provisioning…" : `mend/${vars.name}`,
|
|
298
337
|
baseSha: "",
|
|
299
|
-
baseRef:
|
|
338
|
+
baseRef: vars.base,
|
|
300
339
|
status: "starting",
|
|
301
340
|
summary: null,
|
|
302
341
|
createdAt: new Date().toISOString(),
|
|
@@ -342,6 +381,46 @@ const App = ({ ctx, onQuit }) => {
|
|
|
342
381
|
},
|
|
343
382
|
onSettled: settleRefetch,
|
|
344
383
|
});
|
|
384
|
+
const ADOPT_AUTH_MODES = [
|
|
385
|
+
{
|
|
386
|
+
mode: "ambient",
|
|
387
|
+
label: "ambient",
|
|
388
|
+
hint: "the server machine's own git/ssh setup",
|
|
389
|
+
},
|
|
390
|
+
{
|
|
391
|
+
mode: "mend-key",
|
|
392
|
+
label: "mend-key",
|
|
393
|
+
hint: "the machine's Mend deploy key — add its public half on the git host",
|
|
394
|
+
},
|
|
395
|
+
{
|
|
396
|
+
mode: "bridge",
|
|
397
|
+
label: "bridge",
|
|
398
|
+
hint: "your OWN ssh-agent, relayed while `mend keys share` runs here",
|
|
399
|
+
},
|
|
400
|
+
];
|
|
401
|
+
const adoptMutation = useMutation({
|
|
402
|
+
mutationFn: (offer) => ctx.api("POST", "/projects", {
|
|
403
|
+
name: offer.name,
|
|
404
|
+
source: offer.source,
|
|
405
|
+
gitAuthMode: ADOPT_AUTH_MODES[offer.modeIndex]?.mode ?? "ambient",
|
|
406
|
+
}),
|
|
407
|
+
onMutate: (offer) => {
|
|
408
|
+
setAdoptOffer(null);
|
|
409
|
+
setBusy(`adopting ${offer.name} — cloning into the store ·`);
|
|
410
|
+
setBusyStarted(Date.now());
|
|
411
|
+
},
|
|
412
|
+
onError: (error) => {
|
|
413
|
+
setBusy(null);
|
|
414
|
+
say(errorText(error));
|
|
415
|
+
},
|
|
416
|
+
onSuccess: (project) => {
|
|
417
|
+
setBusy(null);
|
|
418
|
+
say(`adopted · ${project.name} — n starts a worktree`);
|
|
419
|
+
setProjectKey(project.id);
|
|
420
|
+
setSessionKey(null);
|
|
421
|
+
},
|
|
422
|
+
onSettled: settleRefetch,
|
|
423
|
+
});
|
|
345
424
|
// The label lands in the row before the server answers; an error puts the
|
|
346
425
|
// truth back on the next refetch.
|
|
347
426
|
const renameMutation = useMutation({
|
|
@@ -365,7 +444,8 @@ const App = ({ ctx, onQuit }) => {
|
|
|
365
444
|
mutationFn: (session) => ctx.api("POST", `/sessions/${session.id}/stop`),
|
|
366
445
|
onMutate: async (session) => {
|
|
367
446
|
await queryClient.cancelQueries({ queryKey: WORKBENCH_KEY });
|
|
368
|
-
|
|
447
|
+
// The row settles and its live process/service facts drop in one paint.
|
|
448
|
+
patchWorkbench((current) => markSessionStopped(current, session.id));
|
|
369
449
|
say(`stopped · ${worktreeDisplayName(session)} — the record and review remain`);
|
|
370
450
|
},
|
|
371
451
|
onError: (error) => {
|
|
@@ -424,7 +504,13 @@ const App = ({ ctx, onQuit }) => {
|
|
|
424
504
|
mutationFn: (group) => group.id !== null
|
|
425
505
|
? ctx.api("DELETE", `/worktrees/${group.id}`)
|
|
426
506
|
: ctx.api("DELETE", `/sessions/${group.sessions[0]?.session.id ?? ""}`),
|
|
427
|
-
onMutate: (group) => {
|
|
507
|
+
onMutate: async (group) => {
|
|
508
|
+
await queryClient.cancelQueries({ queryKey: WORKBENCH_KEY });
|
|
509
|
+
const projectId = selectedProject?.project.id;
|
|
510
|
+
// The group leaves the list before the server answers; an error refetches truth.
|
|
511
|
+
if (projectId !== undefined) {
|
|
512
|
+
patchWorkbench((current) => removeWorktreeGroup(current, projectId, group));
|
|
513
|
+
}
|
|
428
514
|
say(`removing worktree · ${group.name}`);
|
|
429
515
|
},
|
|
430
516
|
onError: (error) => {
|
|
@@ -492,17 +578,79 @@ const App = ({ ctx, onQuit }) => {
|
|
|
492
578
|
setStopArmed(item.session.id);
|
|
493
579
|
say(`press again to stop · ${worktreeDisplayName(item.session)}`);
|
|
494
580
|
};
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
581
|
+
/** Open the creation modal; the branch list starts loading immediately. */
|
|
582
|
+
const openCreateModal = (projectId) => {
|
|
583
|
+
setCreating({
|
|
584
|
+
projectId,
|
|
585
|
+
step: "name",
|
|
586
|
+
name: "",
|
|
587
|
+
branches: null,
|
|
588
|
+
query: "",
|
|
589
|
+
baseIndex: 0,
|
|
590
|
+
base: null,
|
|
591
|
+
joins: false,
|
|
592
|
+
harnessIndex: 0,
|
|
593
|
+
});
|
|
594
|
+
// The checkout mend ran in names its branch; creating in THAT project
|
|
595
|
+
// prefills the base with it — the list highlights it once loaded.
|
|
596
|
+
const prefill = projectId === homeProject?.id ? ctx.cwdBranch : null;
|
|
597
|
+
void ctx.api("GET", `/projects/${projectId}/branches`).then((branches) => setCreating((current) => {
|
|
598
|
+
if (current === null)
|
|
599
|
+
return current;
|
|
600
|
+
const match = prefill === null ? undefined : branches.find((candidate) => candidate.name === prefill);
|
|
601
|
+
const ordered = filterBranches(branches, "");
|
|
602
|
+
return {
|
|
603
|
+
...current,
|
|
604
|
+
branches,
|
|
605
|
+
base: match === undefined || match.isDefault ? null : match.name,
|
|
606
|
+
baseIndex: match === undefined
|
|
607
|
+
? 0
|
|
608
|
+
: Math.max(0, ordered.findIndex((candidate) => candidate.name === match.name)),
|
|
609
|
+
};
|
|
610
|
+
}), () =>
|
|
611
|
+
// An unreadable list falls back to the default base; the base step
|
|
612
|
+
// then shows only the default and enter moves on.
|
|
613
|
+
setCreating((current) => (current === null ? current : { ...current, branches: [] })));
|
|
614
|
+
};
|
|
615
|
+
/** Commit the name: an existing name JOINS its worktree — base is fixed. */
|
|
616
|
+
const advanceFromName = (current, raw) => {
|
|
617
|
+
const slug = normalizeProjectName(raw.trim());
|
|
618
|
+
const name = raw.trim() === "" ? "" : slug;
|
|
619
|
+
const joins = name !== "" &&
|
|
620
|
+
(workbench()?.details.get(current.projectId)?.worktrees ?? []).some((candidate) => candidate.name === name);
|
|
621
|
+
// A join fixes the base; otherwise whatever is chosen (the checkout
|
|
622
|
+
// prefill included) rides along untouched.
|
|
623
|
+
return {
|
|
624
|
+
...current,
|
|
625
|
+
name,
|
|
626
|
+
joins,
|
|
627
|
+
...(joins ? { base: null } : {}),
|
|
628
|
+
step: joins ? "harness" : "base",
|
|
629
|
+
};
|
|
630
|
+
};
|
|
631
|
+
const submitCreateName = (value) => {
|
|
632
|
+
setCreating((current) => (current === null ? current : advanceFromName(current, value)));
|
|
499
633
|
};
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
634
|
+
const submitCreateBase = () => {
|
|
635
|
+
setCreating((current) => (current === null ? current : advanceFromBase(current)));
|
|
636
|
+
};
|
|
637
|
+
/** Enter on the harness: everything is chosen — launch. */
|
|
638
|
+
const submitCreateHarness = () => {
|
|
639
|
+
const current = creating;
|
|
640
|
+
if (current === null)
|
|
641
|
+
return;
|
|
642
|
+
const choice = deriveHarnesses(null)[current.harnessIndex];
|
|
643
|
+
if (choice?.harness == null)
|
|
644
|
+
return;
|
|
645
|
+
setCreating(null);
|
|
646
|
+
setFocus("sessions");
|
|
647
|
+
launchMutation.mutate({
|
|
648
|
+
projectId: current.projectId,
|
|
649
|
+
harness: choice.harness,
|
|
650
|
+
name: current.name === "" ? null : current.name,
|
|
651
|
+
base: current.base,
|
|
652
|
+
pendingKey: pendingId(),
|
|
653
|
+
});
|
|
506
654
|
};
|
|
507
655
|
const resumeSession = (projectId, session, harness) => {
|
|
508
656
|
setFocus("sessions");
|
|
@@ -558,11 +706,7 @@ const App = ({ ctx, onQuit }) => {
|
|
|
558
706
|
});
|
|
559
707
|
}
|
|
560
708
|
}
|
|
561
|
-
else if (picker.session
|
|
562
|
-
if (choice.harness !== null)
|
|
563
|
-
startSession(projectId, choice.harness);
|
|
564
|
-
}
|
|
565
|
-
else {
|
|
709
|
+
else if (picker.session !== null) {
|
|
566
710
|
resumeSession(projectId, picker.session, choice.harness);
|
|
567
711
|
}
|
|
568
712
|
return;
|
|
@@ -603,15 +747,109 @@ const App = ({ ctx, onQuit }) => {
|
|
|
603
747
|
return;
|
|
604
748
|
if (key.ctrl && key.name === "c")
|
|
605
749
|
return onQuit();
|
|
750
|
+
if (adoptOffer !== null) {
|
|
751
|
+
if (key.name === "return" || key.name === "linefeed" || key.name === "y") {
|
|
752
|
+
adoptMutation.mutate(adoptOffer);
|
|
753
|
+
}
|
|
754
|
+
else if (key.name === "escape" || key.name === "q") {
|
|
755
|
+
setAdoptOffer(null);
|
|
756
|
+
say("not adopted — mend adopt does it any time");
|
|
757
|
+
}
|
|
758
|
+
else if (key.name === "right" ||
|
|
759
|
+
key.name === "l" ||
|
|
760
|
+
key.name === "tab" ||
|
|
761
|
+
key.name === "left" ||
|
|
762
|
+
key.name === "h") {
|
|
763
|
+
const delta = key.name === "left" || key.name === "h" ? -1 : 1;
|
|
764
|
+
setAdoptOffer((current) => current === null
|
|
765
|
+
? current
|
|
766
|
+
: {
|
|
767
|
+
...current,
|
|
768
|
+
modeIndex: (current.modeIndex + delta + ADOPT_AUTH_MODES.length) % ADOPT_AUTH_MODES.length,
|
|
769
|
+
});
|
|
770
|
+
}
|
|
771
|
+
return;
|
|
772
|
+
}
|
|
606
773
|
if (editing !== null) {
|
|
607
774
|
// The input owns the keyboard; only escape leaves without saving.
|
|
608
775
|
if (key.name === "escape")
|
|
609
776
|
setEditing(null);
|
|
610
777
|
return;
|
|
611
778
|
}
|
|
612
|
-
if (
|
|
613
|
-
|
|
614
|
-
|
|
779
|
+
if (creating !== null) {
|
|
780
|
+
// Inputs own the characters (name text, base filter); this handler owns
|
|
781
|
+
// the arrows, esc (a step back; cancel from the name), and the harness
|
|
782
|
+
// step's enter. The name and base enters land through their input's
|
|
783
|
+
// submit so typed-but-unflushed text never races.
|
|
784
|
+
if (key.name === "escape") {
|
|
785
|
+
setCreating((current) => {
|
|
786
|
+
if (current === null)
|
|
787
|
+
return current;
|
|
788
|
+
if (current.step === "harness") {
|
|
789
|
+
return { ...current, step: current.joins ? "name" : "base" };
|
|
790
|
+
}
|
|
791
|
+
if (current.step === "base")
|
|
792
|
+
return { ...current, step: "name" };
|
|
793
|
+
say("new worktree cancelled");
|
|
794
|
+
return null;
|
|
795
|
+
});
|
|
796
|
+
return;
|
|
797
|
+
}
|
|
798
|
+
// Tab walks the steps without the enter ceremony: forward commits the
|
|
799
|
+
// active field (the mirrored name, the highlighted branch), shift+tab
|
|
800
|
+
// steps back, and from the harness it wraps around to the name.
|
|
801
|
+
if (key.name === "tab" || key.name === "backtab") {
|
|
802
|
+
const backward = key.name === "backtab" || key.shift === true;
|
|
803
|
+
setCreating((current) => {
|
|
804
|
+
if (current === null)
|
|
805
|
+
return current;
|
|
806
|
+
if (backward) {
|
|
807
|
+
if (current.step === "harness") {
|
|
808
|
+
return { ...current, step: current.joins ? "name" : "base" };
|
|
809
|
+
}
|
|
810
|
+
if (current.step === "base")
|
|
811
|
+
return { ...current, step: "name" };
|
|
812
|
+
return current;
|
|
813
|
+
}
|
|
814
|
+
if (current.step === "name")
|
|
815
|
+
return advanceFromName(current, current.name);
|
|
816
|
+
if (current.step === "base")
|
|
817
|
+
return advanceFromBase(current);
|
|
818
|
+
return { ...current, step: "name" };
|
|
819
|
+
});
|
|
820
|
+
return;
|
|
821
|
+
}
|
|
822
|
+
const delta = key.name === "down" || (key.ctrl === true && key.name === "n")
|
|
823
|
+
? 1
|
|
824
|
+
: key.name === "up" || (key.ctrl === true && key.name === "p")
|
|
825
|
+
? -1
|
|
826
|
+
: 0;
|
|
827
|
+
if (delta !== 0) {
|
|
828
|
+
setCreating((current) => {
|
|
829
|
+
if (current === null)
|
|
830
|
+
return current;
|
|
831
|
+
if (current.step === "base") {
|
|
832
|
+
const matches = filterBranches(current.branches ?? [], current.query).length;
|
|
833
|
+
return {
|
|
834
|
+
...current,
|
|
835
|
+
baseIndex: Math.max(0, Math.min(matches - 1, current.baseIndex + delta)),
|
|
836
|
+
};
|
|
837
|
+
}
|
|
838
|
+
if (current.step === "harness") {
|
|
839
|
+
const count = deriveHarnesses(null).length;
|
|
840
|
+
return {
|
|
841
|
+
...current,
|
|
842
|
+
harnessIndex: Math.max(0, Math.min(count - 1, current.harnessIndex + delta)),
|
|
843
|
+
};
|
|
844
|
+
}
|
|
845
|
+
return current;
|
|
846
|
+
});
|
|
847
|
+
return;
|
|
848
|
+
}
|
|
849
|
+
if (creating.step === "harness" &&
|
|
850
|
+
(key.name === "return" || key.name === "linefeed" || key.name === "l")) {
|
|
851
|
+
submitCreateHarness();
|
|
852
|
+
}
|
|
615
853
|
return;
|
|
616
854
|
}
|
|
617
855
|
if (picker !== null) {
|
|
@@ -672,9 +910,9 @@ const App = ({ ctx, onQuit }) => {
|
|
|
672
910
|
setFocus(focus === "projects" ? "sessions" : "projects");
|
|
673
911
|
return;
|
|
674
912
|
case "n":
|
|
675
|
-
// The
|
|
913
|
+
// The whole creation is one modal: name, base, harness.
|
|
676
914
|
if (selectedProject !== null)
|
|
677
|
-
|
|
915
|
+
openCreateModal(selectedProject.project.id);
|
|
678
916
|
return;
|
|
679
917
|
case "s": {
|
|
680
918
|
// A new conversation inside the selected worktree.
|
|
@@ -742,15 +980,24 @@ const App = ({ ctx, onQuit }) => {
|
|
|
742
980
|
: picker.session === null
|
|
743
981
|
? "new session — pick a harness"
|
|
744
982
|
: `resume ${picker.session.id.slice(0, 8)} — pick a harness`;
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
983
|
+
// One big fixed-size modal: every step visible at once, nothing shifts as
|
|
984
|
+
// focus moves through name → base → harness.
|
|
985
|
+
const creatingHeight = 2 + 1 + 1 + 6 + 1 + deriveHarnesses(null).length;
|
|
986
|
+
const footerText = adoptOffer !== null
|
|
987
|
+
? " enter adopt · ←→ auth mode · esc not now"
|
|
988
|
+
: editing !== null
|
|
989
|
+
? " enter save · esc cancel"
|
|
990
|
+
: creating !== null
|
|
991
|
+
? creating.step === "name"
|
|
992
|
+
? " enter continue · esc cancel"
|
|
993
|
+
: creating.step === "base"
|
|
994
|
+
? " type to filter · ↑↓ move · enter choose base · esc back"
|
|
995
|
+
: " ↑↓ move · enter launch · esc back"
|
|
996
|
+
: picker !== null
|
|
997
|
+
? " ↑↓ move · enter start · esc cancel"
|
|
998
|
+
: focus === "projects"
|
|
999
|
+
? " ↑↓ move · enter/l sessions · ⇥ panes · r refresh · q quit"
|
|
1000
|
+
: " ↑↓ move · enter attach/resume · n new worktree · s session here · x stop · ⇧D remove · v review · e rename · o web · q quit";
|
|
754
1001
|
const loadFailure = data === undefined && failureReason !== null
|
|
755
1002
|
? failureReason instanceof Error
|
|
756
1003
|
? failureReason.message
|
|
@@ -762,13 +1009,39 @@ const App = ({ ctx, onQuit }) => {
|
|
|
762
1009
|
refetch();
|
|
763
1010
|
}, onQuit: onQuit }));
|
|
764
1011
|
}
|
|
765
|
-
return (_jsxs("box", { flexGrow: 1, flexDirection: "column", backgroundColor: "transparent", children: [_jsxs("box", { height: 1, flexDirection: "row", justifyContent: "space-between", children: [_jsxs("text", { height: 1, bg: "transparent", children: [_jsx("span", { fg: INK, children: " mend" }), _jsxs("span", { fg: MUTED, children: [" ", projectItems.length, " project", projectItems.length === 1 ? "" : "s"] }), _jsx("span", { fg: FAINT, children: " \u00B7 " }), _jsxs("span", { fg: liveTotal > 0 ? MUTED : FAINT, children: [liveTotal, " live"] })] }), _jsx("text", { height: 1, fg: FAINT, bg: "transparent", children: `${ctx.config.url} ` })] }), _jsxs("box", { flexGrow: 1, flexShrink: 1, minHeight: 0, flexDirection: "row", children: [showProjectsPane ? (_jsx(Pane, { title: "projects", focused: focus === "projects", width: PROJECTS_PANE_WIDTH, children: _jsxs("scrollbox", { ref: projectScrollRef, flexGrow: 1, flexShrink: 1, minHeight: 0, style: paneScrollStyle, children: [projectItems.map((item, index) => (_jsx(ProjectRow, { item: item, selected: index === projectIndex, nameWidth: nameWidth }, item.project.id))), data !== undefined && projectItems.length === 0 ? (_jsx("text", { height: 1, fg: FAINT, bg: "transparent", children: " none adopted yet" })) : null] }) })) : null, _jsx(Pane, { title: sessionsTitle, focused: focus === "sessions", grow: true, children: _jsxs("scrollbox", { ref: sessionScrollRef, flexGrow: 1, flexShrink: 1, minHeight: 0, style: paneScrollStyle, children: [rows.map((row, index) => row.kind === "worktree" ? (_jsx(WorktreeHeaderRow, { group: row.group, selected: index === rowIndex }, rowKeyOf(row))) : row.group.sessions.length === 1 ? (_jsx(CombinedRow, { group: row.group, item: row.item, selected: index === rowIndex }, rowKeyOf(row))) : (_jsx(SessionChildRow, { item: row.item, selected: index === rowIndex }, rowKeyOf(row)))), data !== undefined && rows.length === 0 ? (_jsx("text", { height: 1, fg: FAINT, bg: "transparent", children: " no sessions yet — n starts one" })) : null, loadFailure !== null ? (_jsx("text", { height: 1, fg: INK_2, bg: "transparent", children: ` ${loadFailure} — retrying` })) : null] }) })] }), picker !== null ? (_jsx(Pane, { title: pickerTitle, focused: true, height: 2 + pickerItems.length, children: pickerItems.map((item, index) => (_jsx(HarnessRow, { item: item, selected: index === pickerIndex }, String(item.harness)))) })) :
|
|
766
|
-
if (typeof value === "string")
|
|
767
|
-
submitWorktreeName(value);
|
|
768
|
-
} }) })) : editing !== null ? (_jsx("box", { border: true, borderStyle: "rounded", borderColor: COBALT, title: ` label — ${editing.harness} ${editing.id.slice(0, 8)} `, titleAlignment: "left", backgroundColor: "transparent", height: 3, flexShrink: 0, children: _jsx("input", { focused: true, value: editing.label ?? "", placeholder: "a few words for what this session is doing (empty clears)", backgroundColor: "transparent", focusedBackgroundColor: "transparent", textColor: INK, focusedTextColor: INK, placeholderColor: FAINT, cursorColor: INK, flexGrow: 1, onSubmit: (value) => {
|
|
1012
|
+
return (_jsxs("box", { flexGrow: 1, flexDirection: "column", backgroundColor: "transparent", children: [_jsxs("box", { height: 1, flexDirection: "row", justifyContent: "space-between", children: [_jsxs("text", { height: 1, bg: "transparent", children: [_jsx("span", { fg: INK, children: " mend" }), _jsxs("span", { fg: MUTED, children: [" ", projectItems.length, " project", projectItems.length === 1 ? "" : "s"] }), _jsx("span", { fg: FAINT, children: " \u00B7 " }), _jsxs("span", { fg: liveTotal > 0 ? MUTED : FAINT, children: [liveTotal, " live"] })] }), _jsx("text", { height: 1, fg: FAINT, bg: "transparent", children: `${ctx.config.url} ` })] }), _jsxs("box", { flexGrow: 1, flexShrink: 1, minHeight: 0, flexDirection: "row", children: [showProjectsPane ? (_jsx(Pane, { title: "projects", focused: focus === "projects", width: PROJECTS_PANE_WIDTH, children: _jsxs("scrollbox", { ref: projectScrollRef, flexGrow: 1, flexShrink: 1, minHeight: 0, style: paneScrollStyle, children: [projectItems.map((item, index) => (_jsx(ProjectRow, { item: item, selected: index === projectIndex, nameWidth: nameWidth }, item.project.id))), data !== undefined && projectItems.length === 0 ? (_jsx("text", { height: 1, fg: FAINT, bg: "transparent", children: " none adopted yet" })) : null] }) })) : null, _jsx(Pane, { title: sessionsTitle, focused: focus === "sessions", grow: true, children: _jsxs("scrollbox", { ref: sessionScrollRef, flexGrow: 1, flexShrink: 1, minHeight: 0, style: paneScrollStyle, children: [rows.map((row, index) => row.kind === "worktree" ? (_jsx(WorktreeHeaderRow, { group: row.group, selected: index === rowIndex }, rowKeyOf(row))) : row.group.sessions.length === 1 ? (_jsx(CombinedRow, { group: row.group, item: row.item, selected: index === rowIndex }, rowKeyOf(row))) : (_jsx(SessionChildRow, { item: row.item, selected: index === rowIndex }, rowKeyOf(row)))), data !== undefined && rows.length === 0 ? (_jsx("text", { height: 1, fg: FAINT, bg: "transparent", children: " no sessions yet — n starts one" })) : null, loadFailure !== null ? (_jsx("text", { height: 1, fg: INK_2, bg: "transparent", children: ` ${loadFailure} — retrying` })) : null] }) })] }), picker !== null ? (_jsx(Pane, { title: pickerTitle, focused: true, height: 2 + pickerItems.length, children: pickerItems.map((item, index) => (_jsx(HarnessRow, { item: item, selected: index === pickerIndex }, String(item.harness)))) })) : editing !== null ? (_jsx("box", { border: true, borderStyle: "rounded", borderColor: COBALT, title: ` label — ${editing.harness} ${editing.id.slice(0, 8)} `, titleAlignment: "left", backgroundColor: "transparent", height: 3, flexShrink: 0, children: _jsx("input", { focused: true, value: editing.label ?? "", placeholder: "a few words for what this session is doing (empty clears)", backgroundColor: "transparent", focusedBackgroundColor: "transparent", textColor: INK, focusedTextColor: INK, placeholderColor: FAINT, cursorColor: INK, flexGrow: 1, onSubmit: (value) => {
|
|
769
1013
|
if (typeof value === "string")
|
|
770
1014
|
submitRename(editing, value);
|
|
771
|
-
} }) })) : showDetail ? (_jsx(Pane, { title: detailTitle, focused: false, height: 6, children: _jsx(SessionDetail, { item: selectedSession }) })) : null, _jsx(
|
|
1015
|
+
} }) })) : showDetail ? (_jsx(Pane, { title: detailTitle, focused: false, height: 6, children: _jsx(SessionDetail, { item: selectedSession }) })) : null, adoptOffer !== null ? (_jsxs("box", { position: "absolute", zIndex: 11, left: Math.max(1, Math.floor((terminalCols - Math.min(70, terminalCols - 4)) / 2)), top: Math.max(1, Math.floor((terminalRows - 7) / 2)), width: Math.min(70, terminalCols - 4), height: 7, border: true, borderStyle: "rounded", borderColor: COBALT, title: " adopt this repository? ", titleAlignment: "left", backgroundColor: SURFACE, flexDirection: "column", children: [_jsxs("text", { height: 1, bg: SURFACE, children: [_jsx("span", { children: " " }), _jsx("span", { fg: INK, children: adoptOffer.name }), _jsx("span", { fg: FAINT, children: " · not in the store yet" })] }), _jsx("text", { height: 1, bg: SURFACE, fg: MUTED, children: ` ${adoptOffer.source}` }), _jsxs("text", { height: 1, bg: SURFACE, children: [_jsx("span", { fg: FAINT, children: " auth " }), ADOPT_AUTH_MODES.map((candidate, index) => (_jsxs("span", { children: [index > 0 ? _jsx("span", { fg: FAINT, children: " · " }) : null, _jsx("span", { fg: index === adoptOffer.modeIndex ? COBALT : FAINT, children: index === adoptOffer.modeIndex ? `▸ ${candidate.label}` : candidate.label })] }, candidate.mode)))] }), _jsx("text", { height: 1, bg: SURFACE, fg: FAINT, children: ` ${ADOPT_AUTH_MODES[adoptOffer.modeIndex]?.hint ?? ""}` }), _jsx("text", { height: 1, bg: SURFACE, fg: FAINT, children: adoptOffer.source === adoptOffer.root
|
|
1016
|
+
? " enter adopt from this path — needs the server on THIS machine · esc not now"
|
|
1017
|
+
: " enter adopt · ←→ auth mode · esc not now" })] })) : null, creating !== null ? (_jsxs("box", { position: "absolute", zIndex: 10, left: Math.max(1, Math.floor((terminalCols - Math.min(74, terminalCols - 4)) / 2)), top: Math.max(1, Math.floor((terminalRows - creatingHeight) / 2)), width: Math.min(74, terminalCols - 4), height: creatingHeight, border: true, borderStyle: "rounded", borderColor: COBALT, title: ` new worktree — ${selectedProject?.project.name ?? "project"} `, titleAlignment: "left", backgroundColor: SURFACE, flexDirection: "column", children: [_jsxs("box", { height: 1, flexShrink: 0, flexDirection: "row", backgroundColor: SURFACE, children: [_jsxs("text", { height: 1, bg: SURFACE, children: [_jsx(Gutter, { selected: creating.step === "name" }), _jsx("span", { fg: FAINT, children: "name " }), creating.step !== "name" ? (_jsx("span", { fg: INK, children: creating.name === "" ? "auto" : creating.name })) : null, creating.joins ? _jsx("span", { fg: FAINT, children: " · joins the existing worktree" }) : null] }), creating.step === "name" ? (_jsx("input", { focused: true, value: creating.name, placeholder: "e.g. fix-auth (empty = auto \u00B7 an existing name joins it)", backgroundColor: SURFACE, focusedBackgroundColor: SURFACE, textColor: INK, focusedTextColor: INK, placeholderColor: FAINT, cursorColor: INK, flexGrow: 1, onInput: (value) => {
|
|
1018
|
+
const clean = value.replace(/\t/g, "");
|
|
1019
|
+
setCreating((current) => current === null ? current : { ...current, name: clean });
|
|
1020
|
+
}, onSubmit: (value) => {
|
|
1021
|
+
if (typeof value === "string")
|
|
1022
|
+
submitCreateName(value);
|
|
1023
|
+
} })) : null] }), _jsxs("box", { height: 1, flexShrink: 0, flexDirection: "row", backgroundColor: SURFACE, children: [_jsxs("text", { height: 1, bg: SURFACE, children: [_jsx(Gutter, { selected: creating.step === "base" }), _jsx("span", { fg: FAINT, children: "base " }), creating.step !== "base" ? (_jsx("span", { fg: creating.step === "name" && !creating.joins ? FAINT : INK, children: creating.joins
|
|
1024
|
+
? "fixed by the existing worktree"
|
|
1025
|
+
: (creating.base ?? "default branch") })) : null] }), creating.step === "base" ? (creating.branches === null ? (_jsx("text", { height: 1, bg: SURFACE, fg: FAINT, children: "reading branches\u2026" })) : (_jsx("input", { focused: true, value: "", placeholder: "type to filter (enter = highlighted; empty = default)", backgroundColor: SURFACE, focusedBackgroundColor: SURFACE, textColor: INK, focusedTextColor: INK, placeholderColor: FAINT, cursorColor: INK, flexGrow: 1, onInput: (value) => {
|
|
1026
|
+
const clean = value.replace(/\t/g, "");
|
|
1027
|
+
setCreating((current) => current === null ? current : { ...current, query: clean, baseIndex: 0 });
|
|
1028
|
+
}, onSubmit: () => submitCreateBase() }))) : null] }), Array.from({ length: 6 }, (_, index) => {
|
|
1029
|
+
const branch = creating.joins || creating.branches === null
|
|
1030
|
+
? undefined
|
|
1031
|
+
: filterBranches(creating.branches, creating.query)[index];
|
|
1032
|
+
const active = creating.step === "base";
|
|
1033
|
+
if (branch === undefined) {
|
|
1034
|
+
return (_jsx("text", { height: 1, bg: SURFACE, fg: FAINT, children: index === 0 && active && creating.branches !== null
|
|
1035
|
+
? " no branch matches — enter uses the default"
|
|
1036
|
+
: " " }, `slot-${index}`));
|
|
1037
|
+
}
|
|
1038
|
+
const highlighted = active && index === creating.baseIndex;
|
|
1039
|
+
return (_jsx("box", { height: 1, flexShrink: 0, backgroundColor: highlighted ? WASH : SURFACE, children: _jsxs("text", { height: 1, bg: highlighted ? WASH : SURFACE, children: [_jsx("span", { fg: FAINT, children: " " }), _jsx(Gutter, { selected: highlighted }), _jsx("span", { fg: active ? INK : FAINT, children: branch.name.slice(0, 40).padEnd(41) }), _jsx("span", { fg: FAINT, children: branch.sha.slice(0, 8).padEnd(10) }), _jsx("span", { fg: active ? MUTED : FAINT, children: timeAgo(branch.committedAt).replace(" ago", "").padEnd(6) }), branch.isDefault ? _jsx("span", { fg: FAINT, children: "default" }) : null] }) }, branch.name));
|
|
1040
|
+
}), _jsxs("text", { height: 1, bg: SURFACE, children: [_jsx(Gutter, { selected: creating.step === "harness" }), _jsx("span", { fg: FAINT, children: "harness" })] }), deriveHarnesses(null).map((item, index) => {
|
|
1041
|
+
const active = creating.step === "harness";
|
|
1042
|
+
const highlighted = active && index === creating.harnessIndex;
|
|
1043
|
+
return (_jsx("box", { height: 1, flexShrink: 0, backgroundColor: highlighted ? WASH : SURFACE, children: _jsxs("text", { height: 1, bg: highlighted ? WASH : SURFACE, children: [_jsx("span", { fg: FAINT, children: " " }), _jsx(Gutter, { selected: highlighted }), _jsx("span", { fg: active ? INK : FAINT, children: item.label.padEnd(12) }), _jsx("span", { fg: FAINT, children: active ? item.hint : "" })] }) }, String(item.harness)));
|
|
1044
|
+
})] })) : null, _jsx(StatusLine, { busy: busy, busyStarted: busyStarted, status: status }), _jsx("text", { height: 1, fg: FAINT, bg: "transparent", children: footerText })] }));
|
|
772
1045
|
};
|
|
773
1046
|
// ─── entry ──────────────────────────────────────────────────────────────────
|
|
774
1047
|
export const runDashboard = async (ctx) => {
|
package/dist/main.js
CHANGED
|
@@ -11,7 +11,7 @@ import { formatLoadReport } from "./env.js";
|
|
|
11
11
|
import { loginCommand } from "./login.js";
|
|
12
12
|
import { pairCommand, qrCommand } from "./pair.js";
|
|
13
13
|
import { isComposeFile, proposeFromCompose, proposeFromPackageJson, proposeFromWorkspacePackage, renderMendToml, workspaceGlobs, } from "./service-init.js";
|
|
14
|
-
import { agentIsLive, agentOutcome, cwdFacts, gitTopLevel, HARNESS_COMMANDS, isDetachChunk, LIVE_STATUSES, sessionDisplayName, matchProjectByCwd, normalizeProjectName, parseLaunchArgs, } from "./shared.js";
|
|
14
|
+
import { agentIsLive, agentOutcome, cwdFacts, gitTopLevel, HARNESS_COMMANDS, isDetachChunk, LIVE_STATUSES, sessionDisplayName, matchProjectByCwd, normalizeProjectName, gitCurrentBranch, parseLaunchArgs, } from "./shared.js";
|
|
15
15
|
import { sshCommand } from "./ssh-setup.js";
|
|
16
16
|
// `$XDG_CONFIG_HOME/mend`, default `~/.config/mend`; a pre-XDG `~/.mend` stays authoritative
|
|
17
17
|
// when it is the only one present (mirrors @mend/store's resolver — the CLI stays dependency-light).
|
|
@@ -2426,6 +2426,7 @@ const dashboard = async (config) => {
|
|
|
2426
2426
|
await runDashboard({
|
|
2427
2427
|
config,
|
|
2428
2428
|
cwd: process.cwd(),
|
|
2429
|
+
cwdBranch: gitCurrentBranch(process.cwd()),
|
|
2429
2430
|
api: (method, route, body) => request(config, method, route, body),
|
|
2430
2431
|
attachTty: (sessionId, harness, processId) => attachTty(config, sessionId, harness, 0n, processId),
|
|
2431
2432
|
});
|
package/dist/shared.js
CHANGED
|
@@ -263,6 +263,12 @@ export const cwdFacts = (cwd) => ({
|
|
|
263
263
|
repoRoot: gitTopLevel(cwd),
|
|
264
264
|
originUrl: gitOriginUrl(cwd),
|
|
265
265
|
});
|
|
266
|
+
/** The branch checked out at `cwd`, or null outside a repo or on a detached HEAD. */
|
|
267
|
+
export const gitCurrentBranch = (cwd) => {
|
|
268
|
+
const result = spawnSync("git", ["rev-parse", "--abbrev-ref", "HEAD"], { cwd, encoding: "utf8" });
|
|
269
|
+
const out = result.status === 0 ? result.stdout.trim() : "";
|
|
270
|
+
return out === "" || out === "HEAD" ? null : out;
|
|
271
|
+
};
|
|
266
272
|
/** The cwd repo's origin remote, or null when there is no repo or no origin. */
|
|
267
273
|
export const gitOriginUrl = (cwd) => {
|
|
268
274
|
const result = spawnSync("git", ["remote", "get-url", "origin"], { cwd, encoding: "utf8" });
|
package/dist/tui-theme.js
CHANGED
|
@@ -23,6 +23,11 @@ export const RULE = "#37374a";
|
|
|
23
23
|
export const COBALT = "#6f96f5";
|
|
24
24
|
/** The selection wash: a neutral lifted row — visible, not colorful. */
|
|
25
25
|
export const WASH = "#2c2c35";
|
|
26
|
+
/**
|
|
27
|
+
* The one exception to the no-painted-background rule: a floating modal
|
|
28
|
+
* must occlude the content beneath it, so it owns its ground.
|
|
29
|
+
*/
|
|
30
|
+
export const SURFACE = "#17171d";
|
|
26
31
|
export const AMBER = "#e2ab42";
|
|
27
32
|
export const GREEN = "#69c189";
|
|
28
33
|
export const RED = "#e87f76";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sealant/mend",
|
|
3
|
-
"version": "0.15.
|
|
3
|
+
"version": "0.15.2",
|
|
4
4
|
"description": "The mend CLI: adopt a repository into Mend's store, launch your coding agent in a recorded session worktree, detach and reattach from any terminal",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"claude-code",
|