@solaqua/gji 0.11.0 → 0.12.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.
Files changed (67) hide show
  1. package/README.md +82 -496
  2. package/dist/clean.js +2 -0
  3. package/dist/cli.d.ts +4 -0
  4. package/dist/cli.js +40 -4
  5. package/dist/context-card.d.ts +1 -0
  6. package/dist/context-card.js +25 -0
  7. package/dist/doctor.js +21 -0
  8. package/dist/done.js +2 -0
  9. package/dist/gji-bundle.mjs +988 -350
  10. package/dist/go.d.ts +1 -0
  11. package/dist/go.js +8 -0
  12. package/dist/hooks.d.ts +1 -0
  13. package/dist/hooks.js +7 -1
  14. package/dist/hub.d.ts +31 -0
  15. package/dist/hub.js +287 -0
  16. package/dist/ls.js +9 -1
  17. package/dist/new.d.ts +1 -0
  18. package/dist/new.js +9 -0
  19. package/dist/pr-open.js +2 -10
  20. package/dist/pull-requests.d.ts +1 -0
  21. package/dist/pull-requests.js +32 -3
  22. package/dist/remove.js +2 -0
  23. package/dist/repo.d.ts +1 -0
  24. package/dist/repo.js +4 -0
  25. package/dist/run-hook.js +2 -0
  26. package/dist/shell-completion.js +11 -3
  27. package/dist/slots.d.ts +8 -0
  28. package/dist/slots.js +61 -0
  29. package/dist/status.d.ts +2 -0
  30. package/dist/status.js +9 -1
  31. package/dist/task-command.d.ts +9 -0
  32. package/dist/task-command.js +29 -0
  33. package/dist/task.d.ts +9 -0
  34. package/dist/task.js +37 -0
  35. package/dist/terminal-text.d.ts +4 -0
  36. package/dist/terminal-text.js +72 -0
  37. package/dist/worktree-bootstrap.js +3 -0
  38. package/dist/worktree-info.d.ts +6 -1
  39. package/dist/worktree-info.js +14 -4
  40. package/dist/worktree-picker.d.ts +3 -3
  41. package/dist/worktree-picker.js +71 -82
  42. package/dist/worktree-sources.d.ts +1 -0
  43. package/dist/worktree-sources.js +57 -11
  44. package/man/man1/gji-back.1 +1 -1
  45. package/man/man1/gji-clean.1 +1 -1
  46. package/man/man1/gji-completion.1 +1 -1
  47. package/man/man1/gji-config.1 +1 -1
  48. package/man/man1/gji-doctor.1 +1 -1
  49. package/man/man1/gji-done.1 +1 -1
  50. package/man/man1/gji-go.1 +4 -1
  51. package/man/man1/gji-history.1 +1 -1
  52. package/man/man1/gji-init.1 +1 -1
  53. package/man/man1/gji-ls.1 +1 -1
  54. package/man/man1/gji-new.1 +4 -1
  55. package/man/man1/gji-open.1 +1 -1
  56. package/man/man1/gji-pr.1 +1 -1
  57. package/man/man1/gji-remove.1 +1 -1
  58. package/man/man1/gji-root.1 +1 -1
  59. package/man/man1/gji-run-hook.1 +1 -1
  60. package/man/man1/gji-status.1 +1 -1
  61. package/man/man1/gji-sync-files.1 +1 -1
  62. package/man/man1/gji-sync.1 +1 -1
  63. package/man/man1/gji-task.1 +16 -0
  64. package/man/man1/gji-undo.1 +1 -1
  65. package/man/man1/gji-warp.1 +1 -1
  66. package/man/man1/gji.1 +5 -1
  67. package/package.json +1 -1
