ccmanager 0.1.1 → 0.1.3

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/README.md CHANGED
@@ -11,6 +11,7 @@ https://github.com/user-attachments/assets/a6d80e73-dc06-4ef8-849d-e3857f6c7024
11
11
  - Visual status indicators for session states (busy, waiting, idle)
12
12
  - Create, merge, and delete worktrees from within the app
13
13
  - Configurable keyboard shortcuts
14
+ - Status change hooks for automation and notifications
14
15
 
15
16
  ## Why CCManager over Claude Squad?
16
17
 
@@ -76,26 +77,38 @@ The arguments are applied to all Claude Code sessions started by CCManager.
76
77
 
77
78
  You can customize keyboard shortcuts in two ways:
78
79
 
79
- 1. **Through the UI**: Select "Configure Shortcuts" from the main menu
80
- 2. **Configuration file**: Edit `~/.config/ccmanager/shortcuts.json`
80
+ 1. **Through the UI**: Select "Configuration" → "Configure Shortcuts" from the main menu
81
+ 2. **Configuration file**: Edit `~/.config/ccmanager/config.json` (or legacy `~/.config/ccmanager/shortcuts.json`)
81
82
 
82
83
  Example configuration:
83
84
  ```json
85
+ // config.json (new format)
86
+ {
87
+ "shortcuts": {
88
+ "returnToMenu": {
89
+ "ctrl": true,
90
+ "key": "r"
91
+ },
92
+ "cancel": {
93
+ "key": "escape"
94
+ }
95
+ }
96
+ }
97
+
98
+ // shortcuts.json (legacy format, still supported)
84
99
  {
85
100
  "returnToMenu": {
86
101
  "ctrl": true,
87
102
  "key": "r"
88
103
  },
89
- "exitApp": {
90
- "ctrl": true,
91
- "key": "x"
92
- },
93
104
  "cancel": {
94
105
  "key": "escape"
95
106
  }
96
107
  }
97
108
  ```
98
109
 
110
+ Note: Shortcuts from `shortcuts.json` will be automatically migrated to `config.json` on first use.
111
+
99
112
  ### Restrictions
100
113
 
101
114
  - Shortcuts must use a modifier key (Ctrl) except for special keys like Escape
@@ -104,6 +117,20 @@ Example configuration:
104
117
  - Ctrl+D
105
118
  - Ctrl+[ (equivalent to Escape)
106
119
 
120
+ ## Status Change Hooks
121
+
122
+ CCManager can execute custom commands when Claude Code session status changes. This enables powerful automation workflows like desktop notifications, logging, or integration with other tools.
123
+
124
+ ### Overview
125
+
126
+ Status hooks allow you to:
127
+ - Get notified when Claude needs your input
128
+ - Track time spent in different states
129
+ - Trigger automations based on session activity
130
+ - Integrate with notification systems like [noti](https://github.com/variadico/noti)
131
+
132
+ For detailed setup instructions, see [docs/state-hooks.md](docs/state-hooks.md).
133
+
107
134
  ## Development
108
135
 
109
136
  ```bash
@@ -5,7 +5,7 @@ import Session from './Session.js';
5
5
  import NewWorktree from './NewWorktree.js';
6
6
  import DeleteWorktree from './DeleteWorktree.js';
7
7
  import MergeWorktree from './MergeWorktree.js';
8
- import ConfigureShortcuts from './ConfigureShortcuts.js';
8
+ import Configuration from './Configuration.js';
9
9
  import { SessionManager } from '../services/sessionManager.js';
10
10
  import { WorktreeService } from '../services/worktreeService.js';
11
11
  import { shortcutManager } from '../services/shortcutManager.js';
@@ -62,9 +62,9 @@ const App = () => {
62
62
  setView('merge-worktree');
63
63
  return;
64
64
  }
65
- // Check if this is the configure shortcuts option
66
- if (worktree.path === 'CONFIGURE_SHORTCUTS') {
67
- setView('configure-shortcuts');
65
+ // Check if this is the configuration option
66
+ if (worktree.path === 'CONFIGURATION') {
67
+ setView('configuration');
68
68
  return;
69
69
  }
70
70
  // Check if this is the exit application option
@@ -220,8 +220,8 @@ const App = () => {
220
220
  return (React.createElement(Box, { flexDirection: "column" },
221
221
  React.createElement(Text, { color: "green" }, "Merging worktrees...")));
222
222
  }
223
- if (view === 'configure-shortcuts') {
224
- return React.createElement(ConfigureShortcuts, { onComplete: handleReturnToMenu });
223
+ if (view === 'configuration') {
224
+ return React.createElement(Configuration, { onComplete: handleReturnToMenu });
225
225
  }
226
226
  return null;
227
227
  };
@@ -0,0 +1,6 @@
1
+ import React from 'react';
2
+ interface ConfigurationProps {
3
+ onComplete: () => void;
4
+ }
5
+ declare const Configuration: React.FC<ConfigurationProps>;
6
+ export default Configuration;
@@ -0,0 +1,49 @@
1
+ import React, { useState } from 'react';
2
+ import { Box, Text } from 'ink';
3
+ import SelectInput from 'ink-select-input';
4
+ import ConfigureShortcuts from './ConfigureShortcuts.js';
5
+ import ConfigureHooks from './ConfigureHooks.js';
6
+ const Configuration = ({ onComplete }) => {
7
+ const [view, setView] = useState('menu');
8
+ const menuItems = [
9
+ {
10
+ label: '⌨ Configure Shortcuts',
11
+ value: 'shortcuts',
12
+ },
13
+ {
14
+ label: '🔧 Configure Status Hooks',
15
+ value: 'hooks',
16
+ },
17
+ {
18
+ label: '← Back to Main Menu',
19
+ value: 'back',
20
+ },
21
+ ];
22
+ const handleSelect = (item) => {
23
+ if (item.value === 'back') {
24
+ onComplete();
25
+ }
26
+ else if (item.value === 'shortcuts') {
27
+ setView('shortcuts');
28
+ }
29
+ else if (item.value === 'hooks') {
30
+ setView('hooks');
31
+ }
32
+ };
33
+ const handleSubMenuComplete = () => {
34
+ setView('menu');
35
+ };
36
+ if (view === 'shortcuts') {
37
+ return React.createElement(ConfigureShortcuts, { onComplete: handleSubMenuComplete });
38
+ }
39
+ if (view === 'hooks') {
40
+ return React.createElement(ConfigureHooks, { onComplete: handleSubMenuComplete });
41
+ }
42
+ return (React.createElement(Box, { flexDirection: "column" },
43
+ React.createElement(Box, { marginBottom: 1 },
44
+ React.createElement(Text, { bold: true, color: "green" }, "Configuration")),
45
+ React.createElement(Box, { marginBottom: 1 },
46
+ React.createElement(Text, { dimColor: true }, "Select a configuration option:")),
47
+ React.createElement(SelectInput, { items: menuItems, onSelect: handleSelect, isFocused: true })));
48
+ };
49
+ export default Configuration;
@@ -0,0 +1,6 @@
1
+ import React from 'react';
2
+ interface ConfigureHooksProps {
3
+ onComplete: () => void;
4
+ }
5
+ declare const ConfigureHooks: React.FC<ConfigureHooksProps>;
6
+ export default ConfigureHooks;
@@ -0,0 +1,133 @@
1
+ import React, { useState, useEffect } from 'react';
2
+ import { Box, Text, useInput } from 'ink';
3
+ import TextInput from 'ink-text-input';
4
+ import SelectInput from 'ink-select-input';
5
+ import { configurationManager } from '../services/configurationManager.js';
6
+ const STATUS_LABELS = {
7
+ idle: 'Idle',
8
+ busy: 'Busy',
9
+ waiting_input: 'Waiting for Input',
10
+ };
11
+ const ConfigureHooks = ({ onComplete }) => {
12
+ const [view, setView] = useState('menu');
13
+ const [selectedStatus, setSelectedStatus] = useState('idle');
14
+ const [hooks, setHooks] = useState({});
15
+ const [currentCommand, setCurrentCommand] = useState('');
16
+ const [currentEnabled, setCurrentEnabled] = useState(false);
17
+ const [showSaveMessage, setShowSaveMessage] = useState(false);
18
+ useEffect(() => {
19
+ setHooks(configurationManager.getStatusHooks());
20
+ }, []);
21
+ useInput((input, key) => {
22
+ if (key.escape) {
23
+ if (view === 'edit') {
24
+ setView('menu');
25
+ }
26
+ else {
27
+ onComplete();
28
+ }
29
+ }
30
+ else if (key.tab && view === 'edit') {
31
+ toggleEnabled();
32
+ }
33
+ });
34
+ const getMenuItems = () => {
35
+ const items = [];
36
+ // Add status hook items
37
+ ['idle', 'busy', 'waiting_input'].forEach(status => {
38
+ const hook = hooks[status];
39
+ const enabled = hook?.enabled ? '✓' : '✗';
40
+ const command = hook?.command || '(not set)';
41
+ items.push({
42
+ label: `${STATUS_LABELS[status]}: ${enabled} ${command}`,
43
+ value: status,
44
+ });
45
+ });
46
+ items.push({
47
+ label: '─────────────',
48
+ value: 'separator',
49
+ });
50
+ items.push({
51
+ label: '💾 Save and Return',
52
+ value: 'save',
53
+ });
54
+ items.push({
55
+ label: '← Cancel',
56
+ value: 'cancel',
57
+ });
58
+ return items;
59
+ };
60
+ const handleMenuSelect = (item) => {
61
+ if (item.value === 'save') {
62
+ configurationManager.setStatusHooks(hooks);
63
+ setShowSaveMessage(true);
64
+ setTimeout(() => {
65
+ onComplete();
66
+ }, 1000);
67
+ }
68
+ else if (item.value === 'cancel') {
69
+ onComplete();
70
+ }
71
+ else if (item.value !== 'separator') {
72
+ const status = item.value;
73
+ setSelectedStatus(status);
74
+ const hook = hooks[status];
75
+ setCurrentCommand(hook?.command || '');
76
+ setCurrentEnabled(hook?.enabled ?? true); // Default to true if not set
77
+ setView('edit');
78
+ }
79
+ };
80
+ const handleCommandSubmit = (value) => {
81
+ setHooks(prev => ({
82
+ ...prev,
83
+ [selectedStatus]: {
84
+ command: value,
85
+ enabled: currentEnabled,
86
+ },
87
+ }));
88
+ setView('menu');
89
+ };
90
+ const toggleEnabled = () => {
91
+ setCurrentEnabled(prev => !prev);
92
+ };
93
+ if (showSaveMessage) {
94
+ return (React.createElement(Box, { flexDirection: "column" },
95
+ React.createElement(Text, { color: "green" }, "\u2713 Configuration saved successfully!")));
96
+ }
97
+ if (view === 'edit') {
98
+ return (React.createElement(Box, { flexDirection: "column" },
99
+ React.createElement(Box, { marginBottom: 1 },
100
+ React.createElement(Text, { bold: true, color: "green" },
101
+ "Configure ",
102
+ STATUS_LABELS[selectedStatus],
103
+ " Hook")),
104
+ React.createElement(Box, { marginBottom: 1 },
105
+ React.createElement(Text, null,
106
+ "Command to execute when status changes to",
107
+ ' ',
108
+ STATUS_LABELS[selectedStatus],
109
+ ":")),
110
+ React.createElement(Box, { marginBottom: 1 },
111
+ React.createElement(TextInput, { value: currentCommand, onChange: setCurrentCommand, onSubmit: handleCommandSubmit, placeholder: "Enter command (e.g., notify-send 'Claude is idle')" })),
112
+ React.createElement(Box, { marginBottom: 1 },
113
+ React.createElement(Text, null,
114
+ "Enabled: ",
115
+ currentEnabled ? '✓' : '✗',
116
+ " (Press Tab to toggle)")),
117
+ React.createElement(Box, { marginTop: 1 },
118
+ React.createElement(Text, { dimColor: true }, "Environment variables available: CCMANAGER_OLD_STATE, CCMANAGER_NEW_STATE,")),
119
+ React.createElement(Box, null,
120
+ React.createElement(Text, { dimColor: true }, "CCMANAGER_WORKTREE, CCMANAGER_WORKTREE_BRANCH, CCMANAGER_SESSION_ID")),
121
+ React.createElement(Box, { marginTop: 1 },
122
+ React.createElement(Text, { dimColor: true }, "Press Enter to save, Tab to toggle enabled, Esc to cancel"))));
123
+ }
124
+ return (React.createElement(Box, { flexDirection: "column" },
125
+ React.createElement(Box, { marginBottom: 1 },
126
+ React.createElement(Text, { bold: true, color: "green" }, "Configure Status Change Hooks")),
127
+ React.createElement(Box, { marginBottom: 1 },
128
+ React.createElement(Text, { dimColor: true }, "Set commands to run when Claude Code session status changes:")),
129
+ React.createElement(SelectInput, { items: getMenuItems(), onSelect: handleMenuSelect, isFocused: true }),
130
+ React.createElement(Box, { marginTop: 1 },
131
+ React.createElement(Text, { dimColor: true }, "Press Esc to go back"))));
132
+ };
133
+ export default ConfigureHooks;
@@ -67,8 +67,8 @@ const Menu = ({ sessionManager, onSelectWorktree }) => {
67
67
  value: 'delete-worktree',
68
68
  });
69
69
  menuItems.push({
70
- label: `${MENU_ICONS.CONFIGURE_SHORTCUTS} Configure Shortcuts`,
71
- value: 'configure-shortcuts',
70
+ label: `${MENU_ICONS.CONFIGURE_SHORTCUTS} Configuration`,
71
+ value: 'configuration',
72
72
  });
73
73
  menuItems.push({
74
74
  label: `${MENU_ICONS.EXIT} Exit`,
@@ -107,10 +107,10 @@ const Menu = ({ sessionManager, onSelectWorktree }) => {
107
107
  hasSession: false,
108
108
  });
109
109
  }
110
- else if (item.value === 'configure-shortcuts') {
110
+ else if (item.value === 'configuration') {
111
111
  // Handle in parent component - use special marker
112
112
  onSelectWorktree({
113
- path: 'CONFIGURE_SHORTCUTS',
113
+ path: 'CONFIGURATION',
114
114
  branch: '',
115
115
  isMainWorktree: false,
116
116
  hasSession: false,
@@ -39,6 +39,15 @@ const Session = ({ session, sessionManager, onReturnToMenu, }) => {
39
39
  sessionManager.on('sessionRestore', handleSessionRestore);
40
40
  // Mark session as active (this will trigger the restore event)
41
41
  sessionManager.setSessionActive(session.worktreePath, true);
42
+ // Immediately resize the PTY and terminal to current dimensions
43
+ // This fixes rendering issues when terminal width changed while in menu
44
+ // https://github.com/kbwo/ccmanager/issues/2
45
+ const currentCols = process.stdout.columns || 80;
46
+ const currentRows = process.stdout.rows || 24;
47
+ session.process.resize(currentCols, currentRows);
48
+ if (session.terminal) {
49
+ session.terminal.resize(currentCols, currentRows);
50
+ }
42
51
  // Listen for session data events
43
52
  const handleSessionData = (activeSession, data) => {
44
53
  // Only handle data for our session
@@ -0,0 +1,17 @@
1
+ import { ConfigurationData, StatusHookConfig, ShortcutConfig } from '../types/index.js';
2
+ export declare class ConfigurationManager {
3
+ private configPath;
4
+ private legacyShortcutsPath;
5
+ private config;
6
+ constructor();
7
+ private loadConfig;
8
+ private migrateLegacyShortcuts;
9
+ private saveConfig;
10
+ getShortcuts(): ShortcutConfig;
11
+ setShortcuts(shortcuts: ShortcutConfig): void;
12
+ getStatusHooks(): StatusHookConfig;
13
+ setStatusHooks(hooks: StatusHookConfig): void;
14
+ getConfiguration(): ConfigurationData;
15
+ setConfiguration(config: ConfigurationData): void;
16
+ }
17
+ export declare const configurationManager: ConfigurationManager;
@@ -0,0 +1,115 @@
1
+ import { homedir } from 'os';
2
+ import { join } from 'path';
3
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs';
4
+ import { DEFAULT_SHORTCUTS, } from '../types/index.js';
5
+ export class ConfigurationManager {
6
+ constructor() {
7
+ Object.defineProperty(this, "configPath", {
8
+ enumerable: true,
9
+ configurable: true,
10
+ writable: true,
11
+ value: void 0
12
+ });
13
+ Object.defineProperty(this, "legacyShortcutsPath", {
14
+ enumerable: true,
15
+ configurable: true,
16
+ writable: true,
17
+ value: void 0
18
+ });
19
+ Object.defineProperty(this, "config", {
20
+ enumerable: true,
21
+ configurable: true,
22
+ writable: true,
23
+ value: {}
24
+ });
25
+ // Determine config directory based on platform
26
+ const homeDir = homedir();
27
+ const configDir = process.platform === 'win32'
28
+ ? join(process.env['APPDATA'] || join(homeDir, 'AppData', 'Roaming'), 'ccmanager')
29
+ : join(homeDir, '.config', 'ccmanager');
30
+ // Ensure config directory exists
31
+ if (!existsSync(configDir)) {
32
+ mkdirSync(configDir, { recursive: true });
33
+ }
34
+ this.configPath = join(configDir, 'config.json');
35
+ this.legacyShortcutsPath = join(configDir, 'shortcuts.json');
36
+ this.loadConfig();
37
+ }
38
+ loadConfig() {
39
+ // Try to load the new config file
40
+ if (existsSync(this.configPath)) {
41
+ try {
42
+ const configData = readFileSync(this.configPath, 'utf-8');
43
+ this.config = JSON.parse(configData);
44
+ }
45
+ catch (error) {
46
+ console.error('Failed to load configuration:', error);
47
+ this.config = {};
48
+ }
49
+ }
50
+ else {
51
+ // If new config doesn't exist, check for legacy shortcuts.json
52
+ this.migrateLegacyShortcuts();
53
+ }
54
+ // Check if shortcuts need to be loaded from legacy file
55
+ // This handles the case where config.json exists but doesn't have shortcuts
56
+ if (!this.config.shortcuts && existsSync(this.legacyShortcutsPath)) {
57
+ this.migrateLegacyShortcuts();
58
+ }
59
+ // Ensure default values
60
+ if (!this.config.shortcuts) {
61
+ this.config.shortcuts = DEFAULT_SHORTCUTS;
62
+ }
63
+ if (!this.config.statusHooks) {
64
+ this.config.statusHooks = {};
65
+ }
66
+ }
67
+ migrateLegacyShortcuts() {
68
+ if (existsSync(this.legacyShortcutsPath)) {
69
+ try {
70
+ const shortcutsData = readFileSync(this.legacyShortcutsPath, 'utf-8');
71
+ const shortcuts = JSON.parse(shortcutsData);
72
+ // Validate that it's a valid shortcuts config
73
+ if (shortcuts && typeof shortcuts === 'object') {
74
+ this.config.shortcuts = shortcuts;
75
+ // Save to new config format
76
+ this.saveConfig();
77
+ console.log('Migrated shortcuts from legacy shortcuts.json to config.json');
78
+ }
79
+ }
80
+ catch (error) {
81
+ console.error('Failed to migrate legacy shortcuts:', error);
82
+ }
83
+ }
84
+ }
85
+ saveConfig() {
86
+ try {
87
+ writeFileSync(this.configPath, JSON.stringify(this.config, null, 2));
88
+ }
89
+ catch (error) {
90
+ console.error('Failed to save configuration:', error);
91
+ }
92
+ }
93
+ getShortcuts() {
94
+ return this.config.shortcuts || DEFAULT_SHORTCUTS;
95
+ }
96
+ setShortcuts(shortcuts) {
97
+ this.config.shortcuts = shortcuts;
98
+ this.saveConfig();
99
+ }
100
+ getStatusHooks() {
101
+ return this.config.statusHooks || {};
102
+ }
103
+ setStatusHooks(hooks) {
104
+ this.config.statusHooks = hooks;
105
+ this.saveConfig();
106
+ }
107
+ getConfiguration() {
108
+ return this.config;
109
+ }
110
+ setConfiguration(config) {
111
+ this.config = config;
112
+ this.saveConfig();
113
+ }
114
+ }
115
+ export const configurationManager = new ConfigurationManager();
@@ -15,6 +15,7 @@ export declare class SessionManager extends EventEmitter implements ISessionMana
15
15
  setSessionActive(worktreePath: string, active: boolean): void;
16
16
  destroySession(worktreePath: string): void;
17
17
  getAllSessions(): Session[];
18
+ private executeStatusHook;
18
19
  destroy(): void;
19
20
  }
20
21
  export {};
@@ -1,6 +1,9 @@
1
1
  import { spawn } from 'node-pty';
2
2
  import { EventEmitter } from 'events';
3
3
  import pkg from '@xterm/headless';
4
+ import { exec } from 'child_process';
5
+ import { configurationManager } from './configurationManager.js';
6
+ import { WorktreeService } from './worktreeService.js';
4
7
  const { Terminal } = pkg;
5
8
  export class SessionManager extends EventEmitter {
6
9
  stripAnsi(str) {
@@ -131,6 +134,7 @@ export class SessionManager extends EventEmitter {
131
134
  const newState = this.detectTerminalState(session.terminal);
132
135
  if (newState !== oldState) {
133
136
  session.state = newState;
137
+ this.executeStatusHook(oldState, newState, session);
134
138
  this.emit('sessionStateChanged', session);
135
139
  }
136
140
  }, 100); // Check every 100ms
@@ -187,6 +191,36 @@ export class SessionManager extends EventEmitter {
187
191
  getAllSessions() {
188
192
  return Array.from(this.sessions.values());
189
193
  }
194
+ executeStatusHook(oldState, newState, session) {
195
+ const statusHooks = configurationManager.getStatusHooks();
196
+ const hook = statusHooks[newState];
197
+ if (hook && hook.enabled && hook.command) {
198
+ // Get branch information
199
+ const worktreeService = new WorktreeService();
200
+ const worktrees = worktreeService.getWorktrees();
201
+ const worktree = worktrees.find(wt => wt.path === session.worktreePath);
202
+ const branch = worktree?.branch || 'unknown';
203
+ // Execute the hook command in the session's worktree directory
204
+ exec(hook.command, {
205
+ cwd: session.worktreePath,
206
+ env: {
207
+ ...process.env,
208
+ CCMANAGER_OLD_STATE: oldState,
209
+ CCMANAGER_NEW_STATE: newState,
210
+ CCMANAGER_WORKTREE: session.worktreePath,
211
+ CCMANAGER_WORKTREE_BRANCH: branch,
212
+ CCMANAGER_SESSION_ID: session.id,
213
+ },
214
+ }, (error, _stdout, stderr) => {
215
+ if (error) {
216
+ console.error(`Failed to execute ${newState} hook: ${error.message}`);
217
+ }
218
+ if (stderr) {
219
+ console.error(`Hook stderr: ${stderr}`);
220
+ }
221
+ });
222
+ }
223
+ }
190
224
  destroy() {
191
225
  // Clean up all sessions
192
226
  for (const worktreePath of this.sessions.keys()) {
@@ -1,11 +1,8 @@
1
1
  import { ShortcutKey, ShortcutConfig } from '../types/index.js';
2
2
  import { Key } from 'ink';
3
3
  export declare class ShortcutManager {
4
- private shortcuts;
5
- private configPath;
6
4
  private reservedKeys;
7
5
  constructor();
8
- private loadShortcuts;
9
6
  private validateShortcut;
10
7
  private isReservedKey;
11
8
  saveShortcuts(shortcuts: ShortcutConfig): boolean;
@@ -1,21 +1,6 @@
1
- import { DEFAULT_SHORTCUTS, } from '../types/index.js';
2
- import * as fs from 'fs';
3
- import * as path from 'path';
4
- import * as os from 'os';
1
+ import { configurationManager } from './configurationManager.js';
5
2
  export class ShortcutManager {
6
3
  constructor() {
7
- Object.defineProperty(this, "shortcuts", {
8
- enumerable: true,
9
- configurable: true,
10
- writable: true,
11
- value: void 0
12
- });
13
- Object.defineProperty(this, "configPath", {
14
- enumerable: true,
15
- configurable: true,
16
- writable: true,
17
- value: void 0
18
- });
19
4
  Object.defineProperty(this, "reservedKeys", {
20
5
  enumerable: true,
21
6
  configurable: true,
@@ -27,31 +12,6 @@ export class ShortcutManager {
27
12
  { ctrl: true, key: '[' },
28
13
  ]
29
14
  });
30
- // Use platform-specific config directory
31
- const configDir = process.platform === 'win32'
32
- ? path.join(process.env['APPDATA'] || os.homedir(), 'ccmanager')
33
- : path.join(os.homedir(), '.config', 'ccmanager');
34
- this.configPath = path.join(configDir, 'shortcuts.json');
35
- this.shortcuts = this.loadShortcuts();
36
- }
37
- loadShortcuts() {
38
- try {
39
- if (fs.existsSync(this.configPath)) {
40
- const data = fs.readFileSync(this.configPath, 'utf8');
41
- const loaded = JSON.parse(data);
42
- // Validate loaded shortcuts
43
- const validated = {
44
- returnToMenu: this.validateShortcut(loaded.returnToMenu) ||
45
- DEFAULT_SHORTCUTS.returnToMenu,
46
- cancel: this.validateShortcut(loaded.cancel) || DEFAULT_SHORTCUTS.cancel,
47
- };
48
- return validated;
49
- }
50
- }
51
- catch (error) {
52
- console.error('Failed to load shortcuts:', error);
53
- }
54
- return { ...DEFAULT_SHORTCUTS };
55
15
  }
56
16
  validateShortcut(shortcut) {
57
17
  if (!shortcut || typeof shortcut !== 'object') {
@@ -88,30 +48,21 @@ export class ShortcutManager {
88
48
  }
89
49
  saveShortcuts(shortcuts) {
90
50
  // Validate all shortcuts
51
+ const currentShortcuts = configurationManager.getShortcuts();
91
52
  const validated = {
92
53
  returnToMenu: this.validateShortcut(shortcuts.returnToMenu) ||
93
- this.shortcuts.returnToMenu,
94
- cancel: this.validateShortcut(shortcuts.cancel) || this.shortcuts.cancel,
54
+ currentShortcuts.returnToMenu,
55
+ cancel: this.validateShortcut(shortcuts.cancel) || currentShortcuts.cancel,
95
56
  };
96
- try {
97
- const dir = path.dirname(this.configPath);
98
- if (!fs.existsSync(dir)) {
99
- fs.mkdirSync(dir, { recursive: true });
100
- }
101
- fs.writeFileSync(this.configPath, JSON.stringify(validated, null, 2));
102
- this.shortcuts = validated;
103
- return true;
104
- }
105
- catch (error) {
106
- console.error('Failed to save shortcuts:', error);
107
- return false;
108
- }
57
+ configurationManager.setShortcuts(validated);
58
+ return true;
109
59
  }
110
60
  getShortcuts() {
111
- return { ...this.shortcuts };
61
+ return configurationManager.getShortcuts();
112
62
  }
113
63
  matchesShortcut(shortcutName, input, key) {
114
- const shortcut = this.shortcuts[shortcutName];
64
+ const shortcuts = configurationManager.getShortcuts();
65
+ const shortcut = shortcuts[shortcutName];
115
66
  if (!shortcut)
116
67
  return false;
117
68
  // Handle escape key specially
@@ -129,7 +80,8 @@ export class ShortcutManager {
129
80
  return input.toLowerCase() === shortcut.key.toLowerCase();
130
81
  }
131
82
  getShortcutDisplay(shortcutName) {
132
- const shortcut = this.shortcuts[shortcutName];
83
+ const shortcuts = configurationManager.getShortcuts();
84
+ const shortcut = shortcuts[shortcutName];
133
85
  if (!shortcut)
134
86
  return '';
135
87
  const parts = [];
@@ -32,7 +32,12 @@ export class WorktreeService {
32
32
  };
33
33
  }
34
34
  else if (line.startsWith('branch ')) {
35
- currentWorktree.branch = line.substring(7);
35
+ let branch = line.substring(7);
36
+ // Remove refs/heads/ prefix if present
37
+ if (branch.startsWith('refs/heads/')) {
38
+ branch = branch.substring(11);
39
+ }
40
+ currentWorktree.branch = branch;
36
41
  }
37
42
  else if (line === 'bare') {
38
43
  currentWorktree.isMainWorktree = true;
@@ -38,3 +38,16 @@ export interface ShortcutConfig {
38
38
  cancel: ShortcutKey;
39
39
  }
40
40
  export declare const DEFAULT_SHORTCUTS: ShortcutConfig;
41
+ export interface StatusHook {
42
+ command: string;
43
+ enabled: boolean;
44
+ }
45
+ export interface StatusHookConfig {
46
+ idle?: StatusHook;
47
+ busy?: StatusHook;
48
+ waiting_input?: StatusHook;
49
+ }
50
+ export interface ConfigurationData {
51
+ shortcuts?: ShortcutConfig;
52
+ statusHooks?: StatusHookConfig;
53
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ccmanager",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "description": "TUI application for managing multiple Claude Code sessions across Git worktrees",
5
5
  "license": "MIT",
6
6
  "author": "Kodai Kabasawa",