Skip to content
Open
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
5 changes: 3 additions & 2 deletions apps/desktop/src/main/__tests__/streaming-handoff.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -227,8 +227,9 @@ describe('single live-turn handoff', () => {
}],
});

// Thinking and tools own their disclosures; do not wrap them in another.
assert.equal((markup.match(/maka-processing-block/g) ?? []).length, 0);
// A tool-bearing run owns one compact Processing disclosure while the
// model-authored text remains a separate timeline boundary.
assert.equal((markup.match(/maka-processing-block/g) ?? []).length, 1);
assert.ok(markup.indexOf('深度思考') >= 0);
assert.ok(markup.indexOf('深度思考') < markup.indexOf('最终答案'));
assert.ok(markup.indexOf('最终答案') < markup.indexOf('Bash'));
Expand Down
143 changes: 143 additions & 0 deletions apps/desktop/src/renderer/styles/chat-message.css
Original file line number Diff line number Diff line change
Expand Up @@ -116,11 +116,154 @@
padding: var(--space-0-5) 0 0;
}

.maka-work-log {
min-width: 0;
}

.maka-work-log:not([data-collapsible="true"]),
.maka-work-log:not([data-collapsible="true"]) .maka-work-log-content {
display: contents;
}

.maka-work-log-header {
display: flex;
width: 100%;
min-height: var(--h-control-sm);
align-items: center;
gap: var(--space-1);
padding: var(--space-0-5) 0;
border: 0;
border-bottom: var(--border-width-hairline) solid var(--border-soft);
color: var(--muted-foreground);
background: transparent;
font: var(--maka-text-body);
text-align: start;
cursor: default;
}

.maka-work-log-label {
min-width: 0;
flex: 0 1 auto;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}

.maka-work-log-chevron {
flex: 0 0 auto;
color: inherit;
transition: transform var(--duration-fast) var(--ease-standard);
}

.maka-work-log-header[aria-expanded="true"] .maka-work-log-chevron {
transform: rotate(90deg);
}

@media (hover: hover) {
.maka-work-log-header:hover {
color: var(--foreground-secondary);
}
}

.maka-work-log-header:focus-visible {
outline: var(--focus-ring-width) solid var(--focus-ring);
outline-offset: var(--focus-ring-offset);
}

.maka-work-log-content {
display: flex;
min-width: 0;
flex-direction: column;
gap: var(--space-2);
padding-top: var(--space-2);
}

.maka-work-log-content[hidden] {
display: none;
}

.maka-processing-block {
min-width: 0;
}

.maka-processing-header {
display: flex;
width: 100%;
min-height: var(--h-control-sm);
align-items: center;
gap: var(--space-1-5);
padding: var(--space-0-5) 0;
border: 0;
border-radius: var(--radius-element);
color: var(--muted-foreground);
background: transparent;
font: var(--maka-text-body);
text-align: start;
cursor: default;
}

.maka-processing-icon {
display: inline-flex;
width: var(--icon-control);
height: var(--icon-control);
flex: 0 0 auto;
align-items: center;
justify-content: center;
color: inherit;
}

.maka-processing-summary {
min-width: 0;
flex: 1;
overflow: hidden;
color: inherit;
font-weight: var(--font-weight-normal);
text-overflow: ellipsis;
white-space: nowrap;
}

.maka-processing-chevron {
flex: 0 0 auto;
color: var(--muted-foreground);
transition: transform var(--duration-fast) var(--ease-standard);
}

.maka-processing-header[aria-expanded="true"] .maka-processing-chevron {
transform: rotate(90deg);
}

.maka-processing-block[data-running="true"] .maka-processing-summary {
color: var(--foreground-secondary);
}

.maka-processing-block[data-error="true"] .maka-processing-icon,
.maka-processing-block[data-error="true"] .maka-processing-summary {
color: var(--destructive);
}

@media (hover: hover) {
.maka-processing-header:hover {
background: var(--foreground-5);
}
}

.maka-processing-header:focus-visible {
outline: var(--focus-ring-width) solid var(--focus-ring);
outline-offset: var(--focus-ring-offset);
}

.maka-processing-sequence {
display: flex;
min-width: 0;
flex-direction: column;
gap: var(--space-1);
margin-inline-start: var(--space-2);
padding-inline-start: var(--space-2-5);
border-inline-start: var(--border-width-hairline) solid var(--border-soft);
}

.maka-processing-sequence[hidden] {
display: none;
}

