mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-09-30 20:49:41 +02:00
chore: bump version to 0.1520
This commit is contained in:
@@ -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.1519 (must match `package.json` for npm publish)
|
||||
**Version**: 0.1520 (must match `package.json` for npm publish)
|
||||
|
||||
## Project Overview
|
||||
|
||||
|
||||
+174
@@ -0,0 +1,174 @@
|
||||
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
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "claudeman",
|
||||
"version": "0.1519",
|
||||
"version": "0.1520",
|
||||
"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",
|
||||
|
||||
+76
-56
@@ -9436,7 +9436,7 @@ class ClaudemanApp {
|
||||
// Default model
|
||||
const defaultModelEl = document.getElementById('appSettingsDefaultModel');
|
||||
if (defaultModelEl) {
|
||||
defaultModelEl.value = config.defaultModel || 'sonnet';
|
||||
defaultModelEl.value = config.defaultModel || 'opus';
|
||||
}
|
||||
// Show recommendations
|
||||
const showRecsEl = document.getElementById('appSettingsShowModelRecommendations');
|
||||
@@ -9475,7 +9475,7 @@ class ClaudemanApp {
|
||||
if (reviewEl?.value) agentTypeOverrides.review = reviewEl.value;
|
||||
|
||||
const config = {
|
||||
defaultModel: defaultModelEl?.value || 'sonnet',
|
||||
defaultModel: defaultModelEl?.value || 'opus',
|
||||
showRecommendations: showRecsEl?.checked ?? true,
|
||||
agentTypeOverrides,
|
||||
};
|
||||
@@ -12286,60 +12286,77 @@ class ClaudemanApp {
|
||||
|
||||
// Calculate final position - grid layout to avoid overlaps
|
||||
const windowCount = this.subagentWindows.size;
|
||||
const windowWidth = 420;
|
||||
const windowHeight = 350;
|
||||
const isMobile = MobileDetection.getDeviceType() === 'mobile';
|
||||
const windowWidth = isMobile ? window.innerWidth : 420;
|
||||
const windowHeight = isMobile ? Math.round(window.innerHeight * 0.4) : 350;
|
||||
const gap = 20;
|
||||
const viewportWidth = window.innerWidth;
|
||||
const viewportHeight = window.innerHeight;
|
||||
|
||||
// Check if Ralph wizard modal is open - if so, position windows on the sides
|
||||
const wizardModal = document.getElementById('ralphWizardModal');
|
||||
const wizardOpen = wizardModal?.classList.contains('active');
|
||||
let finalX = 0;
|
||||
let finalY = 0;
|
||||
|
||||
let startX, startY, maxCols;
|
||||
|
||||
if (wizardOpen) {
|
||||
// Wizard is ~720px wide, centered. Position windows on left/right sides
|
||||
const wizardWidth = 720;
|
||||
const centerX = viewportWidth / 2;
|
||||
const wizardLeft = centerX - wizardWidth / 2;
|
||||
const wizardRight = centerX + wizardWidth / 2;
|
||||
|
||||
// Alternate between left and right sides of the wizard
|
||||
const leftSideSpace = wizardLeft - 20;
|
||||
const rightSideSpace = viewportWidth - wizardRight - 20;
|
||||
|
||||
if (windowCount % 2 === 0 && rightSideSpace >= windowWidth) {
|
||||
// Even windows go to the right
|
||||
startX = wizardRight + 20;
|
||||
maxCols = Math.floor(rightSideSpace / (windowWidth + gap)) || 1;
|
||||
} else if (leftSideSpace >= windowWidth) {
|
||||
// Odd windows go to the left
|
||||
startX = Math.max(10, wizardLeft - windowWidth - 20);
|
||||
maxCols = 1; // Usually only room for 1 column on left
|
||||
} else {
|
||||
// Not enough side space, use right side
|
||||
startX = wizardRight + 20;
|
||||
maxCols = 1;
|
||||
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);
|
||||
}
|
||||
}
|
||||
startY = 80; // Start higher when wizard is open
|
||||
finalX = 0;
|
||||
finalY = viewportHeight - windowHeight - 40; // toolbar height
|
||||
} else {
|
||||
// Normal positioning
|
||||
startX = 50;
|
||||
startY = 120;
|
||||
maxCols = Math.floor((viewportWidth - startX - 50) / (windowWidth + gap)) || 1;
|
||||
// Check if Ralph wizard modal is open - if so, position windows on the sides
|
||||
const wizardModal = document.getElementById('ralphWizardModal');
|
||||
const wizardOpen = wizardModal?.classList.contains('active');
|
||||
|
||||
let startX, startY, maxCols;
|
||||
|
||||
if (wizardOpen) {
|
||||
// Wizard is ~720px wide, centered. Position windows on left/right sides
|
||||
const wizardWidth = 720;
|
||||
const centerX = viewportWidth / 2;
|
||||
const wizardLeft = centerX - wizardWidth / 2;
|
||||
const wizardRight = centerX + wizardWidth / 2;
|
||||
|
||||
// Alternate between left and right sides of the wizard
|
||||
const leftSideSpace = wizardLeft - 20;
|
||||
const rightSideSpace = viewportWidth - wizardRight - 20;
|
||||
|
||||
if (windowCount % 2 === 0 && rightSideSpace >= windowWidth) {
|
||||
// Even windows go to the right
|
||||
startX = wizardRight + 20;
|
||||
maxCols = Math.floor(rightSideSpace / (windowWidth + gap)) || 1;
|
||||
} else if (leftSideSpace >= windowWidth) {
|
||||
// Odd windows go to the left
|
||||
startX = Math.max(10, wizardLeft - windowWidth - 20);
|
||||
maxCols = 1; // Usually only room for 1 column on left
|
||||
} else {
|
||||
// Not enough side space, use right side
|
||||
startX = wizardRight + 20;
|
||||
maxCols = 1;
|
||||
}
|
||||
startY = 80; // Start higher when wizard is open
|
||||
} else {
|
||||
// Normal positioning
|
||||
startX = 50;
|
||||
startY = 120;
|
||||
maxCols = Math.floor((viewportWidth - startX - 50) / (windowWidth + gap)) || 1;
|
||||
}
|
||||
|
||||
const maxRows = Math.floor((viewportHeight - startY - 50) / (windowHeight + gap)) || 1;
|
||||
const col = windowCount % maxCols;
|
||||
const row = Math.floor(windowCount / maxCols) % maxRows; // Wrap rows to stay in viewport
|
||||
finalX = startX + col * (windowWidth + gap);
|
||||
finalY = startY + row * (windowHeight + gap);
|
||||
|
||||
// Ensure window stays within viewport bounds
|
||||
finalX = Math.max(10, Math.min(finalX, viewportWidth - windowWidth - 10));
|
||||
finalY = Math.max(10, Math.min(finalY, viewportHeight - windowHeight - 10));
|
||||
}
|
||||
|
||||
const maxRows = Math.floor((viewportHeight - startY - 50) / (windowHeight + gap)) || 1;
|
||||
const col = windowCount % maxCols;
|
||||
const row = Math.floor(windowCount / maxCols) % maxRows; // Wrap rows to stay in viewport
|
||||
let finalX = startX + col * (windowWidth + gap);
|
||||
let finalY = startY + row * (windowHeight + gap);
|
||||
|
||||
// Ensure window stays within viewport bounds
|
||||
finalX = Math.max(10, Math.min(finalX, viewportWidth - windowWidth - 10));
|
||||
finalY = Math.max(10, Math.min(finalY, viewportHeight - windowHeight - 10));
|
||||
|
||||
// Get parent session from PERSISTENT map (THE source of truth for tab connections)
|
||||
const parentSessionId = this.subagentParentMap.get(agentId) || agent.parentSessionId;
|
||||
let parentSessionName = null;
|
||||
@@ -12378,7 +12395,8 @@ class ClaudemanApp {
|
||||
|
||||
const teammateInfo = this.getTeammateInfo(agent);
|
||||
const windowTitle = teammateInfo ? teammateInfo.name : (agent.description || agentId.substring(0, 7));
|
||||
const truncatedTitle = windowTitle.length > 50 ? windowTitle.substring(0, 50) + '...' : windowTitle;
|
||||
const maxTitleLen = isMobile ? 30 : 50;
|
||||
const truncatedTitle = windowTitle.length > maxTitleLen ? windowTitle.substring(0, maxTitleLen) + '...' : windowTitle;
|
||||
const modelBadge = agent.modelShort
|
||||
? `<span class="subagent-model-badge ${agent.modelShort}">${agent.modelShort}</span>`
|
||||
: '';
|
||||
@@ -12401,23 +12419,25 @@ class ClaudemanApp {
|
||||
`;
|
||||
|
||||
// If we have a parent tab, start window at tab position for spawn animation
|
||||
if (parentTab) {
|
||||
// On mobile, skip spawn animation — CSS handles bottom-sheet positioning
|
||||
if (parentTab && !isMobile) {
|
||||
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 {
|
||||
// No parent tab, just position normally
|
||||
} else if (!isMobile) {
|
||||
// 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)
|
||||
const dragListeners = this.makeWindowDraggable(win, win.querySelector('.subagent-window-header'));
|
||||
// Make draggable (returns listener refs for cleanup) — skip on mobile (bottom-sheet)
|
||||
const dragListeners = isMobile ? null : 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
|
||||
@@ -12467,8 +12487,8 @@ class ClaudemanApp {
|
||||
// Store observer for cleanup
|
||||
this.subagentWindows.get(agentId).resizeObserver = resizeObserver;
|
||||
|
||||
// Animate to final position if spawning from tab
|
||||
if (parentTab) {
|
||||
// Animate to final position if spawning from tab (desktop only)
|
||||
if (parentTab && !isMobile) {
|
||||
requestAnimationFrame(() => {
|
||||
win.style.transition = 'all 0.4s cubic-bezier(0.34, 1.56, 0.64, 1)';
|
||||
win.style.left = `${finalX}px`;
|
||||
@@ -12484,7 +12504,7 @@ class ClaudemanApp {
|
||||
}, 400);
|
||||
});
|
||||
} else {
|
||||
// No animation, just update connection lines
|
||||
// No animation (mobile uses CSS positioning), just update connection lines
|
||||
this.updateConnectionLines();
|
||||
}
|
||||
|
||||
|
||||
@@ -897,8 +897,8 @@
|
||||
<div class="form-row">
|
||||
<label>Default Model</label>
|
||||
<select id="appSettingsDefaultModel" class="form-select">
|
||||
<option value="sonnet">Sonnet (Recommended - balanced)</option>
|
||||
<option value="opus">Opus (Most capable)</option>
|
||||
<option value="sonnet">Sonnet (Balanced)</option>
|
||||
<option value="haiku">Haiku (Fast & cheap)</option>
|
||||
</select>
|
||||
<span class="form-hint">Model used for execution tasks. Optimizer suggestions are advisory only.</span>
|
||||
|
||||
@@ -145,6 +145,74 @@ html.mobile-init .file-browser-panel {
|
||||
max-width: 280px;
|
||||
font-size: 0.7rem;
|
||||
}
|
||||
|
||||
/* ---- Settings Modal: Tablet Optimizations ---- */
|
||||
|
||||
.modal-tabs {
|
||||
overflow-x: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
scrollbar-width: none;
|
||||
flex-wrap: nowrap;
|
||||
}
|
||||
|
||||
.modal-tabs::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.modal-tab-btn {
|
||||
padding: 0.4rem 0.75rem;
|
||||
font-size: 0.7rem;
|
||||
white-space: nowrap;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* Settings grid stays 2-col on tablet but tighter */
|
||||
.settings-grid {
|
||||
gap: 0.4rem 0.75rem;
|
||||
}
|
||||
|
||||
.settings-item {
|
||||
font-size: 0.7rem;
|
||||
}
|
||||
|
||||
/* Event type grid - slightly tighter */
|
||||
.event-type-grid {
|
||||
grid-template-columns: 1fr 40px 40px 40px;
|
||||
gap: 5px 6px;
|
||||
}
|
||||
|
||||
.event-label {
|
||||
font-size: 0.7rem;
|
||||
}
|
||||
|
||||
/* ---- Subagent Windows: Tablet Optimizations ---- */
|
||||
|
||||
.subagent-window {
|
||||
width: 320px;
|
||||
height: 280px;
|
||||
min-width: 240px;
|
||||
min-height: 160px;
|
||||
}
|
||||
|
||||
.subagent-window-header {
|
||||
padding: 0.35rem 0.5rem;
|
||||
}
|
||||
|
||||
.subagent-window-title .id {
|
||||
font-size: 0.7rem;
|
||||
max-width: 180px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.subagent-window-title .status {
|
||||
font-size: 0.55rem;
|
||||
}
|
||||
|
||||
.subagent-window-body {
|
||||
font-size: 0.7rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* ============================================================================
|
||||
@@ -1022,6 +1090,245 @@ html.mobile-init .file-browser-panel {
|
||||
.mobile-case-picker-footer {
|
||||
padding-bottom: calc(12px + var(--safe-area-bottom));
|
||||
}
|
||||
|
||||
/* ---- Settings Modal: Mobile Optimizations ---- */
|
||||
|
||||
/* Scrollable tabs row - prevent overflow on small screens */
|
||||
.modal-tabs {
|
||||
overflow-x: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
scrollbar-width: none;
|
||||
gap: 0.25rem;
|
||||
padding: 0 0.75rem 0.5rem 0.75rem;
|
||||
flex-wrap: nowrap;
|
||||
}
|
||||
|
||||
.modal-tabs::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.modal-tab-btn {
|
||||
padding: 0.35rem 0.6rem;
|
||||
font-size: 0.65rem;
|
||||
white-space: nowrap;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* Settings grid - single column on phones */
|
||||
.settings-grid {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 0.35rem;
|
||||
}
|
||||
|
||||
.settings-grid-3col {
|
||||
grid-template-columns: 1fr 1fr 1fr;
|
||||
}
|
||||
|
||||
.settings-item {
|
||||
padding: 0.35rem 0.5rem;
|
||||
font-size: 0.7rem;
|
||||
}
|
||||
|
||||
.settings-item-label {
|
||||
font-size: 0.7rem;
|
||||
}
|
||||
|
||||
.settings-section-header {
|
||||
font-size: 0.6rem;
|
||||
padding: 0.35rem 0 0.2rem 0;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
/* Form rows - compact on mobile */
|
||||
.form-row {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.form-row label {
|
||||
font-size: 0.7rem;
|
||||
margin-bottom: 0.2rem;
|
||||
}
|
||||
|
||||
.form-row .form-hint {
|
||||
font-size: 0.6rem;
|
||||
}
|
||||
|
||||
.form-row input[type="text"],
|
||||
.form-row input[type="number"],
|
||||
.form-row .form-select,
|
||||
.form-row select {
|
||||
font-size: 0.75rem;
|
||||
padding: 0.35rem 0.5rem;
|
||||
min-height: 32px;
|
||||
}
|
||||
|
||||
.form-row-switch {
|
||||
gap: 0.3rem;
|
||||
}
|
||||
|
||||
.form-row-switch > label:first-child {
|
||||
font-size: 0.7rem;
|
||||
}
|
||||
|
||||
.form-section-header {
|
||||
font-size: 0.6rem;
|
||||
margin-top: 0.75rem;
|
||||
}
|
||||
|
||||
/* Event type grid - tighter on mobile */
|
||||
.event-type-grid {
|
||||
grid-template-columns: 1fr 36px 36px 36px;
|
||||
gap: 4px 4px;
|
||||
padding: 6px;
|
||||
margin-top: 6px;
|
||||
}
|
||||
|
||||
.event-header {
|
||||
font-size: 0.55rem;
|
||||
}
|
||||
|
||||
.event-label {
|
||||
font-size: 0.65rem;
|
||||
}
|
||||
|
||||
.event-type-grid input[type="checkbox"] {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
}
|
||||
|
||||
/* Modal footer buttons - full width on mobile */
|
||||
.form-actions {
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.form-actions .btn-toolbar {
|
||||
flex: 1;
|
||||
min-height: 36px !important;
|
||||
max-height: 36px !important;
|
||||
height: 36px !important;
|
||||
font-size: 0.75rem !important;
|
||||
}
|
||||
|
||||
/* ---- Subagent Windows: Mobile Optimizations ---- */
|
||||
|
||||
/* Full-width stacked windows at bottom on phones */
|
||||
.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;
|
||||
resize: none;
|
||||
box-shadow: 0 -4px 16px rgba(0, 0, 0, 0.5);
|
||||
transform: none !important;
|
||||
}
|
||||
|
||||
/* Hide resize handle on mobile */
|
||||
.subagent-window::after {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Compact subagent window header */
|
||||
.subagent-window-header {
|
||||
padding: 0.3rem 0.5rem;
|
||||
min-height: 32px;
|
||||
}
|
||||
|
||||
.subagent-window-title {
|
||||
gap: 0.25rem;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.subagent-window-title .icon {
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.subagent-window-title .id {
|
||||
font-size: 0.65rem;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
max-width: 45vw;
|
||||
}
|
||||
|
||||
.subagent-window-title .status {
|
||||
font-size: 0.5rem;
|
||||
padding: 0.05rem 0.2rem;
|
||||
}
|
||||
|
||||
.subagent-model-badge {
|
||||
font-size: 0.5rem !important;
|
||||
padding: 0.05rem 0.2rem !important;
|
||||
}
|
||||
|
||||
.subagent-window-actions button {
|
||||
font-size: 0.8rem;
|
||||
padding: 0.3rem 0.5rem;
|
||||
min-width: 32px;
|
||||
min-height: 32px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.subagent-window-body {
|
||||
font-size: 0.65rem;
|
||||
padding: 0.35rem;
|
||||
}
|
||||
|
||||
.subagent-window-body .activity-line {
|
||||
gap: 0.2rem;
|
||||
padding: 0.1rem 0;
|
||||
}
|
||||
|
||||
.subagent-window-body .activity-line .time {
|
||||
font-size: 0.55rem;
|
||||
}
|
||||
|
||||
.subagent-window-body .activity-line .tool-name {
|
||||
font-size: 0.6rem;
|
||||
}
|
||||
|
||||
.subagent-window-body .activity-line .tool-detail {
|
||||
font-size: 0.6rem;
|
||||
}
|
||||
|
||||
/* Parent label in subagent window - compact */
|
||||
.subagent-window-parent {
|
||||
padding: 0.15rem 0.5rem;
|
||||
font-size: 0.6rem;
|
||||
}
|
||||
|
||||
/* ---- Tab Subagent Badges: Smaller on Mobile ---- */
|
||||
|
||||
.session-tab .tab-subagent-badge {
|
||||
height: 14px;
|
||||
padding: 0 3px;
|
||||
border-radius: 7px;
|
||||
margin-left: 2px;
|
||||
}
|
||||
|
||||
.session-tab .tab-subagent-badge .subagent-label {
|
||||
font-size: 0.45rem;
|
||||
}
|
||||
|
||||
/* Subagent dropdown - full width bottom sheet on mobile */
|
||||
.subagent-dropdown {
|
||||
position: fixed !important;
|
||||
left: 8px !important;
|
||||
right: 8px !important;
|
||||
bottom: calc(40px + var(--safe-area-bottom) + 8px) !important;
|
||||
top: auto !important;
|
||||
min-width: auto !important;
|
||||
max-width: none !important;
|
||||
border-radius: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -3126,6 +3126,47 @@ NOW: Generate the implementation plan for the task above. Think step by step.`;
|
||||
}
|
||||
});
|
||||
|
||||
// ============ Model Configuration Endpoints ============
|
||||
|
||||
this.app.get('/api/execution/model-config', async () => {
|
||||
try {
|
||||
const content = await fs.readFile(settingsPath, 'utf-8');
|
||||
const settings = JSON.parse(content);
|
||||
return { success: true, data: settings.modelConfig || {} };
|
||||
} catch (err) {
|
||||
if ((err as NodeJS.ErrnoException).code !== 'ENOENT') {
|
||||
console.error('Failed to read model config:', err);
|
||||
}
|
||||
return { success: true, data: {} };
|
||||
}
|
||||
});
|
||||
|
||||
this.app.put('/api/execution/model-config', async (req) => {
|
||||
const modelConfig = req.body as Record<string, unknown>;
|
||||
|
||||
try {
|
||||
let settings: Record<string, unknown> = {};
|
||||
try {
|
||||
const content = await fs.readFile(settingsPath, 'utf-8');
|
||||
settings = JSON.parse(content);
|
||||
} catch {
|
||||
// File doesn't exist yet, start fresh
|
||||
}
|
||||
|
||||
settings.modelConfig = modelConfig;
|
||||
|
||||
const dir = dirname(settingsPath);
|
||||
if (!existsSync(dir)) {
|
||||
mkdirSync(dir, { recursive: true });
|
||||
}
|
||||
writeFileSync(settingsPath, JSON.stringify(settings, null, 2));
|
||||
|
||||
return { success: true };
|
||||
} catch (err) {
|
||||
return createErrorResponse(ApiErrorCode.OPERATION_FAILED, getErrorMessage(err));
|
||||
}
|
||||
});
|
||||
|
||||
// ============ CPU Priority Endpoints ============
|
||||
|
||||
// Get Nice priority config for a session
|
||||
|
||||
Reference in New Issue
Block a user