@@ -29,6 +29,7 @@ const TOP_LEVEL_COMMANDS = [
29
29
  { name: "jump", description: "alias of go" },
30
30
  { name: "root", description: "print the main repository root path" },
31
31
  { name: "status", description: "summarize repository and worktree health" },
32
+ { name: "task", description: "show or update the current worktree task" },
32
33
  { name: "sync", description: "fetch and update one or all worktrees" },
33
34
  {
34
35
  name: "sync-files",
@@ -140,7 +141,7 @@ _gji_completion() {
140
141
  cur="\${COMP_WORDS[COMP_CWORD]:-}"
141
142
 
142
143
  if [ "$COMP_CWORD" -eq 1 ]; then
143
- COMPREPLY=( $(compgen -W "${topLevelCommands}" -- "$cur") )
144
+ COMPREPLY=( $(compgen -W "${topLevelCommands} --json --help" -- "$cur") )
144
145
  return 0
145
146
  fi
146
147
 
@@ -148,7 +149,7 @@ _gji_completion() {
148
149
 
149
150
  case "$command_name" in
150
151
  new)
151
- COMPREPLY=( $(compgen -W "--detached --from-current --no-fetch --take --copy --force --open --editor --dry-run --json --help" -- "$cur") )
152
+ COMPREPLY=( $(compgen -W "--detached --from-current --no-fetch --take --copy --task --force --open --editor --dry-run --json --help" -- "$cur") )
152
153
  ;;
153
154
  done)
154
155
  COMPREPLY=( $(compgen -W "--force --keep-branch --json --help" -- "$cur") )
@@ -389,6 +390,7 @@ complete -c gji -n '__fish_seen_subcommand_from new' -l detached -d 'create a de
389
390
  complete -c gji -n '__fish_seen_subcommand_from new' -l from-current -d 'base the new branch on the current worktree instead of the main worktree'
390
391
  complete -c gji -n '__fish_seen_subcommand_from new' -l take -d 'move current uncommitted changes into the new worktree'
391
392
  complete -c gji -n '__fish_seen_subcommand_from new' -l copy -d 'copy current uncommitted changes instead of moving them (requires --take)'
393
+ complete -c gji -n '__fish_seen_subcommand_from new' -l task -r -d 'record the purpose of the new worktree'
392
394
  complete -c gji -n '__fish_seen_subcommand_from new' -l force -d 'remove and recreate the worktree if the target path already exists'
393
395
  complete -c gji -n '__fish_seen_subcommand_from new' -l open -d 'open the new worktree in an editor after creation'
394
396
  complete -c gji -n '__fish_seen_subcommand_from new' -l editor -r -d 'editor CLI to use with --open (code, cursor, zed, …)'
@@ -426,11 +428,14 @@ complete -c gji -n '__fish_seen_subcommand_from open' -a '(__gji_worktree_branch
426
428
  complete -c gji -n '__fish_seen_subcommand_from go jump' -l print -d 'print the resolved worktree path explicitly'
427
429
  complete -c gji -n '__fish_seen_subcommand_from go jump' -l root -d 'navigate to the main repository root'
428
430
  complete -c gji -n '__fish_seen_subcommand_from go jump' -l json -d 'emit JSON for an existing worktree destination'
431
+ complete -c gji -n '__fish_seen_subcommand_from go jump' -l quiet -d 'suppress the context card'
429
432
  complete -c gji -n '__fish_seen_subcommand_from go jump' -a '(__gji_worktree_branches)' -d 'worktree branch'
430
433
 
431
434
  complete -c gji -n '__fish_seen_subcommand_from root' -l print -d 'print the resolved repository root path explicitly'
432
435
 
433
436
  complete -c gji -n '__fish_seen_subcommand_from status' -l json -d 'print repository and worktree health as JSON'
437
+ complete -c gji -n '__fish_seen_subcommand_from task' -l clear -d 'clear the current worktree task'
438
+ complete -c gji -n '__fish_seen_subcommand_from task' -l json -d 'emit task metadata as JSON'
434
439
 
435
440
  complete -c gji -n '__fish_seen_subcommand_from sync' -l all -d 'sync every worktree in the repository'
436
441
  complete -c gji -n '__fish_seen_subcommand_from sync' -l json -d 'emit JSON on success or error instead of human-readable output'
@@ -456,6 +461,8 @@ complete -c gji -n '__fish_seen_subcommand_from remove rm' -a '(__gji_worktree_b
456
461
 
457
462
  ${hookLines}
458
463
 
464
+ complete -c gji -n '__fish_use_subcommand' -l json -d 'emit the repository hub as JSON'
465
+
459
466
  complete -c gji -n '__fish_seen_subcommand_from warp' -l print -d 'print the resolved worktree path without changing directory'
460
467
  complete -c gji -n '__fish_seen_subcommand_from warp' -l json -d 'emit JSON on success or error instead of human-readable output'
461
468
 
@@ -534,6 +541,7 @@ local -a command_entries worktree_branches
534
541
 
535
542
  command_entries=(
536
543
  ${commandLines}
544
+ '--json:emit the repository hub as JSON'
537
545
  )
538
546
 
539
547
  if (( CURRENT == 2 )); then
@@ -543,7 +551,7 @@ fi
543
551
 
544
552
  case "\${words[2]}" in
545
553
  new)
546
- _arguments '--detached[create a detached worktree without a branch]' '--from-current[base the new branch on the current worktree instead of the main worktree]' '--no-fetch[skip refreshing the remote default branch before creating the new branch]' '--take[move current uncommitted changes into the new worktree]' '--copy[copy current uncommitted changes instead of moving them (requires --take)]' '--force[remove and recreate the worktree if the target path already exists]' '--open[open the new worktree in an editor after creation]' '--editor[editor CLI to use with --open (code, cursor, zed, …)]:editor:' '--dry-run[show what would be created without executing any git commands or writing files]' '--json[emit JSON on success or error instead of human-readable output]' '2:branch: '
554
+ _arguments '--detached[create a detached worktree without a branch]' '--from-current[base the new branch on the current worktree instead of the main worktree]' '--no-fetch[skip refreshing the remote default branch before creating the new branch]' '--take[move current uncommitted changes into the new worktree]' '--copy[copy current uncommitted changes instead of moving them (requires --take)]' '--task[record the purpose of the new worktree]:description:' '--force[remove and recreate the worktree if the target path already exists]' '--open[open the new worktree in an editor after creation]' '--editor[editor CLI to use with --open (code, cursor, zed, …)]:editor:' '--dry-run[show what would be created without executing any git commands or writing files]' '--json[emit JSON on success or error instead of human-readable output]' '2:branch: '
547
555
  ;;
548
556
  done)
