vde-worktree 0.0.16 → 0.0.18

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.ja.md CHANGED
@@ -22,7 +22,7 @@
22
22
  - Node.js 22+
23
23
  - pnpm 10+
24
24
  - `fzf`(`cd` に必須)
25
- - `gh`(PR merged 判定に任意)
25
+ - `gh`(PR 状態判定に任意)
26
26
 
27
27
  ## インストール / ビルド
28
28
 
@@ -111,7 +111,7 @@ autoload -Uz compinit && compinit
111
111
  - `--verbose`: 詳細ログ
112
112
  - `--no-hooks`: 今回のみ hook 無効化(`--allow-unsafe` 必須)
113
113
  - `--allow-unsafe`: unsafe 操作の明示同意
114
- - `--no-gh`: 今回の実行で `gh` による PR merged 判定を無効化
114
+ - `--no-gh`: 今回の実行で `gh` による PR 状態判定を無効化
115
115
  - `--hook-timeout-ms <ms>`: hook timeout 上書き
116
116
  - `--lock-timeout-ms <ms>`: repo lock timeout 上書き
117
117
 
@@ -135,13 +135,16 @@ vw init
135
135
  vw list
136
136
  vw list --json
137
137
  vw list --no-gh
138
+ vw list --full-path
138
139
  ```
139
140
 
140
141
  機能:
141
142
 
142
143
  - Git の porcelain 情報から worktree 一覧を取得
143
- - branch/path/dirty/lock/merged/upstream を表示
144
- - `--no-gh` 指定時は PR merged 判定をスキップ(`merged.byPR` は `null`)
144
+ - branch/path/dirty/lock/merged/PR/upstream を表示
145
+ - テーブル表示では長い `path` は端末幅に合わせて `…` で省略
146
+ - `--full-path` でテーブル表示の path 省略を無効化
147
+ - `--no-gh` 指定時は PR 状態判定をスキップ(`pr.status` は `unknown`、`merged.byPR` は `null`)
145
148
  - 対話ターミナルでは Catppuccin 風の ANSI 色で表示
146
149
 
147
150
  ### `status`
@@ -424,6 +427,7 @@ vw completion zsh --install
424
427
  - `merged.byAncestry`: ローカル履歴判定(`git merge-base --is-ancestor`)
425
428
  - `merged.byPR`: GitHub PR merged 判定(`gh`)
426
429
  - `merged.overall`: 最終判定
430
+ - `pr.status`: PR 状態(`none` / `open` / `merged` / `closed_unmerged` / `unknown`)
427
431
 
428
432
  `overall` ポリシー:
429
433
 
@@ -436,7 +440,7 @@ vw completion zsh --install
436
440
  - `byPR === false` または lifecycle が明示的に未取り込みなら `overall = false`
437
441
  - それ以外は `overall = null`
438
442
 
439
- `byPR` が `null` になる例:
443
+ `byPR` が `null` かつ `pr.status` が `unknown` になる例:
440
444
 
441
445
  - `gh` 未導入
442
446
  - `gh auth` 未設定
package/README.md CHANGED
@@ -111,7 +111,7 @@ After `vw init`, the tool manages:
111
111
  - `--verbose`: verbose logging
112
112
  - `--no-hooks`: disable hooks for this run (requires `--allow-unsafe`)
113
113
  - `--allow-unsafe`: explicit unsafe override
114
- - `--no-gh`: disable GitHub CLI based PR merge checks for this run
114
+ - `--no-gh`: disable GitHub CLI based PR status checks for this run
115
115
  - `--hook-timeout-ms <ms>`: hook timeout override
116
116
  - `--lock-timeout-ms <ms>`: repository lock timeout override
117
117
 
@@ -135,13 +135,16 @@ What it does:
135
135
  vw list
136
136
  vw list --json
137
137
  vw list --no-gh
138
+ vw list --full-path
138
139
  ```
139
140
 
140
141
  What it does:
141
142
 
142
143
  - Lists all worktrees from Git porcelain output
143
- - Includes metadata such as branch, path, dirty, lock, merged, and upstream status
144
- - With `--no-gh`, skips PR-based merge checks (`merged.byPR` becomes `null`)
144
+ - Includes metadata such as branch, path, dirty, lock, merged, PR status, and upstream status
145
+ - In table output, long `path` values are truncated with `…` to fit terminal width by default
146
+ - Use `--full-path` to disable path truncation in table output
147
+ - With `--no-gh`, skips PR status checks (`pr.status` becomes `unknown`, `merged.byPR` becomes `null`)
145
148
  - In interactive terminal, uses Catppuccin-style ANSI colors
146
149
 
147
150
  ### `status`
@@ -424,6 +427,7 @@ Each worktree reports:
424
427
  - `merged.byAncestry`: local ancestry check (`git merge-base --is-ancestor <branch> <baseBranch>`)
