diff --git a/CHANGELOG.md b/CHANGELOG.md index e42de519..b66445b7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # aicodeman +## 0.5.5 + +### Patch Changes + +- Add 1M Opus context quick setting — per-case and global toggle that writes `model: "opus[1m]"` to `.claude/settings.local.json` when creating new sessions. Fix mobile layout: banners (respawn, timer, orchestrator) between header and main content now visible by switching from margin-top on `.main` to padding-top on `.app`. Add tablet-optimized respawn banner styles and mobile phone banner refinements. + ## 0.5.4 ### Patch Changes diff --git a/CLAUDE.md b/CLAUDE.md index 95c52cb5..9f69266c 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -52,7 +52,7 @@ When user says "COM": 4. **Sync CLAUDE.md version**: Update the `**Version**` line below to match the new version from `package.json` 5. **Commit and deploy**: `git add -A && git commit -m "chore: version packages" && git push && npm run build && systemctl --user restart codeman-web` -**Version**: 0.5.4 (must match `package.json`) +**Version**: 0.5.5 (must match `package.json`) ## Project Overview diff --git a/package.json b/package.json index 4a564d0e..5df69827 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "aicodeman", - "version": "0.5.4", + "version": "0.5.5", "description": "The missing control plane for AI coding agents - run 20 autonomous agents with real-time monitoring and session persistence", "type": "module", "main": "dist/index.js", diff --git a/src/hooks-config.ts b/src/hooks-config.ts index f9f8b3e6..e5dcc620 100644 --- a/src/hooks-config.ts +++ b/src/hooks-config.ts @@ -116,6 +116,34 @@ export async function updateCaseEnvVars(casePath: string, envVars: Record { + const claudeDir = join(casePath, '.claude'); + if (!existsSync(claudeDir)) { + await mkdir(claudeDir, { recursive: true }); + } + + const settingsPath = join(claudeDir, 'settings.local.json'); + let existing: Record = {}; + + try { + existing = JSON.parse(await readFile(settingsPath, 'utf-8')); + } catch { + existing = {}; + } + + if (model) { + existing.model = model; + } else { + delete existing.model; + } + + await writeFile(settingsPath, JSON.stringify(existing, null, 2) + '\n'); +} + /** * Writes hooks config to .claude/settings.local.json in the given case path. * Merges with existing file content, only touching the `hooks` key. diff --git a/src/web/public/index.html b/src/web/public/index.html index f4ba982a..33e98da3 100644 --- a/src/web/public/index.html +++ b/src/web/public/index.html @@ -384,6 +384,11 @@ Agent Teams Enable experimental Agent Teams for new sessions in this case + + Use 1M token context window for new sessions @@ -408,6 +413,11 @@ Agent Teams Enable Agent Teams for new sessions + + Use 1M token context window @@ -1046,6 +1056,14 @@ Enable experimental Agent Teams for all new Claude sessions (disabled by default) +
+ + + Use 1M token context window (model: opus[1m]) for all new sessions +
Nice Priority
diff --git a/src/web/public/mobile.css b/src/web/public/mobile.css index 9ac68cfc..588d645d 100644 --- a/src/web/public/mobile.css +++ b/src/web/public/mobile.css @@ -65,13 +65,14 @@ html.mobile-init .file-browser-panel { max-height: calc(48px + var(--safe-area-top)); } - /* Add top margin to main content to account for fixed header */ - .main { - margin-top: 54px; + /* Push ALL content below fixed header (not just .main) so banners + (respawn, timer, orchestrator) between header and main are visible */ + .app { + padding-top: 54px; } - .ios-device .main { - margin-top: calc(54px + var(--safe-area-top)); + .ios-device .app { + padding-top: calc(54px + var(--safe-area-top)); } /* Font controls - smaller on tablet, visibility controlled by JS */ @@ -235,6 +236,37 @@ html.mobile-init .file-browser-panel { font-size: 0.7rem; } + /* ---- Respawn Banner: Tablet Optimizations ---- */ + + .respawn-banner { + padding: 0.25rem 0.5rem; + padding-left: calc(0.5rem + var(--safe-area-left)); + padding-right: calc(0.5rem + var(--safe-area-right)); + font-size: 0.65rem; + } + + .respawn-compact-layout { + gap: 0.75rem; + } + + .respawn-status-row1 { + gap: 0.4rem; + } + + .respawn-action-log { + max-height: 2.6em; + font-size: 0.6rem; + } + + .respawn-countdown-timer { + font-size: 0.55rem; + } + + /* Respawn timer bar — slimmer on tablet */ + .respawn-timer-bar { + width: 24px; + } + /* Show desktop voice button on tablet (hidden by max-width:1023px in styles.css, mobile .btn-voice-mobile only shows at <430px) */ .toolbar-center .btn-toolbar.btn-voice { @@ -327,18 +359,22 @@ html.mobile-init .file-browser-panel { max-height: calc(36px + var(--safe-area-top)); } - /* Add top margin to main content to account for fixed header, - and bottom padding for the fixed toolbar (40px) so terminal - content doesn't extend behind it. JS overrides paddingBottom + /* Push ALL content below fixed header (not just .main) so banners + (respawn, timer, orchestrator) between header and main are visible. + Bottom padding for the fixed toolbar (40px) so terminal content + doesn't extend behind it. JS overrides paddingBottom on .main when keyboard is visible, and resetLayout() clears the inline style to re-expose this CSS value. */ - .main { - margin-top: 42px; - padding-bottom: calc(40px + var(--safe-area-bottom)); + .app { + padding-top: 42px; } - .ios-device .main { - margin-top: calc(42px + var(--safe-area-top)); + .ios-device .app { + padding-top: calc(42px + var(--safe-area-top)); + } + + .main { + padding-bottom: calc(40px + var(--safe-area-bottom)); } /* iOS Safari: toolbar is pushed up by (100vh - --app-height) to clear the @@ -1213,31 +1249,70 @@ html.mobile-init .file-browser-panel { /* ---- Respawn Banner: Mobile Optimizations ---- */ .respawn-banner { - padding: 0.4rem 0.5rem; + padding: 0.3rem 0.5rem; padding-left: calc(0.5rem + var(--safe-area-left)); padding-right: calc(0.5rem + var(--safe-area-right)); - font-size: 0.7rem; + font-size: 0.65rem; } /* Stack banner vertically on mobile — status on top, action log below */ .respawn-compact-layout { flex-direction: column; - gap: 0.25rem; + gap: 0.15rem; } /* Let status column shrink to fit */ .respawn-status-col { flex-shrink: 1; min-width: 0; + gap: 0.1rem; } - /* Wrap status row items — tokens/timer may go to second line */ + /* Row1: wrap allowed, key info prioritized via order */ .respawn-status-row1 { flex-wrap: wrap; - gap: 0.35rem; + gap: 0.25rem; + row-gap: 0.1rem; } - /* Stop button — 44px touch target */ + /* State label — smaller on phone, stays first */ + .respawn-state { + font-size: 0.6rem; + padding: 0.05rem 0.3rem; + } + + /* Confidence badge — compact */ + .respawn-banner .detection-confidence { + font-size: 0.5rem; + padding: 0.02rem 0.2rem; + } + + /* Cycles — compact */ + .respawn-cycles { + font-size: 0.55rem; + } + + /* Run timer — compact */ + .respawn-timer { + font-size: 0.55rem; + padding: 0 0.25rem; + } + + /* Tokens — compact, truncate if long */ + .respawn-tokens { + font-size: 0.55rem; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + max-width: 120px; + } + + /* Spinner icon — smaller on phone */ + .respawn-indicator { + font-size: 0.65rem; + } + + /* Stop button — 36px touch target, pushed right */ .respawn-banner .btn-icon-only { min-width: 36px; min-height: 36px; @@ -1247,26 +1322,50 @@ html.mobile-init .file-browser-panel { justify-content: center; margin-left: auto; border-radius: 6px; + flex-shrink: 0; } - /* 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; + /* Row2: tighter spacing, smaller text */ + .respawn-status-row2 { + font-size: 0.55rem; + gap: 0.3rem; + min-height: 0; + flex-wrap: wrap; } - /* Countdown timers — tighter on mobile */ + /* Detection badges — smaller on phone */ + .respawn-banner .detection-hook, + .respawn-banner .detection-ai-check, + .respawn-banner .detection-status { + font-size: 0.55rem; + padding: 0.02rem 0.2rem; + } + + /* Countdown timers — inline, no progress bars */ .respawn-countdown-timers { gap: 0.2rem; } .respawn-countdown-timer { + font-size: 0.5rem; + padding: 0.02rem 0.2rem; + gap: 0.15rem; + } + + /* Hide progress bars on phone — values are enough */ + .respawn-countdown-timer .respawn-timer-bar { + display: none; + } + + /* Action log — single-line strip below status, show latest entry only */ + .respawn-action-log { + border-left: none; + border-top: 1px solid rgba(34, 197, 94, 0.15); + padding-left: 0; + padding-top: 0.15rem; + max-height: 1.4em; font-size: 0.55rem; - padding: 0.05rem 0.25rem; + overflow: hidden; } /* ---- Session Options Modal: Respawn Tab Mobile Optimizations ---- */ @@ -1292,28 +1391,27 @@ html.mobile-init .file-browser-panel { border-radius: 5px; } - /* Duration preset buttons — single row, all 7 items */ + /* Duration preset buttons — grid layout, 4 columns for even spacing */ .duration-presets { - display: flex; - flex-wrap: wrap; - gap: 0.25rem; + display: grid; + grid-template-columns: repeat(4, 1fr); + gap: 0.2rem; } .duration-preset-btn { - min-height: 28px; - padding: 0.2rem 0.4rem; + min-height: 32px; + padding: 0.2rem 0.25rem; font-size: 0.65rem; border-radius: 5px; text-align: center; - flex: 1 1 auto; - min-width: 0; } - /* Custom duration — inline with other presets */ + /* Custom duration — spans full row below */ .duration-custom { - flex-wrap: wrap; + grid-column: 1 / -1; + display: flex; gap: 0.25rem; - flex: 1 1 100%; + align-items: center; } .duration-custom .duration-preset-btn { @@ -1331,24 +1429,23 @@ html.mobile-init .file-browser-panel { font-size: 16px; /* Prevents iOS zoom */ } - /* Preset selector row — compact inline */ + /* Preset selector row — full-width dropdown, buttons below */ .preset-selector { - display: flex; - flex-wrap: wrap; + display: grid; + grid-template-columns: 1fr 1fr; gap: 0.25rem; } .preset-selector select { - width: 100%; - min-height: 28px; + grid-column: 1 / -1; + min-height: 32px; font-size: 16px; /* Prevents iOS zoom */ border-radius: 5px; padding: 0.2rem 0.4rem; } .preset-selector .btn { - flex: 1; - min-height: 28px; + min-height: 32px; font-size: 0.65rem; border-radius: 5px; padding: 0.2rem 0.4rem; diff --git a/src/web/public/session-ui.js b/src/web/public/session-ui.js index 260956e7..654acbb8 100644 --- a/src/web/public/session-ui.js +++ b/src/web/public/session-ui.js @@ -324,6 +324,8 @@ Object.assign(CodemanApp.prototype, { envOverrides.CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS = '1'; } const hasEnvOverrides = Object.keys(envOverrides).length > 0; + const useOpus1m = caseSettings.opusContext1m || globalSettings.opusContext1mEnabled; + const modelOverride = useOpus1m ? 'opus[1m]' : ''; // Step 1: Create all sessions in parallel this.terminal.writeln(`\x1b[90m Creating ${tabCount} session(s)...\x1b[0m`); @@ -331,7 +333,11 @@ Object.assign(CodemanApp.prototype, { fetch('/api/sessions', { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ workingDir, name, ...(hasEnvOverrides ? { envOverrides } : {}) }) + body: JSON.stringify({ + workingDir, name, + ...(hasEnvOverrides ? { envOverrides } : {}), + ...(modelOverride !== undefined ? { modelOverride } : {}), + }) }).then(r => r.json()) ); const createResults = await Promise.all(createPromises); @@ -913,6 +919,7 @@ Object.assign(CodemanApp.prototype, { const caseName = document.getElementById('quickStartCase').value || 'testcase'; const settings = this.getCaseSettings(caseName); document.getElementById('caseAgentTeams').checked = settings.agentTeams; + document.getElementById('caseOpusContext1m').checked = settings.opusContext1m; popover.classList.remove('hidden'); // Close on outside click (one-shot listener) @@ -934,7 +941,7 @@ Object.assign(CodemanApp.prototype, { const stored = localStorage.getItem('caseSettings_' + caseName); if (stored) return JSON.parse(stored); } catch { /* ignore */ } - return { agentTeams: false }; + return { agentTeams: false, opusContext1m: false }; }, saveCaseSettings(caseName, settings) { @@ -945,10 +952,13 @@ Object.assign(CodemanApp.prototype, { const caseName = document.getElementById('quickStartCase').value || 'testcase'; const settings = this.getCaseSettings(caseName); settings.agentTeams = document.getElementById('caseAgentTeams').checked; + settings.opusContext1m = document.getElementById('caseOpusContext1m').checked; this.saveCaseSettings(caseName, settings); - // Sync mobile checkbox + // Sync mobile checkboxes const mobileCheckbox = document.getElementById('caseAgentTeamsMobile'); if (mobileCheckbox) mobileCheckbox.checked = settings.agentTeams; + const mobileOpusCheckbox = document.getElementById('caseOpusContext1mMobile'); + if (mobileOpusCheckbox) mobileOpusCheckbox.checked = settings.opusContext1m; }, toggleCaseSettingsMobile() { @@ -957,6 +967,7 @@ Object.assign(CodemanApp.prototype, { const caseName = document.getElementById('quickStartCase').value || 'testcase'; const settings = this.getCaseSettings(caseName); document.getElementById('caseAgentTeamsMobile').checked = settings.agentTeams; + document.getElementById('caseOpusContext1mMobile').checked = settings.opusContext1m; popover.classList.remove('hidden'); const closeHandler = (e) => { @@ -975,10 +986,13 @@ Object.assign(CodemanApp.prototype, { const caseName = document.getElementById('quickStartCase').value || 'testcase'; const settings = this.getCaseSettings(caseName); settings.agentTeams = document.getElementById('caseAgentTeamsMobile').checked; + settings.opusContext1m = document.getElementById('caseOpusContext1mMobile').checked; this.saveCaseSettings(caseName, settings); - // Sync desktop checkbox + // Sync desktop checkboxes const desktopCheckbox = document.getElementById('caseAgentTeams'); if (desktopCheckbox) desktopCheckbox.checked = settings.agentTeams; + const desktopOpusCheckbox = document.getElementById('caseOpusContext1m'); + if (desktopOpusCheckbox) desktopOpusCheckbox.checked = settings.opusContext1m; }, // ═══════════════════════════════════════════════════════════════ diff --git a/src/web/public/settings-ui.js b/src/web/public/settings-ui.js index d3110875..49e60904 100644 --- a/src/web/public/settings-ui.js +++ b/src/web/public/settings-ui.js @@ -332,6 +332,7 @@ Object.assign(CodemanApp.prototype, { }; // Claude Permissions settings document.getElementById('appSettingsAgentTeams').checked = settings.agentTeamsEnabled ?? false; + document.getElementById('appSettingsOpusContext1m').checked = settings.opusContext1mEnabled ?? false; // CPU Priority settings const niceSettings = settings.nice || {}; document.getElementById('appSettingsNiceEnabled').checked = niceSettings.enabled ?? false; @@ -1130,6 +1131,7 @@ Object.assign(CodemanApp.prototype, { allowedTools: document.getElementById('appSettingsAllowedTools').value.trim(), // Claude Permissions settings agentTeamsEnabled: document.getElementById('appSettingsAgentTeams').checked, + opusContext1mEnabled: document.getElementById('appSettingsOpusContext1m').checked, // CPU Priority settings nice: { enabled: document.getElementById('appSettingsNiceEnabled').checked, diff --git a/src/web/routes/session-routes.ts b/src/web/routes/session-routes.ts index 3b9e5bf7..dd9420e4 100644 --- a/src/web/routes/session-routes.ts +++ b/src/web/routes/session-routes.ts @@ -42,7 +42,7 @@ import { validatePathWithinBase, } from '../route-helpers.js'; import { AUTH_COOKIE_NAME } from '../middleware/auth.js'; -import { writeHooksConfig, updateCaseEnvVars } from '../../hooks-config.js'; +import { writeHooksConfig, updateCaseEnvVars, updateCaseModel } from '../../hooks-config.js'; import { generateClaudeMd } from '../../templates/claude-md.js'; import { imageWatcher } from '../../image-watcher.js'; import { getLifecycleLog } from '../../session-lifecycle-log.js'; @@ -166,6 +166,11 @@ export function registerSessionRoutes( await updateCaseEnvVars(workingDir, body.envOverrides); } + // Write model override to .claude/settings.local.json if provided + if (body.modelOverride !== undefined) { + await updateCaseModel(workingDir, body.modelOverride || null); + } + // Check OpenCode availability if requested if (body.mode === 'opencode') { const { isOpenCodeAvailable } = await import('../../utils/opencode-cli-resolver.js'); diff --git a/src/web/schemas.ts b/src/web/schemas.ts index 4537745d..d142e167 100644 --- a/src/web/schemas.ts +++ b/src/web/schemas.ts @@ -124,6 +124,8 @@ export const CreateSessionSchema = z.object({ mode: z.enum(['claude', 'shell', 'opencode']).optional(), name: z.string().max(100).optional(), envOverrides: safeEnvOverridesSchema, + /** Model override to write to .claude/settings.local.json (e.g., "opus[1m]"). Empty string clears. */ + modelOverride: z.string().max(50).optional(), openCodeConfig: OpenCodeConfigSchema, /** Resume a previous Claude conversation by its session ID (used for reboot recovery) */ resumeSessionId: z