mirror of
https://github.com/MoonshotAI/kimi-code.git
synced 2026-07-09 17:29:12 +00:00
fix(web): clear composer draft synchronously on send (#1163)
When the first message of an empty session is submitted, the optimistic user turn unmounts the empty-session composer before the post-flush text watcher can persist the cleared draft. The docked composer then mounts and reloads the stale text from localStorage. Clear the persisted draft synchronously in the submit / steer / slash command paths instead of relying on the text watcher, so the next mount always starts empty.
This commit is contained in:
parent
b0708464f4
commit
ff6e8bbd7c
5 changed files with 49 additions and 3 deletions
5
.changeset/fix-web-composer-draft-clear.md
Normal file
5
.changeset/fix-web-composer-draft-clear.md
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
"@moonshot-ai/kimi-code": patch
|
||||
---
|
||||
|
||||
Fix the web composer occasionally keeping typed text after sending the first message of a new session.
|
||||
|
|
@ -83,7 +83,7 @@ const { t } = useI18n();
|
|||
// ---------------------------------------------------------------------------
|
||||
// Textarea + per-session draft persistence — see useComposerDraft.
|
||||
// ---------------------------------------------------------------------------
|
||||
const { text, textareaRef, autosize, loadForEdit } = useComposerDraft({
|
||||
const { text, textareaRef, autosize, loadForEdit, clearDraft } = useComposerDraft({
|
||||
sessionId: () => props.sessionId,
|
||||
});
|
||||
|
||||
|
|
@ -180,6 +180,7 @@ const {
|
|||
skills: () => props.skills,
|
||||
emitCommand: (cmd) => emit('command', cmd),
|
||||
historyPush: (entry) => history.push(entry),
|
||||
clearDraft,
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
|
@ -293,6 +294,7 @@ function handleSubmit(): void {
|
|||
: false;
|
||||
if (parsed && known) {
|
||||
text.value = '';
|
||||
clearDraft();
|
||||
slashOpen.value = false;
|
||||
collapseAndRefit();
|
||||
emit('command', parsed.arg ? `${parsed.cmd} ${parsed.arg}` : parsed.cmd);
|
||||
|
|
@ -310,6 +312,7 @@ function handleSubmit(): void {
|
|||
clearAfterSubmit();
|
||||
|
||||
text.value = '';
|
||||
clearDraft();
|
||||
slashOpen.value = false;
|
||||
mentionOpen.value = false;
|
||||
collapseAndRefit();
|
||||
|
|
@ -336,6 +339,7 @@ function handleSteer(): void {
|
|||
clearAfterSubmit();
|
||||
history.push(trimmed);
|
||||
text.value = '';
|
||||
clearDraft();
|
||||
slashOpen.value = false;
|
||||
mentionOpen.value = false;
|
||||
collapseAndRefit();
|
||||
|
|
|
|||
|
|
@ -72,5 +72,16 @@ export function useComposerDraft(deps: ComposerDraftDeps) {
|
|||
});
|
||||
}
|
||||
|
||||
return { text, textareaRef, autosize, loadForEdit };
|
||||
/**
|
||||
* Synchronously clear the persisted draft for the current session.
|
||||
* Call this right after clearing `text.value` on send/steer; relying on the
|
||||
* text watcher is unsafe because the Composer may unmount before the watcher
|
||||
* flushes (e.g. when the optimistic user message replaces the empty-session
|
||||
* composer), causing the next mount to reload the stale draft.
|
||||
*/
|
||||
function clearDraft(): void {
|
||||
saveDraft(sessionId(), '');
|
||||
}
|
||||
|
||||
return { text, textareaRef, autosize, loadForEdit, clearDraft };
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,6 +16,12 @@ export interface SlashMenuDeps {
|
|||
emitCommand: (cmd: string) => void;
|
||||
/** Record a sent command for ↑/↓ recall. */
|
||||
historyPush: (entry: string) => void;
|
||||
/**
|
||||
* Synchronously clear the persisted draft when a bare command is chosen.
|
||||
* Mirrors the explicit clear in Composer's submit/steer paths so a draft
|
||||
* is not left behind if the Composer unmounts before the text watcher flushes.
|
||||
*/
|
||||
clearDraft?: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -27,7 +33,7 @@ export interface SlashMenuDeps {
|
|||
* when an item is chosen.
|
||||
*/
|
||||
export function useSlashMenu(deps: SlashMenuDeps) {
|
||||
const { text, textareaRef, autosize, skills, emitCommand, historyPush } = deps;
|
||||
const { text, textareaRef, autosize, skills, emitCommand, historyPush, clearDraft } = deps;
|
||||
|
||||
const open = ref(false);
|
||||
const items = ref<SlashCommand[]>([]);
|
||||
|
|
@ -61,6 +67,7 @@ export function useSlashMenu(deps: SlashMenuDeps) {
|
|||
return;
|
||||
}
|
||||
text.value = '';
|
||||
clearDraft?.();
|
||||
// Menu-selected bare commands (e.g. /model, /login) reach here directly and
|
||||
// never go through handleSubmit, so record them for recall too. acceptsInput
|
||||
// commands are pushed later by handleSubmit together with their argument.
|
||||
|
|
|
|||
|
|
@ -132,4 +132,23 @@ describe('useComposerDraft', () => {
|
|||
draft.autosize();
|
||||
}).not.toThrow();
|
||||
});
|
||||
|
||||
it('clearDraft removes the persisted draft synchronously', async () => {
|
||||
// Regression: when the first message of an empty session is submitted, the
|
||||
// optimistic user turn unmounts the composer before the post-flush text
|
||||
// watcher can clear the draft. clearDraft must therefore clear it
|
||||
// synchronously so a remount does not reload the stale text.
|
||||
globalThis.localStorage.setItem(draftStorageKey('s1'), 'stale draft');
|
||||
const { draft } = setup('s1');
|
||||
draft.clearDraft();
|
||||
// No nextTick — the write is synchronous.
|
||||
expect(globalThis.localStorage.getItem(draftStorageKey('s1'))).toBeNull();
|
||||
|
||||
// Simulate the remount after the optimistic turn: a fresh composable
|
||||
// instance for the same session should start empty, not restore the draft.
|
||||
const { text } = setup('s1');
|
||||
expect(text.value).toBe('');
|
||||
await nextTick();
|
||||
expect(globalThis.localStorage.getItem(draftStorageKey('s1'))).toBeNull();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue