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
5 changes: 5 additions & 0 deletions .changeset/restore-session-settings.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'aicodeman': patch
---

A session's tab colour, pin and image watcher now survive a Codeman restart or crash. Recovery rebuilt each surviving pane with its name, auto-clear, auto-compact and auto-resume settings but not those three, so they reset to default and the next save overwrote the stored values.
2 changes: 1 addition & 1 deletion src/web/public/gesture/gesture-codeman.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ var __defProp = Object.defineProperty;
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);

// node_modules/@mediapipe/tasks-vision/vision_bundle.mjs
// ../wt-cli/node_modules/@mediapipe/tasks-vision/vision_bundle.mjs
var t = "undefined" != typeof self ? self : {};
function e() {
throw Error("Invalid UTF8");
Expand Down
11 changes: 11 additions & 0 deletions src/web/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3682,6 +3682,17 @@ export class WebServer extends EventEmitter {
if (savedState.flickerFilterEnabled !== undefined) {
session.flickerFilterEnabled = savedState.flickerFilterEnabled;
}
// Tab colour, pin and the image watcher are user-set and persisted like the settings
// above, but this path never read them back: after a restart every surviving pane came
// back uncoloured and unpinned with its image watcher off, and the next persist
// overwrote the saved values. `reapplyPersistedSessionState()` (the reboot-restore
// path) already restores all three. The watcher flag has to be set before
// `setupSessionListeners()` below, which reads it to decide whether to start one.
if (savedState.color) session.setColor(savedState.color);
if (savedState.pinned) session.restorePin(true, savedState.pinnedAt);
if (savedState.imageWatcherEnabled !== undefined) {
session.imageWatcherEnabled = savedState.imageWatcherEnabled;
}
// Respawn controller (not supported for external-CLI sessions)
if (!isExternalCliMode(session.mode) && savedState.respawnEnabled && savedState.respawnConfig) {
try {
Expand Down
86 changes: 86 additions & 0 deletions test/session-settings-recovery.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
/**
* @fileoverview The settings a user puts on a session survive a Codeman restart.
*
* `restoreMuxSessions()` (server.ts) rebuilds a Session around a surviving tmux pane from
* `state.json`, and `reapplyPersistedSessionState()` does the same for a reboot restore. A
* setting one of them forgets is silently reset on the next persist, which writes `toState()`
* wholesale. Found live: after a restart a session came back with its name, auto-clear and
* auto-compact but with its tab colour, pin and image watcher gone.
*
* `restoreMuxSessions()` cannot be reached under vitest (see test/session-model-recovery.test.ts),
* so the wiring is pinned by a source check and the setters it relies on are driven for real.
*/
import { readFileSync } from 'node:fs';
import { join } from 'node:path';
import { fileURLToPath } from 'node:url';
import { describe, expect, it } from 'vitest';
import { Session } from '../src/session.js';

const SRC = fileURLToPath(new URL('../src', import.meta.url));
const server = readFileSync(join(SRC, 'web', 'server.ts'), 'utf-8');

function functionBody(name: string): string {
const start = server.indexOf(`async ${name}(`);
expect(start, `${name} not found`).toBeGreaterThan(-1);
const next = server.indexOf('\n /**', start);
return server.slice(start, next === -1 ? undefined : next);
}

describe('restoreMuxSessions() hands back every persisted user setting', () => {
const body = functionBody('restoreMuxSessions');

it.each([
['name', /name:\s*sessionName/],
['name source', /nameSource:\s*savedState\?\.nameSource/],
['colour', /session\.setColor\(savedState\.color\)/],
['pin', /session\.restorePin\(true,\s*savedState\.pinnedAt\)/],
['image watcher', /session\.imageWatcherEnabled\s*=\s*savedState\.imageWatcherEnabled/],
['flicker filter', /session\.flickerFilterEnabled\s*=\s*savedState\.flickerFilterEnabled/],
['auto-clear', /session\.setAutoClear\(/],
['auto-compact', /session\.setAutoCompact\(/],
['auto-resume', /session\.restoreAutoResume\(/],
['nice priority', /session\.setNice\(/],
])('%s', (_label, pattern) => {
expect(body).toMatch(pattern);
});

it('sets the image-watcher flag before the listeners read it', () => {
expect(body.indexOf('session.imageWatcherEnabled = savedState.imageWatcherEnabled')).toBeLessThan(
body.indexOf('await this.setupSessionListeners(session)')
);
});

it('restores everything the reboot-restore path restores', () => {
const reapply = functionBody('reapplyPersistedSessionState');
// Each persisted setting that path re-applies must also be re-applied here.
for (const call of ['setColor(', 'restorePin(', 'imageWatcherEnabled =', 'flickerFilterEnabled =']) {
expect(reapply, `reapply lacks ${call}`).toContain(call);
expect(body, `restoreMuxSessions lacks ${call}`).toContain(call);
}
});
});

describe('the setters round-trip through toState()', () => {
it('keeps colour, pin, image watcher and name source', () => {
const original = new Session({ workingDir: '/tmp', mode: 'shell', name: 'custom name', nameSource: 'manual' });
original.setColor('red');
original.setPinned(true);
original.imageWatcherEnabled = true;
const state = original.toState();

const rebuilt = new Session({ workingDir: '/tmp', mode: 'shell', name: state.name, nameSource: state.nameSource });
rebuilt.setColor(state.color!);
rebuilt.restorePin(true, state.pinnedAt);
rebuilt.imageWatcherEnabled = state.imageWatcherEnabled!;
const after = rebuilt.toState();

expect(after).toMatchObject({
name: 'custom name',
nameSource: 'manual',
color: 'red',
pinned: true,
pinnedAt: state.pinnedAt,
imageWatcherEnabled: true,
});
});
});
Loading