From 7ab5cc0e1232faae32562143be3fe5adf18b11bf Mon Sep 17 00:00:00 2001 From: Eivind Date: Thu, 16 Apr 2026 15:31:09 +0200 Subject: [PATCH 1/3] fix(timestamps): show every line and include millis in default format Timestamp gutter was deduplicating lines with the same formatted time, causing gaps and inconsistent widths. Now every logical line gets a timestamp. Default format changed from HH:mm:ss to HH:mm:ss.SSS for fixed-width output and better precision. Co-Authored-By: Claude Opus 4.6 (1M context) --- CLAUDE.md | 2 +- src/cli-flags.ts | 2 +- src/types.ts | 2 +- src/ui/pane.test.ts | 48 ++++++++++++++++++++++++++++++++++++++++++ src/ui/pane.ts | 13 ++++++------ src/utils/timestamp.ts | 2 +- 6 files changed, 58 insertions(+), 11 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index ad9d043..dd6158c 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -37,7 +37,7 @@ src/ - Panes are **readonly by default** — keyboard input is not forwarded to processes - Arrow keys (Up/Down) navigate between tabs, PageUp/PageDown scroll by page, Home/End to top/bottom - Mouse drag selects text and auto-copies to clipboard (OSC 52); `Y` key also copies selection -- `T` toggles an `HH:MM:SS` timestamp gutter in TUI mode; also enabled via `timestamps: true` config or `--timestamps` flag. Accepts a format string (e.g. `timestamps: "HH:mm:ss.SSS"`) with tokens: `YYYY`, `MM`, `DD`, `HH`, `hh`, `mm`, `ss`, `SSS`, `A` +- `T` toggles an `HH:mm:ss.SSS` timestamp gutter in TUI mode; also enabled via `timestamps: true` config or `--timestamps` flag. Accepts a format string (e.g. `timestamps: "HH:mm:ss"`) with tokens: `YYYY`, `MM`, `DD`, `HH`, `hh`, `mm`, `ss`, `SSS`, `A` - Keybinding hints are shown in the status bar; config lives in `src/ui/keybindings.ts` - Set `interactive: true` on processes that need stdin (REPLs, shells) - Non-interactive panes hide the terminal cursor diff --git a/src/cli-flags.ts b/src/cli-flags.ts index 8c06f0b..dde9de4 100644 --- a/src/cli-flags.ts +++ b/src/cli-flags.ts @@ -189,7 +189,7 @@ export const FLAGS: FlagDef[] = [ long: '--timestamps', short: '-t', key: 'timestamps', - description: 'Add timestamps to output (default HH:mm:ss, or pass a format string)', + description: 'Add timestamps to output (default HH:mm:ss.SSS, or pass a format string)', valueName: '', completionHint: 'none' }, diff --git a/src/types.ts b/src/types.ts index 2f629da..c4f47b1 100644 --- a/src/types.ts +++ b/src/types.ts @@ -103,7 +103,7 @@ export interface NumuxConfig { * @default false */ prefix?: boolean - /** Add timestamps to output lines. `true` uses default `HH:mm:ss` format, or pass a format string (e.g. `"HH:mm:ss.SSS"`) */ + /** Add timestamps to output lines. `true` uses default `HH:mm:ss.SSS` format, or pass a format string (e.g. `"HH:mm:ss"`) */ timestamps?: boolean | string /** * Kill all processes when any one exits (regardless of exit code) diff --git a/src/ui/pane.test.ts b/src/ui/pane.test.ts index b9c878e..b2dab5b 100644 --- a/src/ui/pane.test.ts +++ b/src/ui/pane.test.ts @@ -103,3 +103,51 @@ describe('Pane timestamp toggle', () => { expect(pane.timestampsEnabled).toBe(true) }) }) + +describe('Pane timestamp signs', () => { + test('every logical line gets a timestamp sign', async () => { + const pane = await createPane({ timestamps: true }) + pane.feed(encoder.encode('line1\nline2\nline3\n')) + const signs = pane.getTimestampSigns()! + // 4 logical lines: line1, line2, line3, and trailing newline + expect(signs.size).toBe(4) + for (let i = 0; i < 4; i++) { + expect(signs.has(i)).toBe(true) + expect(signs.get(i)!.before).toBeDefined() + } + }) + + test('lines with identical timestamps still get individual signs', async () => { + const pane = await createPane({ timestamps: 'HH:mm:ss' }) + // All lines fed in one call — same Date.now() — same formatted second + pane.feed(encoder.encode('a\nb\nc\n')) + const signs = pane.getTimestampSigns()! + expect(signs.size).toBe(4) + // All should have the same formatted value but still be present + const values = [...signs.values()].map(s => s.before) + expect(new Set(values).size).toBe(1) // same second + expect(values.length).toBe(4) // but every line has one + }) + + test('signs include milliseconds with default format', async () => { + const pane = await createPane({ timestamps: true }) + pane.feed(encoder.encode('hello\n')) + const signs = pane.getTimestampSigns()! + const ts = signs.get(0)!.before! + // Default format is HH:mm:ss.SSS — 12 chars + expect(ts).toMatch(/^\d{2}:\d{2}:\d{2}\.\d{3}$/) + }) + + test('signs cleared after clear()', async () => { + const pane = await createPane({ timestamps: true }) + pane.feed(encoder.encode('line1\nline2\n')) + expect(pane.getTimestampSigns()!.size).toBeGreaterThan(0) + pane.clear() + expect(pane.getTimestampSigns()!.size).toBe(0) + }) + + test('returns null when timestamps disabled', async () => { + const pane = await createPane() + expect(pane.getTimestampSigns()).toBeNull() + }) +}) diff --git a/src/ui/pane.ts b/src/ui/pane.ts index 44359a8..d6e228e 100644 --- a/src/ui/pane.ts +++ b/src/ui/pane.ts @@ -268,18 +268,17 @@ export class Pane { return this._timestampFormat !== null } + /** Returns the current line signs map from the timestamp gutter, or null if disabled. */ + getTimestampSigns(): Map | null { + return this.timestampGutter?.getLineSigns() ?? null + } + private updateTimestampSigns(): void { if (!(this.timestampGutter && this._timestampFormat)) return const fmt = this._timestampFormat const signs = new Map() - let prevFormatted = '' for (let i = 0; i < this.lineTimestamps.length; i++) { - const formatted = formatTimestamp(new Date(this.lineTimestamps[i]), fmt) - // Only show timestamp when it changes from the previous line - if (formatted !== prevFormatted) { - signs.set(i, { before: formatted }) - prevFormatted = formatted - } + signs.set(i, { before: formatTimestamp(new Date(this.lineTimestamps[i]), fmt) }) } this.timestampGutter.setLineSigns(signs) } diff --git a/src/utils/timestamp.ts b/src/utils/timestamp.ts index 1bbc2f5..8951042 100644 --- a/src/utils/timestamp.ts +++ b/src/utils/timestamp.ts @@ -1,5 +1,5 @@ /** Default timestamp format */ -export const DEFAULT_TIMESTAMP_FORMAT = 'HH:mm:ss' +export const DEFAULT_TIMESTAMP_FORMAT = 'HH:mm:ss.SSS' /** * Format a Date using a simple token-based format string. From d58a53c746f1fae56de8a33f4ed3f18174031ab0 Mon Sep 17 00:00:00 2001 From: Eivind Date: Thu, 16 Apr 2026 15:38:48 +0200 Subject: [PATCH 2/3] docs: regenerate README for new timestamp default Co-Authored-By: Claude Opus 4.6 (1M context) --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index eb9ba02..c5da15e 100644 --- a/README.md +++ b/README.md @@ -226,7 +226,7 @@ export default defineConfig({ | `--kill-others-on-fail` | Kill all processes when any exits with non-zero code | | `--max-restarts` `` | Max auto-restarts for crashed processes | | `--no-watch` | Disable file watching even if config has watch patterns | -| `-t,` `--timestamps` `[]` | Add timestamps to output (default HH:mm:ss, or pass a format string) | +| `-t,` `--timestamps` `[]` | Add timestamps to output (default HH:mm:ss.SSS, or pass a format string) | | `--log-dir` `` | Write per-process logs to directory | | `--debug` | Enable debug logging to .numux/debug.log | | `-h,` `--help` | Show this help | @@ -277,7 +277,7 @@ Top-level options apply to all processes (process-level settings override): | `watch` | `string \| string[]` | Global watch patterns, inherited by processes without their own watch | | `sort` | `'config' \| 'alphabetical' \| 'topological'` | Tab display order. `'config'` preserves definition order (package.json script order for wildcards), `'alphabetical'` sorts by process name, `'topological'` sorts by dependency tiers. | | `prefix` | `boolean` | Use prefixed output mode instead of TUI (for CI/scripts) | -| `timestamps` | `boolean \| string` | Add timestamps to output lines. `true` uses default `HH:mm:ss` format, or pass a format string (e.g. `"HH:mm:ss.SSS"`) | +| `timestamps` | `boolean \| string` | Add timestamps to output lines. `true` uses default `HH:mm:ss.SSS` format, or pass a format string (e.g. `"HH:mm:ss"`) | | `killOthers` | `boolean` | Kill all processes when any one exits (regardless of exit code) | | `killOthersOnFail` | `boolean` | Kill all processes when any one exits with a non-zero exit code | | `noWatch` | `boolean` | Disable file watching even if processes have watch patterns | From d55c3a21623c64ce0df19e98d941dd5e8495a34b Mon Sep 17 00:00:00 2001 From: Eivind Date: Thu, 16 Apr 2026 15:42:12 +0200 Subject: [PATCH 3/3] test: update prefix test to match new timestamp default Co-Authored-By: Claude Opus 4.6 (1M context) --- src/ui/prefix.test.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/ui/prefix.test.ts b/src/ui/prefix.test.ts index fd679d9..d7a2668 100644 --- a/src/ui/prefix.test.ts +++ b/src/ui/prefix.test.ts @@ -149,7 +149,7 @@ describe('PrefixDisplay (integration)', () => { expect(exitCode).toBe(1) }, 15000) - test('--timestamps prepends HH:MM:SS to output lines', async () => { + test('--timestamps prepends HH:mm:ss.SSS to output lines', async () => { const config = writeConfig( 'timestamps.json', JSON.stringify({ @@ -157,8 +157,8 @@ describe('PrefixDisplay (integration)', () => { }) ) const { stdout, exitCode } = await runPrefix(config, ['--timestamps']) - // Should contain a timestamp like [12:34:56] - expect(stdout).toMatch(/\[\d{2}:\d{2}:\d{2}\]/) + // Should contain a timestamp like [12:34:56.789] + expect(stdout).toMatch(/\[\d{2}:\d{2}:\d{2}\.\d{3}\]/) expect(stdout).toContain('hello') expect(exitCode).toBe(0) }, 10000)