Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import { describe, it } from 'node:test';
import assert from 'node:assert/strict';
import { formatRedactedJson, formatToolIntent } from '@maka/ui';
import { formatToolInvocationLine, projectToolArgsPreview } from '@maka/core/tool-quiet-preview';

describe('tool args redaction', () => {
it('redacts JSON-shaped args before they are rendered', () => {
Expand All @@ -44,4 +45,24 @@ describe('tool args redaction', () => {
assert.ok(rendered.length <= 241);

});

it('keeps secrets out of the collapsed-row invocation line and its wire preview', () => {
// Built at runtime so no literal secret ever sits in the repo.
const bearerToken = ['sk', 'live', 'test', '9f8e7d6c5b4a'].join('-');
const passwordValue = ['maka', 'pw', '1a2b3c4d'].join('-');
const args = {
command: `curl -H "Authorization: Bearer ${bearerToken}" https://example.test`,
password: passwordValue,
};
const line = formatToolInvocationLine({ toolName: 'Bash', args }, 'en');
assert.ok(line !== undefined);
assert.doesNotMatch(line, new RegExp(bearerToken));
assert.match(line, /redacted/i);

const preview = projectToolArgsPreview('Bash', args);
const serialized = JSON.stringify(preview ?? null);
assert.doesNotMatch(serialized, new RegExp(bearerToken));
assert.doesNotMatch(serialized, new RegExp(passwordValue));
assert.doesNotMatch(serialized, /password/);
});
});
84 changes: 84 additions & 0 deletions packages/cli/src/__tests__/pi-transcript.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3852,6 +3852,90 @@ describe('Maka Pi TUI transcript', () => {
}
});

test('names a live quiet Bash row from the wire args preview', () => {
const state = createMakaPiTranscriptState();
// Runtime Host live tool_start omits full args; the bounded preview is all
// the compact row has until the turn-end reconcile.
applyMakaSessionEventToTranscript(
state,
event({
type: 'tool_start',
toolUseId: 'bash-preview',
toolName: 'Bash',
args: undefined,
argsPreview: { command: 'git status --porcelain' },
}),
);
applyMakaSessionEventToTranscript(
state,
event({
type: 'tool_result',
toolUseId: 'bash-preview',
isError: false,
content: {
kind: 'terminal',
cwd: '/repo',
cmd: 'git status --porcelain',
status: 'completed',
exitCode: 0,
output: { mode: 'pipes', stdout: '', stderr: '' },
},
}),
);

const rendered = renderMakaPiTranscript(state, meta(), 80).map(stripAnsi).join('\n');
assert.match(rendered, /\$ git status --porcelain/);
// Once the row names the call, the quiet-success disclaimer is noise.
assert.doesNotMatch(rendered, /\(no output\)/);
});

test('keeps the no-output placeholder when the row cannot name the call', () => {
const state = createMakaPiTranscriptState();
applyMakaSessionEventToTranscript(
state,
event({ type: 'tool_start', toolUseId: 'bash-blind', toolName: 'Bash', args: undefined }),
);
applyMakaSessionEventToTranscript(
state,
event({
type: 'tool_result',
toolUseId: 'bash-blind',
isError: false,
content: {
kind: 'terminal',
cwd: '/repo',
cmd: 'true',
status: 'completed',
exitCode: 0,
output: { mode: 'pipes', stdout: '', stderr: '' },
},
}),
);

const rendered = renderMakaPiTranscript(state, meta(), 80).map(stripAnsi).join('\n');
assert.match(rendered, /\(no output\)/);
});

test('names a task_create row by its first subject, not a JSON dump', () => {
const state = createMakaPiTranscriptState();
applyMakaSessionEventToTranscript(
state,
event({
type: 'tool_start',
toolUseId: 'task-1',
toolName: 'task_create',
displayName: 'Task Create',
args: undefined,
argsPreview: { tasks: [{ subject: '修复登录 bug' }], tasksTotal: 2 },
}),
);

const rendered = renderMakaPiTranscript(state, meta(), 80).map(stripAnsi).join('\n');
assert.match(rendered, /修复登录 bug/);
assert.doesNotMatch(rendered, /tasks:/);
assert.doesNotMatch(rendered, /\(no output\)/);
});

test('orders and de-dupes tool_output_delta by seq and marks redacted chunks', () => {
const state = createMakaPiTranscriptState();
applyMakaSessionEventToTranscript(
Expand Down
28 changes: 22 additions & 6 deletions packages/cli/src/pi-transcript-tools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,14 +93,19 @@ function toolDurationText(entry: MakaPiToolEntry): string {
* expand, so the row needs neither
* a separator glyph nor an expand marker. Short annotations are reserved
* whole during truncation: a long command can never hide an `exit 1`.
*
* The `no output` placeholder appears only when the row cannot name the call
* (no input summary): once the target says what ran, `● Bash $ git add -A`
* reads complete on its own and the disclaimer is noise.
*/
function renderCompactToolBlock(entry: MakaPiToolEntry, width: number): string[] {
const inputSummary = collapseToSingleLine(toolInputSummary(entry));
const head = `${toolDisc(entry)} ${entry.title ?? entry.toolName}`;
const head = `${toolDisc(entry)} ${entry.title ?? entry.intent ?? entry.toolName}`;
const annotation = compactAnnotation(entry);
const annotationText = annotation.placeholderOnly && inputSummary ? '' : annotation.text;
return [
fitLine(
assembleCompactToolRow(head, inputSummary, annotation.text, width, annotation.protect),
assembleCompactToolRow(head, inputSummary, annotationText, width, annotation.protect),
width,
),
];
Expand All @@ -114,19 +119,26 @@ function renderCompactToolBlock(entry: MakaPiToolEntry, width: number): string[]
* `protect` reports whether every part is a fixed shape (durations always
* are): only protected annotations are reserved whole during truncation.
*/
function compactAnnotation(entry: MakaPiToolEntry): { text: string; protect: boolean } {
function compactAnnotation(entry: MakaPiToolEntry): {
text: string;
protect: boolean;
/** True when the annotation is solely the dim `no output` placeholder. */
placeholderOnly: boolean;
} {
const parts: string[] = [];
const duration = toolDurationText(entry);
if (duration) parts.push(duration);
let protect = true;
let placeholderOnly = false;
if (makaPiToolPresentationStatus(entry) !== 'running') {
const summary = compactToolSummary(entry);
if (summary && !(summary.placeholder && parts.length > 0)) {
parts.push(collapseToSingleLine(summary.text));
protect = summary.protect === true;
placeholderOnly = summary.placeholder === true && parts.length === 1;
}
}
return { text: parts.length > 0 ? `(${parts.join(' · ')})` : '', protect };
return { text: parts.length > 0 ? `(${parts.join(' · ')})` : '', protect, placeholderOnly };
}

/**
Expand Down Expand Up @@ -164,7 +176,7 @@ function assembleCompactToolRow(

function renderExpandedToolBlock(entry: MakaPiToolEntry, width: number): string[] {
const duration = toolDurationText(entry);
let header = `${toolDisc(entry)} ${entry.title ?? entry.toolName}`;
let header = `${toolDisc(entry)} ${entry.title ?? entry.intent ?? entry.toolName}`;
if (duration) header += ` (${duration})`;
const lines = [fitLine(header, width)];

Expand Down Expand Up @@ -823,7 +835,11 @@ function toolInputSummary(entry: MakaPiToolEntry): string {
const line = formatToolInvocationLine({ toolName: entry.toolName, args: input }, 'en');
if (line) return limitText(line, 600);
// Absolute last resort — still single-line for the compact header contract.
return `input: ${limitText(formatUnknownInline(input), 600)}`;
// An empty args object carries no information; leave the row bare instead of
// printing `input: {}` noise (and let a quiet result keep its placeholder).
const inline = formatUnknownInline(input);
if (inline === '{}') return '';
return `input: ${limitText(inline, 600)}`;
}

/**
Expand Down
11 changes: 10 additions & 1 deletion packages/cli/src/pi-transcript.ts
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,8 @@ export type MakaPiTranscriptEntry =
toolUseId: string;
toolName: string;
title?: string;
/** Runtime-authored semantic name for a live tool call. */
intent?: string;
input: unknown;
/** Structured result returned by the tool. */
result?: ToolResultContent;
Expand Down Expand Up @@ -451,6 +453,7 @@ export function hydrateToolsWithStoredMessages(
if (!durable) continue;
entry.toolName = durable.toolName;
entry.title = durable.title;
entry.intent = durable.intent;
entry.input = structuredClone(durable.input);
entry.callStatus = mergeToolCallStatus(entry.callStatus, durable.callStatus);
if (
Expand Down Expand Up @@ -654,7 +657,11 @@ export function applyMakaSessionEventToTranscript(
toolUseId: event.toolUseId,
toolName: event.toolName,
...(event.displayName ? { title: event.displayName } : {}),
input: projectToolActivityArgs(event.toolName, event.args),
...(event.intent ? { intent: event.intent } : {}),
// Live Runtime Host frames omit full args; the bounded wire preview
// still lets the compact row name the call. The turn-end reconcile
// replaces it with the durable full args.
input: projectToolActivityArgs(event.toolName, event.args ?? event.argsPreview),
resultVersion: 0,
progress: createProgressBuffer(),
outputDeltas: createOutputBuffer(),
Expand Down Expand Up @@ -951,6 +958,7 @@ function storedToolToTranscriptEntry(
toolUseId: call.id,
toolName: call.toolName,
...(call.displayName ? { title: call.displayName } : {}),
...(call.intent ? { intent: call.intent } : {}),
input: projectToolActivityArgs(call.toolName, call.args),
progress: createProgressBuffer(),
outputDeltas: createOutputBuffer(),
Expand Down Expand Up @@ -1406,6 +1414,7 @@ function transcriptEntrySignature(entry: MakaPiTranscriptEntry, width: number):
makaPiToolPresentationStatus(entry),
entry.durationMs ?? '',
entry.title ?? entry.toolName,
entry.intent ?? '',
entry.progress.version,
entry.outputDeltas.version,
entry.resultVersion,
Expand Down
Loading