mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-25 02:19:33 +01:00
Compare commits
6
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
41a82fcf02 | ||
|
|
eecf74c001 | ||
|
|
23b4dfcd82 | ||
|
|
e017b275fe | ||
|
|
e8a809ea80 | ||
|
|
8006cc5db3 |
No files matched your search
@@ -1,5 +1,42 @@
|
|||||||
# aicodeman
|
# aicodeman
|
||||||
|
|
||||||
|
## 0.6.6
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- **Terminal scrollback significantly increased** — both the xterm.js viewport and the tmux backing buffer were bottlenecking how far back you could scroll. Three changes:
|
||||||
|
- `DEFAULT_SCROLLBACK` raised from 20000 → 50000 lines (xterm.js, main terminal). The previous bump from 5000 only helped users with empty localStorage; existing users were stuck on whatever value they first picked up. The loader now treats `DEFAULT_SCROLLBACK` as a floor — if your stored value is below the new minimum, you're raised to it automatically.
|
||||||
|
- Subagent / teammate terminals (`panels-ui.js`) were stuck at 5000; now use the same `DEFAULT_SCROLLBACK` constant (50000).
|
||||||
|
- New tmux sessions now run with `history-limit 50000` (tmux defaults to 2000). This matters for hard-reload / re-attach — without it, only the last ~2000 lines survive the round-trip back into a fresh xterm.
|
||||||
|
|
||||||
|
**Tmux flicker on session re-attach fixed (PR #80 by @aakhter)**: the PTY now queries the existing tmux window size via `tmux display -p` before spawning, instead of hardcoding 120x40. Previously, every re-attach forced tmux to resize down to 120x40, causing a visible flicker and one frame of scrollback loss. The `-x 120 -y 40` flag was also dropped from `tmux new-session` so the initial size matches the first attaching client. Uses `execFileSync` (not shell) for safety and falls back to 120x40 on any error.
|
||||||
|
|
||||||
|
**Docs**: CLAUDE.md now documents two recurring foot-guns — the `xterm-zerolag-input` overlay code is duplicated between `packages/xterm-zerolag-input/src/` and inline inside `src/web/public/app.js`, so any overlay change must touch both; and the COM workflow explicitly includes a post-push `gh run watch` step to confirm CI before considering the release done.
|
||||||
|
|
||||||
|
## 0.6.5
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- **Mobile fix**
|
||||||
|
- Android virtual keyboard: space character was silently dropped on touch devices using GBoard / SwiftKey / similar IMEs. Root cause: the input-event handler in `terminal-ui.js` treated any whitespace-only textarea value as proof that xterm had already processed the input. A lone space (`' '.trim() === ''`) tripped this guard, so the space was consumed but never forwarded. Now skips only when the textarea is truly empty (or whitespace from a non-space key). Reported and diagnosed by @coolk8 in #79.
|
||||||
|
|
||||||
|
**Docs**
|
||||||
|
- `CLAUDE.md`: added Zod `.optional()`-vs-`null` gotcha (recurring trap from 0.6.3 / 0.6.4 incidents) and a more visible warning against running bare `npm test` (kills the host tmux session).
|
||||||
|
- `docs/local-echo-overlay-plan.md`: marked SHIPPED, corrected xterm version reference (v5.3.0 → `@xterm/xterm` ^6.0.0).
|
||||||
|
|
||||||
|
## 0.6.4
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Fix "Failed to enable respawn: Invalid request body" error when selecting infinity duration (∞) in the respawn modal. Frontend was sending `durationMinutes: null`, which Zod's `.optional()` schema rejected (it accepts `undefined` only). The body now omits the field when no duration is selected.
|
||||||
|
|
||||||
|
## 0.6.3
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- **Fix**
|
||||||
|
- Allowlist `opusContext1mEnabled` in `SettingsUpdateSchema`. Without this entry, the strict schema rejected `PUT /api/settings {"opusContext1mEnabled":...}` with `INVALID_INPUT`, so the toggle's value never persisted across reloads. The frontend was already reading and writing this key (`settings-ui.js:336/1137`, `session-ui.js:340`), so saves were silently failing — users never noticed because the load path falls back to `false` on missing keys, hiding the bug. (#78)
|
||||||
|
|
||||||
## 0.6.2
|
## 0.6.2
|
||||||
|
|
||||||
### Patch Changes
|
### Patch Changes
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
|
|||||||
| Type check | `tsc --noEmit` |
|
| Type check | `tsc --noEmit` |
|
||||||
| Lint | `npm run lint` (fix: `npm run lint:fix`) |
|
| Lint | `npm run lint` (fix: `npm run lint:fix`) |
|
||||||
| Format | `npm run format` (check: `npm run format:check`) |
|
| Format | `npm run format` (check: `npm run format:check`) |
|
||||||
| Single test | `npm test -- test/<file>.test.ts` (or `npx vitest run --config config/vitest.config.ts test/<file>.test.ts`) |
|
| Single test | `npm test -- test/<file>.test.ts` (or `npx vitest run --config config/vitest.config.ts test/<file>.test.ts`) — ⚠ **never** run bare `npm test`, see Testing section |
|
||||||
| Build | `npm run build` (esbuild via `scripts/build.mjs`, NOT tsc — `tsc --noEmit` is type-check only) |
|
| Build | `npm run build` (esbuild via `scripts/build.mjs`, NOT tsc — `tsc --noEmit` is type-check only) |
|
||||||
| Production | `npm run build && systemctl --user restart codeman-web` |
|
| Production | `npm run build && systemctl --user restart codeman-web` |
|
||||||
|
|
||||||
@@ -52,10 +52,11 @@ When user says "COM":
|
|||||||
3. **Consume the changeset**: `npm run version-packages` (auto-bumps `package.json` files, updates `CHANGELOG.md`, runs `npm install --package-lock-only`, and verifies lockfile sync via `scripts/check-lockfile-sync.mjs` — all in one command; never hand-edit `CHANGELOG.md` or `package-lock.json` versions)
|
3. **Consume the changeset**: `npm run version-packages` (auto-bumps `package.json` files, updates `CHANGELOG.md`, runs `npm install --package-lock-only`, and verifies lockfile sync via `scripts/check-lockfile-sync.mjs` — all in one command; never hand-edit `CHANGELOG.md` or `package-lock.json` versions)
|
||||||
4. **Sync CLAUDE.md version**: Update the `**Version**` line below to match the new version from `package.json`
|
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`
|
5. **Commit and deploy**: `git add -A && git commit -m "chore: version packages" && git push && npm run build && systemctl --user restart codeman-web`
|
||||||
|
6. **Wait for CI**: after `git push`, find the run with `gh run list -L 1 --json databaseId,headBranch -q '.[0].databaseId'` and watch it with `gh run watch <id> --exit-status`. Confirm all checks pass before considering the release done.
|
||||||
|
|
||||||
CI runs `npm run check:lockfile` on every push/PR, so lockfile drift fails the build even if the `version-packages` script is bypassed.
|
CI runs `npm run check:lockfile` on every push/PR, so lockfile drift fails the build even if the `version-packages` script is bypassed.
|
||||||
|
|
||||||
**Version**: 0.6.2 (must match `package.json`)
|
**Version**: 0.6.6 (must match `package.json`)
|
||||||
|
|
||||||
## Project Overview
|
## Project Overview
|
||||||
|
|
||||||
@@ -93,6 +94,8 @@ Codeman is a Claude Code session manager with web interface and autonomous Ralph
|
|||||||
- **Package ≠ product name** — npm: `aicodeman`, product: **Codeman**. Release renames tags accordingly
|
- **Package ≠ product name** — npm: `aicodeman`, product: **Codeman**. Release renames tags accordingly
|
||||||
- **Global regex `lastIndex`** — Shared `g`-flag patterns in loops must reset `lastIndex = 0` first, or use the `execPattern()` helper in `utils/regex-patterns.ts` (resets automatically)
|
- **Global regex `lastIndex`** — Shared `g`-flag patterns in loops must reset `lastIndex = 0` first, or use the `execPattern()` helper in `utils/regex-patterns.ts` (resets automatically)
|
||||||
- **`envOverrides` flow `CLAUDE_CODE_*` / `OPENCODE_*` env vars** — Set via `POST /api/sessions { envOverrides }`, stored on `Session._envOverrides`, exported by `tmux-manager.buildEnvExports()` at spawn time, persisted in `SessionState.envOverrides`. **Do NOT** write these to `<case>/.claude/settings.local.json` — that's the old path and creates UI/disk drift
|
- **`envOverrides` flow `CLAUDE_CODE_*` / `OPENCODE_*` env vars** — Set via `POST /api/sessions { envOverrides }`, stored on `Session._envOverrides`, exported by `tmux-manager.buildEnvExports()` at spawn time, persisted in `SessionState.envOverrides`. **Do NOT** write these to `<case>/.claude/settings.local.json` — that's the old path and creates UI/disk drift
|
||||||
|
- **Zod `.optional()` rejects `null`** — accepts `undefined` only. When the frontend builds a request body with `JSON.stringify`, an explicit `null` field is preserved on the wire and fails validation with `INVALID_INPUT`. Convert `null` → `undefined` before stringifying (e.g. `field: value ?? undefined`), or declare the schema `.nullish()`. Real bugs caused: 0.6.4 (`durationMinutes` for ∞ respawn), and the same shape pattern hit `opusContext1mEnabled` in 0.6.3
|
||||||
|
- **`xterm-zerolag-input` is duplicated** — the local-echo overlay lives in BOTH `packages/xterm-zerolag-input/src/` (published package) AND inline inside `src/web/public/app.js` (runtime copy used by the web UI). Any change to overlay behavior MUST be applied to both, or dev and prod diverge. Always test on mobile after touching it.
|
||||||
|
|
||||||
**Import conventions**: Utils from `./utils`, types from `./types` (barrel), config from specific `./config/*` files.
|
**Import conventions**: Utils from `./utils`, types from `./types` (barrel), config from specific `./config/*` files.
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
# Local Echo Overlay — Implementation Plan
|
# Local Echo Overlay — Implementation Plan
|
||||||
|
|
||||||
|
> **Status: SHIPPED.** Implementation lives in `packages/xterm-zerolag-input/src/` (overlay-renderer.ts, prompt-finder.ts, cell-dimensions.ts, zerolag-input-addon.ts) with the embedded copy in `src/web/public/app.js`. This document is retained as historical design context.
|
||||||
|
|
||||||
## Context
|
## Context
|
||||||
|
|
||||||
User accesses Codeman remotely from Thailand to Switzerland over Tailscale (~200-300ms RTT).
|
User accesses Codeman remotely from Thailand to Switzerland over Tailscale (~200-300ms RTT).
|
||||||
@@ -18,9 +20,9 @@ redraws. A DOM overlay sits in a separate rendering layer (z-index 7) and doesn'
|
|||||||
with Ink's cursor management or screen redraws at all. When Ink redraws (server output arrives),
|
with Ink's cursor management or screen redraws at all. When Ink redraws (server output arrives),
|
||||||
we simply hide the overlay.
|
we simply hide the overlay.
|
||||||
|
|
||||||
**Why it will look indistinguishable:** We use the DOM renderer (not canvas/WebGL) in our
|
**Why it will look indistinguishable:** We use the DOM renderer (not canvas/WebGL), so both
|
||||||
xterm.js v5.3.0, so both terminal text and overlay text are rendered by the same browser
|
terminal text and overlay text are rendered by the same browser font engine with identical
|
||||||
font engine with identical sub-pixel rendering.
|
sub-pixel rendering. (Originally designed against xterm.js v5.3.0; project now on `@xterm/xterm` ^6.0.0 — the internal `_core._renderService.dimensions` access path still works in v6.)
|
||||||
|
|
||||||
## Key Technical Details (from research)
|
## Key Technical Details (from research)
|
||||||
|
|
||||||
@@ -36,7 +38,7 @@ const top = cursorY * dims.css.cell.height; // CSS pixels, relative to .xterm-
|
|||||||
- `cursorY` = `terminal.buffer.active.cursorY` (0 to terminal.rows-1, ALREADY viewport-relative)
|
- `cursorY` = `terminal.buffer.active.cursorY` (0 to terminal.rows-1, ALREADY viewport-relative)
|
||||||
- No scroll offset math needed
|
- No scroll offset math needed
|
||||||
|
|
||||||
### Cell Dimensions (v5.3.0 — no public API, use internal)
|
### Cell Dimensions (no public API in v5/v6 — use internal; public in v7+)
|
||||||
```js
|
```js
|
||||||
const dims = terminal._core._renderService.dimensions;
|
const dims = terminal._core._renderService.dimensions;
|
||||||
dims.css.cell.width // e.g., 8.4px
|
dims.css.cell.width // e.g., 8.4px
|
||||||
|
|||||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "aicodeman",
|
"name": "aicodeman",
|
||||||
"version": "0.6.2",
|
"version": "0.6.6",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "aicodeman",
|
"name": "aicodeman",
|
||||||
"version": "0.6.2",
|
"version": "0.6.6",
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"workspaces": [
|
"workspaces": [
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "aicodeman",
|
"name": "aicodeman",
|
||||||
"version": "0.6.2",
|
"version": "0.6.6",
|
||||||
"description": "The missing control plane for AI coding agents - run 20 autonomous agents with real-time monitoring and session persistence",
|
"description": "The missing control plane for AI coding agents - run 20 autonomous agents with real-time monitoring and session persistence",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "dist/index.js",
|
"main": "dist/index.js",
|
||||||
|
|||||||
+20
-3
@@ -29,7 +29,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { EventEmitter } from 'node:events';
|
import { EventEmitter } from 'node:events';
|
||||||
import { execSync } from 'node:child_process';
|
import { execSync, execFileSync } from 'node:child_process';
|
||||||
import { v4 as uuidv4 } from 'uuid';
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
import * as pty from 'node-pty';
|
import * as pty from 'node-pty';
|
||||||
import {
|
import {
|
||||||
@@ -946,11 +946,28 @@ export class Session extends EventEmitter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Attach to the mux session via PTY
|
// Attach to the mux session via PTY
|
||||||
|
// Query existing tmux window size so re-attach matches (avoids flicker from 120x40 default)
|
||||||
|
let ptyCols = 120;
|
||||||
|
let ptyRows = 40;
|
||||||
|
try {
|
||||||
|
const sizeStr = execFileSync(
|
||||||
|
'tmux',
|
||||||
|
['display', '-t', this._muxSession!.muxName, '-p', '#{window_width} #{window_height}'],
|
||||||
|
{ timeout: 2000, encoding: 'utf8' }
|
||||||
|
).trim();
|
||||||
|
const [w, h] = sizeStr.split(' ').map(Number);
|
||||||
|
if (w > 0 && h > 0) {
|
||||||
|
ptyCols = w;
|
||||||
|
ptyRows = h;
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
/* fall back to 120x40 */
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
this.ptyProcess = pty.spawn(mux.getAttachCommand(), mux.getAttachArgs(this._muxSession!.muxName), {
|
this.ptyProcess = pty.spawn(mux.getAttachCommand(), mux.getAttachArgs(this._muxSession!.muxName), {
|
||||||
name: 'xterm-256color',
|
name: 'xterm-256color',
|
||||||
cols: 120,
|
cols: ptyCols,
|
||||||
rows: 40,
|
rows: ptyRows,
|
||||||
cwd: this.workingDir,
|
cwd: this.workingDir,
|
||||||
env: buildMuxAttachEnv(),
|
env: buildMuxAttachEnv(),
|
||||||
});
|
});
|
||||||
|
|||||||
+8
-1
@@ -556,7 +556,7 @@ export class TmuxManager extends EventEmitter implements TerminalMultiplexer {
|
|||||||
// (Production uses systemd which has a clean env, but dev/test may be nested.)
|
// (Production uses systemd which has a clean env, but dev/test may be nested.)
|
||||||
const cleanEnv = { ...process.env };
|
const cleanEnv = { ...process.env };
|
||||||
delete cleanEnv.TMUX;
|
delete cleanEnv.TMUX;
|
||||||
execSync(`tmux new-session -ds "${muxName}" -c "${workingDir}" -x 120 -y 40`, {
|
execSync(`tmux new-session -ds "${muxName}" -c "${workingDir}"`, {
|
||||||
cwd: workingDir,
|
cwd: workingDir,
|
||||||
timeout: EXEC_TIMEOUT_MS,
|
timeout: EXEC_TIMEOUT_MS,
|
||||||
stdio: 'ignore',
|
stdio: 'ignore',
|
||||||
@@ -609,6 +609,13 @@ export class TmuxManager extends EventEmitter implements TerminalMultiplexer {
|
|||||||
.catch(() => {
|
.catch(() => {
|
||||||
/* Already set globally as fallback */
|
/* Already set globally as fallback */
|
||||||
}),
|
}),
|
||||||
|
// Raise tmux scrollback from its 2000-line default so re-attach preserves
|
||||||
|
// more context. Matches the xterm-side default in constants.js.
|
||||||
|
execAsync(`tmux set-option -t "${muxName}" history-limit 50000`, { timeout: EXEC_TIMEOUT_MS })
|
||||||
|
.then(() => {})
|
||||||
|
.catch(() => {
|
||||||
|
/* Non-critical — falls back to tmux default */
|
||||||
|
}),
|
||||||
];
|
];
|
||||||
|
|
||||||
// Enable 24-bit true color passthrough — server-wide, set once per lifetime
|
// Enable 24-bit true color passthrough — server-wide, set once per lifetime
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ function urlBase64ToUint8Array(base64String) {
|
|||||||
// ═══════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
// Default terminal scrollback (can be changed via settings)
|
// Default terminal scrollback (can be changed via settings)
|
||||||
const DEFAULT_SCROLLBACK = 20000;
|
const DEFAULT_SCROLLBACK = 50000;
|
||||||
|
|
||||||
// Timing constants
|
// Timing constants
|
||||||
const STUCK_THRESHOLD_DEFAULT_MS = 600000; // 10 minutes - default for stuck detection
|
const STUCK_THRESHOLD_DEFAULT_MS = 600000; // 10 minutes - default for stuck detection
|
||||||
|
|||||||
@@ -1575,7 +1575,7 @@ Object.assign(CodemanApp.prototype, {
|
|||||||
lineHeight: 1.2,
|
lineHeight: 1.2,
|
||||||
cursorBlink: true,
|
cursorBlink: true,
|
||||||
cursorStyle: 'block',
|
cursorStyle: 'block',
|
||||||
scrollback: 5000,
|
scrollback: DEFAULT_SCROLLBACK,
|
||||||
allowTransparency: true,
|
allowTransparency: true,
|
||||||
allowProposedApi: true,
|
allowProposedApi: true,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -792,7 +792,7 @@ Object.assign(CodemanApp.prototype, {
|
|||||||
const res = await fetch(`/api/sessions/${this.editingSessionId}/respawn/enable`, {
|
const res = await fetch(`/api/sessions/${this.editingSessionId}/respawn/enable`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({ config: respawnConfig, durationMinutes })
|
body: JSON.stringify({ config: respawnConfig, durationMinutes: durationMinutes ?? undefined })
|
||||||
});
|
});
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
if (data.error) throw new Error(data.error);
|
if (data.error) throw new Error(data.error);
|
||||||
|
|||||||
@@ -18,8 +18,10 @@ Object.assign(CodemanApp.prototype, {
|
|||||||
// ═══════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
initTerminal() {
|
initTerminal() {
|
||||||
// Load scrollback setting from localStorage (default 5000)
|
// Load scrollback setting from localStorage, treating DEFAULT_SCROLLBACK as a floor
|
||||||
const scrollback = parseInt(localStorage.getItem('codeman-scrollback')) || DEFAULT_SCROLLBACK;
|
// so users who picked up the previous (smaller) default get the new minimum on upgrade.
|
||||||
|
const stored = parseInt(localStorage.getItem('codeman-scrollback'));
|
||||||
|
const scrollback = Number.isFinite(stored) && stored > 0 ? Math.max(stored, DEFAULT_SCROLLBACK) : DEFAULT_SCROLLBACK;
|
||||||
|
|
||||||
this.terminal = new Terminal({
|
this.terminal = new Terminal({
|
||||||
theme: {
|
theme: {
|
||||||
@@ -156,7 +158,7 @@ Object.assign(CodemanApp.prototype, {
|
|||||||
Promise.resolve().then(() => {
|
Promise.resolve().then(() => {
|
||||||
// If xterm cleared the textarea, it processed the input -- skip.
|
// If xterm cleared the textarea, it processed the input -- skip.
|
||||||
const val = xtermTextarea.value;
|
const val = xtermTextarea.value;
|
||||||
if (!val || val.trim() === '') return;
|
if (!val || (val.trim() === '' && data !== ' ')) return;
|
||||||
// xterm didn't process it -- forward to terminal as if typed.
|
// xterm didn't process it -- forward to terminal as if typed.
|
||||||
// Emit via onData path by writing to terminal's input handler.
|
// Emit via onData path by writing to terminal's input handler.
|
||||||
this.terminal._core.coreService.triggerDataEvent(data, true);
|
this.terminal._core.coreService.triggerDataEvent(data, true);
|
||||||
|
|||||||
@@ -268,6 +268,7 @@ export const SettingsUpdateSchema = z
|
|||||||
tunnelEnabled: z.boolean().optional(),
|
tunnelEnabled: z.boolean().optional(),
|
||||||
tabTwoRows: z.boolean().optional(),
|
tabTwoRows: z.boolean().optional(),
|
||||||
agentTeamsEnabled: z.boolean().optional(),
|
agentTeamsEnabled: z.boolean().optional(),
|
||||||
|
opusContext1mEnabled: z.boolean().optional(),
|
||||||
thinkingEffort: z.string().max(20).optional(),
|
thinkingEffort: z.string().max(20).optional(),
|
||||||
// UI visibility
|
// UI visibility
|
||||||
showFontControls: z.boolean().optional(),
|
showFontControls: z.boolean().optional(),
|
||||||
|
|||||||
Reference in new issue
Block a user