clawt 2.20.0 → 3.1.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 (77) hide show
  1. package/.claude/agents/docs-sync-updater.md +29 -11
  2. package/README.md +19 -30
  3. package/dist/index.js +1127 -222
  4. package/dist/postinstall.js +73 -8
  5. package/docs/alias.md +108 -0
  6. package/docs/completion.md +55 -0
  7. package/docs/config-file.md +43 -0
  8. package/docs/config.md +91 -0
  9. package/docs/create.md +85 -0
  10. package/docs/init.md +65 -0
  11. package/docs/list.md +67 -0
  12. package/docs/log.md +67 -0
  13. package/docs/merge.md +137 -0
  14. package/docs/notification.md +94 -0
  15. package/docs/projects.md +135 -0
  16. package/docs/remove.md +79 -0
  17. package/docs/reset.md +35 -0
  18. package/docs/resume.md +99 -0
  19. package/docs/run.md +146 -0
  20. package/docs/spec.md +157 -1906
  21. package/docs/status.md +298 -0
  22. package/docs/sync.md +114 -0
  23. package/docs/update-check.md +95 -0
  24. package/docs/validate.md +368 -0
  25. package/package.json +1 -1
  26. package/src/commands/alias.ts +1 -1
  27. package/src/commands/create.ts +10 -5
  28. package/src/commands/init.ts +75 -0
  29. package/src/commands/list.ts +1 -1
  30. package/src/commands/merge.ts +11 -4
  31. package/src/commands/remove.ts +10 -3
  32. package/src/commands/reset.ts +3 -0
  33. package/src/commands/resume.ts +1 -1
  34. package/src/commands/run.ts +9 -3
  35. package/src/commands/status.ts +14 -5
  36. package/src/commands/sync.ts +18 -6
  37. package/src/commands/validate.ts +46 -52
  38. package/src/constants/branch.ts +3 -0
  39. package/src/constants/config.ts +1 -1
  40. package/src/constants/index.ts +14 -2
  41. package/src/constants/interactive-panel.ts +44 -0
  42. package/src/constants/messages/completion.ts +1 -1
  43. package/src/constants/messages/create.ts +3 -0
  44. package/src/constants/messages/index.ts +4 -0
  45. package/src/constants/messages/init.ts +18 -0
  46. package/src/constants/messages/interactive-panel.ts +61 -0
  47. package/src/constants/messages/remove.ts +2 -0
  48. package/src/constants/messages/sync.ts +3 -0
  49. package/src/constants/messages/validate.ts +6 -0
  50. package/src/constants/paths.ts +3 -0
  51. package/src/index.ts +2 -0
  52. package/src/types/command.ts +9 -1
  53. package/src/types/index.ts +2 -1
  54. package/src/types/projectConfig.ts +5 -0
  55. package/src/utils/config.ts +2 -1
  56. package/src/utils/git.ts +18 -0
  57. package/src/utils/index.ts +9 -1
  58. package/src/utils/interactive-panel-render.ts +315 -0
  59. package/src/utils/interactive-panel.ts +590 -0
  60. package/src/utils/json.ts +67 -0
  61. package/src/utils/project-config.ts +77 -0
  62. package/src/utils/validate-branch.ts +166 -0
  63. package/src/utils/worktree-matcher.ts +2 -2
  64. package/src/utils/worktree.ts +6 -2
  65. package/tests/unit/commands/create.test.ts +20 -16
  66. package/tests/unit/commands/init.test.ts +146 -0
  67. package/tests/unit/commands/merge.test.ts +7 -1
  68. package/tests/unit/commands/remove.test.ts +4 -0
  69. package/tests/unit/commands/reset.test.ts +2 -0
  70. package/tests/unit/commands/run.test.ts +2 -0
  71. package/tests/unit/commands/sync.test.ts +6 -0
  72. package/tests/unit/commands/validate.test.ts +13 -0
  73. package/tests/unit/utils/config.test.ts +2 -2
  74. package/tests/unit/utils/project-config.test.ts +136 -0
  75. package/tests/unit/utils/update-checker.test.ts +28 -7
  76. package/tests/unit/utils/validate-branch.test.ts +272 -0
  77. package/tests/unit/utils/worktree.test.ts +6 -0
@@ -1,8 +1,9 @@
1
1
  export type { ClawtConfig, ConfigItemDefinition, ConfigDefinitions } from './config.js';
2
- export type { CreateOptions, RunOptions, ValidateOptions, MergeOptions, RemoveOptions, ResumeOptions, SyncOptions, ListOptions, StatusOptions, ProjectsOptions } from './command.js';
2
+ export type { CreateOptions, RunOptions, ValidateOptions, MergeOptions, RemoveOptions, ResumeOptions, SyncOptions, ListOptions, StatusOptions, ProjectsOptions, InitOptions } from './command.js';
3
3
  export type { WorktreeInfo, WorktreeStatus } from './worktree.js';
4
4
  export type { ClaudeCodeResult } from './claudeCode.js';
5
5
  export type { TaskResult, TaskSummary } from './taskResult.js';
6
6
  export type { WorktreeDetailedStatus, MainWorktreeStatus, SnapshotInfo, SnapshotSummary, StatusResult } from './status.js';
7
7
  export type { TaskFileEntry, ParseTaskFileOptions } from './taskFile.js';
8
8
  export type { ProjectOverview, ProjectWorktreeDetail, ProjectDetailResult, ProjectsOverviewResult } from './project.js';
9
+ export type { ProjectConfig } from './projectConfig.js';
@@ -0,0 +1,5 @@
1
+ /** 项目级配置(存储在 ~/.clawt/projects/<projectName>.json) */
2
+ export interface ProjectConfig {
3
+ /** 主 worktree 的工作分支名 */
4
+ clawtMainWorkBranch: string;
5
+ }
@@ -1,5 +1,5 @@
1
1
  import { existsSync, readFileSync, writeFileSync } from 'node:fs';
2
- import { CONFIG_PATH, CLAWT_HOME, LOGS_DIR, WORKTREES_DIR, DEFAULT_CONFIG, MESSAGES } from '../constants/index.js';
2
+ import { CONFIG_PATH, CLAWT_HOME, LOGS_DIR, WORKTREES_DIR, DEFAULT_CONFIG, MESSAGES, PROJECTS_CONFIG_DIR } from '../constants/index.js';
3
3
  import { ClawtError } from '../errors/index.js';
4
4
  import { ensureDir } from './fs.js';
5
5
  import { logger } from '../logger/index.js';
@@ -65,6 +65,7 @@ export function ensureClawtDirs(): void {
65
65
  ensureDir(CLAWT_HOME);
66
66
  ensureDir(LOGS_DIR);
67
67
  ensureDir(WORKTREES_DIR);
68
+ ensureDir(PROJECTS_CONFIG_DIR);
68
69
  }
69
70
 
70
71
  /**
package/src/utils/git.ts CHANGED
@@ -501,3 +501,21 @@ export function getBranchCreatedAt(branchName: string, cwd?: string): string | n
501
501
  return null;
502
502
  }
503
503
  }
504
+
505
+ /**
506
+ * 切换到指定分支
507
+ * @param {string} branchName - 目标分支名
508
+ * @param {string} [cwd] - 工作目录
509
+ */
510
+ export function gitCheckout(branchName: string, cwd?: string): void {
511
+ execCommand(`git checkout ${branchName}`, { cwd });
512
+ }
513
+
514
+ /**
515
+ * 创建本地分支(不切换)
516
+ * @param {string} branchName - 新分支名
517
+ * @param {string} [cwd] - 工作目录
518
+ */
519
+ export function createBranch(branchName: string, cwd?: string): void {
520
+ execCommand(`git branch ${branchName}`, { cwd });
521
+ }
@@ -46,6 +46,8 @@ export {
46
46
  gitDiffTree,
47
47
  gitApplyCachedCheck,
48
48
  getBranchCreatedAt,
49
+ gitCheckout,
50
+ createBranch,
49
51
  } from './git.js';
50
52
  export { sanitizeBranchName, generateBranchNames, validateBranchesNotExist } from './branch.js';
51
53
  export { validateMainWorktree, validateGitInstalled, validateClaudeCodeInstalled } from './validation.js';
@@ -56,7 +58,7 @@ export { ensureDir, removeEmptyDir, calculateDirSize } from './fs.js';
56
58
  export { multilineInput } from './prompt.js';
57
59
  export { launchInteractiveClaude, hasClaudeSessionHistory, launchInteractiveClaudeInNewTerminal } from './claude.js';
58
60
  export { getSnapshotPath, hasSnapshot, getSnapshotModifiedTime, readSnapshotTreeHash, readSnapshot, writeSnapshot, removeSnapshot, removeProjectSnapshots, getProjectSnapshotBranches } from './validate-snapshot.js';
59
- export { findExactMatch, findFuzzyMatches, promptSelectBranch, promptMultiSelectBranches, promptGroupedMultiSelectBranches, resolveTargetWorktree, resolveTargetWorktrees, groupWorktreesByDate, buildGroupedChoices, buildGroupMembershipMap } from './worktree-matcher.js';
61
+ export { findExactMatch, findFuzzyMatches, promptSelectBranch, promptMultiSelectBranches, promptGroupedMultiSelectBranches, resolveTargetWorktree, resolveTargetWorktrees, groupWorktreesByDate, buildGroupedChoices, buildGroupMembershipMap, formatRelativeDate, getWorktreeCreatedDate } from './worktree-matcher.js';
60
62
  export type { WorktreeResolveMessages, WorktreeMultiResolveMessages } from './worktree-matcher.js';
61
63
  export { ProgressRenderer } from './progress.js';
62
64
  export { parseTaskFile, loadTaskFile, parseTasksFromOptions } from './task-file.js';
@@ -68,4 +70,10 @@ export { truncateTaskDesc, printDryRunPreview } from './dry-run.js';
68
70
  export { applyAliases } from './alias.js';
69
71
  export { isValidConfigKey, getValidConfigKeys, parseConfigValue, promptConfigValue, formatConfigValue } from './config-strategy.js';
70
72
  export { checkForUpdates } from './update-checker.js';
73
+ export { getProjectConfigPath, loadProjectConfig, saveProjectConfig, requireProjectConfig, getMainWorkBranch } from './project-config.js';
74
+ export { getValidateBranchName, createValidateBranch, deleteValidateBranch, rebuildValidateBranch, ensureOnMainWorkBranch, handleDirtyWorkingDir } from './validate-branch.js';
75
+ export { safeStringify } from './json.js';
76
+ export { InteractivePanel } from './interactive-panel.js';
77
+ export { buildPanelFrame, buildGroupedWorktreeLines, buildDisplayOrder, renderDateSeparator, renderWorktreeBlock, renderSnapshotSummary, renderFooter, calculateVisibleRows } from './interactive-panel-render.js';
78
+ export type { PanelLine } from './interactive-panel-render.js';
71
79
 
@@ -0,0 +1,315 @@
1
+ import chalk from 'chalk';
2
+ import stringWidth from 'string-width';
3
+ import {
4
+ MESSAGES,
5
+ SELECTED_INDICATOR,
6
+ UNSELECTED_INDICATOR,
7
+ PANEL_DATE_SEPARATOR_PREFIX,
8
+ PANEL_FIXED_ROWS,
9
+ UNKNOWN_DATE_GROUP,
10
+ } from '../constants/index.js';
11
+ import {
12
+ PANEL_FOOTER_SHORTCUTS,
13
+ PANEL_FOOTER_COUNTDOWN,
14
+ PANEL_OVERFLOW_DOWN_HINT,
15
+ PANEL_OVERFLOW_UP_HINT,
16
+ PANEL_SNAPSHOT_SUMMARY,
17
+ PANEL_NO_WORKTREES_MSG,
18
+ PANEL_TITLE,
19
+ } from '../constants/messages/index.js';
20
+ import type { StatusResult, WorktreeDetailedStatus } from '../types/index.js';
21
+ import { formatRelativeTime, groupWorktreesByDate, formatRelativeDate } from './index.js';
22
+
23
+ /** 面板行类型 */
24
+ export interface PanelLine {
25
+ /** 行类型:separator(日期分隔线)或 worktree-content(worktree 内容行) */
26
+ type: 'separator' | 'worktree-content';
27
+ /** 行文本内容 */
28
+ text: string;
29
+ /** worktree 在原列表中的索引(仅 worktree-content 行有值,用于滚动定位) */
30
+ worktreeIndex?: number;
31
+ }
32
+
33
+ /**
34
+ * 构建带可选溢出提示的分隔线
35
+ * 提示文本居中嵌入分隔线中间,不额外占用行数
36
+ * @param {number} cols - 终端列数
37
+ * @param {string} hint - 溢出提示文本(为空则输出纯分隔线)
38
+ * @returns {string} 格式化的分隔线
39
+ */
40
+ function buildSeparatorWithHint(cols: number, hint: string): string {
41
+ const maxWidth = Math.min(cols, 60);
42
+ if (!hint) {
43
+ return chalk.gray('─'.repeat(maxWidth));
44
+ }
45
+ const hintWidth = stringWidth(hint);
46
+ // 左右各留一个空格包裹提示文本,剩余宽度平分给两侧分隔线
47
+ const remaining = Math.max(maxWidth - 2 - hintWidth, 0);
48
+ const leftLen = Math.floor(remaining / 2);
49
+ const rightLen = remaining - leftLen;
50
+ return `${chalk.gray('─'.repeat(leftLen))} ${hint} ${chalk.gray('─'.repeat(rightLen))}`;
51
+ }
52
+
53
+ /**
54
+ * 构建完整的面板帧内容
55
+ * @param {StatusResult} statusResult - 状态数据
56
+ * @param {number} selectedIndex - 当前选中的 worktree 索引
57
+ * @param {number} scrollOffset - 滚动偏移(基于行数)
58
+ * @param {number} rows - 终端行数
59
+ * @param {number} cols - 终端列数
60
+ * @param {number} countdown - 刷新倒计时秒数
61
+ * @returns {string[]} 帧内容行数组
62
+ */
63
+ export function buildPanelFrame(
64
+ statusResult: StatusResult,
65
+ selectedIndex: number,
66
+ scrollOffset: number,
67
+ rows: number,
68
+ cols: number,
69
+ countdown: number,
70
+ ): string[] {
71
+ const lines: string[] = [];
72
+
73
+ // 标题行
74
+ lines.push(PANEL_TITLE(statusResult.main.projectName));
75
+
76
+ // 快照摘要行
77
+ lines.push(renderSnapshotSummary(statusResult.snapshots.total, statusResult.snapshots.orphaned));
78
+
79
+ // 计算可用的 worktree 显示区域行数
80
+ const visibleRows = calculateVisibleRows(rows);
81
+
82
+ if (statusResult.worktrees.length === 0) {
83
+ // 顶部分隔线(无溢出提示)
84
+ lines.push(buildSeparatorWithHint(cols, ''));
85
+ lines.push(PANEL_NO_WORKTREES_MSG);
86
+ // 底部分隔线(无溢出提示)
87
+ lines.push(buildSeparatorWithHint(cols, ''));
88
+ } else {
89
+ // 构建分组的 worktree 行列表
90
+ const panelLines = buildGroupedWorktreeLines(statusResult.worktrees, selectedIndex);
91
+
92
+ // 判断溢出状态
93
+ const hasOverflowUp = scrollOffset > 0;
94
+ const hasOverflowDown = scrollOffset + visibleRows < panelLines.length;
95
+
96
+ // 顶部分隔线(带可选的上溢出提示)
97
+ lines.push(buildSeparatorWithHint(cols, hasOverflowUp ? PANEL_OVERFLOW_UP_HINT : ''));
98
+
99
+ // 根据滚动偏移截取可见区域
100
+ const visibleLines = panelLines.slice(scrollOffset, scrollOffset + visibleRows);
101
+ for (const pl of visibleLines) {
102
+ lines.push(pl.text);
103
+ }
104
+
105
+ // 底部分隔线(带可选的下溢出提示)
106
+ lines.push(buildSeparatorWithHint(cols, hasOverflowDown ? PANEL_OVERFLOW_DOWN_HINT : ''));
107
+ }
108
+
109
+ // 底栏
110
+ lines.push(renderFooter(countdown));
111
+
112
+ return lines;
113
+ }
114
+
115
+ /**
116
+ * 构建按日期分组后的显示顺序索引列表
117
+ * 用于将"显示中第 N 个 worktree"映射到原始 worktrees 数组中的索引
118
+ * @param {WorktreeDetailedStatus[]} worktrees - worktree 详细状态列表
119
+ * @returns {number[]} 按显示顺序排列的原始索引数组
120
+ */
121
+ export function buildDisplayOrder(worktrees: WorktreeDetailedStatus[]): number[] {
122
+ const worktreeInfos = worktrees.map((wt) => ({ path: wt.path, branch: wt.branch }));
123
+ const groups = groupWorktreesByDate(worktreeInfos);
124
+
125
+ // 构建分支名到原始索引的映射
126
+ const branchIndexMap = new Map<string, number>();
127
+ for (let i = 0; i < worktrees.length; i++) {
128
+ branchIndexMap.set(worktrees[i].branch, i);
129
+ }
130
+
131
+ const displayOrder: number[] = [];
132
+ for (const [, groupWorktrees] of groups) {
133
+ for (const gwt of groupWorktrees) {
134
+ displayOrder.push(branchIndexMap.get(gwt.branch)!);
135
+ }
136
+ }
137
+ return displayOrder;
138
+ }
139
+
140
+ /**
141
+ * 按日期分组构建 worktree 行列表
142
+ * @param {WorktreeDetailedStatus[]} worktrees - worktree 详细状态列表
143
+ * @param {number} selectedIndex - 当前选中的 worktree 原始索引
144
+ * @returns {PanelLine[]} 扁平的面板行列表
145
+ */
146
+ export function buildGroupedWorktreeLines(worktrees: WorktreeDetailedStatus[], selectedIndex: number): PanelLine[] {
147
+ const panelLines: PanelLine[] = [];
148
+
149
+ // 构建临时 WorktreeInfo 兼容结构用于分组(groupWorktreesByDate 需要 path 字段)
150
+ const worktreeInfos = worktrees.map((wt) => ({ path: wt.path, branch: wt.branch }));
151
+ const groups = groupWorktreesByDate(worktreeInfos);
152
+
153
+ // 构建分支名到原始索引的映射
154
+ const branchIndexMap = new Map<string, number>();
155
+ for (let i = 0; i < worktrees.length; i++) {
156
+ branchIndexMap.set(worktrees[i].branch, i);
157
+ }
158
+
159
+ for (const [dateKey, groupWorktrees] of groups) {
160
+ // 日期分隔线
161
+ panelLines.push({
162
+ type: 'separator',
163
+ text: renderDateSeparator(dateKey),
164
+ });
165
+
166
+ // 日期分隔线下方空行
167
+ panelLines.push({
168
+ type: 'separator',
169
+ text: '',
170
+ });
171
+
172
+ // 该组内的每个 worktree
173
+ for (const gwt of groupWorktrees) {
174
+ const wtIndex = branchIndexMap.get(gwt.branch)!;
175
+ const wt = worktrees[wtIndex];
176
+ const isSelected = wtIndex === selectedIndex;
177
+ const blockLines = renderWorktreeBlock(wt, isSelected);
178
+
179
+ for (const line of blockLines) {
180
+ panelLines.push({
181
+ type: 'worktree-content',
182
+ text: line,
183
+ worktreeIndex: wtIndex,
184
+ });
185
+ }
186
+ }
187
+ }
188
+
189
+ return panelLines;
190
+ }
191
+
192
+ /**
193
+ * 渲染日期分隔线
194
+ * 左侧预留空格与箭头指示器对齐,日期部分橙色高亮
195
+ * @param {string} dateKey - 日期字符串(YYYY-MM-DD 或 UNKNOWN_DATE_GROUP)
196
+ * @returns {string} 格式化的日期分隔线
197
+ */
198
+ export function renderDateSeparator(dateKey: string): string {
199
+ // 左侧空格与指示器 '▶ ' 等宽对齐
200
+ const leftPad = ' ';
201
+ if (dateKey === UNKNOWN_DATE_GROUP) {
202
+ return `${leftPad}${chalk.gray(PANEL_DATE_SEPARATOR_PREFIX)} ${chalk.bold.hex('#FF8C00')('未知日期')} ${chalk.gray(PANEL_DATE_SEPARATOR_PREFIX)}`;
203
+ }
204
+ const relativeDate = formatRelativeDate(dateKey);
205
+ return `${leftPad}${chalk.gray(PANEL_DATE_SEPARATOR_PREFIX)} ${chalk.bold.hex('#FF8C00')(dateKey)}${chalk.hex('#FF8C00')(`(${relativeDate})`)} ${chalk.gray(PANEL_DATE_SEPARATOR_PREFIX)}`;
206
+ }
207
+
208
+ /**
209
+ * 渲染单个 worktree 的多行块
210
+ * 与 printWorktreeItem 逻辑一致,但返回字符串数组而非直接打印
211
+ * @param {WorktreeDetailedStatus} wt - worktree 详细状态
212
+ * @param {boolean} isSelected - 是否为选中项
213
+ * @returns {string[]} 渲染后的行数组
214
+ */
215
+ export function renderWorktreeBlock(wt: WorktreeDetailedStatus, isSelected: boolean): string[] {
216
+ const lines: string[] = [];
217
+ const indicator = isSelected ? chalk.cyan(SELECTED_INDICATOR) : UNSELECTED_INDICATOR;
218
+ const branchStyle = isSelected ? chalk.bold.cyan : chalk.bold;
219
+
220
+ // 分支名 + 变更状态标签
221
+ const statusLabel = formatChangeStatusLabel(wt.changeStatus);
222
+ lines.push(`${indicator} ${branchStyle(wt.branch)} [${statusLabel}]`);
223
+
224
+ // 缩进前缀(与指示器对齐)
225
+ const indent = ' ';
226
+
227
+ // 变更行数
228
+ if (wt.insertions > 0 || wt.deletions > 0) {
229
+ lines.push(`${indent}${chalk.green(`+${wt.insertions}`)} ${chalk.red(`-${wt.deletions}`)}`);
230
+ }
231
+
232
+ // 本地提交数
233
+ if (wt.commitsAhead > 0) {
234
+ lines.push(`${indent}${chalk.yellow(`${wt.commitsAhead} 个本地提交`)}`);
235
+ }
236
+
237
+ // 与主分支的同步状态
238
+ if (wt.commitsBehind > 0) {
239
+ lines.push(`${indent}${chalk.yellow(`落后主分支 ${wt.commitsBehind} 个提交`)}`);
240
+ } else {
241
+ lines.push(`${indent}${chalk.green('与主分支同步')}`);
242
+ }
243
+
244
+ // 分支创建时间
245
+ if (wt.createdAt) {
246
+ const relativeTime = formatRelativeTime(wt.createdAt);
247
+ if (relativeTime) {
248
+ lines.push(`${indent}${chalk.gray(MESSAGES.STATUS_CREATED_AT(relativeTime))}`);
249
+ }
250
+ }
251
+
252
+ // 验证状态
253
+ if (wt.snapshotTime) {
254
+ const relativeTime = formatRelativeTime(wt.snapshotTime);
255
+ if (relativeTime) {
256
+ lines.push(`${indent}${chalk.green(MESSAGES.STATUS_LAST_VALIDATED(relativeTime))}`);
257
+ }
258
+ } else {
259
+ lines.push(`${indent}${chalk.red(MESSAGES.STATUS_NOT_VALIDATED)}`);
260
+ }
261
+
262
+ // worktree 块之间的空行
263
+ lines.push('');
264
+
265
+ return lines;
266
+ }
267
+
268
+ /**
269
+ * 将变更状态枚举格式化为带颜色的标签文本
270
+ * @param {WorktreeDetailedStatus['changeStatus']} status - 变更状态
271
+ * @returns {string} 格式化后的标签
272
+ */
273
+ function formatChangeStatusLabel(status: WorktreeDetailedStatus['changeStatus']): string {
274
+ switch (status) {
275
+ case 'committed':
276
+ return chalk.green(MESSAGES.STATUS_CHANGE_COMMITTED);
277
+ case 'uncommitted':
278
+ return chalk.yellow(MESSAGES.STATUS_CHANGE_UNCOMMITTED);
279
+ case 'conflict':
280
+ return chalk.red(MESSAGES.STATUS_CHANGE_CONFLICT);
281
+ case 'clean':
282
+ return chalk.gray(MESSAGES.STATUS_CHANGE_CLEAN);
283
+ }
284
+ }
285
+
286
+ /**
287
+ * 渲染快照摘要行
288
+ * @param {number} total - 快照总数
289
+ * @param {number} orphaned - 孤立快照数
290
+ * @returns {string} 格式化的快照摘要文本
291
+ */
292
+ export function renderSnapshotSummary(total: number, orphaned: number): string {
293
+ return PANEL_SNAPSHOT_SUMMARY(total, orphaned);
294
+ }
295
+
296
+ /**
297
+ * 渲染底栏(快捷键提示 + 倒计时)
298
+ * @param {number} countdown - 刷新倒计时秒数
299
+ * @returns {string} 底栏文本
300
+ */
301
+ export function renderFooter(countdown: number): string {
302
+ return `${PANEL_FOOTER_SHORTCUTS} ${PANEL_FOOTER_COUNTDOWN(countdown)}`;
303
+ }
304
+
305
+ /**
306
+ * 计算 worktree 滚动区域的总高度
307
+ * 溢出提示已嵌入分隔线,不再占用滚动区域行数
308
+ * @param {number} terminalRows - 终端总行数
309
+ * @returns {number} 滚动区域总行数
310
+ */
311
+ export function calculateVisibleRows(terminalRows: number): number {
312
+ // 固定行:标题(1) + 快照摘要 + 顶部分隔线 + 底部分隔线 + 底栏(后四项 = PANEL_FIXED_ROWS)
313
+ const fixedRows = PANEL_FIXED_ROWS + 1;
314
+ return Math.max(terminalRows - fixedRows, 3);
315
+ }