Skip to content
Merged
Show file tree
Hide file tree
Changes from 35 commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
128faf1
feat: send prompt-attached videos directly with the prompt
RealKai42 Jul 21, 2026
6946c57
fix: fall back to inline video when the upload channel fails
RealKai42 Jul 21, 2026
ead47c6
test: cover prompt video edge cases and failure paths
RealKai42 Jul 21, 2026
8881ef3
Merge remote-tracking branch 'origin/main' into feat/prompt-video-inline
RealKai42 Jul 21, 2026
64d95ef
fix: surface auth rejections from the video upload channel
RealKai42 Jul 21, 2026
3a4e720
fix: keep the by-design no-hook video error from degrading to inline
RealKai42 Jul 21, 2026
465e7a6
fix: constrain the llm video route param to a safe alphabet
RealKai42 Jul 21, 2026
172a531
style(agent-core-v2): move added explanations into top-of-file commen…
RealKai42 Jul 21, 2026
3e08864
fix: reject prompt video uploads when the model lacks video input
RealKai42 Jul 21, 2026
99cfdab
fix: sniff uploaded video bytes before inlining them
RealKai42 Jul 21, 2026
0f89d7e
test: keep the generation stub pending until the abort lands
RealKai42 Jul 21, 2026
b1c6e03
fix: validate prompt file references before mutating session controls
RealKai42 Jul 21, 2026
a9d0ff8
fix: serialize prompt submissions per session
RealKai42 Jul 21, 2026
ef197c2
fix: play reloaded provider videos through the authenticated fetch path
RealKai42 Jul 21, 2026
ece87aa
fix: fence pending video submits against session and model switches
RealKai42 Jul 21, 2026
b8077ec
fix: preserve the caller's video path verbatim in uploadVideo
RealKai42 Jul 21, 2026
259e4e6
fix: forward provider-issued ids on image URL prompt parts too
RealKai42 Jul 21, 2026
6f0c9ec
fix(web): reconcile inlined video echoes into the optimistic user mes…
RealKai42 Jul 21, 2026
f536783
fix: queue bash submits behind a pending video upload
RealKai42 Jul 21, 2026
a637ea5
test: cast the driver through unknown for the session-switch fence test
RealKai42 Jul 21, 2026
d922a4c
fix: validate prompt file references before resolving the prompt agent
RealKai42 Jul 21, 2026
defe33d
fix: key the playback mapping by the id embedded in the reference URL
RealKai42 Jul 21, 2026
c5f1a71
fix: sanitize provider video ids on the write side of the playback map
RealKai42 Jul 21, 2026
61db503
fix(web): keep recovered provider videos resendable through the edit …
RealKai42 Jul 21, 2026
8bb4bca
fix: fall back to inline video for no-hook providers whose wire carri…
RealKai42 Jul 21, 2026
4f7d3cd
test: satisfy the full IBlobStore shape in the playback-map stub
RealKai42 Jul 21, 2026
c5c370d
fix: queue prompt-producing slash commands behind pending uploads
RealKai42 Jul 21, 2026
5cb32bc
fix: validate media kinds in the prompt file-reference preflight
RealKai42 Jul 21, 2026
78061b9
fix(web): play sent and recovered videos in the file preview
RealKai42 Jul 21, 2026
a780bce
fix(web): preview recovered videos with the authenticated blob URL
RealKai42 Jul 21, 2026
399ae2e
style(agent-core-v2): move the inline-fallback note into the module h…
RealKai42 Jul 21, 2026
0df5e3b
fix: fence delayed bash submits to the originating session
RealKai42 Jul 21, 2026
e76851f
fix: emit prompt video telemetry from the agent scope
RealKai42 Jul 21, 2026
9a9f9d2
fix: preserve provider image ids through legacy projections and web m…
RealKai42 Jul 21, 2026
a08db8a
fix(web): revoke recovered video blob URLs before dropping attachments
RealKai42 Jul 21, 2026
37eacff
fix: serialize foreground slash commands behind pending uploads
RealKai42 Jul 21, 2026
45c3f81
fix: defer session controls until media preparation succeeds
RealKai42 Jul 21, 2026
d6f64ef
chore: bump the new SDK video upload API as a minor release
RealKai42 Jul 21, 2026
0998978
fix: re-check busy state before draining upload-queued commands
RealKai42 Jul 21, 2026
78f5ca9
fix: resolve profile-bound models before choosing the uploader
RealKai42 Jul 21, 2026
6a1bd26
refactor: resolve prompt videos at request time inside the engine
RealKai42 Jul 21, 2026
1c50a40
fix: propagate abort through video upload delivery instead of degrading
RealKai42 Jul 21, 2026
a32e0b6
fix: keep the tag form for no-upload providers whose wire drops inlin…
RealKai42 Jul 21, 2026
353210a
fix: escape the NUL separator in the video upload cache key
RealKai42 Jul 21, 2026
febd214
fix: check the abort signal before the inline video fallback
RealKai42 Jul 22, 2026
57af423
fix: retry transient prompt video upload failures on later steps
RealKai42 Jul 22, 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
5 changes: 5 additions & 0 deletions .changeset/fix-read-media-video-upload-fallback.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@moonshot-ai/kimi-code": patch
---

