mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-02 05:29:42 +02:00
feat(image-popup): add auto-popup for detected screenshots and images
Implements image file watching in session working directories with automatic popup display in the web UI: - Add ImageWatcher class using chokidar for cross-platform file watching - Add ImageDetectedEvent type for SSE broadcasting - Integrate with session lifecycle (watch on create, unwatch on delete) - Add image:detected SSE event handler in frontend - Create draggable image popup window with open-in-new-tab support - Add CSS styling with compact wizard layout Supports: png, jpg, jpeg, gif, webp, bmp, svg Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -24,7 +24,7 @@ Claudeman is a Claude Code session manager with web interface and autonomous Ral
|
||||
|
||||
**Tech Stack**: TypeScript (ES2022/NodeNext, strict mode), Node.js, Fastify, node-pty, xterm.js
|
||||
|
||||
**TypeScript Strictness**: `noUnusedLocals`, `noUnusedParameters`, `noImplicitReturns`, `noImplicitOverride`, `noFallthroughCasesInSwitch`, `allowUnreachableCode: false`
|
||||
**TypeScript Strictness** (see `tsconfig.json`): `noUnusedLocals`, `noUnusedParameters`, `noImplicitReturns`, `noImplicitOverride`, `noFallthroughCasesInSwitch`, `allowUnreachableCode: false`, `allowUnusedLabels: false`
|
||||
|
||||
**Requirements**: Node.js 18+, Claude CLI, GNU Screen
|
||||
|
||||
@@ -111,7 +111,7 @@ journalctl --user -u claudeman-web -f
|
||||
|
||||
**E2E tests**: Use Playwright. Run `npx playwright install chromium` first. See `test/e2e/fixtures/` for helpers. E2E config provides ports, timeouts, and helpers.
|
||||
|
||||
**Test config**: Vitest runs with `globals: true` (no imports needed for `describe`/`it`/`expect`) and `fileParallelism: false` (files run sequentially to respect screen limits). Test timeout is 30s, teardown timeout is 60s.
|
||||
**Test config**: Vitest runs with `globals: true` (no imports needed for `describe`/`it`/`expect`) and `fileParallelism: false` (files run sequentially to respect screen limits). Unit test timeout is 30s, teardown timeout is 60s. E2E tests have longer timeouts defined in `test/e2e/e2e.config.ts` (90s test, 30s session creation).
|
||||
|
||||
**Test safety**: `test/setup.ts` provides:
|
||||
- Screen concurrency limiter (max 10)
|
||||
|
||||
Generated
+187
-3
@@ -1,18 +1,19 @@
|
||||
{
|
||||
"name": "claudeman",
|
||||
"version": "0.1396",
|
||||
"version": "0.1406",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "claudeman",
|
||||
"version": "0.1396",
|
||||
"version": "0.1406",
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@fastify/static": "^8.0.0",
|
||||
"@modelcontextprotocol/sdk": "^1.25.3",
|
||||
"chalk": "^5.3.0",
|
||||
"chokidar": "^3.6.0",
|
||||
"commander": "^12.1.0",
|
||||
"fastify": "^5.1.0",
|
||||
"ink": "^6.6.0",
|
||||
@@ -1657,6 +1658,31 @@
|
||||
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/anymatch": {
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
|
||||
"integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"normalize-path": "^3.0.0",
|
||||
"picomatch": "^2.0.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/anymatch/node_modules/picomatch": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
|
||||
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8.6"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/jonschlinkert"
|
||||
}
|
||||
},
|
||||
"node_modules/argparse": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
|
||||
@@ -1852,6 +1878,18 @@
|
||||
"node": ">=10.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/binary-extensions": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
|
||||
"integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/body-parser": {
|
||||
"version": "2.2.2",
|
||||
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz",
|
||||
@@ -1876,6 +1914,18 @@
|
||||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"node_modules/braces": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
|
||||
"integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"fill-range": "^7.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/buffer-crc32": {
|
||||
"version": "0.2.13",
|
||||
"resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz",
|
||||
@@ -1956,6 +2006,30 @@
|
||||
"url": "https://github.com/chalk/chalk?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/chokidar": {
|
||||
"version": "3.6.0",
|
||||
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
|
||||
"integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"anymatch": "~3.1.2",
|
||||
"braces": "~3.0.2",
|
||||
"glob-parent": "~5.1.2",
|
||||
"is-binary-path": "~2.1.0",
|
||||
"is-glob": "~4.0.1",
|
||||
"normalize-path": "~3.0.0",
|
||||
"readdirp": "~3.6.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 8.10.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://paulmillr.com/funding/"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"fsevents": "~2.3.2"
|
||||
}
|
||||
},
|
||||
"node_modules/chromium-bidi": {
|
||||
"version": "13.0.1",
|
||||
"resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-13.0.1.tgz",
|
||||
@@ -2910,6 +2984,18 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/fill-range": {
|
||||
"version": "7.1.1",
|
||||
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
|
||||
"integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"to-regex-range": "^5.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/finalhandler": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz",
|
||||
@@ -2983,7 +3069,6 @@
|
||||
"version": "2.3.2",
|
||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
|
||||
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
@@ -3129,6 +3214,18 @@
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/glob-parent": {
|
||||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
|
||||
"integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"is-glob": "^4.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/gopd": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
|
||||
@@ -3403,6 +3500,27 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/is-binary-path": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
|
||||
"integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"binary-extensions": "^2.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/is-extglob": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
|
||||
"integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/is-fullwidth-code-point": {
|
||||
"version": "5.1.0",
|
||||
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-5.1.0.tgz",
|
||||
@@ -3418,6 +3536,18 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/is-glob": {
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
|
||||
"integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"is-extglob": "^2.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/is-in-ci": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-in-ci/-/is-in-ci-2.0.0.tgz",
|
||||
@@ -3433,6 +3563,15 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/is-number": {
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
|
||||
"integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/is-promise": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz",
|
||||
@@ -3837,6 +3976,15 @@
|
||||
"node-addon-api": "^7.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/normalize-path": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
|
||||
"integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/object-assign": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
|
||||
@@ -4409,6 +4557,30 @@
|
||||
"react": "^19.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/readdirp": {
|
||||
"version": "3.6.0",
|
||||
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
|
||||
"integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"picomatch": "^2.2.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/readdirp/node_modules/picomatch": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
|
||||
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8.6"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/jonschlinkert"
|
||||
}
|
||||
},
|
||||
"node_modules/real-require": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/real-require/-/real-require-0.2.0.tgz",
|
||||
@@ -5169,6 +5341,18 @@
|
||||
"node": ">=14.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/to-regex-range": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
|
||||
"integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"is-number": "^7.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/toad-cache": {
|
||||
"version": "3.7.0",
|
||||
"resolved": "https://registry.npmjs.org/toad-cache/-/toad-cache-3.7.0.tgz",
|
||||
|
||||
@@ -42,6 +42,7 @@
|
||||
"@fastify/static": "^8.0.0",
|
||||
"@modelcontextprotocol/sdk": "^1.25.3",
|
||||
"chalk": "^5.3.0",
|
||||
"chokidar": "^3.6.0",
|
||||
"commander": "^12.1.0",
|
||||
"fastify": "^5.1.0",
|
||||
"ink": "^6.6.0",
|
||||
|
||||
@@ -0,0 +1,267 @@
|
||||
/**
|
||||
* @fileoverview Image Watcher - Detects new image files in session working directories
|
||||
*
|
||||
* Watches session working directories for new image files (screenshots, generated images)
|
||||
* and emits events to trigger automatic popup display in the web UI.
|
||||
*
|
||||
* Uses chokidar for reliable cross-platform file watching with awaitWriteFinish
|
||||
* to ensure files are fully written before emitting detection events.
|
||||
*/
|
||||
|
||||
import { EventEmitter } from 'events';
|
||||
import { watch, type FSWatcher } from 'chokidar';
|
||||
import { basename, extname } from 'path';
|
||||
import { statSync } from 'fs';
|
||||
import type { ImageDetectedEvent } from './types.js';
|
||||
|
||||
// ========== Types ==========
|
||||
|
||||
export interface ImageWatcherEvents {
|
||||
'image:detected': (event: ImageDetectedEvent) => void;
|
||||
'image:error': (error: Error, sessionId?: string) => void;
|
||||
}
|
||||
|
||||
// ========== Constants ==========
|
||||
|
||||
/** Supported image file extensions (lowercase) */
|
||||
const IMAGE_EXTENSIONS = new Set([
|
||||
'.png',
|
||||
'.jpg',
|
||||
'.jpeg',
|
||||
'.gif',
|
||||
'.webp',
|
||||
'.bmp',
|
||||
'.svg',
|
||||
]);
|
||||
|
||||
/** Time to wait for file writes to stabilize (ms) */
|
||||
const STABILITY_THRESHOLD_MS = 500;
|
||||
|
||||
/** Poll interval for checking file write stability (ms) */
|
||||
const POLL_INTERVAL_MS = 100;
|
||||
|
||||
/** Debounce delay for rapid image creation (ms) */
|
||||
const DEBOUNCE_DELAY_MS = 200;
|
||||
|
||||
// ========== ImageWatcher Class ==========
|
||||
|
||||
/**
|
||||
* Watches session working directories for new image files.
|
||||
*
|
||||
* Follows the SubagentWatcher pattern: extends EventEmitter, manages
|
||||
* file watchers in Maps, emits typed events.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const watcher = new ImageWatcher();
|
||||
* watcher.on('image:detected', (event) => {
|
||||
* console.log(`New image in session ${event.sessionId}: ${event.fileName}`);
|
||||
* });
|
||||
* watcher.watchSession('session-123', '/path/to/working/dir');
|
||||
* ```
|
||||
*/
|
||||
export class ImageWatcher extends EventEmitter {
|
||||
/** Map of sessionId -> FSWatcher for per-session directory watching */
|
||||
private sessionWatchers = new Map<string, FSWatcher>();
|
||||
|
||||
/** Map of sessionId -> working directory path */
|
||||
private sessionDirs = new Map<string, string>();
|
||||
|
||||
/** Debounce timers for rapid image creation */
|
||||
private debounceTimers = new Map<string, NodeJS.Timeout>();
|
||||
|
||||
/** Whether the watcher is currently running */
|
||||
private _isRunning = false;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
|
||||
// ========== Public API ==========
|
||||
|
||||
/**
|
||||
* Check if the watcher is currently running
|
||||
*/
|
||||
isRunning(): boolean {
|
||||
return this._isRunning;
|
||||
}
|
||||
|
||||
/**
|
||||
* Start the image watcher.
|
||||
* After calling start(), use watchSession() to add directories to monitor.
|
||||
*/
|
||||
start(): void {
|
||||
if (this._isRunning) return;
|
||||
this._isRunning = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop the image watcher and clean up all resources.
|
||||
*/
|
||||
stop(): void {
|
||||
this._isRunning = false;
|
||||
|
||||
// Close all session watchers
|
||||
for (const [sessionId, watcher] of this.sessionWatchers) {
|
||||
try {
|
||||
watcher.close();
|
||||
} catch (error) {
|
||||
this.emit('image:error', error instanceof Error ? error : new Error(String(error)), sessionId);
|
||||
}
|
||||
}
|
||||
this.sessionWatchers.clear();
|
||||
this.sessionDirs.clear();
|
||||
|
||||
// Clear all debounce timers
|
||||
for (const timer of this.debounceTimers.values()) {
|
||||
clearTimeout(timer);
|
||||
}
|
||||
this.debounceTimers.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Start watching a session's working directory for new images.
|
||||
*
|
||||
* @param sessionId - Claudeman session ID
|
||||
* @param workingDir - Path to the session's working directory
|
||||
*/
|
||||
watchSession(sessionId: string, workingDir: string): void {
|
||||
if (!this._isRunning) {
|
||||
this.start();
|
||||
}
|
||||
|
||||
// Don't double-watch the same session
|
||||
if (this.sessionWatchers.has(sessionId)) {
|
||||
// If working directory changed, unwatch old and watch new
|
||||
if (this.sessionDirs.get(sessionId) !== workingDir) {
|
||||
this.unwatchSession(sessionId);
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
this.sessionDirs.set(sessionId, workingDir);
|
||||
|
||||
try {
|
||||
// Create chokidar watcher for the directory
|
||||
const watcher = watch(workingDir, {
|
||||
// Only detect NEW files, not existing ones
|
||||
ignoreInitial: true,
|
||||
// Wait for file writes to stabilize before emitting
|
||||
awaitWriteFinish: {
|
||||
stabilityThreshold: STABILITY_THRESHOLD_MS,
|
||||
pollInterval: POLL_INTERVAL_MS,
|
||||
},
|
||||
// Only watch files at the top level (not subdirectories)
|
||||
depth: 0,
|
||||
// Ignore non-image files early for performance
|
||||
ignored: (path: string) => {
|
||||
const ext = extname(path).toLowerCase();
|
||||
// Don't ignore directories (needed for watching to work)
|
||||
// Ignore files that aren't images
|
||||
return ext !== '' && !IMAGE_EXTENSIONS.has(ext);
|
||||
},
|
||||
});
|
||||
|
||||
// Handle new file detection
|
||||
watcher.on('add', (filePath: string) => {
|
||||
this.handleNewFile(sessionId, filePath);
|
||||
});
|
||||
|
||||
// Handle watcher errors
|
||||
watcher.on('error', (error: Error) => {
|
||||
this.emit('image:error', error, sessionId);
|
||||
});
|
||||
|
||||
this.sessionWatchers.set(sessionId, watcher);
|
||||
} catch (error) {
|
||||
this.emit('image:error', error instanceof Error ? error : new Error(String(error)), sessionId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop watching a session's working directory.
|
||||
*
|
||||
* @param sessionId - Claudeman session ID to stop watching
|
||||
*/
|
||||
unwatchSession(sessionId: string): void {
|
||||
const watcher = this.sessionWatchers.get(sessionId);
|
||||
if (watcher) {
|
||||
try {
|
||||
watcher.close();
|
||||
} catch (error) {
|
||||
this.emit('image:error', error instanceof Error ? error : new Error(String(error)), sessionId);
|
||||
}
|
||||
this.sessionWatchers.delete(sessionId);
|
||||
}
|
||||
this.sessionDirs.delete(sessionId);
|
||||
|
||||
// Clear any pending debounce timer for this session
|
||||
const timer = this.debounceTimers.get(sessionId);
|
||||
if (timer) {
|
||||
clearTimeout(timer);
|
||||
this.debounceTimers.delete(sessionId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get list of currently watched session IDs.
|
||||
*/
|
||||
getWatchedSessions(): string[] {
|
||||
return Array.from(this.sessionWatchers.keys());
|
||||
}
|
||||
|
||||
// ========== Private Methods ==========
|
||||
|
||||
/**
|
||||
* Handle a new file being detected.
|
||||
* Verifies it's an image and emits the detection event.
|
||||
*/
|
||||
private handleNewFile(sessionId: string, filePath: string): void {
|
||||
const ext = extname(filePath).toLowerCase();
|
||||
|
||||
// Double-check it's an image extension
|
||||
if (!IMAGE_EXTENSIONS.has(ext)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Debounce rapid file creation (e.g., multiple screenshots quickly)
|
||||
const existingTimer = this.debounceTimers.get(filePath);
|
||||
if (existingTimer) {
|
||||
clearTimeout(existingTimer);
|
||||
}
|
||||
|
||||
const timer = setTimeout(() => {
|
||||
this.debounceTimers.delete(filePath);
|
||||
this.emitImageDetected(sessionId, filePath);
|
||||
}, DEBOUNCE_DELAY_MS);
|
||||
|
||||
this.debounceTimers.set(filePath, timer);
|
||||
}
|
||||
|
||||
/**
|
||||
* Emit the image:detected event with file metadata.
|
||||
*/
|
||||
private emitImageDetected(sessionId: string, filePath: string): void {
|
||||
try {
|
||||
const stat = statSync(filePath);
|
||||
const fileName = basename(filePath);
|
||||
|
||||
const event: ImageDetectedEvent = {
|
||||
sessionId,
|
||||
filePath,
|
||||
fileName,
|
||||
timestamp: Date.now(),
|
||||
size: stat.size,
|
||||
};
|
||||
|
||||
this.emit('image:detected', event);
|
||||
} catch (error) {
|
||||
// File may have been deleted between detection and stat
|
||||
this.emit('image:error', error instanceof Error ? error : new Error(String(error)), sessionId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Export singleton instance for convenience
|
||||
export const imageWatcher = new ImageWatcher();
|
||||
+100
-5
@@ -986,6 +986,9 @@ ${prompt}
|
||||
|
||||
// Update the case CLAUDE.md with research context links
|
||||
this.updateCaseClaudeMd();
|
||||
|
||||
// Update the project's main CLAUDE.md with Ralph Loop context
|
||||
this.updateProjectClaudeMd();
|
||||
} catch (err) {
|
||||
console.error('[PlanOrchestrator] Failed to save final result:', err);
|
||||
}
|
||||
@@ -1068,6 +1071,71 @@ Use \`/init\` to load this context, then read specific files as needed.
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the project's main CLAUDE.md with Ralph Loop context.
|
||||
* This ensures Claude instances running in the project directory know about the current task,
|
||||
* where to find the plan files, and how to track todos.
|
||||
*/
|
||||
private updateProjectClaudeMd(): void {
|
||||
if (!this.outputDir || !this.workingDir) return;
|
||||
|
||||
try {
|
||||
const projectClaudeMdPath = join(this.workingDir, 'CLAUDE.md');
|
||||
const caseDir = dirname(this.outputDir);
|
||||
|
||||
// Build the Ralph Loop context section
|
||||
const contextSection = `
|
||||
## Active Ralph Loop Task
|
||||
|
||||
**Current Task**: ${this.taskDescription}
|
||||
|
||||
**Case Folder**: \`${caseDir}\`
|
||||
|
||||
### Key Files
|
||||
- **Plan Summary**: \`${caseDir}/ralph-wizard/summary.md\` - Human-readable plan overview
|
||||
- **Todo Items**: \`${caseDir}/ralph-wizard/final-result.json\` - Contains \`items\` array with all todo tasks
|
||||
- **Research**: \`${caseDir}/ralph-wizard/research/result.json\` - External resources and codebase patterns
|
||||
|
||||
### How to Work on This Task
|
||||
1. Read the plan summary to understand the overall approach
|
||||
2. Check \`final-result.json\` for the todo items array - each item has \`id\`, \`title\`, \`description\`, \`priority\`
|
||||
3. Work through items in priority order (critical → high → medium → low)
|
||||
4. Use \`<promise>COMPLETION_PHRASE</promise>\` when the entire task is complete
|
||||
|
||||
### Research Insights
|
||||
Check \`${caseDir}/ralph-wizard/research/result.json\` for:
|
||||
- External GitHub repos and documentation links to reference
|
||||
- Existing codebase patterns to follow
|
||||
- Technical recommendations from the research phase
|
||||
`;
|
||||
|
||||
// Read existing CLAUDE.md or create new one
|
||||
let existingContent = '';
|
||||
if (existsSync(projectClaudeMdPath)) {
|
||||
existingContent = readFileSync(projectClaudeMdPath, 'utf-8');
|
||||
|
||||
// Remove any existing Ralph Loop section to avoid duplicates
|
||||
const sectionStart = existingContent.indexOf('## Active Ralph Loop Task');
|
||||
if (sectionStart !== -1) {
|
||||
// Find the next ## heading or end of file
|
||||
const nextSectionMatch = existingContent.slice(sectionStart + 1).match(/\n## /);
|
||||
const sectionEnd = nextSectionMatch
|
||||
? sectionStart + 1 + nextSectionMatch.index!
|
||||
: existingContent.length;
|
||||
existingContent = existingContent.slice(0, sectionStart) + existingContent.slice(sectionEnd);
|
||||
}
|
||||
}
|
||||
|
||||
// Append the new section
|
||||
const newContent = existingContent.trimEnd() + '\n' + contextSection;
|
||||
writeFileSync(projectClaudeMdPath, newContent, 'utf-8');
|
||||
|
||||
console.log(`[PlanOrchestrator] Updated project CLAUDE.md with Ralph Loop context at ${projectClaudeMdPath}`);
|
||||
} catch (err) {
|
||||
console.error('[PlanOrchestrator] Failed to update project CLAUDE.md:', err);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a human-readable summary of the plan.
|
||||
*/
|
||||
@@ -1307,15 +1375,42 @@ Use \`/init\` to load this context, then read specific files as needed.
|
||||
try {
|
||||
const prompt = RESEARCH_AGENT_PROMPT.replace('{TASK}', taskDescription);
|
||||
|
||||
onProgress?.('research', 'Gathering external resources and codebase context...');
|
||||
onProgress?.('research', 'Starting research agent (web search + codebase exploration)...');
|
||||
|
||||
// Periodic progress updates showing elapsed time with contextual messages
|
||||
const researchPhases = [
|
||||
{ min: 0, msg: 'Searching for relevant GitHub repos and documentation...' },
|
||||
{ min: 30, msg: 'Exploring official docs and best practice guides...' },
|
||||
{ min: 60, msg: 'Analyzing codebase patterns and conventions...' },
|
||||
{ min: 120, msg: 'Synthesizing findings and recommendations...' },
|
||||
{ min: 180, msg: 'Compiling research results (complex tasks take time)...' },
|
||||
{ min: 300, msg: 'Almost done - finalizing enriched task description...' },
|
||||
];
|
||||
|
||||
// Periodic progress updates showing elapsed time
|
||||
const progressInterval = setInterval(() => {
|
||||
const elapsedSec = Math.floor((Date.now() - startTime) / 1000);
|
||||
const timeoutSec = Math.floor(RESEARCH_TIMEOUT_MS / 1000);
|
||||
onProgress?.('research', `Research agent working... (${elapsedSec}s / ${timeoutSec}s timeout)`);
|
||||
console.log(`[PlanOrchestrator] Research agent: ${elapsedSec}s elapsed (timeout: ${timeoutSec}s)`);
|
||||
}, 30000); // Update every 30 seconds
|
||||
|
||||
// Find appropriate phase message based on elapsed time
|
||||
let phaseMsg = researchPhases[0].msg;
|
||||
for (const phase of researchPhases) {
|
||||
if (elapsedSec >= phase.min) {
|
||||
phaseMsg = phase.msg;
|
||||
}
|
||||
}
|
||||
|
||||
const progressMsg = `${phaseMsg} (${elapsedSec}s / ${timeoutSec}s)`;
|
||||
onProgress?.('research', progressMsg);
|
||||
onSubagent?.({
|
||||
type: 'progress',
|
||||
agentId,
|
||||
agentType: 'research',
|
||||
model: MODEL_RESEARCH,
|
||||
status: 'running',
|
||||
detail: progressMsg,
|
||||
});
|
||||
console.log(`[PlanOrchestrator] Research agent: ${elapsedSec}s - ${phaseMsg}`);
|
||||
}, 15000); // Update every 15 seconds
|
||||
|
||||
let result: string;
|
||||
try {
|
||||
|
||||
@@ -510,6 +510,14 @@ export class RalphTracker extends EventEmitter {
|
||||
/** Path to the @fix_plan.md file being watched */
|
||||
private _fixPlanPath: string | null = null;
|
||||
|
||||
/**
|
||||
* When @fix_plan.md is active, treat it as the source of truth for todo status.
|
||||
* This prevents output-based detection from overriding file-based status.
|
||||
*/
|
||||
private get isFileAuthoritative(): boolean {
|
||||
return this._fixPlanPath !== null;
|
||||
}
|
||||
|
||||
// ========== Enhanced Plan Management ==========
|
||||
|
||||
/** Current version of the plan (incremented on changes) */
|
||||
@@ -1077,6 +1085,10 @@ export class RalphTracker extends EventEmitter {
|
||||
* @fires loopUpdate - If loop state changes
|
||||
*/
|
||||
private detectAllTasksComplete(line: string): void {
|
||||
// When @fix_plan.md is active, only trust the file for todo status
|
||||
// This prevents false positives from Claude saying "all done" in conversation
|
||||
if (this.isFileAuthoritative) return;
|
||||
|
||||
// Only trigger if line is a clear standalone completion message
|
||||
// Avoid matching commentary like "once all tasks are complete..."
|
||||
if (!ALL_COMPLETE_PATTERN.test(line)) return;
|
||||
@@ -1130,6 +1142,9 @@ export class RalphTracker extends EventEmitter {
|
||||
* Only marks a todo complete if we can match it by task number.
|
||||
*/
|
||||
private detectTaskCompletion(line: string): void {
|
||||
// When @fix_plan.md is active, only trust the file for todo status
|
||||
if (this.isFileAuthoritative) return;
|
||||
|
||||
if (!TASK_DONE_PATTERN.test(line)) return;
|
||||
|
||||
// Only act on explicit task number references like "Task 8 is done"
|
||||
|
||||
@@ -1163,6 +1163,25 @@ export interface ActiveBashTool {
|
||||
sessionId: string;
|
||||
}
|
||||
|
||||
// ========== Image Watcher Types ==========
|
||||
|
||||
/**
|
||||
* Event emitted when a new image file is detected in a session's working directory.
|
||||
* Used to trigger automatic image popup display in the web UI.
|
||||
*/
|
||||
export interface ImageDetectedEvent {
|
||||
/** Claudeman session ID where the image was detected */
|
||||
sessionId: string;
|
||||
/** Full path to the detected image file */
|
||||
filePath: string;
|
||||
/** Image file name (basename) */
|
||||
fileName: string;
|
||||
/** Timestamp when the image was detected */
|
||||
timestamp: number;
|
||||
/** File size in bytes */
|
||||
size: number;
|
||||
}
|
||||
|
||||
// ========== Spawn1337 Protocol Re-exports ==========
|
||||
|
||||
export type {
|
||||
|
||||
@@ -476,6 +476,10 @@ class ClaudemanApp {
|
||||
this.projectInsightsPanelVisible = false;
|
||||
this.currentSessionWorkingDir = null; // Track current session's working dir for path normalization
|
||||
|
||||
// Image popup windows (auto-open for detected screenshots/images)
|
||||
this.imagePopups = new Map(); // Map<imageId, { element, sessionId, filePath }>
|
||||
this.imagePopupZIndex = 3000;
|
||||
|
||||
// Tab alert states: Map<sessionId, 'action' | 'idle'>
|
||||
this.tabAlerts = new Map();
|
||||
|
||||
@@ -1129,6 +1133,7 @@ class ClaudemanApp {
|
||||
this.ralphStates.delete(data.id); // Clean up ralph state for this session
|
||||
this.projectInsights.delete(data.id); // Clean up project insights for this session
|
||||
this.closeSessionLogViewerWindows(data.id); // Close log viewer windows for this session
|
||||
this.closeSessionImagePopups(data.id); // Close image popup windows for this session
|
||||
this.closeSessionSubagentWindows(data.id, true); // Close subagent windows and cleanup activity data
|
||||
// Clean up idle timer for this session
|
||||
const idleTimer = this.idleTimers.get(data.id);
|
||||
@@ -1912,6 +1917,14 @@ class ClaudemanApp {
|
||||
}, 5 * 60 * 1000); // 5 minutes
|
||||
});
|
||||
|
||||
// ========== Image Detection Events (Screenshots & Generated Images) ==========
|
||||
|
||||
addListener('image:detected', (e) => {
|
||||
const data = JSON.parse(e.data);
|
||||
console.log('[Image Detected]', data);
|
||||
this.openImagePopup(data);
|
||||
});
|
||||
|
||||
// Plan subagent visibility events (show Opus agents during plan generation)
|
||||
addListener('plan:subagent', (e) => {
|
||||
const data = JSON.parse(e.data);
|
||||
@@ -9670,6 +9683,127 @@ class ClaudemanApp {
|
||||
}
|
||||
}
|
||||
|
||||
// ========== Image Popup Windows (Auto-popup for Screenshots) ==========
|
||||
|
||||
/**
|
||||
* Open a popup window to display a detected image.
|
||||
* Called automatically when image:detected SSE event is received.
|
||||
*/
|
||||
openImagePopup(imageEvent) {
|
||||
const { sessionId, filePath, fileName, timestamp, size } = imageEvent;
|
||||
|
||||
// Create unique window ID
|
||||
const imageId = `${sessionId}-${timestamp}`;
|
||||
|
||||
// If window already exists for this image, focus it
|
||||
if (this.imagePopups.has(imageId)) {
|
||||
const existing = this.imagePopups.get(imageId);
|
||||
existing.element.style.zIndex = ++this.imagePopupZIndex;
|
||||
return;
|
||||
}
|
||||
|
||||
// Calculate position (cascade from center, with offset for multiple popups)
|
||||
const windowCount = this.imagePopups.size;
|
||||
const centerX = (window.innerWidth - 600) / 2;
|
||||
const centerY = (window.innerHeight - 500) / 2;
|
||||
const offsetX = centerX + (windowCount % 5) * 30;
|
||||
const offsetY = centerY + (windowCount % 5) * 30;
|
||||
|
||||
// Get session name for display
|
||||
const session = this.sessions.get(sessionId);
|
||||
const sessionName = session?.name || sessionId.substring(0, 8);
|
||||
|
||||
// Format file size
|
||||
const sizeKB = (size / 1024).toFixed(1);
|
||||
|
||||
// Build image URL using the existing file-raw endpoint
|
||||
const imageUrl = `/api/sessions/${sessionId}/file-raw?path=${encodeURIComponent(fileName)}`;
|
||||
|
||||
// Create window element
|
||||
const win = document.createElement('div');
|
||||
win.className = 'image-popup-window';
|
||||
win.id = `image-popup-${imageId}`;
|
||||
win.style.left = `${offsetX}px`;
|
||||
win.style.top = `${offsetY}px`;
|
||||
win.style.zIndex = ++this.imagePopupZIndex;
|
||||
|
||||
win.innerHTML = `
|
||||
<div class="image-popup-header">
|
||||
<div class="image-popup-title" title="${this.escapeHtml(filePath)}">
|
||||
<span class="icon">🖼️</span>
|
||||
<span class="filename">${this.escapeHtml(fileName)}</span>
|
||||
<span class="session-badge">${this.escapeHtml(sessionName)}</span>
|
||||
<span class="size-badge">${sizeKB} KB</span>
|
||||
</div>
|
||||
<div class="image-popup-actions">
|
||||
<button onclick="app.openImageInNewTab('${imageUrl}')" title="Open in new tab">↗</button>
|
||||
<button onclick="app.closeImagePopup('${imageId}')" title="Close">×</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="image-popup-body">
|
||||
<img src="${imageUrl}" alt="${this.escapeHtml(fileName)}"
|
||||
onerror="this.parentElement.innerHTML='<div class=\\'image-error\\'>Failed to load image</div>'"
|
||||
onclick="app.openImageInNewTab('${imageUrl}')" />
|
||||
</div>
|
||||
`;
|
||||
|
||||
document.body.appendChild(win);
|
||||
|
||||
// Make draggable
|
||||
const dragListeners = this.makeWindowDraggable(win, win.querySelector('.image-popup-header'));
|
||||
|
||||
// Focus on click
|
||||
win.addEventListener('mousedown', () => {
|
||||
win.style.zIndex = ++this.imagePopupZIndex;
|
||||
});
|
||||
|
||||
// Store reference
|
||||
this.imagePopups.set(imageId, {
|
||||
element: win,
|
||||
sessionId,
|
||||
filePath,
|
||||
dragListeners,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Close an image popup window.
|
||||
*/
|
||||
closeImagePopup(imageId) {
|
||||
const popupData = this.imagePopups.get(imageId);
|
||||
if (!popupData) return;
|
||||
|
||||
// Clean up global drag event listeners
|
||||
if (popupData.dragListeners) {
|
||||
document.removeEventListener('mousemove', popupData.dragListeners.move);
|
||||
document.removeEventListener('mouseup', popupData.dragListeners.up);
|
||||
}
|
||||
|
||||
// Remove element
|
||||
popupData.element.remove();
|
||||
|
||||
// Remove from map
|
||||
this.imagePopups.delete(imageId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Open image in a new browser tab.
|
||||
*/
|
||||
openImageInNewTab(url) {
|
||||
window.open(url, '_blank');
|
||||
}
|
||||
|
||||
/**
|
||||
* Close all image popups for a session.
|
||||
*/
|
||||
closeSessionImagePopups(sessionId) {
|
||||
for (const [imageId, data] of this.imagePopups) {
|
||||
if (data.sessionId === sessionId) {
|
||||
this.closeImagePopup(imageId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ========== Screen Sessions (in Monitor Panel) ==========
|
||||
|
||||
async loadScreens() {
|
||||
|
||||
@@ -942,7 +942,7 @@
|
||||
|
||||
<!-- Ralph Loop Wizard Modal -->
|
||||
<div class="modal" id="ralphWizardModal">
|
||||
<div class="modal-backdrop" onclick="app.closeRalphWizard()"></div>
|
||||
<div class="modal-backdrop"></div>
|
||||
<div class="modal-content modal-wizard">
|
||||
<div class="modal-header">
|
||||
<h3>Start Ralph Loop</h3>
|
||||
@@ -976,7 +976,7 @@
|
||||
<div class="textarea-with-assist">
|
||||
<textarea
|
||||
id="ralphTaskDescription"
|
||||
rows="4"
|
||||
rows="3"
|
||||
placeholder="Build a REST API for user authentication with JWT tokens, login/logout endpoints, and password reset flow."></textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+126
-8
@@ -5282,6 +5282,124 @@ kbd {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
/* ========== Image Popup Windows (Auto-popup for Screenshots) ========== */
|
||||
|
||||
.image-popup-window {
|
||||
position: fixed;
|
||||
width: 600px;
|
||||
height: 500px;
|
||||
min-width: 300px;
|
||||
min-height: 250px;
|
||||
max-width: 90vw;
|
||||
max-height: 90vh;
|
||||
background: var(--bg-card);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
box-shadow: var(--shadow-lg);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
resize: both;
|
||||
z-index: 3000;
|
||||
}
|
||||
|
||||
.image-popup-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0.5rem 0.75rem;
|
||||
background: var(--bg-input);
|
||||
border-bottom: 1px solid var(--border);
|
||||
cursor: move;
|
||||
}
|
||||
|
||||
.image-popup-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.4rem;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.image-popup-title .icon {
|
||||
font-size: 1rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.image-popup-title .filename {
|
||||
font-family: var(--font-mono);
|
||||
font-size: 0.8rem;
|
||||
color: white;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.image-popup-title .session-badge {
|
||||
font-size: 0.6rem;
|
||||
padding: 0.1rem 0.4rem;
|
||||
border-radius: 4px;
|
||||
background: var(--accent);
|
||||
color: white;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.image-popup-title .size-badge {
|
||||
font-size: 0.6rem;
|
||||
padding: 0.1rem 0.3rem;
|
||||
border-radius: 4px;
|
||||
background: var(--text-muted);
|
||||
color: white;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.image-popup-actions {
|
||||
display: flex;
|
||||
gap: 0.3rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.image-popup-actions button {
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: var(--text-muted);
|
||||
cursor: pointer;
|
||||
padding: 0.2rem 0.4rem;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.image-popup-actions button:hover {
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.image-popup-body {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
overflow: hidden;
|
||||
background: #1a1a1a;
|
||||
padding: 0.5rem;
|
||||
}
|
||||
|
||||
.image-popup-body img {
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
object-fit: contain;
|
||||
cursor: pointer;
|
||||
transition: transform 0.1s ease;
|
||||
}
|
||||
|
||||
.image-popup-body img:hover {
|
||||
transform: scale(1.02);
|
||||
}
|
||||
|
||||
.image-popup-body .image-error {
|
||||
color: var(--error);
|
||||
font-size: 0.85rem;
|
||||
padding: 1rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* ========== xterm.js Clickable Links ========== */
|
||||
|
||||
/* Style for detected file path links in terminal */
|
||||
@@ -5321,8 +5439,8 @@ kbd {
|
||||
.wizard-progress {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 2rem;
|
||||
padding: 0.75rem 1rem;
|
||||
gap: 1.5rem;
|
||||
padding: 0.5rem 0.75rem;
|
||||
background: var(--bg-dark);
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
@@ -5345,15 +5463,15 @@ kbd {
|
||||
}
|
||||
|
||||
.wizard-step-number {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: var(--bg-input);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 50%;
|
||||
font-size: 0.75rem;
|
||||
font-size: 0.7rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
@@ -5397,11 +5515,11 @@ kbd {
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
/* Wizard Modal - wider layout with height constraints */
|
||||
/* Wizard Modal - compact layout */
|
||||
.modal-wizard {
|
||||
width: 720px;
|
||||
width: 480px;
|
||||
max-width: 95vw;
|
||||
max-height: 80vh;
|
||||
max-height: 70vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
@@ -30,6 +30,7 @@ import { generateClaudeMd } from '../templates/claude-md.js';
|
||||
import { parseRalphLoopConfig, extractCompletionPhrase } from '../ralph-config.js';
|
||||
import { writeHooksConfig } from '../hooks-config.js';
|
||||
import { subagentWatcher, type SubagentInfo, type SubagentToolCall, type SubagentProgress, type SubagentMessage, type SubagentToolResult } from '../subagent-watcher.js';
|
||||
import { imageWatcher } from '../image-watcher.js';
|
||||
import { TranscriptWatcher } from '../transcript-watcher.js';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { createRequire } from 'node:module';
|
||||
@@ -58,6 +59,7 @@ import {
|
||||
type CaseInfo,
|
||||
type PersistedRespawnConfig,
|
||||
type NiceConfig,
|
||||
type ImageDetectedEvent,
|
||||
DEFAULT_NICE_CONFIG,
|
||||
} from '../types.js';
|
||||
|
||||
@@ -345,6 +347,9 @@ export class WebServer extends EventEmitter {
|
||||
|
||||
// Set up subagent watcher listeners
|
||||
this.setupSubagentWatcherListeners();
|
||||
|
||||
// Set up image watcher listeners
|
||||
this.setupImageWatcherListeners();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -389,6 +394,20 @@ export class WebServer extends EventEmitter {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Set up event listeners for image watcher.
|
||||
* Broadcasts image detection events to SSE clients for auto-popup.
|
||||
*/
|
||||
private setupImageWatcherListeners(): void {
|
||||
imageWatcher.on('image:detected', (event: ImageDetectedEvent) => {
|
||||
this.broadcast('image:detected', event);
|
||||
});
|
||||
|
||||
imageWatcher.on('image:error', (error: Error, sessionId?: string) => {
|
||||
console.error(`[ImageWatcher] Error${sessionId ? ` for ${sessionId}` : ''}:`, error.message);
|
||||
});
|
||||
}
|
||||
|
||||
private async setupRoutes(): Promise<void> {
|
||||
// Serve static files
|
||||
await this.app.register(fastifyStatic, {
|
||||
@@ -3291,6 +3310,8 @@ NOW: Generate the implementation plan for the task above. Think step by step.`;
|
||||
session.removeAllListeners();
|
||||
// Close any active file streams for this session
|
||||
fileStreamManager.closeSessionStreams(sessionId);
|
||||
// Stop watching for images in this session's directory
|
||||
imageWatcher.unwatchSession(sessionId);
|
||||
await session.stop(killScreen);
|
||||
this.sessions.delete(sessionId);
|
||||
// Only remove from state.json if we're also killing the screen.
|
||||
@@ -3312,6 +3333,9 @@ NOW: Generate the implementation plan for the task above. Think step by step.`;
|
||||
// Set working directory for Ralph tracker to auto-load @fix_plan.md
|
||||
session.ralphTracker.setWorkingDir(session.workingDir);
|
||||
|
||||
// Start watching for new images in this session's working directory
|
||||
imageWatcher.watchSession(session.id, session.workingDir);
|
||||
|
||||
session.on('output', (data) => {
|
||||
// Use batching for better performance at high throughput
|
||||
this.batchOutputData(session.id, data);
|
||||
@@ -4338,6 +4362,10 @@ NOW: Generate the implementation plan for the task above. Think step by step.`;
|
||||
} else {
|
||||
console.log('Subagent watcher disabled by user settings');
|
||||
}
|
||||
|
||||
// Start image watcher for auto-popup of screenshots
|
||||
imageWatcher.start();
|
||||
console.log('Image watcher started - monitoring session directories for new images');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -4592,6 +4620,9 @@ NOW: Generate the implementation plan for the task above. Think step by step.`;
|
||||
// Stop subagent watcher
|
||||
subagentWatcher.stop();
|
||||
|
||||
// Stop image watcher
|
||||
imageWatcher.stop();
|
||||
|
||||
await this.app.close();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user