fix: local echo overlay lost on mobile redraws — preserve pending text across SSE reconnect, terminal resets, and Ink redraws

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
arkon
2026-02-19 14:52:39 +01:00
co-authored by Claude Opus 4.6
parent fe1f665531
commit 8b96470cce
3 changed files with 41 additions and 8 deletions
+10 -5
View File
@@ -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.1551 (must match `package.json` for npm publish)
**Version**: 0.1552 (must match `package.json` for npm publish)
## Project Overview
@@ -124,13 +124,13 @@ journalctl --user -u claudeman-web -f
| `src/file-stream-manager.ts` | Manages `tail -f` processes for live log viewing |
| `src/plan-orchestrator.ts` | Multi-agent plan generation with research and planning phases |
| `src/prompts/index.ts` | Barrel export for all agent prompts |
| `src/prompts/*.ts` | Agent prompts (research-agent, code-reviewer, planner) |
| `src/prompts/*.ts` | Agent prompts (research-agent, planner) |
| `src/templates/claude-md.ts` | CLAUDE.md generation for new cases |
| `src/cli.ts` | Command-line interface handlers |
| `src/web/server.ts` | Fastify REST API + SSE at `/api/events` (~90 routes) |
| `src/web/server.ts` | Fastify REST API + SSE at `/api/events` (~99 routes) |
| `src/web/schemas.ts` | Zod v4 validation schemas with path/env security allowlists |
| `src/web/public/app.js` | Frontend: xterm.js, tab management, subagent windows, mobile support (~16K lines) |
| `src/types.ts` | All TypeScript interfaces (~100 types, ~1500 lines) |
| `src/types.ts` | All TypeScript interfaces (~100 types, ~1400 lines) |
**Large files** (>50KB): `app.js`, `ralph-tracker.ts`, `respawn-controller.ts`, `session.ts`, `subagent-watcher.ts` — these contain complex state machines; read `docs/respawn-state-machine.md` before modifying.
@@ -232,7 +232,7 @@ The frontend is a single 16K-line vanilla JS file with these key systems:
### API Route Categories
~90 routes in `server.ts:buildServer()`. Key groups:
~99 routes in `server.ts:buildServer()`. Key groups:
| Group | Prefix | Count | Key endpoints |
|-------|--------|-------|---------------|
@@ -295,6 +295,8 @@ npx vitest run -t "pattern"
**Respawn tests**: Use MockSession from `test/respawn-test-utils.ts` to avoid spawning real Claude processes.
**Mobile tests**: Separate Playwright-based suite in `mobile-test/` with 135 device profiles. Run via `npx vitest run --config mobile-test/vitest.config.ts`. See `mobile-test/README.md`.
## Screenshots ("sc")
When the user says "check the sc", "screenshot", or "sc", they mean uploaded screenshots from their mobile device. Screenshots are saved to `~/.claudeman/screenshots/` and uploaded via `/upload.html` on the Claudeman web UI. To view them, use the Read tool on the image files:
@@ -402,6 +404,9 @@ Use `LRUMap` for bounded caches with eviction, `StaleExpirationMap` for TTL-base
| **Mobile testing** | `docs/mobile-testing-report.md` |
| **Run summary design** | `docs/run-summary-plan.md` |
| **Performance audit** | `docs/perf-audit-first-load.md` |
| **First-load optimization** | `docs/first-load-optimization-plan.md` |
| **Dead code audit** | `docs/cleanup-findings.md` |
| **Mobile test suite** | `mobile-test/README.md` |
## Scripts
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "claudeman",
"version": "0.1551",
"version": "0.1552",
"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",
+30 -2
View File
@@ -970,6 +970,12 @@ class LocalEchoOverlay {
}
}
// Re-render overlay at current prompt position without clearing pending text.
// Used after terminal resets/redraws that move the prompt.
rerender() {
if (this.pendingText) this._render();
}
get hasPending() { return this.pendingText.length > 0; }
dispose() {
@@ -2337,6 +2343,12 @@ class ClaudemanApp {
if (this._wasAtBottomBeforeWrite) {
this.terminal.scrollToBottom();
}
// Re-position local echo overlay after terminal writes — Ink redraws can
// move the ❯ prompt to a different row, making the overlay invisible.
if (this._localEchoOverlay?.hasPending) {
this._localEchoOverlay.rerender();
}
}
/**
@@ -2646,6 +2658,8 @@ class ClaudemanApp {
this.terminal.reset();
await this.chunkedTerminalWrite(data.terminalBuffer);
this.terminal.scrollToBottom();
// Re-position local echo overlay at new prompt location
this._localEchoOverlay?.rerender();
}
} catch (err) {
console.error('needsRefresh reload failed:', err);
@@ -2672,6 +2686,8 @@ class ClaudemanApp {
// Send resize to ensure proper dimensions
await this.sendResize(data.id);
// Re-position local echo overlay at new prompt location
this._localEchoOverlay?.rerender();
} catch (err) {
console.error('clearTerminal refresh failed:', err);
}
@@ -3717,8 +3733,10 @@ class ClaudemanApp {
this.writeFrameScheduled = false;
this._isLoadingBuffer = false;
this._loadBufferQueue = null;
// Clear local echo overlay on SSE reconnect (re-enable via _updateLocalEchoState)
this._localEchoOverlay?.clear();
// Preserve local echo overlay text across SSE reconnect — just hide until
// terminal buffer reloads and prompt is visible again. _render() re-scans
// for the ❯ prompt on every call, so rerender() after buffer load repositions it.
this._localEchoOverlay?.rerender();
// Clear pending hooks
this.pendingHooks.clear();
// Clear subagent activity/results maps (prevents leaks if data.subagents is missing)
@@ -3864,6 +3882,8 @@ class ClaudemanApp {
// Guard: skip if a newer handleInit has already started (race between loadState + SSE init).
if (gen !== this._initGeneration) return;
const previousActiveId = this.activeSessionId;
// Save local echo pending text before selectSession clears it
const savedEchoText = this._localEchoOverlay?.pendingText || '';
this.activeSessionId = null;
if (this.sessionOrder.length > 0) {
// Priority: current active > localStorage > first session
@@ -3877,6 +3897,14 @@ class ClaudemanApp {
this.selectSession(this.sessionOrder[0]);
}
}
// Restore local echo text that was cleared by selectSession during SSE reconnect.
// selectSession loads the terminal buffer, so we schedule rerender after it settles.
if (savedEchoText && this._localEchoOverlay) {
this._localEchoOverlay.pendingText = savedEchoText;
this._localEchoOverlay._persist();
// Delay rerender until buffer load completes and prompt is visible
setTimeout(() => this._localEchoOverlay?.rerender(), 500);
}
}
async loadState() {