claude-code-monitor 1.0.4 → 1.1.1

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 +28 -0
  2. package/README.md +98 -103
  3. package/dist/bin/ccm.js +38 -16
  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,34 @@ 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
+
8
36
  ## [1.0.4] - 2026-01-18
9
37
 
10
38
  ### Fixed
package/README.md CHANGED
@@ -1,167 +1,144 @@
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)
58
+ ### Run
64
59
 
65
60
  ```bash
66
- npx claude-code-monitor
61
+ ccm
67
62
  ```
68
63
 
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
- ---
64
+ On first run, it automatically sets up hooks and launches the monitor.
72
65
 
73
- ## Quick Start
66
+ ### Mobile Access
74
67
 
75
- ```bash
76
- ccm
77
- ```
68
+ 1. Press `h` to show QR code (default port: 3456)
69
+ 2. Scan with your smartphone (same Wi-Fi required)
78
70
 
79
- On first run, it automatically sets up hooks and launches the monitor.
71
+ > If port 3456 is in use, an available port is automatically selected.
80
72
 
81
73
  ---
82
74
 
83
- ## 📖 Commands
75
+ ## 📖 Usage
76
+
77
+ ### Commands
84
78
 
85
79
  | Command | Alias | Description |
86
80
  |---------|-------|-------------|
87
- | `ccm` | - | Launch monitor TUI (auto-setup if not configured) |
88
- | `ccm watch` | `ccm w` | Launch monitor TUI |
81
+ | `ccm` | - | Launch monitor (auto-setup if needed) |
82
+ | `ccm watch` | `ccm w` | Launch monitor |
83
+ | `ccm serve` | `ccm s` | Start mobile web server only |
89
84
  | `ccm setup` | - | Configure Claude Code hooks |
90
85
  | `ccm list` | `ccm ls` | List sessions |
91
86
  | `ccm clear` | - | Clear all sessions |
92
- | `ccm --version` | `ccm -V` | Show version |
93
- | `ccm --help` | `ccm -h` | Show help |
94
87
 
95
- ---
96
-
97
- ## ⌨️ Keybindings (watch mode)
88
+ ### Keybindings
98
89
 
99
90
  | Key | Action |
100
91
  |-----|--------|
101
92
  | `↑` / `k` | Move up |
102
93
  | `↓` / `j` | Move down |
103
94
  | `Enter` / `f` | Focus selected session |
104
- | `1-9` | Quick select & focus by number |
95
+ | `1-9` | Quick select & focus |
96
+ | `h` | Show/Hide QR code |
105
97
  | `c` | Clear all sessions |
106
98
  | `q` / `Esc` | Quit |
107
99
 
108
- ---
109
-
110
- ## 🎨 Status Icons
100
+ ### Status Icons
111
101
 
112
102
  | Icon | Status | Description |
113
103
  |------|--------|-------------|
114
104
  | `●` | Running | Claude Code is processing |
115
- | `◐` | Waiting | Waiting for user input (e.g., permission prompt) |
105
+ | `◐` | Waiting | Waiting for user input |
116
106
  | `✓` | Done | Session ended |
117
107
 
118
108
  ---
119
109
 
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) |
110
+ ## 📱 Mobile Web Interface
129
111
 
130
- > **Note**: Other terminals (Alacritty, kitty, Warp, etc.) can use monitoring but focus feature is not supported.
131
-
132
- ---
112
+ Monitor and control Claude Code sessions from your smartphone.
133
113
 
134
- ## 💾 Data Storage
114
+ ### Features
135
115
 
136
- Session data is stored in `~/.claude-monitor/sessions.json`.
116
+ - Real-time session status via WebSocket
117
+ - View latest Claude messages
118
+ - Focus terminal sessions remotely
119
+ - Send text messages to terminal (multi-line supported)
120
+ - Swipe-to-close gesture on modal
121
+ - Warning display for dangerous commands
137
122
 
138
- ### What is stored
123
+ ### Security
139
124
 
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 |
125
+ > **Important**: Your smartphone and Mac must be on the **same Wi-Fi network**.
147
126
 
148
- Data is automatically removed after 30 minutes of inactivity or when the terminal session ends.
127
+ - **Token Authentication** - A unique token is generated for authentication
128
+ - **Local Network Only** - Not accessible from the internet
129
+ - **Do not share the URL** - Treat it like a password
149
130
 
150
131
  ---
151
132
 
152
- ## 📦 Programmatic Usage
153
-
154
- Can also be used as a library:
133
+ ## 🖥️ Supported Terminals
155
134
 
156
- ```typescript
157
- import { getSessions, getStatusDisplay } from 'claude-code-monitor';
135
+ | Terminal | Focus Support | Notes |
136
+ |----------|--------------|-------|
137
+ | iTerm2 | ✅ Full | TTY-based targeting |
138
+ | Terminal.app | ✅ Full | TTY-based targeting |
139
+ | Ghostty | ⚠️ Limited | App activation only |
158
140
 
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
- ```
141
+ > Other terminals can use monitoring, but focus feature is not supported.
165
142
 
166
143
  ---
167
144
 
