diff --git a/apps/kimi-code/src/tui/utils/image-placeholder.ts b/apps/kimi-code/src/tui/utils/image-placeholder.ts index 56edcee88b..7a9700bb6e 100644 --- a/apps/kimi-code/src/tui/utils/image-placeholder.ts +++ b/apps/kimi-code/src/tui/utils/image-placeholder.ts @@ -89,6 +89,17 @@ export function extractMediaAttachments( const tail = text.slice(cursor); pushText(parts, tail); + // When video attachments are present, prepend a prompt hint so the model + // knows to use ReadMediaFile rather than writing Python frame-extraction scripts. + if (videoAttachmentIds.length > 0) { + parts.unshift({ + type: 'text', + text: + 'Use the ReadMediaFile tool to read and analyze the attached video directly. ' + + 'Do not write Python scripts or Bash commands to extract frames.', + }); + } + return { // Text-only submissions drop the synthesised parts array — the // caller's contract is "parts is meaningful iff hasMedia", and diff --git a/apps/kimi-code/test/tui/input/image-placeholder.test.ts b/apps/kimi-code/test/tui/input/image-placeholder.test.ts index 477727293a..57f68e6137 100644 --- a/apps/kimi-code/test/tui/input/image-placeholder.test.ts +++ b/apps/kimi-code/test/tui/input/image-placeholder.test.ts @@ -103,12 +103,19 @@ describe('extractMediaAttachments', () => { const r = extractMediaAttachments(text, store); expect(r.imageAttachmentIds).toEqual([1]); expect(r.videoAttachmentIds).toEqual([2]); - expect(r.parts[0]).toEqual({ type: 'text', text: 'first ' }); - expect(r.parts[1]).toEqual({ + // First part is the video hint (because video is present) + expect(r.parts[0]).toEqual({ + type: 'text', + text: + 'Use the ReadMediaFile tool to read and analyze the attached video directly. ' + + 'Do not write Python scripts or Bash commands to extract frames.', + }); + expect(r.parts[1]).toEqual({ type: 'text', text: 'first ' }); + expect(r.parts[2]).toEqual({ type: 'image_url', imageUrl: { url: 'data:image/png;base64,AQ==' }, }); - const cachePath = videoPathFromParts(r.parts); + const cachePath = videoPathFromParts(r.parts.slice(3)); expect(cachePath.startsWith(getCacheDir())).toBe(true); expect(readFileSync(cachePath, 'utf8')).toBe('video-bytes'); } finally { @@ -145,8 +152,9 @@ describe('extractMediaAttachments', () => { // The filename drives the cache label; `&` must be escaped in the attribute. const att = store.addVideo('video/mp4', srcVideo, 'a&b.mp4'); const r = extractMediaAttachments(att.placeholder, store); - expect(r.parts).toHaveLength(1); - const text = (r.parts[0] as TextPart).text; + expect(r.parts).toHaveLength(2); + // First part is the video hint; second part is the actual tag. + const text = (r.parts[1] as TextPart).text; expect(text).toMatch(/