vibe-pomo 0.2.0 → 0.2.2
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 +6 -2
- package/README.zh.md +6 -2
- package/bin/pomodoro.mjs +64 -42
- package/install.mjs +3 -0
- package/package.json +1 -1
- package/src/daemon/index.mjs +8 -1
- package/src/daemon/ipc.mjs +1 -1
- package/src/daemon/session.mjs +1 -0
- package/src/hooks/notification.mjs +4 -8
- package/src/hooks/preToolUse.mjs +4 -8
- package/src/hooks/stop.mjs +5 -8
- package/src/shared/findActiveSession.mjs +27 -0
- package/src/tui/timer/TimerApp.tsx +12 -5
package/README.md
CHANGED
|
@@ -27,6 +27,8 @@ Every session is logged — what the agent did, what you did — giving you a cl
|
|
|
27
27
|
|
|
28
28
|
**Prerequisites:** Node.js 20+, Claude Code CLI
|
|
29
29
|
|
|
30
|
+
**Recommended:** run Claude Code inside `tmux`. That is the primary supported interactive timer experience: vibe-pomo uses a `tmux` popup to cover the current pane during a Pomodoro. Outside `tmux`, sessions still run, but timer UI falls back to headless mode.
|
|
31
|
+
|
|
30
32
|
```bash
|
|
31
33
|
npm i -g vibe-pomo
|
|
32
34
|
pomodoro install
|
|
@@ -71,6 +73,8 @@ pomodoro daemon
|
|
|
71
73
|
|
|
72
74
|
### 2. Start a session
|
|
73
75
|
|
|
76
|
+
For the intended covered-screen workflow, open Claude Code inside a `tmux` session before running `/pomodoro ...`.
|
|
77
|
+
|
|
74
78
|
```bash
|
|
75
79
|
# From Claude Code (recommended)
|
|
76
80
|
/pomodoro 25m Refactor the auth module
|
|
@@ -80,7 +84,7 @@ pomodoro start 25m Refactor the auth module
|
|
|
80
84
|
pomodoro start Refactor the auth module # uses default duration
|
|
81
85
|
```
|
|
82
86
|
|
|
83
|
-
|
|
87
|
+
If Claude Code is running inside `tmux`, vibe-pomo opens a popup over the current pane so the ongoing task stays covered until you end or break the session. Outside `tmux`, it stays headless: the session still runs, but visualization remains in the daemon dashboard and stats view instead of opening another terminal window.
|
|
84
88
|
|
|
85
89
|
```
|
|
86
90
|
🍅 Pomodoro
|
|
@@ -163,7 +167,7 @@ Recent Sessions
|
|
|
163
167
|
|--------|--------|-------------|
|
|
164
168
|
| `defaultDurationMs` | ms | Default session duration (25 min = `1500000`) |
|
|
165
169
|
| `decisionStrategy` | `"wait"` / `"break"` | When the agent is blocked: wait silently until you end the session (default), or end immediately |
|
|
166
|
-
| `terminalEmulator` | `"auto"` /
|
|
170
|
+
| `terminalEmulator` | `"auto"` / `"tmux"` / `"none"` | Timer surface. `"tmux"` opens a popup overlay, `"none"` forces headless mode, and `"auto"` uses the popup only when `$TMUX` is set. |
|
|
167
171
|
| `soundOnOvertime` | bool | Play a sound when the timer hits zero |
|
|
168
172
|
|
|
169
173
|
---
|
package/README.zh.md
CHANGED
|
@@ -84,6 +84,8 @@ What did you do during this session?
|
|
|
84
84
|
|
|
85
85
|
**前置条件:** Node.js 20+、Claude Code CLI
|
|
86
86
|
|
|
87
|
+
**推荐条件:** 请在 `tmux` 里运行 Claude Code。这是 vibe-pomo 当前主推的交互方式:番茄钟会通过 `tmux popup` 覆盖当前 pane。非 `tmux` 环境下,会话仍然会正常运行,但计时器 UI 会退化为无界面模式。
|
|
88
|
+
|
|
87
89
|
```bash
|
|
88
90
|
npm install -g vibe-pomo
|
|
89
91
|
pomodoro install
|
|
@@ -111,6 +113,8 @@ pomodoro daemon
|
|
|
111
113
|
|
|
112
114
|
### 2. 开始一个会话
|
|
113
115
|
|
|
116
|
+
如果你希望获得“盖住当前 Claude Code 窗口”的预期体验,请先在 `tmux` session 里打开 Claude Code,再执行 `/pomodoro ...`。
|
|
117
|
+
|
|
114
118
|
```bash
|
|
115
119
|
# 在 Claude Code 中(推荐)
|
|
116
120
|
/pomodoro 25m Refactor the auth module
|
|
@@ -120,7 +124,7 @@ pomodoro start 25m Refactor the auth module
|
|
|
120
124
|
pomodoro start Refactor the auth module # 使用默认时长
|
|
121
125
|
```
|
|
122
126
|
|
|
123
|
-
|
|
127
|
+
如果 Claude Code 跑在 `tmux` 里,vibe-pomo 会在当前 pane 上弹出一个覆盖式 popup,把正在进行的任务界面盖住,直到你结束或中断番茄钟。非 `tmux` 环境下则保持无界面模式:会话照常运行,但只通过 daemon 仪表板和统计视图展示,不再额外弹出终端窗口。
|
|
124
128
|
|
|
125
129
|
### 3. 会话进行中
|
|
126
130
|
|
|
@@ -177,7 +181,7 @@ Recent Sessions
|
|
|
177
181
|
|------|------|------|
|
|
178
182
|
| `defaultDurationMs` | 毫秒数 | 默认会话时长(25 分钟 = `1500000`) |
|
|
179
183
|
| `decisionStrategy` | `"wait"` / `"break"` | 代理被阻塞时的策略:静默等待直到你结束会话(默认),或立即结束 |
|
|
180
|
-
| `terminalEmulator` | `"auto"` /
|
|
184
|
+
| `terminalEmulator` | `"auto"` / `"tmux"` / `"none"` | 计时器展示方式。`"tmux"` 打开覆盖式 popup,`"none"` 强制无界面模式,`"auto"` 仅在检测到 `$TMUX` 时使用 popup |
|
|
181
185
|
| `soundOnOvertime` | 布尔值 | 计时归零时播放提示音 |
|
|
182
186
|
|
|
183
187
|
---
|
package/bin/pomodoro.mjs
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { spawn } from 'node:child_process'
|
|
3
|
+
import { execFileSync } from 'node:child_process'
|
|
3
4
|
import { fileURLToPath } from 'node:url'
|
|
4
5
|
import { dirname, join } from 'node:path'
|
|
5
6
|
import { readLock, removeLock } from '../src/shared/lockfile.mjs'
|
|
@@ -108,14 +109,16 @@ async function cmdStart(args) {
|
|
|
108
109
|
spawnWatcher(resp.sessionId)
|
|
109
110
|
}
|
|
110
111
|
|
|
111
|
-
//
|
|
112
|
+
// Only launch an interactive timer UI when we can safely cover the current
|
|
113
|
+
// Claude Code pane with a tmux popup. Otherwise stay headless and let the
|
|
114
|
+
// daemon/dashboard own visualization so the conversation flow is not disturbed.
|
|
112
115
|
const timerEntry = join(ROOT, 'src', 'tui', 'timer', 'index.tsx')
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
}
|
|
116
|
+
await launchTerminal({
|
|
117
|
+
tsx: TSX,
|
|
118
|
+
entryFile: timerEntry,
|
|
119
|
+
sessionId: resp.sessionId,
|
|
120
|
+
termPref: config.terminalEmulator,
|
|
121
|
+
})
|
|
119
122
|
}
|
|
120
123
|
|
|
121
124
|
async function cmdStop(args) {
|
|
@@ -179,46 +182,24 @@ async function cmdStopDaemon() {
|
|
|
179
182
|
|
|
180
183
|
// ── Terminal launch helpers ───────────────────────────────────────────────────
|
|
181
184
|
|
|
182
|
-
async function launchTerminal(tsx, entryFile, sessionId, termPref) {
|
|
183
|
-
const
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
for (const term of terminals) {
|
|
188
|
-
const ok = await trySpawn(term, tsx, entryFile, sessionId)
|
|
189
|
-
if (ok) return true
|
|
190
|
-
}
|
|
191
|
-
return false
|
|
185
|
+
async function launchTerminal({ tsx, entryFile, sessionId, termPref }) {
|
|
186
|
+
const mode = resolveTimerMode(termPref)
|
|
187
|
+
if (mode !== 'tmux-popup') return false
|
|
188
|
+
return trySpawn(mode, tsx, entryFile, sessionId)
|
|
192
189
|
}
|
|
193
190
|
|
|
194
|
-
function
|
|
195
|
-
const
|
|
196
|
-
if (
|
|
197
|
-
|
|
198
|
-
return
|
|
191
|
+
function resolveTimerMode(termPref) {
|
|
192
|
+
const pref = termPref ?? 'auto'
|
|
193
|
+
if (pref === 'none' || pref === 'headless') return 'none'
|
|
194
|
+
if ((pref === 'auto' || pref === 'tmux' || pref === 'tmux-popup') && process.env.TMUX) {
|
|
195
|
+
return 'tmux-popup'
|
|
199
196
|
}
|
|
200
|
-
|
|
201
|
-
if (process.env.TERM_PROGRAM === 'WezTerm') list.push('wezterm')
|
|
202
|
-
list.push('gnome-terminal', 'xfce4-terminal', 'konsole', 'xterm', 'alacritty', 'wezterm', 'kitty')
|
|
203
|
-
return list
|
|
197
|
+
return 'none'
|
|
204
198
|
}
|
|
205
199
|
|
|
206
200
|
function buildCmd(term, tsx, entryFile, sessionId) {
|
|
207
|
-
// Quote paths for Windows (handles spaces in paths like AppData\Roaming\npm\...)
|
|
208
|
-
const q = isWin ? (p) => `"${p}"` : (p) => p
|
|
209
|
-
const inner = isWin
|
|
210
|
-
? `${q(tsx)} ${q(entryFile)} ${sessionId}`
|
|
211
|
-
: `${tsx} ${entryFile} ${sessionId}`
|
|
212
201
|
switch (term) {
|
|
213
|
-
case '
|
|
214
|
-
case 'cmd': return ['cmd.exe', '/c', `start cmd.exe /k "${inner}"`]
|
|
215
|
-
case 'gnome-terminal': return ['gnome-terminal', '--', 'bash', '-c', inner]
|
|
216
|
-
case 'xfce4-terminal': return ['xfce4-terminal', '-e', inner]
|
|
217
|
-
case 'konsole': return ['konsole', '-e', inner]
|
|
218
|
-
case 'xterm': return ['xterm', '-e', inner]
|
|
219
|
-
case 'alacritty': return ['alacritty', '-e', 'bash', '-c', inner]
|
|
220
|
-
case 'wezterm': return ['wezterm', 'start', '--', 'bash', '-c', inner]
|
|
221
|
-
case 'kitty': return ['kitty', 'bash', '-c', inner]
|
|
202
|
+
case 'tmux-popup': return buildTmuxPopupCmd(tsx, entryFile, sessionId)
|
|
222
203
|
default: return null
|
|
223
204
|
}
|
|
224
205
|
}
|
|
@@ -244,8 +225,7 @@ function isInsideClaudeCode() {
|
|
|
244
225
|
|
|
245
226
|
function spawnWatcher(sessionId) {
|
|
246
227
|
const watcherPath = join(ROOT, 'src', 'watcher.mjs')
|
|
247
|
-
|
|
248
|
-
const watchPid = process.ppid
|
|
228
|
+
const watchPid = getWatchPid()
|
|
249
229
|
const child = spawn(process.execPath, [watcherPath, String(watchPid), sessionId], {
|
|
250
230
|
detached: true,
|
|
251
231
|
stdio: 'ignore',
|
|
@@ -253,6 +233,48 @@ function spawnWatcher(sessionId) {
|
|
|
253
233
|
child.unref()
|
|
254
234
|
}
|
|
255
235
|
|
|
236
|
+
function buildTmuxPopupCmd(tsx, entryFile, sessionId) {
|
|
237
|
+
const timerCmd = [
|
|
238
|
+
'POMODORO_MODAL=1',
|
|
239
|
+
shellQuote(process.execPath),
|
|
240
|
+
shellQuote(tsx),
|
|
241
|
+
shellQuote(entryFile),
|
|
242
|
+
shellQuote(sessionId),
|
|
243
|
+
].join(' ')
|
|
244
|
+
|
|
245
|
+
return [
|
|
246
|
+
'tmux',
|
|
247
|
+
'display-popup',
|
|
248
|
+
'-E',
|
|
249
|
+
'-w', '100%',
|
|
250
|
+
'-h', '100%',
|
|
251
|
+
'-x', '0',
|
|
252
|
+
'-y', '0',
|
|
253
|
+
'-B',
|
|
254
|
+
'-T', 'Pomodoro',
|
|
255
|
+
`bash -lc ${shellQuote(timerCmd)}`,
|
|
256
|
+
]
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function shellQuote(value) {
|
|
260
|
+
return `'${String(value).replace(/'/g, `'\\''`)}'`
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function getWatchPid() {
|
|
264
|
+
if (isWin) return process.ppid
|
|
265
|
+
|
|
266
|
+
try {
|
|
267
|
+
const out = execFileSync('ps', ['-o', 'ppid=', '-p', String(process.ppid)], {
|
|
268
|
+
encoding: 'utf8',
|
|
269
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
270
|
+
}).trim()
|
|
271
|
+
const ppid = Number.parseInt(out, 10)
|
|
272
|
+
if (Number.isInteger(ppid) && ppid > 1) return ppid
|
|
273
|
+
} catch {}
|
|
274
|
+
|
|
275
|
+
return process.ppid
|
|
276
|
+
}
|
|
277
|
+
|
|
256
278
|
function formatMs(ms) {
|
|
257
279
|
const m = Math.floor(ms / 60000)
|
|
258
280
|
const s = Math.floor((ms % 60000) / 1000)
|
package/install.mjs
CHANGED
|
@@ -113,11 +113,14 @@ node ${pomodoroPath} start $ARGUMENTS
|
|
|
113
113
|
|
|
114
114
|
The user has started a Pomodoro focus session and **will not be checking the screen** until the timer ends. Do not interrupt them.
|
|
115
115
|
|
|
116
|
+
**Current project**: Check \`\$CLAUDE_PROJECT_DIR\` or run \`pwd\` — work only on files in that directory. Do not reference or modify any other project.
|
|
117
|
+
|
|
116
118
|
**Task**: $ARGUMENTS
|
|
117
119
|
|
|
118
120
|
- Focus only on what is **unambiguously clear** from the task description
|
|
119
121
|
- If you encounter something that requires a user decision, **stop and record it** in \`.claude/pomodoro-pending.md\` — do NOT make assumptions or proceed on the user's behalf
|
|
120
122
|
- Do not send notifications or ask questions — the user is in focus mode
|
|
123
|
+
- If a timer UI is available, it may appear as a tmux popup. If not, continue headlessly — do not mention missing timer UI to the user unless they ask
|
|
121
124
|
- When you have done all you can, write a summary to \`.claude/pomodoro-summary.md\`
|
|
122
125
|
- Then wait quietly — the Pomodoro hook will manage the session lifecycle
|
|
123
126
|
`
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vibe-pomo",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.2",
|
|
4
4
|
"description": "You and your agent, both in flow. A Pomodoro timer for Claude Code that keeps agents working autonomously while you stay deep in focus — uninterrupted.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
package/src/daemon/index.mjs
CHANGED
|
@@ -21,6 +21,8 @@ const sessions = new Map()
|
|
|
21
21
|
|
|
22
22
|
/** sessionId → DB row id */
|
|
23
23
|
const dbIds = new Map()
|
|
24
|
+
/** sessionId → DB row id for sessions that already ended but may still receive user activity */
|
|
25
|
+
const finishedDbIds = new Map()
|
|
24
26
|
|
|
25
27
|
// ── IPC Handlers ─────────────────────────────────────────────────────────────
|
|
26
28
|
|
|
@@ -78,9 +80,10 @@ ipc.on(MSG.SESSION_BREAK, (msg) => {
|
|
|
78
80
|
})
|
|
79
81
|
|
|
80
82
|
ipc.on(MSG.SESSION_UPDATE_ACTIVITY, (msg) => {
|
|
81
|
-
const dbId = dbIds.get(msg.sessionId)
|
|
83
|
+
const dbId = dbIds.get(msg.sessionId) ?? finishedDbIds.get(msg.sessionId)
|
|
82
84
|
if (!dbId) return { error: 'session not found' }
|
|
83
85
|
updateSessionActivity(dbId, { userActivity: msg.userActivity })
|
|
86
|
+
finishedDbIds.delete(msg.sessionId)
|
|
84
87
|
return { ok: true }
|
|
85
88
|
})
|
|
86
89
|
|
|
@@ -163,6 +166,10 @@ function wireSessionEvents(session) {
|
|
|
163
166
|
}
|
|
164
167
|
|
|
165
168
|
sessions.delete(sessionId)
|
|
169
|
+
if (dbId) {
|
|
170
|
+
finishedDbIds.set(sessionId, dbId)
|
|
171
|
+
setTimeout(() => finishedDbIds.delete(sessionId), 30 * 60 * 1000).unref()
|
|
172
|
+
}
|
|
166
173
|
dbIds.delete(sessionId)
|
|
167
174
|
|
|
168
175
|
ipc.broadcast({ type: EVT.SESSION_ENDED, sessionId, status, actualMs })
|
package/src/daemon/ipc.mjs
CHANGED
|
@@ -62,7 +62,7 @@ export class IpcServer {
|
|
|
62
62
|
const handler = this.#handlers['query']
|
|
63
63
|
if (handler) {
|
|
64
64
|
const state = await handler(msg, socket)
|
|
65
|
-
if (state) socket.write(encode({ type: EVT.
|
|
65
|
+
if (state) socket.write(encode({ type: EVT.SNAPSHOT, ...state }))
|
|
66
66
|
}
|
|
67
67
|
continue
|
|
68
68
|
}
|
package/src/daemon/session.mjs
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Notification hook — queue notifications during an active Pomodoro session.
|
|
3
3
|
*/
|
|
4
|
-
import { readLock
|
|
4
|
+
import { readLock } from '../shared/lockfile.mjs'
|
|
5
5
|
import { sendAndReceive } from '../shared/ipcClient.mjs'
|
|
6
|
-
import {
|
|
6
|
+
import { MSG } from '../shared/protocol.mjs'
|
|
7
|
+
import { findActiveSession } from '../shared/findActiveSession.mjs'
|
|
7
8
|
|
|
8
9
|
async function readStdin() {
|
|
9
10
|
return new Promise((resolve) => {
|
|
@@ -20,8 +21,6 @@ async function main() {
|
|
|
20
21
|
if (!lock) process.exit(0)
|
|
21
22
|
|
|
22
23
|
const projectDir = process.env.CLAUDE_PROJECT_DIR ?? process.cwd()
|
|
23
|
-
const hash = projectHash(projectDir)
|
|
24
|
-
|
|
25
24
|
let state
|
|
26
25
|
try {
|
|
27
26
|
state = await sendAndReceive({ type: MSG.QUERY })
|
|
@@ -29,10 +28,7 @@ async function main() {
|
|
|
29
28
|
process.exit(0)
|
|
30
29
|
}
|
|
31
30
|
|
|
32
|
-
const active = state.sessions
|
|
33
|
-
(s) => s.projectHash === hash &&
|
|
34
|
-
(s.state === STATE.RUNNING || s.state === STATE.OVERTIME)
|
|
35
|
-
)
|
|
31
|
+
const active = findActiveSession(state.sessions, projectDir)
|
|
36
32
|
if (!active) process.exit(0)
|
|
37
33
|
|
|
38
34
|
const raw = await readStdin()
|
package/src/hooks/preToolUse.mjs
CHANGED
|
@@ -2,9 +2,10 @@
|
|
|
2
2
|
* PreToolUse hook — auto-approve tool calls during an active Pomodoro session.
|
|
3
3
|
* Transparency: if no daemon running or no active session for this project, exits 0.
|
|
4
4
|
*/
|
|
5
|
-
import { readLock
|
|
5
|
+
import { readLock } from '../shared/lockfile.mjs'
|
|
6
6
|
import { sendAndReceive } from '../shared/ipcClient.mjs'
|
|
7
|
-
import {
|
|
7
|
+
import { MSG } from '../shared/protocol.mjs'
|
|
8
|
+
import { findActiveSession } from '../shared/findActiveSession.mjs'
|
|
8
9
|
|
|
9
10
|
async function main() {
|
|
10
11
|
const lock = readLock()
|
|
@@ -19,12 +20,7 @@ async function main() {
|
|
|
19
20
|
process.exit(0)
|
|
20
21
|
}
|
|
21
22
|
|
|
22
|
-
|
|
23
|
-
const hash = projectHash(projectDir)
|
|
24
|
-
const active = state.sessions?.find(
|
|
25
|
-
(s) => s.projectHash === hash &&
|
|
26
|
-
(s.state === STATE.RUNNING || s.state === STATE.OVERTIME)
|
|
27
|
-
)
|
|
23
|
+
const active = findActiveSession(state.sessions, projectDir)
|
|
28
24
|
|
|
29
25
|
if (active) {
|
|
30
26
|
process.stdout.write(JSON.stringify({
|
package/src/hooks/stop.mjs
CHANGED
|
@@ -2,9 +2,10 @@
|
|
|
2
2
|
* Stop hook — called when Claude Code agent finishes.
|
|
3
3
|
* Blocks the agent if a Pomodoro is active for this project.
|
|
4
4
|
*/
|
|
5
|
-
import { readLock
|
|
5
|
+
import { readLock } from '../shared/lockfile.mjs'
|
|
6
6
|
import { sendAndReceive } from '../shared/ipcClient.mjs'
|
|
7
|
-
import {
|
|
7
|
+
import { MSG } from '../shared/protocol.mjs'
|
|
8
|
+
import { findActiveSession } from '../shared/findActiveSession.mjs'
|
|
8
9
|
|
|
9
10
|
async function readStdin() {
|
|
10
11
|
return new Promise((resolve) => {
|
|
@@ -21,8 +22,6 @@ async function main() {
|
|
|
21
22
|
if (!lock) process.exit(0)
|
|
22
23
|
|
|
23
24
|
const projectDir = process.env.CLAUDE_PROJECT_DIR ?? process.cwd()
|
|
24
|
-
const hash = projectHash(projectDir)
|
|
25
|
-
|
|
26
25
|
let state
|
|
27
26
|
try {
|
|
28
27
|
state = await sendAndReceive({ type: MSG.QUERY })
|
|
@@ -30,10 +29,7 @@ async function main() {
|
|
|
30
29
|
process.exit(0)
|
|
31
30
|
}
|
|
32
31
|
|
|
33
|
-
const active = state.sessions
|
|
34
|
-
(s) => s.projectHash === hash &&
|
|
35
|
-
(s.state === STATE.RUNNING || s.state === STATE.OVERTIME)
|
|
36
|
-
)
|
|
32
|
+
const active = findActiveSession(state.sessions, projectDir)
|
|
37
33
|
if (!active) process.exit(0)
|
|
38
34
|
|
|
39
35
|
const raw = await readStdin()
|
|
@@ -60,6 +56,7 @@ async function main() {
|
|
|
60
56
|
`Time remaining: ${resp.remainingFormatted}.`,
|
|
61
57
|
'The user is not available. Do not send messages or notifications.',
|
|
62
58
|
'Record any pending decisions in `.claude/pomodoro-pending.md` and wait quietly.',
|
|
59
|
+
'If the user asks how to end the session, tell them to run: pomodoro stop',
|
|
63
60
|
].join(' '),
|
|
64
61
|
}) + '\n')
|
|
65
62
|
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { resolve } from 'node:path'
|
|
2
|
+
import { projectHash } from './lockfile.mjs'
|
|
3
|
+
import { STATE } from './protocol.mjs'
|
|
4
|
+
|
|
5
|
+
function normalizeProjectDir(projectDir) {
|
|
6
|
+
return resolve(projectDir ?? process.cwd())
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function isActiveSession(session) {
|
|
10
|
+
return session?.state === STATE.RUNNING || session?.state === STATE.OVERTIME
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Match the current Claude project to the daemon's active session.
|
|
15
|
+
* Prefer the stable hash, but fall back to normalized projectDir for
|
|
16
|
+
* compatibility with older snapshots that did not include projectHash.
|
|
17
|
+
*/
|
|
18
|
+
export function findActiveSession(sessions, projectDir) {
|
|
19
|
+
const normalizedDir = normalizeProjectDir(projectDir)
|
|
20
|
+
const hash = projectHash(normalizedDir)
|
|
21
|
+
|
|
22
|
+
return sessions?.find((session) => {
|
|
23
|
+
if (!isActiveSession(session)) return false
|
|
24
|
+
if (session.projectHash === hash) return true
|
|
25
|
+
return normalizeProjectDir(session.projectDir) === normalizedDir
|
|
26
|
+
}) ?? null
|
|
27
|
+
}
|
|
@@ -34,6 +34,7 @@ export function TimerApp({
|
|
|
34
34
|
}: TimerAppProps) {
|
|
35
35
|
const { stdout } = useStdout()
|
|
36
36
|
const width = stdout?.columns ?? 60
|
|
37
|
+
const isModal = process.env.POMODORO_MODAL === '1'
|
|
37
38
|
|
|
38
39
|
const [timerState, setTimerState] = useState(initialState)
|
|
39
40
|
const [remaining, setRemaining] = useState(initialRemaining)
|
|
@@ -97,13 +98,13 @@ export function TimerApp({
|
|
|
97
98
|
useInput((input, key) => {
|
|
98
99
|
if (phase === 'activity-input') return // TextInput handles keys in this phase
|
|
99
100
|
if (phase === 'done') {
|
|
100
|
-
if (input === 'q' || key.escape) process.exit(0)
|
|
101
|
+
if (!isModal && (input === 'q' || key.escape)) process.exit(0)
|
|
101
102
|
return
|
|
102
103
|
}
|
|
103
104
|
if (timerState === STATE.ENDED) return
|
|
104
105
|
if (input === 'e' || input === 'E') sendEnd()
|
|
105
106
|
if (input === 'b' || input === 'B') sendBreak()
|
|
106
|
-
if (input === 'q' || key.escape) process.exit(0)
|
|
107
|
+
if (!isModal && (input === 'q' || key.escape)) process.exit(0)
|
|
107
108
|
})
|
|
108
109
|
|
|
109
110
|
const isOvertime = timerState === STATE.OVERTIME
|
|
@@ -141,10 +142,10 @@ export function TimerApp({
|
|
|
141
142
|
|
|
142
143
|
// ── Timer phase ───────────────────────────────────────────────────────────
|
|
143
144
|
const keybindings = isEnded
|
|
144
|
-
? '[Q] Quit'
|
|
145
|
+
? (isModal ? 'Submit your notes to close the Pomodoro overlay' : '[Q] Quit')
|
|
145
146
|
: isOvertime
|
|
146
|
-
? '[E] End Session [B] Break [Q] Quit'
|
|
147
|
-
: '[B] Break [Q] Quit'
|
|
147
|
+
? (isModal ? '[E] End Session [B] Break' : '[E] End Session [B] Break [Q] Quit')
|
|
148
|
+
: (isModal ? '[B] Break' : '[B] Break [Q] Quit')
|
|
148
149
|
|
|
149
150
|
return (
|
|
150
151
|
<Box flexDirection="column" width={width} paddingX={2} paddingY={1}>
|
|
@@ -159,6 +160,12 @@ export function TimerApp({
|
|
|
159
160
|
<Box marginTop={1}>
|
|
160
161
|
<Text color="gray" dimColor>{keybindings}</Text>
|
|
161
162
|
</Box>
|
|
163
|
+
|
|
164
|
+
{isModal && (
|
|
165
|
+
<Box marginTop={1}>
|
|
166
|
+
<Text color="gray" dimColor>The current Claude Code pane will stay covered until you end or break this Pomodoro.</Text>
|
|
167
|
+
</Box>
|
|
168
|
+
)}
|
|
162
169
|
</Box>
|
|
163
170
|
)
|
|
164
171
|
}
|