Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
1b83ebb
fix(core): separate hook context from transcript display
destire-mio Jul 28, 2026
a5d71df
Merge upstream main into codex/fix-7940-user-prompt-context
destire-mio Jul 29, 2026
eca5832
Merge branch 'main' into codex/fix-7940-user-prompt-context
wenshao Jul 29, 2026
10283d9
test(ci): gate desktop transcript projection
destire-mio Jul 29, 2026
0f86770
revert: keep desktop CI scope unchanged
destire-mio Jul 29, 2026
b9eaf55
Merge branch 'main' into codex/fix-7940-user-prompt-context
wenshao Jul 29, 2026
762ba8b
test: cover transcript display fallbacks
destire-mio Jul 29, 2026
ee7ee23
fix(transcript): address review feedback
destire-mio Jul 30, 2026
3879b70
chore: merge origin/main resolving UserPromptSubmit transcript-displa…
qwen-code-dev-bot Jul 31, 2026
00d06c8
Merge branch 'main' into codex/fix-7940-user-prompt-context
wenshao Jul 31, 2026
3052b25
fix(transcript): reconcile post-merge provenance paths
destire-mio Jul 31, 2026
f45208d
chore: merge origin/main into codex/fix-7940-user-prompt-context
qwen-code-dev-bot Aug 1, 2026
eab31c8
fix(webui): preserve legacy transcript concatenation
destire-mio Aug 1, 2026
5e80f24
Merge branch 'main' into codex/fix-7940-user-prompt-context
wenshao Aug 1, 2026
aa66dc1
Merge branch 'main' into codex/fix-7940-user-prompt-context
wenshao Aug 1, 2026
891ee3c
test(transcript): cover projection consumers
destire-mio Aug 2, 2026
8920e28
fix(transcript): consolidate hook context projection
destire-mio Aug 2, 2026
d808b4f
fix(transcript): support single-field display provenance
destire-mio Aug 2, 2026
ad0d857
fix(transcript): strip hook context with invalid metadata
destire-mio Aug 2, 2026
f90c51d
test(acp): cover empty replay display text
destire-mio Aug 3, 2026
a4be4f6
Merge branch 'main' into codex/fix-7940-user-prompt-context
wenshao Aug 3, 2026
d66130b
Merge origin/main into codex/fix-7940-user-prompt-context
destire-mio Aug 7, 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
93 changes: 93 additions & 0 deletions packages/acp-bridge/src/transcript-replay.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,99 @@ describe('createTranscriptReplayMachine', () => {
]);
});

it('uses clean user display metadata while preserving image parts', () => {
const machine = createTranscriptReplayMachine();
const projected = updates(
machine,
record('user-1', 'user', {
message: {
role: 'user',
parts: [
{ text: 'expanded model prompt' },
{
inlineData: {
data: 'image-data',
mimeType: 'image/png',
},
},
{
text: [
'<qwen:user-prompt-submit-context>',
'hook-only context',
'</qwen:user-prompt-submit-context>',
].join('\n'),
},
],
},
systemPayload: {
displayText: 'raw @file prompt',
hookContext: 'hook-only context',
},
}),
);

expect(projected).toMatchObject([
{
sessionUpdate: 'user_message_chunk',
content: { type: 'text', text: 'raw @file prompt' },
},
{
sessionUpdate: 'user_message_chunk',
content: {
type: 'image',
data: 'image-data',
mimeType: 'image/png',
},
},
]);
});

it('strips only a complete final tag-only context part', () => {
const projected = updates(
createTranscriptReplayMachine(),
record('user-1', 'user', {
message: {
role: 'user',
parts: [
{ text: 'user prompt' },
{
text: [
'<qwen:user-prompt-submit-context>',
'hook-only context',
'</qwen:user-prompt-submit-context>',
].join('\n'),
},
],
},
}),
);

expect(projected).toHaveLength(1);
expect(projected[0]).toMatchObject({
content: { type: 'text', text: 'user prompt' },
});
});

it('preserves legacy bare hook context without a reliable boundary', () => {
const projected = updates(
createTranscriptReplayMachine(),
record('user-1', 'user', {
message: {
role: 'user',
parts: [
{ text: 'user prompt' },
{ text: 'legacy bare hook context' },
],
},
}),
);

expect(projected).toMatchObject([
{ content: { type: 'text', text: 'user prompt' } },
{ content: { type: 'text', text: 'legacy bare hook context' } },
]);
});