549
557
  _arguments '--force[remove dirty or unmerged worktrees without prompting]' '--keep-branch[remove the worktree but preserve its branch]' '--json[emit JSON on success or error instead of human-readable output]' '2:branch: '
@@ -0,0 +1,8 @@
1
+ export interface SlotStore {
2
+ [worktreePath: string]: number;
3
+ }
4
+ export declare function SLOTS_FILE_PATH(home?: string): string;
5
+ export declare function loadSlots(home?: string): Promise<SlotStore>;
6
+ export declare function assignWorktreeSlot(repoRoot: string, worktreePath: string, home?: string): Promise<number>;
7
+ export declare function getWorktreeSlot(worktreePath: string, home?: string): Promise<number | null>;
8
+ export declare function releaseWorktreeSlot(worktreePath: string, home?: string): Promise<void>;
package/dist/slots.js ADDED
@@ -0,0 +1,61 @@
1
+ import { mkdir, readFile, writeFile } from "node:fs/promises";
2
+ import { homedir } from "node:os";
3
+ import { dirname, join, resolve } from "node:path";
4
+ import { GLOBAL_CONFIG_DIRECTORY } from "./config.js";
5
+ export function SLOTS_FILE_PATH(home = homedir()) {
6
+ const configDir = process.env.GJI_CONFIG_DIR;
7
+ return configDir
8
+ ? join(resolve(configDir), "slots.json")
9
+ : join(home, GLOBAL_CONFIG_DIRECTORY, "slots.json");
10
+ }
11
+ export async function loadSlots(home = homedir()) {
12
+ try {
13
+ const parsed = JSON.parse(await readFile(SLOTS_FILE_PATH(home), "utf8"));
14
+ if (!isSlotStore(parsed))
15
+ return {};
16
+ return parsed;
17
+ }
18
+ catch {
19
+ return {};
20
+ }
21
+ }
22
+ export async function assignWorktreeSlot(repoRoot, worktreePath, home = homedir()) {
23
+ const slots = await loadSlots(home);
24
+ const existing = slots[worktreePath];
25
+ if (existing !== undefined)
26
+ return existing;
27
+ if (worktreePath !== repoRoot && slots[repoRoot] === undefined)
28
+ slots[repoRoot] = 0;
29
+ const used = new Set(Object.values(slots));
30
+ const slot = worktreePath === repoRoot ? 0 : nextAvailableSlot(used);
31
+ slots[worktreePath] = slot;
32
+ await saveSlots(slots, home);
33
+ return slot;
34
+ }
35
+ export async function getWorktreeSlot(worktreePath, home = homedir()) {
36
+ const slot = (await loadSlots(home))[worktreePath];
37
+ return slot === undefined ? null : slot;
38
+ }
39
+ export async function releaseWorktreeSlot(worktreePath, home = homedir()) {
40
+ const slots = await loadSlots(home);
41
+ if (!(worktreePath in slots))
42
+ return;
43
+ delete slots[worktreePath];
44
+ await saveSlots(slots, home);
45
+ }
46
+ async function saveSlots(slots, home) {
47
+ const path = SLOTS_FILE_PATH(home);
48
+ await mkdir(dirname(path), { recursive: true });
49
+ await writeFile(path, `${JSON.stringify(slots, null, 2)}\n`, "utf8");
50
+ }
51
+ function nextAvailableSlot(used) {
52
+ let slot = 1;
53
+ while (used.has(slot))
54
+ slot += 1;
55
+ return slot;
56
+ }
57
+ function isSlotStore(value) {
58
+ return (typeof value === "object" &&
59
+ value !== null &&
60
+ Object.values(value).every((slot) => Number.isInteger(slot) && slot >= 0));
61
+ }
package/dist/status.d.ts CHANGED
@@ -7,7 +7,9 @@ interface WorktreeStatusRow {
7
7
  branch: string | null;
8
8
  current: boolean;
9
9
  path: string;
10
+ slot: number | null;
10
11
  status: "clean" | "dirty";
12
+ task: string | null;
11
13
  upstream: UpstreamState;
12
14
  }
