Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
48 changes: 48 additions & 0 deletions packages/cli/src/acp-integration/session/Session.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6120,6 +6120,54 @@ describe('Session', () => {
);
});

it('embeds text file resource blocks as File reference parts', async () => {
mockChat.sendMessageStream = vi
.fn()
.mockResolvedValue(createEmptyStream());

await session.prompt({
sessionId: 'test-session-id',
prompt: [
{
type: 'text',
text: 'check this log\n\n@attachment:///app.log',
},
{
type: 'resource',
resource: {
uri: 'attachment:///app.log',
mimeType: 'text/plain',
text: 'line1\nline2',
},
},
],
});

const sendMessageStream = mockChat.sendMessageStream as ReturnType<
typeof vi.fn
>;
const request = sendMessageStream.mock.calls[0]?.[1] as {
message: Array<Record<string, unknown>>;
};
const parts = request.message;
expect(
parts.some(
(p) =>
typeof p['text'] === 'string' &&
(p['text'] as string).includes(
'File: attachment:///app.log\nline1\nline2',
),
),
).toBe(true);
expect(
parts.some(
(p) =>
typeof p['text'] === 'string' &&
(p['text'] as string).includes('@attachment:///app.log'),
),
).toBe(true);
});

it('degrades an oversized inline image to a text placeholder before sending to the model', async () => {
const ENV_KEY = 'QWEN_CODE_MAX_INLINE_MEDIA_BYTES';
const original = process.env[ENV_KEY];
Expand Down
7 changes: 6 additions & 1 deletion packages/sdk-typescript/src/daemon/ui/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,8 +62,13 @@ export function createDaemonTranscriptStore(
text: string,
images?: Array<{ data: string; mimeType: string }>,
meta?: DaemonTextDeltaMeta,
files?: Array<{ name: string; mimeType: string }>,
) {
state = appendLocalUserTranscriptMessage(state, text, { images, meta });
state = appendLocalUserTranscriptMessage(state, text, {
images,
meta,
files,
});
scheduleNotify();
},
reset(nextSeed: Partial<DaemonTranscriptState> = {}) {
Expand Down
4 changes: 4 additions & 0 deletions packages/sdk-typescript/src/daemon/ui/transcript.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@ export function appendLocalUserTranscriptMessage(
text: string,
opts: DaemonTranscriptReducerOptions & {
images?: Array<{ data: string; mimeType: string }>;
files?: Array<{ name: string; mimeType: string }>;
meta?: DaemonTextDeltaMeta;
} = {},
): DaemonTranscriptState {
Expand All @@ -120,6 +121,9 @@ export function appendLocalUserTranscriptMessage(
if (opts.images && opts.images.length > 0) {
(block as DaemonTextTranscriptBlock).images = [...opts.images];
}
if (opts.files && opts.files.length > 0) {
(block as DaemonTextTranscriptBlock).files = [...opts.files];
}
appendBlock(next, block);
next.activeUserBlockId = block.id;
return trimTranscriptState(next);
Expand Down
8 changes: 8 additions & 0 deletions packages/sdk-typescript/src/daemon/ui/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -854,6 +854,13 @@ export interface DaemonTextTranscriptBlock extends DaemonTranscriptBlockBase {
text: string;
/** Images attached to this user message (base64 data URIs). */
images?: Array<{ data: string; mimeType: string }>;
/**
* Text file attachments on this user message (display metadata only —
* the content rides the prompt's resource blocks and is never stored
* on the block). Local optimistic messages only; daemon replays carry
* no attachment metadata.
*/
files?: Array<{ name: string; mimeType: string }>;
streaming?: boolean;
collapsed?: boolean;
/** Used by the reducer for per-subAgent block routing; renderers may use it for nesting. */
Expand Down Expand Up @@ -1060,6 +1067,7 @@ export interface DaemonTranscriptStore {
text: string,
images?: Array<{ data: string; mimeType: string }>,
meta?: DaemonTextDeltaMeta,
files?: Array<{ name: string; mimeType: string }>,
): void;
reset(seed?: Partial<DaemonTranscriptState>): void;
/**
Expand Down
14 changes: 14 additions & 0 deletions packages/sdk-typescript/test/unit/daemonUi.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,20 @@ describe('daemon UI normalizer and transcript reducer', () => {
});
});

it('stores text file attachment metadata on local user messages', () => {
const store = createDaemonTranscriptStore();

store.appendLocalUserMessage('check this', undefined, undefined, [
{ name: 'app.log', mimeType: 'text/plain' },
]);

expect(store.getSnapshot().blocks[0]).toMatchObject({
kind: 'user',
text: 'check this',
files: [{ name: 'app.log', mimeType: 'text/plain' }],
});
});

it('stores input annotations from replayed user message chunks', () => {
const inputAnnotations = [
{
Expand Down
Loading
Loading