Skip to content
Closed
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
17 changes: 17 additions & 0 deletions src/ansi.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
/// Removes ANSI escape sequences from strings sourced from session JSONL before they're
/// rendered by the dashboard / CLI / JSON export. The regex below covers CSI (`ESC [`),
/// OSC (`ESC ]`), and other 7-bit / 8-bit control sequences as defined by ECMA-48 — same
/// shape used by the well-known `ansi-regex` package, kept inline here to avoid a direct
/// dependency. Any non-string input is passed through unchanged.

const ANSI_PATTERN = new RegExp(
[
'[\\u001B\\u009B][[\\]()#;?]*(?:(?:(?:[a-zA-Z\\d]*(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]*)*)?\\u0007)',
'(?:(?:\\d{1,4}(?:;\\d{0,4})*)?[\\dA-PR-TZcf-ntqry=><~]))',
].join('|'),
'g',
)

export function stripAnsi(input: string): string {
return input.replace(ANSI_PATTERN, '')
}
10 changes: 8 additions & 2 deletions src/bash-utils.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,18 @@
import { basename } from 'path'

import { stripAnsi } from './ansi.js'

function stripQuotedStrings(command: string): string {
return command.replace(/"[^"]*"|'[^']*'/g, match => ' '.repeat(match.length))
}

export function extractBashCommands(command: string): string[] {
if (!command || !command.trim()) return []
export function extractBashCommands(rawCommand: string): string[] {
if (!rawCommand || !rawCommand.trim()) return []

// Bash inputs in session JSONL can carry ANSI escape sequences (terminal pastes,
// colorized command-line snippets). Stripping here keeps `bashBreakdown` keys
// and any other downstream display free of raw escape codes.
const command = stripAnsi(rawCommand)
const stripped = stripQuotedStrings(command)

const separatorRegex = /\s*(?:&&|;|\|)\s*/g
Expand Down
32 changes: 32 additions & 0 deletions tests/ansi.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { describe, it, expect } from 'vitest'

import { stripAnsi } from '../src/ansi.js'

describe('stripAnsi', () => {
it('removes basic SGR colour codes', () => {
expect(stripAnsi('red')).toBe('red')
expect(stripAnsi('warn')).toBe('warn')
})

it('removes 256-color and truecolor codes', () => {
expect(stripAnsi('fgbg')).toBe('fgbg')
})

it('removes cursor-movement sequences', () => {
expect(stripAnsi('topleft')).toBe('topleft')
})

it('removes OSC hyperlink sequences (ESC ] … BEL)', () => {
expect(stripAnsi(']8;;https://example.orgclicky]8;;')).toBe('clicky')
})

it('removes 8-bit CSI introducer (\\u009B)', () => {
expect(stripAnsi('›31mred›0m')).toBe('red')
})

it('leaves non-ANSI input untouched', () => {
expect(stripAnsi('npm install --save')).toBe('npm install --save')
expect(stripAnsi('')).toBe('')
expect(stripAnsi('emoji 🔥 ok')).toBe('emoji 🔥 ok')
})
})
5 changes: 5 additions & 0 deletions tests/bash-commands.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,11 @@ describe('extractBashCommands', () => {
expect(extractBashCommands(' git status ')).toEqual(['git'])
})

it('strips ANSI escape codes before extracting basenames (no escapes leak into bashBreakdown keys)', () => {
expect(extractBashCommands('npm install')).toEqual(['npm'])
expect(extractBashCommands('git status && ls')).toEqual(['git', 'ls'])
})

it('handles command with quotes containing separators', () => {
expect(extractBashCommands('echo "hello && world"')).toEqual(['echo'])
})
Expand Down