claude-code-monitor 1.0.3 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/CHANGELOG.md +40 -0
  2. package/README.md +83 -104
  3. package/dist/bin/ccm.js +51 -18
  4. package/dist/components/Dashboard.d.ts +6 -1
  5. package/dist/components/Dashboard.d.ts.map +1 -1
  6. package/dist/components/Dashboard.js +39 -5
  7. package/dist/components/SessionCard.d.ts.map +1 -1
  8. package/dist/components/SessionCard.js +2 -4
  9. package/dist/components/Spinner.js +1 -1
  10. package/dist/constants.d.ts +5 -2
  11. package/dist/constants.d.ts.map +1 -1
  12. package/dist/constants.js +5 -2
  13. package/dist/hook/handler.d.ts.map +1 -1
  14. package/dist/hook/handler.js +16 -15
  15. package/dist/hooks/useServer.d.ts +10 -0
  16. package/dist/hooks/useServer.d.ts.map +1 -0
  17. package/dist/hooks/useServer.js +39 -0
  18. package/dist/index.d.ts +1 -0
  19. package/dist/index.d.ts.map +1 -1
  20. package/dist/index.js +1 -0
  21. package/dist/server/index.d.ts +12 -0
  22. package/dist/server/index.d.ts.map +1 -0
  23. package/dist/server/index.js +309 -0
  24. package/dist/store/file-store.d.ts +5 -0
  25. package/dist/store/file-store.d.ts.map +1 -1
  26. package/dist/store/file-store.js +40 -7
  27. package/dist/types/index.d.ts +2 -0
  28. package/dist/types/index.d.ts.map +1 -1
  29. package/dist/utils/applescript.d.ts +7 -0
  30. package/dist/utils/applescript.d.ts.map +1 -0
  31. package/dist/utils/applescript.js +18 -0
  32. package/dist/utils/focus.d.ts +0 -1
  33. package/dist/utils/focus.d.ts.map +1 -1
  34. package/dist/utils/focus.js +16 -22
  35. package/dist/utils/send-text.d.ts +40 -0
  36. package/dist/utils/send-text.d.ts.map +1 -0
  37. package/dist/utils/send-text.js +324 -0
  38. package/dist/utils/status.js +2 -2
  39. package/dist/utils/stdin.d.ts +6 -0
  40. package/dist/utils/stdin.d.ts.map +1 -0
  41. package/dist/utils/stdin.js +12 -0
  42. package/dist/utils/terminal-strategy.d.ts +18 -0
  43. package/dist/utils/terminal-strategy.d.ts.map +1 -0
  44. package/dist/utils/terminal-strategy.js +15 -0
  45. package/dist/utils/time.d.ts.map +1 -1
  46. package/dist/utils/time.js +1 -3
  47. package/dist/utils/transcript.d.ts +10 -0
  48. package/dist/utils/transcript.d.ts.map +1 -0
  49. package/dist/utils/transcript.js +45 -0
  50. package/dist/utils/tty-cache.d.ts +0 -5
  51. package/dist/utils/tty-cache.d.ts.map +1 -1
  52. package/dist/utils/tty-cache.js +0 -7
  53. package/package.json +6 -2
  54. package/public/index.html +1219 -0
