mirror of
https://github.com/MoonshotAI/kimi-code.git
synced 2026-08-26 09:05:46 +00:00
feat(kimi-web): show empty-workspace hint in sidebar and centred quick-start input in chat
This commit is contained in:
parent
0574a43faf
commit
054c08ea75
8 changed files with 144 additions and 10 deletions
|
|
@ -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)"
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -22,4 +22,5 @@ export default {
|
|||
tabFiles: 'files',
|
||||
tabTasks: 'tasks',
|
||||
tabTodo: 'todo',
|
||||
noSessions: 'No conversations yet',
|
||||
} as const;
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ export default {
|
|||
interrupt: '中断',
|
||||
interruptTitle: '中断当前操作',
|
||||
emptyConversation: '还没有消息 —— 在下方输入开始对话。',
|
||||
quickStartPlaceholder: '输入消息开始新对话…',
|
||||
thinkingSuffix: ' · thinking',
|
||||
|
||||
} as const;
|
||||
|
|
|
|||
|
|
@ -22,4 +22,5 @@ export default {
|
|||
tabFiles: '文件',
|
||||
tabTasks: '后台任务',
|
||||
tabTodo: '待办',
|
||||
noSessions: '暂无对话',
|
||||
};
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue