clawt 3.4.6 → 3.5.1
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/.claude/settings.local.json +12 -0
- package/README.md +0 -4
- package/dist/index.js +583 -314
- package/dist/postinstall.js +37 -2
- package/docs/alias.md +7 -1
- package/docs/completion.md +1 -1
- package/docs/config.md +4 -3
- package/docs/cover-validate.md +4 -3
- package/docs/create.md +28 -12
- package/docs/home.md +12 -8
- package/docs/init.md +16 -9
- package/docs/list.md +13 -7
- package/docs/merge.md +12 -12
- package/docs/remove.md +24 -13
- package/docs/reset.md +6 -4
- package/docs/resume.md +3 -4
- package/docs/status.md +75 -30
- package/docs/sync.md +26 -26
- package/docs/validate.md +13 -7
- package/package.json +1 -1
- package/src/commands/merge.ts +20 -5
- package/src/commands/tasks.ts +51 -0
- package/src/constants/ai-prompts.ts +14 -0
- package/src/constants/config.ts +9 -0
- package/src/constants/index.ts +4 -0
- package/src/constants/interactive-panel.ts +6 -0
- package/src/constants/messages/index.ts +4 -2
- package/src/constants/messages/interactive-panel.ts +12 -0
- package/src/constants/messages/merge.ts +15 -0
- package/src/constants/messages/tasks.ts +9 -0
- package/src/constants/tasks-template.ts +28 -0
- package/src/index.ts +2 -0
- package/src/types/command.ts +8 -0
- package/src/types/config.ts +4 -0
- package/src/types/index.ts +1 -1
- package/src/utils/conflict-resolver.ts +170 -0
- package/src/utils/formatter.ts +19 -0
- package/src/utils/git-branch.ts +116 -0
- package/src/utils/git-core.ts +417 -0
- package/src/utils/git-worktree.ts +40 -0
- package/src/utils/git.ts +3 -521
- package/src/utils/index.ts +7 -2
- package/src/utils/interactive-panel-render.ts +12 -6
- package/src/utils/interactive-panel-state.ts +137 -0
- package/src/utils/interactive-panel.ts +44 -188
- package/src/utils/keyboard-controller.ts +48 -0
- package/src/utils/ui-prompts.ts +240 -0
- package/src/utils/worktree-matcher.ts +21 -251
- package/tests/unit/commands/merge.test.ts +59 -3
- package/tests/unit/commands/tasks.test.ts +153 -0
- package/tests/unit/utils/conflict-resolver.test.ts +250 -0
- package/tests/unit/utils/formatter.test.ts +26 -1
- package/src/constants/messages.ts +0 -179
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import type { StatusResult } from '../types/index.js';
|
|
2
|
+
import { buildDisplayOrder, calculateVisibleRows, buildGroupedWorktreeLines } from './interactive-panel-render.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* 面板状态管理器
|
|
6
|
+
* 负责维护面板的数据状态、滚动偏移和选中项
|
|
7
|
+
*/
|
|
8
|
+
export class PanelStateManager {
|
|
9
|
+
/** 当前状态数据 */
|
|
10
|
+
private statusResult: StatusResult | null = null;
|
|
11
|
+
/** 当前选中的显示位置索引(对应 displayOrder 数组的下标) */
|
|
12
|
+
private selectedDisplayIndex: number = 0;
|
|
13
|
+
/** 显示顺序到原始索引的映射(按日期分组后的排列顺序) */
|
|
14
|
+
private displayOrder: number[] = [];
|
|
15
|
+
/** 滚动偏移(基于行数) */
|
|
16
|
+
private scrollOffset: number = 0;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* 更新状态数据
|
|
20
|
+
* @param {StatusResult} newStatus - 新的状态数据
|
|
21
|
+
* @param {string} [previousBranch] - 刷新前选中的分支名
|
|
22
|
+
*/
|
|
23
|
+
updateData(newStatus: StatusResult, previousBranch?: string): void {
|
|
24
|
+
this.statusResult = newStatus;
|
|
25
|
+
this.displayOrder = buildDisplayOrder(this.statusResult.worktrees);
|
|
26
|
+
|
|
27
|
+
if (previousBranch && this.displayOrder.length > 0) {
|
|
28
|
+
const newDisplayIndex = this.displayOrder.findIndex(
|
|
29
|
+
(origIdx) => this.statusResult!.worktrees[origIdx]?.branch === previousBranch,
|
|
30
|
+
);
|
|
31
|
+
if (newDisplayIndex >= 0) {
|
|
32
|
+
this.selectedDisplayIndex = newDisplayIndex;
|
|
33
|
+
} else {
|
|
34
|
+
this.selectedDisplayIndex = Math.min(this.selectedDisplayIndex, Math.max(0, this.displayOrder.length - 1));
|
|
35
|
+
}
|
|
36
|
+
} else {
|
|
37
|
+
this.selectedDisplayIndex = 0;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** 获取当前状态数据 */
|
|
42
|
+
getStatusResult(): StatusResult | null {
|
|
43
|
+
return this.statusResult;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** 获取当前选中的原始索引 */
|
|
47
|
+
getSelectedOriginalIndex(): number {
|
|
48
|
+
return this.displayOrder[this.selectedDisplayIndex] ?? -1;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** 获取当前滚动偏移 */
|
|
52
|
+
getScrollOffset(): number {
|
|
53
|
+
return this.scrollOffset;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* 向上导航
|
|
58
|
+
* @returns {boolean} 是否发生变化
|
|
59
|
+
*/
|
|
60
|
+
navigateUp(): boolean {
|
|
61
|
+
if (!this.statusResult || this.displayOrder.length === 0) return false;
|
|
62
|
+
|
|
63
|
+
if (this.selectedDisplayIndex > 0) {
|
|
64
|
+
this.selectedDisplayIndex--;
|
|
65
|
+
this.adjustScrollForSelection();
|
|
66
|
+
return true;
|
|
67
|
+
}
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* 向下导航
|
|
73
|
+
* @returns {boolean} 是否发生变化
|
|
74
|
+
*/
|
|
75
|
+
navigateDown(): boolean {
|
|
76
|
+
if (!this.statusResult || this.displayOrder.length === 0) return false;
|
|
77
|
+
|
|
78
|
+
if (this.selectedDisplayIndex < this.displayOrder.length - 1) {
|
|
79
|
+
this.selectedDisplayIndex++;
|
|
80
|
+
this.adjustScrollForSelection();
|
|
81
|
+
return true;
|
|
82
|
+
}
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* 获取当前选中的分支名
|
|
88
|
+
* @returns {string | null} 分支名
|
|
89
|
+
*/
|
|
90
|
+
getSelectedBranch(): string | null {
|
|
91
|
+
const originalIndex = this.getSelectedOriginalIndex();
|
|
92
|
+
if (originalIndex === -1 || !this.statusResult) return null;
|
|
93
|
+
return this.statusResult.worktrees[originalIndex]?.branch || null;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* 调整滚动位置以确保选中项在可见区域内
|
|
98
|
+
*/
|
|
99
|
+
adjustScrollForSelection(): void {
|
|
100
|
+
if (!this.statusResult || this.displayOrder.length === 0) return;
|
|
101
|
+
|
|
102
|
+
const originalIndex = this.getSelectedOriginalIndex();
|
|
103
|
+
const rows = process.stdout.rows || 24;
|
|
104
|
+
const visibleRows = calculateVisibleRows(rows);
|
|
105
|
+
const panelLines = buildGroupedWorktreeLines(this.statusResult.worktrees, originalIndex);
|
|
106
|
+
|
|
107
|
+
// 找到选中 worktree 对应的第一行和最后一行
|
|
108
|
+
let firstLine = -1;
|
|
109
|
+
let lastLine = -1;
|
|
110
|
+
for (let i = 0; i < panelLines.length; i++) {
|
|
111
|
+
if (panelLines[i].worktreeIndex === originalIndex) {
|
|
112
|
+
if (firstLine === -1) firstLine = i;
|
|
113
|
+
lastLine = i;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (firstLine === -1) return;
|
|
118
|
+
|
|
119
|
+
// 向前查找该 worktree 所属日期分组的分隔线行
|
|
120
|
+
let groupStart = firstLine;
|
|
121
|
+
while (groupStart > 0 && panelLines[groupStart - 1].type === 'separator') {
|
|
122
|
+
groupStart--;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (groupStart < this.scrollOffset) {
|
|
126
|
+
this.scrollOffset = groupStart;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (lastLine >= this.scrollOffset + visibleRows) {
|
|
130
|
+
this.scrollOffset = lastLine - visibleRows + 1;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (this.scrollOffset > groupStart) {
|
|
134
|
+
this.scrollOffset = groupStart;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
@@ -20,9 +20,11 @@ import {
|
|
|
20
20
|
} from '../constants/index.js';
|
|
21
21
|
import { PANEL_NOT_TTY, PANEL_PRESS_ENTER_TO_RETURN } from '../constants/messages/index.js';
|
|
22
22
|
import { runCommandInherited } from './shell.js';
|
|
23
|
-
import { buildPanelFrame
|
|
23
|
+
import { buildPanelFrame } from './interactive-panel-render.js';
|
|
24
24
|
import { truncateToTerminalWidth } from './progress-render.js';
|
|
25
25
|
import type { StatusResult } from '../types/index.js';
|
|
26
|
+
import { KeyboardController } from './keyboard-controller.js';
|
|
27
|
+
import { PanelStateManager } from './interactive-panel-state.js';
|
|
26
28
|
|
|
27
29
|
/**
|
|
28
30
|
* 交互式状态面板
|
|
@@ -31,14 +33,10 @@ import type { StatusResult } from '../types/index.js';
|
|
|
31
33
|
* 参照 ProgressRenderer 的生命周期模式实现。
|
|
32
34
|
*/
|
|
33
35
|
export class InteractivePanel {
|
|
34
|
-
/**
|
|
35
|
-
private
|
|
36
|
-
/**
|
|
37
|
-
private
|
|
38
|
-
/** 显示顺序到原始索引的映射(按日期分组后的排列顺序) */
|
|
39
|
-
private displayOrder: number[];
|
|
40
|
-
/** 滚动偏移(基于行数) */
|
|
41
|
-
private scrollOffset: number;
|
|
36
|
+
/** 状态管理器 */
|
|
37
|
+
private stateManager: PanelStateManager;
|
|
38
|
+
/** 键盘控制器 */
|
|
39
|
+
private keyboardController: KeyboardController;
|
|
42
40
|
/** 数据刷新定时器引用 */
|
|
43
41
|
private refreshTimer: ReturnType<typeof setInterval> | null;
|
|
44
42
|
/** 倒计时定时器引用 */
|
|
@@ -53,8 +51,6 @@ export class InteractivePanel {
|
|
|
53
51
|
private resizeHandler: (() => void) | null;
|
|
54
52
|
/** exit 兜底处理器 */
|
|
55
53
|
private exitHandler: (() => void) | null;
|
|
56
|
-
/** stdin 数据处理器引用(用于清理) */
|
|
57
|
-
private stdinDataHandler: ((data: Buffer) => void) | null;
|
|
58
54
|
/** 操作锁(防止操作期间响应按键) */
|
|
59
55
|
private isOperating: boolean;
|
|
60
56
|
/** Promise resolve 函数(stop 时调用以完成 start 返回的 Promise) */
|
|
@@ -67,10 +63,8 @@ export class InteractivePanel {
|
|
|
67
63
|
* @param {() => StatusResult} collectStatusFn - 数据收集函数
|
|
68
64
|
*/
|
|
69
65
|
constructor(collectStatusFn: () => StatusResult) {
|
|
70
|
-
this.
|
|
71
|
-
this.
|
|
72
|
-
this.displayOrder = [];
|
|
73
|
-
this.scrollOffset = 0;
|
|
66
|
+
this.stateManager = new PanelStateManager();
|
|
67
|
+
this.keyboardController = new KeyboardController(this.handleKeypress.bind(this));
|
|
74
68
|
this.refreshTimer = null;
|
|
75
69
|
this.countdownTimer = null;
|
|
76
70
|
this.refreshCountdown = PANEL_REFRESH_INTERVAL_MS / 1000;
|
|
@@ -78,7 +72,6 @@ export class InteractivePanel {
|
|
|
78
72
|
this.isTTY = !!process.stdout.isTTY;
|
|
79
73
|
this.resizeHandler = null;
|
|
80
74
|
this.exitHandler = null;
|
|
81
|
-
this.stdinDataHandler = null;
|
|
82
75
|
this.isOperating = false;
|
|
83
76
|
this.resolveStart = null;
|
|
84
77
|
this.collectStatusFn = collectStatusFn;
|
|
@@ -100,14 +93,13 @@ export class InteractivePanel {
|
|
|
100
93
|
this.resolveStart = resolve;
|
|
101
94
|
|
|
102
95
|
// 收集初始数据
|
|
103
|
-
this.
|
|
104
|
-
this.displayOrder = buildDisplayOrder(this.statusResult.worktrees);
|
|
96
|
+
this.stateManager.updateData(this.collectStatusFn());
|
|
105
97
|
|
|
106
98
|
// 初始化终端
|
|
107
99
|
this.initTerminal();
|
|
108
100
|
|
|
109
101
|
// 启动键盘监听
|
|
110
|
-
this.
|
|
102
|
+
this.keyboardController.start();
|
|
111
103
|
|
|
112
104
|
// 启动自动刷新
|
|
113
105
|
this.startAutoRefresh();
|
|
@@ -128,7 +120,7 @@ export class InteractivePanel {
|
|
|
128
120
|
this.clearTimers();
|
|
129
121
|
|
|
130
122
|
// 停止键盘监听
|
|
131
|
-
this.
|
|
123
|
+
this.keyboardController.stop();
|
|
132
124
|
|
|
133
125
|
// 恢复终端
|
|
134
126
|
this.restoreTerminal();
|
|
@@ -163,6 +155,8 @@ export class InteractivePanel {
|
|
|
163
155
|
// 监听终端宽度变化,立即触发重绘
|
|
164
156
|
this.resizeHandler = () => {
|
|
165
157
|
if (!this.stopped && !this.isOperating) {
|
|
158
|
+
// 在调整大小时也调整滚动
|
|
159
|
+
this.stateManager.adjustScrollForSelection();
|
|
166
160
|
this.render();
|
|
167
161
|
}
|
|
168
162
|
};
|
|
@@ -186,36 +180,6 @@ export class InteractivePanel {
|
|
|
186
180
|
process.stdout.write(ALT_SCREEN_LEAVE);
|
|
187
181
|
}
|
|
188
182
|
|
|
189
|
-
/**
|
|
190
|
-
* 启动键盘监听
|
|
191
|
-
* 将 stdin 设为 raw 模式以捕获每个按键
|
|
192
|
-
*/
|
|
193
|
-
private startKeyboardListener(): void {
|
|
194
|
-
if (process.stdin.isTTY) {
|
|
195
|
-
process.stdin.setRawMode(true);
|
|
196
|
-
}
|
|
197
|
-
process.stdin.resume();
|
|
198
|
-
|
|
199
|
-
this.stdinDataHandler = (data: Buffer) => {
|
|
200
|
-
this.handleKeypress(data);
|
|
201
|
-
};
|
|
202
|
-
process.stdin.on('data', this.stdinDataHandler);
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
/**
|
|
206
|
-
* 停止键盘监听,恢复 stdin 状态
|
|
207
|
-
*/
|
|
208
|
-
private stopKeyboardListener(): void {
|
|
209
|
-
if (this.stdinDataHandler) {
|
|
210
|
-
process.stdin.removeListener('data', this.stdinDataHandler);
|
|
211
|
-
this.stdinDataHandler = null;
|
|
212
|
-
}
|
|
213
|
-
if (process.stdin.isTTY) {
|
|
214
|
-
process.stdin.setRawMode(false);
|
|
215
|
-
}
|
|
216
|
-
process.stdin.pause();
|
|
217
|
-
}
|
|
218
|
-
|
|
219
183
|
/**
|
|
220
184
|
* 处理键盘输入
|
|
221
185
|
* @param {Buffer} data - 按键数据
|
|
@@ -234,13 +198,17 @@ export class InteractivePanel {
|
|
|
234
198
|
|
|
235
199
|
// 方向键上
|
|
236
200
|
if (str === KEY_ARROW_UP) {
|
|
237
|
-
this.navigateUp()
|
|
201
|
+
if (this.stateManager.navigateUp()) {
|
|
202
|
+
this.render();
|
|
203
|
+
}
|
|
238
204
|
return;
|
|
239
205
|
}
|
|
240
206
|
|
|
241
207
|
// 方向键下
|
|
242
208
|
if (str === KEY_ARROW_DOWN) {
|
|
243
|
-
this.navigateDown()
|
|
209
|
+
if (this.stateManager.navigateDown()) {
|
|
210
|
+
this.render();
|
|
211
|
+
}
|
|
244
212
|
return;
|
|
245
213
|
}
|
|
246
214
|
|
|
@@ -288,85 +256,6 @@ export class InteractivePanel {
|
|
|
288
256
|
}
|
|
289
257
|
}
|
|
290
258
|
|
|
291
|
-
/**
|
|
292
|
-
* 向上导航,选中显示顺序中的上一个 worktree
|
|
293
|
-
*/
|
|
294
|
-
private navigateUp(): void {
|
|
295
|
-
if (!this.statusResult || this.displayOrder.length === 0) return;
|
|
296
|
-
|
|
297
|
-
if (this.selectedDisplayIndex > 0) {
|
|
298
|
-
this.selectedDisplayIndex--;
|
|
299
|
-
this.adjustScrollForSelection();
|
|
300
|
-
this.render();
|
|
301
|
-
}
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
/**
|
|
305
|
-
* 向下导航,选中显示顺序中的下一个 worktree
|
|
306
|
-
*/
|
|
307
|
-
private navigateDown(): void {
|
|
308
|
-
if (!this.statusResult || this.displayOrder.length === 0) return;
|
|
309
|
-
|
|
310
|
-
if (this.selectedDisplayIndex < this.displayOrder.length - 1) {
|
|
311
|
-
this.selectedDisplayIndex++;
|
|
312
|
-
this.adjustScrollForSelection();
|
|
313
|
-
this.render();
|
|
314
|
-
}
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
/**
|
|
318
|
-
* 获取当前选中的原始 worktree 索引
|
|
319
|
-
* @returns {number} 原始 worktrees 数组中的索引
|
|
320
|
-
*/
|
|
321
|
-
private getSelectedOriginalIndex(): number {
|
|
322
|
-
return this.displayOrder[this.selectedDisplayIndex];
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
/**
|
|
326
|
-
* 调整滚动偏移以确保选中项在可见区域内
|
|
327
|
-
*/
|
|
328
|
-
private adjustScrollForSelection(): void {
|
|
329
|
-
if (!this.statusResult || this.displayOrder.length === 0) return;
|
|
330
|
-
|
|
331
|
-
const originalIndex = this.getSelectedOriginalIndex();
|
|
332
|
-
const rows = process.stdout.rows || 24;
|
|
333
|
-
const visibleRows = calculateVisibleRows(rows);
|
|
334
|
-
const panelLines = buildGroupedWorktreeLines(this.statusResult.worktrees, originalIndex);
|
|
335
|
-
|
|
336
|
-
// 找到选中 worktree 对应的第一行和最后一行
|
|
337
|
-
let firstLine = -1;
|
|
338
|
-
let lastLine = -1;
|
|
339
|
-
for (let i = 0; i < panelLines.length; i++) {
|
|
340
|
-
if (panelLines[i].worktreeIndex === originalIndex) {
|
|
341
|
-
if (firstLine === -1) firstLine = i;
|
|
342
|
-
lastLine = i;
|
|
343
|
-
}
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
if (firstLine === -1) return;
|
|
347
|
-
|
|
348
|
-
// 向前查找该 worktree 所属日期分组的分隔线行,滚动时一并显示
|
|
349
|
-
let groupStart = firstLine;
|
|
350
|
-
while (groupStart > 0 && panelLines[groupStart - 1].type === 'separator') {
|
|
351
|
-
groupStart--;
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
// 如果选中项(含日期分隔线)在可见区域之上,向上滚动
|
|
355
|
-
if (groupStart < this.scrollOffset) {
|
|
356
|
-
this.scrollOffset = groupStart;
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
// 如果选中项的最后一行在可见区域之下,向下滚动
|
|
360
|
-
if (lastLine >= this.scrollOffset + visibleRows) {
|
|
361
|
-
this.scrollOffset = lastLine - visibleRows + 1;
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
// 终端极小时向下滚动可能把日期分组标题推出屏幕,优先保证分组标题可见
|
|
365
|
-
if (this.scrollOffset > groupStart) {
|
|
366
|
-
this.scrollOffset = groupStart;
|
|
367
|
-
}
|
|
368
|
-
}
|
|
369
|
-
|
|
370
259
|
/**
|
|
371
260
|
* 启动自动刷新:数据刷新定时器 + 倒计时定时器
|
|
372
261
|
*/
|
|
@@ -412,15 +301,13 @@ export class InteractivePanel {
|
|
|
412
301
|
if (this.stopped || this.isOperating) return;
|
|
413
302
|
|
|
414
303
|
// 记录当前选中分支名
|
|
415
|
-
const
|
|
416
|
-
const previousBranch = this.statusResult?.worktrees[originalIndex]?.branch;
|
|
304
|
+
const previousBranch = this.stateManager.getSelectedBranch();
|
|
417
305
|
|
|
418
|
-
//
|
|
419
|
-
this.
|
|
420
|
-
this.displayOrder = buildDisplayOrder(this.statusResult.worktrees);
|
|
306
|
+
// 重新收集数据并更新状态
|
|
307
|
+
this.stateManager.updateData(this.collectStatusFn(), previousBranch || undefined);
|
|
421
308
|
|
|
422
|
-
//
|
|
423
|
-
this.
|
|
309
|
+
// 在重绘前必须确保滚动状态正常
|
|
310
|
+
this.stateManager.adjustScrollForSelection();
|
|
424
311
|
|
|
425
312
|
// 重置倒计时
|
|
426
313
|
this.refreshCountdown = PANEL_REFRESH_INTERVAL_MS / 1000;
|
|
@@ -428,44 +315,21 @@ export class InteractivePanel {
|
|
|
428
315
|
this.render();
|
|
429
316
|
}
|
|
430
317
|
|
|
431
|
-
/**
|
|
432
|
-
* 按分支名恢复选中位置(基于显示顺序)
|
|
433
|
-
* @param {string | undefined} previousBranch - 之前选中的分支名
|
|
434
|
-
*/
|
|
435
|
-
private restoreSelection(previousBranch: string | undefined): void {
|
|
436
|
-
if (!this.statusResult || !previousBranch || this.displayOrder.length === 0) {
|
|
437
|
-
this.selectedDisplayIndex = 0;
|
|
438
|
-
return;
|
|
439
|
-
}
|
|
440
|
-
|
|
441
|
-
// 在显示顺序中查找之前选中的分支
|
|
442
|
-
const newDisplayIndex = this.displayOrder.findIndex(
|
|
443
|
-
(origIdx) => this.statusResult!.worktrees[origIdx]?.branch === previousBranch,
|
|
444
|
-
);
|
|
445
|
-
if (newDisplayIndex >= 0) {
|
|
446
|
-
this.selectedDisplayIndex = newDisplayIndex;
|
|
447
|
-
} else {
|
|
448
|
-
// 分支已不存在,调整到安全范围
|
|
449
|
-
this.selectedDisplayIndex = Math.min(this.selectedDisplayIndex, Math.max(0, this.displayOrder.length - 1));
|
|
450
|
-
}
|
|
451
|
-
|
|
452
|
-
this.adjustScrollForSelection();
|
|
453
|
-
}
|
|
454
|
-
|
|
455
318
|
/**
|
|
456
319
|
* 渲染一帧面板内容
|
|
457
320
|
* 使用同步输出防止闪烁
|
|
458
321
|
*/
|
|
459
322
|
private render(): void {
|
|
460
|
-
|
|
323
|
+
const statusResult = this.stateManager.getStatusResult();
|
|
324
|
+
if (this.stopped || this.isOperating || !statusResult) return;
|
|
461
325
|
|
|
462
326
|
const cols = process.stdout.columns || DEFAULT_TERMINAL_COLUMNS;
|
|
463
327
|
const rows = process.stdout.rows || 24;
|
|
464
328
|
|
|
465
329
|
const frameLines = buildPanelFrame(
|
|
466
|
-
|
|
467
|
-
this.getSelectedOriginalIndex(),
|
|
468
|
-
this.
|
|
330
|
+
statusResult,
|
|
331
|
+
this.stateManager.getSelectedOriginalIndex(),
|
|
332
|
+
this.stateManager.getScrollOffset(),
|
|
469
333
|
rows,
|
|
470
334
|
cols,
|
|
471
335
|
this.refreshCountdown,
|
|
@@ -491,7 +355,8 @@ export class InteractivePanel {
|
|
|
491
355
|
* @param {() => void} action - 要执行的操作
|
|
492
356
|
*/
|
|
493
357
|
private async executeOperation(action: () => void): Promise<void> {
|
|
494
|
-
|
|
358
|
+
const statusResult = this.stateManager.getStatusResult();
|
|
359
|
+
if (!statusResult || this.stateManager.getSelectedOriginalIndex() === -1) return;
|
|
495
360
|
|
|
496
361
|
this.isOperating = true;
|
|
497
362
|
|
|
@@ -502,7 +367,7 @@ export class InteractivePanel {
|
|
|
502
367
|
this.restoreTerminal();
|
|
503
368
|
|
|
504
369
|
// 恢复 stdin 以便子命令交互
|
|
505
|
-
this.
|
|
370
|
+
this.keyboardController.stop();
|
|
506
371
|
|
|
507
372
|
// 执行操作
|
|
508
373
|
action();
|
|
@@ -515,7 +380,7 @@ export class InteractivePanel {
|
|
|
515
380
|
|
|
516
381
|
// 重新进入面板模式
|
|
517
382
|
this.initTerminal();
|
|
518
|
-
this.
|
|
383
|
+
this.keyboardController.start();
|
|
519
384
|
|
|
520
385
|
this.isOperating = false;
|
|
521
386
|
|
|
@@ -527,53 +392,44 @@ export class InteractivePanel {
|
|
|
527
392
|
this.render();
|
|
528
393
|
}
|
|
529
394
|
|
|
530
|
-
/**
|
|
531
|
-
* 获取当前选中的分支名
|
|
532
|
-
* @returns {string} 当前选中的分支名
|
|
533
|
-
*/
|
|
534
|
-
private getSelectedBranch(): string {
|
|
535
|
-
const originalIndex = this.getSelectedOriginalIndex();
|
|
536
|
-
return this.statusResult!.worktrees[originalIndex].branch;
|
|
537
|
-
}
|
|
538
|
-
|
|
539
395
|
/**
|
|
540
396
|
* 执行验证操作
|
|
541
397
|
*/
|
|
542
398
|
private handleValidate(): void {
|
|
543
|
-
const branch = this.getSelectedBranch();
|
|
544
|
-
runCommandInherited(`clawt validate -b ${branch}`);
|
|
399
|
+
const branch = this.stateManager.getSelectedBranch();
|
|
400
|
+
if (branch) runCommandInherited(`clawt validate -b ${branch}`);
|
|
545
401
|
}
|
|
546
402
|
|
|
547
403
|
/**
|
|
548
404
|
* 执行合并操作
|
|
549
405
|
*/
|
|
550
406
|
private handleMerge(): void {
|
|
551
|
-
const branch = this.getSelectedBranch();
|
|
552
|
-
runCommandInherited(`clawt merge -b ${branch}`);
|
|
407
|
+
const branch = this.stateManager.getSelectedBranch();
|
|
408
|
+
if (branch) runCommandInherited(`clawt merge -b ${branch}`);
|
|
553
409
|
}
|
|
554
410
|
|
|
555
411
|
/**
|
|
556
412
|
* 执行删除操作
|
|
557
413
|
*/
|
|
558
414
|
private handleDelete(): void {
|
|
559
|
-
const branch = this.getSelectedBranch();
|
|
560
|
-
runCommandInherited(`clawt remove -b ${branch}`);
|
|
415
|
+
const branch = this.stateManager.getSelectedBranch();
|
|
416
|
+
if (branch) runCommandInherited(`clawt remove -b ${branch}`);
|
|
561
417
|
}
|
|
562
418
|
|
|
563
419
|
/**
|
|
564
420
|
* 执行恢复操作
|
|
565
421
|
*/
|
|
566
422
|
private handleResume(): void {
|
|
567
|
-
const branch = this.getSelectedBranch();
|
|
568
|
-
runCommandInherited(`clawt resume -b ${branch}`);
|
|
423
|
+
const branch = this.stateManager.getSelectedBranch();
|
|
424
|
+
if (branch) runCommandInherited(`clawt resume -b ${branch}`);
|
|
569
425
|
}
|
|
570
426
|
|
|
571
427
|
/**
|
|
572
428
|
* 执行同步操作
|
|
573
429
|
*/
|
|
574
430
|
private handleSync(): void {
|
|
575
|
-
const branch = this.getSelectedBranch();
|
|
576
|
-
runCommandInherited(`clawt sync -b ${branch}`);
|
|
431
|
+
const branch = this.stateManager.getSelectedBranch();
|
|
432
|
+
if (branch) runCommandInherited(`clawt sync -b ${branch}`);
|
|
577
433
|
}
|
|
578
434
|
|
|
579
435
|
/**
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 键盘事件控制器
|
|
3
|
+
* 负责终端键盘事件的绑定与解除
|
|
4
|
+
*/
|
|
5
|
+
export class KeyboardController {
|
|
6
|
+
/** stdin 数据处理器引用(用于清理) */
|
|
7
|
+
private stdinDataHandler: ((data: Buffer) => void) | null = null;
|
|
8
|
+
/** 按键回调函数 */
|
|
9
|
+
private onKeypress: (data: Buffer) => void;
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* 创建键盘事件控制器
|
|
13
|
+
* @param {(data: Buffer) => void} onKeypress - 按键回调函数
|
|
14
|
+
*/
|
|
15
|
+
constructor(onKeypress: (data: Buffer) => void) {
|
|
16
|
+
this.onKeypress = onKeypress;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* 启动键盘监听
|
|
21
|
+
* 将 stdin 设为 raw 模式以捕获每个按键
|
|
22
|
+
*/
|
|
23
|
+
start(): void {
|
|
24
|
+
if (process.stdin.isTTY) {
|
|
25
|
+
process.stdin.setRawMode(true);
|
|
26
|
+
}
|
|
27
|
+
process.stdin.resume();
|
|
28
|
+
|
|
29
|
+
this.stdinDataHandler = (data: Buffer) => {
|
|
30
|
+
this.onKeypress(data);
|
|
31
|
+
};
|
|
32
|
+
process.stdin.on('data', this.stdinDataHandler);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* 停止键盘监听,恢复 stdin 状态
|
|
37
|
+
*/
|
|
38
|
+
stop(): void {
|
|
39
|
+
if (this.stdinDataHandler) {
|
|
40
|
+
process.stdin.removeListener('data', this.stdinDataHandler);
|
|
41
|
+
this.stdinDataHandler = null;
|
|
42
|
+
}
|
|
43
|
+
if (process.stdin.isTTY) {
|
|
44
|
+
process.stdin.setRawMode(false);
|
|
45
|
+
}
|
|
46
|
+
process.stdin.pause();
|
|
47
|
+
}
|
|
48
|
+
}
|