mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-02 13:39:41 +02:00
chore: bump version to 0.1570
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.1568 (must match `package.json` for npm publish)
|
||||
**Version**: 0.1570 (must match `package.json` for npm publish)
|
||||
|
||||
## Project Overview
|
||||
|
||||
|
||||
@@ -19,6 +19,10 @@
|
||||
|
||||
---
|
||||
|
||||
<p align="center">
|
||||
<img src="docs/images/subagent-demo.gif" alt="Claudeman — parallel subagent visualization" width="900">
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<img src="docs/images/claude-overview.png" alt="Claudeman Dashboard" width="900">
|
||||
</p>
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 583 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 3.8 MiB After Width: | Height: | Size: 3.7 MiB |
Binary file not shown.
|
Before Width: | Height: | Size: 168 KiB After Width: | Height: | Size: 278 KiB |
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "claudeman",
|
||||
"version": "0.1568",
|
||||
"version": "0.1570",
|
||||
"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,347 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Capture a GIF of subagent spawning on the LIVE Claudeman server.
|
||||
*
|
||||
* Connects to the running server on port 3000, creates a test session,
|
||||
* sends a prompt that spawns 3 parallel agents, and captures a tight
|
||||
* 8-second GIF starting right before agents spawn.
|
||||
*
|
||||
* Usage: node scripts/capture-subagent-gif.mjs
|
||||
* Output: docs/images/subagent-demo.gif
|
||||
*/
|
||||
|
||||
import { chromium } from 'playwright';
|
||||
import { execSync } from 'child_process';
|
||||
import { join, dirname } from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { existsSync, unlinkSync, mkdirSync, statSync, cpSync } from 'fs';
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const PROJECT_ROOT = join(__dirname, '..');
|
||||
const OUTPUT_DIR = join(PROJECT_ROOT, 'docs', 'images');
|
||||
const BASE_URL = 'http://localhost:3000';
|
||||
const CASE_NAME = 'acme-backend';
|
||||
const VIEWPORT = { width: 1440, height: 810 };
|
||||
|
||||
// GIF settings
|
||||
const GIF_DURATION = 25; // record long, trim later
|
||||
const GIF_FPS = 6;
|
||||
const GIF_WIDTH = 960;
|
||||
const GIF_COLORS = 128;
|
||||
|
||||
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
||||
|
||||
// Prompt that triggers exactly 3 parallel Explore agents with real research work
|
||||
const PROMPT = `Analyze this codebase using exactly 3 Explore agents in parallel. IMPORTANT: Launch exactly 3 agents, no more, no fewer. Each agent should do its own research without spawning sub-agents:
|
||||
|
||||
1. Agent 1: Find all REST API endpoints and their HTTP methods in server.ts
|
||||
2. Agent 2: Analyze the respawn controller state machine in respawn-controller.ts
|
||||
3. Agent 3: Find all SSE event types broadcast by the server
|
||||
|
||||
Run all three in parallel. After they complete, give me a brief summary.`;
|
||||
|
||||
try { mkdirSync(OUTPUT_DIR, { recursive: true }); } catch {}
|
||||
|
||||
async function api(path, opts = {}) {
|
||||
const res = await fetch(`${BASE_URL}${path}`, {
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
...opts,
|
||||
});
|
||||
return res.json();
|
||||
}
|
||||
|
||||
async function capture() {
|
||||
console.log('Subagent GIF Capture');
|
||||
console.log('====================');
|
||||
|
||||
// Verify server is running
|
||||
try {
|
||||
await fetch(`${BASE_URL}/api/status`);
|
||||
} catch {
|
||||
console.error('Server not running on port 3000');
|
||||
process.exit(1);
|
||||
}
|
||||
console.log('✓ Server reachable');
|
||||
|
||||
let sessionId = null;
|
||||
let browser = null;
|
||||
|
||||
try {
|
||||
// Create case and seed with real source files
|
||||
console.log(`Creating case: ${CASE_NAME}`);
|
||||
const caseDir = join(process.env.HOME, 'claudeman-cases', CASE_NAME);
|
||||
const caseSrc = join(caseDir, 'src');
|
||||
const caseTest = join(caseDir, 'test');
|
||||
mkdirSync(caseSrc, { recursive: true });
|
||||
mkdirSync(caseTest, { recursive: true });
|
||||
|
||||
// Copy real source files so agents have content to analyze
|
||||
const filesToCopy = [
|
||||
['src/types.ts', 'src/types.ts'],
|
||||
['src/session.ts', 'src/session.ts'],
|
||||
['src/respawn-controller.ts', 'src/respawn-controller.ts'],
|
||||
['src/web/server.ts', 'src/server.ts'],
|
||||
['src/subagent-watcher.ts', 'src/subagent-watcher.ts'],
|
||||
['src/ralph-tracker.ts', 'src/ralph-tracker.ts'],
|
||||
['CLAUDE.md', 'CLAUDE.md'],
|
||||
['tsconfig.json', 'tsconfig.json'],
|
||||
['package.json', 'package.json'],
|
||||
];
|
||||
for (const [from, to] of filesToCopy) {
|
||||
const src = join(PROJECT_ROOT, from);
|
||||
if (existsSync(src)) cpSync(src, join(caseDir, to));
|
||||
}
|
||||
console.log(`✓ Seeded ${filesToCopy.length} files`);
|
||||
|
||||
await api('/api/cases/link', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ name: CASE_NAME, path: caseDir }),
|
||||
});
|
||||
|
||||
// Launch browser
|
||||
console.log('Launching browser...');
|
||||
browser = await chromium.launch({
|
||||
headless: true,
|
||||
args: [
|
||||
'--no-sandbox',
|
||||
'--disable-setuid-sandbox',
|
||||
'--force-color-profile=srgb',
|
||||
'--disable-gpu-compositing',
|
||||
],
|
||||
});
|
||||
|
||||
// --- Phase 1: Setup (no recording) ---
|
||||
const setupContext = await browser.newContext({
|
||||
viewport: VIEWPORT,
|
||||
colorScheme: 'dark',
|
||||
});
|
||||
const setupPage = await setupContext.newPage();
|
||||
setupPage.setDefaultTimeout(60000);
|
||||
|
||||
await setupPage.goto(BASE_URL, { waitUntil: 'domcontentloaded' });
|
||||
await sleep(1000);
|
||||
|
||||
// Configure settings: enable subagents, disable Files pane
|
||||
await setupPage.evaluate(() => {
|
||||
const existing = JSON.parse(localStorage.getItem('claudeman-app-settings') || '{}');
|
||||
Object.assign(existing, {
|
||||
showSubagents: true,
|
||||
subagentTrackingEnabled: true,
|
||||
subagentActiveTabOnly: false,
|
||||
showMonitor: false,
|
||||
showProjectInsights: false,
|
||||
showFileBrowser: false,
|
||||
});
|
||||
localStorage.setItem('claudeman-app-settings', JSON.stringify(existing));
|
||||
});
|
||||
await setupPage.reload({ waitUntil: 'domcontentloaded' });
|
||||
await sleep(1000);
|
||||
|
||||
// Quick-start session
|
||||
console.log('Starting session...');
|
||||
const startResult = await api('/api/quick-start', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ caseName: CASE_NAME, mode: 'claude' }),
|
||||
});
|
||||
if (!startResult.success) throw new Error(`Quick-start failed: ${startResult.error}`);
|
||||
sessionId = startResult.sessionId;
|
||||
console.log(`✓ Session: ${sessionId}`);
|
||||
|
||||
// Wait for terminal and select our tab
|
||||
await setupPage.waitForSelector('.xterm', { timeout: 30000 });
|
||||
const ourTab = setupPage.locator(`.session-tab[data-id="${sessionId}"]`);
|
||||
if (await ourTab.count() > 0) await ourTab.click();
|
||||
console.log('✓ Terminal ready');
|
||||
|
||||
// Wait for Claude CLI to initialize
|
||||
console.log('Waiting for Claude CLI...');
|
||||
await sleep(15000);
|
||||
|
||||
// Send the prompt
|
||||
console.log('Sending prompt...');
|
||||
await api(`/api/sessions/${sessionId}/input`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ input: PROMPT, useMux: true }),
|
||||
});
|
||||
await sleep(200);
|
||||
await api(`/api/sessions/${sessionId}/input`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ input: '\r', useMux: true }),
|
||||
});
|
||||
console.log('✓ Prompt sent');
|
||||
|
||||
// Wait for Claude to start thinking (first subagent imminent)
|
||||
console.log('Waiting for Claude to start processing...');
|
||||
const pollStart = Date.now();
|
||||
let firstAgentSeen = false;
|
||||
|
||||
while (Date.now() - pollStart < 180000) {
|
||||
const res = await api(`/api/sessions/${sessionId}/subagents`);
|
||||
const agents = res.data || [];
|
||||
if (agents.length >= 1) {
|
||||
console.log(` First agent detected! (${agents.length} total)`);
|
||||
firstAgentSeen = true;
|
||||
break;
|
||||
}
|
||||
await sleep(1000);
|
||||
}
|
||||
|
||||
if (!firstAgentSeen) {
|
||||
console.log('⚠ No agents detected after 3 min, recording anyway...');
|
||||
}
|
||||
|
||||
// Wait for all 3 agents to spawn and start working
|
||||
console.log('Waiting for all agents to spawn and start working...');
|
||||
const spawnWait = Date.now();
|
||||
while (Date.now() - spawnWait < 60000) {
|
||||
const res = await api(`/api/sessions/${sessionId}/subagents`);
|
||||
const agents = res.data || [];
|
||||
if (agents.length >= 3) {
|
||||
console.log(` All ${agents.length} agents spawned`);
|
||||
break;
|
||||
}
|
||||
console.log(` ${agents.length}/3 agents...`);
|
||||
await sleep(2000);
|
||||
}
|
||||
|
||||
// Let agents work for a few seconds so there's content in the windows
|
||||
console.log('Letting agents work for 5s...');
|
||||
await sleep(5000);
|
||||
|
||||
// Close setup context (we'll grab localStorage via the API/new context)
|
||||
await setupContext.close();
|
||||
console.log('✓ Setup phase complete');
|
||||
|
||||
// --- Phase 2: Record (start video right before the action) ---
|
||||
console.log('Starting recording context...');
|
||||
const recordContext = await browser.newContext({
|
||||
viewport: VIEWPORT,
|
||||
colorScheme: 'dark',
|
||||
recordVideo: { dir: OUTPUT_DIR, size: VIEWPORT },
|
||||
});
|
||||
const recordPage = await recordContext.newPage();
|
||||
recordPage.setDefaultTimeout(30000);
|
||||
|
||||
await recordPage.goto(BASE_URL, { waitUntil: 'domcontentloaded' });
|
||||
await sleep(500);
|
||||
|
||||
// Re-apply settings (new context = fresh localStorage)
|
||||
await recordPage.evaluate(() => {
|
||||
const existing = JSON.parse(localStorage.getItem('claudeman-app-settings') || '{}');
|
||||
Object.assign(existing, {
|
||||
showSubagents: true,
|
||||
subagentTrackingEnabled: true,
|
||||
subagentActiveTabOnly: false,
|
||||
showMonitor: false,
|
||||
showProjectInsights: false,
|
||||
showFileBrowser: false,
|
||||
});
|
||||
localStorage.setItem('claudeman-app-settings', JSON.stringify(existing));
|
||||
});
|
||||
await recordPage.reload({ waitUntil: 'domcontentloaded' });
|
||||
await sleep(1000);
|
||||
|
||||
// Select our session tab
|
||||
const recTab = recordPage.locator(`.session-tab[data-id="${sessionId}"]`);
|
||||
if (await recTab.count() > 0) await recTab.click();
|
||||
await sleep(500);
|
||||
|
||||
// Force open any subagent windows that exist
|
||||
const agentRes = await api(`/api/sessions/${sessionId}/subagents`);
|
||||
for (const agent of (agentRes.data || [])) {
|
||||
await recordPage.evaluate((id) => {
|
||||
if (window.app?.openSubagentWindow) window.app.openSubagentWindow(id);
|
||||
}, agent.agentId);
|
||||
await sleep(300);
|
||||
}
|
||||
|
||||
const recordStart = Date.now();
|
||||
console.log('🔴 Recording...');
|
||||
|
||||
// Poll for more agents and open their windows as they appear
|
||||
let maxSeen = (agentRes.data || []).length;
|
||||
while (Date.now() - recordStart < 30000) {
|
||||
const res = await api(`/api/sessions/${sessionId}/subagents`);
|
||||
const agents = res.data || [];
|
||||
if (agents.length > maxSeen) {
|
||||
console.log(` ${agents.length} agent(s) now`);
|
||||
for (const agent of agents) {
|
||||
await recordPage.evaluate((id) => {
|
||||
const existing = document.querySelector(`.subagent-window[data-agent-id="${id}"]`);
|
||||
if (!existing && window.app?.openSubagentWindow) window.app.openSubagentWindow(id);
|
||||
}, agent.agentId);
|
||||
}
|
||||
maxSeen = agents.length;
|
||||
}
|
||||
await sleep(1000);
|
||||
}
|
||||
console.log(` Recorded ${((Date.now() - recordStart) / 1000).toFixed(0)}s total`);
|
||||
|
||||
// Take a static screenshot too
|
||||
await recordPage.screenshot({
|
||||
path: join(OUTPUT_DIR, 'subagent-spawn.png'),
|
||||
fullPage: false,
|
||||
});
|
||||
console.log('✓ Saved: subagent-spawn.png');
|
||||
|
||||
// Finalize video
|
||||
console.log('Finalizing video...');
|
||||
const videoPath = await recordPage.video()?.path();
|
||||
await recordContext.close();
|
||||
|
||||
// --- Phase 3: Convert to optimized GIF ---
|
||||
if (videoPath && existsSync(videoPath)) {
|
||||
const gifPath = join(OUTPUT_DIR, 'subagent-demo.gif');
|
||||
const palettePath = join(OUTPUT_DIR, '_palette.png');
|
||||
|
||||
// Probe video duration to calculate start offset
|
||||
const probeOut = execSync(
|
||||
`ffprobe -v error -show_entries format=duration -of csv=p=0 "${videoPath}"`,
|
||||
{ encoding: 'utf8' }
|
||||
).trim();
|
||||
const videoDuration = parseFloat(probeOut);
|
||||
// Skip the first ~2s (page load/reload) and take GIF_DURATION seconds
|
||||
const ssOffset = Math.max(0, Math.min(2, videoDuration - GIF_DURATION));
|
||||
console.log(`Video: ${videoDuration.toFixed(1)}s, extracting ${GIF_DURATION}s from ${ssOffset.toFixed(1)}s`);
|
||||
|
||||
console.log('Converting to GIF...');
|
||||
// Boost contrast/saturation for vivid terminal colors, then encode
|
||||
// stats_mode=single = per-frame palette (best color accuracy for few accent colors on dark bg)
|
||||
const filters = `fps=${GIF_FPS},scale=${GIF_WIDTH}:-1:flags=lanczos,eq=contrast=1.1:saturation=1.3:brightness=0.02`;
|
||||
execSync(
|
||||
`ffmpeg -y -ss ${ssOffset} -t ${GIF_DURATION} -i "${videoPath}" ` +
|
||||
`-vf "${filters},split[s0][s1];[s0]palettegen=max_colors=${GIF_COLORS}:stats_mode=single:reserve_transparent=0[p];[s1][p]paletteuse=dither=floyd_steinberg:diff_mode=rectangle:new=1" ` +
|
||||
`-loop 0 "${gifPath}"`,
|
||||
{ stdio: 'pipe' }
|
||||
);
|
||||
|
||||
try { unlinkSync(palettePath); } catch {}
|
||||
try { unlinkSync(videoPath); } catch {}
|
||||
|
||||
const sizeMB = (existsSync(gifPath) ? statSync(gifPath).size / (1024 * 1024) : 0).toFixed(1);
|
||||
console.log(`✓ GIF saved: ${gifPath} (${sizeMB} MB)`);
|
||||
} else {
|
||||
console.log('⚠ No video recording found');
|
||||
}
|
||||
|
||||
console.log('\nDone!');
|
||||
|
||||
} catch (err) {
|
||||
console.error('Error:', err.message);
|
||||
process.exitCode = 1;
|
||||
} finally {
|
||||
// Cleanup: ONLY delete the session WE created
|
||||
if (sessionId) {
|
||||
console.log(`Cleaning up session: ${sessionId}`);
|
||||
await api(`/api/sessions/${sessionId}`, { method: 'DELETE' }).catch(() => {});
|
||||
}
|
||||
if (browser) await browser.close().catch(() => {});
|
||||
}
|
||||
}
|
||||
|
||||
process.on('SIGINT', () => {
|
||||
console.log('\nInterrupted');
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
capture();
|
||||
+24
-11
@@ -276,7 +276,8 @@ export class SubagentWatcher extends EventEmitter {
|
||||
|
||||
/**
|
||||
* Run pgrep once and read /proc info for all Claude PIDs in parallel.
|
||||
* Returns a Map of pid -> { environ, cmdline } for all running claude processes.
|
||||
* Returns a Map of pid -> { environ, cmdline } for subagent processes only.
|
||||
* Excludes main Claudeman-managed Claude processes (CLAUDEMAN_MUX=1).
|
||||
*/
|
||||
private async getClaudePids(): Promise<Map<number, { environ: string; cmdline: string }>> {
|
||||
const result = new Map<number, { environ: string; cmdline: string }>();
|
||||
@@ -295,6 +296,8 @@ export class SubagentWatcher extends EventEmitter {
|
||||
let cmdline = '';
|
||||
try { environ = await readFile(`/proc/${pid}/environ`, 'utf8'); } catch { /* skip */ }
|
||||
try { cmdline = await readFile(`/proc/${pid}/cmdline`, 'utf8'); } catch { /* skip */ }
|
||||
// Skip main Claudeman-managed Claude processes — only track subagents
|
||||
if (environ.includes('CLAUDEMAN_MUX=1')) return;
|
||||
if (environ || cmdline) {
|
||||
result.set(pid, { environ, cmdline });
|
||||
}
|
||||
@@ -623,20 +626,26 @@ export class SubagentWatcher extends EventEmitter {
|
||||
}
|
||||
|
||||
/**
|
||||
* Kill all subagents for a specific Claudeman session working directory
|
||||
* Kill all subagents for a specific Claudeman session.
|
||||
* IMPORTANT: Must scope to sessionId to avoid cross-session kills.
|
||||
* All sessions in the same workingDir share a projectHash, so filtering
|
||||
* by workingDir alone would kill subagents belonging to OTHER sessions.
|
||||
*/
|
||||
async killSubagentsForSession(workingDir: string): Promise<void> {
|
||||
async killSubagentsForSession(workingDir: string, sessionId?: string): Promise<void> {
|
||||
const subagents = this.getSubagentsForSession(workingDir);
|
||||
for (const agent of subagents) {
|
||||
if (agent.status === 'active' || agent.status === 'idle') {
|
||||
// Only kill subagents belonging to this specific session
|
||||
if (sessionId && agent.sessionId !== sessionId) continue;
|
||||
await this.killSubagent(agent.agentId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the process ID of a Claude subagent by its session ID
|
||||
* Searches /proc for claude processes with matching session ID in environment
|
||||
* Find the process ID of a Claude subagent by its session ID.
|
||||
* Searches /proc for claude processes with matching session ID in environment.
|
||||
* Skips main Claudeman-managed Claude processes (identified by CLAUDEMAN_MUX=1).
|
||||
*/
|
||||
private async findSubagentProcess(sessionId: string): Promise<number | null> {
|
||||
try {
|
||||
@@ -653,18 +662,22 @@ export class SubagentWatcher extends EventEmitter {
|
||||
const pid = parseInt(pidStr, 10);
|
||||
if (Number.isNaN(pid)) continue;
|
||||
|
||||
let environ = '';
|
||||
try {
|
||||
// Check /proc/{pid}/environ for session ID (async read)
|
||||
const environ = await readFile(`/proc/${pid}/environ`, 'utf8');
|
||||
if (environ.includes(sessionId)) {
|
||||
return pid;
|
||||
}
|
||||
environ = await readFile(`/proc/${pid}/environ`, 'utf8');
|
||||
} catch {
|
||||
// Can't read this process's environ - skip
|
||||
}
|
||||
|
||||
// Defense-in-depth: never kill a main Claudeman-managed Claude process.
|
||||
// Main processes have CLAUDEMAN_MUX=1 in their environment; subagents don't.
|
||||
if (environ.includes('CLAUDEMAN_MUX=1')) continue;
|
||||
|
||||
if (environ.includes(sessionId)) {
|
||||
return pid;
|
||||
}
|
||||
|
||||
try {
|
||||
// Also check /proc/{pid}/cmdline for session ID (async read)
|
||||
const cmdline = await readFile(`/proc/${pid}/cmdline`, 'utf8');
|
||||
if (cmdline.includes(sessionId)) {
|
||||
return pid;
|
||||
|
||||
@@ -2441,7 +2441,7 @@ class ClaudemanApp {
|
||||
_updateLocalEchoState() {
|
||||
const settings = this.loadAppSettingsFromStorage();
|
||||
const session = this.activeSessionId ? this.sessions.get(this.activeSessionId) : null;
|
||||
const echoEnabled = settings.localEchoEnabled ?? true;
|
||||
const echoEnabled = settings.localEchoEnabled ?? MobileDetection.isTouchDevice();
|
||||
const shouldEnable = !!(echoEnabled && session);
|
||||
if (this._localEchoEnabled && !shouldEnable) {
|
||||
// Flush background-buffered keystrokes before disabling
|
||||
@@ -9935,7 +9935,7 @@ class ClaudemanApp {
|
||||
document.getElementById('appSettingsSubagentTracking').checked = settings.subagentTrackingEnabled ?? defaults.subagentTrackingEnabled ?? true;
|
||||
document.getElementById('appSettingsSubagentActiveTabOnly').checked = settings.subagentActiveTabOnly ?? defaults.subagentActiveTabOnly ?? true;
|
||||
document.getElementById('appSettingsImageWatcherEnabled').checked = settings.imageWatcherEnabled ?? defaults.imageWatcherEnabled ?? false;
|
||||
document.getElementById('appSettingsLocalEcho').checked = settings.localEchoEnabled ?? true;
|
||||
document.getElementById('appSettingsLocalEcho').checked = settings.localEchoEnabled ?? MobileDetection.isTouchDevice();
|
||||
document.getElementById('appSettingsTabTwoRows').checked = settings.tabTwoRows ?? defaults.tabTwoRows ?? false;
|
||||
// Claude CLI settings
|
||||
const claudeModeSelect = document.getElementById('appSettingsClaudeMode');
|
||||
|
||||
+165
-1
@@ -1022,7 +1022,8 @@ html.mobile-init .file-browser-panel {
|
||||
padding-right: calc(0.5rem + var(--safe-area-right));
|
||||
}
|
||||
|
||||
/* Respawn/Ralph panels - more compact on mobile, includes safe area padding */
|
||||
/* ---- Respawn Banner: Mobile Optimizations ---- */
|
||||
|
||||
.respawn-banner {
|
||||
padding: 0.4rem 0.5rem;
|
||||
padding-left: calc(0.5rem + var(--safe-area-left));
|
||||
@@ -1030,6 +1031,169 @@ html.mobile-init .file-browser-panel {
|
||||
font-size: 0.7rem;
|
||||
}
|
||||
|
||||
/* Stack banner vertically on mobile — status on top, action log below */
|
||||
.respawn-compact-layout {
|
||||
flex-direction: column;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
/* Let status column shrink to fit */
|
||||
.respawn-status-col {
|
||||
flex-shrink: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
/* Wrap status row items — tokens/timer may go to second line */
|
||||
.respawn-status-row1 {
|
||||
flex-wrap: wrap;
|
||||
gap: 0.35rem;
|
||||
}
|
||||
|
||||
/* Stop button — 44px touch target */
|
||||
.respawn-banner .btn-icon-only {
|
||||
min-width: 36px;
|
||||
min-height: 36px;
|
||||
font-size: 1rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-left: auto;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
/* Action log — horizontal strip below status, border on top instead of left */
|
||||
.respawn-action-log {
|
||||
border-left: none;
|
||||
border-top: 1px solid rgba(34, 197, 94, 0.2);
|
||||
padding-left: 0;
|
||||
padding-top: 0.25rem;
|
||||
max-height: 2em;
|
||||
font-size: 0.6rem;
|
||||
}
|
||||
|
||||
/* Countdown timers — tighter on mobile */
|
||||
.respawn-countdown-timers {
|
||||
gap: 0.2rem;
|
||||
}
|
||||
|
||||
.respawn-countdown-timer {
|
||||
font-size: 0.55rem;
|
||||
padding: 0.05rem 0.25rem;
|
||||
}
|
||||
|
||||
/* ---- Session Options Modal: Respawn Tab Mobile Optimizations ---- */
|
||||
|
||||
/* Respawn header — stack status and buttons vertically */
|
||||
.respawn-header {
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
/* Enable/Stop buttons — full-width touch targets */
|
||||
.respawn-header .respawn-actions {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.respawn-header .respawn-actions .btn-toolbar {
|
||||
flex: 1;
|
||||
min-height: 44px !important;
|
||||
font-size: 0.85rem !important;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
/* Duration preset buttons — 44px touch targets in a grid */
|
||||
.duration-presets {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 0.4rem;
|
||||
}
|
||||
|
||||
.duration-preset-btn {
|
||||
min-height: 40px;
|
||||
padding: 0.5rem 0.4rem;
|
||||
font-size: 0.8rem;
|
||||
border-radius: 8px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* Custom duration — spans full width below the grid */
|
||||
.duration-custom {
|
||||
grid-column: 1 / -1;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.4rem;
|
||||
}
|
||||
|
||||
.duration-custom .duration-preset-btn {
|
||||
flex: 0 0 auto;
|
||||
min-width: 80px;
|
||||
}
|
||||
|
||||
.duration-custom-input.visible {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.duration-custom-input input {
|
||||
width: 100%;
|
||||
min-height: 40px;
|
||||
font-size: 16px; /* Prevents iOS zoom */
|
||||
}
|
||||
|
||||
/* Preset selector row — stack vertically */
|
||||
.preset-selector {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.4rem;
|
||||
}
|
||||
|
||||
.preset-selector select {
|
||||
width: 100%;
|
||||
min-height: 40px;
|
||||
font-size: 16px; /* Prevents iOS zoom */
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.preset-selector .btn {
|
||||
flex: 1;
|
||||
min-height: 40px;
|
||||
font-size: 0.8rem;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
/* Textareas — prevent iOS zoom with 16px font */
|
||||
#sessionOptionsModal textarea {
|
||||
font-size: 16px;
|
||||
min-height: 56px;
|
||||
border-radius: 8px;
|
||||
padding: 0.5rem 0.75rem;
|
||||
}
|
||||
|
||||
/* Checkboxes — larger touch area */
|
||||
#sessionOptionsModal .checkbox-inline {
|
||||
min-height: 40px;
|
||||
font-size: 0.85rem;
|
||||
padding: 0.25rem 0;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
#sessionOptionsModal .checkbox-inline input[type="checkbox"] {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
/* Respawn options row — stack if needed */
|
||||
#sessionOptionsModal .respawn-options-row {
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
/* Form section headers — more breathing room */
|
||||
#sessionOptionsModal .form-section-header {
|
||||
margin-top: 1rem;
|
||||
padding-top: 0.75rem;
|
||||
font-size: 0.7rem;
|
||||
}
|
||||
|
||||
.ralph-panel {
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
+2
-2
@@ -4011,10 +4011,10 @@ NOW: Generate the implementation plan for the task above. Think step by step.`;
|
||||
session.ralphTracker.stopWatchingFixPlan();
|
||||
}
|
||||
|
||||
// Kill all subagents spawned by this session
|
||||
// Kill all subagents spawned by this session (scoped to sessionId to avoid cross-session kills)
|
||||
if (session && killMux) {
|
||||
try {
|
||||
await subagentWatcher.killSubagentsForSession(session.workingDir);
|
||||
await subagentWatcher.killSubagentsForSession(session.workingDir, sessionId);
|
||||
} catch (err) {
|
||||
console.error(`[Server] Failed to kill subagents for session ${sessionId}:`, err);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user