diff --git a/.gitignore b/.gitignore index 2aec0d13..b9ca8c9c 100644 --- a/.gitignore +++ b/.gitignore @@ -18,6 +18,10 @@ coverage/ test/e2e/screenshots/current/ test/e2e/screenshots/diffs/ +# Mobile visual regression failure artifacts +test/mobile/snapshots/*.actual.png +test/mobile/snapshots/*.diff.png + # Logs *.log npm-debug.log* diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 00000000..e772c0e7 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,42 @@ +# Repository Guidelines + +## Project Structure & Module Organization + +Codeman is a TypeScript ESM Node project. Core backend code lives in `src/`, with web routes in `src/web/routes/`, web server wiring in `src/web/`, config in `src/config/`, shared types in `src/types/`, and utilities in `src/utils/`. Browser assets are plain JavaScript/CSS under `src/web/public/`. Tests live in `test/`, with route tests in `test/routes/`, mobile Playwright/Vitest tests in `test/mobile/`, and mocks in `test/mocks/`. The standalone xterm local-echo package lives in `packages/xterm-zerolag-input/`; behavior copied into the web UI must stay in sync. Documentation and plans are in `docs/`, scripts in `scripts/`, and build/lint/test config in `config/`. + +## Build, Test, and Development Commands + +- `npm run dev`: start the local web app via `tsx src/index.ts web`. +- `npm run build`: build production output into `dist/` using `scripts/build.mjs`. +- `npm run start`: run the built CLI/web app from `dist/index.js`. +- `npm run typecheck`: run `tsc --noEmit`. +- `npm run lint` / `npm run lint:fix`: check or fix TypeScript lint issues. +- `npm run format:check` / `npm run format`: check or apply Prettier formatting. +- `npm run check:lockfile`: verify workspace lockfile sync. + +## Coding Style & Naming Conventions + +Use TypeScript with strict compiler settings and ESM imports only; avoid `require()`. Prettier uses single quotes, trailing commas where valid, and a 120-column print width. ESLint allows `console`, errors on `debugger`, and warns on explicit `any`. Prefer domain-specific files such as `src/web/routes/*-routes.ts`, `src/types/*.ts`, and `src/config/*.ts`; import config from specific files rather than barrels. Keep route handlers schema-validated with Zod where request input is accepted. + +## Testing Guidelines + +Vitest is the primary test framework. Do not run bare `npm test` in managed Codeman/tmux sessions because the full suite can spawn and clean up tmux sessions. Prefer targeted runs: + +```bash +npm test -- test/routes/session-routes.test.ts +npm test -- -t "auth" +``` + +Route tests should use `app.inject()` instead of live ports. When adding tests that need ports, search for existing `const PORT =` values and choose a unique one. Coverage is available with `npm run test:coverage`. + +## Commit & Pull Request Guidelines + +Recent history uses Conventional Commit-style messages such as `feat(web): ...`, `fix: ...`, `docs: ...`, and `chore: version packages`. Keep commits scoped and descriptive. Pull requests should include the problem, the approach, linked issues when available, and screenshots or screen recordings for UI/mobile changes. Before opening a PR, run `npm run typecheck`, `npm run lint`, `npm run format:check`, `npm run check:lockfile`, and relevant targeted tests. + +## Branch Workflow + +Do not develop new features or bug fixes directly on `master`. Start from an up-to-date `master`, create a focused branch such as `fix/mobile-toolbar-overlap` or `feat/session-search`, commit there, then merge or open a pull request after verification. Keep uncommitted work on the branch until it is ready to integrate. + +## Security & Configuration Tips + +Do not commit secrets or local state from `~/.codeman/`. Auth and CLI behavior are controlled through environment variables such as `CODEMAN_USERNAME`, `CODEMAN_PASSWORD`, `CLAUDE_CODE_*`, and `OPENCODE_*`; validate new settings through the existing schemas and prefix allowlists. diff --git a/src/session.ts b/src/session.ts index cfc907d8..c7e9084b 100644 --- a/src/session.ts +++ b/src/session.ts @@ -82,6 +82,8 @@ import { SessionTaskCache } from './session-task-cache.js'; export type { BackgroundTask } from './task-tracker.js'; export type { RalphTrackerState, RalphTodoItem, ActiveBashTool } from './types.js'; +export type ResizeViewportType = 'mobile' | 'tablet' | 'desktop'; + /** Line buffer flush interval (100ms) - forces processing of partial lines */ const LINE_BUFFER_FLUSH_INTERVAL = 100; @@ -2058,7 +2060,11 @@ export class Session extends EventEmitter { * @param cols - Number of columns (width in characters) * @param rows - Number of rows (height in lines) */ - resize(cols: number, rows: number): void { + resize(cols: number, rows: number, options: { viewportType?: ResizeViewportType } = {}): void { + const isSmallViewport = options.viewportType === 'mobile' || options.viewportType === 'tablet'; + if (isSmallViewport && cols < this._ptyCols) { + return; + } if (this.ptyProcess && (cols !== this._ptyCols || rows !== this._ptyRows)) { this._ptyCols = cols; this._ptyRows = rows; diff --git a/src/web/public/app.js b/src/web/public/app.js index 821cbf2b..6ff9cbe6 100644 --- a/src/web/public/app.js +++ b/src/web/public/app.js @@ -576,6 +576,7 @@ class CodemanApp { // Apply keyboard bar mode from settings const _kbSettings = this.loadAppSettingsFromStorage(); if (_kbSettings.extendedKeyboardBar) KeyboardAccessoryBar.setMode('extended'); + this.bindMobileHeaderUtilityToggle?.(); this.applyHeaderVisibilitySettings(); this.applyTabWrapSettings(); this.applyMonitorVisibility(); @@ -2026,9 +2027,20 @@ class CodemanApp { const cjkEl = document.getElementById('cjkInput'); if (!cjkEl) return; const settings = this.loadAppSettingsFromStorage(); - const showCjk = this._serverCjkOverride || settings.cjkInputEnabled || false; + const defaults = this.getDefaultSettings?.() || {}; + const isTouchTerminal = + typeof MobileDetection !== 'undefined' && + MobileDetection.isTouchDevice() && + (MobileDetection.isSmallScreen() || MobileDetection.isMediumScreen()); + const showCjk = + this._serverCjkOverride || (!isTouchTerminal && (settings.cjkInputEnabled ?? defaults.cjkInputEnabled ?? false)); + cjkEl.classList.toggle('cjk-input-visible', !!showCjk); + document.body.classList.toggle('cjk-input-visible', !!showCjk); cjkEl.style.display = showCjk ? 'block' : 'none'; + cjkEl.setAttribute('aria-hidden', showCjk ? 'false' : 'true'); + if (showCjk && cjkEl.value === '\u200B') cjkEl.value = ''; if (!showCjk) window.cjkActive = false; + if (typeof KeyboardHandler !== 'undefined') KeyboardHandler.updateLayoutForKeyboard(); } /** @@ -2078,6 +2090,7 @@ class CodemanApp { KeyboardHandler.cleanup(); MobileDetection.init(); KeyboardHandler.init(); + this.bindMobileHeaderUtilityToggle?.(); // Clear tab alerts this.tabAlerts.clear(); // Clear shown completions (used for duplicate notification prevention) @@ -2550,7 +2563,7 @@ class CodemanApp { const tallTabsEnabled = this._tallTabsEnabled ?? false; const showFolder = tallTabsEnabled && session.name && folderName && folderName !== name; - parts.push(`