claude-code-plus-plus 0.3.0 → 0.4.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.
- package/dist/constants.d.ts +6 -1
- package/dist/constants.d.ts.map +1 -1
- package/dist/constants.js +7 -2
- package/dist/constants.js.map +1 -1
- package/dist/diff/diff-handler.d.ts +15 -0
- package/dist/diff/diff-handler.d.ts.map +1 -0
- package/dist/diff/diff-handler.js +235 -0
- package/dist/diff/diff-handler.js.map +1 -0
- package/dist/diff/diff-manager.d.ts +63 -0
- package/dist/diff/diff-manager.d.ts.map +1 -0
- package/dist/diff/diff-manager.js +271 -0
- package/dist/diff/diff-manager.js.map +1 -0
- package/dist/diff/diff-pane-render.d.ts +34 -0
- package/dist/diff/diff-pane-render.d.ts.map +1 -0
- package/dist/diff/diff-pane-render.js +222 -0
- package/dist/diff/diff-pane-render.js.map +1 -0
- package/dist/diff/file-diff-content-handler.d.ts +16 -0
- package/dist/diff/file-diff-content-handler.d.ts.map +1 -0
- package/dist/diff/file-diff-content-handler.js +142 -0
- package/dist/diff/file-diff-content-handler.js.map +1 -0
- package/dist/diff/file-diff-header-handler.d.ts +16 -0
- package/dist/diff/file-diff-header-handler.d.ts.map +1 -0
- package/dist/diff/file-diff-header-handler.js +191 -0
- package/dist/diff/file-diff-header-handler.js.map +1 -0
- package/dist/diff/file-diff-header-render.d.ts +30 -0
- package/dist/diff/file-diff-header-render.d.ts.map +1 -0
- package/dist/diff/file-diff-header-render.js +101 -0
- package/dist/diff/file-diff-header-render.js.map +1 -0
- package/dist/diff/file-diff-render.d.ts +27 -0
- package/dist/diff/file-diff-render.d.ts.map +1 -0
- package/dist/diff/file-diff-render.js +211 -0
- package/dist/diff/file-diff-render.js.map +1 -0
- package/dist/diff/git-diff.d.ts +54 -0
- package/dist/diff/git-diff.d.ts.map +1 -0
- package/dist/diff/git-diff.js +599 -0
- package/dist/diff/git-diff.js.map +1 -0
- package/dist/diff/index.d.ts +8 -0
- package/dist/diff/index.d.ts.map +1 -0
- package/dist/diff/index.js +37 -0
- package/dist/diff/index.js.map +1 -0
- package/dist/sidebar/app.d.ts +69 -0
- package/dist/sidebar/app.d.ts.map +1 -1
- package/dist/sidebar/app.js +508 -10
- package/dist/sidebar/app.js.map +1 -1
- package/dist/sidebar/commands.d.ts +1 -0
- package/dist/sidebar/commands.d.ts.map +1 -1
- package/dist/sidebar/commands.js +11 -0
- package/dist/sidebar/commands.js.map +1 -1
- package/dist/sidebar/pane-orchestrator.d.ts.map +1 -1
- package/dist/sidebar/pane-orchestrator.js +10 -1
- package/dist/sidebar/pane-orchestrator.js.map +1 -1
- package/dist/sidebar/render.d.ts.map +1 -1
- package/dist/sidebar/render.js +2 -1
- package/dist/sidebar/render.js.map +1 -1
- package/dist/tmux/index.d.ts +1 -1
- package/dist/tmux/index.d.ts.map +1 -1
- package/dist/tmux/index.js +2 -1
- package/dist/tmux/index.js.map +1 -1
- package/dist/tmux/pane.d.ts +5 -0
- package/dist/tmux/pane.d.ts.map +1 -1
- package/dist/tmux/pane.js +10 -0
- package/dist/tmux/pane.js.map +1 -1
- package/dist/types.d.ts +10 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/package.json +1 -1
package/dist/sidebar/app.js
CHANGED
|
@@ -47,6 +47,8 @@ const git_1 = require("../git");
|
|
|
47
47
|
const input_1 = require("./input");
|
|
48
48
|
const render_1 = require("./render");
|
|
49
49
|
const constants_1 = require("../constants");
|
|
50
|
+
const diff_1 = require("../diff");
|
|
51
|
+
const diff_2 = require("../diff");
|
|
50
52
|
const pane_orchestrator_1 = require("./pane-orchestrator");
|
|
51
53
|
const sessionManager = __importStar(require("./session-manager"));
|
|
52
54
|
const terminalManager = __importStar(require("./terminal-manager"));
|
|
@@ -71,6 +73,8 @@ class SidebarApp {
|
|
|
71
73
|
state;
|
|
72
74
|
worktreeManager;
|
|
73
75
|
running = false;
|
|
76
|
+
fileWatcherCleanup = null;
|
|
77
|
+
diffPaneOpening = false; // Guard against double openDiffPane calls
|
|
74
78
|
constructor(repoPath, sessionName, mainPaneId, sidebarPaneId) {
|
|
75
79
|
this.worktreeManager = new git_1.WorktreeManager(repoPath);
|
|
76
80
|
this.state = {
|
|
@@ -93,6 +97,10 @@ class SidebarApp {
|
|
|
93
97
|
collapsed: false,
|
|
94
98
|
terminalCommandMode: false,
|
|
95
99
|
terminalCommandBuffer: '',
|
|
100
|
+
diffCommandMode: false,
|
|
101
|
+
diffCommandBuffer: '',
|
|
102
|
+
fileDiffMode: false,
|
|
103
|
+
fileDiffFilename: null,
|
|
96
104
|
};
|
|
97
105
|
}
|
|
98
106
|
// ==========================================================================
|
|
@@ -174,6 +182,8 @@ class SidebarApp {
|
|
|
174
182
|
output = (0, render_1.renderErrorModal)(this.state, dims);
|
|
175
183
|
}
|
|
176
184
|
else {
|
|
185
|
+
// Normal view - sidebar renders normally even in file diff mode
|
|
186
|
+
// (file diff content is shown in a separate pane)
|
|
177
187
|
output = (0, render_1.renderMain)(this.state);
|
|
178
188
|
}
|
|
179
189
|
process.stdout.write(output);
|
|
@@ -188,11 +198,20 @@ class SidebarApp {
|
|
|
188
198
|
handleInput(data) {
|
|
189
199
|
const str = data.toString();
|
|
190
200
|
debugLog('handleInput:', 'hex=' + data.toString('hex'), 'modal=' + this.state.modal);
|
|
191
|
-
// Handle terminal command mode (commands from terminal bar handler)
|
|
201
|
+
// Handle terminal command mode (commands from terminal bar, diff pane handler, or file diff header handler)
|
|
192
202
|
if (this.state.terminalCommandMode) {
|
|
193
203
|
if (str === '\r' || str === '\n') {
|
|
194
|
-
// Enter - execute command
|
|
195
|
-
this.
|
|
204
|
+
// Enter - execute command based on prefix
|
|
205
|
+
const cmd = this.state.terminalCommandBuffer;
|
|
206
|
+
if (cmd.startsWith('DIFF:')) {
|
|
207
|
+
this.executeDiffCommand(cmd);
|
|
208
|
+
}
|
|
209
|
+
else if (cmd.startsWith('FILEDIFF:')) {
|
|
210
|
+
this.executeFileDiffCommand(cmd);
|
|
211
|
+
}
|
|
212
|
+
else {
|
|
213
|
+
this.executeTerminalCommand(cmd);
|
|
214
|
+
}
|
|
196
215
|
this.state.terminalCommandMode = false;
|
|
197
216
|
this.state.terminalCommandBuffer = '';
|
|
198
217
|
return;
|
|
@@ -225,6 +244,11 @@ class SidebarApp {
|
|
|
225
244
|
this.state.terminalCommandBuffer = '';
|
|
226
245
|
return;
|
|
227
246
|
}
|
|
247
|
+
// Handle file diff view mode
|
|
248
|
+
if (this.state.fileDiffMode) {
|
|
249
|
+
this.handleFileDiffInput(key);
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
228
252
|
// Route input based on modal state
|
|
229
253
|
switch (this.state.modal) {
|
|
230
254
|
case 'quit':
|
|
@@ -311,6 +335,7 @@ class SidebarApp {
|
|
|
311
335
|
},
|
|
312
336
|
toggleCollapsed: () => this.toggleCollapsed(),
|
|
313
337
|
createTerminal: () => this.createTerminal(),
|
|
338
|
+
toggleDiffPane: () => this.toggleDiffPane(),
|
|
314
339
|
render: () => this.render(),
|
|
315
340
|
},
|
|
316
341
|
};
|
|
@@ -599,7 +624,22 @@ class SidebarApp {
|
|
|
599
624
|
this.state.hiddenPaneId = null;
|
|
600
625
|
}
|
|
601
626
|
else if (currentSession) {
|
|
602
|
-
// Normal mode - break the current session
|
|
627
|
+
// Normal mode - break the current session's panes
|
|
628
|
+
// Break diff pane first (if any) to prevent duplicates
|
|
629
|
+
if (currentSession.diffPaneId) {
|
|
630
|
+
(0, diff_1.breakDiffPane)(currentSession.diffPaneId);
|
|
631
|
+
}
|
|
632
|
+
// Break terminals if any
|
|
633
|
+
if (currentSession.terminals.length > 0) {
|
|
634
|
+
const activeTerminal = currentSession.terminals[currentSession.activeTerminalIndex];
|
|
635
|
+
if (activeTerminal) {
|
|
636
|
+
tmux.breakPane(activeTerminal.paneId);
|
|
637
|
+
}
|
|
638
|
+
if (currentSession.terminalBarPaneId) {
|
|
639
|
+
tmux.breakPane(currentSession.terminalBarPaneId);
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
// Break Claude pane
|
|
603
643
|
tmux.breakPane(currentSession.paneId);
|
|
604
644
|
}
|
|
605
645
|
paneId = tmux.splitHorizontal(this.state.sessionName, 80, worktree.path);
|
|
@@ -615,6 +655,13 @@ class SidebarApp {
|
|
|
615
655
|
terminals: [],
|
|
616
656
|
activeTerminalIndex: 0,
|
|
617
657
|
terminalBarPaneId: null,
|
|
658
|
+
// Diff pane
|
|
659
|
+
diffPaneId: null,
|
|
660
|
+
diffPaneManuallyHidden: false,
|
|
661
|
+
diffViewMode: 'whole-file',
|
|
662
|
+
// File diff view panes
|
|
663
|
+
fileDiffHeaderPaneId: null,
|
|
664
|
+
fileDiffContentPaneId: null,
|
|
618
665
|
};
|
|
619
666
|
this.state.sessions.push(session);
|
|
620
667
|
this.state.activeSessionId = sessionId;
|
|
@@ -622,17 +669,27 @@ class SidebarApp {
|
|
|
622
669
|
// Focus the Claude pane so user can start interacting immediately
|
|
623
670
|
tmux.selectPane(paneId);
|
|
624
671
|
this.render();
|
|
672
|
+
// Set up file watcher for this session's worktree
|
|
673
|
+
this.setupFileWatcher(worktree.path, session);
|
|
674
|
+
// Auto-open diff pane if there are changes in this worktree
|
|
675
|
+
this.autoOpenDiffPaneIfNeeded(session, worktree);
|
|
625
676
|
}
|
|
626
|
-
switchToSession(session) {
|
|
677
|
+
async switchToSession(session) {
|
|
627
678
|
if (session.id === this.state.activeSessionId) {
|
|
628
679
|
// Already active - focus pane
|
|
629
680
|
tmux.selectPane(session.paneId);
|
|
630
681
|
return;
|
|
631
682
|
}
|
|
632
|
-
//
|
|
683
|
+
// Clean up file watcher for current session
|
|
684
|
+
this.cleanupFileWatcher();
|
|
685
|
+
// Break current session's panes (Claude pane + terminals + diff pane)
|
|
633
686
|
const currentSession = this.state.sessions.find(s => s.id === this.state.activeSessionId);
|
|
634
687
|
if (currentSession) {
|
|
635
|
-
// Break
|
|
688
|
+
// Break diff pane first (if any)
|
|
689
|
+
if (currentSession.diffPaneId) {
|
|
690
|
+
(0, diff_1.breakDiffPane)(currentSession.diffPaneId);
|
|
691
|
+
}
|
|
692
|
+
// Break active terminal (if any)
|
|
636
693
|
if (currentSession.terminals.length > 0) {
|
|
637
694
|
const activeTerminal = currentSession.terminals[currentSession.activeTerminalIndex];
|
|
638
695
|
if (activeTerminal) {
|
|
@@ -648,6 +705,15 @@ class SidebarApp {
|
|
|
648
705
|
}
|
|
649
706
|
// Join new session's Claude pane
|
|
650
707
|
tmux.joinPane(session.paneId, this.state.sidebarPaneId, true);
|
|
708
|
+
// Join diff pane if exists
|
|
709
|
+
if (session.diffPaneId) {
|
|
710
|
+
(0, diff_1.joinDiffPane)(session.diffPaneId, session.paneId);
|
|
711
|
+
// Set up file watcher for new session
|
|
712
|
+
const worktree = this.state.worktrees.find(w => w.id === session.worktreeId);
|
|
713
|
+
if (worktree) {
|
|
714
|
+
this.setupFileWatcher(worktree.path, session);
|
|
715
|
+
}
|
|
716
|
+
}
|
|
651
717
|
// Join terminal bar and active terminal if session has terminals
|
|
652
718
|
if (session.terminals.length > 0 && session.terminalBarPaneId) {
|
|
653
719
|
// Join terminal bar below Claude pane
|
|
@@ -670,6 +736,23 @@ class SidebarApp {
|
|
|
670
736
|
this.enforceSidebarWidth();
|
|
671
737
|
tmux.selectPane(this.state.sidebarPaneId);
|
|
672
738
|
this.render();
|
|
739
|
+
// Set up file watcher and potentially auto-open diff pane for the new session
|
|
740
|
+
const worktree = this.state.worktrees.find(w => w.id === session.worktreeId);
|
|
741
|
+
if (worktree) {
|
|
742
|
+
if (!session.diffPaneId) {
|
|
743
|
+
// Diff pane not open - check if we should auto-open
|
|
744
|
+
if (!session.diffPaneManuallyHidden) {
|
|
745
|
+
// Auto-open will set up watcher if it opens
|
|
746
|
+
await this.autoOpenDiffPaneIfNeeded(session, worktree);
|
|
747
|
+
}
|
|
748
|
+
// If diff pane still not open (either manually hidden or no changes),
|
|
749
|
+
// set up watcher for future auto-open
|
|
750
|
+
if (!session.diffPaneId) {
|
|
751
|
+
this.setupFileWatcher(worktree.path, session);
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
// If diff pane exists, watcher was already set up in joinDiffPane section above
|
|
755
|
+
}
|
|
673
756
|
}
|
|
674
757
|
async deleteSelected() {
|
|
675
758
|
const item = this.getSelectedItem();
|
|
@@ -683,6 +766,14 @@ class SidebarApp {
|
|
|
683
766
|
}
|
|
684
767
|
}
|
|
685
768
|
deleteSession(session) {
|
|
769
|
+
// Clean up file watcher if this is the active session
|
|
770
|
+
if (session.id === this.state.activeSessionId) {
|
|
771
|
+
this.cleanupFileWatcher();
|
|
772
|
+
}
|
|
773
|
+
// Kill diff pane
|
|
774
|
+
if (session.diffPaneId) {
|
|
775
|
+
(0, diff_1.closeDiffPane)(session.diffPaneId);
|
|
776
|
+
}
|
|
686
777
|
// Kill all terminal panes
|
|
687
778
|
for (const terminal of session.terminals) {
|
|
688
779
|
tmux.killPane(terminal.paneId);
|
|
@@ -708,6 +799,15 @@ class SidebarApp {
|
|
|
708
799
|
const nextSession = this.state.sessions[0];
|
|
709
800
|
tmux.joinPane(nextSession.paneId, this.state.sidebarPaneId, true);
|
|
710
801
|
this.state.activeSessionId = nextSession.id;
|
|
802
|
+
// Join diff pane if next session has one
|
|
803
|
+
if (nextSession.diffPaneId) {
|
|
804
|
+
(0, diff_1.joinDiffPane)(nextSession.diffPaneId, nextSession.paneId);
|
|
805
|
+
// Set up file watcher for the new session
|
|
806
|
+
const worktree = this.state.worktrees.find(w => w.id === nextSession.worktreeId);
|
|
807
|
+
if (worktree) {
|
|
808
|
+
this.setupFileWatcher(worktree.path, nextSession);
|
|
809
|
+
}
|
|
810
|
+
}
|
|
711
811
|
// Join terminal panes if next session has terminals
|
|
712
812
|
if (nextSession.terminals.length > 0 && nextSession.terminalBarPaneId) {
|
|
713
813
|
tmux.joinPane(nextSession.terminalBarPaneId, nextSession.paneId, false);
|
|
@@ -1071,6 +1171,395 @@ class SidebarApp {
|
|
|
1071
1171
|
tmux.sendKeys(session.terminalBarPaneId, `RENDER:${renderData}`, false);
|
|
1072
1172
|
}
|
|
1073
1173
|
// ==========================================================================
|
|
1174
|
+
// Diff Pane Management
|
|
1175
|
+
// ==========================================================================
|
|
1176
|
+
/**
|
|
1177
|
+
* Toggle the diff pane visibility
|
|
1178
|
+
*/
|
|
1179
|
+
async toggleDiffPane() {
|
|
1180
|
+
const session = this.state.sessions.find(s => s.id === this.state.activeSessionId);
|
|
1181
|
+
if (!session) {
|
|
1182
|
+
debugLog('toggleDiffPane: no active session');
|
|
1183
|
+
return;
|
|
1184
|
+
}
|
|
1185
|
+
if (session.diffPaneId) {
|
|
1186
|
+
// Close the diff pane
|
|
1187
|
+
this.closeDiffPane(session);
|
|
1188
|
+
}
|
|
1189
|
+
else {
|
|
1190
|
+
// Open the diff pane
|
|
1191
|
+
await this.openDiffPane(session);
|
|
1192
|
+
}
|
|
1193
|
+
}
|
|
1194
|
+
/**
|
|
1195
|
+
* Open the diff pane for a session
|
|
1196
|
+
* @param manualOpen - True if user manually opened (toggle), false if auto-open
|
|
1197
|
+
*/
|
|
1198
|
+
async openDiffPane(session, manualOpen = true) {
|
|
1199
|
+
// Guard against double opening (race condition protection)
|
|
1200
|
+
if (session.diffPaneId || this.diffPaneOpening)
|
|
1201
|
+
return;
|
|
1202
|
+
this.diffPaneOpening = true;
|
|
1203
|
+
try {
|
|
1204
|
+
const worktree = this.state.worktrees.find(w => w.id === session.worktreeId);
|
|
1205
|
+
if (!worktree)
|
|
1206
|
+
return;
|
|
1207
|
+
debugLog('openDiffPane: creating diff pane for session', session.title, 'manual=' + manualOpen);
|
|
1208
|
+
// Reset manually hidden flag when user opens it
|
|
1209
|
+
if (manualOpen) {
|
|
1210
|
+
session.diffPaneManuallyHidden = false;
|
|
1211
|
+
}
|
|
1212
|
+
// Get initial diff data
|
|
1213
|
+
const files = await (0, diff_1.getDiffSummary)(worktree.path);
|
|
1214
|
+
// Create the diff pane to the right of the Claude pane
|
|
1215
|
+
const diffPaneId = (0, diff_1.createDiffPane)(this.state.sessionName, session.paneId);
|
|
1216
|
+
session.diffPaneId = diffPaneId;
|
|
1217
|
+
// Start the diff handler
|
|
1218
|
+
(0, diff_1.startDiffHandler)(diffPaneId, this.state.sidebarPaneId, session.id, worktree.path, files);
|
|
1219
|
+
// Set up file watcher for auto-refresh
|
|
1220
|
+
this.setupFileWatcher(worktree.path, session);
|
|
1221
|
+
// Ensure sidebar stays at fixed width
|
|
1222
|
+
this.enforceSidebarWidth();
|
|
1223
|
+
tmux.selectPane(this.state.sidebarPaneId);
|
|
1224
|
+
this.render();
|
|
1225
|
+
}
|
|
1226
|
+
finally {
|
|
1227
|
+
this.diffPaneOpening = false;
|
|
1228
|
+
}
|
|
1229
|
+
}
|
|
1230
|
+
/**
|
|
1231
|
+
* Auto-open diff pane if there are changes and not manually hidden
|
|
1232
|
+
*/
|
|
1233
|
+
async autoOpenDiffPaneIfNeeded(session, worktree) {
|
|
1234
|
+
// Don't auto-open if already open or manually hidden
|
|
1235
|
+
if (session.diffPaneId || session.diffPaneManuallyHidden) {
|
|
1236
|
+
return;
|
|
1237
|
+
}
|
|
1238
|
+
// Check if there are changes in this worktree
|
|
1239
|
+
const files = await (0, diff_1.getDiffSummary)(worktree.path);
|
|
1240
|
+
if (files.length > 0) {
|
|
1241
|
+
debugLog('autoOpenDiffPaneIfNeeded: found', files.length, 'changed files, opening diff pane');
|
|
1242
|
+
await this.openDiffPane(session, false); // Auto-open, not manual
|
|
1243
|
+
}
|
|
1244
|
+
}
|
|
1245
|
+
/**
|
|
1246
|
+
* Close the diff pane for a session
|
|
1247
|
+
* @param manualClose - True if user manually closed (toggle), false if system close
|
|
1248
|
+
*/
|
|
1249
|
+
closeDiffPane(session, manualClose = true) {
|
|
1250
|
+
if (!session.diffPaneId)
|
|
1251
|
+
return;
|
|
1252
|
+
debugLog('closeDiffPane: closing diff pane for session', session.title, 'manual=' + manualClose);
|
|
1253
|
+
(0, diff_1.closeDiffPane)(session.diffPaneId);
|
|
1254
|
+
session.diffPaneId = null;
|
|
1255
|
+
// Mark as manually hidden if user closed it (so it won't auto-reopen)
|
|
1256
|
+
if (manualClose) {
|
|
1257
|
+
session.diffPaneManuallyHidden = true;
|
|
1258
|
+
// Also clean up file watcher when manually closed (user doesn't want auto-open)
|
|
1259
|
+
this.cleanupFileWatcher();
|
|
1260
|
+
}
|
|
1261
|
+
// Note: Don't clean up file watcher if not manual close - it may be needed for auto-open
|
|
1262
|
+
// Exit file diff mode if active
|
|
1263
|
+
if (this.state.fileDiffMode) {
|
|
1264
|
+
this.hideFileDiff();
|
|
1265
|
+
}
|
|
1266
|
+
this.enforceSidebarWidth();
|
|
1267
|
+
tmux.selectPane(this.state.sidebarPaneId);
|
|
1268
|
+
this.render();
|
|
1269
|
+
}
|
|
1270
|
+
/**
|
|
1271
|
+
* Set up file watcher for auto-refresh and auto-open
|
|
1272
|
+
*/
|
|
1273
|
+
setupFileWatcher(repoPath, session) {
|
|
1274
|
+
// Clean up any existing watcher
|
|
1275
|
+
this.cleanupFileWatcher();
|
|
1276
|
+
this.fileWatcherCleanup = (0, diff_1.watchForChanges)(repoPath, async () => {
|
|
1277
|
+
const files = await (0, diff_1.getDiffSummary)(repoPath);
|
|
1278
|
+
if (session.diffPaneId) {
|
|
1279
|
+
// Diff pane is open - just update it
|
|
1280
|
+
(0, diff_1.updateDiffPane)(session.diffPaneId, files);
|
|
1281
|
+
}
|
|
1282
|
+
else if (!session.diffPaneManuallyHidden && files.length > 0) {
|
|
1283
|
+
// Diff pane is closed but not manually hidden and there are changes
|
|
1284
|
+
// Auto-open the diff pane
|
|
1285
|
+
debugLog('setupFileWatcher: detected changes, auto-opening diff pane');
|
|
1286
|
+
await this.openDiffPane(session, false); // Auto-open, not manual
|
|
1287
|
+
}
|
|
1288
|
+
});
|
|
1289
|
+
}
|
|
1290
|
+
/**
|
|
1291
|
+
* Clean up file watcher
|
|
1292
|
+
*/
|
|
1293
|
+
cleanupFileWatcher() {
|
|
1294
|
+
if (this.fileWatcherCleanup) {
|
|
1295
|
+
this.fileWatcherCleanup();
|
|
1296
|
+
this.fileWatcherCleanup = null;
|
|
1297
|
+
}
|
|
1298
|
+
}
|
|
1299
|
+
/**
|
|
1300
|
+
* Execute a diff command received from the diff pane handler
|
|
1301
|
+
* Format: "DIFF:<action>:<data>"
|
|
1302
|
+
*/
|
|
1303
|
+
async executeDiffCommand(command) {
|
|
1304
|
+
debugLog('executeDiffCommand:', command);
|
|
1305
|
+
if (!command.startsWith('DIFF:'))
|
|
1306
|
+
return;
|
|
1307
|
+
const parts = command.slice(5).split(':');
|
|
1308
|
+
const action = parts[0];
|
|
1309
|
+
const data = parts.slice(1).join(':'); // Rejoin in case filename has colons
|
|
1310
|
+
const session = this.state.sessions.find(s => s.id === this.state.activeSessionId);
|
|
1311
|
+
switch (action) {
|
|
1312
|
+
case 'close':
|
|
1313
|
+
if (session) {
|
|
1314
|
+
this.closeDiffPane(session);
|
|
1315
|
+
}
|
|
1316
|
+
break;
|
|
1317
|
+
case 'viewfile':
|
|
1318
|
+
if (data && session) {
|
|
1319
|
+
await this.showFileDiff(data, session);
|
|
1320
|
+
}
|
|
1321
|
+
break;
|
|
1322
|
+
case 'refresh':
|
|
1323
|
+
if (session && session.diffPaneId) {
|
|
1324
|
+
const worktree = this.state.worktrees.find(w => w.id === session.worktreeId);
|
|
1325
|
+
if (worktree) {
|
|
1326
|
+
const files = await (0, diff_1.getDiffSummary)(worktree.path);
|
|
1327
|
+
(0, diff_1.updateDiffPane)(session.diffPaneId, files);
|
|
1328
|
+
}
|
|
1329
|
+
}
|
|
1330
|
+
break;
|
|
1331
|
+
}
|
|
1332
|
+
}
|
|
1333
|
+
/**
|
|
1334
|
+
* Show file diff view (replaces Claude pane + terminals with header + content panes)
|
|
1335
|
+
*
|
|
1336
|
+
* Architecture:
|
|
1337
|
+
* - Claude pane is broken to background (process preserved)
|
|
1338
|
+
* - Terminal panes are broken to background (if any)
|
|
1339
|
+
* - Diff pane (file list sidebar) remains visible
|
|
1340
|
+
* - Content pane shows full file with inline diffs (streaming)
|
|
1341
|
+
* - 1-row header pane above content for "Back" button + filename
|
|
1342
|
+
* - Content handler handles Esc key to close
|
|
1343
|
+
*/
|
|
1344
|
+
async showFileDiff(filename, session) {
|
|
1345
|
+
const worktree = this.state.worktrees.find(w => w.id === session.worktreeId);
|
|
1346
|
+
if (!worktree)
|
|
1347
|
+
return;
|
|
1348
|
+
debugLog('showFileDiff:', filename);
|
|
1349
|
+
// Get stats for header
|
|
1350
|
+
const files = await (0, diff_1.getDiffSummary)(worktree.path);
|
|
1351
|
+
const fileInfo = files.find(f => f.file === filename);
|
|
1352
|
+
const insertions = fileInfo?.insertions || 0;
|
|
1353
|
+
const deletions = fileInfo?.deletions || 0;
|
|
1354
|
+
// If already viewing a file diff, just replace the content (don't break/join again)
|
|
1355
|
+
if (this.state.fileDiffMode && session.fileDiffHeaderPaneId && session.fileDiffContentPaneId) {
|
|
1356
|
+
debugLog('showFileDiff: replacing existing view');
|
|
1357
|
+
// Break diff pane first (if exists) so we can recreate proper layout
|
|
1358
|
+
const hadDiffPane = !!session.diffPaneId;
|
|
1359
|
+
if (session.diffPaneId) {
|
|
1360
|
+
(0, diff_1.breakDiffPane)(session.diffPaneId);
|
|
1361
|
+
}
|
|
1362
|
+
// Kill existing header and content panes
|
|
1363
|
+
(0, diff_2.closeFileDiffHeaderPane)(session.fileDiffHeaderPaneId);
|
|
1364
|
+
(0, diff_2.closeFileDiffContentPane)(session.fileDiffContentPaneId);
|
|
1365
|
+
// Create new content pane (to the right of sidebar)
|
|
1366
|
+
const contentPaneId = (0, diff_2.createFileDiffContentPane)(this.state.sessionName, this.state.sidebarPaneId);
|
|
1367
|
+
session.fileDiffContentPaneId = contentPaneId;
|
|
1368
|
+
// Rejoin diff pane FIRST (to the right of content) - BEFORE creating header
|
|
1369
|
+
if (hadDiffPane && session.diffPaneId) {
|
|
1370
|
+
(0, diff_1.joinDiffPane)(session.diffPaneId, contentPaneId);
|
|
1371
|
+
}
|
|
1372
|
+
// NOW create new header pane (1-row above content only)
|
|
1373
|
+
const headerPaneId = (0, diff_2.createFileDiffHeaderPane)(this.state.sessionName, contentPaneId);
|
|
1374
|
+
session.fileDiffHeaderPaneId = headerPaneId;
|
|
1375
|
+
// Start header handler with new file info
|
|
1376
|
+
(0, diff_2.startFileDiffHeaderHandler)(headerPaneId, this.state.sidebarPaneId, filename, insertions, deletions, session.diffViewMode);
|
|
1377
|
+
// Start content handler (streams full file with inline diffs, handles Esc)
|
|
1378
|
+
(0, diff_2.startFileDiffContentHandler)(contentPaneId, this.state.sidebarPaneId, worktree.path, filename, session.diffViewMode);
|
|
1379
|
+
// Update state
|
|
1380
|
+
this.state.fileDiffFilename = filename;
|
|
1381
|
+
// Ensure sidebar width
|
|
1382
|
+
this.enforceSidebarWidth();
|
|
1383
|
+
// Focus the content pane
|
|
1384
|
+
tmux.selectPane(contentPaneId);
|
|
1385
|
+
return;
|
|
1386
|
+
}
|
|
1387
|
+
// First time opening - break Claude pane to background (preserve Claude CLI process)
|
|
1388
|
+
tmux.breakPane(session.paneId);
|
|
1389
|
+
// Also break terminals if any
|
|
1390
|
+
if (session.terminals.length > 0) {
|
|
1391
|
+
const activeTerminal = session.terminals[session.activeTerminalIndex];
|
|
1392
|
+
if (activeTerminal) {
|
|
1393
|
+
tmux.breakPane(activeTerminal.paneId);
|
|
1394
|
+
}
|
|
1395
|
+
if (session.terminalBarPaneId) {
|
|
1396
|
+
tmux.breakPane(session.terminalBarPaneId);
|
|
1397
|
+
}
|
|
1398
|
+
}
|
|
1399
|
+
// Break diff pane temporarily (we'll rejoin it BEFORE creating header)
|
|
1400
|
+
const hadDiffPane = !!session.diffPaneId;
|
|
1401
|
+
if (session.diffPaneId) {
|
|
1402
|
+
(0, diff_1.breakDiffPane)(session.diffPaneId);
|
|
1403
|
+
}
|
|
1404
|
+
// Create content pane (fills Claude pane's space, to the right of sidebar)
|
|
1405
|
+
const contentPaneId = (0, diff_2.createFileDiffContentPane)(this.state.sessionName, this.state.sidebarPaneId);
|
|
1406
|
+
session.fileDiffContentPaneId = contentPaneId;
|
|
1407
|
+
// Rejoin diff pane FIRST (to the right of content) - BEFORE creating header
|
|
1408
|
+
// This ensures header only spans content area, not the diff sidebar
|
|
1409
|
+
if (hadDiffPane && session.diffPaneId) {
|
|
1410
|
+
(0, diff_1.joinDiffPane)(session.diffPaneId, contentPaneId);
|
|
1411
|
+
}
|
|
1412
|
+
// NOW create 1-row header pane above content pane (after diff pane is positioned)
|
|
1413
|
+
const headerPaneId = (0, diff_2.createFileDiffHeaderPane)(this.state.sessionName, contentPaneId);
|
|
1414
|
+
session.fileDiffHeaderPaneId = headerPaneId;
|
|
1415
|
+
// Start header handler in header pane
|
|
1416
|
+
(0, diff_2.startFileDiffHeaderHandler)(headerPaneId, this.state.sidebarPaneId, filename, insertions, deletions, session.diffViewMode);
|
|
1417
|
+
// Start content handler (streams full file with inline diffs, handles Esc)
|
|
1418
|
+
(0, diff_2.startFileDiffContentHandler)(contentPaneId, this.state.sidebarPaneId, worktree.path, filename, session.diffViewMode);
|
|
1419
|
+
// Update state
|
|
1420
|
+
this.state.fileDiffMode = true;
|
|
1421
|
+
this.state.fileDiffFilename = filename;
|
|
1422
|
+
// Ensure sidebar stays at fixed width
|
|
1423
|
+
this.enforceSidebarWidth();
|
|
1424
|
+
// Focus the content pane (content handler handles Esc)
|
|
1425
|
+
tmux.selectPane(contentPaneId);
|
|
1426
|
+
this.render();
|
|
1427
|
+
}
|
|
1428
|
+
/**
|
|
1429
|
+
* Hide file diff view and restore Claude pane
|
|
1430
|
+
*
|
|
1431
|
+
* New architecture:
|
|
1432
|
+
* - Kill header + content panes
|
|
1433
|
+
* - Join Claude pane back from background
|
|
1434
|
+
* - Restore diff pane and terminals if any
|
|
1435
|
+
*/
|
|
1436
|
+
hideFileDiff() {
|
|
1437
|
+
if (!this.state.fileDiffMode)
|
|
1438
|
+
return;
|
|
1439
|
+
debugLog('hideFileDiff');
|
|
1440
|
+
const session = this.state.sessions.find(s => s.id === this.state.activeSessionId);
|
|
1441
|
+
if (!session)
|
|
1442
|
+
return;
|
|
1443
|
+
// 1. Kill header pane
|
|
1444
|
+
if (session.fileDiffHeaderPaneId) {
|
|
1445
|
+
(0, diff_2.closeFileDiffHeaderPane)(session.fileDiffHeaderPaneId);
|
|
1446
|
+
session.fileDiffHeaderPaneId = null;
|
|
1447
|
+
}
|
|
1448
|
+
// 2. Kill content pane
|
|
1449
|
+
if (session.fileDiffContentPaneId) {
|
|
1450
|
+
(0, diff_2.closeFileDiffContentPane)(session.fileDiffContentPaneId);
|
|
1451
|
+
session.fileDiffContentPaneId = null;
|
|
1452
|
+
}
|
|
1453
|
+
// 3. Join Claude pane back (was broken to background)
|
|
1454
|
+
tmux.joinPane(session.paneId, this.state.sidebarPaneId, true);
|
|
1455
|
+
// 4. Restore diff pane if it exists
|
|
1456
|
+
if (session.diffPaneId) {
|
|
1457
|
+
(0, diff_1.joinDiffPane)(session.diffPaneId, session.paneId);
|
|
1458
|
+
}
|
|
1459
|
+
// 5. Restore terminals if any
|
|
1460
|
+
if (session.terminals.length > 0 && session.terminalBarPaneId) {
|
|
1461
|
+
tmux.joinPane(session.terminalBarPaneId, session.paneId, false);
|
|
1462
|
+
const activeTerminal = session.terminals[session.activeTerminalIndex];
|
|
1463
|
+
if (activeTerminal) {
|
|
1464
|
+
tmux.joinPane(activeTerminal.paneId, session.terminalBarPaneId, false);
|
|
1465
|
+
}
|
|
1466
|
+
tmux.resizePane(session.terminalBarPaneId, undefined, constants_1.TERMINAL_BAR_HEIGHT);
|
|
1467
|
+
}
|
|
1468
|
+
// 6. Clear state
|
|
1469
|
+
this.state.fileDiffMode = false;
|
|
1470
|
+
this.state.fileDiffFilename = null;
|
|
1471
|
+
this.enforceSidebarWidth();
|
|
1472
|
+
tmux.selectPane(this.state.sidebarPaneId);
|
|
1473
|
+
this.render();
|
|
1474
|
+
}
|
|
1475
|
+
/**
|
|
1476
|
+
* Handle input in file diff view mode
|
|
1477
|
+
* With the new architecture, scrolling is handled natively by `less`.
|
|
1478
|
+
* The sidebar only handles Escape to close.
|
|
1479
|
+
*/
|
|
1480
|
+
handleFileDiffInput(key) {
|
|
1481
|
+
// Escape: close file diff view
|
|
1482
|
+
if (key.key === 'escape') {
|
|
1483
|
+
this.hideFileDiff();
|
|
1484
|
+
return;
|
|
1485
|
+
}
|
|
1486
|
+
// Other keys are passed through to sidebar's normal handling
|
|
1487
|
+
}
|
|
1488
|
+
/**
|
|
1489
|
+
* Execute a file diff command received from the file diff header handler
|
|
1490
|
+
* Format: "FILEDIFF:<action>" or "FILEDIFF:mode:<mode>"
|
|
1491
|
+
*/
|
|
1492
|
+
executeFileDiffCommand(command) {
|
|
1493
|
+
debugLog('executeFileDiffCommand:', command);
|
|
1494
|
+
if (!command.startsWith('FILEDIFF:'))
|
|
1495
|
+
return;
|
|
1496
|
+
const action = command.slice(9); // Remove "FILEDIFF:" prefix
|
|
1497
|
+
switch (action) {
|
|
1498
|
+
case 'close':
|
|
1499
|
+
this.hideFileDiff();
|
|
1500
|
+
break;
|
|
1501
|
+
case 'mode:diffs-only':
|
|
1502
|
+
this.setDiffViewMode('diffs-only');
|
|
1503
|
+
break;
|
|
1504
|
+
case 'mode:whole-file':
|
|
1505
|
+
this.setDiffViewMode('whole-file');
|
|
1506
|
+
break;
|
|
1507
|
+
}
|
|
1508
|
+
}
|
|
1509
|
+
/**
|
|
1510
|
+
* Set the diff view mode and recreate the file diff panes
|
|
1511
|
+
*/
|
|
1512
|
+
async setDiffViewMode(mode) {
|
|
1513
|
+
const session = this.state.sessions.find(s => s.id === this.state.activeSessionId);
|
|
1514
|
+
if (!session || !this.state.fileDiffMode || !this.state.fileDiffFilename)
|
|
1515
|
+
return;
|
|
1516
|
+
// Already in this mode
|
|
1517
|
+
if (session.diffViewMode === mode)
|
|
1518
|
+
return;
|
|
1519
|
+
debugLog('setDiffViewMode:', mode);
|
|
1520
|
+
const worktree = this.state.worktrees.find(w => w.id === session.worktreeId);
|
|
1521
|
+
if (!worktree)
|
|
1522
|
+
return;
|
|
1523
|
+
// Update mode in session
|
|
1524
|
+
session.diffViewMode = mode;
|
|
1525
|
+
// Get stats for header
|
|
1526
|
+
const files = await (0, diff_1.getDiffSummary)(worktree.path);
|
|
1527
|
+
const fileInfo = files.find(f => f.file === this.state.fileDiffFilename);
|
|
1528
|
+
const insertions = fileInfo?.insertions || 0;
|
|
1529
|
+
const deletions = fileInfo?.deletions || 0;
|
|
1530
|
+
const filename = this.state.fileDiffFilename;
|
|
1531
|
+
// Break diff pane first (if exists) so we can recreate proper layout
|
|
1532
|
+
const hadDiffPane = !!session.diffPaneId;
|
|
1533
|
+
if (session.diffPaneId) {
|
|
1534
|
+
(0, diff_1.breakDiffPane)(session.diffPaneId);
|
|
1535
|
+
}
|
|
1536
|
+
// Kill existing header and content panes
|
|
1537
|
+
if (session.fileDiffHeaderPaneId) {
|
|
1538
|
+
(0, diff_2.closeFileDiffHeaderPane)(session.fileDiffHeaderPaneId);
|
|
1539
|
+
}
|
|
1540
|
+
if (session.fileDiffContentPaneId) {
|
|
1541
|
+
(0, diff_2.closeFileDiffContentPane)(session.fileDiffContentPaneId);
|
|
1542
|
+
}
|
|
1543
|
+
// Create new content pane (to the right of sidebar)
|
|
1544
|
+
const contentPaneId = (0, diff_2.createFileDiffContentPane)(this.state.sessionName, this.state.sidebarPaneId);
|
|
1545
|
+
session.fileDiffContentPaneId = contentPaneId;
|
|
1546
|
+
// Rejoin diff pane FIRST (to the right of content) - BEFORE creating header
|
|
1547
|
+
if (hadDiffPane && session.diffPaneId) {
|
|
1548
|
+
(0, diff_1.joinDiffPane)(session.diffPaneId, contentPaneId);
|
|
1549
|
+
}
|
|
1550
|
+
// NOW create new header pane (1-row above content only)
|
|
1551
|
+
const headerPaneId = (0, diff_2.createFileDiffHeaderPane)(this.state.sessionName, contentPaneId);
|
|
1552
|
+
session.fileDiffHeaderPaneId = headerPaneId;
|
|
1553
|
+
// Start header handler with new mode
|
|
1554
|
+
(0, diff_2.startFileDiffHeaderHandler)(headerPaneId, this.state.sidebarPaneId, filename, insertions, deletions, mode);
|
|
1555
|
+
// Start content handler with new mode
|
|
1556
|
+
(0, diff_2.startFileDiffContentHandler)(contentPaneId, this.state.sidebarPaneId, worktree.path, filename, mode);
|
|
1557
|
+
// Ensure sidebar width
|
|
1558
|
+
this.enforceSidebarWidth();
|
|
1559
|
+
// Focus the content pane
|
|
1560
|
+
tmux.selectPane(contentPaneId);
|
|
1561
|
+
}
|
|
1562
|
+
// ==========================================================================
|
|
1074
1563
|
// Fullscreen Modal Management
|
|
1075
1564
|
// ==========================================================================
|
|
1076
1565
|
/**
|
|
@@ -1081,11 +1570,15 @@ class SidebarApp {
|
|
|
1081
1570
|
return; // Already in fullscreen
|
|
1082
1571
|
debugLog('enterFullscreenModal: hiding panes');
|
|
1083
1572
|
if (this.state.activeSessionId) {
|
|
1084
|
-
// Hide the active session's panes (Claude pane + terminals)
|
|
1573
|
+
// Hide the active session's panes (Claude pane + terminals + diff pane)
|
|
1085
1574
|
const activeSession = this.state.sessions.find(s => s.id === this.state.activeSessionId);
|
|
1086
1575
|
if (activeSession) {
|
|
1087
1576
|
try {
|
|
1088
|
-
// Break
|
|
1577
|
+
// Break diff pane first (if any)
|
|
1578
|
+
if (activeSession.diffPaneId) {
|
|
1579
|
+
(0, diff_1.breakDiffPane)(activeSession.diffPaneId);
|
|
1580
|
+
}
|
|
1581
|
+
// Break active terminal (if any)
|
|
1089
1582
|
if (activeSession.terminals.length > 0) {
|
|
1090
1583
|
const activeTerminal = activeSession.terminals[activeSession.activeTerminalIndex];
|
|
1091
1584
|
if (activeTerminal) {
|
|
@@ -1131,8 +1624,13 @@ class SidebarApp {
|
|
|
1131
1624
|
// Join Claude pane
|
|
1132
1625
|
tmux.joinPane(this.state.hiddenPaneId, this.state.sidebarPaneId, true);
|
|
1133
1626
|
debugLog('exitFullscreenModal: joined Claude pane');
|
|
1134
|
-
// If active session has terminals, join those too
|
|
1135
1627
|
const activeSession = this.state.sessions.find(s => s.id === this.state.activeSessionId);
|
|
1628
|
+
// Join diff pane if it exists (to the right of Claude pane)
|
|
1629
|
+
if (activeSession && activeSession.diffPaneId) {
|
|
1630
|
+
(0, diff_1.joinDiffPane)(activeSession.diffPaneId, activeSession.paneId);
|
|
1631
|
+
debugLog('exitFullscreenModal: joined diff pane');
|
|
1632
|
+
}
|
|
1633
|
+
// If active session has terminals, join those too
|
|
1136
1634
|
if (activeSession && activeSession.terminals.length > 0 && activeSession.terminalBarPaneId) {
|
|
1137
1635
|
// Join terminal bar below Claude pane
|
|
1138
1636
|
tmux.joinPane(activeSession.terminalBarPaneId, activeSession.paneId, false);
|