mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-03 14:09:42 +02:00
fix: image watcher now uses relative path for subdirectory images
- Added relativePath field to ImageDetectedEvent - Compute relative path from working directory in image-watcher - Use relativePath in frontend URL (fixes 'Failed to load image' for subdirs) - Minor CLAUDE.md improvements chore: bump version to 0.1461
This commit is contained in:
@@ -16,7 +16,7 @@ When user says "COM":
|
||||
1. Increment version in BOTH `package.json` AND `CLAUDE.md`
|
||||
2. Run: `git add -A && git commit -m "chore: bump version to X.XXXX" && git push && npm run build && systemctl --user restart claudeman-web`
|
||||
|
||||
**Version**: 0.1460 (must match `package.json`)
|
||||
**Version**: 0.1461 (must match `package.json` for npm publish)
|
||||
|
||||
## Project Overview
|
||||
|
||||
@@ -58,12 +58,6 @@ systemctl --user restart claudeman-web
|
||||
journalctl --user -u claudeman-web -f
|
||||
```
|
||||
|
||||
## Binaries
|
||||
|
||||
| Binary | Purpose |
|
||||
|--------|---------|
|
||||
| `claudeman` | Main CLI and web server |
|
||||
|
||||
## Architecture
|
||||
|
||||
### Core Files
|
||||
@@ -73,17 +67,26 @@ journalctl --user -u claudeman-web -f
|
||||
| `src/session.ts` | PTY wrapper: `runPrompt()`, `startInteractive()`, `startShell()` |
|
||||
| `src/screen-manager.ts` | GNU screen persistence, ghost discovery |
|
||||
| `src/session-manager.ts` | Session lifecycle, cleanup |
|
||||
| `src/state-store.ts` | State persistence to `~/.claudeman/state.json` |
|
||||
| `src/respawn-controller.ts` | State machine for autonomous cycling |
|
||||
| `src/ralph-tracker.ts` | Detects `<promise>PHRASE</promise>`, todos |
|
||||
| `src/ralph-loop.ts` | Autonomous task execution loop (polls queue, assigns tasks) |
|
||||
| `src/ralph-config.ts` | Parses `.claude/ralph-loop.local.md` plugin config |
|
||||
| `src/task.ts` | Task model for prompt execution |
|
||||
| `src/task-queue.ts` | Priority queue for tasks with dependencies |
|
||||
| `src/task-tracker.ts` | Background task tracker for subagent detection |
|
||||
| `src/subagent-watcher.ts` | Monitors Claude Code's Task tool (background agents) |
|
||||
| `src/run-summary.ts` | Timeline events for "what happened while away" |
|
||||
| `src/ai-idle-checker.ts` | AI-powered idle detection with `ai-checker-base.ts` |
|
||||
| `src/ai-plan-checker.ts` | AI-powered plan completion checker |
|
||||
| `src/bash-tool-parser.ts` | Parses Claude's bash tool invocations from output |
|
||||
| `src/transcript-watcher.ts` | Watches Claude's transcript files for changes |
|
||||
| `src/hooks-config.ts` | Manages `.claude/settings.local.json` hook configuration |
|
||||
| `src/image-watcher.ts` | Watches for image file creation (screenshots, etc.) |
|
||||
| `src/file-stream-manager.ts` | Manages `tail -f` processes for live log viewing |
|
||||
| `src/plan-orchestrator.ts` | Multi-agent plan generation with research and planning phases |
|
||||
| `src/prompts/*.ts` | Agent prompts (research-agent, code-reviewer, planner) |
|
||||
| `src/cli.ts` | Command-line interface handlers |
|
||||
| `src/web/server.ts` | Fastify REST API + SSE at `/api/events` |
|
||||
| `src/web/public/app.js` | Frontend: xterm.js, tab management, subagent windows |
|
||||
| `src/types.ts` | All TypeScript interfaces |
|
||||
@@ -275,3 +278,11 @@ When adding new event listeners or timers:
|
||||
**Frontend**: Store drag/resize handlers on elements, clean up in `close*()` functions. SSE reconnect calls `handleInit()` which resets state.
|
||||
|
||||
Run `npx vitest run test/memory-leak-prevention.test.ts` to verify patterns.
|
||||
|
||||
## Common Workflows
|
||||
|
||||
**Investigating a bug**: Start dev server (`npx tsx src/index.ts web`), reproduce in browser, check terminal output and `~/.claudeman/state.json` for clues.
|
||||
|
||||
**Adding a new API endpoint**: Define types in `types.ts`, add route in `server.ts:buildServer()`, broadcast SSE events if needed, handle in `app.js:handleSSEEvent()`.
|
||||
|
||||
**Modifying respawn behavior**: Study `docs/respawn-state-machine.md` first. The state machine is in `respawn-controller.ts`. Use MockSession from `test/respawn-test-utils.ts` for testing.
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 201 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 362 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 388 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 362 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 10 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 285 KiB |
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "claudeman",
|
||||
"version": "0.1460",
|
||||
"version": "0.1461",
|
||||
"description": "The missing control plane for Claude Code - run 20 autonomous agents with real-time monitoring and session persistence",
|
||||
"type": "module",
|
||||
"main": "dist/index.js",
|
||||
|
||||
@@ -0,0 +1,179 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Take mobile screenshots of Claudeman using Playwright
|
||||
* Emulates iPhone 15 Pro (closest to iPhone 17 Pro)
|
||||
* Includes keyboard simulation
|
||||
*/
|
||||
|
||||
import { chromium, devices } from 'playwright';
|
||||
import { join, dirname } from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { mkdirSync } from 'fs';
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const BASE_URL = 'http://localhost:3000';
|
||||
const OUTPUT_DIR = join(__dirname, '..', 'docs', 'screenshots');
|
||||
|
||||
// Ensure output directory exists
|
||||
try { mkdirSync(OUTPUT_DIR, { recursive: true }); } catch {}
|
||||
|
||||
// iPhone 15 Pro specs (closest to requested iPhone 17 Pro)
|
||||
// Screen: 393 x 852 at 3x = 1179 x 2556 physical pixels
|
||||
const VIEWPORT_WIDTH = 393;
|
||||
const VIEWPORT_HEIGHT = 852;
|
||||
const KEYBOARD_HEIGHT = 336; // Typical iOS keyboard height
|
||||
|
||||
const iPhone15Pro = {
|
||||
...devices['iPhone 14 Pro'],
|
||||
viewport: { width: VIEWPORT_WIDTH, height: VIEWPORT_HEIGHT },
|
||||
deviceScaleFactor: 3,
|
||||
userAgent: 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Mobile/15E148 Safari/604.1',
|
||||
};
|
||||
|
||||
async function takeScreenshots() {
|
||||
console.log('Launching browser with iPhone 15 Pro emulation...');
|
||||
console.log('Output directory:', OUTPUT_DIR);
|
||||
|
||||
const browser = await chromium.launch({ headless: true });
|
||||
const context = await browser.newContext({
|
||||
...iPhone15Pro,
|
||||
hasTouch: true,
|
||||
isMobile: true,
|
||||
});
|
||||
|
||||
const page = await context.newPage();
|
||||
|
||||
try {
|
||||
// Screenshot 1: Main view (no keyboard)
|
||||
console.log('Loading main page...');
|
||||
await page.goto(BASE_URL, { waitUntil: 'domcontentloaded', timeout: 10000 });
|
||||
await page.waitForTimeout(2000);
|
||||
|
||||
await page.screenshot({
|
||||
path: join(OUTPUT_DIR, 'mobile-no-keyboard.png'),
|
||||
fullPage: false
|
||||
});
|
||||
console.log('✓ Saved: mobile-no-keyboard.png');
|
||||
|
||||
// Screenshot 2: Simulate keyboard appearing
|
||||
console.log('Simulating virtual keyboard...');
|
||||
|
||||
// Resize viewport to simulate keyboard taking up space
|
||||
await page.setViewportSize({
|
||||
width: VIEWPORT_WIDTH,
|
||||
height: VIEWPORT_HEIGHT - KEYBOARD_HEIGHT
|
||||
});
|
||||
|
||||
// Trigger the keyboard handler manually
|
||||
await page.evaluate((keyboardHeight) => {
|
||||
// Add keyboard-visible class
|
||||
document.body.classList.add('keyboard-visible');
|
||||
|
||||
// Move toolbar up by keyboard height
|
||||
const toolbar = document.querySelector('.toolbar');
|
||||
if (toolbar) {
|
||||
toolbar.style.transform = `translateY(${-keyboardHeight}px)`;
|
||||
}
|
||||
}, KEYBOARD_HEIGHT);
|
||||
|
||||
await page.waitForTimeout(300);
|
||||
|
||||
// Take screenshot of the reduced viewport (simulating keyboard up)
|
||||
await page.screenshot({
|
||||
path: join(OUTPUT_DIR, 'mobile-keyboard-visible.png'),
|
||||
fullPage: false
|
||||
});
|
||||
console.log('✓ Saved: mobile-keyboard-visible.png');
|
||||
|
||||
// Screenshot 3: Full view with keyboard overlay visualization
|
||||
// Reset viewport but keep toolbar position to show the effect
|
||||
await page.setViewportSize({
|
||||
width: VIEWPORT_WIDTH,
|
||||
height: VIEWPORT_HEIGHT
|
||||
});
|
||||
|
||||
// Add a visual keyboard overlay
|
||||
await page.evaluate((keyboardHeight) => {
|
||||
const overlay = document.createElement('div');
|
||||
overlay.id = 'keyboard-overlay';
|
||||
overlay.style.cssText = `
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: ${keyboardHeight}px;
|
||||
background: linear-gradient(to bottom, #2a2a2a 0%, #1a1a1a 100%);
|
||||
border-top: 1px solid #444;
|
||||
z-index: 9999;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-content: flex-start;
|
||||
padding: 8px 4px;
|
||||
gap: 4px;
|
||||
`;
|
||||
|
||||
// Add fake keyboard keys
|
||||
const rows = [
|
||||
'Q W E R T Y U I O P',
|
||||
'A S D F G H J K L',
|
||||
'⇧ Z X C V B N M ⌫',
|
||||
'123 🌐 space return'
|
||||
];
|
||||
|
||||
rows.forEach((row, rowIndex) => {
|
||||
const rowDiv = document.createElement('div');
|
||||
rowDiv.style.cssText = 'display: flex; width: 100%; justify-content: center; gap: 4px;';
|
||||
|
||||
row.split(' ').forEach(key => {
|
||||
const keyEl = document.createElement('div');
|
||||
const isSpecial = ['⇧', '⌫', '123', '🌐', 'space', 'return'].includes(key);
|
||||
const isSpace = key === 'space';
|
||||
const isReturn = key === 'return';
|
||||
|
||||
keyEl.style.cssText = `
|
||||
background: ${isSpecial ? '#3a3a3a' : '#505050'};
|
||||
color: #fff;
|
||||
border-radius: 5px;
|
||||
padding: ${isSpace ? '10px 60px' : isReturn ? '10px 20px' : '10px 8px'};
|
||||
font-size: ${rowIndex === 3 ? '12px' : '14px'};
|
||||
min-width: ${isSpace ? '120px' : isReturn ? '60px' : '28px'};
|
||||
text-align: center;
|
||||
box-shadow: 0 1px 0 #222;
|
||||
`;
|
||||
keyEl.textContent = isSpace ? '' : key;
|
||||
rowDiv.appendChild(keyEl);
|
||||
});
|
||||
|
||||
overlay.appendChild(rowDiv);
|
||||
});
|
||||
|
||||
document.body.appendChild(overlay);
|
||||
}, KEYBOARD_HEIGHT);
|
||||
|
||||
await page.waitForTimeout(200);
|
||||
|
||||
await page.screenshot({
|
||||
path: join(OUTPUT_DIR, 'mobile-with-keyboard.png'),
|
||||
fullPage: false
|
||||
});
|
||||
console.log('✓ Saved: mobile-with-keyboard.png');
|
||||
|
||||
// Screenshot 4: Toolbar close-up with keyboard
|
||||
const toolbar = page.locator('.toolbar');
|
||||
if (await toolbar.count() > 0) {
|
||||
await toolbar.screenshot({
|
||||
path: join(OUTPUT_DIR, 'mobile-toolbar-above-keyboard.png')
|
||||
});
|
||||
console.log('✓ Saved: mobile-toolbar-above-keyboard.png');
|
||||
}
|
||||
|
||||
console.log('\n✅ All screenshots saved to:', OUTPUT_DIR);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error taking screenshots:', error.message);
|
||||
} finally {
|
||||
await browser.close();
|
||||
}
|
||||
}
|
||||
|
||||
takeScreenshots();
|
||||
@@ -10,7 +10,7 @@
|
||||
|
||||
import { EventEmitter } from 'node:events';
|
||||
import { watch, type FSWatcher } from 'chokidar';
|
||||
import { basename, extname } from 'node:path';
|
||||
import { basename, extname, relative } from 'node:path';
|
||||
import { statSync } from 'node:fs';
|
||||
import type { ImageDetectedEvent } from './types.js';
|
||||
|
||||
@@ -265,10 +265,14 @@ export class ImageWatcher extends EventEmitter {
|
||||
try {
|
||||
const stat = statSync(filePath);
|
||||
const fileName = basename(filePath);
|
||||
const workingDir = this.sessionDirs.get(sessionId);
|
||||
// Compute relative path from working directory (for file-raw endpoint)
|
||||
const relativePath = workingDir ? relative(workingDir, filePath) : fileName;
|
||||
|
||||
const event: ImageDetectedEvent = {
|
||||
sessionId,
|
||||
filePath,
|
||||
relativePath,
|
||||
fileName,
|
||||
timestamp: Date.now(),
|
||||
size: stat.size,
|
||||
|
||||
@@ -1511,6 +1511,8 @@ export interface ImageDetectedEvent {
|
||||
sessionId: string;
|
||||
/** Full path to the detected image file */
|
||||
filePath: string;
|
||||
/** Path relative to the session's working directory (for file-raw endpoint) */
|
||||
relativePath: string;
|
||||
/** Image file name (basename) */
|
||||
fileName: string;
|
||||
/** Timestamp when the image was detected */
|
||||
|
||||
+56
-8
@@ -185,18 +185,20 @@ function getEventCoords(e) {
|
||||
/**
|
||||
* KeyboardHandler - Simple handler to scroll inputs into view when keyboard appears.
|
||||
* Uses focusin event and scrollIntoView - keeps it simple and reliable.
|
||||
* Also handles terminal scrolling via visualViewport API.
|
||||
* Also handles terminal scrolling and toolbar repositioning via visualViewport API.
|
||||
*/
|
||||
const KeyboardHandler = {
|
||||
lastViewportHeight: 0,
|
||||
keyboardVisible: false,
|
||||
initialViewportHeight: 0,
|
||||
|
||||
/** Initialize keyboard handling */
|
||||
init() {
|
||||
// Only initialize on touch devices
|
||||
if (!MobileDetection.isTouchDevice()) return;
|
||||
|
||||
this.lastViewportHeight = window.visualViewport?.height || window.innerHeight;
|
||||
this.initialViewportHeight = window.visualViewport?.height || window.innerHeight;
|
||||
this.lastViewportHeight = this.initialViewportHeight;
|
||||
|
||||
// Simple focus handler - scroll input into view after keyboard appears
|
||||
document.addEventListener('focusin', (e) => {
|
||||
@@ -209,32 +211,68 @@ const KeyboardHandler = {
|
||||
}, 400);
|
||||
});
|
||||
|
||||
// Use visualViewport to detect keyboard for terminal scrolling
|
||||
// Use visualViewport to detect keyboard and reposition toolbar
|
||||
if (window.visualViewport) {
|
||||
window.visualViewport.addEventListener('resize', () => {
|
||||
this.handleViewportResize();
|
||||
});
|
||||
// Also handle scroll (iOS scrolls viewport when keyboard appears)
|
||||
window.visualViewport.addEventListener('scroll', () => {
|
||||
this.updateToolbarPosition();
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
/** Handle viewport resize (keyboard show/hide) */
|
||||
handleViewportResize() {
|
||||
const currentHeight = window.visualViewport?.height || window.innerHeight;
|
||||
const heightDiff = this.lastViewportHeight - currentHeight;
|
||||
const heightDiff = this.initialViewportHeight - currentHeight;
|
||||
|
||||
// Keyboard appeared (viewport shrunk by more than 150px)
|
||||
if (heightDiff > 150 && !this.keyboardVisible) {
|
||||
this.keyboardVisible = true;
|
||||
document.body.classList.add('keyboard-visible');
|
||||
this.onKeyboardShow();
|
||||
}
|
||||
// Keyboard hidden (viewport grew back)
|
||||
else if (heightDiff < -150 && this.keyboardVisible) {
|
||||
// Keyboard hidden (viewport grew back close to initial)
|
||||
else if (heightDiff < 50 && this.keyboardVisible) {
|
||||
this.keyboardVisible = false;
|
||||
document.body.classList.remove('keyboard-visible');
|
||||
this.onKeyboardHide();
|
||||
}
|
||||
|
||||
this.updateToolbarPosition();
|
||||
this.lastViewportHeight = currentHeight;
|
||||
},
|
||||
|
||||
/** Update toolbar position based on visual viewport */
|
||||
updateToolbarPosition() {
|
||||
if (!window.visualViewport) return;
|
||||
|
||||
const toolbar = document.querySelector('.toolbar');
|
||||
if (!toolbar) return;
|
||||
|
||||
// Only reposition on mobile
|
||||
if (!MobileDetection.isSmallScreen() && !MobileDetection.isMediumScreen()) {
|
||||
toolbar.style.transform = '';
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.keyboardVisible) {
|
||||
// The toolbar is fixed at bottom: 0 (relative to layout viewport)
|
||||
// We need to move it up to be at the bottom of the visual viewport
|
||||
// Distance from visual viewport bottom to layout viewport bottom:
|
||||
const layoutHeight = window.innerHeight;
|
||||
const visualBottom = window.visualViewport.offsetTop + window.visualViewport.height;
|
||||
const offsetFromBottom = layoutHeight - visualBottom;
|
||||
|
||||
// Move toolbar up by this offset (negative translateY moves up)
|
||||
toolbar.style.transform = `translateY(${-offsetFromBottom}px)`;
|
||||
} else {
|
||||
toolbar.style.transform = '';
|
||||
}
|
||||
},
|
||||
|
||||
/** Called when keyboard appears */
|
||||
onKeyboardShow() {
|
||||
// Scroll active terminal to bottom so input line is visible
|
||||
@@ -248,6 +286,15 @@ const KeyboardHandler = {
|
||||
}, 100);
|
||||
},
|
||||
|
||||
/** Called when keyboard hides */
|
||||
onKeyboardHide() {
|
||||
// Reset toolbar position
|
||||
const toolbar = document.querySelector('.toolbar');
|
||||
if (toolbar) {
|
||||
toolbar.style.transform = '';
|
||||
}
|
||||
},
|
||||
|
||||
/** Check if element is an input that triggers keyboard (excludes terminal) */
|
||||
isInputElement(el) {
|
||||
if (!el) return false;
|
||||
@@ -12803,7 +12850,7 @@ class ClaudemanApp {
|
||||
* Called automatically when image:detected SSE event is received.
|
||||
*/
|
||||
openImagePopup(imageEvent) {
|
||||
const { sessionId, filePath, fileName, timestamp, size } = imageEvent;
|
||||
const { sessionId, filePath, relativePath, fileName, timestamp, size } = imageEvent;
|
||||
|
||||
// Create unique window ID
|
||||
const imageId = `${sessionId}-${timestamp}`;
|
||||
@@ -12830,7 +12877,8 @@ class ClaudemanApp {
|
||||
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)}`;
|
||||
// Use relativePath (path from working dir) instead of fileName (basename) for subdirectory images
|
||||
const imageUrl = `/api/sessions/${sessionId}/file-raw?path=${encodeURIComponent(relativePath || fileName)}`;
|
||||
|
||||
// Create window element
|
||||
const win = document.createElement('div');
|
||||
|
||||
@@ -8056,7 +8056,7 @@ kbd {
|
||||
|
||||
/* Clean dark toolbar - positioned near bottom */
|
||||
.toolbar {
|
||||
position: absolute;
|
||||
position: fixed;
|
||||
bottom: var(--safe-area-bottom);
|
||||
left: 0;
|
||||
right: 0;
|
||||
@@ -8073,6 +8073,10 @@ kbd {
|
||||
background: #111111;
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.1);
|
||||
z-index: 50;
|
||||
/* Smooth transition for keyboard show/hide */
|
||||
transition: transform 0.15s ease-out;
|
||||
/* Ensure transform works properly */
|
||||
will-change: transform;
|
||||
}
|
||||
|
||||
/* Show case selector in center */
|
||||
@@ -8438,6 +8442,18 @@ kbd {
|
||||
scroll-margin-bottom: 200px;
|
||||
scroll-margin-top: 80px;
|
||||
}
|
||||
|
||||
/* When keyboard is visible, adjust content to account for moved toolbar */
|
||||
.keyboard-visible .terminal-container {
|
||||
/* Reduce terminal height when keyboard is up so it doesn't overlap toolbar */
|
||||
padding-bottom: 50px;
|
||||
}
|
||||
|
||||
/* Ensure modals scroll properly when keyboard is visible */
|
||||
.keyboard-visible .modal-body {
|
||||
max-height: 40vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user