mirror of
https://github.com/MoonshotAI/kimi-code.git
synced 2026-07-09 17:29:12 +00:00
style(web): polish web UI typography and controls (#1502)
* style(web): polish sidebar and tool typography - Use UI font and medium weight for sidebar and composer controls - Add reusable shortcut and tool output blocks - Cap long tool output at 50 lines with a scrollbar - Update sidebar show-more copy and muted styling * style(web): refine workspace picker sizing * style(web): align composer mode menus * style(web): tune list and question typography * style(web): reuse shortcut keys in approvals * style(web): size workspace picker from content * feat(web): localize chat status labels * style(web): refine composer toolbar controls * style(web): use complete Inter variable font * style(web): tune sidebar workspace typography * style(web): polish composer and workspace picker * style(web): refine markdown and thinking typography * chore: add web UI polish changeset * fix(web): pin Inter package for Nix build * style(web): polish goal tool calls * style: polish goal mode display * fix: layer latest message pill below menus * style: align goal strip content
This commit is contained in:
parent
735922c291
commit
ad30a1c632
39 changed files with 941 additions and 256 deletions
5
.changeset/web-ui-polish.md
Normal file
5
.changeset/web-ui-polish.md
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
"@moonshot-ai/kimi-code": patch
|
||||
---
|
||||
|
||||
web: Polish the chat UI with Inter typography, localized labels, and tighter sidebar, composer, and menu styling.
|
||||
|
|
@ -13,7 +13,8 @@
|
|||
"check:style": "node scripts/check-style.mjs"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fontsource-variable/inter": "^5.2.8",
|
||||
"@chenglou/pretext": "0.0.8",
|
||||
"@fontsource-variable/inter": "5.2.8",
|
||||
"@fontsource-variable/jetbrains-mono": "^5.2.8",
|
||||
"@xterm/addon-fit": "^0.11.0",
|
||||
"@xterm/xterm": "^6.0.0",
|
||||
|
|
|
|||
|
|
@ -341,8 +341,9 @@ defineExpose({ closeMenu });
|
|||
|
||||
.t {
|
||||
color: inherit;
|
||||
font-size: var(--ui-font-size);
|
||||
font-weight: var(--weight-regular);
|
||||
font-size: var(--text-base);
|
||||
font-weight: 450;
|
||||
line-height: var(--leading-tight);
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
|
|
@ -353,7 +354,10 @@ defineExpose({ closeMenu });
|
|||
.ts {
|
||||
color: var(--color-text-faint);
|
||||
font-size: var(--text-xs);
|
||||
font-family: var(--font-mono);
|
||||
font-family: var(--font-ui);
|
||||
font-weight: 475;
|
||||
font-variant-numeric: tabular-nums;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
/* Trailing action slot: time and kebab share one grid cell (grid-area:1/1).
|
||||
|
|
@ -366,7 +370,7 @@ defineExpose({ closeMenu });
|
|||
display: inline-grid;
|
||||
flex: none;
|
||||
align-items: center;
|
||||
justify-items: center;
|
||||
justify-items: end;
|
||||
}
|
||||
.act .ts,
|
||||
.act .kebab { grid-area: 1 / 1; }
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ import IconButton from './ui/IconButton.vue';
|
|||
import Icon from './ui/Icon.vue';
|
||||
import Menu from './ui/Menu.vue';
|
||||
import MenuItem from './ui/MenuItem.vue';
|
||||
import ShortcutKey from './ui/ShortcutKey.vue';
|
||||
import { useConfirmDialog } from '../composables/useConfirmDialog';
|
||||
|
||||
const { t } = useI18n();
|
||||
|
|
@ -599,7 +600,10 @@ onBeforeUnmount(() => {
|
|||
<!-- Session search — opens the Spotlight-style search dialog -->
|
||||
<button class="search" type="button" @click="openSearch">
|
||||
<Icon class="search-icon" name="search" />
|
||||
<span class="search-input">{{ t('sidebar.searchShortcut', { shortcut: sessionSearchShortcut }) }}</span>
|
||||
<span class="search-input">
|
||||
<span class="search-label">{{ t('sidebar.searchPlaceholder') }}</span>
|
||||
<ShortcutKey>{{ sessionSearchShortcut }}</ShortcutKey>
|
||||
</span>
|
||||
</button>
|
||||
|
||||
<!-- New chat + new workspace buttons -->
|
||||
|
|
@ -911,6 +915,7 @@ onBeforeUnmount(() => {
|
|||
color: var(--color-text);
|
||||
font-family: var(--font-ui);
|
||||
font-size: var(--ui-font-size);
|
||||
font-weight: var(--weight-medium);
|
||||
cursor: pointer;
|
||||
text-align: left;
|
||||
}
|
||||
|
|
@ -950,15 +955,25 @@ onBeforeUnmount(() => {
|
|||
flex: none;
|
||||
}
|
||||
.search-input {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: var(--space-1);
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
color: var(--color-text);
|
||||
font-family: var(--mono);
|
||||
font-family: var(--font-ui);
|
||||
font-size: var(--ui-font-size);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.search-input > span {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.search-label {
|
||||
font-weight: var(--weight-medium);
|
||||
}
|
||||
|
||||
/* Sessions */
|
||||
.sessions {
|
||||
|
|
|
|||
|
|
@ -265,7 +265,7 @@ function onHeaderDragStart(event: DragEvent): void {
|
|||
|
||||
.gh-name {
|
||||
font-size: var(--ui-font-size-lg);
|
||||
font-weight: var(--weight-medium);
|
||||
font-weight: 550;
|
||||
color: var(--color-text);
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
|
|
@ -276,6 +276,7 @@ function onHeaderDragStart(event: DragEvent): void {
|
|||
}
|
||||
.gh-path {
|
||||
color: var(--color-text-faint);
|
||||
font-weight: 425;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
|
|
@ -317,10 +318,10 @@ function onHeaderDragStart(event: DragEvent): void {
|
|||
.gh-more.open { color: var(--color-text); background: var(--color-line); }
|
||||
|
||||
.group-empty {
|
||||
padding: var(--space-1) var(--space-2) var(--space-1) calc(var(--sb-pad-x) + var(--sb-gutter) + var(--sb-gap));
|
||||
padding: var(--space-1) var(--space-2) var(--space-1) calc(var(--sb-pad-x) - var(--space-2) + var(--sb-gutter) + var(--sb-gap));
|
||||
font-size: var(--text-xs);
|
||||
color: var(--color-text-faint);
|
||||
font-family: var(--font-mono);
|
||||
font-family: var(--font-ui);
|
||||
}
|
||||
/* Show-more / show-less — a session-row-shaped compact list control (§07). The
|
||||
empty lead slot mirrors a session row's status gutter, so the label text lands
|
||||
|
|
@ -338,7 +339,7 @@ function onHeaderDragStart(event: DragEvent): void {
|
|||
border: none;
|
||||
border-radius: var(--radius-md);
|
||||
background: transparent;
|
||||
color: var(--color-text);
|
||||
color: var(--color-text-muted);
|
||||
font-family: var(--font-ui);
|
||||
font-size: var(--text-xs);
|
||||
text-align: left;
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import Badge from '../ui/Badge.vue';
|
|||
import Button from '../ui/Button.vue';
|
||||
import IconButton from '../ui/IconButton.vue';
|
||||
import Icon from '../ui/Icon.vue';
|
||||
import ShortcutKey from '../ui/ShortcutKey.vue';
|
||||
import Tooltip from '../ui/Tooltip.vue';
|
||||
|
||||
const props = defineProps<{
|
||||
|
|
@ -314,20 +315,20 @@ onUnmounted(() => document.removeEventListener('keydown', handleKeydown));
|
|||
:loading="pendingAction === `option:${opt.label}`"
|
||||
:disabled="busy"
|
||||
@click="approveOption(opt.label)"
|
||||
>{{ opt.label }}<span class="k">[{{ i + 1 }}]</span></Button>
|
||||
>{{ opt.label }}<ShortcutKey class="k">[{{ i + 1 }}]</ShortcutKey></Button>
|
||||
</Tooltip>
|
||||
</template>
|
||||
<Button v-else class="kbtn" size="sm" variant="primary" :loading="pendingAction === 'approvePlan'" :disabled="busy" @click="approvePlan">{{ t('approval.approvePlan') }}<span class="k">[1]</span></Button>
|
||||
<Button class="kbtn" size="sm" variant="secondary" :disabled="busy" @click="revisePlan">{{ t('approval.revise') }}<span v-if="planReview.options.length === 0" class="k">[2]</span></Button>
|
||||
<Button class="kbtn" size="sm" variant="danger-soft" :loading="pendingAction === 'rejectAndExit'" :disabled="busy" @click="rejectAndExitPlan">{{ t('approval.rejectAndExit') }}<span v-if="planReview.options.length === 0" class="k">[3]</span></Button>
|
||||
<Button v-else class="kbtn" size="sm" variant="primary" :loading="pendingAction === 'approvePlan'" :disabled="busy" @click="approvePlan">{{ t('approval.approvePlan') }}<ShortcutKey class="k">[1]</ShortcutKey></Button>
|
||||
<Button class="kbtn" size="sm" variant="secondary" :disabled="busy" @click="revisePlan">{{ t('approval.revise') }}<ShortcutKey v-if="planReview.options.length === 0" class="k">[2]</ShortcutKey></Button>
|
||||
<Button class="kbtn" size="sm" variant="danger-soft" :loading="pendingAction === 'rejectAndExit'" :disabled="busy" @click="rejectAndExitPlan">{{ t('approval.rejectAndExit') }}<ShortcutKey v-if="planReview.options.length === 0" class="k">[3]</ShortcutKey></Button>
|
||||
</div>
|
||||
|
||||
<!-- default actions row -->
|
||||
<div v-else class="abtn">
|
||||
<Button class="kbtn" size="sm" variant="primary" :loading="pendingAction === 'approve'" :disabled="busy" @click="approve">{{ t('approval.approve') }}<span class="k">[1]</span></Button>
|
||||
<Button class="kbtn" size="sm" variant="secondary" :loading="pendingAction === 'approveSession'" :disabled="busy" @click="approveSession">{{ t('approval.approveSession') }}<span class="k">[2]</span></Button>
|
||||
<Button class="kbtn" size="sm" variant="secondary" :loading="pendingAction === 'reject'" :disabled="busy" @click="reject">{{ t('approval.reject') }}<span class="k">[3]</span></Button>
|
||||
<Button class="kbtn" size="sm" variant="secondary" :disabled="busy" @click="openFeedback">{{ t('approval.feedback') }}<span class="k">[4]</span></Button>
|
||||
<Button class="kbtn" size="sm" variant="primary" :loading="pendingAction === 'approve'" :disabled="busy" @click="approve">{{ t('approval.approve') }}<ShortcutKey class="k">[1]</ShortcutKey></Button>
|
||||
<Button class="kbtn" size="sm" variant="secondary" :loading="pendingAction === 'approveSession'" :disabled="busy" @click="approveSession">{{ t('approval.approveSession') }}<ShortcutKey class="k">[2]</ShortcutKey></Button>
|
||||
<Button class="kbtn" size="sm" variant="secondary" :loading="pendingAction === 'reject'" :disabled="busy" @click="reject">{{ t('approval.reject') }}<ShortcutKey class="k">[3]</ShortcutKey></Button>
|
||||
<Button class="kbtn" size="sm" variant="secondary" :disabled="busy" @click="openFeedback">{{ t('approval.feedback') }}<ShortcutKey class="k">[4]</ShortcutKey></Button>
|
||||
</div>
|
||||
</template>
|
||||
</Card>
|
||||
|
|
@ -554,7 +555,7 @@ onUnmounted(() => document.removeEventListener('keydown', handleKeydown));
|
|||
width: 100%;
|
||||
}
|
||||
.plan-actions { flex-wrap: wrap; }
|
||||
.k { margin-left: var(--space-2); font: var(--text-xs) var(--font-mono); opacity: .7; }
|
||||
.k { opacity: .75; }
|
||||
|
||||
/* =========================================================================
|
||||
MOBILE (≤640px): the card spans the full chat column, inner previews scroll
|
||||
|
|
|
|||
|
|
@ -251,6 +251,7 @@ defineExpose({ loadForEdit, loadAttachmentsForEdit, focus });
|
|||
:plan-mode="planMode"
|
||||
:swarm-mode="swarmMode"
|
||||
:goal-mode="goalMode"
|
||||
:goal="goal"
|
||||
:activation-badges="activationBadges"
|
||||
:models="models"
|
||||
:starred-ids="starredIds"
|
||||
|
|
|
|||
|
|
@ -51,6 +51,25 @@ const behind = computed(() => props.behind ?? 0);
|
|||
const adds = computed(() => props.gitDiffStats?.totalAdditions ?? 0);
|
||||
const dels = computed(() => props.gitDiffStats?.totalDeletions ?? 0);
|
||||
const hasLineStats = computed(() => adds.value > 0 || dels.value > 0);
|
||||
const PR_STATE_LABEL_KEYS: Record<string, string> = {
|
||||
open: 'header.prStatusOpen',
|
||||
closed: 'header.prStatusClosed',
|
||||
merged: 'header.prStatusMerged',
|
||||
draft: 'header.prStatusDraft',
|
||||
};
|
||||
|
||||
function normalizedPrState(state: string): string {
|
||||
return state.trim().toLowerCase().replaceAll('_', '-');
|
||||
}
|
||||
|
||||
function prStateClass(state: string): string {
|
||||
const stateClass = normalizedPrState(state);
|
||||
return PR_STATE_LABEL_KEYS[stateClass] ? `pr-${stateClass}` : 'pr-unknown';
|
||||
}
|
||||
|
||||
function prStateLabel(state: string): string {
|
||||
return t(PR_STATE_LABEL_KEYS[normalizedPrState(state)] ?? 'header.prStatusUnknown');
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// More-menu (kebab dropdown)
|
||||
|
|
@ -295,11 +314,11 @@ async function startArchive(): Promise<void> {
|
|||
v-if="pr"
|
||||
type="button"
|
||||
class="ch-pill ch-pr"
|
||||
:class="`pr-${pr.state}`"
|
||||
:class="prStateClass(pr.state)"
|
||||
@click="pr && emit('openPr', pr.url)"
|
||||
>
|
||||
<Icon name="git-pull-request" size="sm" />
|
||||
<span>PR #{{ pr.number }} · {{ pr.state }}</span>
|
||||
<span>PR #{{ pr.number }} · {{ prStateLabel(pr.state) }}</span>
|
||||
</button>
|
||||
|
||||
</header>
|
||||
|
|
@ -420,6 +439,7 @@ async function startArchive(): Promise<void> {
|
|||
.ch-pr.pr-merged { color: var(--color-done); border-color: var(--color-done-bd); background: var(--color-done-soft); }
|
||||
.ch-pr.pr-closed { color: var(--color-danger); border-color: var(--color-danger-bd); background: var(--color-danger-soft); }
|
||||
.ch-pr.pr-draft { color: var(--color-text-muted); border-color: var(--color-line-strong); background: var(--color-surface-sunken); }
|
||||
.ch-pr.pr-unknown { color: var(--color-text-muted); border-color: var(--color-line-strong); background: var(--color-surface-sunken); }
|
||||
.ch-pr:hover { border-color: var(--color-line-strong); }
|
||||
|
||||
/* Fixed more-menu, anchored to the kebab trigger. Surface / items come from
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
<!-- apps/kimi-web/src/components/chat/Composer.vue -->
|
||||
<script setup lang="ts">
|
||||
import { measureNaturalWidth, prepareWithSegments } from '@chenglou/pretext';
|
||||
import { computed, nextTick, onMounted, onUnmounted, ref, watch } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import SlashMenu from './SlashMenu.vue';
|
||||
|
|
@ -7,7 +8,7 @@ import MentionMenu from './MentionMenu.vue';
|
|||
import { buildSlashItems, parseSlash, SKILL_COMMAND_PREFIX } from '../../lib/slashCommands';
|
||||
import type { FileItem } from './MentionMenu.vue';
|
||||
import type { ActivationBadges, ConversationStatus, PermissionMode, QueuedPromptView } from '../../types';
|
||||
import type { AppModel, AppSkill, ThinkingLevel } from '../../api/types';
|
||||
import type { AppGoal, AppModel, AppSkill, ThinkingLevel } from '../../api/types';
|
||||
import {
|
||||
coerceThinkingForModel,
|
||||
commitLevel,
|
||||
|
|
@ -22,6 +23,7 @@ import { useMentionMenu } from '../../composables/useMentionMenu';
|
|||
import { useComposerDraft } from '../../composables/useComposerDraft';
|
||||
import { useAttachmentUpload } from '../../composables/useAttachmentUpload';
|
||||
import Spinner from '../ui/Spinner.vue';
|
||||
import Button from '../ui/Button.vue';
|
||||
import IconButton from '../ui/IconButton.vue';
|
||||
import Icon from '../ui/Icon.vue';
|
||||
import ContextRing from '../ui/ContextRing.vue';
|
||||
|
|
@ -45,6 +47,7 @@ const props = withDefaults(defineProps<{
|
|||
planMode?: boolean;
|
||||
swarmMode?: boolean;
|
||||
goalMode?: boolean;
|
||||
goal?: AppGoal | null;
|
||||
activationBadges?: ActivationBadges;
|
||||
/** Available models for the quick-switch dropdown. */
|
||||
models?: AppModel[];
|
||||
|
|
@ -94,7 +97,7 @@ const emit = defineEmits<{
|
|||
selectModel: [modelId: string];
|
||||
}>();
|
||||
|
||||
const { t } = useI18n();
|
||||
const { t, locale } = useI18n();
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Textarea + per-session draft persistence — see useComposerDraft.
|
||||
|
|
@ -646,12 +649,20 @@ function setThinkingSegment(draft: string): void {
|
|||
if (thinkingReadonly.value) return;
|
||||
emit('setThinking', commitLevel(currentModel.value, draft));
|
||||
}
|
||||
function thinkingSegmentLabel(segment: string): string {
|
||||
if (segment === 'on') return t('status.thinkingOn');
|
||||
if (segment === 'off') return t('status.thinkingOff');
|
||||
return effortLabel(segment);
|
||||
}
|
||||
|
||||
// Plan toggle
|
||||
const planOn = computed(() => props.planMode === true);
|
||||
const swarmOn = computed(() => props.swarmMode === true);
|
||||
const goalActive = computed(() => props.activationBadges?.goal !== null);
|
||||
const goalStatus = computed(() => props.goal?.status ?? props.activationBadges?.goal?.status ?? null);
|
||||
const goalActive = computed(() => goalStatus.value !== null && goalStatus.value !== 'complete');
|
||||
const goalArmed = computed(() => goalActive.value || props.goalMode === true);
|
||||
const goalCanPause = computed(() => goalStatus.value === 'active');
|
||||
const goalCanResume = computed(() => goalStatus.value === 'paused' || goalStatus.value === 'blocked');
|
||||
|
||||
// Modes selector (plan / goal / swarm) — the popover that replaces the bare
|
||||
// "plan" pill. Plan/Swarm are real client toggles; goal reflects agent-driven
|
||||
|
|
@ -696,6 +707,87 @@ const PERM_MODES: { mode: PermissionMode; color: string; labelKey: string; descK
|
|||
{ mode: 'yolo', color: 'var(--color-warning)', labelKey: 'status.permissionYolo', descKey: 'status.permissionYoloDesc' },
|
||||
{ mode: 'auto', color: 'var(--color-danger)', labelKey: 'status.permissionAuto', descKey: 'status.permissionAutoDesc' },
|
||||
];
|
||||
const MODE_DESC_KEYS = ['status.planDesc', 'status.swarmDesc', 'status.goalDesc'] as const;
|
||||
|
||||
const menuMeasureRef = ref<HTMLElement | null>(null);
|
||||
const permissionDescriptionWidth = ref('');
|
||||
const modeDescriptionWidth = ref('');
|
||||
function menuDescStyle(width: string): Record<string, string> {
|
||||
const style: Record<string, string> = {};
|
||||
if (width) style['--composer-menu-desc-width'] = width;
|
||||
return style;
|
||||
}
|
||||
const permissionMenuStyle = computed<Record<string, string>>(() => menuDescStyle(permissionDescriptionWidth.value));
|
||||
const modeMenuMeasureStyle = computed<Record<string, string>>(() => menuDescStyle(modeDescriptionWidth.value));
|
||||
const modesMenuInlineStyle = computed<Record<string, string>>(() => ({
|
||||
...modesMenuStyle.value,
|
||||
...modeMenuMeasureStyle.value,
|
||||
}));
|
||||
let menuMeasureFrame: number | null = null;
|
||||
|
||||
function cssPx(value: string): number {
|
||||
const n = Number.parseFloat(value);
|
||||
return Number.isFinite(n) ? n : 0;
|
||||
}
|
||||
|
||||
function canvasFont(style: CSSStyleDeclaration): string {
|
||||
return `${style.fontStyle || 'normal'} ${style.fontWeight || '400'} ${style.fontSize} ${style.fontFamily}`;
|
||||
}
|
||||
|
||||
function letterSpacingPx(style: CSSStyleDeclaration): number {
|
||||
return style.letterSpacing === 'normal' ? 0 : cssPx(style.letterSpacing);
|
||||
}
|
||||
|
||||
function measureTextWidth(text: string, style: CSSStyleDeclaration): number {
|
||||
if (!text) return 0;
|
||||
const prepared = prepareWithSegments(text, canvasFont(style), {
|
||||
letterSpacing: letterSpacingPx(style),
|
||||
});
|
||||
return measureNaturalWidth(prepared);
|
||||
}
|
||||
|
||||
function measureMenuDescriptions(): void {
|
||||
const probe = menuMeasureRef.value?.querySelector<HTMLElement>('.pd-desc');
|
||||
if (!probe) return;
|
||||
const style = getComputedStyle(probe);
|
||||
const permissionWidth = Math.max(
|
||||
0,
|
||||
...PERM_MODES.map((opt) => measureTextWidth(t(opt.descKey), style)),
|
||||
);
|
||||
const modeWidth = Math.max(
|
||||
0,
|
||||
...MODE_DESC_KEYS.map((key) => measureTextWidth(t(key), style)),
|
||||
);
|
||||
permissionDescriptionWidth.value = permissionWidth > 0 ? `${Math.ceil(permissionWidth)}px` : '';
|
||||
modeDescriptionWidth.value = modeWidth > 0 ? `${Math.ceil(modeWidth)}px` : '';
|
||||
}
|
||||
|
||||
function scheduleMenuDescriptionMeasure(): void {
|
||||
if (typeof window === 'undefined') return;
|
||||
if (menuMeasureFrame !== null) {
|
||||
window.cancelAnimationFrame(menuMeasureFrame);
|
||||
}
|
||||
void nextTick(() => {
|
||||
menuMeasureFrame = window.requestAnimationFrame(() => {
|
||||
menuMeasureFrame = null;
|
||||
measureMenuDescriptions();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
watch(locale, scheduleMenuDescriptionMeasure, { immediate: true });
|
||||
|
||||
onMounted(() => {
|
||||
scheduleMenuDescriptionMeasure();
|
||||
void document.fonts?.ready.then(scheduleMenuDescriptionMeasure);
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
if (menuMeasureFrame !== null) {
|
||||
window.cancelAnimationFrame(menuMeasureFrame);
|
||||
menuMeasureFrame = null;
|
||||
}
|
||||
});
|
||||
|
||||
function choosePermission(mode: PermissionMode): void {
|
||||
emit('setPermission', mode);
|
||||
|
|
@ -853,6 +945,10 @@ function selectModel(modelId: string): void {
|
|||
|
||||
<!-- Bottom toolbar — split into individual controls -->
|
||||
<div ref="toolbarRef" class="toolbar">
|
||||
<div ref="menuMeasureRef" class="menu-measure" aria-hidden="true">
|
||||
<span class="pd-desc" />
|
||||
</div>
|
||||
|
||||
<!-- Left: attach + permission + plan -->
|
||||
<div class="toolbar-left">
|
||||
<IconButton
|
||||
|
|
@ -877,7 +973,13 @@ function selectModel(modelId: string): void {
|
|||
>{{ permLabel }}</span>
|
||||
|
||||
<!-- Permission dropdown — anchored to the toolbar left side -->
|
||||
<div v-if="permDropdownOpen && status" class="perm-dropdown" role="menu" @click.stop>
|
||||
<div
|
||||
v-if="permDropdownOpen && status"
|
||||
class="perm-dropdown"
|
||||
:style="permissionMenuStyle"
|
||||
role="menu"
|
||||
@click.stop
|
||||
>
|
||||
<button
|
||||
v-for="opt in PERM_MODES"
|
||||
:key="opt.mode"
|
||||
|
|
@ -908,15 +1010,23 @@ function selectModel(modelId: string): void {
|
|||
<span v-if="goalArmed" class="mode-tag">{{ t('status.goalLabel') }}</span>
|
||||
</button>
|
||||
|
||||
<div v-if="modesOpen" ref="modesMenuRef" class="modes-menu" :style="modesMenuStyle">
|
||||
<div v-if="modesOpen" ref="modesMenuRef" class="modes-menu" :style="modesMenuInlineStyle" role="menu">
|
||||
<!-- Plan — functional client toggle -->
|
||||
<button type="button" class="mode-row" :class="{ on: planOn }" @click="emit('togglePlan')">
|
||||
<span class="mode-row-name">{{ t('status.planLabel') }}</span>
|
||||
<button type="button" class="mode-row" :class="{ on: planOn }" role="menuitem" @click="emit('togglePlan')">
|
||||
<span class="mode-row-icon"><Icon name="file-edit" size="sm" /></span>
|
||||
<span class="mode-row-info">
|
||||
<span class="mode-row-name">{{ t('status.planLabel') }}</span>
|
||||
<span class="mode-row-desc">{{ t('status.planDesc') }}</span>
|
||||
</span>
|
||||
<span class="mode-switch" :class="{ on: planOn }"><span class="mode-knob" /></span>
|
||||
</button>
|
||||
<!-- Swarm — functional client toggle -->
|
||||
<button type="button" class="mode-row" :class="{ on: swarmOn }" @click="emit('toggleSwarm')">
|
||||
<span class="mode-row-name">{{ t('status.swarmLabel') }}</span>
|
||||
<button type="button" class="mode-row" :class="{ on: swarmOn }" role="menuitem" @click="emit('toggleSwarm')">
|
||||
<span class="mode-row-icon"><Icon name="sparkles" size="sm" /></span>
|
||||
<span class="mode-row-info">
|
||||
<span class="mode-row-name">{{ t('status.swarmLabel') }}</span>
|
||||
<span class="mode-row-desc">{{ t('status.swarmDesc') }}</span>
|
||||
</span>
|
||||
<span class="mode-switch" :class="{ on: swarmOn }"><span class="mode-knob" /></span>
|
||||
</button>
|
||||
<!-- Goal — lifecycle controls when active; switch is on when active or armed. -->
|
||||
|
|
@ -924,22 +1034,46 @@ function selectModel(modelId: string): void {
|
|||
<button
|
||||
type="button"
|
||||
class="mode-row-main"
|
||||
@click="goalActive ? emit('controlGoal', 'cancel') : emit('toggleGoal')"
|
||||
role="menuitem"
|
||||
@click="goalActive ? emit('focusGoal') : emit('toggleGoal')"
|
||||
>
|
||||
<span class="mode-row-name">{{ t('status.goalLabel') }}</span>
|
||||
<span class="mode-row-icon"><Icon name="target" size="sm" /></span>
|
||||
<span class="mode-row-info">
|
||||
<span class="mode-row-name">{{ t('status.goalLabel') }}</span>
|
||||
<span class="mode-row-desc">{{ t('status.goalDesc') }}</span>
|
||||
</span>
|
||||
<span v-if="!goalActive" class="mode-switch" :class="{ on: props.goalMode }"><span class="mode-knob" /></span>
|
||||
</button>
|
||||
<div v-if="goalActive" class="mode-row-actions">
|
||||
<button
|
||||
type="button"
|
||||
<Button
|
||||
v-if="goalCanPause"
|
||||
size="sm"
|
||||
variant="secondary"
|
||||
class="mode-row-action"
|
||||
@click="emit('controlGoal', 'pause')"
|
||||
>{{ t('status.goalPause') }}</button>
|
||||
<button
|
||||
type="button"
|
||||
>
|
||||
<Icon name="pause" size="sm" />
|
||||
<span>{{ t('status.goalPause') }}</span>
|
||||
</Button>
|
||||
<Button
|
||||
v-if="goalCanResume"
|
||||
size="sm"
|
||||
variant="primary"
|
||||
class="mode-row-action"
|
||||
@click="emit('controlGoal', 'resume')"
|
||||
>{{ t('status.goalResume') }}</button>
|
||||
>
|
||||
<Icon name="play" size="sm" />
|
||||
<span>{{ t('status.goalResume') }}</span>
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="danger-soft"
|
||||
class="mode-row-action"
|
||||
@click="emit('controlGoal', 'cancel')"
|
||||
>
|
||||
<Icon name="close" size="sm" />
|
||||
<span>{{ t('status.goalCancel') }}</span>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -1063,7 +1197,7 @@ function selectModel(modelId: string): void {
|
|||
:class="{ 'is-active': seg === activeThinkingSegment }"
|
||||
:disabled="thinkingReadonly"
|
||||
@click="setThinkingSegment(seg)"
|
||||
>{{ effortLabel(seg) }}</button>
|
||||
>{{ thinkingSegmentLabel(seg) }}</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -1092,9 +1226,11 @@ function selectModel(modelId: string): void {
|
|||
|
||||
/* Main composer card */
|
||||
.composer-card {
|
||||
--composer-send-size: 32px;
|
||||
--composer-send-inset: var(--space-2);
|
||||
position: relative;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 16px;
|
||||
border-radius: calc((var(--composer-send-size) / 2) + var(--composer-send-inset));
|
||||
background: var(--bg);
|
||||
box-shadow: var(--shadow-md);
|
||||
transition: border-color 0.15s, box-shadow 0.15s;
|
||||
|
|
@ -1358,8 +1494,8 @@ function selectModel(modelId: string): void {
|
|||
(handled upstream). Interrupt is a separate Stop button so the two are never
|
||||
confused. */
|
||||
.send {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
width: var(--composer-send-size);
|
||||
height: var(--composer-send-size);
|
||||
border-radius: 50%;
|
||||
background: var(--color-accent);
|
||||
color: var(--color-text-on-accent); /* white on accent — readable in light and dark */
|
||||
|
|
@ -1386,14 +1522,16 @@ function selectModel(modelId: string): void {
|
|||
|
||||
.send svg {
|
||||
flex: none;
|
||||
width: var(--p-ic-lg);
|
||||
height: var(--p-ic-lg);
|
||||
}
|
||||
|
||||
/* Stop button — sibling of Send, shown only while running. Red at rest so the
|
||||
destructive action is easy to spot; fills solid danger on hover. Kept softer
|
||||
than the accent Send so Send stays the primary action. */
|
||||
.stop {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
width: var(--composer-send-size);
|
||||
height: var(--composer-send-size);
|
||||
border-radius: 50%;
|
||||
background: var(--color-danger-soft);
|
||||
color: var(--color-danger);
|
||||
|
|
@ -1418,6 +1556,8 @@ function selectModel(modelId: string): void {
|
|||
}
|
||||
.stop svg {
|
||||
flex: none;
|
||||
width: var(--p-ic-lg);
|
||||
height: var(--p-ic-lg);
|
||||
}
|
||||
|
||||
/* Bottom toolbar */
|
||||
|
|
@ -1425,10 +1565,19 @@ function selectModel(modelId: string): void {
|
|||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 6px 8px 8px;
|
||||
padding: 6px var(--composer-send-inset) var(--composer-send-inset);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.menu-measure {
|
||||
position: absolute;
|
||||
width: max-content;
|
||||
height: 0;
|
||||
overflow: hidden;
|
||||
visibility: hidden;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.toolbar-left,
|
||||
.toolbar-right {
|
||||
display: flex;
|
||||
|
|
@ -1450,7 +1599,8 @@ function selectModel(modelId: string): void {
|
|||
cursor: pointer;
|
||||
user-select: none;
|
||||
transition: background 0.1s, color 0.15s;
|
||||
font-family: var(--sans);
|
||||
font-family: var(--font-ui);
|
||||
font-weight: var(--weight-medium);
|
||||
}
|
||||
.perm-pill:hover {
|
||||
background: var(--color-surface-sunken);
|
||||
|
|
@ -1486,7 +1636,10 @@ function selectModel(modelId: string): void {
|
|||
.ctx-num {
|
||||
font-size: var(--ui-font-size);
|
||||
color: var(--muted);
|
||||
font-family: var(--mono);
|
||||
font-family: var(--font-ui);
|
||||
font-variant-numeric: tabular-nums;
|
||||
font-feature-settings: "tnum";
|
||||
letter-spacing: 0;
|
||||
line-height: 16px;
|
||||
}
|
||||
|
||||
|
|
@ -1498,8 +1651,10 @@ function selectModel(modelId: string): void {
|
|||
padding: 2px 7px;
|
||||
border-radius: 6px;
|
||||
font-size: var(--ui-font-size);
|
||||
line-height: 16px;
|
||||
line-height: var(--leading-normal);
|
||||
color: var(--dim);
|
||||
font-family: var(--font-ui);
|
||||
font-weight: var(--weight-medium);
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
transition: background 0.1s;
|
||||
|
|
@ -1551,15 +1706,16 @@ function selectModel(modelId: string): void {
|
|||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1px;
|
||||
font-family: var(--font-ui);
|
||||
}
|
||||
|
||||
.md-section {
|
||||
padding: 4px 7px 2px;
|
||||
font-size: var(--ui-font-size);
|
||||
font-size: var(--text-xs);
|
||||
color: var(--muted);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.04em;
|
||||
font-weight: 500;
|
||||
letter-spacing: 0;
|
||||
font-weight: var(--weight-semibold);
|
||||
}
|
||||
|
||||
.md-row {
|
||||
|
|
@ -1570,7 +1726,7 @@ function selectModel(modelId: string): void {
|
|||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
font-family: var(--mono);
|
||||
font-family: var(--font-ui);
|
||||
font-size: var(--ui-font-size);
|
||||
color: var(--color-text);
|
||||
padding: 5px 7px;
|
||||
|
|
@ -1637,7 +1793,7 @@ function selectModel(modelId: string): void {
|
|||
border-radius: var(--radius-sm);
|
||||
}
|
||||
.md-thinking .md-name {
|
||||
font-family: var(--mono);
|
||||
font-family: var(--font-ui);
|
||||
font-size: var(--ui-font-size);
|
||||
color: var(--color-text);
|
||||
flex: none;
|
||||
|
|
@ -1660,7 +1816,7 @@ function selectModel(modelId: string): void {
|
|||
border: none;
|
||||
background: none;
|
||||
cursor: pointer;
|
||||
font-family: var(--mono);
|
||||
font-family: var(--font-ui);
|
||||
font-size: var(--ui-font-size-xs);
|
||||
line-height: 1;
|
||||
color: var(--color-text-muted);
|
||||
|
|
@ -1702,7 +1858,8 @@ function selectModel(modelId: string): void {
|
|||
left: 10px;
|
||||
z-index: var(--z-dropdown);
|
||||
min-width: 220px;
|
||||
max-width: 280px;
|
||||
width: max-content;
|
||||
max-width: calc(100vw - var(--space-8));
|
||||
background: var(--color-surface-raised);
|
||||
border: 1px solid var(--color-line);
|
||||
border-radius: var(--radius-lg);
|
||||
|
|
@ -1714,9 +1871,11 @@ function selectModel(modelId: string): void {
|
|||
}
|
||||
|
||||
.pd-row {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 7px;
|
||||
display: grid;
|
||||
grid-template-columns: 14px var(--composer-menu-desc-width, max-content);
|
||||
column-gap: 7px;
|
||||
row-gap: 2px;
|
||||
align-items: start;
|
||||
width: 100%;
|
||||
background: none;
|
||||
border: none;
|
||||
|
|
@ -1729,34 +1888,41 @@ function selectModel(modelId: string): void {
|
|||
.pd-row.is-current { background: var(--color-accent-soft); }
|
||||
|
||||
.pd-check {
|
||||
grid-column: 1;
|
||||
grid-row: 1;
|
||||
width: 14px;
|
||||
flex: none;
|
||||
min-height: 1lh;
|
||||
color: var(--color-accent);
|
||||
font-weight: 500;
|
||||
font-size: var(--ui-font-size);
|
||||
font-weight: var(--weight-medium);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-top: 1px;
|
||||
line-height: var(--leading-normal);
|
||||
}
|
||||
|
||||
.pd-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: contents;
|
||||
}
|
||||
|
||||
.pd-name {
|
||||
font-family: var(--sans);
|
||||
grid-column: 2;
|
||||
grid-row: 1;
|
||||
font-family: var(--font-ui);
|
||||
font-size: var(--ui-font-size);
|
||||
font-weight: 500;
|
||||
font-weight: var(--weight-medium);
|
||||
line-height: var(--leading-normal);
|
||||
}
|
||||
|
||||
.pd-desc {
|
||||
font-family: var(--sans);
|
||||
font-size: var(--ui-font-size);
|
||||
grid-column: 2;
|
||||
grid-row: 2;
|
||||
width: var(--composer-menu-desc-width, auto);
|
||||
font-family: var(--font-ui);
|
||||
font-size: var(--text-xs);
|
||||
font-weight: var(--weight-medium);
|
||||
color: var(--muted);
|
||||
line-height: 1.4;
|
||||
line-height: var(--leading-normal);
|
||||
}
|
||||
|
||||
/* Toggle pills (Thinking / Plan) */
|
||||
|
|
@ -1773,7 +1939,8 @@ function selectModel(modelId: string): void {
|
|||
background: none;
|
||||
border-radius: 6px;
|
||||
font-size: var(--ui-font-size);
|
||||
font-family: var(--sans);
|
||||
font-family: var(--font-ui);
|
||||
font-weight: var(--weight-medium);
|
||||
color: var(--color-text);
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
|
|
@ -1785,7 +1952,7 @@ function selectModel(modelId: string): void {
|
|||
.mode-label { flex: none; }
|
||||
.mode-tag {
|
||||
flex: none;
|
||||
font-family: var(--mono);
|
||||
font-family: var(--font-ui);
|
||||
font-size: calc(var(--ui-font-size) - 3px);
|
||||
color: var(--color-accent-hover);
|
||||
background: var(--bg);
|
||||
|
|
@ -1800,39 +1967,82 @@ function selectModel(modelId: string): void {
|
|||
position: fixed;
|
||||
z-index: var(--z-dropdown);
|
||||
min-width: 220px;
|
||||
width: max-content;
|
||||
max-width: calc(100vw - var(--space-8));
|
||||
background: var(--color-surface-raised);
|
||||
border: 1px solid var(--color-line);
|
||||
border-radius: var(--radius-lg);
|
||||
box-shadow: var(--shadow-sm);
|
||||
padding: 4px;
|
||||
padding: 5px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1px;
|
||||
}
|
||||
.mode-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 10px;
|
||||
display: grid;
|
||||
grid-template-columns: 14px var(--composer-menu-desc-width, max-content);
|
||||
column-gap: 7px;
|
||||
row-gap: 2px;
|
||||
align-items: start;
|
||||
width: 100%;
|
||||
padding: 7px 10px;
|
||||
padding: 6px 7px;
|
||||
border: none;
|
||||
background: none;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
font-family: var(--sans);
|
||||
font-family: var(--font-ui);
|
||||
text-align: left;
|
||||
}
|
||||
.mode-row:hover:not(:disabled) { background: var(--panel2); }
|
||||
.mode-row:hover:not(:disabled) { background: var(--color-surface-sunken); }
|
||||
.mode-row:disabled { cursor: not-allowed; opacity: 0.45; }
|
||||
.mode-row-name { font-size: var(--ui-font-size-sm); color: var(--color-text); }
|
||||
.mode-row-info {
|
||||
display: contents;
|
||||
}
|
||||
.mode-row-icon {
|
||||
grid-column: 1;
|
||||
grid-row: 1;
|
||||
width: 14px;
|
||||
min-height: 1lh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: var(--muted);
|
||||
font-size: var(--ui-font-size);
|
||||
line-height: var(--leading-normal);
|
||||
}
|
||||
.mode-row-name {
|
||||
grid-column: 2;
|
||||
grid-row: 1;
|
||||
font-size: var(--ui-font-size);
|
||||
font-weight: var(--weight-medium);
|
||||
color: var(--color-text);
|
||||
line-height: var(--leading-normal);
|
||||
}
|
||||
.mode-row-desc {
|
||||
grid-column: 2;
|
||||
grid-row: 2;
|
||||
width: var(--composer-menu-desc-width, auto);
|
||||
font-size: var(--text-xs);
|
||||
font-weight: var(--weight-medium);
|
||||
color: var(--muted);
|
||||
line-height: var(--leading-normal);
|
||||
}
|
||||
.mode-row-not-supported {
|
||||
margin-left: auto;
|
||||
font-size: var(--ui-font-size-xs);
|
||||
color: var(--muted);
|
||||
}
|
||||
.mode-row.on .mode-row-name { color: var(--color-accent-hover); font-weight: 500; }
|
||||
.mode-row.on {
|
||||
background: var(--color-accent-soft);
|
||||
}
|
||||
.mode-row.on .mode-row-name { color: var(--color-accent-hover); }
|
||||
.mode-row.on .mode-row-icon { color: var(--color-accent-hover); }
|
||||
.mode-row-meta { font-family: var(--mono); font-size: calc(var(--ui-font-size) - 3px); color: var(--muted); }
|
||||
.mode-row:disabled .mode-row-meta { color: var(--faint); }
|
||||
.mode-switch {
|
||||
flex: none;
|
||||
grid-column: 2;
|
||||
grid-row: 1;
|
||||
justify-self: end;
|
||||
width: 34px;
|
||||
height: 19px;
|
||||
border-radius: 999px;
|
||||
|
|
@ -1856,45 +2066,49 @@ function selectModel(modelId: string): void {
|
|||
.mode-switch.on .mode-knob { transform: translateX(15px); }
|
||||
|
||||
.mode-row-goal {
|
||||
flex-wrap: wrap;
|
||||
--mode-row-icon-col: 14px;
|
||||
--mode-row-col-gap: 7px;
|
||||
--mode-row-pad-x: 7px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
cursor: default;
|
||||
padding: 0;
|
||||
gap: 0;
|
||||
}
|
||||
.mode-row-goal:hover { background: transparent; }
|
||||
.mode-row-goal.on {
|
||||
background: var(--color-accent-soft);
|
||||
}
|
||||
.mode-row-main {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 10px;
|
||||
display: grid;
|
||||
grid-template-columns: var(--mode-row-icon-col) var(--composer-menu-desc-width, max-content);
|
||||
column-gap: var(--mode-row-col-gap);
|
||||
row-gap: 2px;
|
||||
align-items: start;
|
||||
width: 100%;
|
||||
padding: 7px 10px;
|
||||
padding: 6px var(--mode-row-pad-x);
|
||||
border: none;
|
||||
background: none;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
font-family: var(--sans);
|
||||
font-family: var(--font-ui);
|
||||
text-align: left;
|
||||
}
|
||||
.mode-row-main:hover { background: var(--panel2); }
|
||||
.mode-row-goal.on .mode-row-main .mode-row-name { color: var(--color-accent-hover); font-weight: 500; }
|
||||
.mode-row-main:hover { background: var(--color-surface-sunken); }
|
||||
.mode-row-goal.on .mode-row-main .mode-row-name { color: var(--color-accent-hover); }
|
||||
.mode-row-actions {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
flex: 1 1 100%;
|
||||
justify-content: flex-end;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--space-2);
|
||||
justify-content: flex-start;
|
||||
padding: 0 var(--mode-row-pad-x) var(--mode-row-pad-x)
|
||||
calc(var(--mode-row-pad-x) + var(--mode-row-icon-col) + var(--mode-row-col-gap));
|
||||
}
|
||||
.mode-row-action {
|
||||
padding: 3px 8px;
|
||||
border-radius: var(--radius-sm);
|
||||
border: 1px solid var(--line);
|
||||
background: var(--panel);
|
||||
color: var(--color-text);
|
||||
font-size: calc(var(--ui-font-size) - 3px);
|
||||
cursor: pointer;
|
||||
flex: none;
|
||||
}
|
||||
.mode-row-action:hover:not(:disabled) { background: var(--panel2); }
|
||||
.mode-row-action:disabled { opacity: 0.5; cursor: default; }
|
||||
.mode-row-action :deep(.ui-button__content) { gap: var(--space-1); }
|
||||
.mode-row-input {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
|
|
@ -1949,7 +2163,7 @@ function selectModel(modelId: string): void {
|
|||
var(--dock-inline-left, max(12px, env(safe-area-inset-left)));
|
||||
}
|
||||
.composer-card {
|
||||
border-radius: var(--radius-xl);
|
||||
--composer-send-size: 36px;
|
||||
max-width: 100%;
|
||||
}
|
||||
.input-row {
|
||||
|
|
@ -1958,9 +2172,9 @@ function selectModel(modelId: string): void {
|
|||
}
|
||||
/* Send → 36px round (hide the SVG arrow, show only the ::after glyph) */
|
||||
.send {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
min-width: 36px;
|
||||
width: var(--composer-send-size);
|
||||
height: var(--composer-send-size);
|
||||
min-width: var(--composer-send-size);
|
||||
padding: 0;
|
||||
border-radius: 50%;
|
||||
font-size: 0;
|
||||
|
|
@ -1979,9 +2193,9 @@ function selectModel(modelId: string): void {
|
|||
}
|
||||
/* Stop → 36px round "■" glyph to match the mobile Send sizing. */
|
||||
.stop {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
min-width: 36px;
|
||||
width: var(--composer-send-size);
|
||||
height: var(--composer-send-size);
|
||||
min-width: var(--composer-send-size);
|
||||
padding: 0;
|
||||
border-radius: 50%;
|
||||
font-size: 0;
|
||||
|
|
@ -1994,7 +2208,7 @@ function selectModel(modelId: string): void {
|
|||
.stop::after {
|
||||
content: "■";
|
||||
/* Fixed icon glyph size — not part of the UI font scale. */
|
||||
font-size: 14px;
|
||||
font-size: 17px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
|
|
@ -2065,7 +2279,7 @@ function selectModel(modelId: string): void {
|
|||
font-size: var(--ui-font-size);
|
||||
}
|
||||
.pd-desc {
|
||||
font-size: var(--ui-font-size);
|
||||
font-size: var(--text-xs);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
<!-- apps/kimi-web/src/components/chat/ConversationPane.vue -->
|
||||
<script setup lang="ts">
|
||||
import { measureNaturalWidth, prepareWithSegments } from '@chenglou/pretext';
|
||||
import { computed, nextTick, onMounted, onUnmounted, provide, ref, watch, type ComponentPublicInstance } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import type { ActivationBadges, ApprovalBlock, ChatTurn, ConversationStatus, FilePreviewRequest, PermissionMode, QueuedPromptView, TaskItem, TodoView, ToolMedia, UIQuestion, WorkspaceView } from '../../types';
|
||||
|
|
@ -15,6 +16,8 @@ import Tooltip from '../ui/Tooltip.vue';
|
|||
import { getVisibleWorkspaces } from '../../lib/workspacePicker';
|
||||
import { safeRemove, STORAGE_KEYS } from '../../lib/storage';
|
||||
|
||||
const { t, locale } = useI18n();
|
||||
|
||||
const props = defineProps<{
|
||||
turns: ChatTurn[];
|
||||
sessionId?: string;
|
||||
|
|
@ -134,6 +137,13 @@ const emit = defineEmits<{
|
|||
// Empty-composer workspace picker.
|
||||
const wsPickOpen = ref(false);
|
||||
const wsPickExpanded = ref(false);
|
||||
const contentWrapRef = ref<HTMLElement | null>(null);
|
||||
const wsPickMeasureRef = ref<HTMLElement | null>(null);
|
||||
const wsPickMenuWidth = ref<string>('');
|
||||
const wsPickStyle = computed<Record<string, string> | undefined>(() =>
|
||||
wsPickMenuWidth.value ? { '--ws-pick-menu-width': wsPickMenuWidth.value } : undefined,
|
||||
);
|
||||
let wsPickMeasureFrame: number | null = null;
|
||||
|
||||
const activeWorkspaceLabel = computed(() => {
|
||||
const w = props.workspaces?.find((ws) => ws.id === props.activeWorkspaceId);
|
||||
|
|
@ -161,7 +171,81 @@ function pickWorkspace(id: string): void {
|
|||
if (id !== props.activeWorkspaceId) emit('selectWorkspace', id);
|
||||
}
|
||||
|
||||
const { t } = useI18n();
|
||||
function cssPx(value: string): number {
|
||||
const n = Number.parseFloat(value);
|
||||
return Number.isFinite(n) ? n : 0;
|
||||
}
|
||||
|
||||
function canvasFont(style: CSSStyleDeclaration): string {
|
||||
return `${style.fontStyle || 'normal'} ${style.fontWeight || '400'} ${style.fontSize} ${style.fontFamily}`;
|
||||
}
|
||||
|
||||
function letterSpacingPx(style: CSSStyleDeclaration): number {
|
||||
return style.letterSpacing === 'normal' ? 0 : cssPx(style.letterSpacing);
|
||||
}
|
||||
|
||||
function measureText(text: string, style: CSSStyleDeclaration): number {
|
||||
if (!text) return 0;
|
||||
const prepared = prepareWithSegments(text, canvasFont(style), { letterSpacing: letterSpacingPx(style) });
|
||||
return measureNaturalWidth(prepared);
|
||||
}
|
||||
|
||||
function workspacePickerMaxWidth(): number {
|
||||
const containerWidth = contentWrapRef.value?.getBoundingClientRect().width ?? window.innerWidth;
|
||||
return Math.max(0, containerWidth * 0.75);
|
||||
}
|
||||
|
||||
function updateWorkspacePickerWidth(): void {
|
||||
const probe = wsPickMeasureRef.value;
|
||||
if (!probe) return;
|
||||
|
||||
const itemPath = probe.querySelector<HTMLElement>('.ws-pick-item-path');
|
||||
if (!itemPath) return;
|
||||
|
||||
const itemPathStyle = getComputedStyle(itemPath);
|
||||
|
||||
const measuredPathWidth = (props.workspaces ?? []).reduce(
|
||||
(max, workspace) => Math.max(max, measureText(workspace.shortPath, itemPathStyle)),
|
||||
0,
|
||||
);
|
||||
wsPickMenuWidth.value = measuredPathWidth
|
||||
? `${Math.ceil(Math.min(measuredPathWidth, workspacePickerMaxWidth()))}px`
|
||||
: '';
|
||||
}
|
||||
|
||||
function scheduleWorkspacePickerMeasure(): void {
|
||||
if (typeof window === 'undefined') return;
|
||||
void nextTick(() => {
|
||||
if (wsPickMeasureFrame !== null) window.cancelAnimationFrame(wsPickMeasureFrame);
|
||||
wsPickMeasureFrame = window.requestAnimationFrame(() => {
|
||||
wsPickMeasureFrame = null;
|
||||
updateWorkspacePickerWidth();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
watch(
|
||||
() => [
|
||||
props.activeWorkspaceId,
|
||||
props.workspaceName,
|
||||
props.workspaces?.map((w) => `${w.id}\u0000${w.name}\u0000${w.shortPath}`).join('\u0001') ?? '',
|
||||
hiddenWorkspaceCount.value,
|
||||
locale.value,
|
||||
],
|
||||
scheduleWorkspacePickerMeasure,
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
onMounted(() => {
|
||||
scheduleWorkspacePickerMeasure();
|
||||
window.addEventListener('resize', scheduleWorkspacePickerMeasure);
|
||||
void document.fonts?.ready.then(scheduleWorkspacePickerMeasure);
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
if (wsPickMeasureFrame !== null) window.cancelAnimationFrame(wsPickMeasureFrame);
|
||||
window.removeEventListener('resize', scheduleWorkspacePickerMeasure);
|
||||
});
|
||||
|
||||
// The align toggle was removed with its UI (6e50cb7) — reading layout is
|
||||
// always centered now. Drop the old persisted preference so users who once
|
||||
|
|
@ -1045,7 +1129,7 @@ defineExpose({ loadComposerForEdit, focusComposer });
|
|||
class="panes chat-scroll"
|
||||
@scroll.passive="onPanesScroll"
|
||||
>
|
||||
<div class="content-wrap" :class="[mobile ? 'align-mobile' : 'align-center']">
|
||||
<div ref="contentWrapRef" class="content-wrap" :class="[mobile ? 'align-mobile' : 'align-center']">
|
||||
<template v-if="turns.length === 0 && !sessionLoading">
|
||||
<!-- Empty session: Composer rendered in the centre of the pane -->
|
||||
<div class="empty-spacer" />
|
||||
|
|
@ -1053,7 +1137,27 @@ defineExpose({ loadComposerForEdit, focusComposer });
|
|||
<span class="empty-hint-title">{{ t('composer.emptyConversationTitle') }}</span>
|
||||
<span class="empty-hint-text">{{ t('composer.emptyConversation') }}</span>
|
||||
<!-- Workspace picker: choose where this new conversation starts. -->
|
||||
<div v-if="hasWorkspaces" class="ws-pick">
|
||||
<div v-if="hasWorkspaces" class="ws-pick" :style="wsPickStyle">
|
||||
<div ref="wsPickMeasureRef" class="ws-pick-measure" aria-hidden="true">
|
||||
<button type="button" class="ws-pick-btn" tabindex="-1">
|
||||
<Icon name="folder" size="sm" />
|
||||
<span class="ws-pick-name">{{ activeWorkspaceLabel }}</span>
|
||||
<Icon class="ws-pick-chev" name="chevron-down" size="sm" />
|
||||
</button>
|
||||
<div class="ws-pick-menu">
|
||||
<button type="button" class="ws-pick-item" tabindex="-1">
|
||||
<span class="ws-pick-item-name" />
|
||||
<span class="ws-pick-item-path" />
|
||||
</button>
|
||||
<button type="button" class="ws-pick-item ws-pick-more" tabindex="-1">
|
||||
<span>{{ t('conversation.moreWorkspaces', { count: hiddenWorkspaceCount }) }}</span>
|
||||
</button>
|
||||
<button type="button" class="ws-pick-action" tabindex="-1">
|
||||
<Icon name="plus" size="sm" />
|
||||
<span>{{ t('conversation.addWorkspace') }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<Tooltip :text="t('conversation.switchWorkspace')">
|
||||
<button type="button" class="ws-pick-btn" @click.stop="wsPickOpen = !wsPickOpen">
|
||||
<Icon name="folder" size="sm" />
|
||||
|
|
@ -1116,6 +1220,7 @@ defineExpose({ loadComposerForEdit, focusComposer });
|
|||
:plan-mode="planMode"
|
||||
:swarm-mode="swarmMode"
|
||||
:goal-mode="goalMode"
|
||||
:goal="goal"
|
||||
:activation-badges="activationBadges"
|
||||
:models="models"
|
||||
:starred-ids="starredIds"
|
||||
|
|
@ -1337,11 +1442,12 @@ defineExpose({ loadComposerForEdit, focusComposer });
|
|||
text-align: center;
|
||||
padding: 0 16px 16px;
|
||||
color: var(--color-text);
|
||||
font-family: var(--sans);
|
||||
font-family: var(--font-ui);
|
||||
}
|
||||
.empty-hint-title {
|
||||
font-size: calc(var(--ui-font-size) + 16px);
|
||||
font-weight: 500;
|
||||
font-optical-sizing: auto;
|
||||
font-weight: 600;
|
||||
}
|
||||
.empty-hint-text {
|
||||
display: inline-block;
|
||||
|
|
@ -1382,13 +1488,31 @@ defineExpose({ loadComposerForEdit, focusComposer });
|
|||
/* Empty-composer workspace picker */
|
||||
.ws-pick {
|
||||
position: relative;
|
||||
font-family: var(--mono);
|
||||
font-family: var(--font-ui);
|
||||
}
|
||||
.ws-pick-measure {
|
||||
position: absolute;
|
||||
visibility: hidden;
|
||||
pointer-events: none;
|
||||
width: max-content;
|
||||
height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
.ws-pick-measure .ws-pick-menu {
|
||||
position: static;
|
||||
transform: none;
|
||||
width: max-content;
|
||||
min-width: 0;
|
||||
max-width: none;
|
||||
max-height: none;
|
||||
overflow: visible;
|
||||
}
|
||||
.ws-pick-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 7px;
|
||||
max-width: 320px;
|
||||
width: max-content;
|
||||
max-width: min(100%, calc(100vw - var(--space-8)));
|
||||
padding: 5px 10px;
|
||||
background: var(--panel);
|
||||
border: 1px solid var(--line);
|
||||
|
|
@ -1399,7 +1523,7 @@ defineExpose({ loadComposerForEdit, focusComposer });
|
|||
cursor: pointer;
|
||||
}
|
||||
.ws-pick-btn:hover { border-color: var(--color-accent-bd); color: var(--color-text); }
|
||||
.ws-pick-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.ws-pick-name { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.ws-pick-chev { flex: none; color: var(--muted); transition: transform 0.15s; }
|
||||
.ws-pick-chev.open { transform: rotate(180deg); }
|
||||
.ws-pick-backdrop {
|
||||
|
|
@ -1413,8 +1537,9 @@ defineExpose({ loadComposerForEdit, focusComposer });
|
|||
transform: translateX(-50%);
|
||||
top: calc(100% + 6px);
|
||||
z-index: var(--z-dropdown);
|
||||
min-width: 220px;
|
||||
max-width: min(86vw, 340px);
|
||||
width: var(--ws-pick-menu-width, max-content);
|
||||
min-width: var(--ws-pick-menu-width, max-content);
|
||||
max-width: calc(100vw - var(--space-8));
|
||||
max-height: 50vh;
|
||||
overflow-y: auto;
|
||||
background: var(--color-surface-raised);
|
||||
|
|
@ -1435,16 +1560,23 @@ defineExpose({ loadComposerForEdit, focusComposer });
|
|||
border-radius: 6px;
|
||||
padding: 6px 10px;
|
||||
cursor: pointer;
|
||||
font-family: var(--mono);
|
||||
font-family: var(--font-ui);
|
||||
}
|
||||
.ws-pick-item:hover { background: var(--panel2); }
|
||||
.ws-pick-item.on { background: var(--color-accent-soft); }
|
||||
.ws-pick-item-name { font-size: var(--ui-font-size-sm); color: var(--color-text); }
|
||||
.ws-pick-item.on .ws-pick-item-name { color: var(--color-accent-hover); font-weight: 500; }
|
||||
.ws-pick-item-path { font-size: var(--text-base); color: var(--muted); }
|
||||
.ws-pick-item-name {
|
||||
font-size: var(--text-base);
|
||||
font-weight: var(--weight-medium);
|
||||
color: var(--color-text);
|
||||
}
|
||||
.ws-pick-item.on .ws-pick-item-name { color: var(--color-accent-hover); }
|
||||
.ws-pick-item-path { font-size: var(--text-xs); font-weight: 475; color: var(--muted); }
|
||||
.ws-pick-item.ws-pick-more {
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
font-size: var(--text-base);
|
||||
font-weight: var(--weight-medium);
|
||||
color: var(--dim);
|
||||
}
|
||||
.ws-pick-item.ws-pick-more:hover { color: var(--color-text); }
|
||||
|
|
@ -1464,8 +1596,9 @@ defineExpose({ loadComposerForEdit, focusComposer });
|
|||
border-radius: 6px;
|
||||
padding: 7px 10px;
|
||||
cursor: pointer;
|
||||
font-family: var(--mono);
|
||||
font-size: var(--ui-font-size-sm);
|
||||
font-family: var(--font-ui);
|
||||
font-size: var(--text-base);
|
||||
font-weight: var(--weight-medium);
|
||||
color: var(--dim);
|
||||
}
|
||||
.ws-pick-action:hover { background: var(--panel2); color: var(--color-text); }
|
||||
|
|
@ -1500,7 +1633,9 @@ defineExpose({ loadComposerForEdit, focusComposer });
|
|||
font-size: var(--ui-font-size-sm);
|
||||
cursor: pointer;
|
||||
box-shadow: var(--shadow-sm);
|
||||
z-index: var(--z-sticky);
|
||||
/* Positioned after the message flow, so base z-index is enough to float above
|
||||
content while staying below composer dropdowns. */
|
||||
z-index: var(--z-base);
|
||||
}
|
||||
.newmsg-pill:hover { background: var(--panel2); }
|
||||
.pill-chevron {
|
||||
|
|
|
|||
|
|
@ -27,6 +27,15 @@ const tokenPct = computed(() => {
|
|||
return Math.max(0, Math.min(100, Math.round((props.goal.tokensUsed / budget) * 100)));
|
||||
});
|
||||
|
||||
function goalStatusLabel(status: AppGoal['status']): string {
|
||||
switch (status) {
|
||||
case 'active': return t('status.goalStatusActive');
|
||||
case 'paused': return t('status.goalStatusPaused');
|
||||
case 'blocked': return t('status.goalStatusBlocked');
|
||||
case 'complete': return t('status.goalStatusComplete');
|
||||
}
|
||||
}
|
||||
|
||||
function formatMs(ms: number): string {
|
||||
const sec = Math.max(0, Math.round(ms / 1000));
|
||||
const min = Math.floor(sec / 60);
|
||||
|
|
@ -42,13 +51,13 @@ function formatMs(ms: number): string {
|
|||
<Card class="goal-strip" :class="{ expanded }">
|
||||
<template #head>
|
||||
<button class="goal-row" type="button" @click="expanded = !expanded">
|
||||
<span class="goal-kicker">Goal</span>
|
||||
<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"
|
||||
>{{ goal.status }}</Badge>
|
||||
>{{ goalStatusLabel(goal.status) }}</Badge>
|
||||
<span class="goal-progress" aria-hidden="true">
|
||||
<span class="goal-progress-fill" :style="{ width: `${tokenPct}%` }"></span>
|
||||
</span>
|
||||
|
|
@ -62,36 +71,47 @@ function formatMs(ms: number): string {
|
|||
<span>Done when</span>
|
||||
<p>{{ goal.completionCriterion }}</p>
|
||||
</div>
|
||||
<div class="goal-stats">
|
||||
<Badge variant="neutral" size="sm">{{ goal.turnsUsed }} turns</Badge>
|
||||
<Badge variant="neutral" size="sm">{{ goal.tokensUsed.toLocaleString() }} tokens</Badge>
|
||||
<Badge variant="neutral" size="sm">{{ formatMs(goal.wallClockMs) }}</Badge>
|
||||
<Badge v-if="goal.budget.tokenBudget !== null" variant="neutral" size="sm">{{ tokenPct }}% token budget</Badge>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-if="expanded" #foot>
|
||||
<div class="goal-actions">
|
||||
<Button
|
||||
v-if="goal.status !== 'paused'"
|
||||
size="sm"
|
||||
variant="secondary"
|
||||
class="goal-action"
|
||||
@click.stop="emit('controlGoal', 'pause')"
|
||||
>{{ t('status.goalPause') }}</Button>
|
||||
<Button
|
||||
v-if="goal.status === 'paused'"
|
||||
size="sm"
|
||||
variant="primary"
|
||||
class="goal-action"
|
||||
@click.stop="emit('controlGoal', 'resume')"
|
||||
>{{ t('status.goalResume') }}</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="danger-soft"
|
||||
class="goal-action"
|
||||
@click.stop="emit('controlGoal', 'cancel')"
|
||||
>{{ t('status.goalCancel') }}</Button>
|
||||
<div class="goal-footer">
|
||||
<div class="goal-meta">
|
||||
<span>{{ goal.turnsUsed }} turns</span>
|
||||
<span>{{ goal.tokensUsed.toLocaleString() }} tokens</span>
|
||||
<span>{{ formatMs(goal.wallClockMs) }}</span>
|
||||
<span v-if="goal.budget.tokenBudget !== null">{{ tokenPct }}% token budget</span>
|
||||
</div>
|
||||
<div class="goal-actions">
|
||||
<Button
|
||||
v-if="goal.status === 'active'"
|
||||
size="sm"
|
||||
variant="secondary"
|
||||
class="goal-action"
|
||||
@click.stop="emit('controlGoal', 'pause')"
|
||||
>
|
||||
<Icon name="pause" size="md" />
|
||||
<span>{{ t('status.goalPause') }}</span>
|
||||
</Button>
|
||||
<Button
|
||||
v-if="goal.status === 'paused' || goal.status === 'blocked'"
|
||||
size="sm"
|
||||
variant="primary"
|
||||
class="goal-action"
|
||||
@click.stop="emit('controlGoal', 'resume')"
|
||||
>
|
||||
<Icon name="play" size="md" />
|
||||
<span>{{ t('status.goalResume') }}</span>
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="danger-soft"
|
||||
class="goal-action"
|
||||
@click.stop="emit('controlGoal', 'cancel')"
|
||||
>
|
||||
<Icon name="close" size="md" />
|
||||
<span>{{ t('status.goalCancel') }}</span>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</Card>
|
||||
|
|
@ -99,8 +119,21 @@ function formatMs(ms: number): string {
|
|||
|
||||
<style scoped>
|
||||
.goal-strip {
|
||||
--composer-send-size: 32px;
|
||||
--composer-send-inset: var(--space-2);
|
||||
margin: var(--space-2) var(--space-4) 0;
|
||||
}
|
||||
.goal-strip.ui-card {
|
||||
border-radius: calc((var(--composer-send-size) / 2) + var(--composer-send-inset));
|
||||
}
|
||||
.goal-strip :deep(.ui-card__foot) {
|
||||
padding: var(--composer-send-inset);
|
||||
}
|
||||
.goal-strip :deep(.ui-card__head),
|
||||
.goal-strip :deep(.ui-card__body),
|
||||
.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; }
|
||||
|
|
@ -116,13 +149,14 @@ function formatMs(ms: number): string {
|
|||
background: transparent;
|
||||
color: var(--color-text);
|
||||
font: var(--text-base)/var(--leading-normal) var(--font-ui);
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
}
|
||||
.goal-kicker {
|
||||
flex: none;
|
||||
color: var(--color-success);
|
||||
font: var(--weight-bold) var(--text-xs) var(--font-mono);
|
||||
text-transform: uppercase;
|
||||
font: var(--text-base)/var(--leading-normal) var(--font-ui);
|
||||
font-weight: var(--weight-semibold);
|
||||
}
|
||||
.goal-objective {
|
||||
min-width: 0;
|
||||
|
|
@ -132,6 +166,7 @@ function formatMs(ms: number): string {
|
|||
white-space: nowrap;
|
||||
color: var(--color-text);
|
||||
font-size: var(--text-base);
|
||||
text-align: left;
|
||||
}
|
||||
.goal-objective.expanded-hidden {
|
||||
visibility: hidden;
|
||||
|
|
@ -183,25 +218,43 @@ function formatMs(ms: number): string {
|
|||
font: var(--text-xs)/var(--leading-normal) var(--font-ui);
|
||||
text-transform: none;
|
||||
}
|
||||
.goal-stats {
|
||||
.goal-footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: var(--space-3);
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
}
|
||||
.goal-meta {
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--space-2);
|
||||
margin-top: var(--space-3);
|
||||
color: var(--color-text-muted);
|
||||
font: var(--text-xs) var(--font-mono);
|
||||
font: 12px/var(--leading-normal) var(--font-ui);
|
||||
font-weight: 450;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
.goal-actions {
|
||||
display: flex;
|
||||
gap: var(--space-2);
|
||||
width: 100%;
|
||||
justify-content: flex-end;
|
||||
flex: none;
|
||||
}
|
||||
.goal-action {
|
||||
flex: 1;
|
||||
flex: none;
|
||||
min-width: 0;
|
||||
height: var(--composer-send-size);
|
||||
border-radius: calc(var(--composer-send-size) / 2);
|
||||
padding-inline: var(--space-4);
|
||||
}
|
||||
.goal-action :deep(.ui-button__content) {
|
||||
gap: var(--space-1);
|
||||
}
|
||||
@media (max-width: 640px) {
|
||||
.goal-strip {
|
||||
--composer-send-size: 36px;
|
||||
margin: var(--space-2) var(--space-3) 0;
|
||||
}
|
||||
.goal-progress {
|
||||
|
|
|
|||
|
|
@ -474,12 +474,12 @@ function copyDiff(code: string, idx: number) {
|
|||
|
||||
/* Base prose — assistant message text. */
|
||||
.md {
|
||||
font: 500 15px/1.6 var(--font-ui);
|
||||
font: 400 15px/1.6 var(--font-ui);
|
||||
color: var(--color-text);
|
||||
word-break: break-word;
|
||||
}
|
||||
.md :deep(.markdown-renderer) {
|
||||
font: 500 15px/1.6 var(--font-ui);
|
||||
font: 400 15px/1.6 var(--font-ui);
|
||||
color: var(--color-text);
|
||||
}
|
||||
.md :deep(.markstream-vue),
|
||||
|
|
@ -523,8 +523,9 @@ function copyDiff(code: string, idx: number) {
|
|||
font-size: var(--content-font-size);
|
||||
}
|
||||
|
||||
/* Emphasis — bold steps up from the body (medium/500) to semibold (700). */
|
||||
/* Emphasis — keep the weight strong, but soften the ink slightly. */
|
||||
.md :deep(strong) {
|
||||
color: color-mix(in srgb, var(--color-text) 86%, var(--color-text-muted));
|
||||
font-weight: var(--weight-semibold);
|
||||
}
|
||||
|
||||
|
|
@ -534,7 +535,8 @@ function copyDiff(code: string, idx: number) {
|
|||
.md :deep(h3),
|
||||
.md :deep(h4) {
|
||||
color: var(--color-text);
|
||||
font-weight: var(--weight-medium);
|
||||
font-optical-sizing: auto;
|
||||
font-weight: 600;
|
||||
margin: 0.85em 0 0.35em;
|
||||
line-height: var(--leading-tight);
|
||||
}
|
||||
|
|
@ -573,9 +575,15 @@ function copyDiff(code: string, idx: number) {
|
|||
font: .9em var(--font-mono);
|
||||
background: var(--color-surface-sunken);
|
||||
color: var(--color-fg);
|
||||
padding: 0 5px;
|
||||
padding: 0 4px;
|
||||
border-radius: var(--radius-sm);
|
||||
}
|
||||
.md :deep(strong code),
|
||||
.md :deep(strong .inline-code),
|
||||
.md :deep(b code),
|
||||
.md :deep(b .inline-code) {
|
||||
font-weight: var(--weight-semibold);
|
||||
}
|
||||
|
||||
/* ---------------------------------------------------------------------------
|
||||
Code blocks — sunken surface, 1px line border, radius md, soft shadow, plus
|
||||
|
|
@ -602,6 +610,9 @@ function copyDiff(code: string, idx: number) {
|
|||
color: var(--color-text-muted);
|
||||
font: var(--text-xs) var(--font-mono);
|
||||
}
|
||||
.md :deep(.code-block-header .code-header-main) {
|
||||
font-family: var(--font-ui);
|
||||
}
|
||||
/* Copy button — mirrors the §03 IconButton: muted glyph, sunken hover, soft
|
||||
radius, and the shared focus ring. markstream renders its own button, so we
|
||||
restyle it in place instead of swapping in the IconButton primitive. */
|
||||
|
|
|
|||
|
|
@ -452,7 +452,7 @@ onUnmounted(() => document.removeEventListener('keydown', handleKeydown));
|
|||
}
|
||||
.qstep {
|
||||
color: var(--color-text-muted);
|
||||
font: var(--text-xs) var(--font-mono);
|
||||
font: var(--text-xs) var(--font-ui);
|
||||
margin-left: var(--space-1);
|
||||
}
|
||||
/* Minimize toggle — pinned to the right of the header row. */
|
||||
|
|
@ -482,6 +482,7 @@ onUnmounted(() => document.removeEventListener('keydown', handleKeydown));
|
|||
align-items: center;
|
||||
gap: var(--space-2);
|
||||
margin-bottom: var(--space-3);
|
||||
font-family: var(--font-ui);
|
||||
}
|
||||
.qstep-dot {
|
||||
display: inline-flex;
|
||||
|
|
@ -493,7 +494,7 @@ onUnmounted(() => document.removeEventListener('keydown', handleKeydown));
|
|||
border: 1px solid var(--color-line);
|
||||
background: var(--color-surface);
|
||||
color: var(--color-text-muted);
|
||||
font: var(--text-xs) var(--font-mono);
|
||||
font: var(--text-xs) var(--font-ui);
|
||||
cursor: pointer;
|
||||
padding: 0;
|
||||
transition: background var(--duration-fast) var(--ease-out), border-color var(--duration-fast) var(--ease-out), color var(--duration-fast) var(--ease-out);
|
||||
|
|
@ -545,7 +546,8 @@ onUnmounted(() => document.removeEventListener('keydown', handleKeydown));
|
|||
|
||||
.qopt-key {
|
||||
color: var(--color-text-muted);
|
||||
font: var(--text-xs) var(--font-mono);
|
||||
font: var(--text-xs) var(--font-ui);
|
||||
font-weight: var(--weight-medium);
|
||||
width: 12px;
|
||||
flex: none;
|
||||
text-align: center;
|
||||
|
|
@ -560,8 +562,16 @@ onUnmounted(() => document.removeEventListener('keydown', handleKeydown));
|
|||
flex-direction: column;
|
||||
gap: 2px;
|
||||
}
|
||||
.qopt-label { color: var(--color-text); }
|
||||
.qopt-desc { color: var(--color-text-muted); font: var(--text-xs)/var(--leading-normal) var(--font-ui); }
|
||||
.qopt-label {
|
||||
color: var(--color-text);
|
||||
font-size: var(--text-base);
|
||||
font-weight: var(--weight-medium);
|
||||
}
|
||||
.qopt-desc {
|
||||
color: var(--color-text-muted);
|
||||
font: var(--text-xs)/var(--leading-normal) var(--font-ui);
|
||||
font-weight: var(--weight-medium);
|
||||
}
|
||||
|
||||
.chk, .rad { font: var(--text-base) var(--font-mono); }
|
||||
|
||||
|
|
@ -603,7 +613,7 @@ onUnmounted(() => document.removeEventListener('keydown', handleKeydown));
|
|||
.qstep-dot {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
font: var(--text-xs) var(--font-mono);
|
||||
font: var(--text-xs) var(--font-ui);
|
||||
}
|
||||
|
||||
/* Options → taller, finger-friendly rows. Label + description already stack
|
||||
|
|
|
|||
|
|
@ -118,14 +118,16 @@ watch(
|
|||
|
||||
.prev {
|
||||
color: var(--color-text-faint);
|
||||
font: var(--text-base)/var(--leading-relaxed) var(--font-mono);
|
||||
font: var(--text-base)/var(--leading-relaxed) var(--font-ui);
|
||||
font-weight: 425;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.tc {
|
||||
font: var(--text-base)/var(--leading-relaxed) var(--font-mono);
|
||||
font: var(--text-base)/var(--leading-relaxed) var(--font-ui);
|
||||
font-weight: 425;
|
||||
color: var(--color-text-muted);
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
|
|
|
|||
|
|
@ -64,7 +64,8 @@ watch(
|
|||
overflow-y: auto;
|
||||
margin: 0;
|
||||
padding: 12px 14px;
|
||||
font: var(--text-base)/var(--leading-relaxed) var(--font-mono);
|
||||
font: var(--text-base)/var(--leading-relaxed) var(--font-ui);
|
||||
font-weight: 425;
|
||||
color: var(--color-text-muted);
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
|
|
|
|||
|
|
@ -55,10 +55,12 @@ function onHeadClick(): void {
|
|||
>
|
||||
<div class="bh" ref="bhEl" @click="onHeadClick">
|
||||
<span v-if="icon" class="gl" v-html="icon" aria-hidden="true" />
|
||||
<span class="a">{{ name }}</span>
|
||||
<Tooltip :text="arg">
|
||||
<span v-if="arg" class="p">{{ arg }}</span>
|
||||
</Tooltip>
|
||||
<span class="bh-text">
|
||||
<span class="a">{{ name }}</span>
|
||||
<Tooltip :text="arg">
|
||||
<span v-if="arg" class="p">{{ arg }}</span>
|
||||
</Tooltip>
|
||||
</span>
|
||||
<span class="rt">
|
||||
<span class="status" :class="status" role="status" :aria-label="status">
|
||||
<Icon v-if="status === 'ok'" name="check" size="sm" />
|
||||
|
|
@ -133,6 +135,13 @@ function onHeadClick(): void {
|
|||
color: var(--color-text-faint);
|
||||
flex: none;
|
||||
}
|
||||
.bh-text {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: inherit;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
.a {
|
||||
color: var(--color-text);
|
||||
font-weight: var(--weight-medium);
|
||||
|
|
@ -140,6 +149,7 @@ function onHeadClick(): void {
|
|||
}
|
||||
.p {
|
||||
color: var(--color-text-muted);
|
||||
font-size: var(--text-xs);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
|
|
@ -160,6 +170,7 @@ function onHeadClick(): void {
|
|||
}
|
||||
:slotted(.chip) {
|
||||
color: var(--color-text-muted);
|
||||
font-family: var(--font-ui);
|
||||
font-size: var(--text-xs);
|
||||
flex: none;
|
||||
}
|
||||
|
|
@ -196,7 +207,7 @@ function onHeadClick(): void {
|
|||
.bb-pad {
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
padding: 0 11px 11px;
|
||||
padding: var(--space-2) var(--space-3) var(--space-3);
|
||||
background: var(--color-surface-sunken);
|
||||
border-top: 1px solid var(--color-line);
|
||||
color: var(--color-text);
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import { diffStats } from '../../../lib/diffLines';
|
|||
import { buildEditDiffLines } from '../../../lib/toolDiff';
|
||||
import { toolGlyph, toolLabel, toolSummary } from '../../../lib/toolMeta';
|
||||
import ToolRow from '../ToolRow.vue';
|
||||
import ToolOutputBlock from './ToolOutputBlock.vue';
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
|
|
@ -69,10 +70,7 @@ function toggle(): void {
|
|||
<span v-if="chip" class="chip">{{ chip }}</span>
|
||||
</template>
|
||||
<div v-if="summaryFull" class="bb-summary">{{ summaryFull }}</div>
|
||||
<div class="bb-code">
|
||||
<div v-if="!hasOutput" class="bb-empty">Waiting for output…</div>
|
||||
<div v-for="(line, i) in tool.output ?? []" :key="i">{{ line }}</div>
|
||||
</div>
|
||||
<ToolOutputBlock :lines="tool.output" empty-text="Waiting for output…" />
|
||||
</ToolRow>
|
||||
</template>
|
||||
|
||||
|
|
@ -89,14 +87,4 @@ function toggle(): void {
|
|||
margin-bottom: 6px;
|
||||
word-break: break-all;
|
||||
}
|
||||
.bb-empty {
|
||||
color: var(--color-text-muted);
|
||||
font-style: italic;
|
||||
}
|
||||
.bb-code {
|
||||
margin-top: 10px;
|
||||
padding: 11px 13px;
|
||||
border: 1px solid var(--color-line);
|
||||
border-radius: var(--radius-md);
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import { computed, ref, watch } from 'vue';
|
|||
import type { FilePreviewRequest, ToolCall, ToolMedia } from '../../../types';
|
||||
import { toolChip, toolGlyph, toolLabel, toolSummary } from '../../../lib/toolMeta';
|
||||
import ToolRow from '../ToolRow.vue';
|
||||
import ToolOutputBlock from './ToolOutputBlock.vue';
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
|
|
@ -72,10 +73,7 @@ watch(
|
|||
<span v-if="chip" class="chip">{{ chip }}</span>
|
||||
</template>
|
||||
<div v-if="summaryFull" class="bb-summary">{{ summaryFull }}</div>
|
||||
<div class="bb-code">
|
||||
<div v-if="!hasOutput" class="bb-empty">Waiting for output…</div>
|
||||
<div v-for="(line, i) in tool.output ?? []" :key="i">{{ line }}</div>
|
||||
</div>
|
||||
<ToolOutputBlock :lines="tool.output" empty-text="Waiting for output…" />
|
||||
</ToolRow>
|
||||
</template>
|
||||
|
||||
|
|
@ -92,14 +90,4 @@ watch(
|
|||
font-size: var(--text-xs);
|
||||
flex: none;
|
||||
}
|
||||
.bb-empty {
|
||||
color: var(--color-text-muted);
|
||||
font-style: italic;
|
||||
}
|
||||
.bb-code {
|
||||
margin-top: 10px;
|
||||
padding: 11px 13px;
|
||||
border: 1px solid var(--color-line);
|
||||
border-radius: var(--radius-md);
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,42 @@
|
|||
<!-- Shared line-oriented tool output block. Keeps long outputs to a readable
|
||||
viewport while preserving the tool card's normal typography. -->
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
|
||||
const OUTPUT_SCROLL_LINE_COUNT = 50;
|
||||
|
||||
const props = defineProps<{
|
||||
lines?: string[];
|
||||
emptyText?: string;
|
||||
}>();
|
||||
|
||||
const outputLines = computed(() => props.lines ?? []);
|
||||
const isScrollable = computed(() => outputLines.value.length > OUTPUT_SCROLL_LINE_COUNT);
|
||||
const outputStyle = { '--tool-output-visible-lines': String(OUTPUT_SCROLL_LINE_COUNT) };
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="bb-code tool-output-block" :class="{ scroll: isScrollable }" :style="outputStyle">
|
||||
<div v-if="outputLines.length === 0 && emptyText" class="bb-empty">{{ emptyText }}</div>
|
||||
<div v-for="(line, i) in outputLines" :key="i">{{ line }}</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.tool-output-block {
|
||||
margin-top: var(--space-2);
|
||||
padding: var(--space-3);
|
||||
border: 1px solid var(--color-line);
|
||||
border-radius: var(--radius-md);
|
||||
background: var(--color-surface-raised);
|
||||
}
|
||||
.tool-output-block.scroll {
|
||||
max-height: calc(var(--tool-output-visible-lines) * 1lh);
|
||||
overflow-y: auto;
|
||||
scrollbar-gutter: stable;
|
||||
}
|
||||
.bb-empty {
|
||||
color: var(--color-text-muted);
|
||||
font-style: italic;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -394,7 +394,7 @@ async function onDeleteWorkspace(ws: WorkspaceView): Promise<void> {
|
|||
}
|
||||
.mgh-name {
|
||||
font-size: var(--ui-font-size-lg);
|
||||
font-weight: 500;
|
||||
font-weight: 550;
|
||||
color: var(--color-text);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
|
|
@ -402,6 +402,7 @@ async function onDeleteWorkspace(ws: WorkspaceView): Promise<void> {
|
|||
}
|
||||
.mgh-path {
|
||||
font-size: var(--text-base);
|
||||
font-weight: 425;
|
||||
color: var(--color-text-faint);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
|
|
@ -430,12 +431,14 @@ async function onDeleteWorkspace(ws: WorkspaceView): Promise<void> {
|
|||
.srow .m { flex: 1; min-width: 0; }
|
||||
.srow .m .t {
|
||||
font-size: var(--text-base);
|
||||
font-weight: 450;
|
||||
line-height: var(--leading-tight);
|
||||
color: var(--color-text);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.srow.cur .m .t { font-weight: 500; color: var(--color-accent-hover); }
|
||||
.srow.cur .m .t { color: var(--color-accent-hover); }
|
||||
|
||||
/* Running indicator — pulse dot in the indent gutter left of the title,
|
||||
mirroring the desktop SessionRow (.t.run::before). */
|
||||
|
|
@ -471,6 +474,8 @@ async function onDeleteWorkspace(ws: WorkspaceView): Promise<void> {
|
|||
}
|
||||
.srow .m .s {
|
||||
font-size: var(--text-base);
|
||||
font-weight: 475;
|
||||
font-variant-numeric: tabular-nums;
|
||||
color: var(--color-text-faint);
|
||||
margin-top: 1px;
|
||||
overflow: hidden;
|
||||
|
|
|
|||
|
|
@ -38,9 +38,9 @@ defineEmits<{ click: [event: MouseEvent] }>();
|
|||
border: none;
|
||||
border-radius: var(--radius-sm);
|
||||
background: transparent;
|
||||
color: var(--color-text-muted);
|
||||
color: var(--color-text);
|
||||
font-family: var(--font-ui);
|
||||
font-size: var(--text-sm);
|
||||
font-size: var(--text-base);
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
transition: background var(--duration-base), color var(--duration-base);
|
||||
|
|
|
|||
24
apps/kimi-web/src/components/ui/ShortcutKey.vue
Normal file
24
apps/kimi-web/src/components/ui/ShortcutKey.vue
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
<!-- Token-styled keyboard shortcut. Keeps the semantic <kbd> element reusable
|
||||
across compact UI surfaces. -->
|
||||
<template>
|
||||
<kbd class="shortcut-key">
|
||||
<slot />
|
||||
</kbd>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.shortcut-key {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex: none;
|
||||
padding: 0 var(--space-1);
|
||||
border: 1px solid var(--color-line);
|
||||
border-radius: var(--radius-sm);
|
||||
background: var(--color-surface-raised);
|
||||
color: var(--color-text-muted);
|
||||
font-family: var(--font-ui);
|
||||
font-size: var(--ui-font-size-xs);
|
||||
line-height: var(--leading-tight);
|
||||
}
|
||||
</style>
|
||||
|
|
@ -1380,7 +1380,7 @@ function formatTime(iso: string, _status: string): string {
|
|||
const diffD = diffMs / 86400000;
|
||||
if (diffD < 7) return `${Math.round(diffD)}d`;
|
||||
if (diffD < 30) return `${Math.round(diffD / 7)}w`;
|
||||
if (diffD < 365) return `${Math.round(diffD / 30)}m`;
|
||||
if (diffD < 365) return `${Math.round(diffD / 30)}mo`;
|
||||
return `${Math.round(diffD / 365)}y`;
|
||||
} catch {
|
||||
return iso;
|
||||
|
|
|
|||
|
|
@ -10,6 +10,11 @@ export default {
|
|||
gitTooltip: 'Open Files > Changed',
|
||||
detached: 'detached',
|
||||
openPr: 'Open pull request',
|
||||
prStatusOpen: 'open',
|
||||
prStatusClosed: 'closed',
|
||||
prStatusMerged: 'merged',
|
||||
prStatusDraft: 'draft',
|
||||
prStatusUnknown: 'unknown',
|
||||
options: 'Options',
|
||||
copySessionId: 'Copy Session ID',
|
||||
renameSession: 'Rename',
|
||||
|
|
|
|||
|
|
@ -31,9 +31,9 @@ export default {
|
|||
language: 'Language',
|
||||
daemon: 'Daemon',
|
||||
noSessions: 'No conversations yet',
|
||||
showMore: 'Load more ({count})',
|
||||
showMore: 'Load {count} more conversations',
|
||||
showLess: 'Show less',
|
||||
showAll: 'Show all ({count})',
|
||||
showAll: 'Show {count} more conversations',
|
||||
loadingMore: 'Loading…',
|
||||
collapseSidebar: 'Collapse sidebar',
|
||||
expandSidebar: 'Expand sidebar',
|
||||
|
|
|
|||
|
|
@ -12,23 +12,32 @@ export default {
|
|||
permissionYoloDesc: 'Auto-approve tool actions, but agent may still ask questions',
|
||||
// Plan mode pill
|
||||
planLabel: 'Plan',
|
||||
planDesc: 'Have the agent make a plan before changing files',
|
||||
planOn: 'on',
|
||||
planOff: 'off',
|
||||
planTooltip: 'Toggle plan mode (research before editing)',
|
||||
// Mode selector (Plan / Goal / Swarm)
|
||||
modesLabel: 'Mode',
|
||||
goalLabel: 'Goal',
|
||||
goalDesc: 'Track one objective until it is complete',
|
||||
swarmLabel: 'Swarm',
|
||||
swarmDesc: 'Run parallel agents for broader exploration',
|
||||
modeOff: 'Off',
|
||||
goalPlaceholder: 'What should the agent achieve?',
|
||||
goalStart: 'Start',
|
||||
goalPause: 'Pause',
|
||||
goalResume: 'Resume',
|
||||
goalCancel: 'Cancel',
|
||||
goalStatusActive: 'Active',
|
||||
goalStatusPaused: 'Paused',
|
||||
goalStatusBlocked: 'Blocked',
|
||||
goalStatusComplete: 'Complete',
|
||||
modeNotSupported: 'Not supported',
|
||||
// Thinking selector
|
||||
thinkingLabel: 'thinking',
|
||||
thinkingTooltip: 'Toggle thinking mode',
|
||||
thinkingOn: 'On',
|
||||
thinkingOff: 'Off',
|
||||
starredModels: 'Starred',
|
||||
moreModels: 'More models…',
|
||||
// Status panel
|
||||
|
|
|
|||
|
|
@ -13,6 +13,10 @@ export default {
|
|||
task: 'Task',
|
||||
swarm: 'Swarm',
|
||||
ask_user: 'Question',
|
||||
goal_create: 'Start Goal',
|
||||
goal_get: 'Read Goal',
|
||||
goal_budget: 'Set Goal Budget',
|
||||
goal_update: 'Update Goal',
|
||||
},
|
||||
swarm: {
|
||||
progress: '{done} / {total}',
|
||||
|
|
@ -32,6 +36,17 @@ export default {
|
|||
created: 'created',
|
||||
todos: '{count} items',
|
||||
},
|
||||
goal: {
|
||||
objectiveWithCriterion: '{objective} · {criterion}',
|
||||
status: 'Status: {status}',
|
||||
budget: '{value} {unit}',
|
||||
turns: '{value} turns',
|
||||
tokens: '{value} tokens',
|
||||
milliseconds: '{value} ms',
|
||||
seconds: '{value} sec',
|
||||
minutes: '{value} min',
|
||||
hours: '{value} hr',
|
||||
},
|
||||
group: {
|
||||
title: '{count} tool call | {count} tool calls',
|
||||
running: 'running',
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ export default {
|
|||
emptyConversationTitle: 'Kimi Code',
|
||||
emptyConversation: '还没有消息 —— 在下方输入开始对话',
|
||||
quickStartPlaceholder: '输入消息开始新对话…',
|
||||
thinkingSuffix: ' · thinking',
|
||||
thinkingSuffixEffort: ' · thinking: {level}',
|
||||
thinkingSuffix: ' · 思考',
|
||||
thinkingSuffixEffort: ' · 思考: {level}',
|
||||
|
||||
} as const;
|
||||
|
|
|
|||
|
|
@ -10,6 +10,11 @@ export default {
|
|||
gitTooltip: '打开「文件 > 改动」',
|
||||
detached: '游离',
|
||||
openPr: '打开 Pull Request',
|
||||
prStatusOpen: '已打开',
|
||||
prStatusClosed: '已关闭',
|
||||
prStatusMerged: '已合并',
|
||||
prStatusDraft: '草稿',
|
||||
prStatusUnknown: '未知',
|
||||
options: '选项',
|
||||
copySessionId: '复制 Session ID',
|
||||
renameSession: '重命名',
|
||||
|
|
|
|||
|
|
@ -31,9 +31,9 @@ export default {
|
|||
language: '语言',
|
||||
daemon: '后台',
|
||||
noSessions: '暂无对话',
|
||||
showMore: '加载更多 ({count})',
|
||||
showMore: '加载更多 {count} 个对话',
|
||||
showLess: '收起',
|
||||
showAll: '展开 ({count})',
|
||||
showAll: '展开剩余 {count} 个对话',
|
||||
loadingMore: '加载中…',
|
||||
collapseSidebar: '收起侧边栏',
|
||||
expandSidebar: '展开侧边栏',
|
||||
|
|
|
|||
|
|
@ -8,27 +8,36 @@ export default {
|
|||
permissionAuto: '完全自主',
|
||||
permissionYolo: '自动通过',
|
||||
permissionManualDesc: '每个工具操作都需要你手动确认',
|
||||
permissionAutoDesc: '完全自主运行,agent 自己做决定,不再询问',
|
||||
permissionAutoDesc: '完全自主运行,智能体自己做决定,不再询问',
|
||||
permissionYoloDesc: '自动批准工具操作,但遇到关键问题仍会询问',
|
||||
// 计划模式
|
||||
planLabel: '计划',
|
||||
planDesc: '先让智能体梳理计划,再修改文件',
|
||||
planOn: '开',
|
||||
planOff: '关',
|
||||
planTooltip: '切换计划模式(先调研再修改)',
|
||||
// 模式选择器(计划 / 目标 / Swarm)
|
||||
modesLabel: '模式',
|
||||
goalLabel: '目标',
|
||||
goalDesc: '持续跟踪一个目标,直到任务完成',
|
||||
swarmLabel: 'Swarm',
|
||||
swarmDesc: '并行运行多个智能体,适合大范围探索',
|
||||
modeOff: '未启用',
|
||||
goalPlaceholder: '让 Agent 完成什么目标?',
|
||||
goalPlaceholder: '让智能体完成什么目标?',
|
||||
goalStart: '开始',
|
||||
goalPause: '暂停',
|
||||
goalResume: '继续',
|
||||
goalCancel: '取消',
|
||||
goalStatusActive: '进行中',
|
||||
goalStatusPaused: '已暂停',
|
||||
goalStatusBlocked: '已阻塞',
|
||||
goalStatusComplete: '已完成',
|
||||
modeNotSupported: '暂不支持',
|
||||
// 思考强度选择
|
||||
thinkingLabel: '思考',
|
||||
thinkingTooltip: '切换思考模式',
|
||||
thinkingOn: '开',
|
||||
thinkingOff: '关',
|
||||
starredModels: '收藏',
|
||||
moreModels: '更多模型…',
|
||||
// 状态面板
|
||||
|
|
|
|||
|
|
@ -13,6 +13,10 @@ export default {
|
|||
task: '任务',
|
||||
swarm: 'Swarm',
|
||||
ask_user: '提问',
|
||||
goal_create: '启动目标',
|
||||
goal_get: '读取目标',
|
||||
goal_budget: '设置目标预算',
|
||||
goal_update: '更新目标',
|
||||
},
|
||||
swarm: {
|
||||
progress: '{done} / {total}',
|
||||
|
|
@ -32,6 +36,17 @@ export default {
|
|||
created: '已创建',
|
||||
todos: '{count} 项',
|
||||
},
|
||||
goal: {
|
||||
objectiveWithCriterion: '{objective} · {criterion}',
|
||||
status: '状态:{status}',
|
||||
budget: '{value} {unit}',
|
||||
turns: '{value} 轮',
|
||||
tokens: '{value} token',
|
||||
milliseconds: '{value} 毫秒',
|
||||
seconds: '{value} 秒',
|
||||
minutes: '{value} 分钟',
|
||||
hours: '{value} 小时',
|
||||
},
|
||||
group: {
|
||||
title: '{count} 个工具调用',
|
||||
running: '运行中',
|
||||
|
|
|
|||
|
|
@ -43,6 +43,7 @@ import RiExpandRightLine from '~icons/ri/expand-right-line';
|
|||
import RiExternalLinkLine from '~icons/ri/external-link-line';
|
||||
import RiFileAddLine from '~icons/ri/file-add-line';
|
||||
import RiFileCopyLine from '~icons/ri/file-copy-line';
|
||||
import RiFileEditLine from '~icons/ri/file-edit-line';
|
||||
import RiFileLine from '~icons/ri/file-line';
|
||||
import RiFileTextLine from '~icons/ri/file-text-line';
|
||||
import RiFlashlightLine from '~icons/ri/flashlight-line';
|
||||
|
|
@ -61,6 +62,7 @@ import RiLoginBoxLine from '~icons/ri/login-box-line';
|
|||
import RiMailLine from '~icons/ri/mail-line';
|
||||
import RiMessageLine from '~icons/ri/message-line';
|
||||
import RiMoreLine from '~icons/ri/more-line';
|
||||
import RiPauseFill from '~icons/ri/pause-fill';
|
||||
import RiPencilLine from '~icons/ri/pencil-line';
|
||||
import RiPlayFill from '~icons/ri/play-fill';
|
||||
import RiQuestionLine from '~icons/ri/question-line';
|
||||
|
|
@ -72,6 +74,7 @@ import RiStarFill from '~icons/ri/star-fill';
|
|||
import RiStarLine from '~icons/ri/star-line';
|
||||
import RiStopFill from '~icons/ri/stop-fill';
|
||||
import RiSubtractLine from '~icons/ri/subtract-line';
|
||||
import RiTargetLine from '~icons/ri/target-line';
|
||||
import RiTerminalBoxLine from '~icons/ri/terminal-box-line';
|
||||
import RiTimeLine from '~icons/ri/time-line';
|
||||
import RiToolsLine from '~icons/ri/tools-line';
|
||||
|
|
@ -104,6 +107,7 @@ import RawExpandRightLine from '~icons/ri/expand-right-line?raw';
|
|||
import RawExternalLinkLine from '~icons/ri/external-link-line?raw';
|
||||
import RawFileAddLine from '~icons/ri/file-add-line?raw';
|
||||
import RawFileCopyLine from '~icons/ri/file-copy-line?raw';
|
||||
import RawFileEditLine from '~icons/ri/file-edit-line?raw';
|
||||
import RawFileLine from '~icons/ri/file-line?raw';
|
||||
import RawFileTextLine from '~icons/ri/file-text-line?raw';
|
||||
import RawFlashlightLine from '~icons/ri/flashlight-line?raw';
|
||||
|
|
@ -122,6 +126,7 @@ import RawLoginBoxLine from '~icons/ri/login-box-line?raw';
|
|||
import RawMailLine from '~icons/ri/mail-line?raw';
|
||||
import RawMessageLine from '~icons/ri/message-line?raw';
|
||||
import RawMoreLine from '~icons/ri/more-line?raw';
|
||||
import RawPauseFill from '~icons/ri/pause-fill?raw';
|
||||
import RawPencilLine from '~icons/ri/pencil-line?raw';
|
||||
import RawPlayFill from '~icons/ri/play-fill?raw';
|
||||
import RawQuestionLine from '~icons/ri/question-line?raw';
|
||||
|
|
@ -133,6 +138,7 @@ import RawStarFill from '~icons/ri/star-fill?raw';
|
|||
import RawStarLine from '~icons/ri/star-line?raw';
|
||||
import RawStopFill from '~icons/ri/stop-fill?raw';
|
||||
import RawSubtractLine from '~icons/ri/subtract-line?raw';
|
||||
import RawTargetLine from '~icons/ri/target-line?raw';
|
||||
import RawTerminalBoxLine from '~icons/ri/terminal-box-line?raw';
|
||||
import RawTimeLine from '~icons/ri/time-line?raw';
|
||||
import RawToolsLine from '~icons/ri/tools-line?raw';
|
||||
|
|
@ -177,6 +183,7 @@ export type IconName =
|
|||
| 'folder-solid'
|
||||
| 'file'
|
||||
| 'file-text'
|
||||
| 'file-edit'
|
||||
| 'file-plus'
|
||||
| 'file-off'
|
||||
| 'image-off'
|
||||
|
|
@ -197,6 +204,8 @@ export type IconName =
|
|||
| 'alert-triangle'
|
||||
| 'clock'
|
||||
| 'sparkles'
|
||||
| 'target'
|
||||
| 'pause'
|
||||
| 'play'
|
||||
| 'stop'
|
||||
| 'star'
|
||||
|
|
@ -256,6 +265,7 @@ export const ICONS: Record<IconName, IconEntry> = {
|
|||
'folder-solid': entry(RiFolderFill, RawFolderFill),
|
||||
file: entry(RiFileLine, RawFileLine),
|
||||
'file-text': entry(RiFileTextLine, RawFileTextLine),
|
||||
'file-edit': entry(RiFileEditLine, RawFileEditLine),
|
||||
'file-plus': entry(RiFileAddLine, RawFileAddLine),
|
||||
'file-off': entry(RiFileLine, RawFileLine),
|
||||
'image-off': entry(RiImageLine, RawImageLine),
|
||||
|
|
@ -276,6 +286,8 @@ export const ICONS: Record<IconName, IconEntry> = {
|
|||
'alert-triangle': entry(RiAlertLine, RawAlertLine),
|
||||
clock: entry(RiTimeLine, RawTimeLine),
|
||||
sparkles: entry(RiSparklingLine, RawSparklingLine),
|
||||
target: entry(RiTargetLine, RawTargetLine),
|
||||
pause: entry(RiPauseFill, RawPauseFill),
|
||||
play: entry(RiPlayFill, RawPlayFill),
|
||||
stop: entry(RiStopFill, RawStopFill),
|
||||
star: entry(RiStarFill, RawStarFill),
|
||||
|
|
@ -353,6 +365,7 @@ export const ICON_GROUPS: ReadonlyArray<readonly [string, readonly IconName[]]>
|
|||
'folder-solid',
|
||||
'file',
|
||||
'file-text',
|
||||
'file-edit',
|
||||
'file-plus',
|
||||
'file-off',
|
||||
'image-off',
|
||||
|
|
@ -365,6 +378,7 @@ export const ICON_GROUPS: ReadonlyArray<readonly [string, readonly IconName[]]>
|
|||
'check-list',
|
||||
'bolt',
|
||||
'git-pull-request',
|
||||
'target',
|
||||
'calendar-schedule',
|
||||
'calendar-todo',
|
||||
'calendar-close',
|
||||
|
|
@ -379,6 +393,7 @@ export const ICON_GROUPS: ReadonlyArray<readonly [string, readonly IconName[]]>
|
|||
'alert-triangle',
|
||||
'clock',
|
||||
'sparkles',
|
||||
'pause',
|
||||
'play',
|
||||
'stop',
|
||||
'star',
|
||||
|
|
|
|||
|
|
@ -25,6 +25,10 @@ const TOOL_LABEL_KEYS: Record<string, string> = {
|
|||
task: 'tools.label.task',
|
||||
agentswarm: 'tools.label.swarm',
|
||||
askuserquestion: 'tools.label.ask_user',
|
||||
creategoal: 'tools.label.goal_create',
|
||||
getgoal: 'tools.label.goal_get',
|
||||
setgoalbudget: 'tools.label.goal_budget',
|
||||
updategoal: 'tools.label.goal_update',
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
|
@ -60,6 +64,10 @@ const NAME_ALIASES: Record<string, string> = {
|
|||
subagent: 'task',
|
||||
websearch: 'search',
|
||||
web_search: 'search',
|
||||
create_goal: 'creategoal',
|
||||
get_goal: 'getgoal',
|
||||
set_goal_budget: 'setgoalbudget',
|
||||
update_goal: 'updategoal',
|
||||
};
|
||||
|
||||
export function normalizeToolName(name: string): string {
|
||||
|
|
@ -93,6 +101,10 @@ const TOOL_GLYPH: Record<string, IconName> = {
|
|||
task: 'sparkles',
|
||||
agentswarm: 'git-pull-request',
|
||||
askuserquestion: 'help-circle',
|
||||
creategoal: 'target',
|
||||
getgoal: 'target',
|
||||
setgoalbudget: 'target',
|
||||
updategoal: 'target',
|
||||
// Cron scheduling tools share a calendar motif: schedule / list / cancel.
|
||||
croncreate: 'calendar-schedule',
|
||||
cronlist: 'calendar-todo',
|
||||
|
|
@ -182,6 +194,41 @@ function filePath(d: Record<string, unknown>): string | undefined {
|
|||
return str(d.path) ?? str(d.file_path) ?? str(d.filePath) ?? str(d.filename);
|
||||
}
|
||||
|
||||
const GOAL_STATUS_KEYS: Record<string, string> = {
|
||||
active: 'status.goalStatusActive',
|
||||
blocked: 'status.goalStatusBlocked',
|
||||
complete: 'status.goalStatusComplete',
|
||||
};
|
||||
|
||||
function goalStatusLabel(value: unknown): string | undefined {
|
||||
const status = str(value);
|
||||
if (!status) return undefined;
|
||||
const key = GOAL_STATUS_KEYS[status];
|
||||
return key ? t(key) : status;
|
||||
}
|
||||
|
||||
function goalBudgetSummary(d: Record<string, unknown>): string | undefined {
|
||||
const value = num(d.value);
|
||||
const unit = str(d.unit);
|
||||
if (value === undefined || !unit) return undefined;
|
||||
switch (unit) {
|
||||
case 'turns':
|
||||
return t('tools.goal.turns', { value });
|
||||
case 'tokens':
|
||||
return t('tools.goal.tokens', { value });
|
||||
case 'milliseconds':
|
||||
return t('tools.goal.milliseconds', { value });
|
||||
case 'seconds':
|
||||
return t('tools.goal.seconds', { value });
|
||||
case 'minutes':
|
||||
return t('tools.goal.minutes', { value });
|
||||
case 'hours':
|
||||
return t('tools.goal.hours', { value });
|
||||
default:
|
||||
return t('tools.goal.budget', { value, unit });
|
||||
}
|
||||
}
|
||||
|
||||
const BASH_MAX = 64;
|
||||
|
||||
/**
|
||||
|
|
@ -255,6 +302,27 @@ export function toolSummary(name: string, arg: string, full = false): string {
|
|||
if (items) return c(t('tools.chip.todos', { count: items.length }));
|
||||
return fallback();
|
||||
}
|
||||
case 'creategoal': {
|
||||
if (full) return fallback();
|
||||
const objective = str(d.objective);
|
||||
const criterion = str(d.completionCriterion);
|
||||
if (objective && criterion) return c(t('tools.goal.objectiveWithCriterion', { objective, criterion }));
|
||||
return objective ? c(objective) : fallback();
|
||||
}
|
||||
case 'getgoal': {
|
||||
if (full) return fallback();
|
||||
return '';
|
||||
}
|
||||
case 'setgoalbudget': {
|
||||
if (full) return fallback();
|
||||
const summary = goalBudgetSummary(d);
|
||||
return summary ? c(summary) : fallback();
|
||||
}
|
||||
case 'updategoal': {
|
||||
if (full) return fallback();
|
||||
const status = goalStatusLabel(d.status);
|
||||
return status ? c(t('tools.goal.status', { status })) : fallback();
|
||||
}
|
||||
default:
|
||||
return fallback();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,8 @@ import { createApp } from 'vue';
|
|||
import App from './App.vue';
|
||||
import i18n from './i18n';
|
||||
import { installClientErrorCapture } from './debug/trace';
|
||||
import '@fontsource-variable/inter/wght.css';
|
||||
import '@fontsource-variable/inter/opsz.css';
|
||||
import '@fontsource-variable/inter/opsz-italic.css';
|
||||
import '@fontsource-variable/jetbrains-mono/wght.css';
|
||||
import './style.css';
|
||||
|
||||
|
|
|
|||
|
|
@ -206,9 +206,8 @@ summary {
|
|||
--content-font-size: calc(var(--base-ui-font-size) + 1px);
|
||||
--sidebar-ui-font-size: calc(var(--base-ui-font-size) + 1px);
|
||||
--mono: "JetBrains Mono Variable", "JetBrains Mono", ui-monospace, "SF Mono", Menlo, Consolas, "Liberation Mono", monospace;
|
||||
/* Body/UI font follows the design-system canonical token (system UI font
|
||||
first; Inter intentionally NOT in the body chain — it stays reserved for
|
||||
--font-display headings). Mirrors --font-ui so the two can never drift. */
|
||||
/* Body/UI font follows the design-system canonical token. Mirrors --font-ui
|
||||
so the legacy alias can never drift from the current text face. */
|
||||
--sans: var(--font-ui);
|
||||
color-scheme: light dark;
|
||||
}
|
||||
|
|
@ -454,14 +453,15 @@ html[data-color-scheme="dark"][data-accent="mono"] {
|
|||
--duration-slow: 260ms;
|
||||
|
||||
/* -- type families ------------------------------------------------------- */
|
||||
/* UI/body use the platform's native UI font first (design-system §02);
|
||||
Inter is intentionally NOT in this chain — it is reserved for
|
||||
--font-display (optional headings / brand wordmarks) below. */
|
||||
--font-ui: -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC",
|
||||
"Hiragino Sans GB", "Microsoft YaHei", "Source Han Sans SC", "Noto Sans SC",
|
||||
Roboto, Ubuntu, "Helvetica Neue", Arial, sans-serif, "Apple Color Emoji",
|
||||
"Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
|
||||
--font-display: "Inter Variable", "Inter", var(--font-ui);
|
||||
/* UI/body use self-hosted Inter first. CJK and platform system UI families
|
||||
stay late in the fallback chain so Latin glyphs resolve to Inter while
|
||||
Chinese text can still fall through to native CJK fonts. */
|
||||
--font-ui: "Inter Variable", "Inter", "Helvetica Neue", Arial,
|
||||
"PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", "Source Han Sans SC",
|
||||
"Noto Sans SC", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
|
||||
Ubuntu, sans-serif, "Apple Color Emoji", "Segoe UI Emoji",
|
||||
"Segoe UI Symbol", "Noto Color Emoji";
|
||||
--font-display: var(--font-ui);
|
||||
--font-mono: "JetBrains Mono Variable", "JetBrains Mono", ui-monospace,
|
||||
"SF Mono", Menlo, Consolas, "Liberation Mono", monospace;
|
||||
|
||||
|
|
@ -643,6 +643,7 @@ body {
|
|||
font-size: var(--ui-font-size);
|
||||
font-weight: 400;
|
||||
line-height: 1.6;
|
||||
font-optical-sizing: auto;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
text-rendering: auto;
|
||||
|
|
|
|||
|
|
@ -227,18 +227,19 @@ onUnmounted(() => {
|
|||
<p>All disabled controls use <code>opacity:.5</code> + <code>cursor:not-allowed</code> uniformly; do not separately grey out or recolor.</p>
|
||||
|
||||
<h3 class="sub">Font families</h3>
|
||||
<p>Kimi Web uses two font families: <b>--font-ui</b> (UI and body, system fonts first) and <b>--font-mono</b> (code and monospace). Components always reference the variables; do not hard-code font names.</p>
|
||||
<p>Kimi Web uses two font families: <b>--font-ui</b> (UI and body, Inter first) and <b>--font-mono</b> (code and monospace). Components always reference the variables; do not hard-code font names.</p>
|
||||
|
||||
<h4 class="mini">--font-ui · UI & body (system fonts first)</h4>
|
||||
<p>Body and UI use each platform's native UI font — close to the system feel, comfortable for long text and CJK. Fallback chain:</p>
|
||||
<div class="code"><div class="code-bar"><span class="d"></span><span class="d"></span><span class="d"></span><span class="fn">--font-ui</span></div><pre>--font-ui: -apple-system, BlinkMacSystemFont, "Segoe UI",
|
||||
<h4 class="mini">--font-ui · UI & body (Inter first)</h4>
|
||||
<p>Body and UI use self-hosted Inter as the primary face. CJK and platform system UI fonts sit late in the fallback chain so Latin glyphs resolve to Inter while Chinese text can fall through to native CJK fonts:</p>
|
||||
<div class="code"><div class="code-bar"><span class="d"></span><span class="d"></span><span class="d"></span><span class="fn">--font-ui</span></div><pre>--font-ui: "Inter Variable", "Inter", "Helvetica Neue", Arial,
|
||||
"PingFang SC", "Microsoft YaHei", "Noto Sans SC",
|
||||
"Helvetica Neue", Arial, sans-serif,
|
||||
-apple-system, BlinkMacSystemFont, "Segoe UI",
|
||||
Roboto, Ubuntu, sans-serif,
|
||||
"Apple Color Emoji", "Segoe UI Emoji", "Noto Color Emoji";</pre></div>
|
||||
<ul class="clean">
|
||||
<li>System UI fonts first: SF Pro on macOS / iOS, Segoe UI on Windows.</li>
|
||||
<li>CJK next: PingFang SC (macOS) / Microsoft YaHei (Windows) / Noto Sans SC (Linux).</li>
|
||||
<li>Helvetica Neue / Arial / sans-serif as generic fallbacks; emoji fonts at the end.</li>
|
||||
<li>Inter first: self-hosted Latin UI and body text, loaded through the optical-size normal and italic variable faces.</li>
|
||||
<li>Western fallbacks next: Helvetica Neue / Arial for environments where Inter cannot load.</li>
|
||||
<li>CJK and system UI fallbacks late: PingFang SC / Microsoft YaHei / Noto Sans SC, then platform UI fonts and emoji fonts.</li>
|
||||
</ul>
|
||||
|
||||
<h4 class="mini">--font-mono · Code & monospace</h4>
|
||||
|
|
@ -251,8 +252,8 @@ onUnmounted(() => {
|
|||
<thead><tr><th>Font</th><th>Source</th><th>Bundled</th><th>Usage</th></tr></thead>
|
||||
<tbody>
|
||||
<tr><td class="tk">JetBrains Mono</td><td class="val">@fontsource-variable/jetbrains-mono</td><td class="val">✓ self-hosted</td><td>monospace / code (--font-mono)</td></tr>
|
||||
<tr><td class="tk">Inter</td><td class="val">@fontsource-variable/inter</td><td class="val">✓ self-hosted</td><td>optional: page titles / brand wordmark (--font-display)</td></tr>
|
||||
<tr><td class="tk">System UI / CJK fonts</td><td class="val">operating system</td><td class="val">—</td><td>body / UI (--font-ui), not bundled</td></tr>
|
||||
<tr><td class="tk">Inter</td><td class="val">@fontsource-variable/inter/opsz.css + opsz-italic.css</td><td class="val">✓ self-hosted</td><td>UI / body / display (--font-ui, --font-display), wght 100-900, opsz 14-32, normal + italic</td></tr>
|
||||
<tr><td class="tk">System UI / CJK fonts</td><td class="val">operating system</td><td class="val">—</td><td>late fallback for UI / body, not bundled</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="callout good"><span class="ico">✓</span><div>
|
||||
|
|
@ -262,8 +263,9 @@ onUnmounted(() => {
|
|||
<h4 class="mini">Usage rules</h4>
|
||||
<ul class="clean check">
|
||||
<li>Components always use <code>var(--font-ui)</code> / <code>var(--font-mono)</code>; do not hard-code font names like <code>'Inter'</code> / <code>'JetBrains Mono'</code>.</li>
|
||||
<li>Body / UI use <code>--font-ui</code> (system fonts first); code / monospace use <code>--font-mono</code> (JetBrains Mono).</li>
|
||||
<li>Inter is used only for headings / brand scenarios that need a unified look (optional <code>--font-display</code>); it is no longer the body default.</li>
|
||||
<li>Body / UI use <code>--font-ui</code> (Inter first); code / monospace use <code>--font-mono</code> (JetBrains Mono).</li>
|
||||
<li>Inter is loaded from the complete optical-size variable faces, including normal and italic styles; <code>font-optical-sizing: auto</code> is enabled globally.</li>
|
||||
<li>CJK and platform system UI fonts stay late in the <code>--font-ui</code> fallback chain, after Inter and Western fallbacks.</li>
|
||||
</ul>
|
||||
|
||||
<h3 class="sub">Type scale & weight</h3>
|
||||
|
|
@ -281,7 +283,7 @@ onUnmounted(() => {
|
|||
<table class="dt">
|
||||
<thead><tr><th>Token</th><th>Value</th><th>Usage</th></tr></thead>
|
||||
<tbody>
|
||||
<tr><td class="tk">--font-ui</td><td class="val">-apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC"…</td><td>UI & body (system fonts first)</td></tr>
|
||||
<tr><td class="tk">--font-ui</td><td class="val">"Inter Variable", "Inter", "Helvetica Neue", Arial…</td><td>UI & body (Inter first)</td></tr>
|
||||
<tr><td class="tk">--font-mono</td><td class="val">JetBrains Mono…</td><td>code, tool names, line numbers, diffs</td></tr>
|
||||
<tr><td class="tk">--base-ui-font-size</td><td class="val">14px user preference</td><td>root setting that drives UI, reading body, and sidebar font sizes</td></tr>
|
||||
<tr><td class="tk">--content-font-size</td><td class="val">calc(base + 1px)</td><td>chat Markdown, message bubbles, composer</td></tr>
|
||||
|
|
|
|||
|
|
@ -152,7 +152,7 @@
|
|||
inherit (finalAttrs) pname version src pnpmWorkspaces;
|
||||
inherit pnpm;
|
||||
fetcherVersion = 3;
|
||||
hash = "sha256-RPjCWL7NqDSKgpHGL16zPlUOfjWN2rkaDY/4GFAD8VA=";
|
||||
hash = "sha256-hUn5Srn3HnEEzU5DLxgjIzFjI0ukM3iSP4QagftEXdE=";
|
||||
};
|
||||
|
||||
nativeBuildInputs = [
|
||||
|
|
|
|||
10
pnpm-lock.yaml
generated
10
pnpm-lock.yaml
generated
|
|
@ -166,8 +166,11 @@ importers:
|
|||
|
||||
apps/kimi-web:
|
||||
dependencies:
|
||||
'@chenglou/pretext':
|
||||
specifier: 0.0.8
|
||||
version: 0.0.8
|
||||
'@fontsource-variable/inter':
|
||||
specifier: ^5.2.8
|
||||
specifier: 5.2.8
|
||||
version: 5.2.8
|
||||
'@fontsource-variable/jetbrains-mono':
|
||||
specifier: ^5.2.8
|
||||
|
|
@ -995,6 +998,9 @@ packages:
|
|||
'@chenglou/pretext@0.0.5':
|
||||
resolution: {integrity: sha512-A8GZN10REdFGsyuiUgLV8jjPDDFMg5GmgxGWV0I3igxBOnzj+jgz2VMmVD7g+SFyoctfeqHFxbNatKSzVRWtRg==}
|
||||
|
||||
'@chenglou/pretext@0.0.8':
|
||||
resolution: {integrity: sha512-yqm2GMxnPI7VHcHwe84P8ZF0JK/2d2DMKPqMN+s95jQhwDMYYXKVFVJUMEaVWckQStdsjdLav/0Vu+d9YbtGxA==}
|
||||
|
||||
'@chevrotain/types@11.1.2':
|
||||
resolution: {integrity: sha512-U+HFai5+zmJCkK86QsaJtoITlboZHBqrVketcO2ROv865xfCMSFpELQoz1GkX5GzME8pTa+3kbKrZHQtI0gdbw==}
|
||||
|
||||
|
|
@ -8077,6 +8083,8 @@ snapshots:
|
|||
|
||||
'@chenglou/pretext@0.0.5': {}
|
||||
|
||||
'@chenglou/pretext@0.0.8': {}
|
||||
|
||||
'@chevrotain/types@11.1.2': {}
|
||||
|
||||
'@colors/colors@1.5.0':
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue