Skip to content

Commit 341aa46

Browse files
committed
Strip ANSI escapes from bash commands across all providers
Use strip-ansi (already in dep tree via Ink) in extractBashCommands to prevent terminal escape codes from leaking into dashboard bash breakdown keys. Route goose, gemini, qwen, and openclaw through extractBashCommands instead of inline split, which also gives them multi-command extraction (matching claude/codex/droid behavior).
1 parent 292265b commit 341aa46

7 files changed

Lines changed: 18 additions & 12 deletions

File tree

package-lock.json

Lines changed: 2 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,8 @@
4646
"chalk": "^5.4.1",
4747
"commander": "^13.1.0",
4848
"ink": "^7.0.0",
49-
"react": "^19.2.5"
49+
"react": "^19.2.5",
50+
"strip-ansi": "^7.2.0"
5051
},
5152
"devDependencies": {
5253
"@types/node": "^22.19.17",

src/bash-utils.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
import { basename } from 'path'
2+
import stripAnsi from 'strip-ansi'
23

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

7-
export function extractBashCommands(command: string): string[] {
8-
if (!command || !command.trim()) return []
8+
export function extractBashCommands(rawCommand: string): string[] {
9+
if (!rawCommand || !rawCommand.trim()) return []
910

11+
const command = stripAnsi(rawCommand)
1012
const stripped = stripQuotedStrings(command)
1113

1214
const separatorRegex = /\s*(?:&&|;|\|)\s*/g

src/providers/gemini.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { join } from 'path'
33
import { homedir } from 'os'
44

55
import { calculateCost } from '../models.js'
6+
import { extractBashCommands } from '../bash-utils.js'
67
import type { Provider, SessionSource, SessionParser, ParsedProviderCall } from './types.js'
78

89
const toolNameMap: Record<string, string> = {
@@ -93,8 +94,7 @@ function parseSession(data: GeminiSession, seenKeys: Set<string>): ParsedProvide
9394
const mapped = toolNameMap[tc.displayName ?? ''] ?? toolNameMap[tc.name] ?? tc.displayName ?? tc.name
9495
allTools.push(mapped)
9596
if (mapped === 'Bash' && tc.args && typeof tc.args.command === 'string') {
96-
const cmd = tc.args.command.split(/\s+/)[0] ?? ''
97-
if (cmd) bashCommands.push(cmd)
97+
bashCommands.push(...extractBashCommands(tc.args.command))
9898
}
9999
}
100100
}

src/providers/goose.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { join } from 'path'
22
import { homedir, platform } from 'os'
33

44
import { calculateCost, getShortModelName } from '../models.js'
5+
import { extractBashCommands } from '../bash-utils.js'
56
import { isSqliteAvailable, getSqliteLoadError, openDatabase, type SqliteDatabase } from '../sqlite.js'
67
import type { Provider, SessionSource, SessionParser, ParsedProviderCall } from './types.js'
78

@@ -109,8 +110,9 @@ function extractToolsFromMessages(db: SqliteDatabase, sessionId: string): { tool
109110
if (mapped === 'Bash') {
110111
const cmd = item.toolCall?.value?.arguments?.command
111112
if (typeof cmd === 'string') {
112-
const first = cmd.split(/\s+/)[0] ?? ''
113-
if (first && !bashCommands.includes(first)) bashCommands.push(first)
113+
for (const c of extractBashCommands(cmd)) {
114+
if (!bashCommands.includes(c)) bashCommands.push(c)
115+
}
114116
}
115117
}
116118
}

src/providers/openclaw.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { homedir } from 'os'
44

55
import { readSessionFile } from '../fs-utils.js'
66
import { calculateCost } from '../models.js'
7+
import { extractBashCommands } from '../bash-utils.js'
78
import type { Provider, SessionSource, SessionParser, ParsedProviderCall } from './types.js'
89

910
const toolNameMap: Record<string, string> = {
@@ -78,8 +79,7 @@ function extractTools(content: Array<{ type?: string; name?: string; arguments?:
7879
const mapped = toolNameMap[block.name] ?? block.name
7980
tools.push(mapped)
8081
if (mapped === 'Bash' && block.arguments && typeof block.arguments.command === 'string') {
81-
const cmd = block.arguments.command.split(/\s+/)[0] ?? ''
82-
if (cmd) bashCommands.push(cmd)
82+
bashCommands.push(...extractBashCommands(block.arguments.command))
8383
}
8484
}
8585
}

src/providers/qwen.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { homedir } from 'os'
44

55
import { readSessionFile } from '../fs-utils.js'
66
import { calculateCost } from '../models.js'
7+
import { extractBashCommands } from '../bash-utils.js'
78
import type { Provider, SessionSource, SessionParser, ParsedProviderCall } from './types.js'
89

910
const toolNameMap: Record<string, string> = {
@@ -66,8 +67,7 @@ function extractTools(parts: QwenPart[]): { tools: string[]; bashCommands: strin
6667
const mapped = toolNameMap[part.functionCall.name] ?? part.functionCall.name
6768
tools.push(mapped)
6869
if (mapped === 'Bash' && part.functionCall.args && typeof part.functionCall.args['command'] === 'string') {
69-
const cmd = (part.functionCall.args['command'] as string).split(/\s+/)[0] ?? ''
70-
if (cmd) bashCommands.push(cmd)
70+
bashCommands.push(...extractBashCommands(part.functionCall.args['command'] as string))
7171
}
7272
}
7373
}

0 commit comments

Comments
 (0)