it('projects ordered message parts with source metadata', () => {
const machine = createTranscriptReplayMachine();
const projected = updates(
Expand Down
59 changes: 38 additions & 21 deletions packages/acp-bridge/src/transcript-replay.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import type {
TranscriptRecordInput,
TranscriptReplayGapInput,
} from '@qwen-code/qwen-code-core/transcriptRecords';
import { projectUserTranscriptForDisplay } from '@qwen-code/qwen-code-core/transcriptRecords';
import {
parseGoalSnapshotV2,
parseGoalStateRecordPayloadV2,
Expand Down Expand Up @@ -473,29 +474,21 @@ class DefaultTranscriptReplayMachine implements TranscriptReplayMachine {
emit: (update: SessionUpdate) => TranscriptReplayEmission,
meta: UpdateMetaOptions,
): Iterable<TranscriptReplayEmission> {
if (
record.subtype === 'goal_runtime' ||
record.subtype === 'notification' ||
record.subtype === 'cron' ||
record.subtype === 'mid_turn_user_message'
) {
const payload = isObjectRecord(record.systemPayload)
? record.systemPayload
const projection = projectUserTranscriptForDisplay(record);
const payload = isObjectRecord(record.systemPayload)
? record.systemPayload
: undefined;
const backgroundTask =
payload && isObjectRecord(payload['backgroundTask'])
? payload['backgroundTask']
: undefined;
const displayText =
payload && typeof payload['displayText'] === 'string'
? payload['displayText']
: undefined;
const backgroundTask =
payload && isObjectRecord(payload['backgroundTask'])
? payload['backgroundTask']
: undefined;
if (displayText) {
if (projection.displayText !== undefined) {
if (projection.displayText.length > 0) {
const isNotification = record.subtype === 'notification';
yield emit(
createTranscriptMessageUpdate({
role: 'user',
text: displayText,
text: projection.displayText,
...meta,
...(isNotification
? {
Expand All @@ -510,11 +503,34 @@ class DefaultTranscriptReplayMachine implements TranscriptReplayMachine {
: {}),
}),
);
return;
}
yield* this.projectMessageParts(
record,
'user',
emit,
meta,
undefined,
projection.parts,
);
return;
}

if (
record.subtype === 'goal_runtime' ||
record.subtype === 'notification' ||
record.subtype === 'cron' ||
record.subtype === 'mid_turn_user_message'
) {
if (record.subtype !== 'mid_turn_user_message') return;
}
yield* this.projectMessageParts(record, 'user', emit, meta);
yield* this.projectMessageParts(
record,
'user',
emit,
meta,
undefined,
projection.parts,
);
}

private *projectAssistantRecord(
Expand Down Expand Up @@ -549,8 +565,9 @@ class DefaultTranscriptReplayMachine implements TranscriptReplayMachine {
emit: (update: SessionUpdate) => TranscriptReplayEmission,
meta: UpdateMetaOptions,
beforeToolCall?: () => SessionUpdate | undefined,
partsOverride?: readonly unknown[],
): Iterable<TranscriptReplayEmission> {
const parts = record.message?.parts;
const parts = partsOverride ?? record.message?.parts;
if (!parts) return;
for (let partIndex = 0; partIndex < parts.length; partIndex += 1) {
const part = parts[partIndex];
Expand Down
39 changes: 39 additions & 0 deletions packages/cli/src/services/insight/generators/DataProcessor.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,45 @@ describe('DataProcessor', () => {
expect(result).toContain('[User]: Hello, world!');
});

it('should analyze clean user display text instead of hook context', () => {
const records: ChatRecord[] = [
{
sessionId: 'test-session',
timestamp: new Date().toISOString(),
type: 'user',
message: {
role: 'user',
parts: [
{ text: 'expanded model prompt' },
{
text: [
'<qwen:user-prompt-submit-context>',
'hook-only context',
'</qwen:user-prompt-submit-context>',
].join('\n'),
},
],
},
systemPayload: {
displayText: 'raw @file prompt',
hookContext: 'hook-only context',
},
uuid: '',
parentUuid: null,
cwd: '',
version: '',
},
];
const result = (
dataProcessor as unknown as {
formatRecordsForAnalysis(records: ChatRecord[]): string;
}
).formatRecordsForAnalysis(records);

expect(result).toContain('[User]: raw @file prompt');
expect(result).not.toContain('hook-only context');
});

it('should format assistant text messages correctly', () => {
const records: ChatRecord[] = [
{
Expand Down
17 changes: 13 additions & 4 deletions packages/cli/src/services/insight/generators/DataProcessor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import type {
import {
getInsightPrompt,
runSideQuery,
projectUserTranscriptForDisplay,
type Config,
type ChatRecord,
} from '@qwen-code/qwen-code-core';
Expand Down Expand Up @@ -217,10 +218,18 @@ export class DataProcessor {

for (const record of records) {
if (record.type === 'user') {
const text =
record.message?.parts
?.map((p) => ('text' in p ? p.text : ''))
.join('') || '';
const projection = projectUserTranscriptForDisplay(record);
const fallbackText = projection.parts
.map((part: unknown) =>
typeof part === 'object' &&
part !== null &&
'text' in part &&
typeof part.text === 'string'
? part.text
: '',
)
.join('');
const text = projection.displayText ?? fallbackText;
output += `[User]: ${text}\n`;
} else if (record.type === 'assistant') {
if (record.message?.parts) {
Expand Down
Loading
Loading