mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-09-30 20:49:41 +02:00
- buildSplitPickerSessions() now excludes any session with pid === null (exited CLI, tripped PTY-exit breaker, a restore that never re-attached). Pane B has no equivalent of selectSession()'s auto re-attach POST, so a split opened onto one had nothing reading its tmux pane: no terminal events ever arrived and Session.write() silently dropped every keystroke with no ack either way, while the socket itself reported healthy. - Fixed the hollow chord regression test: the synthetic keydowns carried no keyCode, which is what xterm's evaluateKeyboardEvent switches on to produce a data frame at all, so the assertion held regardless of whether the gate fired. Adding real keyCodes surfaced a second, real bug in the Alt+B case: the event bubbles to app.js's own document-level shortcut dispatcher, which really toggles the sidebar and resets the layout attribute the gate reads before Pane B's own (later, non-capture) handler ever sees it — fixed by driving the app's real settings cache instead of only the DOM attribute. - Ported the two remaining primary-pane gates with real consequences: Ctrl+Z (SIGTSTP) is swallowed for every non-shell session, matching terminal-ui.js's reasoning (an Ink/TUI agent loop stops dead with no visible output otherwise), and Shift/Ctrl+Enter now POSTs to /api/sessions/:id/send-key for THIS pane's own session instead of letting xterm send a bare \r, which used to submit an incomplete prompt instead of inserting a newline. Smart-copy Ctrl+C is re-implemented against Pane B's own terminal (copying app.copyTerminalSelection() would have copied Pane A's selection instead). - Updated docs/architecture-invariants.md and docs/split-pane-sessions-plan.md to match, and added CLAUDE.md's missing .split-picker-menu z-index entry. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
89 lines
3.4 KiB
TypeScript
89 lines
3.4 KiB
TypeScript
import { readFileSync } from 'node:fs';
|
|
import { resolve } from 'node:path';
|
|
import vm from 'node:vm';
|
|
import { describe, expect, it } from 'vitest';
|
|
|
|
function loadSplitPaneHelper() {
|
|
const context = vm.createContext({ window: {}, globalThis: {} });
|
|
const source = readFileSync(resolve(import.meta.dirname, '../src/web/public/constants.js'), 'utf8');
|
|
vm.runInContext(source, context, { filename: 'constants.js' });
|
|
return (context.window as { CodemanSplitPane: any }).CodemanSplitPane;
|
|
}
|
|
|
|
describe('CodemanSplitPane.clampDividerPercent', () => {
|
|
it('passes through a value inside the clamp range', () => {
|
|
const { clampDividerPercent } = loadSplitPaneHelper();
|
|
expect(clampDividerPercent(50)).toBe(50);
|
|
expect(clampDividerPercent(35.5)).toBe(35.5);
|
|
});
|
|
|
|
it('clamps below the floor to the floor', () => {
|
|
const { clampDividerPercent } = loadSplitPaneHelper();
|
|
expect(clampDividerPercent(5)).toBe(20);
|
|
});
|
|
|
|
it('clamps above the ceiling to the ceiling', () => {
|
|
const { clampDividerPercent } = loadSplitPaneHelper();
|
|
expect(clampDividerPercent(95)).toBe(80);
|
|
});
|
|
|
|
it('honors custom min/max', () => {
|
|
const { clampDividerPercent } = loadSplitPaneHelper();
|
|
expect(clampDividerPercent(10, 15, 85)).toBe(15);
|
|
expect(clampDividerPercent(90, 15, 85)).toBe(85);
|
|
});
|
|
});
|
|
|
|
describe('CodemanSplitPane.buildSplitPickerSessions', () => {
|
|
it('excludes the active session and preserves tab order', () => {
|
|
const { buildSplitPickerSessions } = loadSplitPaneHelper();
|
|
const sessions = new Map([
|
|
['a', { name: 'w1-codeman' }],
|
|
['b', { name: 'w1-mcp-memory' }],
|
|
['c', { name: null }],
|
|
]);
|
|
const sessionOrder = ['a', 'b', 'c'];
|
|
const result = buildSplitPickerSessions(sessions, sessionOrder, 'a');
|
|
expect(result).toEqual([
|
|
{ id: 'b', label: 'w1-mcp-memory' },
|
|
{ id: 'c', label: 'Session' },
|
|
]);
|
|
});
|
|
|
|
it('drops order entries with no matching session (stale ids)', () => {
|
|
const { buildSplitPickerSessions } = loadSplitPaneHelper();
|
|
const sessions = new Map([['a', { name: 'w1-codeman' }]]);
|
|
const sessionOrder = ['a', 'ghost'];
|
|
const result = buildSplitPickerSessions(sessions, sessionOrder, null);
|
|
expect(result).toEqual([{ id: 'a', label: 'w1-codeman' }]);
|
|
});
|
|
|
|
it('returns an empty list when only the excluded session exists', () => {
|
|
const { buildSplitPickerSessions } = loadSplitPaneHelper();
|
|
const sessions = new Map([['a', { name: 'w1-codeman' }]]);
|
|
const result = buildSplitPickerSessions(sessions, ['a'], 'a');
|
|
expect(result).toEqual([]);
|
|
});
|
|
|
|
it('excludes a session with no PTY attached (pid === null)', () => {
|
|
const { buildSplitPickerSessions } = loadSplitPaneHelper();
|
|
const sessions = new Map([
|
|
['a', { name: 'w1-codeman' }],
|
|
['b', { name: 'w2-exited', pid: null }],
|
|
['c', { name: 'w3-alive', pid: 12345 }],
|
|
]);
|
|
const result = buildSplitPickerSessions(sessions, ['a', 'b', 'c'], 'a');
|
|
expect(result).toEqual([{ id: 'c', label: 'w3-alive' }]);
|
|
});
|
|
|
|
it('excludes a detached session even when it also has no PTY', () => {
|
|
const { buildSplitPickerSessions } = loadSplitPaneHelper();
|
|
const sessions = new Map([
|
|
['a', { name: 'w1-codeman' }],
|
|
['b', { name: 'w2-detached', pid: null }],
|
|
]);
|
|
const result = buildSplitPickerSessions(sessions, ['a', 'b'], 'a', new Set(['b']));
|
|
expect(result).toEqual([]);
|
|
});
|
|
});
|