package/CHANGELOG.md CHANGED
@@ -5,6 +5,46 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [1.1.0] - 2026-01-22
9
+
10
+ ### Added
11
+
12
+ - **Mobile Web Interface** - Monitor and control sessions from your smartphone
13
+ - Real-time session status via WebSocket
14
+ - View latest Claude messages with markdown rendering
15
+ - Focus terminal sessions remotely
16
+ - Send text messages to terminal (multi-line supported)
17
+ - Permission prompt responses (y/n/a) and Ctrl+C support
18
+ - Bottom sheet modal with swipe-to-close gesture
19
+ - New command: `ccm serve` - Start mobile web server only
20
+ - QR code display in terminal UI (press `h` to toggle)
21
+ - Token-based authentication for mobile access
22
+ - Auto-select available port when default port (3456) is in use
23
+
24
+ ### Changed
25
+
26
+ - Redesigned README with demo GIFs for both Terminal UI and Mobile Web
27
+ - Consolidated terminal fallback strategy for better code maintainability
28
+
29
+ ### Security
30
+
31
+ - Mobile Web requires same Wi-Fi network (local network only)
32
+ - Unique token generated per session for authentication
33
+ - Warning messages about not sharing the access URL
34
+ - Dangerous command detection in mobile input
35
+
36
+ ## [1.0.4] - 2026-01-18
37
+
38
+ ### Fixed
39
+
40
+ - Use alternate screen buffer to prevent TUI stacking on re-render ([#5](https://github.com/onikan27/claude-code-monitor/pull/5)) by [@msdjzmst](https://github.com/msdjzmst)
41
+
42
+ ## [1.0.3] - 2026-01-17
43
+
44
+ ### Changed
45
+
46
+ - Update README: Add macOS-only badge and note, rename demo gif
47
+
8
48
  ## [1.0.2] - 2026-01-17
9
49
 
10
50
  ### Fixed
package/README.md CHANGED
@@ -1,76 +1,61 @@
1
- # Claude Code Monitor CLI
1
+ # Claude Code Monitor
2
2
 
3
3
  [![npm version](https://img.shields.io/npm/v/claude-code-monitor.svg)](https://www.npmjs.com/package/claude-code-monitor)
4
4
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
5
5
  [![macOS](https://img.shields.io/badge/platform-macOS-lightgrey.svg)](https://www.apple.com/macos/)
6
6
 
7
- **A CLI tool to monitor multiple Claude Code sessions in real-time from your terminal.**
7
+ **Monitor multiple Claude Code sessions in real-time from your terminal or smartphone.**
8
+
9
+ ### Terminal UI
10
+ Monitor sessions with keyboard navigation
8
11
 
9
12
  <p align="center">
10
- <img src="https://raw.githubusercontent.com/onikan27/claude-code-monitor/main/docs/ccm-demo.gif" alt="Claude Code Monitor Demo" width="1000">
13
+ <img src="https://raw.githubusercontent.com/onikan27/claude-code-monitor/main/docs/ccm-demo.gif" alt="Terminal UI Demo" width="800">
11
14
  </p>
12
15
 
13
- ---
16
+ ### Mobile Web
17
+ Control from your phone (same Wi-Fi required)
14
18
 
15
- ## 📑 Table of Contents
16
-
17
- - [✨ Features](#-features)
18
- - [📋 Requirements](#-requirements)
19
- - [🚀 Installation](#-installation)
20
- - [⚡ Quick Start](#-quick-start)
21
- - [📖 Commands](#-commands)
22
- - [⌨️ Keybindings](#️-keybindings-watch-mode)
23
- - [🎨 Status Icons](#-status-icons)
24
- - [🖥️ Supported Terminals](#️-supported-terminals)
25
- - [💾 Data Storage](#-data-storage)
26
- - [📦 Programmatic Usage](#-programmatic-usage)
27
- - [🔧 Troubleshooting](#-troubleshooting)
28
- - [🔒 Security](#-security)
29
- - [⚠️ Disclaimer](#️-disclaimer)
30
- - [📝 Changelog](#-changelog)
31
- - [📄 License](#-license)
19
+ <p align="center">
20
+ <img src="https://raw.githubusercontent.com/onikan27/claude-code-monitor/main/docs/mobile-web-demo.gif" alt="Mobile Web Demo" width="800">
21
+ </p>
32
22
 
33
23
  ---
34
24
 
35
25
  ## ✨ Features
36
26
 
37
- - 🔌 **Serverless** - File-based session state management (no API server required)
38
- - 🔄 **Real-time** - Auto-updates on file changes
39
- - 🎯 **Tab Focus** - Instantly switch to the terminal tab of a selected session
40
- - 🎨 **Simple UI** - Displays only status and directory
27
+ | Terminal (TUI) | Mobile Web |
28
+ |----------------|------------|
29
+ | Real-time session monitoring | Monitor from your smartphone |
30
+ | Quick tab focus with keyboard | Remote terminal focus |
31
+ | Vim-style navigation | Send messages to terminal |
32
+ | Simple status display | Real-time sync via WebSocket |
33
+
34
+ - 🔌 **Serverless** - File-based state management, no API server required
41
35
  - ⚡ **Easy Setup** - One command `ccm` for automatic setup and launch
36
+ - 🔒 **Secure** - No external data transmission, token-based mobile auth
42
37
 
43
38
  ---
44
39
 
45
40
  ## 📋 Requirements
46
41
 
47
- > **Note**: This tool is **macOS only** due to its use of AppleScript for terminal focus features.
42
+ > **Note**: This tool is **macOS only** due to its use of AppleScript for terminal control.
48
43
 
49
- - **macOS** (focus feature is macOS only)
44
+ - **macOS**
50
45
  - **Node.js** >= 18.0.0
51
46
  - **Claude Code** installed
52
47
 
53
48
  ---
54
49
 
55
- ## 🚀 Installation
50
+ ## 🚀 Quick Start
56
51
 
57
- ### Global install (Recommended)
52
+ ### Install
58
53
 
59
54
  ```bash
60
55
  npm install -g claude-code-monitor
61
56
  ```
62
57
 
63
- ### Run with npx (no install required)
64
-
65
- ```bash
66
- npx claude-code-monitor
67
- ```
68
-
69
- > **Note**: With npx, you must run `npx claude-code-monitor` each time (the `ccm` shortcut is only available with global install). Global install is recommended since this tool requires hook setup and is designed for continuous use.
70
-
71
- ---
72
-
73
- ## ⚡ Quick Start
58
+ ### Run
74
59
 
75
60
  ```bash
76
61
  ccm
@@ -78,90 +63,78 @@ ccm
78
63
 
79
64
  On first run, it automatically sets up hooks and launches the monitor.
80
65
 
66
+ ### Mobile Access
67
+
68
+ 1. Press `h` to show QR code
69
+ 2. Scan with your smartphone (same Wi-Fi required)
70
+
81
71
  ---
82
72
 
83
- ## 📖 Commands
73
+ ## 📖 Usage
74
+
75
+ ### Commands
84
76
 
85
77
  | Command | Alias | Description |
86
78
  |---------|-------|-------------|
87
- | `ccm` | - | Launch monitor TUI (auto-setup if not configured) |
88
- | `ccm watch` | `ccm w` | Launch monitor TUI |
79
+ | `ccm` | - | Launch monitor (auto-setup if needed) |
80
+ | `ccm watch` | `ccm w` | Launch monitor |
81
+ | `ccm serve` | `ccm s` | Start mobile web server only |
89
82
  | `ccm setup` | - | Configure Claude Code hooks |
90
83
  | `ccm list` | `ccm ls` | List sessions |
91
84
  | `ccm clear` | - | Clear all sessions |
92
- | `ccm --version` | `ccm -V` | Show version |
93
- | `ccm --help` | `ccm -h` | Show help |
94
85
 
95
- ---
96
-
97
- ## ⌨️ Keybindings (watch mode)
86
+ ### Keybindings
98
87
 
99
88
  | Key | Action |
100
89
  |-----|--------|
101
90
  | `↑` / `k` | Move up |
102
91
  | `↓` / `j` | Move down |
103
92
  | `Enter` / `f` | Focus selected session |
104
- | `1-9` | Quick select & focus by number |
93
+ | `1-9` | Quick select & focus |
94
+ | `h` | Show/Hide QR code |
105
95
  | `c` | Clear all sessions |
106
96
  | `q` / `Esc` | Quit |
107
97
 
108
- ---
109
-
110
- ## 🎨 Status Icons
98
+ ### Status Icons
111
99
 
112
100
  | Icon | Status | Description |
113
101
  |------|--------|-------------|
114
102
  | `●` | Running | Claude Code is processing |
115
- | `◐` | Waiting | Waiting for user input (e.g., permission prompt) |
103
+ | `◐` | Waiting | Waiting for user input |
116
104
  | `✓` | Done | Session ended |
117
105
 
118
106
  ---
119
107
 
120
- ## 🖥️ Supported Terminals
121
-
122
- Focus feature works with the following terminals:
123
-
124
- | Terminal | Focus Support | Notes |
125
- |----------|--------------|-------|
126
- | iTerm2 | ✅ Full | TTY-based window/tab targeting |
127
- | Terminal.app | ✅ Full | TTY-based window/tab targeting |
128
- | Ghostty | ⚠️ Limited | Activates app only (cannot target specific window/tab) |
129
-
130
- > **Note**: Other terminals (Alacritty, kitty, Warp, etc.) can use monitoring but focus feature is not supported.
108
+ ## 📱 Mobile Web Interface
131
109
 
132
- ---
110
+ Monitor and control Claude Code sessions from your smartphone.
133
111
 
134
- ## 💾 Data Storage
112
+ ### Features
135
113
 
136
- Session data is stored in `~/.claude-monitor/sessions.json`.
114
+ - Real-time session status via WebSocket
115
+ - View latest Claude messages
116
+ - Focus terminal sessions remotely
117
+ - Send text messages to terminal
137
118
 
138
- ### What is stored
119
+ ### Security
139
120
 
140
- | Field | Description |
141
- |-------|-------------|
142
- | `session_id` | Claude Code session identifier |
143
- | `cwd` | Working directory path |
144
- | `tty` | Terminal device path (e.g., `/dev/ttys001`) |
145
- | `status` | Session status (running/waiting_input/stopped) |
146
- | `updated_at` | Last update timestamp |
121
+ > **Important**: Your smartphone and Mac must be on the **same Wi-Fi network**.
147
122
 
148
- Data is automatically removed after 30 minutes of inactivity or when the terminal session ends.
123
+ - **Token Authentication** - Each session generates a unique token
124
+ - **Local Network Only** - Not accessible from the internet
125
+ - **Do not share the URL** - Treat it like a password
149
126
 
150
127
  ---
151
128
 
152
- ## 📦 Programmatic Usage
153
-
154
- Can also be used as a library:
129
+ ## 🖥️ Supported Terminals
155
130
 
156
- ```typescript
157
- import { getSessions, getStatusDisplay } from 'claude-code-monitor';
131
+ | Terminal | Focus Support | Notes |
132
+ |----------|--------------|-------|
133
+ | iTerm2 | ✅ Full | TTY-based targeting |
134
+ | Terminal.app | ✅ Full | TTY-based targeting |
135
+ | Ghostty | ⚠️ Limited | App activation only |
158
136
 
159
- const sessions = getSessions();
160
- for (const session of sessions) {
161
- const { symbol, label } = getStatusDisplay(session.status);
162
- console.log(`${symbol} ${label}: ${session.cwd}`);
163
- }
164
- ```
137
+ > Other terminals can use monitoring, but focus feature is not supported.
165
138
 
166
139
  ---
167
140
 
@@ -170,48 +143,54 @@ for (const session of sessions) {
170
143
  ### Sessions not showing
171
144
 
172
145
  1. Run `ccm setup` to verify hook configuration
173
- 2. Check if `~/.claude/settings.json` contains hook settings
146
+ 2. Check `~/.claude/settings.json` for hook settings
174
147
  3. Restart Claude Code
175
148
 
176
- ```bash
177
- # Check configuration
178
- cat ~/.claude/settings.json | grep ccm
179
- ```
180
-
181
149
  ### Focus not working
182
150
 
183
- 1. Verify you're using macOS
184
- 2. Verify you're using iTerm2, Terminal.app, or Ghostty
185
- 3. Check System Preferences > Privacy & Security > Accessibility permissions
151
+ 1. Verify you're using a supported terminal
152
+ 2. Check System Preferences > Privacy & Security > Accessibility
186
153
 
187
- ### Reset session data
154
+ ### Reset data
188
155
 
189
156
  ```bash
190
157
  ccm clear
191
- # or
192
- rm ~/.claude-monitor/sessions.json
193
158
  ```
194
159
 
195
160
  ---
196
161
 
197
162
  ## 🔒 Security
198
163
 
199
- - This tool modifies `~/.claude/settings.json` to register hooks
200
- - Focus feature uses AppleScript to control terminal applications
201
- - All data is stored locally; no network requests are made
164
+ - **No data sent to external servers** - All data stays on your machine
165
+ - Hook registration modifies `~/.claude/settings.json`
166
+ - Focus feature uses AppleScript for terminal control
167
+ - Mobile Web uses token authentication on local network only
168
+
169
+ ---
170
+
171
+ ## 📦 Programmatic Usage
172
+
173
+ ```typescript
174
+ import { getSessions, focusSession } from 'claude-code-monitor';
175
+
176
+ const sessions = getSessions();
177
+ if (sessions[0]?.tty) {
178
+ focusSession(sessions[0].tty);
179
+ }
180
+ ```
202
181
 
203
182
  ---
204
183
 
205
184
  ## ⚠️ Disclaimer
206
185
 
207
- This is an unofficial community tool and is not affiliated with, endorsed by, or associated with Anthropic.
186
+ This is an unofficial community tool and is not affiliated with Anthropic.
208
187
  "Claude" and "Claude Code" are trademarks of Anthropic.
209
188
 
210
189
  ---
211
190
 
212
191
  ## 📝 Changelog
213
192
 
214
- See [CHANGELOG.md](./CHANGELOG.md) for a list of changes.
193
+ See [CHANGELOG.md](./CHANGELOG.md) for details.
215
194
 
216
195
  ---
217
196
 
package/dist/bin/ccm.js CHANGED
@@ -6,12 +6,15 @@ import { Command } from 'commander';
6
6
  import { render } from 'ink';
7
7
  import { Dashboard } from '../components/Dashboard.js';
8
8
  import { handleHookEvent } from '../hook/handler.js';
9
+ import { startServer } from '../server/index.js';
9
10
  import { isHooksConfigured, setupHooks } from '../setup/index.js';
10
11
  import { clearSessions, getSessions } from '../store/file-store.js';
11
12
  import { getStatusDisplay } from '../utils/status.js';
12
13
  const require = createRequire(import.meta.url);
13
14
  const pkg = require('../../package.json');
14
- const CLEAR_SCREEN = '\x1B[2J\x1B[0f';
15
+ // Alternate screen buffer escape sequences
16
+ const ENTER_ALT_SCREEN = '\x1b[?1049h\x1b[H';
17
+ const EXIT_ALT_SCREEN = '\x1b[?1049l';
15
18
  /**
16
19
  * Get TTY from ancestor processes
17
20
  */
@@ -42,19 +45,43 @@ function getTtyFromAncestors() {
42
45
  }
43
46
  return undefined;
44
47
  }
48
+ /**
49
+ * Run TUI with alternate screen buffer
50
+ */
51
+ async function runWithAltScreen(options = {}) {
52
+ process.stdout.write(ENTER_ALT_SCREEN);
53
+ // カーソルを非表示にして、より安定したレンダリングを行う
54
+ process.stdout.write('\x1b[?25l');
55
+ const instance = render(_jsx(Dashboard, { initialShowQr: options.qr }), { patchConsole: false });
56
+ // リサイズ時にInkの描画をクリアして再描画
57
+ const handleResize = () => {
58
+ instance.clear();
59
+ instance.rerender(_jsx(Dashboard, { initialShowQr: options.qr }));
60
+ };
61
+ process.stdout.on('resize', handleResize);
62
+ try {
63
+ await instance.waitUntilExit();
64
+ }
65
+ finally {
66
+ process.stdout.off('resize', handleResize);
67
+ // カーソルを再表示
68
+ process.stdout.write('\x1b[?25h');
69
+ process.stdout.write(EXIT_ALT_SCREEN);
70
+ }
71
+ }
45
72
  const program = new Command();
46
73
  program
47
74
  .name('ccm')
48
75
  .description('Claude Code Monitor - CLI-based session monitoring')
49
- .version(pkg.version);
76
+ .version(pkg.version)
77
+ .option('--qr', 'Show QR code for mobile access');
50
78
  program
51
79
  .command('watch')
52
80
  .alias('w')
53
81
  .description('Start the monitoring TUI')
54
- .action(() => {
55
- process.stdout.write(CLEAR_SCREEN);
56
- const { waitUntilExit } = render(_jsx(Dashboard, {}));
57
- waitUntilExit().catch(console.error);
82
+ .option('--qr', 'Show QR code for mobile access')
83
+ .action(async (options) => {
84
+ await runWithAltScreen({ qr: options.qr });
58
85
  });
59
86
  program
60
87
  .command('hook <event>')
@@ -98,12 +125,21 @@ program
98
125
  .action(async () => {
99
126
  await setupHooks();
100
127
  });
128
+ program
129
+ .command('serve')
130
+ .alias('s')
131
+ .description('Start web server for mobile monitoring')
132
+ .option('-p, --port <port>', 'Port number', '3456')
133
+ .action(async (options) => {
134
+ const port = parseInt(options.port, 10);
135
+ await startServer(port);
136
+ });
101
137
  /**
102
- * Default action (when launched without arguments)
138
+ * Default action (when launched without arguments or with --qr only)
103
139
  * - Run setup if not configured
104
140
  * - Launch monitor if already configured
105
141
  */
106
- async function defaultAction() {
142
+ async function defaultAction(options = {}) {
107
143
  if (!isHooksConfigured()) {
108
144
  console.log('Initial setup required.\n');
109
145
  await setupHooks();
@@ -115,14 +151,11 @@ async function defaultAction() {
115
151
  console.log('');
116
152
  }
117
153
  // Launch monitor
118
- process.stdout.write(CLEAR_SCREEN);
119
- const { waitUntilExit } = render(_jsx(Dashboard, {}));
120
- await waitUntilExit();
121
- }
122
- // Default action when executed without commands
123
- if (process.argv.length === 2) {
124
- defaultAction().catch(console.error);
125
- }
126
- else {
127
- program.parse();
154
+ await runWithAltScreen(options);
128
155
  }
156
+ // Handle default action (no subcommand)
157
+ program.action(async () => {
158
+ const options = program.opts();
159
+ await defaultAction({ qr: options.qr });
160
+ });
161
+ program.parse();
@@ -1,3 +1,8 @@
1
1
  import type React from 'react';
2
- export declare function Dashboard(): React.ReactElement;
2
+ interface DashboardProps {
3
+ /** Override default QR code visibility (e.g., from --qr CLI flag) */
4
+ initialShowQr?: boolean;
5
+ }
6
+ export declare function Dashboard({ initialShowQr }: DashboardProps): React.ReactElement;
7
+ export {};
3
8
  //# sourceMappingURL=Dashboard.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"Dashboard.d.ts","sourceRoot":"","sources":["../../src/components/Dashboard.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAS/B,wBAAgB,SAAS,IAAI,KAAK,CAAC,YAAY,CAoH9C"}
1
+ {"version":3,"file":"Dashboard.d.ts","sourceRoot":"","sources":["../../src/components/Dashboard.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAW/B,UAAU,cAAc;IACtB,qEAAqE;IACrE,aAAa,CAAC,EAAE,OAAO,CAAC;CACzB;AAED,wBAAgB,SAAS,CAAC,EAAE,aAAa,EAAE,EAAE,cAAc,GAAG,KAAK,CAAC,YAAY,CA8L/E"}
@@ -1,15 +1,44 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { Box, Text, useApp, useInput } from 'ink';
3
- import { useMemo, useState } from 'react';
2
+ import { Box, Text, useApp, useInput, useStdout } from 'ink';
3
+ import { useEffect, useMemo, useState } from 'react';
4
+ import { MIN_TERMINAL_HEIGHT_FOR_QR } from '../constants.js';
5
+ import { useServer } from '../hooks/useServer.js';
4
6
  import { useSessions } from '../hooks/useSessions.js';
5
- import { clearSessions } from '../store/file-store.js';
7
+ import { clearSessions, readSettings, writeSettings } from '../store/file-store.js';
6
8
  import { focusSession } from '../utils/focus.js';
7
9
  import { SessionCard } from './SessionCard.js';
8
10
  const QUICK_SELECT_KEYS = ['1', '2', '3', '4', '5', '6', '7', '8', '9'];
9
- export function Dashboard() {
11
+ export function Dashboard({ initialShowQr }) {
10
12
  const { sessions, loading, error } = useSessions();
13
+ const { url, qrCode, loading: serverLoading } = useServer();
11
14
  const [selectedIndex, setSelectedIndex] = useState(0);
12
15
  const { exit } = useApp();
16
+ const { stdout } = useStdout();
17
+ // QRコード表示状態: --qrフラグが指定された場合はそれを優先、なければ設定を読み込む
18
+ const [qrCodeUserPref, setQrCodeUserPref] = useState(() => initialShowQr ?? readSettings().qrCodeVisible);
19
+ const [terminalHeight, setTerminalHeight] = useState(stdout?.rows ?? 40);
20
+ // Monitor terminal size changes
21
+ useEffect(() => {
22
+ if (!stdout)
23
+ return;
24
+ const handleResize = () => {
25
+ setTerminalHeight(stdout.rows ?? 40);
26
+ };
27
+ // Initial size
28
+ setTerminalHeight(stdout.rows ?? 40);
29
+ stdout.on('resize', handleResize);
30
+ return () => {
31
+ stdout.off('resize', handleResize);
32
+ };
33
+ }, [stdout]);
34
+ // QR code visibility: user preference AND terminal has enough space
35
+ const canShowQr = terminalHeight >= MIN_TERMINAL_HEIGHT_FOR_QR;
36
+ const qrCodeVisible = qrCodeUserPref && canShowQr;
37
+ const toggleQrCode = () => {
38
+ const newValue = !qrCodeUserPref;
39
+ setQrCodeUserPref(newValue);
40
+ writeSettings({ qrCodeVisible: newValue });
41
+ };
13
42
  const focusSessionByIndex = (index) => {
14
43
  const session = sessions[index];
15
44
  if (session?.tty) {
@@ -51,6 +80,11 @@ export function Dashboard() {
51
80
  if (input === 'c') {
52
81
  clearSessions();
53
82
  setSelectedIndex(0);
83
+ return;
84
+ }
85
+ if (input === 'h') {
86
+ toggleQrCode();
87
+ return;
54
88
  }
55
89
  });
56
90
  if (loading) {
@@ -60,5 +94,5 @@ export function Dashboard() {
60
94
  return _jsxs(Text, { color: "red", children: ["Error: ", error.message] });
61
95
  }
62
96
  const { running, waiting_input: waitingInput, stopped } = statusCounts;
63
- return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { borderStyle: "round", borderColor: "cyan", paddingX: 1, children: [_jsx(Text, { bold: true, color: "cyan", children: "Claude Code Monitor" }), _jsx(Text, { dimColor: true, children: " \u2502 " }), _jsxs(Text, { color: "green", children: ["\u25CF ", running] }), _jsx(Text, { dimColor: true, children: " " }), _jsxs(Text, { color: "yellow", children: ["\u25D0 ", waitingInput] }), _jsx(Text, { dimColor: true, children: " " }), _jsxs(Text, { color: "cyan", children: ["\u2713 ", stopped] })] }), _jsx(Box, { flexDirection: "column", borderStyle: "round", borderColor: "gray", marginTop: 1, paddingX: 1, paddingY: 0, children: sessions.length === 0 ? (_jsx(Box, { paddingY: 1, children: _jsx(Text, { dimColor: true, children: "No active sessions" }) })) : (sessions.map((session, index) => (_jsx(SessionCard, { session: session, index: index, isSelected: index === selectedIndex }, `${session.session_id}:${session.tty || ''}`)))) }), _jsxs(Box, { marginTop: 1, justifyContent: "center", gap: 1, children: [_jsx(Text, { dimColor: true, children: "[\u2191\u2193]Select" }), _jsx(Text, { dimColor: true, children: "[Enter]Focus" }), _jsx(Text, { dimColor: true, children: "[1-9]Quick" }), _jsx(Text, { dimColor: true, children: "[c]Clear" }), _jsx(Text, { dimColor: true, children: "[q]Quit" })] })] }));
97
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingX: 1, children: [_jsxs(Box, { children: [_jsx(Text, { bold: true, color: "cyan", children: "Claude Code Monitor" }), _jsx(Text, { dimColor: true, children: " " }), _jsxs(Text, { color: "gray", children: ["\u25CF ", running] }), _jsx(Text, { dimColor: true, children: " " }), _jsxs(Text, { color: "yellow", children: ["\u25D0 ", waitingInput] }), _jsx(Text, { dimColor: true, children: " " }), _jsxs(Text, { color: "green", children: ["\u2713 ", stopped] })] }), _jsx(Box, { flexDirection: "column", marginTop: 1, children: sessions.length === 0 ? (_jsx(Box, { children: _jsx(Text, { dimColor: true, children: "No active sessions" }) })) : (sessions.map((session, index) => (_jsx(SessionCard, { session: session, index: index, isSelected: index === selectedIndex }, `${session.session_id}:${session.tty || ''}`)))) })] }), _jsxs(Box, { marginTop: 1, justifyContent: "center", gap: 1, children: [_jsx(Text, { dimColor: true, children: "[\u2191\u2193]Select" }), _jsx(Text, { dimColor: true, children: "[Enter]Focus" }), _jsx(Text, { dimColor: true, children: "[1-9]Quick" }), _jsx(Text, { dimColor: true, children: "[c]Clear" }), _jsxs(Text, { dimColor: true, children: ["[h]", qrCodeUserPref ? 'Hide' : 'Show', "URL"] }), _jsx(Text, { dimColor: true, children: "[q]Quit" })] }), !serverLoading && url && !qrCodeUserPref && (_jsx(Box, { marginTop: 1, borderStyle: "round", borderColor: "gray", paddingX: 1, children: _jsx(Text, { color: "white", children: "\uD83D\uDCF1 Web UI available. Press [h] to show QR code for mobile access. (Same Wi-Fi required)" }) })), !serverLoading && url && qrCodeUserPref && (_jsxs(Box, { marginTop: 1, paddingX: 1, children: [qrCodeVisible && qrCode && (_jsx(Box, { flexShrink: 0, children: _jsx(Text, { children: qrCode }) })), _jsxs(Box, { flexDirection: "column", marginLeft: qrCodeVisible && qrCode ? 2 : 0, justifyContent: "center", children: [_jsx(Text, { bold: true, color: "magenta", children: "Web UI" }), _jsx(Text, { dimColor: true, children: url }), _jsx(Text, { dimColor: true, children: "Scan QR code to monitor sessions from your phone." }), _jsx(Text, { dimColor: true, children: "Tap a session to focus its terminal on this Mac." }), _jsx(Text, { color: "yellow", children: "\u26A0 Do not share this URL with others." }), !canShowQr && _jsx(Text, { color: "yellow", children: "\u26A0 Resize window to show QR code" })] })] }))] }));
64
98
  }
@@ -1 +1 @@
1
- {"version":3,"file":"SessionCard.d.ts","sourceRoot":"","sources":["../../src/components/SessionCard.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAE/B,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,mBAAmB,CAAC;AAKjD,UAAU,gBAAgB;IACxB,OAAO,EAAE,OAAO,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;IACd,UAAU,EAAE,OAAO,CAAC;CACrB;AAOD,eAAO,MAAM,WAAW,8CAiCtB,CAAC"}
1
+ {"version":3,"file":"SessionCard.d.ts","sourceRoot":"","sources":["../../src/components/SessionCard.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAE/B,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,mBAAmB,CAAC;AAIjD,UAAU,gBAAgB;IACxB,OAAO,EAAE,OAAO,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;IACd,UAAU,EAAE,OAAO,CAAC;CACrB;AAOD,eAAO,MAAM,WAAW,8CAyBtB,CAAC"}
@@ -1,9 +1,8 @@
1
- import { jsxs as _jsxs, jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
1
+ import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
2
2
  import { Box, Text } from 'ink';
3
3
  import { memo } from 'react';
4
4
  import { getStatusDisplay } from '../utils/status.js';
5
5
  import { formatRelativeTime } from '../utils/time.js';
6
- import { Spinner } from './Spinner.js';
7
6
  function abbreviateHomePath(path) {
8
7
  if (!path)
9
8
  return '(unknown)';
@@ -13,6 +12,5 @@ export const SessionCard = memo(function SessionCard({ session, index, isSelecte
13
12
  const { symbol, color, label } = getStatusDisplay(session.status);
14
13
  const dir = abbreviateHomePath(session.cwd);
15
14
  const relativeTime = formatRelativeTime(session.updated_at);
16
- const isRunning = session.status === 'running';
17
- return (_jsxs(Box, { paddingX: 1, children: [_jsxs(Text, { color: isSelected ? 'cyan' : undefined, bold: isSelected, children: [isSelected ? '>' : ' ', " [", index + 1, "]"] }), _jsx(Text, { children: " " }), _jsx(Box, { width: 10, children: isRunning ? (_jsxs(_Fragment, { children: [_jsx(Spinner, { color: "green" }), _jsxs(Text, { color: color, children: [" ", label] })] })) : (_jsxs(Text, { color: color, children: [symbol, " ", label] })) }), _jsx(Text, { children: " " }), _jsx(Text, { dimColor: true, children: relativeTime.padEnd(8) }), _jsx(Text, { color: isSelected ? 'white' : 'gray', children: dir })] }));
15
+ return (_jsxs(Box, { paddingX: 1, children: [_jsxs(Text, { color: isSelected ? 'cyan' : undefined, bold: isSelected, children: [isSelected ? '>' : ' ', " [", index + 1, "]"] }), _jsx(Text, { children: " " }), _jsx(Box, { width: 10, children: _jsxs(Text, { color: color, children: [symbol, " ", label] }) }), _jsx(Text, { children: " " }), _jsx(Text, { dimColor: true, children: relativeTime.padEnd(8) }), _jsx(Text, { color: isSelected ? 'white' : 'gray', children: dir })] }));
18
16
  });
@@ -2,7 +2,7 @@ import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { Text } from 'ink';
3
3
  import { useSyncExternalStore } from 'react';
4
4
  const SPINNER_FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
5
- const FRAME_INTERVAL_MS = 120; // Slightly slower for less CPU usage
5
+ const FRAME_INTERVAL_MS = 200; // 200msでチラつきを軽減
6
6
  // Shared global spinner state - only ONE timer for all spinners
7
7
  let globalFrame = 0;
8
8
  let subscriberCount = 0;
@@ -3,8 +3,6 @@
3
3
  */
4
4
  /** Package name used for npx commands */
5
5
  export declare const PACKAGE_NAME = "claude-code-monitor";
6
- /** Session timeout in milliseconds (30 minutes) */
7
- export declare const SESSION_TIMEOUT_MS: number;
8
6
  /** TTY cache TTL in milliseconds (30 seconds) */
9
7
  export declare const TTY_CACHE_TTL_MS = 30000;
10
8
  /** Maximum number of entries in TTY cache */
@@ -15,6 +13,11 @@ export declare const SESSION_UPDATE_DEBOUNCE_MS = 150;
15
13
  export declare const WRITE_DEBOUNCE_MS = 100;
16
14
  /** Periodic refresh interval for timeout detection in milliseconds (60 seconds) */
17
15
  export declare const SESSION_REFRESH_INTERVAL_MS = 60000;
16
+ /**
17
+ * QRコード表示に必要な最小ターミナル高さ
18
+ * Header(1) + Sessions(3) + Shortcuts(2) + WebUI with QR(16) = 22行
19
+ */
20
+ export declare const MIN_TERMINAL_HEIGHT_FOR_QR = 22;
18
21
  /** Hook event types supported by Claude Code */
19
22
  export declare const HOOK_EVENTS: readonly ["UserPromptSubmit", "PreToolUse", "PostToolUse", "Notification", "Stop"];
20
23
  export type HookEventName = (typeof HOOK_EVENTS)[number];
@@ -1 +1 @@
1
- {"version":3,"file":"constants.d.ts","sourceRoot":"","sources":["../src/constants.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,yCAAyC;AACzC,eAAO,MAAM,YAAY,wBAAwB,CAAC;AAElD,mDAAmD;AACnD,eAAO,MAAM,kBAAkB,QAAiB,CAAC;AAEjD,iDAAiD;AACjD,eAAO,MAAM,gBAAgB,QAAS,CAAC;AAEvC,6CAA6C;AAC7C,eAAO,MAAM,kBAAkB,MAAM,CAAC;AAEtC,6DAA6D;AAC7D,eAAO,MAAM,0BAA0B,MAAM,CAAC;AAE9C,0DAA0D;AAC1D,eAAO,MAAM,iBAAiB,MAAM,CAAC;AAErC,mFAAmF;AACnF,eAAO,MAAM,2BAA2B,QAAS,CAAC;AAElD,gDAAgD;AAChD,eAAO,MAAM,WAAW,oFAMd,CAAC;AAEX,MAAM,MAAM,aAAa,GAAG,CAAC,OAAO,WAAW,CAAC,CAAC,MAAM,CAAC,CAAC"}
1
+ {"version":3,"file":"constants.d.ts","sourceRoot":"","sources":["../src/constants.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,yCAAyC;AACzC,eAAO,MAAM,YAAY,wBAAwB,CAAC;AAElD,iDAAiD;AACjD,eAAO,MAAM,gBAAgB,QAAS,CAAC;AAEvC,6CAA6C;AAC7C,eAAO,MAAM,kBAAkB,MAAM,CAAC;AAEtC,6DAA6D;AAC7D,eAAO,MAAM,0BAA0B,MAAM,CAAC;AAE9C,0DAA0D;AAC1D,eAAO,MAAM,iBAAiB,MAAM,CAAC;AAErC,mFAAmF;AACnF,eAAO,MAAM,2BAA2B,QAAS,CAAC;AAElD;;;GAGG;AACH,eAAO,MAAM,0BAA0B,KAAK,CAAC;AAE7C,gDAAgD;AAChD,eAAO,MAAM,WAAW,oFAMd,CAAC;AAEX,MAAM,MAAM,aAAa,GAAG,CAAC,OAAO,WAAW,CAAC,CAAC,MAAM,CAAC,CAAC"}
package/dist/constants.js CHANGED
@@ -3,8 +3,6 @@
3
3
  */
4
4
  /** Package name used for npx commands */
5
5
  export const PACKAGE_NAME = 'claude-code-monitor';
6
- /** Session timeout in milliseconds (30 minutes) */
7
- export const SESSION_TIMEOUT_MS = 30 * 60 * 1000;
8
6
  /** TTY cache TTL in milliseconds (30 seconds) */
9
7
  export const TTY_CACHE_TTL_MS = 30_000;
10
8
  /** Maximum number of entries in TTY cache */
@@ -15,6 +13,11 @@ export const SESSION_UPDATE_DEBOUNCE_MS = 150;
15
13
  export const WRITE_DEBOUNCE_MS = 100;
16
14
  /** Periodic refresh interval for timeout detection in milliseconds (60 seconds) */
17
15
  export const SESSION_REFRESH_INTERVAL_MS = 60_000;
16
+ /**
17
+ * QRコード表示に必要な最小ターミナル高さ
18
+ * Header(1) + Sessions(3) + Shortcuts(2) + WebUI with QR(16) = 22行
19
+ */
20
+ export const MIN_TERMINAL_HEIGHT_FOR_QR = 22;
18
21
  /** Hook event types supported by Claude Code */
19
22
  export const HOOK_EVENTS = [
20
23
  'UserPromptSubmit',
@@ -1 +1 @@
1
- {"version":3,"file":"handler.d.ts","sourceRoot":"","sources":["../../src/hook/handler.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAa,aAAa,EAAE,MAAM,mBAAmB,CAAC;AAGlE,gBAAgB;AAChB,eAAO,MAAM,iBAAiB,EAAE,WAAW,CAAC,MAAM,CAMhD,CAAC;AAEH,gBAAgB;AAChB,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,IAAI,aAAa,CAExE;AAED,gBAAgB;AAChB,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,MAAM,CAEhE;AAED,wBAAsB,eAAe,CAAC,SAAS,EAAE,MAAM,EAAE,GAAG,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAsDpF"}
1
+ {"version":3,"file":"handler.d.ts","sourceRoot":"","sources":["../../src/hook/handler.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAa,aAAa,EAAE,MAAM,mBAAmB,CAAC;AAKlE,gBAAgB;AAChB,eAAO,MAAM,iBAAiB,EAAE,WAAW,CAAC,MAAM,CAMhD,CAAC;AAEH,gBAAgB;AAChB,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,IAAI,aAAa,CAExE;AAED,gBAAgB;AAChB,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,MAAM,CAEhE;AAED,wBAAsB,eAAe,CAAC,SAAS,EAAE,MAAM,EAAE,GAAG,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAoDpF"}