diff --git a/.changeset/web-first-prompt-fixes.md b/.changeset/web-first-prompt-fixes.md new file mode 100644 index 000000000..fa34159ab --- /dev/null +++ b/.changeset/web-first-prompt-fixes.md @@ -0,0 +1,5 @@ +--- +"@moonshot-ai/kimi-code": patch +--- + +web: Fix an occasional "another turn is active" error when sending the first message of a new conversation, and show a starting state while it is being sent. diff --git a/apps/kimi-web/src/App.vue b/apps/kimi-web/src/App.vue index e46e8a2fc..26dfcaefc 100644 --- a/apps/kimi-web/src/App.vue +++ b/apps/kimi-web/src/App.vue @@ -739,6 +739,7 @@ function openPr(url: string): void { :search-files="client.searchFiles" :upload-image="client.uploadImage" :sending="client.isSending.value" + :starting="client.isStartingFirstPrompt.value" :fast-moon="client.fastMoon.value" :file-reload-key="client.activeSessionId.value" :session-loading="client.sessionLoading.value" diff --git a/apps/kimi-web/src/components/chat/ChatDock.vue b/apps/kimi-web/src/components/chat/ChatDock.vue index 8da9067d2..ab84330a3 100644 --- a/apps/kimi-web/src/components/chat/ChatDock.vue +++ b/apps/kimi-web/src/components/chat/ChatDock.vue @@ -20,6 +20,10 @@ import Pill from '../ui/Pill.vue'; const props = defineProps<{ sessionId?: string; running?: boolean; + /** True while the empty-composer first prompt is being created + submitted. + * Covers the gap where draft-session creation already selected the new + * session (empty state → dock) before the first prompt is submitted. */ + starting?: boolean; queued?: QueuedPromptView[]; searchFiles?: (q: string) => Promise; uploadImage?: (file: Blob, name?: string) => Promise<{ fileId: string; name: string; mediaType: string } | null>; @@ -256,6 +260,7 @@ defineExpose({ loadForEdit, loadAttachmentsForEdit, focus }); :models="models" :starred-ids="starredIds" :skills="skills" + :starting="starting" @submit="emit('submit', $event)" @steer="emit('steer', $event)" @command="emit('command', $event)" diff --git a/apps/kimi-web/src/components/chat/Composer.vue b/apps/kimi-web/src/components/chat/Composer.vue index 92031302e..2b3968788 100644 --- a/apps/kimi-web/src/components/chat/Composer.vue +++ b/apps/kimi-web/src/components/chat/Composer.vue @@ -35,6 +35,9 @@ import Tooltip from '../ui/Tooltip.vue'; const props = withDefaults(defineProps<{ running?: boolean; + /** True while the empty-composer first prompt is being created + submitted. + * Disables the textarea and swaps the send button for a spinner. */ + starting?: boolean; /** Active session id — scopes the persisted unsent draft (per session). */ sessionId?: string; queued?: QueuedPromptView[]; @@ -59,6 +62,7 @@ const props = withDefaults(defineProps<{ hideContext?: boolean; }>(), { running: false, + starting: false, queued: () => [], searchFiles: undefined, uploadImage: undefined, @@ -68,11 +72,13 @@ const props = withDefaults(defineProps<{ }); const placeholder = computed(() => - props.running - ? t('composer.placeholderRunning') - : props.goalMode - ? t('status.goalPlaceholder') - : t('composer.placeholder') + props.starting + ? t('composer.starting') + : props.running + ? t('composer.placeholderRunning') + : props.goalMode + ? t('status.goalPlaceholder') + : t('composer.placeholder') ); const emit = defineEmits<{ @@ -913,6 +919,7 @@ function selectModel(modelId: string): void { v-model="text" class="ph" :placeholder="placeholder" + :disabled="starting" rows="1" @keydown="handleKeydown" @compositionstart="handleCompositionStart" @@ -1131,10 +1138,13 @@ function selectModel(modelId: string): void { @@ -1520,6 +1530,25 @@ function selectModel(modelId: string): void { transform: scale(0.92); } +.send:disabled { + cursor: not-allowed; + opacity: 0.88; +} + +.send:disabled:active { + transform: none; +} + +/* Spinner-on-accent: recolor the ring so the arc reads on the accent fill. + Spinner.vue styles are scoped, so pierce them with :deep(). */ +.send.is-starting :deep(.ui-spinner) { + color: var(--color-text-on-accent); +} + +.send.is-starting :deep(.ui-spinner__track) { + stroke: rgba(255, 255, 255, 0.32); +} + .send svg { flex: none; width: var(--p-ic-lg); diff --git a/apps/kimi-web/src/components/chat/ConversationPane.vue b/apps/kimi-web/src/components/chat/ConversationPane.vue index 88fdc5dce..7c7f47511 100644 --- a/apps/kimi-web/src/components/chat/ConversationPane.vue +++ b/apps/kimi-web/src/components/chat/ConversationPane.vue @@ -12,6 +12,7 @@ import Composer from './Composer.vue'; import ChatDock from './ChatDock.vue'; import ConversationToc, { type ConversationTocItem } from './ConversationToc.vue'; import Icon from '../ui/Icon.vue'; +import Spinner from '../ui/Spinner.vue'; import Tooltip from '../ui/Tooltip.vue'; import { getVisibleWorkspaces } from '../../lib/workspacePicker'; import { safeRemove, STORAGE_KEYS } from '../../lib/storage'; @@ -48,6 +49,9 @@ const props = defineProps<{ /** Cache-buster that remounts the chat pane when the active session changes. */ fileReloadKey?: string | number; sending?: boolean; + /** True while the empty-composer first prompt is being created + submitted. + * Drives the empty-session "starting conversation…" loading state. */ + starting?: boolean; fastMoon?: boolean; /** Mobile shell: compact chrome. */ mobile?: boolean; @@ -1134,10 +1138,14 @@ defineExpose({ loadComposerForEdit, focusComposer });
- {{ t('composer.emptyConversationTitle') }} - {{ t('composer.emptyConversation') }} - -
+ + + {{ starting ? t('conversation.starting') : t('composer.emptyConversationTitle') }} + + {{ t('composer.emptyConversation') }} + +