425
428
  - `merged.byPR`: PR-based merged check via GitHub CLI
426
429
  - `merged.overall`: final decision
430
+ - `pr.status`: PR state (`none` / `open` / `merged` / `closed_unmerged` / `unknown`)
427
431
 
428
432
  Overall policy:
429
433
 
@@ -436,7 +440,7 @@ Overall policy:
436
440
  - `byPR === false` or explicit lifecycle "not merged" evidence => `overall = false`
437
441
  - otherwise `overall = null`
438
442
 
439
- `byPR` becomes `null` when PR lookup is unavailable (for example: `gh` missing, auth missing, API error, `vde-worktree.enableGh=false`, or `--no-gh`).
443
+ `byPR` becomes `null` and `pr.status` becomes `unknown` when PR lookup is unavailable (for example: `gh` missing, auth missing, API error, `vde-worktree.enableGh=false`, or `--no-gh`).
440
444
 
441
445
  ## JSON Contract
442
446
 
@@ -68,6 +68,10 @@ const toFlag = (value) => {
68
68
  if (value === false) return "no"
69
69
  return "unknown"
70
70
  }
71
+ const toPrStatus = (value) => {
72
+ if (typeof value !== "string" || value.length === 0) return "n/a"
73
+ return value
74
+ }
71
75
  let payload
