chore: bump version to 0.1521

This commit is contained in:
arkon
2026-02-17 07:08:26 +01:00
parent ea3e534607
commit 28b175c74a
5 changed files with 144 additions and 247 deletions
+1 -1
View File
@@ -35,7 +35,7 @@ When user says "COM":
1. Increment version in BOTH `package.json` AND `CLAUDE.md` (verify they match with `grep version package.json && grep Version 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.1520 (must match `package.json` for npm publish)
**Version**: 0.1521 (must match `package.json` for npm publish)
## Project Overview
-174
View File
@@ -1,174 +0,0 @@
import { chromium } from 'playwright';
const BASE = 'http://localhost:3000';
// iPhone 14 Pro viewport
const MOBILE_VP = { width: 393, height: 852 };
// iPad viewport
const TABLET_VP = { width: 768, height: 1024 };
async function testMobile(page, label, vp) {
await page.setViewportSize(vp);
await page.goto(BASE, { waitUntil: 'domcontentloaded' });
await page.waitForTimeout(2000);
const results = [];
// 1. Check settings modal renders and tabs are visible
// Open settings via mobile toolbar button or header button
const settingsBtn = await page.$('.btn-settings-mobile') || await page.$('.btn-settings');
if (settingsBtn) {
await settingsBtn.click();
await page.waitForTimeout(500);
const modal = await page.$('#appSettingsModal.active');
results.push({ test: `${label}: Settings modal opens`, pass: !!modal });
if (modal) {
// Check tab buttons are visible and not overflowing
const tabs = await page.$$('#appSettingsModal .modal-tab-btn');
results.push({ test: `${label}: Settings has ${tabs.length} tabs`, pass: tabs.length === 5 });
// Check first tab (Display) content is visible
const displayTab = await page.$('#settings-display');
const displayVisible = displayTab ? await displayTab.isVisible() : false;
results.push({ test: `${label}: Display tab content visible`, pass: displayVisible });
// Check settings-grid layout
const settingsGrid = await page.$('#settings-display .settings-grid');
if (settingsGrid) {
const gridStyle = await settingsGrid.evaluate(el => {
const cs = window.getComputedStyle(el);
return cs.gridTemplateColumns;
});
const isSingleCol = vp.width < 430;
if (isSingleCol) {
// On phone, should be single column
const colCount = gridStyle.split(' ').length;
results.push({ test: `${label}: Settings grid is single-col (${colCount} cols)`, pass: colCount === 1 });
} else {
results.push({ test: `${label}: Settings grid columns = ${gridStyle}`, pass: true });
}
}
// Switch to notifications tab and check event grid
const notifTab = await page.$('[data-tab="settings-notifications"]');
if (notifTab) {
await notifTab.click();
await page.waitForTimeout(300);
const eventGrid = await page.$('.event-type-grid');
if (eventGrid) {
const eventGridVisible = await eventGrid.isVisible();
results.push({ test: `${label}: Event type grid visible`, pass: eventGridVisible });
const gridCols = await eventGrid.evaluate(el => {
return window.getComputedStyle(el).gridTemplateColumns;
});
results.push({ test: `${label}: Event grid cols = ${gridCols}`, pass: true });
}
}
// Check modal tab scrollability
const tabsContainer = await page.$('#appSettingsModal .modal-tabs');
if (tabsContainer) {
const overflow = await tabsContainer.evaluate(el => {
const cs = window.getComputedStyle(el);
return { overflowX: cs.overflowX, flexWrap: cs.flexWrap };
});
results.push({ test: `${label}: Tab bar overflow-x=${overflow.overflowX}, flex-wrap=${overflow.flexWrap}`, pass: overflow.overflowX === 'auto' || overflow.flexWrap === 'nowrap' });
}
// Close modal
const closeBtn = await page.$('#appSettingsModal .modal-close');
if (closeBtn) await closeBtn.click();
await page.waitForTimeout(300);
}
} else {
results.push({ test: `${label}: Settings button found`, pass: false });
}
// 2. Check subagent badge sizing
// We can check the CSS computed style for .tab-subagent-badge if any exist
const badges = await page.$$('.tab-subagent-badge');
if (badges.length > 0) {
const badgeHeight = await badges[0].evaluate(el => {
return parseFloat(window.getComputedStyle(el).height);
});
if (vp.width < 430) {
results.push({ test: `${label}: Subagent badge height=${badgeHeight}px (expect ~14)`, pass: badgeHeight <= 16 });
} else {
results.push({ test: `${label}: Subagent badge height=${badgeHeight}px`, pass: true });
}
} else {
results.push({ test: `${label}: No subagent badges to check (OK)`, pass: true });
}
// 3. Check subagent windows if any are open
const subWindows = await page.$$('.subagent-window');
if (subWindows.length > 0 && vp.width < 430) {
for (const sw of subWindows) {
const swStyle = await sw.evaluate(el => {
const cs = window.getComputedStyle(el);
return {
position: cs.position,
width: cs.width,
borderRadius: cs.borderRadius,
};
});
results.push({ test: `${label}: Subagent window position=${swStyle.position}, width=${swStyle.width}`, pass: swStyle.position === 'fixed' });
}
}
// 4. Check header height
const header = await page.$('.header');
if (header) {
const headerHeight = await header.evaluate(el => {
return parseFloat(window.getComputedStyle(el).height);
});
const expectedMax = vp.width < 430 ? 40 : 55;
results.push({ test: `${label}: Header height=${headerHeight}px (max ${expectedMax})`, pass: headerHeight <= expectedMax });
}
// 5. Check toolbar is fixed at bottom on mobile
if (vp.width < 430) {
const toolbar = await page.$('.toolbar');
if (toolbar) {
const toolbarPos = await toolbar.evaluate(el => {
return window.getComputedStyle(el).position;
});
results.push({ test: `${label}: Toolbar position=${toolbarPos}`, pass: toolbarPos === 'fixed' });
}
}
return results;
}
(async () => {
const browser = await chromium.launch({ headless: true });
try {
// Test mobile viewport
const mobilePage = await browser.newPage();
const mobileResults = await testMobile(mobilePage, 'Phone (393px)', MOBILE_VP);
await mobilePage.close();
// Test tablet viewport
const tabletPage = await browser.newPage();
const tabletResults = await testMobile(tabletPage, 'Tablet (768px)', TABLET_VP);
await tabletPage.close();
const all = [...mobileResults, ...tabletResults];
const passed = all.filter(r => r.pass).length;
const failed = all.filter(r => !r.pass).length;
console.log('\n=== Mobile Optimization Test Results ===\n');
for (const r of all) {
console.log(` ${r.pass ? '✓' : '✗'} ${r.test}`);
}
console.log(`\n ${passed} passed, ${failed} failed out of ${all.length} tests\n`);
if (failed > 0) process.exit(1);
} finally {
await browser.close();
}
})();
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "claudeman",
"version": "0.1520",
"version": "0.1521",
"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",
+110 -34
View File
@@ -6007,6 +6007,11 @@ class ClaudemanApp {
if (windowData.dragListeners) {
document.removeEventListener('mousemove', windowData.dragListeners.move);
document.removeEventListener('mouseup', windowData.dragListeners.up);
if (windowData.dragListeners.touchMove) {
document.removeEventListener('touchmove', windowData.dragListeners.touchMove);
document.removeEventListener('touchend', windowData.dragListeners.up);
document.removeEventListener('touchcancel', windowData.dragListeners.up);
}
}
if (windowData.element) {
windowData.element.remove();
@@ -12287,8 +12292,10 @@ class ClaudemanApp {
// Calculate final position - grid layout to avoid overlaps
const windowCount = this.subagentWindows.size;
const isMobile = MobileDetection.getDeviceType() === 'mobile';
const mobileCardHeight = 110;
const mobileCardGap = 4;
const windowWidth = isMobile ? window.innerWidth : 420;
const windowHeight = isMobile ? Math.round(window.innerHeight * 0.4) : 350;
const windowHeight = isMobile ? mobileCardHeight : 350;
const gap = 20;
const viewportWidth = window.innerWidth;
const viewportHeight = window.innerHeight;
@@ -12297,16 +12304,16 @@ class ClaudemanApp {
let finalY = 0;
if (isMobile) {
// Mobile: CSS handles full-width bottom-sheet positioning via .subagent-window rules.
// Only one window visible at a time on mobile — new window replaces the old.
// Minimize all existing windows first so only the latest is visible.
for (const [existingId, existingData] of this.subagentWindows) {
if (!existingData.minimized) {
this.minimizeSubagentWindow(existingId);
}
// Mobile: stack compact cards above toolbar. Count visible (non-minimized) windows.
let visibleCount = 0;
for (const [, data] of this.subagentWindows) {
if (!data.minimized && !data.hidden) visibleCount++;
}
finalX = 0;
finalY = viewportHeight - windowHeight - 40; // toolbar height
const toolbarHeight = 40;
// Stack from bottom: first card just above toolbar, next above that, etc.
const bottomOffset = toolbarHeight + visibleCount * (mobileCardHeight + mobileCardGap);
finalX = 4;
finalY = viewportHeight - bottomOffset - mobileCardHeight;
} else {
// Check if Ralph wizard modal is open - if so, position windows on the sides
const wizardModal = document.getElementById('ralphWizardModal');
@@ -12419,25 +12426,32 @@ class ClaudemanApp {
`;
// If we have a parent tab, start window at tab position for spawn animation
// On mobile, skip spawn animation — CSS handles bottom-sheet positioning
if (parentTab && !isMobile) {
if (isMobile) {
// Mobile: position as stacked card via inline bottom offset
let visibleCount = 0;
for (const [, data] of this.subagentWindows) {
if (!data.minimized && !data.hidden) visibleCount++;
}
const bottomPx = 40 + visibleCount * (mobileCardHeight + mobileCardGap);
win.style.bottom = `${bottomPx}px`;
win.style.top = 'auto';
} else if (parentTab) {
const tabRect = parentTab.getBoundingClientRect();
win.style.left = `${tabRect.left}px`;
win.style.top = `${tabRect.bottom}px`;
win.style.transform = 'scale(0.3)';
win.style.opacity = '0';
win.classList.add('spawning');
} else if (!isMobile) {
} else {
// No parent tab, just position normally (desktop/tablet)
win.style.left = `${finalX}px`;
win.style.top = `${finalY}px`;
}
// On mobile: CSS positions via fixed bottom-sheet rules, no inline positioning needed
document.body.appendChild(win);
// Make draggable (returns listener refs for cleanup) — skip on mobile (bottom-sheet)
const dragListeners = isMobile ? null : this.makeWindowDraggable(win, win.querySelector('.subagent-window-header'));
// Make draggable (returns listener refs for cleanup)
const dragListeners = this.makeWindowDraggable(win, win.querySelector('.subagent-window-header'));
// Check if this window should be visible based on settings
// Use the PERSISTENT parent map for accurate tab-based visibility
@@ -12588,6 +12602,11 @@ class ClaudemanApp {
if (windowData.dragListeners) {
document.removeEventListener('mousemove', windowData.dragListeners.move);
document.removeEventListener('mouseup', windowData.dragListeners.up);
if (windowData.dragListeners.touchMove) {
document.removeEventListener('touchmove', windowData.dragListeners.touchMove);
document.removeEventListener('touchend', windowData.dragListeners.up);
document.removeEventListener('touchcancel', windowData.dragListeners.up);
}
}
windowData.element.remove();
this.subagentWindows.delete(agentId);
@@ -12615,6 +12634,11 @@ class ClaudemanApp {
if (windowData.dragListeners) {
document.removeEventListener('mousemove', windowData.dragListeners.move);
document.removeEventListener('mouseup', windowData.dragListeners.up);
if (windowData.dragListeners.touchMove) {
document.removeEventListener('touchmove', windowData.dragListeners.touchMove);
document.removeEventListener('touchend', windowData.dragListeners.up);
document.removeEventListener('touchcancel', windowData.dragListeners.up);
}
}
windowData.element.remove();
}
@@ -12638,6 +12662,11 @@ class ClaudemanApp {
if (data.dragListeners) {
document.removeEventListener('mousemove', data.dragListeners.move);
document.removeEventListener('mouseup', data.dragListeners.up);
if (data.dragListeners.touchMove) {
document.removeEventListener('touchmove', data.dragListeners.touchMove);
document.removeEventListener('touchend', data.dragListeners.up);
document.removeEventListener('touchcancel', data.dragListeners.up);
}
}
data.element.remove();
}
@@ -12676,6 +12705,11 @@ class ClaudemanApp {
if (popupData.dragListeners) {
document.removeEventListener('mousemove', popupData.dragListeners.move);
document.removeEventListener('mouseup', popupData.dragListeners.up);
if (popupData.dragListeners.touchMove) {
document.removeEventListener('touchmove', popupData.dragListeners.touchMove);
document.removeEventListener('touchend', popupData.dragListeners.up);
document.removeEventListener('touchcancel', popupData.dragListeners.up);
}
}
popupData.element.remove();
}
@@ -12709,6 +12743,11 @@ class ClaudemanApp {
if (this.teamTasksDragListeners) {
document.removeEventListener('mousemove', this.teamTasksDragListeners.move);
document.removeEventListener('mouseup', this.teamTasksDragListeners.up);
if (this.teamTasksDragListeners.touchMove) {
document.removeEventListener('touchmove', this.teamTasksDragListeners.touchMove);
document.removeEventListener('touchend', this.teamTasksDragListeners.up);
document.removeEventListener('touchcancel', this.teamTasksDragListeners.up);
}
this.teamTasksDragListeners = null;
}
@@ -12772,28 +12811,27 @@ class ClaudemanApp {
let startX, startY, startLeft, startTop;
let dragUpdateScheduled = false;
handle.addEventListener('mousedown', (e) => {
if (e.target.tagName === 'BUTTON') return;
const startDrag = (clientX, clientY) => {
isDragging = true;
startX = e.clientX;
startY = e.clientY;
startLeft = parseInt(win.style.left) || 0;
startTop = parseInt(win.style.top) || 0;
e.preventDefault();
});
startX = clientX;
startY = clientY;
startLeft = parseInt(win.style.left) || win.getBoundingClientRect().left;
startTop = parseInt(win.style.top) || win.getBoundingClientRect().top;
// On drag start, switch from bottom-positioned to top-positioned so left/top work
win.style.bottom = 'auto';
};
// Store references to document-level listeners so they can be removed on window close
const moveListener = (e) => {
const moveDrag = (clientX, clientY) => {
if (!isDragging) return;
const dx = e.clientX - startX;
const dy = e.clientY - startY;
const dx = clientX - startX;
const dy = clientY - startY;
// Constrain to viewport bounds
const winWidth = win.offsetWidth || 420;
const winHeight = win.offsetHeight || 350;
const maxX = window.innerWidth - winWidth - 10;
const maxY = window.innerHeight - winHeight - 10;
const newLeft = Math.max(10, Math.min(startLeft + dx, maxX));
const newTop = Math.max(10, Math.min(startTop + dy, maxY));
const maxX = window.innerWidth - winWidth - 4;
const maxY = window.innerHeight - winHeight - 4;
const newLeft = Math.max(4, Math.min(startLeft + dx, maxX));
const newTop = Math.max(4, Math.min(startTop + dy, maxY));
win.style.left = `${newLeft}px`;
win.style.top = `${newTop}px`;
// Throttle connection line updates during drag
@@ -12806,7 +12844,7 @@ class ClaudemanApp {
}
};
const upListener = () => {
const endDrag = () => {
if (isDragging) {
isDragging = false;
// Save position after drag ends
@@ -12814,11 +12852,44 @@ class ClaudemanApp {
}
};
// Mouse events
handle.addEventListener('mousedown', (e) => {
if (e.target.tagName === 'BUTTON') return;
startDrag(e.clientX, e.clientY);
e.preventDefault();
});
// Touch events
handle.addEventListener('touchstart', (e) => {
if (e.target.tagName === 'BUTTON') return;
const touch = e.touches[0];
startDrag(touch.clientX, touch.clientY);
}, { passive: true });
// Store references to document-level listeners so they can be removed on window close
const moveListener = (e) => {
moveDrag(e.clientX, e.clientY);
};
const touchMoveListener = (e) => {
if (!isDragging) return;
e.preventDefault(); // Prevent page scroll while dragging
const touch = e.touches[0];
moveDrag(touch.clientX, touch.clientY);
};
const upListener = () => {
endDrag();
};
document.addEventListener('mousemove', moveListener);
document.addEventListener('mouseup', upListener);
document.addEventListener('touchmove', touchMoveListener, { passive: false });
document.addEventListener('touchend', upListener);
document.addEventListener('touchcancel', upListener);
// Return listener references for cleanup
return { move: moveListener, up: upListener };
return { move: moveListener, up: upListener, touchMove: touchMoveListener };
}
renderSubagentWindowContent(agentId) {
@@ -13355,6 +13426,11 @@ class ClaudemanApp {
if (this.teamTasksDragListeners) {
document.removeEventListener('mousemove', this.teamTasksDragListeners.move);
document.removeEventListener('mouseup', this.teamTasksDragListeners.up);
if (this.teamTasksDragListeners.touchMove) {
document.removeEventListener('touchmove', this.teamTasksDragListeners.touchMove);
document.removeEventListener('touchend', this.teamTasksDragListeners.up);
document.removeEventListener('touchcancel', this.teamTasksDragListeners.up);
}
this.teamTasksDragListeners = null;
}
}
+32 -37
View File
@@ -1211,22 +1211,18 @@ html.mobile-init .file-browser-panel {
/* ---- Subagent Windows: Mobile Optimizations ---- */
/* Full-width stacked windows at bottom on phones */
/* Compact stacked cards — multiple visible at once, draggable via touch */
.subagent-window {
position: fixed !important;
left: 0 !important;
right: 0 !important;
width: 100% !important;
max-width: 100% !important;
height: 40vh !important;
min-height: 150px !important;
max-height: 50vh !important;
bottom: calc(40px + var(--safe-area-bottom)) !important;
top: auto !important;
border-radius: 8px 8px 0 0;
position: fixed;
width: calc(100% - 8px);
max-width: calc(100% - 8px);
height: 110px;
min-height: 80px;
max-height: 110px;
min-width: 200px;
border-radius: 6px;
resize: none;
box-shadow: 0 -4px 16px rgba(0, 0, 0, 0.5);
transform: none !important;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.4);
}
/* Hide resize handle on mobile */
@@ -1236,21 +1232,21 @@ html.mobile-init .file-browser-panel {
/* Compact subagent window header */
.subagent-window-header {
padding: 0.3rem 0.5rem;
min-height: 32px;
padding: 0.2rem 0.4rem;
min-height: 24px;
}
.subagent-window-title {
gap: 0.25rem;
gap: 0.2rem;
overflow: hidden;
}
.subagent-window-title .icon {
font-size: 0.8rem;
font-size: 0.7rem;
}
.subagent-window-title .id {
font-size: 0.65rem;
font-size: 0.6rem;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
@@ -1258,51 +1254,50 @@ html.mobile-init .file-browser-panel {
}
.subagent-window-title .status {
font-size: 0.5rem;
padding: 0.05rem 0.2rem;
font-size: 0.45rem;
padding: 0.05rem 0.15rem;
}
.subagent-model-badge {
font-size: 0.5rem !important;
padding: 0.05rem 0.2rem !important;
font-size: 0.45rem !important;
padding: 0.05rem 0.15rem !important;
}
.subagent-window-actions button {
font-size: 0.8rem;
padding: 0.3rem 0.5rem;
min-width: 32px;
min-height: 32px;
font-size: 0.7rem;
padding: 0.15rem 0.35rem;
min-width: 26px;
min-height: 26px;
display: flex;
align-items: center;
justify-content: center;
}
.subagent-window-body {
font-size: 0.65rem;
padding: 0.35rem;
font-size: 0.6rem;
padding: 0.2rem 0.35rem;
}
.subagent-window-body .activity-line {
gap: 0.2rem;
padding: 0.1rem 0;
gap: 0.15rem;
padding: 0.05rem 0;
}
.subagent-window-body .activity-line .time {
font-size: 0.55rem;
font-size: 0.5rem;
}
.subagent-window-body .activity-line .tool-name {
font-size: 0.6rem;
font-size: 0.55rem;
}
.subagent-window-body .activity-line .tool-detail {
font-size: 0.6rem;
font-size: 0.55rem;
}
/* Parent label in subagent window - compact */
/* Hide parent label on mobile to save space */
.subagent-window-parent {
padding: 0.15rem 0.5rem;
font-size: 0.6rem;
display: none;
}
/* ---- Tab Subagent Badges: Smaller on Mobile ---- */