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.
Files changed (66) hide show
  1. package/dist/constants.d.ts +6 -1
  2. package/dist/constants.d.ts.map +1 -1
  3. package/dist/constants.js +7 -2
  4. package/dist/constants.js.map +1 -1
  5. package/dist/diff/diff-handler.d.ts +15 -0
  6. package/dist/diff/diff-handler.d.ts.map +1 -0
  7. package/dist/diff/diff-handler.js +235 -0
  8. package/dist/diff/diff-handler.js.map +1 -0
  9. package/dist/diff/diff-manager.d.ts +63 -0
  10. package/dist/diff/diff-manager.d.ts.map +1 -0
  11. package/dist/diff/diff-manager.js +271 -0
  12. package/dist/diff/diff-manager.js.map +1 -0
  13. package/dist/diff/diff-pane-render.d.ts +34 -0
  14. package/dist/diff/diff-pane-render.d.ts.map +1 -0
  15. package/dist/diff/diff-pane-render.js +222 -0
  16. package/dist/diff/diff-pane-render.js.map +1 -0
  17. package/dist/diff/file-diff-content-handler.d.ts +16 -0
  18. package/dist/diff/file-diff-content-handler.d.ts.map +1 -0
  19. package/dist/diff/file-diff-content-handler.js +142 -0
  20. package/dist/diff/file-diff-content-handler.js.map +1 -0
  21. package/dist/diff/file-diff-header-handler.d.ts +16 -0
  22. package/dist/diff/file-diff-header-handler.d.ts.map +1 -0
  23. package/dist/diff/file-diff-header-handler.js +191 -0
  24. package/dist/diff/file-diff-header-handler.js.map +1 -0
  25. package/dist/diff/file-diff-header-render.d.ts +30 -0
  26. package/dist/diff/file-diff-header-render.d.ts.map +1 -0
  27. package/dist/diff/file-diff-header-render.js +101 -0
  28. package/dist/diff/file-diff-header-render.js.map +1 -0
  29. package/dist/diff/file-diff-render.d.ts +27 -0
  30. package/dist/diff/file-diff-render.d.ts.map +1 -0
  31. package/dist/diff/file-diff-render.js +211 -0
  32. package/dist/diff/file-diff-render.js.map +1 -0
  33. package/dist/diff/git-diff.d.ts +54 -0
  34. package/dist/diff/git-diff.d.ts.map +1 -0
  35. package/dist/diff/git-diff.js +599 -0
  36. package/dist/diff/git-diff.js.map +1 -0
  37. package/dist/diff/index.d.ts +8 -0
  38. package/dist/diff/index.d.ts.map +1 -0
  39. package/dist/diff/index.js +37 -0
  40. package/dist/diff/index.js.map +1 -0
  41. package/dist/sidebar/app.d.ts +69 -0
  42. package/dist/sidebar/app.d.ts.map +1 -1
  43. package/dist/sidebar/app.js +508 -10
  44. package/dist/sidebar/app.js.map +1 -1
  45. package/dist/sidebar/commands.d.ts +1 -0
  46. package/dist/sidebar/commands.d.ts.map +1 -1
  47. package/dist/sidebar/commands.js +11 -0
  48. package/dist/sidebar/commands.js.map +1 -1
  49. package/dist/sidebar/pane-orchestrator.d.ts.map +1 -1
  50. package/dist/sidebar/pane-orchestrator.js +10 -1
  51. package/dist/sidebar/pane-orchestrator.js.map +1 -1
  52. package/dist/sidebar/render.d.ts.map +1 -1
  53. package/dist/sidebar/render.js +2 -1
  54. package/dist/sidebar/render.js.map +1 -1
  55. package/dist/tmux/index.d.ts +1 -1
  56. package/dist/tmux/index.d.ts.map +1 -1
  57. package/dist/tmux/index.js +2 -1
  58. package/dist/tmux/index.js.map +1 -1
  59. package/dist/tmux/pane.d.ts +5 -0
  60. package/dist/tmux/pane.d.ts.map +1 -1
  61. package/dist/tmux/pane.js +10 -0
  62. package/dist/tmux/pane.js.map +1 -1
  63. package/dist/types.d.ts +10 -0
  64. package/dist/types.d.ts.map +1 -1
  65. package/dist/types.js.map +1 -1
  66. package/package.json +1 -1
@@ -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.executeTerminalCommand(this.state.terminalCommandBuffer);
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 pane
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
- // Break current session's panes (Claude pane + terminals)
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 active terminal first (if any)
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 active terminal first (if any)
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);