mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-03 22:19:42 +02:00
chore: version packages
Release 1.16.6: phone overview started/idle stamps, plus fixes for the selection-dialog keyboard lockout, the accessory bar arrows bypassing the local-echo overlay, and recovered sessions being restamped as newly created on every server restart. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -211,6 +211,60 @@ describe('mobile overview model', () => {
|
||||
expect(empty).toMatchObject({ needsYou: [], current: [], past: [], sessionCount: 0 });
|
||||
});
|
||||
|
||||
it('anchors the "how long" stamp on last activity, and on the last Enter while working', () => {
|
||||
const app = loadOverviewApp();
|
||||
const now = Date.now();
|
||||
const model = app.buildMobileOverviewModel({
|
||||
sessions: [
|
||||
// A working pane repaints about once a second, so lastActivityAt is
|
||||
// always "now" and would report every running turn as 0m. The turn's
|
||||
// own start is the last Enter.
|
||||
session({
|
||||
id: 'w',
|
||||
status: 'busy',
|
||||
createdAt: now - 7200_000,
|
||||
lastActivityAt: now,
|
||||
lastSubmitAt: now - 300_000,
|
||||
}),
|
||||
// A quiet pane prints nothing, so its last byte IS when it went idle.
|
||||
session({ id: 'i', status: 'idle', createdAt: now - 7200_000, lastActivityAt: now - 900_000 }),
|
||||
],
|
||||
cases: CASES,
|
||||
});
|
||||
|
||||
const rows = Object.fromEntries(model.current.map((r: any) => [r.id, r]));
|
||||
expect(rows.w.since).toEqual({ key: 'working', at: now - 300_000 });
|
||||
expect(rows.i.since).toEqual({ key: 'idle', at: now - 900_000 });
|
||||
expect(rows.i.createdAt).toBe(now - 7200_000);
|
||||
});
|
||||
|
||||
it('leaves the stamp off rather than inventing an anchor', () => {
|
||||
const app = loadOverviewApp();
|
||||
const model = app.buildMobileOverviewModel({
|
||||
// A session that has never submitted has no turn start to measure from.
|
||||
sessions: [session({ id: 'w', status: 'busy', lastActivityAt: Date.now() })],
|
||||
cases: CASES,
|
||||
});
|
||||
expect(model.current[0].since).toBeNull();
|
||||
expect(model.current[0].createdAt).toBe(0);
|
||||
});
|
||||
|
||||
it('formats a moment as "ago" and a span as a bare duration', () => {
|
||||
const app = loadOverviewApp();
|
||||
app.formatRelativeTime = () => '3d ago';
|
||||
const now = Date.now();
|
||||
|
||||
expect(app._mobileOverviewStampText(now - 86_400_000, 'ago')).toBe('3d ago');
|
||||
expect(app._mobileOverviewStampText(now - 20_000, 'for')).toBe('<1m');
|
||||
expect(app._mobileOverviewStampText(now - 12 * 60_000, 'for')).toBe('12m');
|
||||
expect(app._mobileOverviewStampText(now - 125 * 60_000, 'for')).toBe('2h 5m');
|
||||
expect(app._mobileOverviewStampText(now - 3 * 3600_000, 'for')).toBe('3h');
|
||||
expect(app._mobileOverviewStampText(now - 50 * 3600_000, 'for')).toBe('2d 2h');
|
||||
// No anchor renders as a dash, never as "56 years ago" off epoch 0.
|
||||
expect(app._mobileOverviewStampText(0, 'for')).toBe('—');
|
||||
expect(app._mobileOverviewStampText(0, 'ago')).toBe('—');
|
||||
});
|
||||
|
||||
it('no longer builds a spaces section', () => {
|
||||
const app = loadOverviewApp();
|
||||
const model = app.buildMobileOverviewModel({ sessions: [session({ id: 'a' })], cases: CASES });
|
||||
|
||||
@@ -573,3 +573,70 @@ describe('armed styling survives the light-skin overrides', () => {
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('composer nav keys from the bar', () => {
|
||||
/** loadBar()'s app stub plus the local-echo state the flush path reads. */
|
||||
function barWithDraft(draft: string) {
|
||||
const loaded = loadBar('claude');
|
||||
const overlay = {
|
||||
pendingText: draft,
|
||||
clear: vi.fn(() => {
|
||||
overlay.pendingText = '';
|
||||
}),
|
||||
suppressBufferDetection: vi.fn(),
|
||||
};
|
||||
const app = loaded.app as unknown as Record<string, unknown>;
|
||||
app._localEchoEnabled = true;
|
||||
app._localEchoOverlay = overlay;
|
||||
app.sendInput = vi.fn();
|
||||
app._flushedOffsets = new Map([['session-1', 3]]);
|
||||
app._flushedTexts = new Map([['session-1', 'dra']]);
|
||||
return { ...loaded, app, overlay };
|
||||
}
|
||||
|
||||
const sentKeys = (fetchMock: { mock: { calls: unknown[][] } }) =>
|
||||
fetchMock.mock.calls.map((call) => JSON.parse((call[1] as { body: string }).body).input);
|
||||
|
||||
it('flushes the unsent draft before sending the arrow', () => {
|
||||
// On a phone the typed text lives in the overlay and has NEVER reached the
|
||||
// PTY, so an arrow sent on its own arrives at a composer the CLI still
|
||||
// considers empty: Up recalls a history entry into it while the overlay
|
||||
// goes on painting the draft over the same row and still believes it is
|
||||
// pending. Flushing first is also what makes the draft recoverable: the
|
||||
// CLI stashes the live composer and hands it back on Down.
|
||||
const { bar, app, overlay, fetchMock } = barWithDraft('draft I typed');
|
||||
|
||||
bar.handleAction('scroll-up');
|
||||
|
||||
expect(app.sendInput).toHaveBeenCalledWith('draft I typed');
|
||||
expect(sentKeys(fetchMock)).toEqual(['\x1b[A']);
|
||||
expect(overlay.pendingText).toBe('');
|
||||
expect(overlay.suppressBufferDetection).toHaveBeenCalled();
|
||||
// The overlay's bookkeeping for this session has to go with it.
|
||||
expect((app._flushedOffsets as Map<string, number>).has('session-1')).toBe(false);
|
||||
expect((app._flushedTexts as Map<string, string>).has('session-1')).toBe(false);
|
||||
});
|
||||
|
||||
it('hands the session to plain PTY echo, like a typed nav key does', () => {
|
||||
// After a nav key the real cursor can sit mid-text, where the overlay's
|
||||
// append-only buffering cannot track edits (issue #218). terminal-ui.js's
|
||||
// onData branch does exactly this for a nav key typed on a keyboard.
|
||||
const { bar, app } = barWithDraft('');
|
||||
|
||||
bar.handleAction('arrow-left');
|
||||
|
||||
expect([...(app._echoPassthroughSessions as Set<string>)]).toEqual(['session-1']);
|
||||
});
|
||||
|
||||
it('sends all four arrows and skips the flush when there is no draft', () => {
|
||||
const { bar, app, fetchMock } = barWithDraft('');
|
||||
|
||||
bar.handleAction('scroll-up');
|
||||
bar.handleAction('scroll-down');
|
||||
bar.handleAction('arrow-left');
|
||||
bar.handleAction('arrow-right');
|
||||
|
||||
expect(sentKeys(fetchMock)).toEqual(['\x1b[A', '\x1b[B', '\x1b[D', '\x1b[C']);
|
||||
expect(app.sendInput).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -166,6 +166,37 @@ describe('terminal touch tap mouse guard', () => {
|
||||
expect(app._classifyMobileTerminalTap(9, 49)).toBe('content');
|
||||
});
|
||||
|
||||
it('keeps the keyboard reachable while a selection dialog is on screen', () => {
|
||||
// The lock this pins: a visible dialog used to make EVERY row of the
|
||||
// terminal "actionable" (both menu tests scanned the whole viewport), so
|
||||
// every tap blurred and the on-screen keyboard could not be opened until
|
||||
// the dialog was answered, leaving tapping an option (the one gesture that
|
||||
// commits an answer) as the only thing a phone could do.
|
||||
const { app, setActiveElement } = loadTerminalUiHarness();
|
||||
app.activeSessionId = 'sess-1';
|
||||
app.sessions = new Map([['sess-1', { mode: 'claude' }]]);
|
||||
app.terminal = createTerminalGrid(
|
||||
['Do you want to proceed?', '', '❯ 1. Yes', ' 2. No, tell Claude what to do', '', ''],
|
||||
2
|
||||
);
|
||||
app._sendInputAsync = vi.fn();
|
||||
setActiveElement(null);
|
||||
|
||||
// The dialog's own rows stay TUI-owned: report the tap, keep the keyboard down.
|
||||
expect(app._isActionableMobileTerminalTap(9, 33)).toBe(true); // ❯ 1. Yes
|
||||
expect(app._isActionableMobileTerminalTap(9, 49)).toBe(true); // 2. No, …
|
||||
// Everything else is inert, and must still be able to summon the keyboard.
|
||||
expect(app._isActionableMobileTerminalTap(9, 1)).toBe(false); // question title
|
||||
expect(app._isActionableMobileTerminalTap(9, 65)).toBe(false); // blank row
|
||||
|
||||
app._handleMobileTerminalTap({ clientX: 9, clientY: 1 }, false);
|
||||
expect(app.terminal.focus).toHaveBeenCalledOnce();
|
||||
|
||||
app.terminal.focus.mockClear();
|
||||
app._handleMobileTerminalTap({ clientX: 9, clientY: 33 }, false);
|
||||
expect(app.terminal.focus).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('collapses TUI readback content without opening or retaining the keyboard', () => {
|
||||
const { app, setActiveElement } = loadTerminalUiHarness();
|
||||
app.activeSessionId = 'sess-1';
|
||||
|
||||
Reference in New Issue
Block a user