Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions CLAUDE.md

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion src/web/public/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -686,7 +686,7 @@ <h3 class="history-title" id="historyTitle">Resume Conversation</h3>
<button class="btn-toolbar btn-shell" onclick="app.runShell()" title="Run Shell">
Run Shell
</button>
<!-- Phone-only: replaces the Shell button on ≤430px (Shell moves into the Run
<!-- Phone-only: replaces the Shell button under 600px (Shell moves into the Run
dropdown there). Sends a bare Enter to the active session, the complement
to the accessory bar's Esc. Hidden everywhere else — see styles.css. -->
<button class="btn-toolbar btn-enter" onclick="app.sendEnterKey()" title="Send Enter">
Expand Down
10 changes: 5 additions & 5 deletions src/web/public/mobile-handlers.js
Original file line number Diff line number Diff line change
Expand Up @@ -85,20 +85,20 @@ const MobileDetection = {
return /^((?!chrome|android).)*safari/i.test(navigator.userAgent);
},

/** Check if screen is small (phone-sized, <430px) */
/** Check if screen is small (phone-sized, <600px) */
isSmallScreen() {
return window.innerWidth < 430;
return window.innerWidth < 600;
},

/** Check if screen is medium (tablet-sized, 430-768px) */
/** Check if screen is medium (tablet-sized, 600-768px) */
isMediumScreen() {
return window.innerWidth >= 430 && window.innerWidth < 768;
return window.innerWidth >= 600 && window.innerWidth < 768;
},

/** Get device type based on screen width */
getDeviceType() {
const width = window.innerWidth;
if (width < 430) return 'mobile';
if (width < 600) return 'mobile';
if (width < 768) return 'tablet';
return 'desktop';
},
Expand Down
4 changes: 2 additions & 2 deletions src/web/public/mobile-overview.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
* the SAME comparator the desktop rail uses: blocked longest-first, then
* running longest-first, then quiet most-recently-quiet first.
*
* PHONE ONLY. The gate is `shouldUseMobileOverview()` (viewport < 430px, not a
* PHONE ONLY. The gate is `shouldUseMobileOverview()` (viewport < 600px, not a
* popped-out solo window, per-device setting on). Tablet and desktop keep the
* welcome overlay untouched. The container ships with the `hidden` attribute and
* only this module removes it, so desktop (which never loads mobile.css) cannot
Expand All @@ -38,7 +38,7 @@
*/

/** Viewport width that counts as a phone. Matches the mobile.css phone block. */
const MOBILE_OVERVIEW_PHONE_QUERY = '(max-width: 430px)';
const MOBILE_OVERVIEW_PHONE_QUERY = '(max-width: 599px)';

/** How many past conversations show before the "Show all" toggle. */
const MOBILE_OVERVIEW_PAST_LIMIT = 8;
Expand Down
24 changes: 12 additions & 12 deletions src/web/public/mobile.css
Original file line number Diff line number Diff line change
Expand Up @@ -46,9 +46,9 @@ html.mobile-init .file-browser-panel {
}

/* ============================================================================
Tablet Breakpoint (430px - 768px)
Tablet Breakpoint (600px - 768px)
============================================================================ */
@media (max-width: 768px) and (min-width: 430px) {
@media (max-width: 768px) and (min-width: 600px) {
/* Compact header for tablet - fixed at top, includes safe area padding */
.header {
position: fixed;
Expand Down Expand Up @@ -303,12 +303,12 @@ html.mobile-init .file-browser-panel {
}

/* Show desktop voice button on tablet (hidden by max-width:1023px in styles.css,
mobile .btn-voice-mobile only shows at <430px) */
mobile .btn-voice-mobile only shows at <600px) */
.toolbar-center .btn-toolbar.btn-voice {
display: flex !important;
}

/* Toolbar — use desktop-style sizing on tablet (plenty of room at 430-768px) */
/* Toolbar — use desktop-style sizing on tablet (plenty of room at 600-768px) */
.toolbar {
padding: 0 0.5rem;
gap: 0.5rem;
Expand All @@ -333,9 +333,9 @@ html.mobile-init .file-browser-panel {
}

/* ============================================================================
Phone Breakpoint (<430px)
Phone Breakpoint (<600px)
============================================================================ */
@media (max-width: 430px) {
@media (max-width: 599px) {
/* Phones get a 44px header, up from 36px. Every header control is a touch
target and 44px is the floor for one; the brand "C" that gets you home is
the one that matters most. Redefined as the TOKEN rather than a literal so
Expand Down Expand Up @@ -499,7 +499,7 @@ html.mobile-init .file-browser-panel {

/* Exception to the 26px shrink above: in sidebar layout this button is the
ONLY way to open the session list — the strip it replaced is gone. A 26px
target is below --touch-target-min (44px), which the 430-768px block
target is below --touch-target-min (44px), which the 600-768px block
already enforces for every other header button. */
html[data-session-list='sidebar'] #sidebarToggleBtn {
width: 44px;
Expand Down Expand Up @@ -1167,7 +1167,7 @@ html.mobile-init .file-browser-panel {
}
}

@media (max-width: 430px) {
@media (max-width: 599px) {
.btn-case-settings-mobile {
display: none !important;
}
Expand Down Expand Up @@ -3144,7 +3144,7 @@ html:is([data-skin="paper-gray"], [data-skin="solarized-light"], [data-skin="cat

/* Keyboard accessory bar + paste overlay base styles moved to styles.css
(always loaded — covers iPad landscape where mobile.css doesn't load).
Phone-specific overrides remain in @media (max-width: 430px) above. */
Phone-specific overrides remain in @media (max-width: 599px) above. */

/* ============================================================================
iOS Safari Specific Fixes
Expand Down Expand Up @@ -3187,7 +3187,7 @@ html:is([data-skin="paper-gray"], [data-skin="solarized-light"], [data-skin="cat
}
}

@media (max-width: 430px) {
@media (max-width: 599px) {
/* Attachment history (COD-18): full-screen sheet on phones */
.attachment-history-drawer {
top: 0;
Expand Down Expand Up @@ -3237,7 +3237,7 @@ html:is([data-skin="paper-gray"], [data-skin="solarized-light"], [data-skin="cat
already reserves that space), so it needs the same safe-area padding as the
other banners. The overlay is fixed and handles its own insets.
============================================================================ */
@media (max-width: 430px) {
@media (max-width: 599px) {
.offline-banner {
padding: 0.4rem 0.5rem;
padding-left: calc(0.5rem + var(--safe-area-left));
Expand Down Expand Up @@ -3727,7 +3727,7 @@ html:is([data-skin="paper-gray"], [data-skin="solarized-light"], [data-skin="cat
This whole file is served with media="(max-width: 1023px)", so these
top-level rules cover the entire handheld range — deliberately NOT wrapped in
a nested @media, because the two compact `.session-tabs` blocks above live in
`max-width: 768px` and `max-width: 430px` and would leave 769-1023px
`max-width: 768px` and `max-width: 599px` and would leave 769-1023px
unhandled.

Placement at the END of the file is load-bearing: the compact strip blocks at
Expand Down
2 changes: 1 addition & 1 deletion src/web/public/session-ui.js
Original file line number Diff line number Diff line change
Expand Up @@ -2526,7 +2526,7 @@ Object.assign(CodemanApp.prototype, {
if (!input._mobileScrollWired) {
input._mobileScrollWired = true;
input.addEventListener('focus', () => {
if (window.innerWidth <= 430) {
if (window.innerWidth < 600) {
setTimeout(() => input.scrollIntoView({ behavior: 'smooth', block: 'center' }), 300);
}
});
Expand Down
2 changes: 1 addition & 1 deletion src/web/public/settings-ui.js
Original file line number Diff line number Diff line change
Expand Up @@ -376,7 +376,7 @@ Object.assign(CodemanApp.prototype, {
document.getElementById('appSettingsShowMultiMonitorButton').checked = settings.showMultiMonitorButton ?? defaults.showMultiMonitorButton ?? false;
document.getElementById('appSettingsShowPlanUsageLimits').checked = this.planUsageChipEnabled(settings);
document.getElementById('appSettingsShowRedrawButton').checked = settings.showRedrawButton ?? defaults.showRedrawButton ?? false;
// Phone overview home screen: only meaningful under 430px, so the row is
// Phone overview home screen: only meaningful under 600px, so the row is
// hidden elsewhere rather than offering a toggle that changes nothing.
// Spawn lineage lines: desktop-only (the overlay sits UNDER the fixed mobile
// header), so the row is hidden elsewhere rather than offering a toggle that
Expand Down
6 changes: 3 additions & 3 deletions src/web/public/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -5156,7 +5156,7 @@ body.touch-device .terminal-container .xterm .xterm-helper-textarea {
.run-mode-dot.shell { background: #94a3b8; }

/* Phone-only Enter button (see index.html). Hidden by default at every width;
mobile.css turns it on inside @media (max-width: 430px), where it takes over
mobile.css turns it on inside @media (max-width: 599px), where it takes over
the slot the Shell button occupies on wider screens. */
.btn-toolbar.btn-enter {
display: none;
Expand Down Expand Up @@ -11807,7 +11807,7 @@ kbd {
}
/* Footer row: the buttons are btn-toolbar (display: flex, block-level), so
without this rule the four of them stack vertically. Mirrors the
runSummaryModal footer; the ≤430px block in mobile.css adds wrapping. */
runSummaryModal footer; the phone block (under 600px) in mobile.css adds wrapping. */
.readmymind-modal .modal-footer {
display: flex;
justify-content: flex-end;
Expand Down Expand Up @@ -13184,7 +13184,7 @@ body.touch-device.cjk-input-visible .main {
Keyboard Accessory Bar
Base styles in styles.css (always loaded) so iPad landscape (≥1024px,
where mobile.css doesn't load) still gets dark styling. Phone overrides
remain in mobile.css @media (max-width: 430px).
remain in mobile.css @media (max-width: 599px).
═══════════════════════════════════════════════════════════════ */

.keyboard-accessory-bar {
Expand Down
2 changes: 1 addition & 1 deletion src/web/public/terminal-ui.js
Original file line number Diff line number Diff line change
Expand Up @@ -4842,7 +4842,7 @@ Object.assign(CodemanApp.prototype, {
const viewportType =
typeof MobileDetection !== 'undefined' && MobileDetection.getDeviceType
? MobileDetection.getDeviceType()
: window.innerWidth < 430
: window.innerWidth < 600
? 'mobile'
: window.innerWidth < 768
? 'tablet'
Expand Down
2 changes: 1 addition & 1 deletion test/home-sessions.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ function loadHomeSessionsApp(overrides: Record<string, any> = {}, innerWidth = 1
createElement: () => fakeElement(),
createElementNS: () => fakeElement(),
},
MobileDetection: { getDeviceType: () => (innerWidth < 430 ? 'mobile' : 'desktop') },
MobileDetection: { getDeviceType: () => (innerWidth < 600 ? 'mobile' : 'desktop') },
});
for (const file of ['constants.js', 'mobile-overview.js', 'home-sessions.js']) {
vm.runInContext(readFileSync(resolve(PUBLIC, file), 'utf8'), context, { filename: file });
Expand Down
6 changes: 3 additions & 3 deletions test/mobile-header-buttons-policy.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
//
// Policy: every header button that is VISIBLE BY DEFAULT on desktop must have an
// explicit decision for phones — either it's hidden via an @media (max-width:
// 430px) display:none rule in mobile.css, or it's added to MOBILE_VISIBLE_ALLOWLIST
// 600px) display:none rule in mobile.css, or it's added to MOBILE_VISIBLE_ALLOWLIST
// below with a reason. A new default-visible header button with neither fails this
// test, forcing the author to decide its mobile behavior.
//
Expand Down Expand Up @@ -123,7 +123,7 @@ describe('Mobile header button policy (static guard)', () => {
hidden || allowed,
`Header button .${btn.distinguishing.join('.')} (id=${btn.id || '?'}) is VISIBLE BY DEFAULT but has ` +
`no mobile-visibility decision.\n` +
` → To hide it on phones: add it to the @media (max-width: 430px) "display: none" block in ` +
` → To hide it on phones: add it to the @media (max-width: 599px) "display: none" block in ` +
`src/web/public/mobile.css (next to .btn-settings / .btn-lifecycle-log).\n` +
` → To keep it visible on phones: add '${btn.distinguishing[0]}' to MOBILE_VISIBLE_ALLOWLIST in ` +
`this test, with a reason.\n` +
Expand All @@ -137,7 +137,7 @@ describe('Mobile header button policy (static guard)', () => {
for (const cls of KNOWN_PHONE_HIDDEN) {
expect(
phoneHidden.has(cls),
`${cls} must stay hidden on phones — restore its rule in the @media (max-width: 430px) ` +
`${cls} must stay hidden on phones — restore its rule in the @media (max-width: 599px) ` +
`display:none block in src/web/public/mobile.css.`
).toBe(true);
}
Expand Down
2 changes: 1 addition & 1 deletion test/mobile-overview.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// Port: none (pure model + static markup assertions — no browser, no server).
//
// The phone home screen (src/web/public/mobile-overview.js) replaces the welcome
// overlay under 430px. Its grouping logic is the part that can silently go wrong:
// overlay under 600px. Its grouping logic is the part that can silently go wrong:
// a session blocked on a permission prompt landing in "idle" is exactly the bug
// this surface exists to prevent. buildMobileOverviewModel() is pure for that
// reason, so it can be exercised here against plain objects.
Expand Down
2 changes: 1 addition & 1 deletion test/mobile-tab-tap-zones.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ import { describe, expect, it } from 'vitest';
const CSS = readFileSync(resolve(import.meta.dirname, '../src/web/public/mobile.css'), 'utf8');
const ROOT = postcss.parse(CSS);
/** The phone block. Tablets keep the roomier layout and are deliberately out of scope. */
const PHONE_QUERY = '(max-width: 430px)';
const PHONE_QUERY = '(max-width: 599px)';
/** `.session-tab` border, from styles.css: `border: 1px solid transparent`. */
const TAB_BORDER = 1;
/**
Expand Down
6 changes: 3 additions & 3 deletions test/mobile/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,11 +89,11 @@ Matching `app.js MobileDetection` and `mobile.css` media queries:

| Breakpoint | Width | CSS Class | Header | Toolbar |
|------------|-------|-----------|--------|---------|
| **Phone** | ≤ 430px | `device-mobile` | Fixed at top | Fixed at bottom |
| **Tablet** | 431–768px | `device-tablet` | Fixed at top | Relative (in flow) |
| **Phone** | ≤ 599px | `device-mobile` | Fixed at top | Fixed at bottom |
| **Tablet** | 600–768px | `device-tablet` | Fixed at top | Relative (in flow) |
| **Desktop** | > 768px | `device-desktop` | Relative (in flow) | Relative (in flow) |

Breakpoint boundaries (430px, 768px) use `max-width` which is **inclusive** — a 430px device is phone, a 768px device is tablet.
The phone block is `max-width: 599px` and the tablet block starts at `min-width: 600px`, so a 599px device is phone and a 600px device (Nexus 7) is a small tablet in both CSS and the JS `getDeviceType()` cutoff (`< 600`). The tablet/desktop boundary (768px) is `max-width` inclusive: a 768px device is tablet.

## Architecture

Expand Down
4 changes: 2 additions & 2 deletions test/mobile/device-matrix.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ const BASE_URL = `http://localhost:${PORT}`;

let server: WebServer;

// Hidden on phones (< 430px width)
// Hidden on phones (< 600px width)
const PHONE_HIDDEN_SELECTORS = [
SELECTORS.HEADER_BRAND,
SELECTORS.CASE_SELECT_GROUP,
Expand Down Expand Up @@ -94,7 +94,7 @@ describe('Device Matrix', () => {
const { width } = device.viewport;
const position = await getCSSProperty(page, SELECTORS.TOOLBAR, 'position');
if (width <= BREAKPOINTS.PHONE_MAX) {
// Phone (max-width: 430px includes 430): fixed toolbar
// Phone (max-width: 599px): fixed toolbar
expect(position).toBe('fixed');
} else {
// Tablet/desktop: relative toolbar
Expand Down
2 changes: 1 addition & 1 deletion test/mobile/devices.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ export interface DeviceEntry {
// ---------------------------------------------------------------------------

function breakpointFor(width: number): 'phone' | 'tablet' | 'desktop' {
if (width < 430) return 'phone';
if (width < 600) return 'phone';
if (width < 768) return 'tablet';
return 'desktop';
}
Expand Down
2 changes: 1 addition & 1 deletion test/mobile/helpers/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ export const SELECTORS = {

// Device breakpoints (match app.js MobileDetection)
export const BREAKPOINTS = {
PHONE_MAX: 430,
PHONE_MAX: 599,
TABLET_MAX: 768,
} as const;

Expand Down
6 changes: 3 additions & 3 deletions test/mobile/layout.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,7 @@ describe('Mobile Layout', () => {
}
});

it('does not render the desktop voice button at the 430px phone/tablet boundary', async () => {
it('does not render the desktop voice button on a 430px large phone', async () => {
const device = REPRESENTATIVE_DEVICES['large-phone'];
const { context, page } = await createDevicePage(device, BASE_URL, 'chromium');
try {
Expand Down Expand Up @@ -331,7 +331,7 @@ describe('Mobile Layout', () => {
}
});

it('width < 430 adds device-mobile', async () => {
it('width < 600 adds device-mobile', async () => {
const { context, page } = await createDevicePage(iPhone14Pro, BASE_URL);
try {
await assertDeviceClasses(page, iPhone14Pro.viewport.width);
Expand All @@ -340,7 +340,7 @@ describe('Mobile Layout', () => {
}
});

it('width 430-768 adds device-tablet', async () => {
it('width 600-768 adds device-tablet', async () => {
const tablet = REPRESENTATIVE_DEVICES['small-tablet'];
const { context, page } = await createDevicePage(tablet, BASE_URL);
try {
Expand Down
8 changes: 4 additions & 4 deletions test/mobile/visual-regression.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ describe('Visual Regression', () => {
isMobile: width < 768,
hasTouch: width < 768,
userAgent: 'Mozilla/5.0 (iPhone; CPU iPhone OS 18_0 like Mac OS X) AppleWebKit/605.1.15',
expectedBreakpoint: (width < 430 ? 'phone' : width < 768 ? 'tablet' : 'desktop') as 'phone' | 'tablet' | 'desktop',
expectedBreakpoint: (width < 600 ? 'phone' : width < 768 ? 'tablet' : 'desktop') as 'phone' | 'tablet' | 'desktop',
isIOS: true,
defaultBrowserType: 'chromium' as const,
};
Expand All @@ -71,7 +71,7 @@ describe('Visual Regression', () => {
isMobile: width < 768,
hasTouch: width < 768,
userAgent: 'Mozilla/5.0 (iPhone; CPU iPhone OS 18_0 like Mac OS X) AppleWebKit/605.1.15',
expectedBreakpoint: (width < 430 ? 'phone' : width < 768 ? 'tablet' : 'desktop') as 'phone' | 'tablet' | 'desktop',
expectedBreakpoint: (width < 600 ? 'phone' : width < 768 ? 'tablet' : 'desktop') as 'phone' | 'tablet' | 'desktop',
isIOS: true,
defaultBrowserType: 'chromium' as const,
};
Expand Down Expand Up @@ -104,7 +104,7 @@ describe('Visual Regression', () => {
isMobile: width < 768,
hasTouch: width < 768,
userAgent: 'Mozilla/5.0 (iPhone; CPU iPhone OS 18_0 like Mac OS X) AppleWebKit/605.1.15',
expectedBreakpoint: (width < 430 ? 'phone' : width < 768 ? 'tablet' : 'desktop') as 'phone' | 'tablet' | 'desktop',
expectedBreakpoint: (width < 600 ? 'phone' : width < 768 ? 'tablet' : 'desktop') as 'phone' | 'tablet' | 'desktop',
isIOS: true,
defaultBrowserType: 'chromium' as const,
};
Expand All @@ -115,7 +115,7 @@ describe('Visual Regression', () => {

// Open settings modal - try mobile button first, then desktop
const mobileBtn = page.locator(SELECTORS.SETTINGS_MOBILE);
const isPhone = width < 430;
const isPhone = width < 600;
if (isPhone && await mobileBtn.isVisible()) {
await mobileBtn.click();
} else {
Expand Down
4 changes: 2 additions & 2 deletions test/readmymind-phone-key.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,8 @@ describe('read my mind phone key + alternates (static guards)', () => {
const html = read('index.html');
const ui = read('readmymind-ui.js');
const settingsUi = read('settings-ui.js');
// Everything phone-specific lives in the max-width 430px block of mobile.css.
const phoneBlock = mobile.slice(mobile.indexOf('@media (max-width: 430px)'));
// Everything phone-specific lives in the max-width 599px block of mobile.css.
const phoneBlock = mobile.slice(mobile.indexOf('@media (max-width: 599px)'));

it('ships the 🧠 key in BOTH accessory bar templates and routes it to the modal', () => {
const simple = accessory.match(/_simpleButtons\s*:\s*`([\s\S]*?)`/)?.[1] ?? '';
Expand Down