feat(kimi-web): show empty-workspace hint in sidebar and centred quick-start input in chat

This commit is contained in:
qer 2026-06-10 21:20:08 +08:00 committed by haozhe.yang
parent 0574a43faf
commit 054c08ea75
8 changed files with 144 additions and 10 deletions

View file

@ -45,6 +45,10 @@ const activeWorkspaceSessionCount = computed<number>(
() => client.sessionsForView.value.length,
);
// True when the active workspace has no sessions drives the centred input
// placeholder in ChatPane so the user can start typing immediately.
const workspaceEmpty = computed<boolean>(() => activeWorkspaceSessionCount.value === 0);
// Thinking is on/off (TUI parity no effort-level cycling). The /thinking
// command flips between off and the backend default effort ('high').
function nextThinkingLevel(current: ThinkingLevel): ThinkingLevel {
@ -250,7 +254,11 @@ function handleEditQueued(index: number): void {
client.unqueue(index);
}
function handleSubmit(payload: { text: string; attachments: { fileId: string }[] }): void {
async function handleSubmit(payload: { text: string; attachments: { fileId: string }[] }): Promise<void> {
const wsId = client.activeWorkspaceId.value;
if (wsId && workspaceEmpty.value) {
await client.createSessionInWorkspace(wsId);
}
void client.sendPrompt(payload.text, payload.attachments);
}
@ -354,6 +362,7 @@ function handleCreateSession(): void {
:changes-by-path="client.changesByPath.value"
:file-reload-key="client.activeSessionId.value"
:session-loading="client.sessionLoading.value"
:workspace-empty="workspaceEmpty"
@submit="handleSubmit($event)"
@approval="(approvalId, response) => client.respondApproval(approvalId, response)"
@cancel-task="client.cancelTask($event)"

View file

@ -65,8 +65,13 @@ const props = withDefaults(
* the empty-conversation state.
*/
sessionLoading?: boolean;
/**
* True when the active workspace has no sessions shows a centred input
* placeholder so the user can start typing immediately.
*/
workspaceEmpty?: boolean;
}>(),
{ approvals: () => [], bubble: false, mobile: false, running: false, sending: false },
{ approvals: () => [], bubble: false, mobile: false, running: false, sending: false, workspaceEmpty: false },
);
// Bubble layout is active on phones AND on the Modern desktop theme. ThinkingBlock
@ -87,11 +92,30 @@ const emit = defineEmits<{
/** The user toggled a process fold the scroller must NOT auto-follow to
the bottom, or the fold visually expands upward from a pinned bottom. */
foldToggle: [];
/** Centred quick-start submit (workspace has no sessions yet). */
submit: [payload: { text: string; attachments: { fileId: string }[] }];
}>();
// Per-turn copy button state (keyed by turn id)
const copiedTurn = ref<string | null>(null);
// Centred quick-start textarea for empty workspaces
const quickText = ref('');
function handleQuickSubmit(): void {
const text = quickText.value.trim();
if (!text) return;
quickText.value = '';
emit('submit', { text, attachments: [] });
}
function onQuickKeydown(e: KeyboardEvent): void {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
handleQuickSubmit();
}
}
/** Assemble the full content of a turn for copying follows the ordered
blocks so thinking/text/tool output copy in the order they happened. */
function turnPlainText(turn: ChatTurn): string {
@ -226,10 +250,28 @@ function processSummary(turn: ChatTurn): string {
<span class="chat-loading-text">{{ t('conversation.loading') }}</span>
</div>
<div v-else-if="turns.length === 0 && (!approvals || approvals.length === 0)" class="chat-empty">
<svg viewBox="0 0 24 24" width="30" height="30" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<path d="M21 11.5a8.38 8.38 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.38 8.38 0 0 1-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.38 8.38 0 0 1 3.8-.9h.5a8.48 8.48 0 0 1 8 8v.5z" />
</svg>
<div class="chat-empty-text">{{ t('composer.emptyConversation') }}</div>
<template v-if="workspaceEmpty">
<div class="quick-start">
<textarea
v-model="quickText"
class="quick-ta"
:placeholder="t('composer.quickStartPlaceholder')"
rows="1"
@keydown="onQuickKeydown"
/>
<button class="quick-send" :disabled="!quickText.trim()" @click="handleQuickSubmit">
<svg viewBox="0 0 16 16" width="14" height="14" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<path d="M2 8h12M9 3l5 5-5 5"/>
</svg>
</button>
</div>
</template>
<template v-else>
<svg viewBox="0 0 24 24" width="30" height="30" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<path d="M21 11.5a8.38 8.38 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.38 8.38 0 0 1-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.38 8.38 0 0 1 3.8-.9h.5a8.48 8.48 0 0 1 8 8v.5z" />
</svg>
<div class="chat-empty-text">{{ t('composer.emptyConversation') }}</div>
</template>
</div>
<template v-for="turn in turns" :key="turn.id">
@ -314,10 +356,28 @@ function processSummary(turn: ChatTurn): string {
</div>
<!-- Empty state: a fresh/empty session shows a hint instead of a blank pane -->
<div v-else-if="turns.length === 0 && (!approvals || approvals.length === 0)" class="chat-empty">
<svg viewBox="0 0 24 24" width="30" height="30" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<path d="M21 11.5a8.38 8.38 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.38 8.38 0 0 1-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.38 8.38 0 0 1 3.8-.9h.5a8.48 8.48 0 0 1 8 8v.5z" />
</svg>
<div class="chat-empty-text">{{ t('composer.emptyConversation') }}</div>
<template v-if="workspaceEmpty">
<div class="quick-start">
<textarea
v-model="quickText"
class="quick-ta"
:placeholder="t('composer.quickStartPlaceholder')"
rows="1"
@keydown="onQuickKeydown"
/>
<button class="quick-send" :disabled="!quickText.trim()" @click="handleQuickSubmit">
<svg viewBox="0 0 16 16" width="14" height="14" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<path d="M2 8h12M9 3l5 5-5 5"/>
</svg>
</button>
</div>
</template>
<template v-else>
<svg viewBox="0 0 24 24" width="30" height="30" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<path d="M21 11.5a8.38 8.38 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.38 8.38 0 0 1-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.38 8.38 0 0 1 3.8-.9h.5a8.48 8.48 0 0 1 8 8v.5z" />
</svg>
<div class="chat-empty-text">{{ t('composer.emptyConversation') }}</div>
</template>
</div>
<template v-for="turn in turns" :key="turn.id">
@ -428,6 +488,54 @@ function processSummary(turn: ChatTurn): string {
}
.chat-empty-text { font-size: 13px; }
/* Centred quick-start input for empty workspaces */
.quick-start {
display: flex;
align-items: center;
gap: 8px;
width: 100%;
max-width: 480px;
padding: 8px 12px;
background: var(--bg);
border: 1px solid var(--line);
border-radius: 8px;
box-shadow: 0 1px 3px rgba(0,0,0,0.04);
}
.quick-ta {
flex: 1;
min-width: 0;
border: none;
outline: none;
background: transparent;
resize: none;
font-size: 14px;
line-height: 1.5;
color: var(--ink);
font-family: var(--mono);
max-height: 120px;
}
.quick-ta::placeholder { color: var(--faint); }
.quick-send {
flex: none;
width: 32px;
height: 32px;
display: flex;
align-items: center;
justify-content: center;
background: var(--blue);
border: none;
border-radius: 6px;
color: #fff;
cursor: pointer;
transition: background 0.15s;
}
.quick-send:hover:not(:disabled) { background: var(--blue2); }
.quick-send:disabled {
background: var(--line);
color: var(--faint);
cursor: not-allowed;
}
.chat-loading {
flex: 1;
display: flex;

View file

@ -55,6 +55,9 @@ const props = defineProps<{
sessionLoading?: boolean;
/** Available models for the quick-switch dropdown in the composer toolbar. */
models?: AppModel[];
/** True when the active workspace has no sessions shows a centred input
placeholder so the user can start typing immediately. */
workspaceEmpty?: boolean;
}>();
const emit = defineEmits<{
@ -505,8 +508,10 @@ onUnmounted(() => {
:running="running"
:sending="sending"
:session-loading="sessionLoading"
:workspace-empty="workspaceEmpty"
@approval-decide="handleApprovalDecide"
@fold-toggle="handleFoldToggle"
@submit="emit('submit', $event)"
/>
</div>
<TasksPane

View file

@ -405,6 +405,7 @@ function blinkOnce(): void {
@rename="(id, title) => emit('rename', id, title)"
@delete="emit('delete', $event)"
/>
<div v-if="g.sessions.length === 0" class="group-empty">{{ t('sidebar.noSessions') }}</div>
</div>
</div>
</template>
@ -756,6 +757,13 @@ function blinkOnce(): void {
}
.gh-add:hover { color: #666; }
.group-empty {
padding: 8px 10px 8px calc(var(--sb-gutter) + var(--sb-gap));
font-size: 12.5px;
color: var(--faint);
font-family: var(--mono);
}
/* Inline workspace rename input */
.gh-rename {
flex: 1;

View file

@ -14,6 +14,7 @@ export default {
interrupt: 'Interrupt',
interruptTitle: 'Interrupt current operation',
emptyConversation: 'No messages yet — type below to start the conversation.',
quickStartPlaceholder: 'Type a message to start a new conversation…',
thinkingSuffix: ' · thinking',
} as const;

View file

@ -22,4 +22,5 @@ export default {
tabFiles: 'files',
tabTasks: 'tasks',
tabTodo: 'todo',
noSessions: 'No conversations yet',
} as const;

View file

@ -14,6 +14,7 @@ export default {
interrupt: '中断',
interruptTitle: '中断当前操作',
emptyConversation: '还没有消息 —— 在下方输入开始对话。',
quickStartPlaceholder: '输入消息开始新对话…',
thinkingSuffix: ' · thinking',
} as const;

View file

@ -22,4 +22,5 @@ export default {
tabFiles: '文件',
tabTasks: '后台任务',
tabTodo: '待办',
noSessions: '暂无对话',
};