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:
arkon
2026-01-31 14:32:20 +01:00
parent c7488c4a56
commit b655d2e29e
13 changed files with 278 additions and 18 deletions
+5 -1
View File
@@ -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,
+2
View File
@@ -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
View File
@@ -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');
+17 -1
View File
@@ -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;
}
}