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/quiet-goals.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@moonshot-ai/kimi-code": patch
---

web: Refine goal mode controls with animated strip interactions, budget-aware progress, and design-system cancellation confirmation.
3 changes: 2 additions & 1 deletion apps/kimi-web/src/components/chat/Composer.vue
Original file line number Diff line number Diff line change
Expand Up @@ -1236,7 +1236,8 @@ function selectModel(modelId: string): void {
--composer-send-inset: var(--space-2);
position: relative;
border: 1px solid var(--line);
border-radius: calc((var(--composer-send-size) / 2) + var(--composer-send-inset));
border-radius: calc((var(--composer-send-size) / 2) + var(--composer-send-inset) + var(--space-3));
corner-shape: superellipse(1.5);
background: var(--bg);
box-shadow: var(--shadow-md);
transition: border-color 0.15s, box-shadow 0.15s;
Expand Down
99 changes: 87 additions & 12 deletions apps/kimi-web/src/components/chat/GoalStrip.vue
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import { computed, ref, watch } from 'vue';
import { useI18n } from 'vue-i18n';
import type { AppGoal } from '../../api/types';
import { useConfirmDialog } from '../../composables/useConfirmDialog';
import Card from '../ui/Card.vue';
import Badge from '../ui/Badge.vue';
import Button from '../ui/Button.vue';
Expand All @@ -11,6 +12,7 @@ const props = defineProps<{ goal: AppGoal; forceExpanded?: number }>();
const emit = defineEmits<{ controlGoal: [action: 'pause' | 'resume' | 'cancel'] }>();

const { t } = useI18n();
const { confirm } = useConfirmDialog();

const expanded = ref(false);

Expand Down Expand Up @@ -45,36 +47,52 @@ function formatMs(ms: number): string {
const hour = Math.floor(min / 60);
return `${hour}h ${min % 60}m`;
}

async function onCancel(): Promise<void> {
const confirmed = await confirm({
title: t('status.goalCancel'),
message: t('status.goalCancelConfirm'),
confirmLabel: t('status.goalCancelConfirmYes'),
cancelLabel: t('status.goalCancelConfirmNo'),
variant: 'danger',
});
if (confirmed) emit('controlGoal', 'cancel');
}
</script>

<template>
<Card class="goal-strip" :class="{ expanded }">
<template #head>
<button class="goal-row" type="button" @click="expanded = !expanded">
<Icon class="goal-icon" name="target" size="md" />
<span class="goal-kicker">{{ t('status.goalLabel') }}</span>
<span class="goal-objective" :class="{ 'expanded-hidden': expanded }">{{ goal.objective }}</span>
<Badge
:variant="goal.status === 'active' ? 'success' : goal.status === 'blocked' ? 'danger' : goal.status === 'paused' ? 'warning' : 'neutral'"
size="sm"
class="goal-status"
>{{ goalStatusLabel(goal.status) }}</Badge>
<span class="goal-progress" aria-hidden="true">
<span v-if="goal.budget.tokenBudget !== null" class="goal-progress" aria-hidden="true">
<span class="goal-progress-fill" :style="{ width: `${tokenPct}%` }"></span>
</span>
<Icon class="goal-chevron" :class="{ open: expanded }" name="chevron-right" size="md" />
</button>
</template>

<template v-if="expanded" #default>
<template #default>
<div class="goal-full">{{ goal.objective }}</div>
<div v-if="goal.completionCriterion" class="goal-criterion">
<span>Done when</span>
<p>{{ goal.completionCriterion }}</p>
</div>
</template>

<template v-if="expanded" #foot>
<div class="goal-footer">
<template #foot>
Comment thread
chengluyu marked this conversation as resolved.
<div
class="goal-footer"
:inert="!expanded"
:aria-hidden="!expanded"
>
<div class="goal-meta">
<span>{{ goal.turnsUsed }} turns</span>
<span>{{ goal.tokensUsed.toLocaleString() }} tokens</span>
Expand Down Expand Up @@ -106,7 +124,7 @@ function formatMs(ms: number): string {
size="sm"
variant="danger-soft"
class="goal-action"
@click.stop="emit('controlGoal', 'cancel')"
@click.stop="onCancel"
>
<Icon name="close" size="md" />
<span>{{ t('status.goalCancel') }}</span>
Expand All @@ -121,10 +139,17 @@ function formatMs(ms: number): string {
.goal-strip {
--composer-send-size: 32px;
--composer-send-inset: var(--space-2);
--goal-corner-radius: calc((var(--composer-send-size) / 2) + var(--composer-send-inset) + var(--space-3));
margin: var(--space-2) var(--space-4) 0;
box-shadow: var(--shadow-md);
}
.goal-strip.ui-card {
border-radius: calc((var(--composer-send-size) / 2) + var(--composer-send-inset));
border-radius: var(--goal-corner-radius);
corner-shape: superellipse(1.5);
}
.goal-strip:not(.expanded).ui-card {
border-radius: var(--radius-full);
corner-shape: round;
}
.goal-strip :deep(.ui-card__foot) {
padding: var(--composer-send-inset);
Expand All @@ -134,10 +159,31 @@ function formatMs(ms: number): string {
.goal-strip :deep(.ui-card__foot) {
padding-left: calc((var(--composer-send-inset) + var(--composer-send-size)) / 2);
}
/* When collapsed the body/foot slots are not rendered; collapse the (always-
rendered) Card body and drop the head border so the strip is a single row. */
.goal-strip:not(.expanded) :deep(.ui-card__body) { display: none; }
.goal-strip:not(.expanded) :deep(.ui-card__head) { border-bottom: none; }
.goal-strip :deep(.ui-card__body) {
background: var(--color-surface-raised);
max-height: 480px;
overflow: hidden;
opacity: 1;
transition: max-height var(--duration-slow) var(--ease-out),
padding-top var(--duration-slow) var(--ease-out),
padding-bottom var(--duration-slow) var(--ease-out),
opacity var(--duration-base) var(--ease-out);
}
/* Collapse the body and footer while keeping both mounted so their height and
padding can animate instead of jumping between two layouts. */
.goal-strip:not(.expanded) :deep(.ui-card__body) {
max-height: 0;
padding-top: 0;
padding-bottom: 0;
opacity: 0;
}
.goal-strip :deep(.ui-card__head) {
border-bottom-color: var(--color-line);
transition: border-bottom-color var(--duration-base) var(--ease-out);
}
.goal-strip:not(.expanded) :deep(.ui-card__head) {
border-bottom-color: transparent;
}

.goal-row {
width: 100%;
Expand All @@ -158,6 +204,11 @@ function formatMs(ms: number): string {
font: var(--text-base)/var(--leading-normal) var(--font-ui);
font-weight: var(--weight-semibold);
}
.goal-icon {
flex: none;
color: var(--color-success);
margin-right: calc(-1 * var(--space-1));
}
.goal-objective {
min-width: 0;
flex: 1;
Expand Down Expand Up @@ -201,7 +252,7 @@ function formatMs(ms: number): string {
}
.goal-full {
color: var(--color-text);
font-size: var(--text-sm);
font-size: var(--text-base);
line-height: var(--leading-normal);
white-space: pre-wrap;
overflow-wrap: anywhere;
Expand All @@ -226,13 +277,30 @@ function formatMs(ms: number): string {
width: 100%;
min-width: 0;
}
.goal-strip :deep(.ui-card__foot) {
max-height: 100px;
overflow: hidden;
opacity: 1;
transition: max-height var(--duration-slow) var(--ease-out),
padding-top var(--duration-slow) var(--ease-out),
padding-bottom var(--duration-slow) var(--ease-out),
opacity var(--duration-base) var(--ease-out),
border-top-color var(--duration-base) var(--ease-out);
}
.goal-strip:not(.expanded) :deep(.ui-card__foot) {
max-height: 0;
padding-top: 0;
padding-bottom: 0;
border-top-color: transparent;
opacity: 0;
}
.goal-meta {
min-width: 0;
display: flex;
flex-wrap: wrap;
gap: var(--space-2);
color: var(--color-text-muted);
font: 12px/var(--leading-normal) var(--font-ui);
font: var(--text-xs)/var(--leading-normal) var(--font-ui);
font-weight: 450;
font-variant-numeric: tabular-nums;
}
Expand Down Expand Up @@ -261,4 +329,11 @@ function formatMs(ms: number): string {
display: none;
}
}
@media (prefers-reduced-motion: reduce) {
.goal-strip :deep(.ui-card__head),
.goal-strip :deep(.ui-card__body),
.goal-strip :deep(.ui-card__foot) {
transition: none;
}
}
</style>
3 changes: 3 additions & 0 deletions apps/kimi-web/src/i18n/locales/en/status.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@ export default {
goalPause: 'Pause',
goalResume: 'Resume',
goalCancel: 'Cancel',
goalCancelConfirm: 'Cancel this goal? It cannot be resumed afterwards.',
goalCancelConfirmYes: 'Yes',
goalCancelConfirmNo: 'No',
goalStatusActive: 'Active',
goalStatusPaused: 'Paused',
goalStatusBlocked: 'Blocked',
Expand Down
3 changes: 3 additions & 0 deletions apps/kimi-web/src/i18n/locales/zh/status.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@ export default {
goalPause: '暂停',
goalResume: '继续',
goalCancel: '取消',
goalCancelConfirm: '是否需要取消当前目标?取消后将无法恢复。',
goalCancelConfirmYes: '是',
goalCancelConfirmNo: '否',
goalStatusActive: '进行中',
goalStatusPaused: '已暂停',
goalStatusBlocked: '已阻塞',
Expand Down
6 changes: 6 additions & 0 deletions docs/en/guides/goals.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,12 @@ A goal can stop in three ways:

Write stop conditions into the objective. `/goal` does not have a separate stop-limit flag.

## Manage goals in the web UI

The web UI shows the current goal in a strip below the conversation. Select the strip to expand or collapse its details. When a token budget is configured, the header shows its progress; goals without a token budget do not show a progress bar.

Use the strip actions to pause an active goal, resume a paused or blocked goal, or cancel the current goal. Cancellation requires confirmation because it cannot be resumed afterwards.

## Queue upcoming goals

Agents sometimes complete a goal too quickly. Users can be disappointed that they can assign only one goal at a time. Many people already know the upcoming goals they want to pursue. They had to wait for the current goal to complete, open the TUI, and submit the next goal manually.
Expand Down
6 changes: 6 additions & 0 deletions docs/zh/guides/goals.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,12 @@ Kimi Code 会保存该目标,把它作为下一条用户消息发送,并进

停止条件需要写在目标本身里。`/goal` 没有单独用于描述停止限制的语法。

## 在 Web 界面中管理目标

Web 界面会在对话下方显示当前目标条。点击目标条可以展开或收起详细信息。配置 token 预算时,标题栏会显示预算进度;没有配置 token 预算的目标不会显示进度条。

使用目标条中的操作可以暂停进行中的目标、继续已暂停或已阻塞的目标,或取消当前目标。取消操作需要确认,因为取消后无法继续。

## 安排后续目标

Agent 有时会很快完成一个目标。如果一次只能安排一个目标,用户可能会失望。很多人已经知道接下来想完成哪些后续目标,但原来需要等当前目标完成后,打开 TUI,再手动提交下一个目标。
Expand Down
Loading