@@ -170,48 +147,66 @@ for (const session of sessions) {
170
147
  ### Sessions not showing
171
148
 
172
149
  1. Run `ccm setup` to verify hook configuration
173
- 2. Check if `~/.claude/settings.json` contains hook settings
150
+ 2. Check `~/.claude/settings.json` for hook settings
174
151
  3. Restart Claude Code
175
152
 
176
- ```bash
177
- # Check configuration
178
- cat ~/.claude/settings.json | grep ccm
179
- ```
180
-
181
153
  ### Focus not working
182
154
 
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
155
+ 1. Verify you're using a supported terminal
156
+ 2. Check System Preferences > Privacy & Security > Accessibility
186
157
 
187
- ### Reset session data
158
+ ### Reset data
188
159
 
189
160
  ```bash
190
161
  ccm clear
191
- # or
192
- rm ~/.claude-monitor/sessions.json
193
162
  ```
194
163
 
195
164
  ---
196
165
 
197
166
  ## 🔒 Security
198
167
 
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
168
+ - **No data sent to external servers** - All data stays on your machine
169
+ - Hook registration modifies `~/.claude/settings.json`
170
+ - Focus feature uses AppleScript for terminal control
171
+ - Mobile Web uses token authentication on local network only
172
+
173
+ ---
174
+
175
+ ## 📦 Programmatic Usage
176
+
177
+ ```typescript
178
+ import { getSessions, focusSession } from 'claude-code-monitor';
179
+
180
+ const sessions = getSessions();
181
+ if (sessions[0]?.tty) {
182
+ focusSession(sessions[0].tty);
183
+ }
184
+ ```
202
185
 
203
186
  ---
204
187
 
205
188
  ## ⚠️ Disclaimer
206
189
 
207
- This is an unofficial community tool and is not affiliated with, endorsed by, or associated with Anthropic.
190
+ This is an unofficial community tool and is not affiliated with Anthropic.
208
191
  "Claude" and "Claude Code" are trademarks of Anthropic.
209
192
 
210
193
  ---
211
194
 
195
+ ## 🐛 Issues
196
+
197
+ Found a bug? [Open an issue](https://github.com/onikan27/claude-code-monitor/issues)
198
+
199
+ ---
200
+
201
+ ## 🤝 Contributing
202
+
203
+ Contributions are welcome! Please open an issue or submit a PR.
204
+
205
+ ---
206
+
212
207
  ## 📝 Changelog
213
208
 
214
- See [CHANGELOG.md](./CHANGELOG.md) for a list of changes.
209
+ See [CHANGELOG.md](./CHANGELOG.md) for details.
215
210
 
216
211
  ---
217
212
 
package/dist/bin/ccm.js CHANGED
@@ -6,6 +6,7 @@ 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';
@@ -47,13 +48,24 @@ function getTtyFromAncestors() {
47
48
  /**
48
49
  * Run TUI with alternate screen buffer
49
50
  */
50
- async function runWithAltScreen(renderFn) {
51
+ async function runWithAltScreen(options = {}) {
51
52
  process.stdout.write(ENTER_ALT_SCREEN);
52
- const { waitUntilExit } = renderFn();
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);
53
62
  try {
54
- await waitUntilExit();
63
+ await instance.waitUntilExit();
55
64
  }
56
65
  finally {
66
+ process.stdout.off('resize', handleResize);
67
+ // カーソルを再表示
68
+ process.stdout.write('\x1b[?25h');
57
69
  process.stdout.write(EXIT_ALT_SCREEN);
58
70
  }
59
71
  }
@@ -61,13 +73,15 @@ const program = new Command();
61
73
  program
62
74
  .name('ccm')
63
75
  .description('Claude Code Monitor - CLI-based session monitoring')
64
- .version(pkg.version);
76
+ .version(pkg.version)
77
+ .option('--qr', 'Show QR code for mobile access');
65
78
  program
66
79
  .command('watch')
67
80
  .alias('w')
68
81
  .description('Start the monitoring TUI')
69
- .action(async () => {
70
- await runWithAltScreen(() => render(_jsx(Dashboard, {})));
82
+ .option('--qr', 'Show QR code for mobile access')
83
+ .action(async (options) => {
84
+ await runWithAltScreen({ qr: options.qr });
71
85
  });
72
86
  program
73
87
  .command('hook <event>')
@@ -111,12 +125,21 @@ program
111
125
  .action(async () => {
112
126
  await setupHooks();
113
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
+ });
114
137
  /**
115
- * Default action (when launched without arguments)
138
+ * Default action (when launched without arguments or with --qr only)
116
139
  * - Run setup if not configured
117
140
  * - Launch monitor if already configured
118
141
  */
119
- async function defaultAction() {
142
+ async function defaultAction(options = {}) {
120
143
  if (!isHooksConfigured()) {
121
144
  console.log('Initial setup required.\n');
122
145
  await setupHooks();
@@ -128,12 +151,11 @@ async function defaultAction() {
128
151
  console.log('');
129
152
  }
130
153
  // Launch monitor
131
- await runWithAltScreen(() => render(_jsx(Dashboard, {})));
132
- }
133
- // Default action when executed without commands
134
- if (process.argv.length === 2) {
135
- defaultAction().catch(console.error);
136
- }
137
- else {
138
- program.parse();
154
+ await runWithAltScreen(options);
139
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"}