Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
5 changes: 5 additions & 0 deletions .changeset/focus-new-session-input.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@moonshot-ai/kimi-code": patch
---

In the bundled web UI, `/new` and `/clear` are now aliases that open the session onboarding composer and focus the input. iOS auto-zoom is prevented by keeping text inputs at 16px instead of disabling viewport scaling.
12 changes: 11 additions & 1 deletion apps/kimi-web/src/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -394,9 +394,11 @@ function handleCommand(cmd: string): void {
return;
}
switch (cmd) {
// `/new` and `/clear` are aliases: both open the onboarding composer (or
// the new-session dialog when no workspace is active).
case '/new':
case '/clear':
showNewSession.value = true;
handleCreateSession();
break;
case '/sessions':
showSessions.value = true;
Expand Down Expand Up @@ -496,13 +498,20 @@ function handleCloseAddWorkspace(): void {
showAddWorkspace.value = false;
}

function focusComposerAfterDraft(): void {
void nextTick(() => {
conversationPaneRef.value?.focusComposer();
});
}

// Primary "+ New": enter the draft state in the current workspace so the
// right pane shows the onboarding composer. The session is only created when
// the user sends the first message.
function handleCreateSession(): void {
const wsId = client.activeWorkspaceId.value;
if (wsId) {
client.openWorkspaceDraft(wsId);
focusComposerAfterDraft();
} else {
showNewSession.value = true;
}
Expand All @@ -513,6 +522,7 @@ function handleCreateSession(): void {
// actually sends a message.
function handleCreateSessionInWorkspace(workspaceId: string): void {
client.openWorkspaceDraft(workspaceId);
focusComposerAfterDraft();
}

// Chat header: open a GitHub PR in a new tab.
Expand Down
8 changes: 6 additions & 2 deletions apps/kimi-web/src/components/chat/ChatDock.vue
Original file line number Diff line number Diff line change
Expand Up @@ -75,14 +75,18 @@ const emit = defineEmits<{
}>();

const { t } = useI18n();
const composerRef = ref<{ loadForEdit: (value: string) => void } | null>(null);
const composerRef = ref<{ loadForEdit: (value: string) => void; focus: () => void } | null>(null);
const workPanelRef = ref<HTMLElement | null>(null);
const workbarRef = ref<HTMLElement | null>(null);

function loadForEdit(value: string): void {
composerRef.value?.loadForEdit(value);
}

function focus(): void {
composerRef.value?.focus();
}

function handleEditQueued(index: number): void {
const text = props.queued?.[index]?.text ?? '';
if (text) loadForEdit(text);
Expand Down Expand Up @@ -114,7 +118,7 @@ onUnmounted(() => {
}
});

defineExpose({ loadForEdit });
defineExpose({ loadForEdit, focus });
</script>

<template>
Expand Down
7 changes: 6 additions & 1 deletion apps/kimi-web/src/components/chat/Composer.vue
Original file line number Diff line number Diff line change
Expand Up @@ -257,7 +257,12 @@ onUnmounted(() => {
// ---------------------------------------------------------------------------

// loadForEdit comes from useComposerDraft (it lives next to the text state).
defineExpose({ loadForEdit });
function focus(): void {
// preventScroll keeps the pane from jumping if the composer is already in view
// or if focus is triggered during an animation/transition.
textareaRef.value?.focus({ preventScroll: true });
}
defineExpose({ loadForEdit, focus });

function handleSubmit(): void {
const trimmed = text.value.trim();
Expand Down
23 changes: 17 additions & 6 deletions apps/kimi-web/src/components/chat/ConversationPane.vue
Original file line number Diff line number Diff line change
Expand Up @@ -178,8 +178,8 @@ const { t } = useI18n();
safeRemove(STORAGE_KEYS.contentAlign);

const chatPaneRef = ref<InstanceType<typeof ChatPane> | null>(null);
const emptyComposerRef = ref<{ loadForEdit: (v: string) => void } | null>(null);
const dockedComposerRef = ref<{ loadForEdit: (v: string) => void } | null>(null);
const emptyComposerRef = ref<ComposerHandle | null>(null);
const dockedComposerRef = ref<ComposerHandle | null>(null);
const copyConversationCopied = ref(false);
const goalExpandSignal = ref(0);
let copyConversationCopiedTimer: ReturnType<typeof setTimeout> | null = null;
Expand Down Expand Up @@ -355,7 +355,7 @@ const dockHeight = ref(0);
const chatDockStyle = computed(() => ({
'--panes-scrollbar-width': `${panesScrollbarWidth.value}px`,
}));
type ComposerHandle = { loadForEdit: (value: string) => void };
type ComposerHandle = { loadForEdit: (value: string) => void; focus: () => void };
type RefArg = Element | (ComponentPublicInstance & Partial<ComposerHandle>) | null;

function toHtmlEl(el: RefArg): HTMLElement | null {
Expand All @@ -379,8 +379,15 @@ function bindChatPane(el: RefArg): void {
function bindChatDock(el: RefArg): void {
const node = toHtmlEl(el);
dockRef.value = node ?? null;
if (el && 'loadForEdit' in el && typeof el.loadForEdit === 'function') {
dockedComposerRef.value = { loadForEdit: el.loadForEdit.bind(el) };
if (
el &&
'loadForEdit' in el && typeof el.loadForEdit === 'function' &&
'focus' in el && typeof el.focus === 'function'
) {
dockedComposerRef.value = {
loadForEdit: el.loadForEdit.bind(el),
focus: el.focus.bind(el),
};
} else {
dockedComposerRef.value = null;
}
Expand Down Expand Up @@ -848,7 +855,11 @@ onUnmounted(() => {
}
});

defineExpose({ loadComposerForEdit });
function focusComposer(): void {
(dockedComposerRef.value ?? emptyComposerRef.value)?.focus();
}

defineExpose({ loadComposerForEdit, focusComposer });
</script>

<template>
Expand Down
23 changes: 17 additions & 6 deletions apps/kimi-web/src/components/dialogs/NewSessionDialog.vue
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ const emit = defineEmits<{

const cwdInput = ref('');
const titleInput = ref('');
const cwdInputEl = ref<HTMLInputElement | null>(null);

// Pre-fill with the first recentCwd if available
watch(
Expand Down Expand Up @@ -57,22 +58,29 @@ function pickRecent(cwd: string): void {
// Keyboard
// -------------------------------------------------------------------------

function handleKeydown(e: KeyboardEvent): void {
function handleEnter(e: KeyboardEvent): void {
if (e.key === 'Enter' && !(e.target instanceof HTMLButtonElement)) {
handleCreate();
}
}

function handleEscape(e: KeyboardEvent): void {
if (e.key === 'Escape') {
emit('close');
} else if (e.key === 'Enter' && !(e.target instanceof HTMLButtonElement)) {
handleCreate();
}
}

onMounted(() => document.addEventListener('keydown', handleKeydown));
onUnmounted(() => document.removeEventListener('keydown', handleKeydown));
onMounted(() => {
cwdInputEl.value?.focus();
document.addEventListener('keydown', handleEscape);
});
onUnmounted(() => document.removeEventListener('keydown', handleEscape));
</script>

<template>
<!-- Backdrop -->
<div class="backdrop" @click.self="emit('close')">
<div class="dialog" role="dialog" :aria-label="t('newSession.title')">
<div class="dialog" role="dialog" :aria-label="t('newSession.title')" @keydown="handleEnter">

<!-- Header -->
<div class="dh">
Expand All @@ -92,6 +100,7 @@ onUnmounted(() => document.removeEventListener('keydown', handleKeydown));
<label class="flabel" for="ns-cwd">{{ t('newSession.cwdLabel') }}</label>
<input
id="ns-cwd"
ref="cwdInputEl"
v-model="cwdInput"
class="finput"
type="text"
Expand Down Expand Up @@ -364,6 +373,8 @@ onUnmounted(() => document.removeEventListener('keydown', handleKeydown));
.finput {
width: 100%;
box-sizing: border-box;
/* Pinned at 16px to prevent iOS auto-zoom on focus. */
font-size: 16px;
}
.recent-section {
padding-left: 0;
Expand Down
Loading