/* Expanded activity headers stay reachable while their own detail is being
Expand Down
6 changes: 3 additions & 3 deletions apps/desktop/stories/app-shell.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -550,7 +550,7 @@ export const StreamingTurn: Story = {
// attribute, and the elapsed clock is dropped rather than pinned under it,
// because any value it could print is a real wall-clock difference that would
// differ between two captures. In the app the same row reads
// "正在琢磨… · 2m 1s", with the phrase swapping every 20s.
// "等待模型输出… · 2m 1s".
export const RunningStatusDuringToolRun: Story = {
render: () => (
<ComposedShell
Expand Down Expand Up @@ -761,7 +761,7 @@ export const ProviderRateLimited: Story = {
};

// Real path: the provider throttles a live request and Runtime schedules a
// retry. The running turn swaps its working phrase for the retry Banner
// retry. The running turn swaps its waiting status for the retry Banner
// (`ModelProviderRetryIndicator`) — the "retrying" state no story reached.
export const ProviderRetrying: Story = {
render: () => (
Expand Down Expand Up @@ -855,7 +855,7 @@ export const ManyTurns: Story = {
// Real path: Desktop Computer Use is exposed through the Runtime Host Client
// Capability bridge. The settled observation establishes the confirmed target;
// the following sequence inherits it while live progress replaces the generic
// working phrase at the bottom of the turn.
// waiting status at the bottom of the turn.
export const ComputerUseObservability: Story = {
render: () => (
<ComposedShell
Expand Down
101 changes: 101 additions & 0 deletions packages/cli/src/__tests__/pi-transcript.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import assert from 'node:assert/strict';
import { describe, test } from 'node:test';
import { visibleWidth } from '@earendil-works/pi-tui';
import { ASSISTANT_PROGRESS_TOOL_NAME } from '@maka/core/events';
import type { PipeShellOutput, PtyShellOutput } from '@maka/core/shell-run';
import type { ShellRunToolResult } from '@maka/core/shell-run-result';
import type { SessionEvent, ShellRunSnapshotResult, ToolResultContent } from '@maka/core/events';
Expand Down Expand Up @@ -53,6 +54,81 @@ function toolStatus(entry: MakaPiToolEntry | undefined): string | undefined {
}

describe('Maka Pi TUI transcript', () => {
test('renders commentary without exposing the progress transport tool', () => {
const state = createMakaPiTranscriptState();
appendUserPrompt(state, 'inspect the repository', 'message-1', true);

applyMakaSessionEventToTranscript(
state,
event({
type: 'text_delta',
messageId: 'step-1',
text: 'I am checking the recent repository activity.',
phase: 'commentary',
}),
);
applyMakaSessionEventToTranscript(
state,
event({
type: 'tool_start',
stepId: 'step-1',
toolUseId: 'progress-1',
toolName: ASSISTANT_PROGRESS_TOOL_NAME,
args: { text: 'I am checking the recent repository activity.' },
}),
);
applyMakaSessionEventToTranscript(
state,
event({
type: 'tool_result',
toolUseId: 'progress-1',
isError: false,
content: { kind: 'json', value: { status: 'displayed' } },
}),
);

assert.deepEqual(
state.entries.map((entry) => entry.kind),
['user', 'assistant'],
);
assert.equal(
state.entries[1]?.kind === 'assistant' ? state.entries[1].phase : undefined,
'commentary',
);
});

test('omits empty durable assistant steps between thinking and tools', () => {
const state = createMakaPiTranscriptState();
replaceTranscriptWithStoredMessages(state, [
{
type: 'assistant',
id: 'assistant-1',
turnId: 'turn-1',
ts: 1,
text: '',
thinking: { text: 'Need to inspect the file.' },
modelId: 'model-1',
},
{
type: 'tool_call',
id: 'read-1',
turnId: 'turn-1',
ts: 2,
toolName: 'Read',
args: { path: 'package.json' },
},
]);

assert.deepEqual(
state.entries.map((entry) => entry.kind),
['thinking', 'tool'],
);
const rendered = renderMakaPiTranscript(state, meta(), 80).map(stripAnsi);
const thinkingIndex = rendered.findIndex((line) => line.includes('Thinking…'));
const toolIndex = rendered.findIndex((line) => line.includes('Read'));
assert.equal(toolIndex - thinkingIndex, 2);
});

test('renders manual compaction from the typed terminal outcome', async () => {
for (const [outcome, expected] of [
[{ kind: 'compacted' as const, checkpointId: 'checkpoint-1' }, 'Context compacted.'],
Expand Down Expand Up @@ -774,6 +850,31 @@ describe('Maka Pi TUI transcript', () => {
assert.match(renderMakaPiTranscript(state, meta(), 80).map(stripAnsi).join('\n'), /final/);
});

test('keeps the assistant text phase when completion replaces streamed text', () => {
const state = createMakaPiTranscriptState();
applyMakaSessionEventToTranscript(
state,
event({
type: 'text_delta',
messageId: 'message-1',
text: 'checking',
phase: 'commentary',
}),
);
applyMakaSessionEventToTranscript(
state,
event({
type: 'text_complete',
messageId: 'message-1',
text: 'checking the repository',
phase: 'commentary',
}),
);

const entry = state.entries[0];
assert.equal(entry?.kind === 'assistant' ? entry.phase : undefined, 'commentary');
});

test('allows text_complete to replace streamed assistant text with empty text', () => {
const state = createMakaPiTranscriptState();
applyMakaSessionEventToTranscript(
Expand Down
Loading
Loading