Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
e39eeb3
feat(runtime): add assistant commentary phases
hqhq1025 Aug 30, 2026
2fbfba5
feat(ui): present commentary as a collapsible work log
hqhq1025 Aug 31, 2026
b49a168
Merge remote-tracking branch 'origin/main' into codex/model-commentar…
hqhq1025 Aug 31, 2026
5bb1905
fix: harden commentary replay and work-log lifecycle
hqhq1025 Aug 31, 2026
b1b8b1a
fix(runtime): split text around provider tools
hqhq1025 Aug 31, 2026
3367477
Merge origin/main into codex/model-commentary-phase
hqhq1025 Sep 1, 2026
c902ea2
fix(ui): keep progress out of final replies
hqhq1025 Sep 1, 2026
beb0932
Merge remote-tracking branch 'origin/main' into codex/model-commentar…
hqhq1025 Sep 1, 2026
ca628d0
Merge remote-tracking branch 'origin/main' into codex/model-commentar…
hqhq1025 Sep 1, 2026
9a23473
test(desktop): follow collapsed work log keyboard path
hqhq1025 Sep 2, 2026
eab3fa8
Merge remote-tracking branch 'origin/main' into codex/model-commentar…
hqhq1025 Sep 2, 2026
fc96cfe
fix(ui): close commentary review gaps
hqhq1025 Sep 2, 2026
9c2b17a
fix(desktop): keep turn presentation within architecture budget
hqhq1025 Sep 2, 2026
88f4eb5
fix(desktop): use the supported muted ink token
hqhq1025 Sep 2, 2026
588447b
fix(ui): preserve legacy work log semantics
hqhq1025 Sep 2, 2026
b782d78
Merge remote-tracking branch 'origin/main' into codex/model-commentar…
hqhq1025 Sep 2, 2026
5a293b6
test(desktop): stabilize settled transcript interactions
hqhq1025 Sep 2, 2026
446f0b4
Merge latest main and adopt upstream code-scroll stabilization
hqhq1025 Sep 3, 2026
c41191e
Merge remote-tracking branch 'origin/main' into codex/model-commentar…
hqhq1025 Sep 3, 2026
25fbb6b
test(desktop): wait for code overflow before measuring
hqhq1025 Sep 3, 2026
25c7742
Merge remote-tracking branch 'origin/main' into codex/model-commentar…
hqhq1025 Sep 3, 2026
ca59da1
fix: address commentary review feedback
hqhq1025 Sep 3, 2026
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
2 changes: 1 addition & 1 deletion apps/desktop/renderer-architecture.json
Original file line number Diff line number Diff line change
Expand Up @@ -785,7 +785,7 @@
"react": 1
},
"importSpecifiers": 18,
"nonTriviaTokens": 1410
"nonTriviaTokens": 1408
},
"src/renderer/app-shell.tsx": {
"importDeclarations": 102,
Expand Down
6 changes: 3 additions & 3 deletions apps/desktop/src/renderer/app-shell-turn-view-model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,13 @@ import { useRef } from 'react';
import type { UiLocale } from '@maka/core/ui-locale';
import {
deriveTurnLineageMap,
finalAssistantReplyText,
formatTurnDuration,
isSandboxDeniedTool,
type TurnFooterActionMeta,
type TurnLineageBadge,
type TurnLineageTarget,
type TurnPresentation,
type TurnPresentationDeriver,
type TurnViewModel,
} from '@maka/ui';
import {
Expand Down Expand Up @@ -207,7 +207,7 @@ function deriveTurnPresentationEntry(input: {
const footerActions = deriveTurnFooterActions({
status: turn.status,
locale: uiLocale,
hasContent: Boolean(turn.assistant?.text && turn.assistant.text.trim().length > 0),
hasContent: finalAssistantReplyText(turn).trim().length > 0,
// Match the badge lineage rule (regenerate ?? legacy retry) so a turn
// that already has a parallel answer hints at it in the tooltip too.
...((lineageEntry?.regeneratedToTurnId ?? lineageEntry?.retriedToTurnId)
Expand Down Expand Up @@ -267,7 +267,7 @@ export function deriveAppShellTurnPresentation(
*/
export function useAppShellTurnPresentation(
context: AppShellTurnPresentationContext,
): TurnPresentationDeriver {
): (turns: readonly TurnViewModel[]) => TurnPresentation {
const derivation = useRef<AppShellTurnPresentationDerivation>(undefined);
derivation.current ??= createAppShellTurnPresentationDerivation();
return (turns) => derivation.current!.derive(turns, context);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import {
ChatSurfaceLayout,
Composer,
ClientCapabilityPrompt,
finalAssistantReplyText,
SandboxBoundaryPrompt,
UserQuestionPrompt,
useToast,
Expand Down Expand Up @@ -247,7 +248,7 @@ export function QuoteCompanionPanel(props: {
deriveTurnFooterActions({
status: turn.status,
locale,
hasContent: Boolean(turn.assistant?.text?.trim()),
hasContent: finalAssistantReplyText(turn).trim().length > 0,
...(companion.regeneratePendingTurnId === turn.turnId
? { pendingActions: new Set(['regenerate'] as const) }
: {}),
Expand Down
274 changes: 274 additions & 0 deletions packages/runtime/src/__tests__/ai-sdk-backend.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9981,6 +9981,97 @@ describe('AiSdkBackend RunTrace', () => {
assert.equal(typeof failureTrace?.data?.redactedErrorStackSha256, 'string');
});

test('retries an idle watchdog timeout after an unstarted Responses text item', async () => {
const timers = manualWatchdogTimer();
let calls = 0;
const appended: StoredMessage[] = [];
const model = new MockLanguageModelV4({
doStream: async (options) => {
calls += 1;
if (calls === 1) {
return {
stream: hangingProviderStream(
[
{ type: 'stream-start', warnings: [] },
{
type: 'text-start',
id: 'text-1',
providerMetadata: {
openai: { itemId: 'message-item-1', phase: 'commentary' },
},
},
],
options.abortSignal,
),
};
}
return {
stream: simulateReadableStream({
chunks: [
{ type: 'stream-start', warnings: [] },
{ type: 'text-start', id: 'text-2' },
{ type: 'text-delta', id: 'text-2', delta: 'recovered' },
{ type: 'text-end', id: 'text-2' },
{
type: 'finish',
finishReason: { unified: 'stop', raw: 'stop' },
usage: emptyUsage(),
},
],
initialDelayInMs: null,
chunkDelayInMs: null,
}),
};
},
});
const backend = createTestAiSdkBackend({
sessionId: 'session-1',
header: header(),
appendMessage: async (message) => {
appended.push(message);
},
connection: {
...connection(),
slug: 'openai',
providerType: 'openai',
models: [{ id: 'gpt-5.6', apiProtocol: 'openai-responses' }],
},
apiKey: 'sk-test',
modelId: 'gpt-5.6',
modelFactory: () => model,
tools: [],
newId: idGenerator(),
now: monotonicClock(),
streamWatchdogTimer: timers.clock,
providerRetrySleep: async () => {},
});

const events: SessionEvent[] = [];
const eventsPromise = collectEvents(
backend.send({ turnId: 'turn-1', text: 'hi', context: [] }),
events,
);
await waitFor(() => calls === 1 && timers.armCount() >= 3);
timers.fire();
await eventsPromise;

assert.equal(calls, 2);
assert.equal(
events.some((event) => event.type === 'provider_retry' && event.phase === 'started'),
true,
);
assert.equal(
events.some((event) => event.type === 'error'),
false,
);
assert.equal(events.find((event) => event.type === 'complete')?.stopReason, 'end_turn');
const recovered = appended.find(
(message): message is AssistantMessage => message.type === 'assistant',
);
assert.equal(recovered?.text, 'recovered');
assert.equal(recovered?.providerOptions, undefined);
});

test('does not retry an idle watchdog timeout after provider continuation metadata', async () => {
const timers = manualWatchdogTimer();
let calls = 0;
Expand Down Expand Up @@ -14828,6 +14919,7 @@ describe('AiSdkBackend steering durability and identity', () => {
(message): message is AssistantMessage => message.type === 'assistant',
);
assert.equal(assistant?.text, 'OneTwo');
assert.equal(assistant?.contentOrder, undefined);
assert.deepEqual(assistant?.providerOptions, {
openai: {
annotations: [
Expand All @@ -14838,6 +14930,187 @@ describe('AiSdkBackend steering durability and identity', () => {
});
});

test('preserves native Responses text item boundaries as separate assistant messages', async () => {
const model = new MockLanguageModelV4({
doStream: async () => ({
stream: simulateReadableStream({
chunks: [
{ type: 'stream-start', warnings: [] },
{
type: 'text-start',
id: 'text-1',
providerMetadata: {
openai: { itemId: 'message-1', phase: 'commentary' },
},
},
{ type: 'text-delta', id: 'text-1', delta: 'I am checking it.' },
{
type: 'text-end',
id: 'text-1',
providerMetadata: {
openai: { itemId: 'message-1', phase: 'commentary' },
},
},
{
type: 'text-start',
id: 'text-2',
providerMetadata: {
openai: { itemId: 'message-2', phase: 'final_answer' },
},
},
{ type: 'text-delta', id: 'text-2', delta: 'It is ready.' },
{
type: 'text-end',
id: 'text-2',
providerMetadata: {
openai: { itemId: 'message-2', phase: 'final_answer' },
},
},
{
type: 'finish',
finishReason: { unified: 'stop', raw: 'stop' },
usage: emptyUsage(),
},
] as LanguageModelV4StreamPart[],
initialDelayInMs: null,
chunkDelayInMs: null,
}),
}),
});
const appended: StoredMessage[] = [];
const backend = createTestAiSdkBackend({
sessionId: 'session-1',
header: header(),
appendMessage: async (message) => {
appended.push(message);
},
connection: {
...connection(),
slug: 'openai',
providerType: 'openai',
defaultModel: 'gpt-5',
},
apiKey: 'sk-test',
modelId: 'gpt-5',
modelFactory: () => model,
tools: [],
newId: idGenerator(),
now: monotonicClock(),
});

await drain(backend.send({ turnId: 'turn-1', text: 'inspect it', context: [] }));

assert.deepEqual(
appended
.filter((message): message is AssistantMessage => message.type === 'assistant')
.map((message) => ({
text: message.text,
providerOptions: message.providerOptions,
})),
[
{
text: 'I am checking it.',
providerOptions: {
openai: { itemId: 'message-1', phase: 'commentary' },
},
},
{
text: 'It is ready.',
providerOptions: {
openai: { itemId: 'message-2', phase: 'final_answer' },
},
},
],
);
});

test('does not carry metadata from an empty Responses text item into the next item', async () => {
const model = new MockLanguageModelV4({
doStream: async () => ({
stream: simulateReadableStream({
chunks: [
{ type: 'stream-start', warnings: [] },
{
type: 'text-start',
id: 'text-empty',
providerMetadata: {
openai: { itemId: 'message-empty', phase: 'commentary' },
},
},
{
type: 'text-end',
id: 'text-empty',
providerMetadata: {
openai: { itemId: 'message-empty', phase: 'commentary' },
},
},
{
type: 'text-start',
id: 'text-final',
providerMetadata: {
openai: { itemId: 'message-final', phase: 'final_answer' },
},
},
{ type: 'text-delta', id: 'text-final', delta: 'Done.' },
{
type: 'text-end',
id: 'text-final',
providerMetadata: {
openai: { itemId: 'message-final', phase: 'final_answer' },
},
},
{
type: 'finish',
finishReason: { unified: 'stop', raw: 'stop' },
usage: emptyUsage(),
},
] as LanguageModelV4StreamPart[],
initialDelayInMs: null,
chunkDelayInMs: null,
}),
}),
});
const appended: StoredMessage[] = [];
const backend = createTestAiSdkBackend({
sessionId: 'session-1',
header: header(),
appendMessage: async (message) => {
appended.push(message);
},
connection: {
...connection(),
slug: 'openai',
providerType: 'openai',
defaultModel: 'gpt-5',
},
apiKey: 'sk-test',
modelId: 'gpt-5',
modelFactory: () => model,
tools: [],
newId: idGenerator(),
now: monotonicClock(),
});

await drain(backend.send({ turnId: 'turn-1', text: 'finish it', context: [] }));

assert.deepEqual(
appended
.filter((message): message is AssistantMessage => message.type === 'assistant')
.map((message) => ({
text: message.text,
providerOptions: message.providerOptions,
})),
[
{
text: 'Done.',
providerOptions: {
openai: { itemId: 'message-final', phase: 'final_answer' },
},
},
],
);
});

test('executes native WebSearch inside the primary provider stream', async () => {
const model = new MockLanguageModelV4({
doStream: async () => ({
Expand Down Expand Up @@ -14941,6 +15214,7 @@ describe('AiSdkBackend steering durability and identity', () => {
const assistant = appended.find(
(message): message is AssistantMessage => message.type === 'assistant',
);
assert.deepEqual(assistant?.contentOrder, ['tools', 'text']);
assert.deepEqual(assistant?.providerOptions, {
openai: {
itemId: 'message-1',
Expand Down
Loading