claude-code-plus-plus 0.1.0 → 0.2.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/app.d.ts +52 -0
- package/dist/app.d.ts.map +1 -0
- package/dist/app.js +318 -0
- package/dist/app.js.map +1 -0
- package/dist/cli/commands/index.d.ts +5 -0
- package/dist/cli/commands/index.d.ts.map +1 -0
- package/dist/cli/commands/index.js +5 -0
- package/dist/cli/commands/index.js.map +1 -0
- package/dist/cli/commands/start.d.ts +16 -0
- package/dist/cli/commands/start.d.ts.map +1 -0
- package/dist/cli/commands/start.js +20 -0
- package/dist/cli/commands/start.js.map +1 -0
- package/dist/cli/index.d.ts +7 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +7 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli/parser.d.ts +31 -0
- package/dist/cli/parser.d.ts.map +1 -0
- package/dist/cli/parser.js +112 -0
- package/dist/cli/parser.js.map +1 -0
- package/dist/cli/validators.d.ts +46 -0
- package/dist/cli/validators.d.ts.map +1 -0
- package/dist/cli/validators.js +149 -0
- package/dist/cli/validators.js.map +1 -0
- package/dist/config/defaults.d.ts +14 -0
- package/dist/config/defaults.d.ts.map +1 -0
- package/dist/config/defaults.js +27 -0
- package/dist/config/defaults.js.map +1 -0
- package/dist/config/index.d.ts +10 -0
- package/dist/config/index.d.ts.map +1 -0
- package/dist/config/index.js +12 -0
- package/dist/config/index.js.map +1 -0
- package/dist/config/loader.d.ts +38 -0
- package/dist/config/loader.d.ts.map +1 -0
- package/dist/config/loader.js +107 -0
- package/dist/config/loader.js.map +1 -0
- package/dist/config/paths.d.ts +47 -0
- package/dist/config/paths.d.ts.map +1 -0
- package/dist/config/paths.js +86 -0
- package/dist/config/paths.js.map +1 -0
- package/dist/config/schema.d.ts +15 -0
- package/dist/config/schema.d.ts.map +1 -0
- package/dist/config/schema.js +32 -0
- package/dist/config/schema.js.map +1 -0
- package/dist/core/index.d.ts +13 -1
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +13 -1
- package/dist/core/index.js.map +1 -1
- package/dist/core/session/index.d.ts +7 -0
- package/dist/core/session/index.d.ts.map +1 -0
- package/dist/core/session/index.js +6 -0
- package/dist/core/session/index.js.map +1 -0
- package/dist/core/session/session-manager.d.ts +46 -0
- package/dist/core/session/session-manager.d.ts.map +1 -0
- package/dist/core/session/session-manager.js +245 -0
- package/dist/core/session/session-manager.js.map +1 -0
- package/dist/core/session/session.d.ts +44 -0
- package/dist/core/session/session.d.ts.map +1 -0
- package/dist/core/session/session.js +78 -0
- package/dist/core/session/session.js.map +1 -0
- package/dist/core/terminal/index.d.ts +7 -0
- package/dist/core/terminal/index.d.ts.map +1 -0
- package/dist/core/terminal/index.js +6 -0
- package/dist/core/terminal/index.js.map +1 -0
- package/dist/core/terminal/terminal-manager.d.ts +36 -0
- package/dist/core/terminal/terminal-manager.d.ts.map +1 -0
- package/dist/core/terminal/terminal-manager.js +182 -0
- package/dist/core/terminal/terminal-manager.js.map +1 -0
- package/dist/core/terminal/terminal.d.ts +27 -0
- package/dist/core/terminal/terminal.d.ts.map +1 -0
- package/dist/core/terminal/terminal.js +38 -0
- package/dist/core/terminal/terminal.js.map +1 -0
- package/dist/core/workspace/index.d.ts +7 -0
- package/dist/core/workspace/index.d.ts.map +1 -0
- package/dist/core/workspace/index.js +6 -0
- package/dist/core/workspace/index.js.map +1 -0
- package/dist/core/workspace/workspace-manager.d.ts +59 -0
- package/dist/core/workspace/workspace-manager.d.ts.map +1 -0
- package/dist/core/workspace/workspace-manager.js +224 -0
- package/dist/core/workspace/workspace-manager.js.map +1 -0
- package/dist/core/workspace/workspace.d.ts +56 -0
- package/dist/core/workspace/workspace.d.ts.map +1 -0
- package/dist/core/workspace/workspace.js +96 -0
- package/dist/core/workspace/workspace.js.map +1 -0
- package/dist/core/worktree/index.d.ts +7 -0
- package/dist/core/worktree/index.d.ts.map +1 -0
- package/dist/core/worktree/index.js +6 -0
- package/dist/core/worktree/index.js.map +1 -0
- package/dist/core/worktree/worktree-manager.d.ts +47 -0
- package/dist/core/worktree/worktree-manager.d.ts.map +1 -0
- package/dist/core/worktree/worktree-manager.js +170 -0
- package/dist/core/worktree/worktree-manager.js.map +1 -0
- package/dist/core/worktree/worktree.d.ts +35 -0
- package/dist/core/worktree/worktree.d.ts.map +1 -0
- package/dist/core/worktree/worktree.js +50 -0
- package/dist/core/worktree/worktree.js.map +1 -0
- package/dist/events/bus.d.ts +76 -0
- package/dist/events/bus.d.ts.map +1 -0
- package/dist/events/bus.js +223 -0
- package/dist/events/bus.js.map +1 -0
- package/dist/events/handlers/index.d.ts +28 -0
- package/dist/events/handlers/index.d.ts.map +1 -0
- package/dist/events/handlers/index.js +44 -0
- package/dist/events/handlers/index.js.map +1 -0
- package/dist/events/handlers/session-events.d.ts +12 -0
- package/dist/events/handlers/session-events.d.ts.map +1 -0
- package/dist/events/handlers/session-events.js +38 -0
- package/dist/events/handlers/session-events.js.map +1 -0
- package/dist/events/handlers/terminal-events.d.ts +12 -0
- package/dist/events/handlers/terminal-events.d.ts.map +1 -0
- package/dist/events/handlers/terminal-events.js +37 -0
- package/dist/events/handlers/terminal-events.js.map +1 -0
- package/dist/events/handlers/ui-events.d.ts +11 -0
- package/dist/events/handlers/ui-events.d.ts.map +1 -0
- package/dist/events/handlers/ui-events.js +202 -0
- package/dist/events/handlers/ui-events.js.map +1 -0
- package/dist/events/index.d.ts +9 -0
- package/dist/events/index.d.ts.map +1 -0
- package/dist/events/index.js +10 -0
- package/dist/events/index.js.map +1 -0
- package/dist/git/index.d.ts +5 -0
- package/dist/git/index.d.ts.map +1 -0
- package/dist/git/index.js +5 -0
- package/dist/git/index.js.map +1 -0
- package/dist/git/worktree.d.ts +44 -0
- package/dist/git/worktree.d.ts.map +1 -0
- package/dist/git/worktree.js +158 -0
- package/dist/git/worktree.js.map +1 -0
- package/dist/index.d.ts +0 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +37 -144
- package/dist/index.js.map +1 -1
- package/dist/launcher/index.d.ts +24 -0
- package/dist/launcher/index.d.ts.map +1 -0
- package/dist/launcher/index.js +226 -0
- package/dist/launcher/index.js.map +1 -0
- package/dist/multiplexer/base.d.ts +73 -0
- package/dist/multiplexer/base.d.ts.map +1 -0
- package/dist/multiplexer/base.js +68 -0
- package/dist/multiplexer/base.js.map +1 -0
- package/dist/multiplexer/factory.d.ts +28 -0
- package/dist/multiplexer/factory.d.ts.map +1 -0
- package/dist/multiplexer/factory.js +72 -0
- package/dist/multiplexer/factory.js.map +1 -0
- package/dist/multiplexer/index.d.ts +14 -0
- package/dist/multiplexer/index.d.ts.map +1 -0
- package/dist/multiplexer/index.js +15 -0
- package/dist/multiplexer/index.js.map +1 -0
- package/dist/multiplexer/tmux/index.d.ts +9 -0
- package/dist/multiplexer/tmux/index.d.ts.map +1 -0
- package/dist/multiplexer/tmux/index.js +7 -0
- package/dist/multiplexer/tmux/index.js.map +1 -0
- package/dist/multiplexer/tmux/tmux-commands.d.ts +96 -0
- package/dist/multiplexer/tmux/tmux-commands.d.ts.map +1 -0
- package/dist/multiplexer/tmux/tmux-commands.js +282 -0
- package/dist/multiplexer/tmux/tmux-commands.js.map +1 -0
- package/dist/multiplexer/tmux/tmux-config.d.ts +57 -0
- package/dist/multiplexer/tmux/tmux-config.d.ts.map +1 -0
- package/dist/multiplexer/tmux/tmux-config.js +103 -0
- package/dist/multiplexer/tmux/tmux-config.js.map +1 -0
- package/dist/multiplexer/tmux/tmux-multiplexer.d.ts +94 -0
- package/dist/multiplexer/tmux/tmux-multiplexer.d.ts.map +1 -0
- package/dist/multiplexer/tmux/tmux-multiplexer.js +302 -0
- package/dist/multiplexer/tmux/tmux-multiplexer.js.map +1 -0
- package/dist/multiplexer/windows/index.d.ts +5 -0
- package/dist/multiplexer/windows/index.d.ts.map +1 -0
- package/dist/multiplexer/windows/index.js +5 -0
- package/dist/multiplexer/windows/index.js.map +1 -0
- package/dist/multiplexer/windows/windows-multiplexer.d.ts +61 -0
- package/dist/multiplexer/windows/windows-multiplexer.d.ts.map +1 -0
- package/dist/multiplexer/windows/windows-multiplexer.js +151 -0
- package/dist/multiplexer/windows/windows-multiplexer.js.map +1 -0
- package/dist/platform/clipboard.d.ts +26 -0
- package/dist/platform/clipboard.d.ts.map +1 -0
- package/dist/platform/clipboard.js +154 -0
- package/dist/platform/clipboard.js.map +1 -0
- package/dist/platform/detector.d.ts +78 -0
- package/dist/platform/detector.d.ts.map +1 -0
- package/dist/platform/detector.js +169 -0
- package/dist/platform/detector.js.map +1 -0
- package/dist/platform/index.d.ts +14 -0
- package/dist/platform/index.d.ts.map +1 -0
- package/dist/platform/index.js +15 -0
- package/dist/platform/index.js.map +1 -0
- package/dist/platform/paths.d.ts +63 -0
- package/dist/platform/paths.d.ts.map +1 -0
- package/dist/platform/paths.js +149 -0
- package/dist/platform/paths.js.map +1 -0
- package/dist/platform/shell.d.ts +43 -0
- package/dist/platform/shell.d.ts.map +1 -0
- package/dist/platform/shell.js +187 -0
- package/dist/platform/shell.js.map +1 -0
- package/dist/services/claude/claude-detector.d.ts +37 -0
- package/dist/services/claude/claude-detector.d.ts.map +1 -0
- package/dist/services/claude/claude-detector.js +83 -0
- package/dist/services/claude/claude-detector.js.map +1 -0
- package/dist/services/claude/claude-service.d.ts +57 -0
- package/dist/services/claude/claude-service.d.ts.map +1 -0
- package/dist/services/claude/claude-service.js +108 -0
- package/dist/services/claude/claude-service.js.map +1 -0
- package/dist/services/claude/index.d.ts +7 -0
- package/dist/services/claude/index.d.ts.map +1 -0
- package/dist/services/claude/index.js +6 -0
- package/dist/services/claude/index.js.map +1 -0
- package/dist/services/git/branch-service.d.ts +64 -0
- package/dist/services/git/branch-service.d.ts.map +1 -0
- package/dist/services/git/branch-service.js +217 -0
- package/dist/services/git/branch-service.js.map +1 -0
- package/dist/services/git/git-service.d.ts +44 -0
- package/dist/services/git/git-service.d.ts.map +1 -0
- package/dist/services/git/git-service.js +145 -0
- package/dist/services/git/git-service.js.map +1 -0
- package/dist/services/git/index.d.ts +8 -0
- package/dist/services/git/index.d.ts.map +1 -0
- package/dist/services/git/index.js +7 -0
- package/dist/services/git/index.js.map +1 -0
- package/dist/services/git/worktree-service.d.ts +61 -0
- package/dist/services/git/worktree-service.d.ts.map +1 -0
- package/dist/services/git/worktree-service.js +243 -0
- package/dist/services/git/worktree-service.js.map +1 -0
- package/dist/services/index.d.ts +12 -0
- package/dist/services/index.d.ts.map +1 -0
- package/dist/services/index.js +12 -0
- package/dist/services/index.js.map +1 -0
- package/dist/services/process/index.d.ts +8 -0
- package/dist/services/process/index.d.ts.map +1 -0
- package/dist/services/process/index.js +6 -0
- package/dist/services/process/index.js.map +1 -0
- package/dist/services/process/process-monitor.d.ts +98 -0
- package/dist/services/process/process-monitor.d.ts.map +1 -0
- package/dist/services/process/process-monitor.js +173 -0
- package/dist/services/process/process-monitor.js.map +1 -0
- package/dist/services/process/process-spawner.d.ts +55 -0
- package/dist/services/process/process-spawner.d.ts.map +1 -0
- package/dist/services/process/process-spawner.js +93 -0
- package/dist/services/process/process-spawner.js.map +1 -0
- package/dist/sidebar/app.d.ts +70 -0
- package/dist/sidebar/app.d.ts.map +1 -0
- package/dist/sidebar/app.js +1203 -0
- package/dist/sidebar/app.js.map +1 -0
- package/dist/sidebar/index.d.ts +8 -0
- package/dist/sidebar/index.d.ts.map +1 -0
- package/dist/sidebar/index.js +26 -0
- package/dist/sidebar/index.js.map +1 -0
- package/dist/sidebar/input.d.ts +33 -0
- package/dist/sidebar/input.d.ts.map +1 -0
- package/dist/sidebar/input.js +153 -0
- package/dist/sidebar/input.js.map +1 -0
- package/dist/sidebar/render.d.ts +73 -0
- package/dist/sidebar/render.d.ts.map +1 -0
- package/dist/sidebar/render.js +403 -0
- package/dist/sidebar/render.js.map +1 -0
- package/dist/state/actions.d.ts +49 -0
- package/dist/state/actions.d.ts.map +1 -0
- package/dist/state/actions.js +512 -0
- package/dist/state/actions.js.map +1 -0
- package/dist/state/index.d.ts +16 -0
- package/dist/state/index.d.ts.map +1 -0
- package/dist/state/index.js +28 -0
- package/dist/state/index.js.map +1 -0
- package/dist/state/middleware/index.d.ts +7 -0
- package/dist/state/middleware/index.d.ts.map +1 -0
- package/dist/state/middleware/index.js +6 -0
- package/dist/state/middleware/index.js.map +1 -0
- package/dist/state/middleware/logger.d.ts +22 -0
- package/dist/state/middleware/logger.d.ts.map +1 -0
- package/dist/state/middleware/logger.js +45 -0
- package/dist/state/middleware/logger.js.map +1 -0
- package/dist/state/middleware/persistence.d.ts +29 -0
- package/dist/state/middleware/persistence.d.ts.map +1 -0
- package/dist/state/middleware/persistence.js +71 -0
- package/dist/state/middleware/persistence.js.map +1 -0
- package/dist/state/persistence/file-adapter.d.ts +48 -0
- package/dist/state/persistence/file-adapter.d.ts.map +1 -0
- package/dist/state/persistence/file-adapter.js +96 -0
- package/dist/state/persistence/file-adapter.js.map +1 -0
- package/dist/state/persistence/index.d.ts +8 -0
- package/dist/state/persistence/index.d.ts.map +1 -0
- package/dist/state/persistence/index.js +6 -0
- package/dist/state/persistence/index.js.map +1 -0
- package/dist/state/persistence/serializer.d.ts +41 -0
- package/dist/state/persistence/serializer.d.ts.map +1 -0
- package/dist/state/persistence/serializer.js +114 -0
- package/dist/state/persistence/serializer.js.map +1 -0
- package/dist/state/selectors.d.ts +67 -0
- package/dist/state/selectors.d.ts.map +1 -0
- package/dist/state/selectors.js +177 -0
- package/dist/state/selectors.js.map +1 -0
- package/dist/state/store.d.ts +86 -0
- package/dist/state/store.d.ts.map +1 -0
- package/dist/state/store.js +161 -0
- package/dist/state/store.js.map +1 -0
- package/dist/state/types.d.ts +269 -0
- package/dist/state/types.d.ts.map +1 -0
- package/dist/state/types.js +7 -0
- package/dist/state/types.js.map +1 -0
- package/dist/terminal/bar-handler.d.ts +15 -0
- package/dist/terminal/bar-handler.d.ts.map +1 -0
- package/dist/terminal/bar-handler.js +242 -0
- package/dist/terminal/bar-handler.js.map +1 -0
- package/dist/terminal/bar-render.d.ts +33 -0
- package/dist/terminal/bar-render.d.ts.map +1 -0
- package/dist/terminal/bar-render.js +122 -0
- package/dist/terminal/bar-render.js.map +1 -0
- package/dist/terminal/index.d.ts +8 -0
- package/dist/terminal/index.d.ts.map +1 -0
- package/dist/terminal/index.js +9 -0
- package/dist/terminal/index.js.map +1 -0
- package/dist/tmux/commands.d.ts +26 -0
- package/dist/tmux/commands.d.ts.map +1 -0
- package/dist/tmux/commands.js +67 -0
- package/dist/tmux/commands.js.map +1 -0
- package/dist/tmux/index.d.ts +6 -0
- package/dist/tmux/index.d.ts.map +1 -0
- package/dist/tmux/index.js +16 -0
- package/dist/tmux/index.js.map +1 -0
- package/dist/tmux/pane.d.ts +111 -0
- package/dist/tmux/pane.d.ts.map +1 -0
- package/dist/tmux/pane.js +206 -0
- package/dist/tmux/pane.js.map +1 -0
- package/dist/types/config.d.ts +103 -0
- package/dist/types/config.d.ts.map +1 -0
- package/dist/types/config.js +7 -0
- package/dist/types/config.js.map +1 -0
- package/dist/types/entities.d.ts +88 -0
- package/dist/types/entities.d.ts.map +1 -0
- package/dist/types/entities.js +8 -0
- package/dist/types/entities.js.map +1 -0
- package/dist/types/events.d.ts +156 -0
- package/dist/types/events.d.ts.map +1 -0
- package/dist/types/events.js +8 -0
- package/dist/types/events.js.map +1 -0
- package/dist/types/index.d.ts +11 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +8 -0
- package/dist/types/index.js.map +1 -0
- package/dist/types/multiplexer.d.ts +238 -0
- package/dist/types/multiplexer.d.ts.map +1 -0
- package/dist/types/multiplexer.js +8 -0
- package/dist/types/multiplexer.js.map +1 -0
- package/dist/types.d.ts +67 -3
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +6 -2
- package/dist/types.js.map +1 -1
- package/dist/ui/components/base.d.ts +155 -0
- package/dist/ui/components/base.d.ts.map +1 -0
- package/dist/ui/components/base.js +269 -0
- package/dist/ui/components/base.js.map +1 -0
- package/dist/ui/components/index.d.ts +10 -0
- package/dist/ui/components/index.d.ts.map +1 -0
- package/dist/ui/components/index.js +10 -0
- package/dist/ui/components/index.js.map +1 -0
- package/dist/ui/components/input-field.d.ts +98 -0
- package/dist/ui/components/input-field.d.ts.map +1 -0
- package/dist/ui/components/input-field.js +274 -0
- package/dist/ui/components/input-field.js.map +1 -0
- package/dist/ui/components/list.d.ts +94 -0
- package/dist/ui/components/list.d.ts.map +1 -0
- package/dist/ui/components/list.js +294 -0
- package/dist/ui/components/list.js.map +1 -0
- package/dist/ui/components/modal.d.ts +83 -0
- package/dist/ui/components/modal.d.ts.map +1 -0
- package/dist/ui/components/modal.js +249 -0
- package/dist/ui/components/modal.js.map +1 -0
- package/dist/ui/components/tabs.d.ts +97 -0
- package/dist/ui/components/tabs.d.ts.map +1 -0
- package/dist/ui/components/tabs.js +252 -0
- package/dist/ui/components/tabs.js.map +1 -0
- package/dist/ui/components/text.d.ts +60 -0
- package/dist/ui/components/text.d.ts.map +1 -0
- package/dist/ui/components/text.js +224 -0
- package/dist/ui/components/text.js.map +1 -0
- package/dist/ui/index.d.ts +12 -0
- package/dist/ui/index.d.ts.map +1 -0
- package/dist/ui/index.js +18 -0
- package/dist/ui/index.js.map +1 -0
- package/dist/ui/input/index.d.ts +8 -0
- package/dist/ui/input/index.d.ts.map +1 -0
- package/dist/ui/input/index.js +8 -0
- package/dist/ui/input/index.js.map +1 -0
- package/dist/ui/input/input-manager.d.ts +83 -0
- package/dist/ui/input/input-manager.d.ts.map +1 -0
- package/dist/ui/input/input-manager.js +200 -0
- package/dist/ui/input/input-manager.js.map +1 -0
- package/dist/ui/input/keybindings.d.ts +47 -0
- package/dist/ui/input/keybindings.d.ts.map +1 -0
- package/dist/ui/input/keybindings.js +168 -0
- package/dist/ui/input/keybindings.js.map +1 -0
- package/dist/ui/input/keyboard.d.ts +24 -0
- package/dist/ui/input/keyboard.d.ts.map +1 -0
- package/dist/ui/input/keyboard.js +230 -0
- package/dist/ui/input/keyboard.js.map +1 -0
- package/dist/ui/input/mouse.d.ts +44 -0
- package/dist/ui/input/mouse.d.ts.map +1 -0
- package/dist/ui/input/mouse.js +161 -0
- package/dist/ui/input/mouse.js.map +1 -0
- package/dist/ui/layout/box.d.ts +63 -0
- package/dist/ui/layout/box.d.ts.map +1 -0
- package/dist/ui/layout/box.js +160 -0
- package/dist/ui/layout/box.js.map +1 -0
- package/dist/ui/layout/constraints.d.ts +77 -0
- package/dist/ui/layout/constraints.d.ts.map +1 -0
- package/dist/ui/layout/constraints.js +154 -0
- package/dist/ui/layout/constraints.js.map +1 -0
- package/dist/ui/layout/flex.d.ts +59 -0
- package/dist/ui/layout/flex.d.ts.map +1 -0
- package/dist/ui/layout/flex.js +163 -0
- package/dist/ui/layout/flex.js.map +1 -0
- package/dist/ui/layout/index.d.ts +7 -0
- package/dist/ui/layout/index.d.ts.map +1 -0
- package/dist/ui/layout/index.js +7 -0
- package/dist/ui/layout/index.js.map +1 -0
- package/dist/ui/modals/delete-modal.d.ts +27 -0
- package/dist/ui/modals/delete-modal.d.ts.map +1 -0
- package/dist/ui/modals/delete-modal.js +59 -0
- package/dist/ui/modals/delete-modal.js.map +1 -0
- package/dist/ui/modals/index.d.ts +7 -0
- package/dist/ui/modals/index.d.ts.map +1 -0
- package/dist/ui/modals/index.js +7 -0
- package/dist/ui/modals/index.js.map +1 -0
- package/dist/ui/modals/input-modal.d.ts +66 -0
- package/dist/ui/modals/input-modal.d.ts.map +1 -0
- package/dist/ui/modals/input-modal.js +185 -0
- package/dist/ui/modals/input-modal.js.map +1 -0
- package/dist/ui/modals/quit-modal.d.ts +30 -0
- package/dist/ui/modals/quit-modal.d.ts.map +1 -0
- package/dist/ui/modals/quit-modal.js +65 -0
- package/dist/ui/modals/quit-modal.js.map +1 -0
- package/dist/ui/renderer/ansi.d.ts +206 -0
- package/dist/ui/renderer/ansi.d.ts.map +1 -0
- package/dist/ui/renderer/ansi.js +318 -0
- package/dist/ui/renderer/ansi.js.map +1 -0
- package/dist/ui/renderer/canvas.d.ts +126 -0
- package/dist/ui/renderer/canvas.d.ts.map +1 -0
- package/dist/ui/renderer/canvas.js +346 -0
- package/dist/ui/renderer/canvas.js.map +1 -0
- package/dist/ui/renderer/colors.d.ts +65 -0
- package/dist/ui/renderer/colors.d.ts.map +1 -0
- package/dist/ui/renderer/colors.js +260 -0
- package/dist/ui/renderer/colors.js.map +1 -0
- package/dist/ui/renderer/index.d.ts +8 -0
- package/dist/ui/renderer/index.d.ts.map +1 -0
- package/dist/ui/renderer/index.js +8 -0
- package/dist/ui/renderer/index.js.map +1 -0
- package/dist/ui/renderer/symbols.d.ts +176 -0
- package/dist/ui/renderer/symbols.d.ts.map +1 -0
- package/dist/ui/renderer/symbols.js +246 -0
- package/dist/ui/renderer/symbols.js.map +1 -0
- package/dist/ui/views/index.d.ts +7 -0
- package/dist/ui/views/index.d.ts.map +1 -0
- package/dist/ui/views/index.js +7 -0
- package/dist/ui/views/index.js.map +1 -0
- package/dist/ui/views/sidebar-view.d.ts +89 -0
- package/dist/ui/views/sidebar-view.d.ts.map +1 -0
- package/dist/ui/views/sidebar-view.js +259 -0
- package/dist/ui/views/sidebar-view.js.map +1 -0
- package/dist/ui/views/terminal-bar-view.d.ts +67 -0
- package/dist/ui/views/terminal-bar-view.d.ts.map +1 -0
- package/dist/ui/views/terminal-bar-view.js +172 -0
- package/dist/ui/views/terminal-bar-view.js.map +1 -0
- package/dist/ui/views/welcome-view.d.ts +34 -0
- package/dist/ui/views/welcome-view.d.ts.map +1 -0
- package/dist/ui/views/welcome-view.js +116 -0
- package/dist/ui/views/welcome-view.js.map +1 -0
- package/dist/utils/async.d.ts +68 -0
- package/dist/utils/async.d.ts.map +1 -0
- package/dist/utils/async.js +138 -0
- package/dist/utils/async.js.map +1 -0
- package/dist/utils/errors.d.ts +136 -0
- package/dist/utils/errors.d.ts.map +1 -0
- package/dist/utils/errors.js +224 -0
- package/dist/utils/errors.js.map +1 -0
- package/dist/utils/id.d.ts +51 -0
- package/dist/utils/id.d.ts.map +1 -0
- package/dist/utils/id.js +79 -0
- package/dist/utils/id.js.map +1 -0
- package/dist/utils/index.d.ts +15 -0
- package/dist/utils/index.d.ts.map +1 -0
- package/dist/utils/index.js +19 -0
- package/dist/utils/index.js.map +1 -0
- package/dist/utils/logger.d.ts +75 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/dist/utils/logger.js +212 -0
- package/dist/utils/logger.js.map +1 -0
- package/dist/utils/string.d.ts +71 -0
- package/dist/utils/string.d.ts.map +1 -0
- package/dist/utils/string.js +163 -0
- package/dist/utils/string.js.map +1 -0
- package/dist/utils/validation.d.ts +72 -0
- package/dist/utils/validation.d.ts.map +1 -0
- package/dist/utils/validation.js +196 -0
- package/dist/utils/validation.js.map +1 -0
- package/package.json +4 -4
|
@@ -0,0 +1,1203 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sidebar Application
|
|
3
|
+
*
|
|
4
|
+
* Main sidebar class that handles state management, input handling, and rendering.
|
|
5
|
+
*/
|
|
6
|
+
import { appendFileSync, existsSync, writeFileSync } from 'fs';
|
|
7
|
+
import { execSync } from 'child_process';
|
|
8
|
+
import { fileURLToPath } from 'url';
|
|
9
|
+
import { dirname, resolve } from 'path';
|
|
10
|
+
import * as tmux from '../tmux';
|
|
11
|
+
import { WorktreeManager } from '../git';
|
|
12
|
+
import { parseKey, parseMouseEvent, isMouseEvent, setupRawMode, restoreMode } from './input';
|
|
13
|
+
import { ansi, buildListItems, renderMain, renderQuitModal, renderDeleteModal, renderInputModal, renderCollapsed, } from './render';
|
|
14
|
+
// Debug logging
|
|
15
|
+
function debugLog(...args) {
|
|
16
|
+
const msg = `[${new Date().toISOString()}] ${args.map(a => typeof a === 'object' ? JSON.stringify(a) : String(a)).join(' ')}\n`;
|
|
17
|
+
appendFileSync('/tmp/claude-pp-sidebar.log', msg);
|
|
18
|
+
}
|
|
19
|
+
// ============================================================================
|
|
20
|
+
// Constants
|
|
21
|
+
// ============================================================================
|
|
22
|
+
const SIDEBAR_WIDTH = 25;
|
|
23
|
+
const DEFAULT_CLAUDE_CMD = 'claude --dangerously-skip-permissions';
|
|
24
|
+
const TERMINAL_BAR_HEIGHT = 1;
|
|
25
|
+
const CLAUDE_PANE_PERCENT = 70; // Claude pane gets 70%, terminal area gets 30%
|
|
26
|
+
/**
|
|
27
|
+
* Set up terminal bar resize enforcement (hook + mouse binding).
|
|
28
|
+
* Uses a file-based script to avoid complex quoting issues.
|
|
29
|
+
* Sets up both after-resize-pane hook AND MouseDragEnd1Border binding.
|
|
30
|
+
*/
|
|
31
|
+
function setupTerminalBarResize(sessionName, claudePaneId, barPaneId, terminalBodyPaneId) {
|
|
32
|
+
const hookCmd = getTerminalBarResizeHook(claudePaneId, barPaneId, terminalBodyPaneId);
|
|
33
|
+
// Set up the after-resize-pane hook
|
|
34
|
+
tmux.setHook(sessionName, 'after-resize-pane', hookCmd);
|
|
35
|
+
// Also bind MouseDragEnd1Border to run the script when mouse drag ends
|
|
36
|
+
// This ensures the fix runs when user releases mouse after dragging border
|
|
37
|
+
const safeName = `${claudePaneId}-${barPaneId}`.replace(/%/g, '');
|
|
38
|
+
const scriptPath = `/tmp/cpp-resize-hook-${safeName}.sh`;
|
|
39
|
+
try {
|
|
40
|
+
execSync(`tmux bind-key -T root MouseDragEnd1Border run-shell "sh ${scriptPath}"`, { stdio: 'ignore' });
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
// Ignore errors
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Create a shell script for terminal bar resize hook and return the hook command.
|
|
48
|
+
* Uses a file-based script to avoid complex quoting issues.
|
|
49
|
+
*/
|
|
50
|
+
function getTerminalBarResizeHook(claudePaneId, barPaneId, terminalBodyPaneId) {
|
|
51
|
+
// Use pane IDs in filename to make it unique per terminal configuration
|
|
52
|
+
const safeName = `${claudePaneId}-${barPaneId}`.replace(/%/g, '');
|
|
53
|
+
const scriptPath = `/tmp/cpp-resize-hook-${safeName}.sh`;
|
|
54
|
+
// Write the script file
|
|
55
|
+
const scriptContent = `#!/bin/sh
|
|
56
|
+
# Terminal bar resize hook - keeps bar at ${TERMINAL_BAR_HEIGHT} row(s)
|
|
57
|
+
# Lock check - prevent recursion
|
|
58
|
+
LOCK=$(tmux show-option -gqv @cpp-resizing 2>/dev/null)
|
|
59
|
+
[ -n "$LOCK" ] && exit 0
|
|
60
|
+
|
|
61
|
+
# Get current heights
|
|
62
|
+
BAR_H=$(tmux display-message -p -t "${barPaneId}" '#{pane_height}' 2>/dev/null)
|
|
63
|
+
[ -z "$BAR_H" ] && exit 0
|
|
64
|
+
[ "$BAR_H" -eq ${TERMINAL_BAR_HEIGHT} ] && exit 0
|
|
65
|
+
|
|
66
|
+
CLAUDE_H=$(tmux display-message -p -t "${claudePaneId}" '#{pane_height}' 2>/dev/null)
|
|
67
|
+
BODY_H=$(tmux display-message -p -t "${terminalBodyPaneId}" '#{pane_height}' 2>/dev/null)
|
|
68
|
+
|
|
69
|
+
# Get previous heights (to detect which pane shrank)
|
|
70
|
+
PREV_CLAUDE=$(tmux show-option -gqv @cpp-prev-claude 2>/dev/null)
|
|
71
|
+
PREV_BODY=$(tmux show-option -gqv @cpp-prev-body 2>/dev/null)
|
|
72
|
+
|
|
73
|
+
# Acquire lock and set trap
|
|
74
|
+
tmux set-option -g @cpp-resizing 1
|
|
75
|
+
trap 'tmux set-option -gu @cpp-resizing 2>/dev/null' EXIT
|
|
76
|
+
|
|
77
|
+
# Calculate how much bar is over target
|
|
78
|
+
D=$((BAR_H - ${TERMINAL_BAR_HEIGHT}))
|
|
79
|
+
|
|
80
|
+
# Determine which pane to grow based on which one shrank
|
|
81
|
+
if [ -n "$PREV_CLAUDE" ] && [ "$CLAUDE_H" -lt "$PREV_CLAUDE" ]; then
|
|
82
|
+
# Claude shrank (user dragged tabs ceiling UP) -> grow Terminal body
|
|
83
|
+
tmux resize-pane -t "${terminalBodyPaneId}" -U "$D" 2>/dev/null
|
|
84
|
+
elif [ -n "$PREV_BODY" ] && [ "$BODY_H" -lt "$PREV_BODY" ]; then
|
|
85
|
+
# Terminal body shrank (user dragged body ceiling DOWN) -> grow Claude
|
|
86
|
+
tmux resize-pane -t "${claudePaneId}" -D "$D" 2>/dev/null
|
|
87
|
+
else
|
|
88
|
+
# Fallback: just set bar height directly, let tmux decide
|
|
89
|
+
:
|
|
90
|
+
fi
|
|
91
|
+
|
|
92
|
+
# Set bar to exact height
|
|
93
|
+
tmux resize-pane -t "${barPaneId}" -y ${TERMINAL_BAR_HEIGHT} 2>/dev/null
|
|
94
|
+
|
|
95
|
+
# Store FINAL heights (after adjustment) for next comparison
|
|
96
|
+
FINAL_CLAUDE=$(tmux display-message -p -t "${claudePaneId}" '#{pane_height}' 2>/dev/null)
|
|
97
|
+
FINAL_BODY=$(tmux display-message -p -t "${terminalBodyPaneId}" '#{pane_height}' 2>/dev/null)
|
|
98
|
+
tmux set-option -g @cpp-prev-claude "$FINAL_CLAUDE"
|
|
99
|
+
tmux set-option -g @cpp-prev-body "$FINAL_BODY"
|
|
100
|
+
`;
|
|
101
|
+
// Write script to file (synchronously)
|
|
102
|
+
writeFileSync(scriptPath, scriptContent, { mode: 0o755 });
|
|
103
|
+
return `"run-shell 'sh ${scriptPath}'"`;
|
|
104
|
+
}
|
|
105
|
+
// ============================================================================
|
|
106
|
+
// Sidebar App
|
|
107
|
+
// ============================================================================
|
|
108
|
+
export class SidebarApp {
|
|
109
|
+
state;
|
|
110
|
+
worktreeManager;
|
|
111
|
+
running = false;
|
|
112
|
+
constructor(repoPath, sessionName, mainPaneId, sidebarPaneId) {
|
|
113
|
+
this.worktreeManager = new WorktreeManager(repoPath);
|
|
114
|
+
this.state = {
|
|
115
|
+
repoPath,
|
|
116
|
+
sessionName,
|
|
117
|
+
mainPaneId,
|
|
118
|
+
sidebarPaneId,
|
|
119
|
+
worktrees: [],
|
|
120
|
+
sessions: [],
|
|
121
|
+
selectedIndex: 0,
|
|
122
|
+
activeSessionId: null,
|
|
123
|
+
expandedWorktrees: new Set(),
|
|
124
|
+
modal: 'none',
|
|
125
|
+
modalSelection: 0,
|
|
126
|
+
inputBuffer: '',
|
|
127
|
+
deleteTarget: null,
|
|
128
|
+
fullscreenModal: false,
|
|
129
|
+
hiddenPaneId: null,
|
|
130
|
+
collapsed: false,
|
|
131
|
+
terminalCommandMode: false,
|
|
132
|
+
terminalCommandBuffer: '',
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
// ==========================================================================
|
|
136
|
+
// Lifecycle
|
|
137
|
+
// ==========================================================================
|
|
138
|
+
async init() {
|
|
139
|
+
this.state.worktrees = await this.worktreeManager.list();
|
|
140
|
+
// Fallback if not a git repo
|
|
141
|
+
if (this.state.worktrees.length === 0) {
|
|
142
|
+
const { basename } = await import('path');
|
|
143
|
+
this.state.worktrees = [{
|
|
144
|
+
id: 'current',
|
|
145
|
+
path: this.state.repoPath,
|
|
146
|
+
branch: basename(this.state.repoPath),
|
|
147
|
+
isMain: true,
|
|
148
|
+
}];
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
start() {
|
|
152
|
+
this.running = true;
|
|
153
|
+
// Set up terminal
|
|
154
|
+
process.stdout.write(ansi.hideCursor);
|
|
155
|
+
process.stdout.write(ansi.enableMouse);
|
|
156
|
+
setupRawMode();
|
|
157
|
+
// Input handling
|
|
158
|
+
process.stdin.on('data', (data) => this.handleInput(data));
|
|
159
|
+
// Resize handling
|
|
160
|
+
process.stdout.on('resize', () => {
|
|
161
|
+
this.syncCollapsedState();
|
|
162
|
+
this.render();
|
|
163
|
+
});
|
|
164
|
+
// Initial render
|
|
165
|
+
this.render();
|
|
166
|
+
}
|
|
167
|
+
stop() {
|
|
168
|
+
this.running = false;
|
|
169
|
+
process.stdout.write(ansi.disableMouse);
|
|
170
|
+
process.stdout.write(ansi.showCursor);
|
|
171
|
+
process.stdout.write(ansi.reset);
|
|
172
|
+
restoreMode();
|
|
173
|
+
}
|
|
174
|
+
// ==========================================================================
|
|
175
|
+
// Rendering
|
|
176
|
+
// ==========================================================================
|
|
177
|
+
render() {
|
|
178
|
+
if (!this.running)
|
|
179
|
+
return;
|
|
180
|
+
let output;
|
|
181
|
+
// Get dimensions - use tmux dimensions for fullscreen modals
|
|
182
|
+
let dims;
|
|
183
|
+
if (this.state.fullscreenModal) {
|
|
184
|
+
const paneDims = tmux.getPaneDimensions(this.state.sidebarPaneId);
|
|
185
|
+
dims = { cols: paneDims.width, rows: paneDims.height };
|
|
186
|
+
debugLog('render: fullscreen dims', dims);
|
|
187
|
+
}
|
|
188
|
+
if (this.state.collapsed) {
|
|
189
|
+
output = renderCollapsed(this.state.sessions.length);
|
|
190
|
+
}
|
|
191
|
+
else if (this.state.modal === 'quit') {
|
|
192
|
+
output = renderQuitModal(this.state, dims);
|
|
193
|
+
}
|
|
194
|
+
else if (this.state.modal === 'delete') {
|
|
195
|
+
const target = this.state.deleteTarget;
|
|
196
|
+
const name = target?.name || '';
|
|
197
|
+
output = renderDeleteModal(this.state, name, dims);
|
|
198
|
+
}
|
|
199
|
+
else if (this.state.modal === 'new-worktree') {
|
|
200
|
+
output = renderInputModal(this.state, 'New Worktree', 'Branch name:', dims);
|
|
201
|
+
}
|
|
202
|
+
else if (this.state.modal === 'rename') {
|
|
203
|
+
const target = this.getSelectedItem();
|
|
204
|
+
const title = target?.type === 'session' ? 'Rename Session' : 'Rename Branch';
|
|
205
|
+
output = renderInputModal(this.state, title, 'New name:', dims);
|
|
206
|
+
}
|
|
207
|
+
else if (this.state.modal === 'new-session') {
|
|
208
|
+
output = renderInputModal(this.state, 'New Session', 'Session name:', dims);
|
|
209
|
+
}
|
|
210
|
+
else {
|
|
211
|
+
output = renderMain(this.state);
|
|
212
|
+
}
|
|
213
|
+
process.stdout.write(output);
|
|
214
|
+
}
|
|
215
|
+
syncCollapsedState() {
|
|
216
|
+
const width = process.stdout.columns || SIDEBAR_WIDTH;
|
|
217
|
+
this.state.collapsed = width < SIDEBAR_WIDTH / 2;
|
|
218
|
+
}
|
|
219
|
+
// ==========================================================================
|
|
220
|
+
// Input Handling
|
|
221
|
+
// ==========================================================================
|
|
222
|
+
handleInput(data) {
|
|
223
|
+
const str = data.toString();
|
|
224
|
+
debugLog('handleInput:', 'hex=' + data.toString('hex'), 'modal=' + this.state.modal);
|
|
225
|
+
// Handle terminal command mode (commands from terminal bar handler)
|
|
226
|
+
if (this.state.terminalCommandMode) {
|
|
227
|
+
if (str === '\r' || str === '\n') {
|
|
228
|
+
// Enter - execute command
|
|
229
|
+
this.executeTerminalCommand(this.state.terminalCommandBuffer);
|
|
230
|
+
this.state.terminalCommandMode = false;
|
|
231
|
+
this.state.terminalCommandBuffer = '';
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
else if (str === '\x1b') {
|
|
235
|
+
// Escape - cancel
|
|
236
|
+
this.state.terminalCommandMode = false;
|
|
237
|
+
this.state.terminalCommandBuffer = '';
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
else {
|
|
241
|
+
// Accumulate command
|
|
242
|
+
this.state.terminalCommandBuffer += str;
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
// Handle mouse events
|
|
247
|
+
if (isMouseEvent(str)) {
|
|
248
|
+
const event = parseMouseEvent(str);
|
|
249
|
+
if (event && event.button === 0 && event.release) {
|
|
250
|
+
this.handleClick(event.y, event.x);
|
|
251
|
+
}
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
const key = parseKey(data);
|
|
255
|
+
debugLog('parsedKey:', key.key, 'ctrl=' + key.ctrl);
|
|
256
|
+
// Ctrl+U - enter terminal command mode (for commands from bar handler)
|
|
257
|
+
if (key.ctrl && key.key === 'u') {
|
|
258
|
+
this.state.terminalCommandMode = true;
|
|
259
|
+
this.state.terminalCommandBuffer = '';
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
// Route input based on modal state
|
|
263
|
+
switch (this.state.modal) {
|
|
264
|
+
case 'quit':
|
|
265
|
+
this.handleQuitModalInput(key);
|
|
266
|
+
break;
|
|
267
|
+
case 'delete':
|
|
268
|
+
this.handleDeleteModalInput(key);
|
|
269
|
+
break;
|
|
270
|
+
case 'new-worktree':
|
|
271
|
+
case 'rename':
|
|
272
|
+
case 'new-session':
|
|
273
|
+
this.handleTextInput(key, data);
|
|
274
|
+
break;
|
|
275
|
+
default:
|
|
276
|
+
this.handleMainInput(key);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
handleMainInput(key) {
|
|
280
|
+
// Ctrl+C - show quit modal (when sidebar is focused)
|
|
281
|
+
if (key.ctrl && key.key === 'c') {
|
|
282
|
+
this.enterFullscreenModal();
|
|
283
|
+
this.state.modal = 'quit';
|
|
284
|
+
this.state.modalSelection = 0;
|
|
285
|
+
this.render();
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
// If collapsed, only expand on any key
|
|
289
|
+
if (this.state.collapsed) {
|
|
290
|
+
this.state.collapsed = false;
|
|
291
|
+
this.enforceSidebarWidth();
|
|
292
|
+
this.render();
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
// Ctrl+Q - show quit modal (works from any pane via tmux binding)
|
|
296
|
+
if (key.ctrl && key.key === 'q') {
|
|
297
|
+
this.enterFullscreenModal();
|
|
298
|
+
this.state.modal = 'quit';
|
|
299
|
+
this.state.modalSelection = 0;
|
|
300
|
+
this.render();
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
// Ctrl+G - toggle collapsed
|
|
304
|
+
if (key.ctrl && key.key === 'g') {
|
|
305
|
+
this.toggleCollapsed();
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
// Ctrl+T - create new terminal for active session
|
|
309
|
+
if (key.ctrl && key.key === 't') {
|
|
310
|
+
this.createTerminal();
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
// Navigation
|
|
314
|
+
if (key.key === 'up' || key.key === 'k') {
|
|
315
|
+
this.state.selectedIndex = Math.max(0, this.state.selectedIndex - 1);
|
|
316
|
+
this.render();
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
if (key.key === 'down' || key.key === 'j') {
|
|
320
|
+
const maxIndex = this.getMaxIndex();
|
|
321
|
+
this.state.selectedIndex = Math.min(maxIndex, this.state.selectedIndex + 1);
|
|
322
|
+
this.render();
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
// Enter - activate selected
|
|
326
|
+
if (key.key === 'enter') {
|
|
327
|
+
debugLog('handleMainInput: enter pressed, calling activateSelected');
|
|
328
|
+
this.activateSelected();
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
// n - new worktree
|
|
332
|
+
if (key.key === 'n') {
|
|
333
|
+
this.enterFullscreenModal();
|
|
334
|
+
this.state.modal = 'new-worktree';
|
|
335
|
+
this.state.inputBuffer = '';
|
|
336
|
+
this.render();
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
339
|
+
// d - delete
|
|
340
|
+
if (key.key === 'd') {
|
|
341
|
+
const item = this.getSelectedItem();
|
|
342
|
+
if (item && !(item.type === 'worktree' && item.worktree?.isMain)) {
|
|
343
|
+
// Store delete target info for context in modal
|
|
344
|
+
this.state.deleteTarget = {
|
|
345
|
+
type: item.type,
|
|
346
|
+
id: item.id,
|
|
347
|
+
name: item.type === 'session' ? (item.session?.title || '') : (item.worktree?.branch || ''),
|
|
348
|
+
worktree: item.worktree,
|
|
349
|
+
session: item.session,
|
|
350
|
+
};
|
|
351
|
+
this.enterFullscreenModal();
|
|
352
|
+
this.state.modal = 'delete';
|
|
353
|
+
this.state.modalSelection = 0; // Default to No
|
|
354
|
+
this.render();
|
|
355
|
+
}
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
358
|
+
// r - rename
|
|
359
|
+
if (key.key === 'r') {
|
|
360
|
+
const item = this.getSelectedItem();
|
|
361
|
+
if (item && !(item.type === 'worktree' && item.worktree?.isMain)) {
|
|
362
|
+
this.enterFullscreenModal();
|
|
363
|
+
this.state.modal = 'rename';
|
|
364
|
+
this.state.inputBuffer = item.type === 'session'
|
|
365
|
+
? item.session?.title || ''
|
|
366
|
+
: item.worktree?.branch || '';
|
|
367
|
+
this.render();
|
|
368
|
+
}
|
|
369
|
+
return;
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
handleQuitModalInput(key) {
|
|
373
|
+
// Escape - close modal
|
|
374
|
+
if (key.key === 'escape') {
|
|
375
|
+
this.state.modal = 'none';
|
|
376
|
+
this.exitFullscreenModal();
|
|
377
|
+
this.render();
|
|
378
|
+
return;
|
|
379
|
+
}
|
|
380
|
+
// Arrow keys - toggle selection
|
|
381
|
+
if (key.key === 'up' || key.key === 'down' || key.key === 'j' || key.key === 'k') {
|
|
382
|
+
this.state.modalSelection = this.state.modalSelection === 0 ? 1 : 0;
|
|
383
|
+
this.render();
|
|
384
|
+
return;
|
|
385
|
+
}
|
|
386
|
+
// Enter - confirm
|
|
387
|
+
if (key.key === 'enter') {
|
|
388
|
+
if (this.state.modalSelection === 0) {
|
|
389
|
+
// Detach
|
|
390
|
+
this.state.modal = 'none';
|
|
391
|
+
this.exitFullscreenModal();
|
|
392
|
+
this.render();
|
|
393
|
+
tmux.detachClient();
|
|
394
|
+
}
|
|
395
|
+
else {
|
|
396
|
+
// Kill - no need to restore pane since we're exiting
|
|
397
|
+
this.stop();
|
|
398
|
+
tmux.killSession(this.state.sessionName);
|
|
399
|
+
process.exit(0);
|
|
400
|
+
}
|
|
401
|
+
return;
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
handleDeleteModalInput(key) {
|
|
405
|
+
// Escape - cancel
|
|
406
|
+
if (key.key === 'escape') {
|
|
407
|
+
this.state.modal = 'none';
|
|
408
|
+
this.state.deleteTarget = null;
|
|
409
|
+
this.exitFullscreenModal();
|
|
410
|
+
this.render();
|
|
411
|
+
return;
|
|
412
|
+
}
|
|
413
|
+
// Arrow keys - toggle selection
|
|
414
|
+
if (key.key === 'up' || key.key === 'down' || key.key === 'j' || key.key === 'k') {
|
|
415
|
+
this.state.modalSelection = this.state.modalSelection === 0 ? 1 : 0;
|
|
416
|
+
this.render();
|
|
417
|
+
return;
|
|
418
|
+
}
|
|
419
|
+
// Enter - confirm
|
|
420
|
+
if (key.key === 'enter') {
|
|
421
|
+
if (this.state.modalSelection === 1) {
|
|
422
|
+
this.deleteSelected();
|
|
423
|
+
}
|
|
424
|
+
this.state.modal = 'none';
|
|
425
|
+
this.state.deleteTarget = null;
|
|
426
|
+
this.exitFullscreenModal();
|
|
427
|
+
this.render();
|
|
428
|
+
return;
|
|
429
|
+
}
|
|
430
|
+
// Quick keys
|
|
431
|
+
if (key.key === 'y' || key.key === 'Y') {
|
|
432
|
+
this.deleteSelected();
|
|
433
|
+
this.state.modal = 'none';
|
|
434
|
+
this.state.deleteTarget = null;
|
|
435
|
+
this.exitFullscreenModal();
|
|
436
|
+
this.render();
|
|
437
|
+
return;
|
|
438
|
+
}
|
|
439
|
+
if (key.key === 'n' || key.key === 'N') {
|
|
440
|
+
this.state.modal = 'none';
|
|
441
|
+
this.state.deleteTarget = null;
|
|
442
|
+
this.exitFullscreenModal();
|
|
443
|
+
this.render();
|
|
444
|
+
return;
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
handleTextInput(key, data) {
|
|
448
|
+
debugLog('handleTextInput: key=' + key.key, 'modal=' + this.state.modal, 'buffer=' + this.state.inputBuffer);
|
|
449
|
+
// Escape - cancel
|
|
450
|
+
if (key.key === 'escape') {
|
|
451
|
+
debugLog('handleTextInput: escape pressed, canceling');
|
|
452
|
+
this.state.modal = 'none';
|
|
453
|
+
this.state.inputBuffer = '';
|
|
454
|
+
this.exitFullscreenModal();
|
|
455
|
+
this.render();
|
|
456
|
+
return;
|
|
457
|
+
}
|
|
458
|
+
// Enter - confirm
|
|
459
|
+
if (key.key === 'enter') {
|
|
460
|
+
const value = this.state.inputBuffer.trim();
|
|
461
|
+
debugLog('handleTextInput: enter pressed, value=' + value);
|
|
462
|
+
const wasNewSession = this.state.modal === 'new-session';
|
|
463
|
+
if (value) {
|
|
464
|
+
debugLog('handleTextInput: calling confirmTextInput');
|
|
465
|
+
this.confirmTextInput(value);
|
|
466
|
+
}
|
|
467
|
+
this.state.modal = 'none';
|
|
468
|
+
this.state.inputBuffer = '';
|
|
469
|
+
this.exitFullscreenModal();
|
|
470
|
+
// After creating a new session, focus the Claude pane (exitFullscreenModal selects sidebar)
|
|
471
|
+
if (wasNewSession && value) {
|
|
472
|
+
const activeSession = this.state.sessions.find(s => s.id === this.state.activeSessionId);
|
|
473
|
+
if (activeSession) {
|
|
474
|
+
tmux.selectPane(activeSession.paneId);
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
this.render();
|
|
478
|
+
return;
|
|
479
|
+
}
|
|
480
|
+
// Backspace
|
|
481
|
+
if (key.key === 'backspace') {
|
|
482
|
+
this.state.inputBuffer = this.state.inputBuffer.slice(0, -1);
|
|
483
|
+
this.render();
|
|
484
|
+
return;
|
|
485
|
+
}
|
|
486
|
+
// Regular characters
|
|
487
|
+
if (data.length === 1 && data[0] >= 32 && data[0] < 127) {
|
|
488
|
+
const char = String.fromCharCode(data[0]);
|
|
489
|
+
// For branch names, only allow valid characters
|
|
490
|
+
if (this.state.modal === 'new-worktree') {
|
|
491
|
+
if (/[a-zA-Z0-9\-_\/.]/.test(char)) {
|
|
492
|
+
this.state.inputBuffer += char;
|
|
493
|
+
this.render();
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
else {
|
|
497
|
+
this.state.inputBuffer += char;
|
|
498
|
+
this.render();
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
confirmTextInput(value) {
|
|
503
|
+
switch (this.state.modal) {
|
|
504
|
+
case 'new-worktree':
|
|
505
|
+
this.createWorktree(value);
|
|
506
|
+
break;
|
|
507
|
+
case 'rename':
|
|
508
|
+
this.renameSelected(value);
|
|
509
|
+
break;
|
|
510
|
+
case 'new-session':
|
|
511
|
+
this.createSession(value);
|
|
512
|
+
break;
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
handleClick(row, col) {
|
|
516
|
+
const cols = process.stdout.columns || SIDEBAR_WIDTH;
|
|
517
|
+
if (this.state.collapsed) {
|
|
518
|
+
this.state.collapsed = false;
|
|
519
|
+
this.enforceSidebarWidth();
|
|
520
|
+
this.render();
|
|
521
|
+
return;
|
|
522
|
+
}
|
|
523
|
+
// Ignore clicks when modals are open
|
|
524
|
+
if (this.state.modal !== 'none')
|
|
525
|
+
return;
|
|
526
|
+
// Check for collapse button click (row 1, right side)
|
|
527
|
+
if (row === 1 && col >= cols - 3) {
|
|
528
|
+
this.toggleCollapsed();
|
|
529
|
+
return;
|
|
530
|
+
}
|
|
531
|
+
// Build items and find clicked item
|
|
532
|
+
const items = buildListItems(this.state);
|
|
533
|
+
const itemRow = row - 3; // Header takes 2 rows
|
|
534
|
+
// Check for "New Worktree" button click (after items + 1 empty row)
|
|
535
|
+
const newWorktreeRow = items.length + 1; // +1 for empty row after items
|
|
536
|
+
if (itemRow === newWorktreeRow) {
|
|
537
|
+
this.enterFullscreenModal();
|
|
538
|
+
this.state.modal = 'new-worktree';
|
|
539
|
+
this.state.inputBuffer = '';
|
|
540
|
+
this.render();
|
|
541
|
+
return;
|
|
542
|
+
}
|
|
543
|
+
if (itemRow >= 0 && itemRow < items.length) {
|
|
544
|
+
this.state.selectedIndex = itemRow;
|
|
545
|
+
this.activateSelected();
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
// ==========================================================================
|
|
549
|
+
// Actions
|
|
550
|
+
// ==========================================================================
|
|
551
|
+
activateSelected() {
|
|
552
|
+
debugLog('activateSelected: selectedIndex=' + this.state.selectedIndex);
|
|
553
|
+
const item = this.getSelectedItem();
|
|
554
|
+
debugLog('activateSelected: item=', item);
|
|
555
|
+
if (!item) {
|
|
556
|
+
debugLog('activateSelected: no item found');
|
|
557
|
+
return;
|
|
558
|
+
}
|
|
559
|
+
if (item.type === 'worktree') {
|
|
560
|
+
// Show new session modal
|
|
561
|
+
debugLog('activateSelected: showing new-session modal for worktree', item.worktree?.branch);
|
|
562
|
+
this.enterFullscreenModal();
|
|
563
|
+
this.state.modal = 'new-session';
|
|
564
|
+
const sessions = this.state.sessions.filter(s => s.worktreeId === item.id);
|
|
565
|
+
this.state.inputBuffer = `${sessions.length + 1}: ${item.worktree?.branch || 'session'}`;
|
|
566
|
+
this.render();
|
|
567
|
+
}
|
|
568
|
+
else {
|
|
569
|
+
// Switch to session
|
|
570
|
+
debugLog('activateSelected: switching to session', item.session?.title);
|
|
571
|
+
this.switchToSession(item.session);
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
async createWorktree(branchName) {
|
|
575
|
+
try {
|
|
576
|
+
const worktree = await this.worktreeManager.create(branchName, true);
|
|
577
|
+
this.state.worktrees.push(worktree);
|
|
578
|
+
this.state.selectedIndex = this.getMaxIndex();
|
|
579
|
+
this.render();
|
|
580
|
+
}
|
|
581
|
+
catch (err) {
|
|
582
|
+
// Failed to create worktree
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
createSession(title) {
|
|
586
|
+
debugLog('createSession: title=' + title);
|
|
587
|
+
const item = this.getSelectedItem();
|
|
588
|
+
debugLog('createSession: selectedItem=', item);
|
|
589
|
+
if (!item || item.type !== 'worktree') {
|
|
590
|
+
debugLog('createSession: FAILED - item is not a worktree or is null');
|
|
591
|
+
return;
|
|
592
|
+
}
|
|
593
|
+
const worktree = item.worktree;
|
|
594
|
+
debugLog('createSession: worktree=' + worktree.branch, 'path=' + worktree.path);
|
|
595
|
+
const sessionId = `session-${Date.now()}`;
|
|
596
|
+
let paneId;
|
|
597
|
+
const claudeCmd = DEFAULT_CLAUDE_CMD;
|
|
598
|
+
if (this.state.sessions.length === 0) {
|
|
599
|
+
// First session - use existing main pane
|
|
600
|
+
// If in fullscreen mode, the pane is broken but we can still send keys to it
|
|
601
|
+
// exitFullscreenModal will join it back
|
|
602
|
+
paneId = this.state.mainPaneId;
|
|
603
|
+
tmux.sendControlKey(paneId, 'C-c');
|
|
604
|
+
tmux.sendKeys(paneId, `cd "${worktree.path}" && clear && ${claudeCmd}`, true);
|
|
605
|
+
}
|
|
606
|
+
else {
|
|
607
|
+
// Additional session - need to handle fullscreen mode specially
|
|
608
|
+
const currentSession = this.state.sessions.find(s => s.id === this.state.activeSessionId);
|
|
609
|
+
if (this.state.fullscreenModal) {
|
|
610
|
+
// In fullscreen mode, the current session pane is already broken
|
|
611
|
+
// We'll create a new pane and DON'T want to rejoin the old one
|
|
612
|
+
// Clear hiddenPaneId so exitFullscreenModal won't join it back
|
|
613
|
+
// The old session stays hidden and can be switched to later
|
|
614
|
+
debugLog('createSession: in fullscreen mode, clearing hiddenPaneId to keep old session hidden');
|
|
615
|
+
this.state.hiddenPaneId = null;
|
|
616
|
+
}
|
|
617
|
+
else if (currentSession) {
|
|
618
|
+
// Normal mode - break the current session pane
|
|
619
|
+
tmux.breakPane(currentSession.paneId);
|
|
620
|
+
}
|
|
621
|
+
paneId = tmux.splitHorizontal(this.state.sessionName, 80, worktree.path);
|
|
622
|
+
tmux.sendKeys(paneId, claudeCmd, true);
|
|
623
|
+
}
|
|
624
|
+
const session = {
|
|
625
|
+
id: sessionId,
|
|
626
|
+
worktreeId: worktree.id,
|
|
627
|
+
paneId,
|
|
628
|
+
title,
|
|
629
|
+
createdAt: Date.now(),
|
|
630
|
+
// Terminal management
|
|
631
|
+
terminals: [],
|
|
632
|
+
activeTerminalIndex: 0,
|
|
633
|
+
terminalBarPaneId: null,
|
|
634
|
+
};
|
|
635
|
+
this.state.sessions.push(session);
|
|
636
|
+
this.state.activeSessionId = sessionId;
|
|
637
|
+
this.enforceSidebarWidth();
|
|
638
|
+
// Focus the Claude pane so user can start interacting immediately
|
|
639
|
+
tmux.selectPane(paneId);
|
|
640
|
+
this.render();
|
|
641
|
+
}
|
|
642
|
+
switchToSession(session) {
|
|
643
|
+
if (session.id === this.state.activeSessionId) {
|
|
644
|
+
// Already active - focus pane
|
|
645
|
+
tmux.selectPane(session.paneId);
|
|
646
|
+
return;
|
|
647
|
+
}
|
|
648
|
+
// Break current session's panes (Claude pane + terminals)
|
|
649
|
+
const currentSession = this.state.sessions.find(s => s.id === this.state.activeSessionId);
|
|
650
|
+
if (currentSession) {
|
|
651
|
+
// Break active terminal first (if any)
|
|
652
|
+
if (currentSession.terminals.length > 0) {
|
|
653
|
+
const activeTerminal = currentSession.terminals[currentSession.activeTerminalIndex];
|
|
654
|
+
if (activeTerminal) {
|
|
655
|
+
tmux.breakPane(activeTerminal.paneId);
|
|
656
|
+
}
|
|
657
|
+
// Break terminal bar
|
|
658
|
+
if (currentSession.terminalBarPaneId) {
|
|
659
|
+
tmux.breakPane(currentSession.terminalBarPaneId);
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
// Break Claude pane
|
|
663
|
+
tmux.breakPane(currentSession.paneId);
|
|
664
|
+
}
|
|
665
|
+
// Join new session's Claude pane
|
|
666
|
+
tmux.joinPane(session.paneId, this.state.sidebarPaneId, true);
|
|
667
|
+
// Join terminal bar and active terminal if session has terminals
|
|
668
|
+
if (session.terminals.length > 0 && session.terminalBarPaneId) {
|
|
669
|
+
// Join terminal bar below Claude pane
|
|
670
|
+
tmux.joinPane(session.terminalBarPaneId, session.paneId, false);
|
|
671
|
+
// Join active terminal below terminal bar
|
|
672
|
+
const activeTerminal = session.terminals[session.activeTerminalIndex];
|
|
673
|
+
if (activeTerminal) {
|
|
674
|
+
tmux.joinPane(activeTerminal.paneId, session.terminalBarPaneId, false);
|
|
675
|
+
}
|
|
676
|
+
// Ensure terminal bar is exactly 1 row
|
|
677
|
+
tmux.resizePane(session.terminalBarPaneId, undefined, TERMINAL_BAR_HEIGHT);
|
|
678
|
+
// Update resize enforcement for this session's terminal bar
|
|
679
|
+
if (activeTerminal) {
|
|
680
|
+
setupTerminalBarResize(this.state.sessionName, session.paneId, session.terminalBarPaneId, activeTerminal.paneId);
|
|
681
|
+
}
|
|
682
|
+
// Update terminal bar display
|
|
683
|
+
this.updateTerminalBar(session);
|
|
684
|
+
}
|
|
685
|
+
this.state.activeSessionId = session.id;
|
|
686
|
+
this.enforceSidebarWidth();
|
|
687
|
+
tmux.selectPane(this.state.sidebarPaneId);
|
|
688
|
+
this.render();
|
|
689
|
+
}
|
|
690
|
+
async deleteSelected() {
|
|
691
|
+
const item = this.getSelectedItem();
|
|
692
|
+
if (!item)
|
|
693
|
+
return;
|
|
694
|
+
if (item.type === 'session') {
|
|
695
|
+
this.deleteSession(item.session);
|
|
696
|
+
}
|
|
697
|
+
else if (item.type === 'worktree' && !item.worktree?.isMain) {
|
|
698
|
+
await this.deleteWorktree(item.worktree);
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
deleteSession(session) {
|
|
702
|
+
// Kill all terminal panes
|
|
703
|
+
for (const terminal of session.terminals) {
|
|
704
|
+
tmux.killPane(terminal.paneId);
|
|
705
|
+
}
|
|
706
|
+
// Kill terminal bar pane and remove hook
|
|
707
|
+
if (session.terminalBarPaneId) {
|
|
708
|
+
try {
|
|
709
|
+
tmux.removeHook(this.state.sessionName, 'after-resize-pane');
|
|
710
|
+
}
|
|
711
|
+
catch {
|
|
712
|
+
// Hook may not exist, ignore
|
|
713
|
+
}
|
|
714
|
+
tmux.killPane(session.terminalBarPaneId);
|
|
715
|
+
}
|
|
716
|
+
// Kill the Claude pane
|
|
717
|
+
tmux.killPane(session.paneId);
|
|
718
|
+
// Remove from sessions
|
|
719
|
+
this.state.sessions = this.state.sessions.filter(s => s.id !== session.id);
|
|
720
|
+
// If this was active, switch to another
|
|
721
|
+
if (this.state.activeSessionId === session.id) {
|
|
722
|
+
this.state.activeSessionId = null;
|
|
723
|
+
if (this.state.sessions.length > 0) {
|
|
724
|
+
const nextSession = this.state.sessions[0];
|
|
725
|
+
tmux.joinPane(nextSession.paneId, this.state.sidebarPaneId, true);
|
|
726
|
+
this.state.activeSessionId = nextSession.id;
|
|
727
|
+
// Join terminal panes if next session has terminals
|
|
728
|
+
if (nextSession.terminals.length > 0 && nextSession.terminalBarPaneId) {
|
|
729
|
+
tmux.joinPane(nextSession.terminalBarPaneId, nextSession.paneId, false);
|
|
730
|
+
const activeTerminal = nextSession.terminals[nextSession.activeTerminalIndex];
|
|
731
|
+
if (activeTerminal) {
|
|
732
|
+
tmux.joinPane(activeTerminal.paneId, nextSession.terminalBarPaneId, false);
|
|
733
|
+
}
|
|
734
|
+
tmux.resizePane(nextSession.terminalBarPaneId, undefined, TERMINAL_BAR_HEIGHT);
|
|
735
|
+
// Update resize enforcement for the new session's terminal bar
|
|
736
|
+
if (activeTerminal) {
|
|
737
|
+
setupTerminalBarResize(this.state.sessionName, nextSession.paneId, nextSession.terminalBarPaneId, activeTerminal.paneId);
|
|
738
|
+
}
|
|
739
|
+
this.updateTerminalBar(nextSession);
|
|
740
|
+
}
|
|
741
|
+
this.enforceSidebarWidth();
|
|
742
|
+
}
|
|
743
|
+
else {
|
|
744
|
+
// No more sessions - create empty pane
|
|
745
|
+
const newPaneId = tmux.splitHorizontal(this.state.sessionName, 80, this.state.repoPath);
|
|
746
|
+
this.state.mainPaneId = newPaneId;
|
|
747
|
+
tmux.sendKeys(newPaneId, 'echo "Press Enter in sidebar to start a session"', true);
|
|
748
|
+
this.enforceSidebarWidth();
|
|
749
|
+
}
|
|
750
|
+
tmux.selectPane(this.state.sidebarPaneId);
|
|
751
|
+
}
|
|
752
|
+
// Adjust selection
|
|
753
|
+
this.state.selectedIndex = Math.max(0, this.state.selectedIndex - 1);
|
|
754
|
+
this.render();
|
|
755
|
+
}
|
|
756
|
+
async deleteWorktree(worktree) {
|
|
757
|
+
// Delete all sessions for this worktree (including their terminals)
|
|
758
|
+
const sessionsToDelete = this.state.sessions.filter(s => s.worktreeId === worktree.id);
|
|
759
|
+
for (const session of sessionsToDelete) {
|
|
760
|
+
// Kill all terminal panes
|
|
761
|
+
for (const terminal of session.terminals) {
|
|
762
|
+
tmux.killPane(terminal.paneId);
|
|
763
|
+
}
|
|
764
|
+
// Kill terminal bar pane and remove hook
|
|
765
|
+
if (session.terminalBarPaneId) {
|
|
766
|
+
try {
|
|
767
|
+
tmux.removeHook(this.state.sessionName, 'after-resize-pane');
|
|
768
|
+
}
|
|
769
|
+
catch {
|
|
770
|
+
// Hook may not exist, ignore
|
|
771
|
+
}
|
|
772
|
+
tmux.killPane(session.terminalBarPaneId);
|
|
773
|
+
}
|
|
774
|
+
// Kill Claude pane
|
|
775
|
+
tmux.killPane(session.paneId);
|
|
776
|
+
}
|
|
777
|
+
this.state.sessions = this.state.sessions.filter(s => s.worktreeId !== worktree.id);
|
|
778
|
+
// If active session was deleted, switch to another
|
|
779
|
+
if (sessionsToDelete.some(s => s.id === this.state.activeSessionId)) {
|
|
780
|
+
this.state.activeSessionId = null;
|
|
781
|
+
if (this.state.sessions.length > 0) {
|
|
782
|
+
const nextSession = this.state.sessions[0];
|
|
783
|
+
tmux.joinPane(nextSession.paneId, this.state.sidebarPaneId, true);
|
|
784
|
+
this.state.activeSessionId = nextSession.id;
|
|
785
|
+
// Join terminal panes if next session has terminals
|
|
786
|
+
if (nextSession.terminals.length > 0 && nextSession.terminalBarPaneId) {
|
|
787
|
+
tmux.joinPane(nextSession.terminalBarPaneId, nextSession.paneId, false);
|
|
788
|
+
const activeTerminal = nextSession.terminals[nextSession.activeTerminalIndex];
|
|
789
|
+
if (activeTerminal) {
|
|
790
|
+
tmux.joinPane(activeTerminal.paneId, nextSession.terminalBarPaneId, false);
|
|
791
|
+
}
|
|
792
|
+
tmux.resizePane(nextSession.terminalBarPaneId, undefined, TERMINAL_BAR_HEIGHT);
|
|
793
|
+
// Update resize enforcement for the new session's terminal bar
|
|
794
|
+
if (activeTerminal) {
|
|
795
|
+
setupTerminalBarResize(this.state.sessionName, nextSession.paneId, nextSession.terminalBarPaneId, activeTerminal.paneId);
|
|
796
|
+
}
|
|
797
|
+
this.updateTerminalBar(nextSession);
|
|
798
|
+
}
|
|
799
|
+
this.enforceSidebarWidth();
|
|
800
|
+
}
|
|
801
|
+
tmux.selectPane(this.state.sidebarPaneId);
|
|
802
|
+
}
|
|
803
|
+
// Remove worktree via git
|
|
804
|
+
try {
|
|
805
|
+
await this.worktreeManager.remove(worktree.path, true);
|
|
806
|
+
}
|
|
807
|
+
catch {
|
|
808
|
+
// Ignore errors
|
|
809
|
+
}
|
|
810
|
+
// Remove from state
|
|
811
|
+
this.state.worktrees = this.state.worktrees.filter(w => w.id !== worktree.id);
|
|
812
|
+
// Adjust selection
|
|
813
|
+
const totalItems = this.getTotalItemCount();
|
|
814
|
+
if (this.state.selectedIndex >= totalItems) {
|
|
815
|
+
this.state.selectedIndex = Math.max(0, totalItems - 1);
|
|
816
|
+
}
|
|
817
|
+
this.render();
|
|
818
|
+
}
|
|
819
|
+
renameSelected(newName) {
|
|
820
|
+
const item = this.getSelectedItem();
|
|
821
|
+
if (!item)
|
|
822
|
+
return;
|
|
823
|
+
if (item.type === 'session' && item.session) {
|
|
824
|
+
item.session.title = newName;
|
|
825
|
+
}
|
|
826
|
+
// Worktree rename would require git branch rename - skip for now
|
|
827
|
+
}
|
|
828
|
+
toggleCollapsed() {
|
|
829
|
+
this.state.collapsed = !this.state.collapsed;
|
|
830
|
+
if (this.state.collapsed) {
|
|
831
|
+
tmux.resizePane(this.state.sidebarPaneId, 2);
|
|
832
|
+
}
|
|
833
|
+
else {
|
|
834
|
+
this.enforceSidebarWidth();
|
|
835
|
+
}
|
|
836
|
+
this.render();
|
|
837
|
+
}
|
|
838
|
+
enforceSidebarWidth() {
|
|
839
|
+
if (!this.state.collapsed) {
|
|
840
|
+
tmux.resizePane(this.state.sidebarPaneId, SIDEBAR_WIDTH);
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
// ==========================================================================
|
|
844
|
+
// Terminal Management
|
|
845
|
+
// ==========================================================================
|
|
846
|
+
/**
|
|
847
|
+
* Execute a terminal command received from the bar handler
|
|
848
|
+
* Format: "TERM:<action>:<data>"
|
|
849
|
+
*/
|
|
850
|
+
executeTerminalCommand(command) {
|
|
851
|
+
debugLog('executeTerminalCommand:', command);
|
|
852
|
+
if (!command.startsWith('TERM:'))
|
|
853
|
+
return;
|
|
854
|
+
const parts = command.slice(5).split(':');
|
|
855
|
+
const action = parts[0];
|
|
856
|
+
const data = parts[1] || '';
|
|
857
|
+
const session = this.state.sessions.find(s => s.id === this.state.activeSessionId);
|
|
858
|
+
if (!session && action !== 'escape')
|
|
859
|
+
return;
|
|
860
|
+
switch (action) {
|
|
861
|
+
case 'switch':
|
|
862
|
+
const index = parseInt(data, 10);
|
|
863
|
+
if (!isNaN(index) && session) {
|
|
864
|
+
this.switchTerminal(session, index);
|
|
865
|
+
}
|
|
866
|
+
break;
|
|
867
|
+
case 'new':
|
|
868
|
+
this.createTerminal();
|
|
869
|
+
break;
|
|
870
|
+
case 'delete':
|
|
871
|
+
const delIndex = parseInt(data, 10);
|
|
872
|
+
if (!isNaN(delIndex) && session) {
|
|
873
|
+
this.deleteTerminal(session, delIndex);
|
|
874
|
+
}
|
|
875
|
+
break;
|
|
876
|
+
case 'focus':
|
|
877
|
+
if (session && session.terminals.length > 0) {
|
|
878
|
+
const activeTerminal = session.terminals[session.activeTerminalIndex];
|
|
879
|
+
if (activeTerminal) {
|
|
880
|
+
tmux.selectPane(activeTerminal.paneId);
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
break;
|
|
884
|
+
case 'escape':
|
|
885
|
+
tmux.selectPane(this.state.sidebarPaneId);
|
|
886
|
+
break;
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
/**
|
|
890
|
+
* Create a new terminal for the active session
|
|
891
|
+
*/
|
|
892
|
+
createTerminal() {
|
|
893
|
+
if (!this.state.activeSessionId) {
|
|
894
|
+
debugLog('createTerminal: no active session');
|
|
895
|
+
return;
|
|
896
|
+
}
|
|
897
|
+
const session = this.state.sessions.find(s => s.id === this.state.activeSessionId);
|
|
898
|
+
if (!session)
|
|
899
|
+
return;
|
|
900
|
+
const worktree = this.state.worktrees.find(w => w.id === session.worktreeId);
|
|
901
|
+
if (!worktree)
|
|
902
|
+
return;
|
|
903
|
+
const terminalNum = session.terminals.length + 1;
|
|
904
|
+
const terminalTitle = `Terminal ${terminalNum}`;
|
|
905
|
+
const terminalId = `terminal-${Date.now()}`;
|
|
906
|
+
debugLog('createTerminal:', terminalTitle, 'for session', session.title);
|
|
907
|
+
if (session.terminals.length === 0) {
|
|
908
|
+
// First terminal - need to create terminal bar pane + terminal pane
|
|
909
|
+
// Split Claude pane vertically (Claude gets top 70%, terminal area gets bottom 30%)
|
|
910
|
+
tmux.selectPane(session.paneId);
|
|
911
|
+
const terminalAreaPaneId = tmux.splitVertical(this.state.sessionName, 100 - CLAUDE_PANE_PERCENT, worktree.path);
|
|
912
|
+
// Split terminal area: top 1 row for bar, rest for terminal
|
|
913
|
+
tmux.selectPane(terminalAreaPaneId);
|
|
914
|
+
const terminalPaneId = tmux.splitVertical(this.state.sessionName, 95, worktree.path);
|
|
915
|
+
// The terminalAreaPaneId is now the bar pane (top part after split)
|
|
916
|
+
const terminalBarPaneId = terminalAreaPaneId;
|
|
917
|
+
// Resize bar pane to exactly 1 row
|
|
918
|
+
tmux.resizePane(terminalBarPaneId, undefined, TERMINAL_BAR_HEIGHT);
|
|
919
|
+
// Set up resize enforcement (hook + mouse binding)
|
|
920
|
+
setupTerminalBarResize(this.state.sessionName, session.paneId, terminalBarPaneId, terminalPaneId);
|
|
921
|
+
// Update session
|
|
922
|
+
session.terminalBarPaneId = terminalBarPaneId;
|
|
923
|
+
session.terminals.push({
|
|
924
|
+
id: terminalId,
|
|
925
|
+
sessionId: session.id,
|
|
926
|
+
paneId: terminalPaneId,
|
|
927
|
+
title: terminalTitle,
|
|
928
|
+
createdAt: Date.now(),
|
|
929
|
+
});
|
|
930
|
+
session.activeTerminalIndex = 0;
|
|
931
|
+
// Start terminal bar handler in the bar pane
|
|
932
|
+
this.startTerminalBarHandler(session);
|
|
933
|
+
// Focus the terminal pane
|
|
934
|
+
tmux.selectPane(terminalPaneId);
|
|
935
|
+
}
|
|
936
|
+
else {
|
|
937
|
+
// Additional terminal - split current terminal, break old, show new
|
|
938
|
+
const currentTerminal = session.terminals[session.activeTerminalIndex];
|
|
939
|
+
// Split from current terminal to create new one
|
|
940
|
+
tmux.selectPane(currentTerminal.paneId);
|
|
941
|
+
const newTerminalPaneId = tmux.splitVertical(this.state.sessionName, 50, worktree.path);
|
|
942
|
+
// Break the current terminal to background
|
|
943
|
+
tmux.breakPane(currentTerminal.paneId);
|
|
944
|
+
// Add new terminal
|
|
945
|
+
session.terminals.push({
|
|
946
|
+
id: terminalId,
|
|
947
|
+
sessionId: session.id,
|
|
948
|
+
paneId: newTerminalPaneId,
|
|
949
|
+
title: terminalTitle,
|
|
950
|
+
createdAt: Date.now(),
|
|
951
|
+
});
|
|
952
|
+
session.activeTerminalIndex = session.terminals.length - 1;
|
|
953
|
+
// Ensure terminal bar stays at 1 row after the split
|
|
954
|
+
if (session.terminalBarPaneId) {
|
|
955
|
+
tmux.resizePane(session.terminalBarPaneId, undefined, TERMINAL_BAR_HEIGHT);
|
|
956
|
+
}
|
|
957
|
+
// Update terminal bar
|
|
958
|
+
this.updateTerminalBar(session);
|
|
959
|
+
// Focus the new terminal
|
|
960
|
+
tmux.selectPane(newTerminalPaneId);
|
|
961
|
+
}
|
|
962
|
+
// Ensure sidebar stays at fixed width
|
|
963
|
+
this.enforceSidebarWidth();
|
|
964
|
+
this.render();
|
|
965
|
+
}
|
|
966
|
+
/**
|
|
967
|
+
* Switch to a different terminal tab within a session
|
|
968
|
+
*/
|
|
969
|
+
switchTerminal(session, targetIndex) {
|
|
970
|
+
if (targetIndex < 0 || targetIndex >= session.terminals.length)
|
|
971
|
+
return;
|
|
972
|
+
if (targetIndex === session.activeTerminalIndex)
|
|
973
|
+
return;
|
|
974
|
+
debugLog('switchTerminal:', targetIndex, 'in session', session.title);
|
|
975
|
+
const currentTerminal = session.terminals[session.activeTerminalIndex];
|
|
976
|
+
const newTerminal = session.terminals[targetIndex];
|
|
977
|
+
// Break current terminal to background
|
|
978
|
+
tmux.breakPane(currentTerminal.paneId);
|
|
979
|
+
// Join new terminal below the bar pane
|
|
980
|
+
if (session.terminalBarPaneId) {
|
|
981
|
+
tmux.joinPane(newTerminal.paneId, session.terminalBarPaneId, false);
|
|
982
|
+
// Ensure terminal bar stays at 1 row
|
|
983
|
+
tmux.resizePane(session.terminalBarPaneId, undefined, TERMINAL_BAR_HEIGHT);
|
|
984
|
+
}
|
|
985
|
+
// Update active index
|
|
986
|
+
session.activeTerminalIndex = targetIndex;
|
|
987
|
+
// Update terminal bar
|
|
988
|
+
this.updateTerminalBar(session);
|
|
989
|
+
this.enforceSidebarWidth();
|
|
990
|
+
}
|
|
991
|
+
/**
|
|
992
|
+
* Delete a terminal at the given index
|
|
993
|
+
*/
|
|
994
|
+
deleteTerminal(session, index) {
|
|
995
|
+
if (index < 0 || index >= session.terminals.length)
|
|
996
|
+
return;
|
|
997
|
+
debugLog('deleteTerminal:', index, 'in session', session.title);
|
|
998
|
+
const terminal = session.terminals[index];
|
|
999
|
+
const wasActive = index === session.activeTerminalIndex;
|
|
1000
|
+
// Kill the terminal pane
|
|
1001
|
+
tmux.killPane(terminal.paneId);
|
|
1002
|
+
// Remove from terminals array
|
|
1003
|
+
session.terminals.splice(index, 1);
|
|
1004
|
+
if (session.terminals.length === 0) {
|
|
1005
|
+
// No more terminals - kill terminal bar pane and remove resize hook
|
|
1006
|
+
if (session.terminalBarPaneId) {
|
|
1007
|
+
// Remove the after-resize-pane hook for this session
|
|
1008
|
+
try {
|
|
1009
|
+
tmux.removeHook(this.state.sessionName, 'after-resize-pane');
|
|
1010
|
+
}
|
|
1011
|
+
catch {
|
|
1012
|
+
// Hook may not exist, ignore
|
|
1013
|
+
}
|
|
1014
|
+
tmux.killPane(session.terminalBarPaneId);
|
|
1015
|
+
session.terminalBarPaneId = null;
|
|
1016
|
+
}
|
|
1017
|
+
session.activeTerminalIndex = 0;
|
|
1018
|
+
}
|
|
1019
|
+
else {
|
|
1020
|
+
// Adjust activeTerminalIndex
|
|
1021
|
+
if (index < session.activeTerminalIndex) {
|
|
1022
|
+
session.activeTerminalIndex--;
|
|
1023
|
+
}
|
|
1024
|
+
else if (session.activeTerminalIndex >= session.terminals.length) {
|
|
1025
|
+
session.activeTerminalIndex = session.terminals.length - 1;
|
|
1026
|
+
}
|
|
1027
|
+
// If deleted was the visible terminal, show the new active one
|
|
1028
|
+
if (wasActive) {
|
|
1029
|
+
const newActiveTerminal = session.terminals[session.activeTerminalIndex];
|
|
1030
|
+
if (newActiveTerminal && session.terminalBarPaneId) {
|
|
1031
|
+
tmux.joinPane(newActiveTerminal.paneId, session.terminalBarPaneId, false);
|
|
1032
|
+
tmux.resizePane(session.terminalBarPaneId, undefined, TERMINAL_BAR_HEIGHT);
|
|
1033
|
+
}
|
|
1034
|
+
}
|
|
1035
|
+
// Update terminal bar
|
|
1036
|
+
this.updateTerminalBar(session);
|
|
1037
|
+
}
|
|
1038
|
+
this.enforceSidebarWidth();
|
|
1039
|
+
this.render();
|
|
1040
|
+
}
|
|
1041
|
+
/**
|
|
1042
|
+
* Start the terminal bar handler in a session's bar pane
|
|
1043
|
+
*/
|
|
1044
|
+
startTerminalBarHandler(session) {
|
|
1045
|
+
if (!session.terminalBarPaneId)
|
|
1046
|
+
return;
|
|
1047
|
+
// Build the command to run the bar handler
|
|
1048
|
+
// We pass the initial state as a JSON argument
|
|
1049
|
+
const initialState = JSON.stringify({
|
|
1050
|
+
terminals: session.terminals,
|
|
1051
|
+
activeIndex: session.activeTerminalIndex,
|
|
1052
|
+
});
|
|
1053
|
+
// Get path to bar-handler - check for .ts first (dev mode), then .js (compiled)
|
|
1054
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
1055
|
+
const __dirname = dirname(__filename);
|
|
1056
|
+
// Check both src and dist locations
|
|
1057
|
+
const tsPath = resolve(__dirname, '../terminal/bar-handler.ts');
|
|
1058
|
+
const jsPath = resolve(__dirname, '../terminal/bar-handler.js');
|
|
1059
|
+
let cmd;
|
|
1060
|
+
const escapedState = initialState.replace(/'/g, "'\\''");
|
|
1061
|
+
const args = `"${this.state.sidebarPaneId}" "${session.id}" '${escapedState}'`;
|
|
1062
|
+
// Check which file exists
|
|
1063
|
+
if (existsSync(tsPath)) {
|
|
1064
|
+
cmd = `npx tsx "${tsPath}" ${args}`;
|
|
1065
|
+
}
|
|
1066
|
+
else if (existsSync(jsPath)) {
|
|
1067
|
+
cmd = `node "${jsPath}" ${args}`;
|
|
1068
|
+
}
|
|
1069
|
+
else {
|
|
1070
|
+
debugLog('startTerminalBarHandler: bar-handler not found at', tsPath, 'or', jsPath);
|
|
1071
|
+
return;
|
|
1072
|
+
}
|
|
1073
|
+
debugLog('startTerminalBarHandler:', cmd);
|
|
1074
|
+
tmux.sendKeys(session.terminalBarPaneId, cmd, true);
|
|
1075
|
+
}
|
|
1076
|
+
/**
|
|
1077
|
+
* Send updated state to the terminal bar handler
|
|
1078
|
+
*/
|
|
1079
|
+
updateTerminalBar(session) {
|
|
1080
|
+
if (!session.terminalBarPaneId)
|
|
1081
|
+
return;
|
|
1082
|
+
const renderData = JSON.stringify({
|
|
1083
|
+
terminals: session.terminals,
|
|
1084
|
+
activeIndex: session.activeTerminalIndex,
|
|
1085
|
+
});
|
|
1086
|
+
// Send render command to bar handler
|
|
1087
|
+
tmux.sendKeys(session.terminalBarPaneId, `RENDER:${renderData}`, false);
|
|
1088
|
+
}
|
|
1089
|
+
// ==========================================================================
|
|
1090
|
+
// Fullscreen Modal Management
|
|
1091
|
+
// ==========================================================================
|
|
1092
|
+
/**
|
|
1093
|
+
* Enter fullscreen modal mode - hides all session panes so sidebar can expand
|
|
1094
|
+
*/
|
|
1095
|
+
enterFullscreenModal() {
|
|
1096
|
+
if (this.state.fullscreenModal)
|
|
1097
|
+
return; // Already in fullscreen
|
|
1098
|
+
debugLog('enterFullscreenModal: hiding panes');
|
|
1099
|
+
if (this.state.activeSessionId) {
|
|
1100
|
+
// Hide the active session's panes (Claude pane + terminals)
|
|
1101
|
+
const activeSession = this.state.sessions.find(s => s.id === this.state.activeSessionId);
|
|
1102
|
+
if (activeSession) {
|
|
1103
|
+
try {
|
|
1104
|
+
// Break active terminal first (if any)
|
|
1105
|
+
if (activeSession.terminals.length > 0) {
|
|
1106
|
+
const activeTerminal = activeSession.terminals[activeSession.activeTerminalIndex];
|
|
1107
|
+
if (activeTerminal) {
|
|
1108
|
+
tmux.breakPane(activeTerminal.paneId);
|
|
1109
|
+
}
|
|
1110
|
+
// Break terminal bar
|
|
1111
|
+
if (activeSession.terminalBarPaneId) {
|
|
1112
|
+
tmux.breakPane(activeSession.terminalBarPaneId);
|
|
1113
|
+
}
|
|
1114
|
+
}
|
|
1115
|
+
// Break Claude pane
|
|
1116
|
+
tmux.breakPane(activeSession.paneId);
|
|
1117
|
+
this.state.hiddenPaneId = activeSession.paneId;
|
|
1118
|
+
debugLog('enterFullscreenModal: broke session panes');
|
|
1119
|
+
}
|
|
1120
|
+
catch (err) {
|
|
1121
|
+
debugLog('enterFullscreenModal: failed to break panes', err);
|
|
1122
|
+
}
|
|
1123
|
+
}
|
|
1124
|
+
}
|
|
1125
|
+
else {
|
|
1126
|
+
// Hide the main/welcome pane
|
|
1127
|
+
try {
|
|
1128
|
+
tmux.breakPane(this.state.mainPaneId);
|
|
1129
|
+
this.state.hiddenPaneId = this.state.mainPaneId;
|
|
1130
|
+
debugLog('enterFullscreenModal: broke main pane');
|
|
1131
|
+
}
|
|
1132
|
+
catch (err) {
|
|
1133
|
+
debugLog('enterFullscreenModal: failed to break main pane', err);
|
|
1134
|
+
}
|
|
1135
|
+
}
|
|
1136
|
+
this.state.fullscreenModal = true;
|
|
1137
|
+
}
|
|
1138
|
+
/**
|
|
1139
|
+
* Exit fullscreen modal mode - restores all session panes
|
|
1140
|
+
*/
|
|
1141
|
+
exitFullscreenModal() {
|
|
1142
|
+
if (!this.state.fullscreenModal)
|
|
1143
|
+
return; // Not in fullscreen
|
|
1144
|
+
debugLog('exitFullscreenModal: restoring panes');
|
|
1145
|
+
if (this.state.hiddenPaneId) {
|
|
1146
|
+
try {
|
|
1147
|
+
// Join Claude pane
|
|
1148
|
+
tmux.joinPane(this.state.hiddenPaneId, this.state.sidebarPaneId, true);
|
|
1149
|
+
debugLog('exitFullscreenModal: joined Claude pane');
|
|
1150
|
+
// If active session has terminals, join those too
|
|
1151
|
+
const activeSession = this.state.sessions.find(s => s.id === this.state.activeSessionId);
|
|
1152
|
+
if (activeSession && activeSession.terminals.length > 0 && activeSession.terminalBarPaneId) {
|
|
1153
|
+
// Join terminal bar below Claude pane
|
|
1154
|
+
tmux.joinPane(activeSession.terminalBarPaneId, activeSession.paneId, false);
|
|
1155
|
+
// Join active terminal below terminal bar
|
|
1156
|
+
const activeTerminal = activeSession.terminals[activeSession.activeTerminalIndex];
|
|
1157
|
+
if (activeTerminal) {
|
|
1158
|
+
tmux.joinPane(activeTerminal.paneId, activeSession.terminalBarPaneId, false);
|
|
1159
|
+
}
|
|
1160
|
+
// Ensure terminal bar is exactly 1 row
|
|
1161
|
+
tmux.resizePane(activeSession.terminalBarPaneId, undefined, TERMINAL_BAR_HEIGHT);
|
|
1162
|
+
// Update terminal bar display
|
|
1163
|
+
this.updateTerminalBar(activeSession);
|
|
1164
|
+
debugLog('exitFullscreenModal: joined terminal panes');
|
|
1165
|
+
}
|
|
1166
|
+
this.enforceSidebarWidth();
|
|
1167
|
+
}
|
|
1168
|
+
catch (err) {
|
|
1169
|
+
debugLog('exitFullscreenModal: failed to join panes', err);
|
|
1170
|
+
}
|
|
1171
|
+
this.state.hiddenPaneId = null;
|
|
1172
|
+
}
|
|
1173
|
+
this.state.fullscreenModal = false;
|
|
1174
|
+
tmux.selectPane(this.state.sidebarPaneId);
|
|
1175
|
+
}
|
|
1176
|
+
// ==========================================================================
|
|
1177
|
+
// Helpers
|
|
1178
|
+
// ==========================================================================
|
|
1179
|
+
getMaxIndex() {
|
|
1180
|
+
return Math.max(0, this.getTotalItemCount() - 1);
|
|
1181
|
+
}
|
|
1182
|
+
getTotalItemCount() {
|
|
1183
|
+
let count = 0;
|
|
1184
|
+
for (const wt of this.state.worktrees) {
|
|
1185
|
+
count++; // worktree
|
|
1186
|
+
count += this.state.sessions.filter(s => s.worktreeId === wt.id).length;
|
|
1187
|
+
}
|
|
1188
|
+
return count;
|
|
1189
|
+
}
|
|
1190
|
+
getSelectedItem() {
|
|
1191
|
+
const items = buildListItems(this.state);
|
|
1192
|
+
const item = items[this.state.selectedIndex];
|
|
1193
|
+
if (!item)
|
|
1194
|
+
return null;
|
|
1195
|
+
return {
|
|
1196
|
+
type: item.type,
|
|
1197
|
+
id: item.id,
|
|
1198
|
+
worktree: item.worktree,
|
|
1199
|
+
session: item.session,
|
|
1200
|
+
};
|
|
1201
|
+
}
|
|
1202
|
+
}
|
|
1203
|
+
//# sourceMappingURL=app.js.map
|