13
15
  type UpstreamState = {
package/dist/status.js CHANGED
@@ -1,6 +1,8 @@
1
1
  import { readWorktreeHealth } from "./git.js";
2
2
  import { comparePaths } from "./paths.js";
3
3
  import { detectRepository, listWorktrees } from "./repo.js";
4
+ import { getWorktreeSlot } from "./slots.js";
5
+ import { readTask } from "./task.js";
4
6
  export async function runStatusCommand(options) {
5
7
  const repository = await detectRepository(options.cwd);
6
8
  const worktrees = sortWorktreesByPath(await listWorktrees(options.cwd));
@@ -36,12 +38,18 @@ export function formatStatusJson(repoRoot, currentRoot, rows) {
36
38
  };
37
39
  }
38
40
  async function buildStatusRow(worktree) {
39
- const health = await readWorktreeHealth(worktree.path);
41
+ const [health, slot, task] = await Promise.all([
42
+ readWorktreeHealth(worktree.path),
43
+ getWorktreeSlot(worktree.path),
44
+ readTask(worktree.path),
45
+ ]);
40
46
  return {
41
47
  branch: worktree.branch,
42
48
  current: worktree.isCurrent,
43
49
  path: worktree.path,
50
+ slot,
44
51
  status: health.status,
52
+ task: task?.task ?? null,
45
53
  upstream: buildUpstreamState(worktree.branch, health),
46
54
  };
47
55
  }
@@ -0,0 +1,9 @@
1
+ export interface TaskCommandOptions {
2
+ clear?: boolean;
3
+ cwd: string;
4
+ json?: boolean;
5
+ task?: string;
6
+ stderr: (chunk: string) => void;
7
+ stdout: (chunk: string) => void;
8
+ }
9
+ export declare function runTaskCommand(options: TaskCommandOptions): Promise<number>;
@@ -0,0 +1,29 @@
1
+ import { detectRepository } from "./repo.js";
2
+ import { clearTask, readTask, writeTask } from "./task.js";
3
+ export async function runTaskCommand(options) {
4
+ const repository = await detectRepository(options.cwd);
5
+ if (options.clear && options.task !== undefined)
6
+ return emitTaskError(options, "--clear cannot be combined with a task");
7
+ if (options.clear)
8
+ await clearTask(repository.currentRoot);
9
+ else if (options.task !== undefined)
10
+ await writeTask(repository.currentRoot, options.task);
11
+ const task = await readTask(repository.currentRoot);
12
+ if (options.json) {
13
+ options.stdout(`${JSON.stringify({ task: task?.task ?? null }, null, 2)}\n`);
14
+ }
15
+ else if (task) {
16
+ options.stdout(`${task.task}\n`);
17
+ }
18
+ else {
19
+ options.stdout('no task set — set one: gji task "..."\n');
20
+ }
21
+ return 0;
22
+ }
23
+ function emitTaskError(options, message) {
24
+ if (options.json)
25
+ options.stderr(`${JSON.stringify({ error: message })}\n`);
26
+ else
27
+ options.stderr(`gji task: ${message}\n`);
28
+ return 1;
29
+ }
package/dist/task.d.ts ADDED
@@ -0,0 +1,9 @@
1
+ export type TaskSource = "manual" | "pr";
2
+ export interface TaskMetadata {
3
+ source: TaskSource;
4
+ task: string;
5
+ updatedAt: number;
6
+ }
7
+ export declare function readTask(worktreePath: string): Promise<TaskMetadata | null>;
8
+ export declare function writeTask(worktreePath: string, task: string, source?: TaskSource): Promise<TaskMetadata>;
9
+ export declare function clearTask(worktreePath: string): Promise<void>;
package/dist/task.js ADDED
@@ -0,0 +1,37 @@
1
+ import { readFile, unlink, writeFile } from "node:fs/promises";
2
+ import { join } from "node:path";
3
+ import { worktreeGitDir } from "./repo.js";
4
+ export async function readTask(worktreePath) {
5
+ try {
6
+ const parsed = JSON.parse(await readFile(await taskPath(worktreePath), "utf8"));
7
+ return isTaskMetadata(parsed) ? parsed : null;
8
+ }
9
+ catch {
10
+ return null;
11
+ }
12
+ }
13
+ export async function writeTask(worktreePath, task, source = "manual") {
14
+ const metadata = { source, task, updatedAt: Date.now() };
15
+ const path = await taskPath(worktreePath);
16
+ await writeFile(path, `${JSON.stringify(metadata, null, 2)}\n`, "utf8");
17
+ return metadata;
18
+ }
19
+ export async function clearTask(worktreePath) {
20
+ try {
21
+ await unlink(await taskPath(worktreePath));
22
+ }
23
+ catch {
24
+ // Missing task metadata is already the desired state.
25
+ }
26
+ }
27
+ async function taskPath(worktreePath) {
28
+ return join(await worktreeGitDir(worktreePath), "gji-task.json");
29
+ }
30
+ function isTaskMetadata(value) {
31
+ if (typeof value !== "object" || value === null)
32
+ return false;
33
+ const candidate = value;
34
+ return (typeof candidate.task === "string" &&
35
+ (candidate.source === "manual" || candidate.source === "pr") &&
36
+ typeof candidate.updatedAt === "number");
37
+ }
@@ -0,0 +1,4 @@
1
+ export declare function sanitizeTerminalText(value: string): string;
2
+ export declare function middleEllipsize(value: string, maxLength: number): string;
3
+ export declare function startEllipsize(value: string, maxLength: number): string;
4
+ export declare function terminalWidth(value: string): number;
@@ -0,0 +1,72 @@
1
+ export function sanitizeTerminalText(value) {
2
+ return Array.from(value, (character) => {
3
+ const codePoint = character.codePointAt(0) ?? 0;
4
+ return codePoint <= 0x1f || (codePoint >= 0x7f && codePoint <= 0x9f)
5
+ ? " "
6
+ : character;
7
+ }).join("");
8
+ }
9
+ export function middleEllipsize(value, maxLength) {
10
+ if (terminalWidth(value) <= maxLength)
11
+ return value;
12
+ if (maxLength <= 1)
13
+ return "…";
14
+ const keep = maxLength - 1;
15
+ const start = takeTerminalColumns(value, Math.ceil(keep / 2), "start");
16
+ const end = takeTerminalColumns(value, Math.floor(keep / 2), "end");
17
+ return `${start}…${end}`;
18
+ }
19
+ export function startEllipsize(value, maxLength) {
20
+ if (terminalWidth(value) <= maxLength)
21
+ return value;
22
+ if (maxLength <= 1)
23
+ return "…";
24
+ return `…${takeTerminalColumns(value, maxLength - 1, "end")}`;
25
+ }
26
+ export function terminalWidth(value) {
27
+ let width = 0;
28
+ for (const character of Array.from(value)) {
29
+ width += characterTerminalWidth(character);
30
+ }
31
+ return width;
32
+ }
33
+ function takeTerminalColumns(value, maxWidth, direction) {
34
+ const characters = direction === "start" ? Array.from(value) : Array.from(value).reverse();
35
+ const kept = [];
36
+ let width = 0;
37
+ for (const character of characters) {
38
+ const characterWidth = characterTerminalWidth(character);
39
+ if (width + characterWidth > maxWidth)
40
+ break;
41
+ kept.push(character);
42
+ width += characterWidth;
43
+ }
44
+ return direction === "start" ? kept.join("") : kept.reverse().join("");
45
+ }
46
+ function characterTerminalWidth(character) {
47
+ const codePoint = character.codePointAt(0);
48
+ if (codePoint === undefined)
49
+ return 0;
50
+ if (isZeroWidthCodePoint(codePoint) || /\p{Mark}/u.test(character))
51
+ return 0;
52
+ return isWideCodePoint(codePoint) ? 2 : 1;
53
+ }
54
+ function isZeroWidthCodePoint(codePoint) {
55
+ return (codePoint === 0 ||
56
+ codePoint === 0x200d ||
57
+ (codePoint >= 0xfe00 && codePoint <= 0xfe0f));
58
+ }
59
+ function isWideCodePoint(codePoint) {
60
+ return ((codePoint >= 0x1100 && codePoint <= 0x115f) ||
61
+ codePoint === 0x2329 ||
62
+ codePoint === 0x232a ||
63
+ (codePoint >= 0x2e80 && codePoint <= 0xa4cf) ||
64
+ (codePoint >= 0xac00 && codePoint <= 0xd7a3) ||
65
+ (codePoint >= 0xf900 && codePoint <= 0xfaff) ||
66
+ (codePoint >= 0xfe10 && codePoint <= 0xfe19) ||
67
+ (codePoint >= 0xfe30 && codePoint <= 0xfe6f) ||
68
+ (codePoint >= 0xff00 && codePoint <= 0xff60) ||
69
+ (codePoint >= 0xffe0 && codePoint <= 0xffe6) ||
70
+ (codePoint >= 0x1f300 && codePoint <= 0x1faff) ||
71
+ (codePoint >= 0x20000 && codePoint <= 0x3fffd));
72
+ }
@@ -4,6 +4,7 @@ import { cloneDir } from "./dir-clone.js";
4
4
  import { syncFiles } from "./file-sync.js";
5
5
  import { extractHooks, runHook } from "./hooks.js";
6
6
  import { maybeRunInstallPrompt, } from "./install-prompt.js";
7
+ import { assignWorktreeSlot } from "./slots.js";
7
8
  import { executeSyncDirectoryPlan, } from "./sync-directories.js";
8
9
  import { prepareSyncDirectoryPlan } from "./sync-plan.js";
9
10
  export async function bootstrapWorktree(options) {
@@ -73,10 +74,12 @@ export async function bootstrapWorktree(options) {
73
74
  await maybeRunInstallPrompt(options.worktreePath, options.repoRoot, options.config, options.reporter.write, options.installDependencies, options.nonInteractive);
74
75
  }
75
76
  const hooks = extractHooks(options.config);
77
+ const slot = await assignWorktreeSlot(options.repoRoot, options.worktreePath);
76
78
  await runHook(hooks["after-create"], options.worktreePath, {
77
79
  branch: options.branch,
78
80
  path: options.worktreePath,
79
81
  repo: basename(options.repoRoot),
82
+ slot,
80
83
  }, options.reporter.write, options.json
81
84
  ? () => undefined
82
85
  : (options.commandStdout ?? ((chunk) => process.stdout.write(chunk))));
@@ -2,14 +2,18 @@ import { type WorktreeHealth } from "./git.js";
2
2
  import type { WorktreeEntry } from "./repo.js";
3
3
  export interface WorktreeInfo extends WorktreeEntry {
4
4
  lastCommitTimestamp: number | null;
5
+ slot: number | null;
5
6
  status: WorktreeHealth["status"] | "unknown";
7
+ task: string | null;
6
8
  upstream: UpstreamState;
7
9
  }
8
10
  export interface SerializedWorktreeInfo {
9
11
  branch: string | null;
10
12
  lastCommitTimestamp: number | null;
11
13
  path: string;
14
+ slot: number | null;
12
15
  status: WorktreeInfo["status"];
16
+ task: string | null;
13
17
  upstream: UpstreamState;
14
18
  }
15
19
  export type UpstreamState = {
@@ -26,7 +30,8 @@ export type UpstreamState = {
26
30
  kind: "unknown";
27
31
  };
28
32
  export declare function readWorktreeInfos(worktrees: WorktreeEntry[]): Promise<WorktreeInfo[]>;
33
+ export declare function readWorktreeInfo(worktree: WorktreeEntry): Promise<WorktreeInfo>;
29
34
  export declare function serializeWorktreeInfo(info: WorktreeInfo): SerializedWorktreeInfo;
30
35
  export declare function formatUpstreamState(upstream: UpstreamState): string;
31
36
  export declare function formatLastCommit(timestampSeconds: number | null): string;
32
- export declare function formatRelativeAge(timestampSeconds: number): string;
37
+ export declare function formatRelativeAge(timestampSeconds: number, nowSeconds?: number): string;
@@ -1,4 +1,6 @@
1
1
  import { readBranchLastCommitTimestamp, readWorktreeHealth, } from "./git.js";
2
+ import { getWorktreeSlot } from "./slots.js";
3
+ import { readTask } from "./task.js";
2
4
  const MAX_WORKTREE_INFO_READ_CONCURRENCY = 8;
3
5
  export async function readWorktreeInfos(worktrees) {
4
6
  return mapWithConcurrency(worktrees, MAX_WORKTREE_INFO_READ_CONCURRENCY, readWorktreeInfo);
@@ -18,19 +20,25 @@ async function mapWithConcurrency(items, limit, mapper) {
18
20
  await Promise.all(Array.from({ length: Math.min(limit, items.length) }, () => readNext()));
19
21
  return results;
20
22
  }
21
- async function readWorktreeInfo(worktree) {
22
- const [healthResult, lastCommitResult] = await Promise.allSettled([
23
+ export async function readWorktreeInfo(worktree) {
24
+ const [healthResult, lastCommitResult, slotResult, taskResult] = await Promise.allSettled([
23
25
  readWorktreeHealth(worktree.path),
24
26
  worktree.branch === null
25
27
  ? null
26
28
  : readBranchLastCommitTimestamp(worktree.path, worktree.branch),
29
+ getWorktreeSlot(worktree.path),
30
+ readTask(worktree.path),
27
31
  ]);
28
32
  const health = healthResult.status === "fulfilled" ? healthResult.value : null;
29
33
  const lastCommitTimestamp = lastCommitResult.status === "fulfilled" ? lastCommitResult.value : null;
34
+ const slot = slotResult.status === "fulfilled" ? slotResult.value : null;
35
+ const task = taskResult.status === "fulfilled" ? (taskResult.value?.task ?? null) : null;
30
36
  return {
31
37
  ...worktree,
32
38
  lastCommitTimestamp,
39
+ slot,
33
40
  status: health?.status ?? "unknown",
41
+ task,
34
42
  upstream: buildUpstreamState(worktree.branch, health),
35
43
  };
36
44
  }
@@ -58,7 +66,9 @@ export function serializeWorktreeInfo(info) {
58
66
  branch: info.branch,
59
67
  lastCommitTimestamp: info.lastCommitTimestamp,
60
68
  path: info.path,
69
+ slot: info.slot,
61
70
  status: info.status,
71
+ task: info.task,
62
72
  upstream: info.upstream,
63
73
  };
64
74
  }
@@ -94,8 +104,8 @@ export function formatLastCommit(timestampSeconds) {
94
104
  ? "n/a"
95
105
  : formatRelativeAge(timestampSeconds);
96
106
  }
97
- export function formatRelativeAge(timestampSeconds) {
98
- const ageSeconds = Math.max(0, Math.floor(Date.now() / 1000) - timestampSeconds);
107
+ export function formatRelativeAge(timestampSeconds, nowSeconds = Math.floor(Date.now() / 1000)) {
108
+ const ageSeconds = Math.max(0, nowSeconds - timestampSeconds);
99
109
  const units = [
100
110
  { label: "y", seconds: 365 * 24 * 60 * 60 },
101
111
  { label: "mo", seconds: 30 * 24 * 60 * 60 },
@@ -7,9 +7,9 @@ export interface WorktreePromptEntry extends WorktreeEntry {
7
7
  group: "recent" | "other";
8
8
  label: string;
9
9
  metadata?: string | null;
10
- pullRequestNumbers?: number[];
11
- pullRequestUrls?: string[];
10
+ pullRequests?: PullRequestInfo[];
12
11
  repoName: string;
12
+ task?: string | null;
13
13
  }
14
14
  export type QueryWorktreePullRequests = (repoRoot: string, sourceBranch: string) => Promise<PullRequestInfo[]>;
15
15
  export type QueryRepositoryPullRequests = (repoRoot: string) => Promise<PullRequestInfo[]>;
@@ -47,4 +47,4 @@ export declare function promptForSingleWorktree(message: string, worktrees: Work
47
47
  scope?: WorktreePromptScope;
48
48
  }): Promise<string | null>;
49
49
  export declare function promptForMultipleWorktrees(message: string, worktrees: WorktreePromptEntry[], io?: WorktreePickerIO): Promise<string[] | null>;
50
- export declare function formatWorktreeBranchLabel(branch: string | null, pullRequestNumbers?: number[]): string;
50
+ export declare function formatWorktreeBranchLabel(branch: string | null, pullRequests?: PullRequestInfo[]): string;