Fix ReadMediaFile failing on videos when the model's provider has no usable file upload channel — the video now falls back to being sent inline.
5 changes: 5 additions & 0 deletions .changeset/prompt-video-inline.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@moonshot-ai/kimi-code": minor
---

Send pasted videos to the model directly with the prompt instead of routing them through a ReadMediaFile tool call, so video input is faster and no longer depends on an extra tool round trip.
5 changes: 5 additions & 0 deletions .changeset/sdk-session-upload-video.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@moonshot-ai/kimi-code-sdk": patch
Comment thread
RealKai42 marked this conversation as resolved.
Outdated
---

Add Session.uploadVideo to upload a local video through the session's current model and embed the returned reference as a prompt part.
5 changes: 5 additions & 0 deletions .changeset/web-video-playback-after-reload.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@moonshot-ai/kimi-code": patch
---

web: Keep uploaded videos playable in the chat after reloading or resuming a session.
159 changes: 135 additions & 24 deletions apps/kimi-code/src/tui/kimi-tui.ts
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@ import { isDeadTerminalError } from './utils/dead-terminal';
import { formatErrorMessage } from './utils/event-payload';
import { pickForegroundTasks } from './utils/foreground-task';
import { ImageAttachmentStore, type ImageAttachment } from './utils/image-attachment-store';
import { extractMediaAttachments, rewriteMediaPlaceholders } from './utils/image-placeholder';
import { extractMediaSegments, materializeMediaSegments, rewriteMediaPlaceholders, segmentsToPromptParts } from './utils/image-placeholder';
import { REPLAY_TURN_LIMIT } from './utils/message-replay';
import { hasPatchChanges } from './utils/object-patch';
import { sessionRowsForPicker } from './utils/session-picker-rows';
Expand Down Expand Up @@ -1000,6 +1000,31 @@ export class KimiTUI {
const historyText = wasBashMode ? `!${text}` : text;
void this.persistInputHistory(historyText);
if (wasBashMode) {
// A pending video upload is busy time too: chain behind it so the
// command cannot record shell context before the earlier prompt is
// dispatched (which would reorder user actions).
if (this.pendingSubmits > 0) {
// Shell commands are session-scoped: capture the originating session
// and re-verify at dispatch, or a mid-upload switch would run the
// command in another session's workspace and record it there.
const sessionAtSubmit = this.session;
this.inputSubmitChain = this.inputSubmitChain.then(() => {
if (sessionAtSubmit === undefined || this.session !== sessionAtSubmit) {
this.showError(
'The session changed while the video was uploading — please resend the command.',
);
return;
}
if (this.state.appState.streamingPhase !== 'idle') {
this.enqueueMessage(text, undefined, 'bash');
} else {
this.runShellCommandFromInput(text);
}
this.updateQueueDisplay();
this.state.ui.requestRender();
});
return;
}
// Only one foreground action at a time: queue the shell command while
// another shell command is running or an agent turn is in progress.
if (this.state.appState.streamingPhase !== 'idle') {
Expand Down Expand Up @@ -1129,30 +1154,84 @@ export class KimiTUI {
this.updateQueueDisplay();
}

// Submits chained behind an in-flight video upload (see sendNormalUserInput).
private pendingSubmits = 0;
private inputSubmitChain: Promise<void> = Promise.resolve();

sendNormalUserInput(text: string): void {
if (this.btwPanelController.sendUserInput(text)) return;
if (this.state.appState.model.trim().length === 0) {
this.showError(LLM_NOT_SET_MESSAGE);
return;
}
const extraction = extractMediaAttachments(text, this.imageStore);
const extraction = extractMediaSegments(text, this.imageStore);
if (!this.validateMediaCapabilities(extraction)) return;
const session = this.session;
if (session === undefined) {
this.showError(LLM_NOT_SET_MESSAGE);
return;
}
if (extraction.hasMedia) {
this.sendMessage(session, text, {
hasMedia: true,
parts: extraction.parts,
imageAttachmentIds: extraction.imageAttachmentIds,
});
} else {
this.sendMessage(session, text);
const hasVideo = extraction.videoAttachmentIds.length > 0;
if (!hasVideo && this.pendingSubmits === 0) {
// Fully synchronous path: nothing to upload, nothing queued behind an
// upload, so ordering is trivially safe.
if (extraction.hasMedia) {
this.sendMessage(session, text, {
hasMedia: true,
parts: segmentsToPromptParts(extraction.segments),
imageAttachmentIds: extraction.imageAttachmentIds,
});
} else {
this.sendMessage(session, text);
}
this.updateQueueDisplay();
this.state.ui.requestRender();
return;
}
this.updateQueueDisplay();
this.state.ui.requestRender();
// Video attachments upload asynchronously before the prompt goes out.
// Chain this submit (and any submitted while an upload is in flight) so
// a slow upload cannot be overtaken by a later message.
const modelAtSubmit = this.state.appState.model;
this.pendingSubmits += 1;
this.inputSubmitChain = this.inputSubmitChain.then(async () => {
Comment thread
RealKai42 marked this conversation as resolved.
Outdated
try {
let parts: readonly PromptPart[] | undefined;
if (extraction.hasMedia) {
parts = hasVideo
? await materializeMediaSegments(extraction.segments, (attachment) =>
session.uploadVideo(attachment.sourcePath),
)
: segmentsToPromptParts(extraction.segments);
}
// Re-checked at dispatch time: if the user switched the session or
// the model mid-upload, the provider-issued reference (or the upload
// itself) would now go to the wrong place. Drop the message and ask
// for a resend instead.
if (this.session !== session || this.state.appState.model !== modelAtSubmit) {
this.showError(
'The session or model changed while the video was uploading — please resend.',
);
return;
}
if (parts !== undefined) {
this.sendMessage(session, text, {
hasMedia: true,
parts,
imageAttachmentIds: extraction.imageAttachmentIds,
});
} else {
this.sendMessage(session, text);
}
} catch (error) {
// The tag fallback itself could not be prepared (unwritable cache
// dir, vanished video source…); nothing was dispatched.
this.showError(`Failed to prepare media attachment: ${formatErrorMessage(error)}`);
} finally {
this.pendingSubmits -= 1;
}
this.updateQueueDisplay();
this.state.ui.requestRender();
});
}

validateMediaCapabilities(extraction: {
Expand Down Expand Up @@ -1319,11 +1398,27 @@ export class KimiTUI {
return;
}
if (!this.validateMediaCapabilities(rewrite)) return;
this.beginSessionRequest();
void session.activateSkill(skillName, rewrite.text).catch((error: unknown) => {
const message = formatErrorMessage(error);
this.failSessionRequest(`Skill "${skillName}" failed: ${message}`);
});
const modelAtSubmit = this.state.appState.model;
const dispatch = (): void => {
if (this.session !== session || this.state.appState.model !== modelAtSubmit) {
this.showError(
'The session or model changed while the video was uploading — please resend.',
);
return;
}
this.beginSessionRequest();
void session.activateSkill(skillName, rewrite.text).catch((error: unknown) => {
const message = formatErrorMessage(error);
this.failSessionRequest(`Skill "${skillName}" failed: ${message}`);
});
};
// A pending video upload is busy time: chain behind it so the skill
// prompt cannot start a turn ahead of the earlier message.
if (this.pendingSubmits > 0) {
this.inputSubmitChain = this.inputSubmitChain.then(dispatch);
return;
}
dispatch();
}

activatePluginCommand(
Expand All @@ -1343,13 +1438,29 @@ export class KimiTUI {
return;
}
if (!this.validateMediaCapabilities(rewrite)) return;
this.beginSessionRequest();
void session
.activatePluginCommand(pluginId, commandName, rewrite.text)
.catch((error: unknown) => {
const message = formatErrorMessage(error);
this.failSessionRequest(`Command "${pluginId}:${commandName}" failed: ${message}`);
});
const modelAtSubmit = this.state.appState.model;
const dispatch = (): void => {
if (this.session !== session || this.state.appState.model !== modelAtSubmit) {
this.showError(
'The session or model changed while the video was uploading — please resend.',
);
return;
}
this.beginSessionRequest();
void session
.activatePluginCommand(pluginId, commandName, rewrite.text)
.catch((error: unknown) => {
const message = formatErrorMessage(error);
this.failSessionRequest(`Command "${pluginId}:${commandName}" failed: ${message}`);
});
};
// A pending video upload is busy time: chain behind it so the command
// prompt cannot start a turn ahead of the earlier message.
if (this.pendingSubmits > 0) {
this.inputSubmitChain = this.inputSubmitChain.then(dispatch);
return;
}
dispatch();
}

private sendMessage(session: Session, input: string, options?: SendMessageOptions): void {
Expand Down
Loading
Loading