72
76
  try {
73
77
  payload = JSON.parse(fs.readFileSync(0, "utf8"))
@@ -78,10 +82,11 @@ const worktrees = Array.isArray(payload.worktrees) ? payload.worktrees : []
78
82
  for (const worktree of worktrees) {
79
83
  if (typeof worktree?.branch !== "string" || worktree.branch.length === 0) continue
80
84
  const merged = toFlag(worktree?.merged?.overall)
85
+ const pr = toPrStatus(worktree?.pr?.status)
81
86
  const dirty = worktree?.dirty === true ? "yes" : "no"
82
87
  const locked = worktree?.locked?.value === true ? "yes" : "no"
83
88
  const path = toDisplayPath(worktree?.path)
84
- const summary = `merged=${merged} dirty=${dirty} locked=${locked}${path ? ` path=${path}` : ""}`
89
+ const summary = `merged=${merged} pr=${pr} dirty=${dirty} locked=${locked}${path ? ` path=${path}` : ""}`
85
90
  const sanitized = summary.replace(/[\t\r\n]+/g, " ").trim()
86
91
  process.stdout.write(`${worktree.branch}\t${sanitized}\n`)
87
92
  }
@@ -101,6 +106,10 @@ const toFlag = (value) => {
101
106
  if (value === false) return "no"
102
107
  return "unknown"
103
108
  }
109
+ const toPrStatus = (value) => {
110
+ if (typeof value !== "string" || value.length === 0) return "n/a"
111
+ return value
112
+ }
104
113
  let payload
105
114
  try {
106
115
  payload = JSON.parse(fs.readFileSync(0, "utf8"))
@@ -118,9 +127,10 @@ for (const worktree of worktrees) {
118
127
  const name = rel.split(path.sep).join("/")
119
128
  const branch = typeof worktree?.branch === "string" && worktree.branch.length > 0 ? worktree.branch : "(detached)"
120
129
  const merged = toFlag(worktree?.merged?.overall)
130
+ const pr = toPrStatus(worktree?.pr?.status)
121
131
  const dirty = worktree?.dirty === true ? "yes" : "no"
122
132
  const locked = worktree?.locked?.value === true ? "yes" : "no"
123
- const summary = `branch=${branch} merged=${merged} dirty=${dirty} locked=${locked}`
133
+ const summary = `branch=${branch} merged=${merged} pr=${pr} dirty=${dirty} locked=${locked}`
124
134
  const sanitized = summary.replace(/[\t\r\n]+/g, " ").trim()
125
135
  process.stdout.write(`${name}\t${sanitized}\n`)
126
136
  }
@@ -179,6 +189,8 @@ for __vw_bin in vw vde-worktree
179
189
  complete -c $__vw_bin -l json -d "Output machine-readable JSON"
180
190
  complete -c $__vw_bin -l verbose -d "Enable verbose logs"
181
191
  complete -c $__vw_bin -l no-hooks -d "Disable hooks for this run (requires --allow-unsafe)"
192
+ complete -c $__vw_bin -l no-gh -d "Disable GitHub CLI based PR status checks for this run"
193
+ complete -c $__vw_bin -l full-path -d "Disable list table path truncation"
182
194
  complete -c $__vw_bin -l allow-unsafe -d "Explicit unsafe override in non-TTY mode"
183
195
  complete -c $__vw_bin -l strict-post-hooks -d "Fail when post hooks fail"
184
196
  complete -c $__vw_bin -l hook-timeout-ms -r -d "Override hook timeout"
@@ -190,6 +202,7 @@ for __vw_bin in vw vde-worktree
190
202
  complete -c $__vw_bin -n "__fish_seen_subcommand_from path" -a "(__vw_worktree_candidates_with_meta)"
191
203
  complete -c $__vw_bin -n "__fish_seen_subcommand_from switch" -a "(__vw_worktree_candidates_with_meta)"
192
204
  complete -c $__vw_bin -n "__fish_seen_subcommand_from del" -a "(__vw_worktree_candidates_with_meta)"
205
+ complete -c $__vw_bin -n "__fish_seen_subcommand_from list" -l full-path -d "Disable list table path truncation"
193
206
  complete -c $__vw_bin -n "__fish_seen_subcommand_from get" -a "(__vw_remote_branches)"
194
207
  complete -c $__vw_bin -n "__fish_seen_subcommand_from absorb" -a "(__vw_worktree_candidates_with_meta)"
195
208
  complete -c $__vw_bin -n "__fish_seen_subcommand_from unabsorb" -a "(__vw_worktree_candidates_with_meta)"
@@ -45,6 +45,10 @@ const toFlag = (value) => {
45
45
  if (value === false) return "no"
46
46
  return "unknown"
47
47
  }
48
+ const toPrStatus = (value) => {
49
+ if (typeof value !== "string" || value.length === 0) return "n/a"
50
+ return value
51
+ }
48
52
  let payload
49
53
  try {
50
54
  payload = JSON.parse(fs.readFileSync(0, "utf8"))
@@ -55,10 +59,11 @@ const worktrees = Array.isArray(payload.worktrees) ? payload.worktrees : []
55
59
  for (const worktree of worktrees) {
56
60
  if (typeof worktree?.branch !== "string" || worktree.branch.length === 0) continue
57
61
  const merged = toFlag(worktree?.merged?.overall)
62
+ const pr = toPrStatus(worktree?.pr?.status)
58
63
  const dirty = worktree?.dirty === true ? "yes" : "no"
59
64
  const locked = worktree?.locked?.value === true ? "yes" : "no"
60
65
  const path = toDisplayPath(worktree?.path)
61
- const summary = `merged=${merged} dirty=${dirty} locked=${locked}${path ? ` path=${path}` : ""}`
66
+ const summary = `merged=${merged} pr=${pr} dirty=${dirty} locked=${locked}${path ? ` path=${path}` : ""}`
62
67
  const sanitized = summary.replace(/[\t\r\n]+/g, " ").trim()
63
68
  process.stdout.write(`${worktree.branch}\t${sanitized}\n`)
64
69
  }
@@ -78,6 +83,10 @@ const toFlag = (value) => {
78
83
  if (value === false) return "no"
79
84
  return "unknown"
80
85
  }
86
+ const toPrStatus = (value) => {
87
+ if (typeof value !== "string" || value.length === 0) return "n/a"
88
+ return value
89
+ }
81
90
  let payload
82
91
  try {
83
92
  payload = JSON.parse(fs.readFileSync(0, "utf8"))
@@ -95,9 +104,10 @@ for (const worktree of worktrees) {
95
104
  const name = rel.split(path.sep).join("/")
96
105
  const branch = typeof worktree?.branch === "string" && worktree.branch.length > 0 ? worktree.branch : "(detached)"
97
106
  const merged = toFlag(worktree?.merged?.overall)
107
+ const pr = toPrStatus(worktree?.pr?.status)
98
108
  const dirty = worktree?.dirty === true ? "yes" : "no"
99
109
  const locked = worktree?.locked?.value === true ? "yes" : "no"
100
- const summary = `branch=${branch} merged=${merged} dirty=${dirty} locked=${locked}`
110
+ const summary = `branch=${branch} merged=${merged} pr=${pr} dirty=${dirty} locked=${locked}`
101
111
  const sanitized = summary.replace(/[\t\r\n]+/g, " ").trim()
102
112
  process.stdout.write(`${name}\t${sanitized}\n`)
103
113
  }
@@ -253,6 +263,8 @@ _vw() {
253
263
  "--json[Output machine-readable JSON]"
254
264
  "--verbose[Enable verbose logs]"
255
265
  "--no-hooks[Disable hooks for this run]"
266
+ "--no-gh[Disable GitHub CLI based PR status checks for this run]"
267
+ "--full-path[Disable list table path truncation]"
256
268
  "--allow-unsafe[Allow unsafe behavior in non-TTY mode]"
257
269
  "--strict-post-hooks[Fail when post hooks fail]"
258
270
  "--hook-timeout-ms[Override hook timeout]:ms:"
@@ -274,6 +286,10 @@ _vw() {
274
286
  arguments)
275
287
  local current_command="${line[1]:-${words[2]}}"
276
288
  case "${current_command}" in
289
+ list)
290
+ _arguments \
291
+ "--full-path[Disable list table path truncation]"
292
+ ;;
277
293
  status)
278
294
  _arguments \
279
295
  "1:branch:_vw_complete_worktree_branches_with_meta"
package/dist/index.mjs CHANGED
@@ -853,43 +853,68 @@ const toTargetBranches = ({ branches, baseBranch }) => {
853
853
  }
854
854
  return [...uniqueBranches];
855
855
  };
856
- const buildUnknownBranchMap = (branches) => {
857
- return new Map(branches.map((branch) => [branch, null]));
856
+ const buildUnknownPrStatusMap = (branches) => {
857
+ return new Map(branches.map((branch) => [branch, "unknown"]));
858
858
  };
859
- const parseMergedBranches = ({ raw, targetBranches }) => {
859
+ const parseUpdatedAtMillis = (value) => {
860
+ if (typeof value !== "string" || value.length === 0) return Number.NEGATIVE_INFINITY;
861
+ const parsed = Date.parse(value);
862
+ if (Number.isNaN(parsed)) return Number.NEGATIVE_INFINITY;
863
+ return parsed;
864
+ };
865
+ const toPrStatus = (record) => {
866
+ if (typeof record.mergedAt === "string" && record.mergedAt.length > 0) return "merged";
867
+ const state = typeof record.state === "string" ? record.state.toUpperCase() : "";
868
+ if (state === "MERGED") return "merged";
869
+ if (state === "OPEN") return "open";
870
+ if (state === "CLOSED") return "closed_unmerged";
871
+ return "unknown";
872
+ };
873
+ const parsePrStatusByBranch = ({ raw, targetBranches }) => {
860
874
  try {
861
875
  const parsed = JSON.parse(raw);
862
876
  if (Array.isArray(parsed) !== true) return null;
877
+ const targetBranchSet = new Set(targetBranches);
863
878
  const records = parsed;
864
- const mergedBranches = /* @__PURE__ */ new Set();
865
- for (const record of records) {
879
+ const latestByBranch = /* @__PURE__ */ new Map();
880
+ for (const [index, record] of records.entries()) {
866
881
  if (typeof record?.headRefName !== "string" || record.headRefName.length === 0) continue;
867
- if (targetBranches.has(record.headRefName) !== true) continue;
868
- if (typeof record.mergedAt !== "string" || record.mergedAt.length === 0) continue;
869
- mergedBranches.add(record.headRefName);
882
+ if (targetBranchSet.has(record.headRefName) !== true) continue;
883
+ const updatedAtMillis = parseUpdatedAtMillis(record.updatedAt);
884
+ const status = toPrStatus(record);
885
+ const current = latestByBranch.get(record.headRefName);
886
+ if (current === void 0 || updatedAtMillis > current.updatedAtMillis || updatedAtMillis === current.updatedAtMillis && index > current.index) latestByBranch.set(record.headRefName, {
887
+ updatedAtMillis,
888
+ index,
889
+ status
890
+ });
870
891
  }
871
- return mergedBranches;
892
+ const result = /* @__PURE__ */ new Map();
893
+ for (const branch of targetBranches) {
894
+ const latest = latestByBranch.get(branch);
895
+ result.set(branch, latest?.status ?? "none");
896
+ }
897
+ return result;
872
898
  } catch {
873
899
  return null;
874
900
  }
875
901
  };
876
- const resolveMergedByPrBatch = async ({ repoRoot, baseBranch, branches, enabled = true, runGh = defaultRunGh }) => {
877
- if (enabled !== true) return /* @__PURE__ */ new Map();
902
+ const resolvePrStatusByBranchBatch = async ({ repoRoot, baseBranch, branches, enabled = true, runGh = defaultRunGh }) => {
878
903
  if (baseBranch === null) return /* @__PURE__ */ new Map();
879
904
  const targetBranches = toTargetBranches({
880
905
  branches,
881
906
  baseBranch
882
907
  });
883
908
  if (targetBranches.length === 0) return /* @__PURE__ */ new Map();
909
+ if (enabled !== true) return buildUnknownPrStatusMap(targetBranches);
884
910
  try {
885
- const targetBranchSet = new Set(targetBranches);
886
911
  const result = await runGh({
887
912
  cwd: repoRoot,
888
913
  args: [
889
914
  "pr",
890
915
  "list",
891
916
  "--state",
892
- "merged",
917
+ "all",
893
918
  "--base",
894
919
  baseBranch,
895
920
  "--search",
@@ -897,21 +922,19 @@ const resolveMergedByPrBatch = async ({ repoRoot, baseBranch, branches, enabled
897
922
  "--limit",
898
923
  "1000",
899
924
  "--json",
900
- "headRefName,mergedAt"
925
+ "headRefName,state,mergedAt,updatedAt"
901
926
  ]
902
927
  });
903
- if (result.exitCode !== 0) return buildUnknownBranchMap(targetBranches);
904
- const mergedBranches = parseMergedBranches({
928
+ if (result.exitCode !== 0) return buildUnknownPrStatusMap(targetBranches);
929
+ const prStatusByBranch = parsePrStatusByBranch({
905
930
  raw: result.stdout,
906
- targetBranches: targetBranchSet
931
+ targetBranches
907
932
  });
908
- if (mergedBranches === null) return buildUnknownBranchMap(targetBranches);
909
- return new Map(targetBranches.map((branch) => {
910
- return [branch, mergedBranches.has(branch)];
911
- }));
933
+ if (prStatusByBranch === null) return buildUnknownPrStatusMap(targetBranches);
934
+ return prStatusByBranch;
912
935
  } catch (error) {
913
- if (error.code === "ENOENT") return buildUnknownBranchMap(targetBranches);
914
- return buildUnknownBranchMap(targetBranches);
936
+ if (error.code === "ENOENT") return buildUnknownPrStatusMap(targetBranches);
937
+ return buildUnknownPrStatusMap(targetBranches);
915
938
  }
916
939
  };
917
940
 
@@ -1110,7 +1133,7 @@ const resolveLifecycleFromReflog = async ({ repoRoot, branch, baseBranch }) => {
1110
1133
  divergedHead: latestWorkHead
1111
1134
  };
1112
1135
  };
1113
- const resolveMergedState = async ({ repoRoot, branch, head, baseBranch, mergedByPrByBranch }) => {
1136
+ const resolveMergedState = async ({ repoRoot, branch, head, baseBranch, prStatusByBranch }) => {
1114
1137
  if (branch === null) return {
1115
1138
  byAncestry: null,
1116
1139
  byPR: null,
@@ -1131,7 +1154,10 @@ const resolveMergedState = async ({ repoRoot, branch, head, baseBranch, mergedBy
1131
1154
  if (result.exitCode === 0) byAncestry = true;
1132
1155
  else if (result.exitCode === 1) byAncestry = false;
1133
1156
  }
1134
- const byPR = branch === baseBranch ? null : mergedByPrByBranch.get(branch) ?? null;
1157
+ const prStatus = branch === baseBranch ? null : prStatusByBranch.get(branch) ?? null;
1158
+ let byPR = null;
1159
+ if (prStatus === "merged") byPR = true;
1160
+ else if (prStatus === "none" || prStatus === "open" || prStatus === "closed_unmerged") byPR = false;
1135
1161
  let byLifecycle = null;
1136
1162
  if (baseBranch !== null) {
1137
1163
  const lifecycle = await upsertWorktreeMergeLifecycle({
@@ -1182,6 +1208,10 @@ const resolveMergedState = async ({ repoRoot, branch, head, baseBranch, mergedBy
1182
1208
  })
1183
1209
  };
1184
1210
  };
1211
+ const resolvePrState = ({ branch, baseBranch, prStatusByBranch }) => {
1212
+ if (branch === null || branch === baseBranch) return { status: null };
1213
+ return { status: prStatusByBranch.get(branch) ?? null };
1214
+ };
1185
1215
  const resolveMergedOverall = ({ byAncestry, byPR, byLifecycle }) => {
1186
1216
  if (byPR === true || byLifecycle === true) return true;
1187
1217
  if (byAncestry === false) return false;
@@ -1228,7 +1258,7 @@ const resolveUpstreamState = async (worktreePath) => {
1228
1258
  remote: upstreamRef.stdout.trim()
1229
1259
  };
1230
1260
  };
1231
- const enrichWorktree = async ({ repoRoot, worktree, baseBranch, mergedByPrByBranch }) => {
1261
+ const enrichWorktree = async ({ repoRoot, worktree, baseBranch, prStatusByBranch }) => {
1232
1262
  const [dirty, locked, merged, upstream] = await Promise.all([
1233
1263
  resolveDirty(worktree.path),
1234
1264
  resolveLockState({
@@ -1240,10 +1270,15 @@ const enrichWorktree = async ({ repoRoot, worktree, baseBranch, mergedByPrByBran
1240
1270
  branch: worktree.branch,
1241
1271
  head: worktree.head,
1242
1272
  baseBranch,
1243
- mergedByPrByBranch
1273
+ prStatusByBranch
1244
1274
  }),
1245
1275
  resolveUpstreamState(worktree.path)
1246
1276
  ]);
1277
+ const pr = resolvePrState({
1278
+ branch: worktree.branch,
1279
+ baseBranch,
1280
+ prStatusByBranch
1281
+ });
1247
1282
  return {
1248
1283
  branch: worktree.branch,
1249
1284
  path: worktree.path,
@@ -1251,6 +1286,7 @@ const enrichWorktree = async ({ repoRoot, worktree, baseBranch, mergedByPrByBran
1251
1286
  dirty,
1252
1287
  locked,
1253
1288
  merged,
1289
+ pr,
1254
1290
  upstream
1255
1291
  };
1256
1292
  };
@@ -1260,7 +1296,7 @@ const collectWorktreeSnapshot = async (repoRoot, { noGh = false } = {}) => {
1260
1296
  listGitWorktrees(repoRoot),
1261
1297
  resolveEnableGh(repoRoot)
1262
1298
  ]);
1263
- const mergedByPrByBranch = await resolveMergedByPrBatch({
1299
+ const prStatusByBranch = await resolvePrStatusByBranchBatch({
1264
1300
  repoRoot,
1265
1301
  baseBranch,
1266
1302
  branches: worktrees.map((worktree) => worktree.branch),
@@ -1274,7 +1310,7 @@ const collectWorktreeSnapshot = async (repoRoot, { noGh = false } = {}) => {
1274
1310
  repoRoot,
1275
1311
  worktree,
1276
1312
  baseBranch,
1277
- mergedByPrByBranch
1313
+ prStatusByBranch
1278
1314
  });
1279
1315
  }))
1280
1316
  };
@@ -1452,6 +1488,10 @@ const CD_FZF_EXTRA_ARGS = [
1452
1488
  "--preview-window=right,60%,wrap",
1453
1489
  "--ansi"
1454
1490
  ];
1491
+ const LIST_TABLE_COLUMN_COUNT = 8;
1492
+ const LIST_TABLE_PATH_COLUMN_INDEX = 7;
1493
+ const LIST_TABLE_PATH_MIN_WIDTH = 12;
1494
+ const LIST_TABLE_CELL_HORIZONTAL_PADDING = 2;
1455
1495
  const COMPLETION_SHELLS = ["zsh", "fish"];
1456
1496
  const COMPLETION_FILE_BY_SHELL = {
1457
1497
  zsh: "zsh/_vw",
@@ -1530,9 +1570,9 @@ const colorizeListTableLine = ({ line, theme }) => {
1530
1570
  const segments = line.split("│");
1531
1571
  if (segments.length < 3) return line;
1532
1572
  const cells = segments.slice(1, -1);
1533
- if (cells.length !== 7) return line;
1573
+ if (cells.length !== 8) return line;
1534
1574
  const headers = cells.map((cell) => cell.trim());
1535
- if (headers[0] === "branch" && headers[1] === "dirty" && headers[2] === "merged" && headers[3] === "locked" && headers[4] === "ahead" && headers[5] === "behind" && headers[6] === "path") {
1575
+ if (headers[0] === "branch" && headers[1] === "dirty" && headers[2] === "merged" && headers[3] === "pr" && headers[4] === "locked" && headers[5] === "ahead" && headers[6] === "behind" && headers[7] === "path") {
1536
1576
  const nextCells = cells.map((cell) => colorizeCellContent({
1537
1577
  cell,
1538
1578
  color: theme.header
@@ -1546,15 +1586,18 @@ const colorizeListTableLine = ({ line, theme }) => {
1546
1586
  const branchCell = cells[0];
1547
1587
  const dirtyCell = cells[1];
1548
1588
  const mergedCell = cells[2];
1549
- const lockedCell = cells[3];
1550
- const aheadCell = cells[4];
1551
- const behindCell = cells[5];
1552
- const pathCell = cells[6];
1589
+ const prCell = cells[3];
1590
+ const lockedCell = cells[4];
1591
+ const aheadCell = cells[5];
1592
+ const behindCell = cells[6];
1593
+ const pathCell = cells[7];
1553
1594
  const branchColor = branchCell.includes("(detached)") === true ? theme.branchDetached : branchCell.trimStart().startsWith("*") ? theme.branchCurrent : theme.branch;
1554
1595
  const dirtyTrimmed = dirtyCell.trim();
1555
1596
  const dirtyColor = dirtyTrimmed === "dirty" ? theme.dirty : dirtyTrimmed === "clean" ? theme.clean : theme.value;
1556
1597
  const mergedTrimmed = mergedCell.trim();
1557
1598
  const mergedColor = mergedTrimmed === "merged" ? theme.merged : mergedTrimmed === "unmerged" ? theme.unmerged : mergedTrimmed === "-" ? theme.base : theme.unknown;
1599
+ const prTrimmed = prCell.trim();
1600
+ const prColor = prTrimmed === "merged" ? theme.merged : prTrimmed === "open" ? theme.value : prTrimmed === "closed_unmerged" ? theme.unmerged : prTrimmed === "none" ? theme.muted : prTrimmed === "-" ? theme.base : theme.unknown;
1558
1601
  const lockedColor = lockedCell.trim() === "locked" ? theme.locked : theme.muted;
1559
1602
  const aheadTrimmed = aheadCell.trim();
1560
1603
  const aheadValue = Number.parseInt(aheadTrimmed, 10);
@@ -1575,6 +1618,10 @@ const colorizeListTableLine = ({ line, theme }) => {
1575
1618
  cell: mergedCell,
1576
1619
  color: mergedColor
1577
1620
  }),
1621
+ colorizeCellContent({
1622
+ cell: prCell,
1623
+ color: prColor
1624
+ }),
1578
1625
  colorizeCellContent({
1579
1626
  cell: lockedCell,
1580
1627
  color: lockedColor
@@ -1613,9 +1660,14 @@ const commandHelpEntries = [
1613
1660
  },
1614
1661
  {
1615
1662
  name: "list",
1616
- usage: "vw list [--json]",
1663
+ usage: "vw list [--json] [--full-path]",
1617
1664
  summary: "List worktrees with status metadata.",
1618
- details: ["Table output includes branch, path, dirty, lock, merged, and ahead/behind vs base branch.", "JSON output includes upstream metadata fields."]
1665
+ details: [
1666
+ "Table output includes branch, path, dirty, lock, merged, PR state, and ahead/behind vs base branch.",
1667
+ "By default, long path values are truncated to fit terminal width.",
1668
+ "JSON output includes PR and upstream metadata fields."
1669
+ ],
1670
+ options: ["--full-path"]
1619
1671
  },
1620
1672
  {
1621
1673
  name: "status",
@@ -2432,10 +2484,35 @@ const formatMergedColor = ({ mergedState, theme }) => {
2432
2484
  if (normalized === "base") return theme.base(mergedState);
2433
2485
  return theme.unknown(mergedState);
2434
2486
  };
2487
+ const formatPrDisplayState = ({ prStatus, isBaseBranch }) => {
2488
+ if (isBaseBranch || prStatus === null) return "-";
2489
+ if (prStatus === "none" || prStatus === "open" || prStatus === "merged" || prStatus === "closed_unmerged" || prStatus === "unknown") return prStatus;
2490
+ return "unknown";
2491
+ };
2435
2492
  const formatListUpstreamCount = (value) => {
2436
2493
  if (value === null) return "-";
2437
2494
  return String(value);
2438
2495
  };
2496
+ const resolveListColumnContentWidth = ({ rows, columnIndex }) => {
2497
+ return rows.reduce((width, row) => {
2498
+ const cell = row[columnIndex] ?? "";
2499
+ return Math.max(width, stringWidth(cell));
2500
+ }, 0);
2501
+ };
2502
+ const resolveListPathColumnWidth = ({ rows, disablePathTruncation }) => {
2503
+ if (disablePathTruncation) return null;
2504
+ if (process.stdout.isTTY !== true) return null;
2505
+ const terminalColumns = process.stdout.columns;
2506
+ if (typeof terminalColumns !== "number" || Number.isFinite(terminalColumns) !== true || terminalColumns <= 0) return null;
2507
+ const measuredNonPathWidth = Array.from({ length: LIST_TABLE_PATH_COLUMN_INDEX }).map((_, index) => resolveListColumnContentWidth({
2508
+ rows,
2509
+ columnIndex: index
2510
+ })).reduce((sum, width) => sum + width, 0);
2511
+ const borderWidth = LIST_TABLE_COLUMN_COUNT + 1;
2512
+ const paddingWidth = LIST_TABLE_COLUMN_COUNT * LIST_TABLE_CELL_HORIZONTAL_PADDING;
2513
+ const availablePathWidth = Math.floor(terminalColumns) - borderWidth - paddingWidth - measuredNonPathWidth;
2514
+ return Math.max(LIST_TABLE_PATH_MIN_WIDTH, availablePathWidth);
2515
+ };
2439
2516
  const resolveAheadBehindAgainstBaseBranch = async ({ repoRoot, baseBranch, worktree }) => {
2440
2517
  if (baseBranch === null) return {
2441
2518
  ahead: null,
@@ -2593,7 +2670,8 @@ const renderGeneralHelpText = ({ version }) => {
2593
2670
  " --json Output machine-readable JSON.",
2594
2671
  " --verbose Enable verbose logs.",
2595
2672
  " --no-hooks Disable hooks for this run (requires --allow-unsafe).",
2596
- " --no-gh Disable GitHub CLI based PR merge checks for this run.",
2673
+ " --no-gh Disable GitHub CLI based PR status checks for this run.",
2674
+ " --full-path Disable list table path truncation.",
2597
2675
  " --allow-unsafe Explicitly allow unsafe behavior in non-TTY mode.",
2598
2676
  " --hook-timeout-ms <ms> Override hook timeout.",
2599
2677
  " --lock-timeout-ms <ms> Override repository lock timeout.",
@@ -2696,9 +2774,13 @@ const createCli = (options = {}) => {
2696
2774
  },
2697
2775
  gh: {
2698
2776
  type: "boolean",
2699
- description: "Enable GitHub CLI based PR merge checks (disable with --no-gh)",
2777
+ description: "Enable GitHub CLI based PR status checks (disable with --no-gh)",
2700
2778
  default: true
2701
2779
  },
2780
+ fullPath: {
2781
+ type: "boolean",
2782
+ description: "Disable list table path truncation"
2783
+ },
2702
2784
  allowUnsafe: {
2703
2785
  type: "boolean",
2704
2786
  description: "Allow unsafe operations"
@@ -3025,36 +3107,53 @@ const createCli = (options = {}) => {
3025
3107
  return EXIT_CODE.OK;
3026
3108
  }
3027
3109
  const theme = createCatppuccinTheme({ enabled: shouldUseAnsiColors({ interactive: runtime.isInteractive }) });
3110
+ const rows = [[
3111
+ "branch",
3112
+ "dirty",
3113
+ "merged",
3114
+ "pr",
3115
+ "locked",
3116
+ "ahead",
3117
+ "behind",
3118
+ "path"
3119
+ ], ...await Promise.all(snapshot.worktrees.map(async (worktree) => {
3120
+ const distanceFromBase = await resolveAheadBehindAgainstBaseBranch({
3121
+ repoRoot,
3122
+ baseBranch: snapshot.baseBranch,
3123
+ worktree
3124
+ });
3125
+ const isBaseBranch = worktree.branch !== null && snapshot.baseBranch !== null && worktree.branch === snapshot.baseBranch;
3126
+ const mergedState = isBaseBranch === true ? "-" : worktree.merged.overall === true ? "merged" : worktree.merged.overall === false ? "unmerged" : "unknown";
3127
+ const prState = formatPrDisplayState({
3128
+ prStatus: worktree.pr.status,
3129
+ isBaseBranch
3130
+ });
3131
+ return [
3132
+ `${worktree.path === repoContext.currentWorktreeRoot ? "*" : " "} ${worktree.branch ?? "(detached)"}`,
3133
+ worktree.dirty ? "dirty" : "clean",
3134
+ mergedState,
3135
+ prState,
3136
+ worktree.locked.value ? "locked" : "-",
3137
+ formatListUpstreamCount(distanceFromBase.ahead),
3138
+ formatListUpstreamCount(distanceFromBase.behind),
3139
+ formatDisplayPath(worktree.path)
3140
+ ];
3141
+ }))];
3142
+ const pathColumnWidth = resolveListPathColumnWidth({
3143
+ rows,
3144
+ disablePathTruncation: parsedArgs.fullPath === true
3145
+ });
3146
+ const columnsConfig = pathColumnWidth === null ? void 0 : { [LIST_TABLE_PATH_COLUMN_INDEX]: {
3147
+ width: pathColumnWidth,
3148
+ truncate: pathColumnWidth
3149
+ } };
3028
3150
  const colorized = colorizeListTable({
3029
- rendered: table([[
3030
- "branch",
3031
- "dirty",
3032
- "merged",
3033
- "locked",
3034
- "ahead",
3035
- "behind",
3036
- "path"
3037
- ], ...await Promise.all(snapshot.worktrees.map(async (worktree) => {
3038
- const distanceFromBase = await resolveAheadBehindAgainstBaseBranch({
3039
- repoRoot,
3040
- baseBranch: snapshot.baseBranch,
3041
- worktree
3042
- });
3043
- const mergedState = (worktree.branch !== null && snapshot.baseBranch !== null && worktree.branch === snapshot.baseBranch) === true ? "-" : worktree.merged.overall === true ? "merged" : worktree.merged.overall === false ? "unmerged" : "unknown";
3044
- return [
3045
- `${worktree.path === repoContext.currentWorktreeRoot ? "*" : " "} ${worktree.branch ?? "(detached)"}`,
3046
- worktree.dirty ? "dirty" : "clean",
3047
- mergedState,
3048
- worktree.locked.value ? "locked" : "-",
3049
- formatListUpstreamCount(distanceFromBase.ahead),
3050
- formatListUpstreamCount(distanceFromBase.behind),
3051
- formatDisplayPath(worktree.path)
3052
- ];
3053
- }))], {
3151
+ rendered: table(rows, {
3054
3152
  border: getBorderCharacters("norc"),
3055
3153
  drawHorizontalLine: (lineIndex, rowCount) => {
3056
3154
  return lineIndex === 0 || lineIndex === 1 || lineIndex === rowCount;
3057
- }
3155
+ },
3156
+ columns: columnsConfig
3058
3157
  }),
3059
3158
  theme
3060
3159
  });