fix(web): clarify archive session confirmation copy (#1428)

* fix(web): clarify archive session confirmation copy

* fix(web): drop delete mention from archive confirmation copy

* feat(web): add archived-sessions restore entry to mobile settings

Mobile used a separate settings bottom-sheet with no archived-sessions
restore surface, so the archive confirmation pointing users to Settings
to restore was untrue on mobile. Add an Archived sessions sub-view to
the mobile settings sheet with search, sort and restore, mirroring the
desktop Settings tab.

* fix(web): refresh mobile archived list and use Input primitive

Refresh the archived-sessions list each time the mobile sub-view opens so
sessions archived from the mobile switcher show up without remounting the
sheet, and replace the hand-rolled search input plus inline svg with the
shared Input primitive.

* fix(web): use Button primitive for mobile archived restore

Render the mobile Archived sessions restore action with the shared Button
primitive instead of a hand-rolled button and scoped CSS, matching the
desktop archived list and the app's design-system rule.
This commit is contained in:
qer 2026-07-06 22:48:01 +08:00 committed by GitHub
parent 2374bc41c3
commit 903e8ed93a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 228 additions and 4 deletions

View file

@ -0,0 +1,5 @@
---
"@moonshot-ai/kimi-code": patch
---
web: Add an Archived sessions entry to the mobile settings sheet and clarify the archive confirmation to mention restoring from Settings.

View file

@ -6,11 +6,12 @@
<!-- (theme / color scheme / language) and the sign-in/out entry, which previously -->
<!-- had no mobile counterpart. -->
<script setup lang="ts">
import { computed } from 'vue';
import { computed, ref, watch } from 'vue';
import { useI18n } from 'vue-i18n';
import type { ConversationStatus, PermissionMode } from '../../types';
import type { AppModel, ThinkingLevel } from '../../api/types';
import type { AppModel, AppSession, ThinkingLevel } from '../../api/types';
import type { ColorScheme } from '../../composables/useKimiWebClient';
import { useKimiWebClient } from '../../composables/useKimiWebClient';
import {
coerceThinkingForModel,
commitLevel,
@ -20,6 +21,8 @@ import {
} from '../../lib/modelThinking';
import BottomSheet from '../dialogs/BottomSheet.vue';
import LanguageSwitcher from '../settings/LanguageSwitcher.vue';
import Button from '../ui/Button.vue';
import Input from '../ui/Input.vue';
import SegmentedControl from '../ui/SegmentedControl.vue';
const { t } = useI18n();
@ -146,6 +149,94 @@ function onLogout(): void {
emit('logout');
emit('update:modelValue', false);
}
// ---------------------------------------------------------------------------
// Archived-sessions sub-view mirrors the desktop Settings "Archived" tab so
// the mobile archive confirmation (which points users to Settings to restore)
// is true here too. Loads all archived sessions once when the view opens;
// search + sort run client-side over the full set.
// ---------------------------------------------------------------------------
const client = useKimiWebClient();
type SheetView = 'main' | 'archived';
const view = ref<SheetView>('main');
const archivedItems = ref<AppSession[]>([]);
const archivedLoading = ref(false);
const archivedLoaded = ref(false);
const archiveQuery = ref('');
const archiveSort = ref<'archived-desc' | 'created-desc' | 'name-asc'>('archived-desc');
const ARCHIVED_PAGE_SIZE = 100;
async function loadAllArchived(): Promise<void> {
if (archivedLoading.value) return;
archivedLoading.value = true;
archivedLoaded.value = false;
try {
const all: AppSession[] = [];
let beforeId: string | undefined;
for (;;) {
const page = await client.loadArchivedSessions({ beforeId, pageSize: ARCHIVED_PAGE_SIZE });
all.push(...page.items);
if (!page.hasMore || page.items.length === 0) break;
const next = page.items.at(-1)?.id;
if (next === undefined) break;
beforeId = next;
}
archivedItems.value = all;
archivedLoaded.value = true;
} catch (err) {
console.warn('loadAllArchived failed', err);
} finally {
archivedLoading.value = false;
}
}
function openArchived(): void {
view.value = 'archived';
archiveQuery.value = '';
void loadAllArchived();
}
function backToMain(): void {
view.value = 'main';
}
const filteredArchived = computed<AppSession[]>(() => {
const q = archiveQuery.value.trim().toLowerCase();
let rows = archivedItems.value.filter((s) => s.archived === true);
if (q) rows = rows.filter((s) => s.title.toLowerCase().includes(q));
rows = rows.slice();
if (archiveSort.value === 'archived-desc') {
rows.sort((a, b) => b.updatedAt.localeCompare(a.updatedAt));
} else if (archiveSort.value === 'created-desc') {
rows.sort((a, b) => b.createdAt.localeCompare(a.createdAt));
} else {
rows.sort((a, b) => a.title.localeCompare(b.title, 'zh'));
}
return rows;
});
async function onRestore(id: string): Promise<void> {
const ok = await client.restoreSession(id);
if (ok) archivedItems.value = archivedItems.value.filter((s) => s.id !== id);
}
function archiveTime(iso: string): string {
const d = new Date(iso);
if (Number.isNaN(d.getTime())) return iso;
const pad = (n: number): string => String(n).padStart(2, '0');
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}`;
}
// Reset to the main view whenever the sheet is closed, so reopening starts at
// the top rather than mid-list.
watch(
() => props.modelValue,
(open) => {
if (!open) view.value = 'main';
},
);
</script>
<template>
@ -154,6 +245,7 @@ function onLogout(): void {
:title="t('mobile.settingsTitle')"
@update:model-value="emit('update:modelValue', $event)"
>
<template v-if="view === 'main'">
<div class="group-title">{{ t('mobile.groupSession') }}</div>
<!-- Model opens ModelPicker -->
@ -228,6 +320,15 @@ function onLogout(): void {
<div class="group-title">{{ t('mobile.groupApp') }}</div>
<!-- Archived sessions opens the archived restore sub-view -->
<button type="button" class="srow" @click="openArchived">
<span class="srow-main">
<span class="srow-label">{{ t('mobile.archivedSessions') }}</span>
<span class="srow-sub">{{ t('mobile.archivedSessionsSub') }}</span>
</span>
<span class="chev"></span>
</button>
<!-- App preferences (the desktop settings-popover controls) -->
<div class="srow read-only pref">
<span class="srow-main">
@ -297,6 +398,53 @@ function onLogout(): void {
</span>
<span class="srow-val dim">{{ serverVersion }}</span>
</div>
</template>
<template v-else>
<!-- Archived sessions sub-view -->
<div class="arch-subhead">
<button type="button" class="arch-back" @click="backToMain">
<span class="chev back"></span> {{ t('mobile.archivedBack') }}
</button>
<span class="arch-count">{{ t('mobile.sessionCount', { n: filteredArchived.length }) }}</span>
</div>
<div class="arch-tools">
<Input
class="arch-search-input"
:model-value="archiveQuery"
size="sm"
:placeholder="t('settings.archivedSearch')"
@update:model-value="archiveQuery = $event"
/>
<SegmentedControl
size="sm"
:model-value="archiveSort"
:options="[
{ value: 'archived-desc', label: t('settings.archivedSortArchived') },
{ value: 'created-desc', label: t('settings.archivedSortCreated') },
{ value: 'name-asc', label: t('settings.archivedSortName') },
]"
@update:model-value="archiveSort = $event as 'archived-desc' | 'created-desc' | 'name-asc'"
/>
</div>
<div v-if="archivedLoading" class="arch-empty">{{ t('settings.archivedLoadingAll') }}</div>
<template v-else-if="filteredArchived.length > 0">
<div v-for="s in filteredArchived" :key="s.id" class="arch-row">
<div class="arch-meta">
<div class="arch-name">{{ s.title }}</div>
<div class="arch-time">{{ t('settings.archivedAt', { time: archiveTime(s.updatedAt) }) }}</div>
</div>
<Button variant="secondary" size="sm" @click="onRestore(s.id)">{{ t('settings.archivedRestore') }}</Button>
</div>
</template>
<div v-else class="arch-empty">
{{ archivedItems.length === 0 ? t('settings.archivedEmpty') : t('settings.archivedNoMatch') }}
</div>
</template>
</BottomSheet>
</template>
@ -478,4 +626,69 @@ function onLogout(): void {
.srow,
.srow-sub,
.srow-val { font-family: var(--sans); }
/* Archived sessions sub-view */
.arch-subhead {
display: flex;
align-items: center;
justify-content: space-between;
gap: var(--space-3);
padding: var(--space-2) var(--space-3) var(--space-1);
}
.arch-back {
display: inline-flex;
align-items: center;
gap: 2px;
border: none;
background: none;
padding: var(--space-1) var(--space-2) var(--space-1) 0;
font-family: var(--font-ui);
font-size: var(--text-base);
color: var(--color-accent-hover);
cursor: pointer;
}
.chev.back { font-size: 20px; }
.arch-count {
font-family: var(--font-ui);
font-size: var(--text-sm);
color: var(--color-text-faint);
}
.arch-tools {
display: flex;
align-items: center;
gap: var(--space-2);
padding: var(--space-2) var(--space-3);
flex-wrap: wrap;
}
.arch-search-input { flex: 1; min-width: 160px; }
.arch-row {
display: flex;
align-items: center;
gap: var(--space-3);
min-height: 56px;
padding: var(--space-2) var(--space-3);
border-top: 1px solid var(--color-line);
}
.arch-row:first-of-type { border-top: none; }
.arch-meta { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 1px; }
.arch-name {
font-family: var(--font-ui);
font-size: var(--text-base);
color: var(--color-text);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.arch-time {
font-family: var(--font-mono);
font-size: var(--text-xs);
color: var(--color-text-faint);
}
.arch-empty {
padding: var(--space-6) var(--space-4);
text-align: center;
font-family: var(--font-ui);
font-size: var(--text-sm);
color: var(--color-text-faint);
}
</style>

View file

@ -16,4 +16,7 @@ export default {
permYoloSub: 'auto-approve all',
planModeSub: 'Plan mode',
swarmModeSub: 'Swarm mode',
archivedSessions: 'Archived sessions',
archivedSessionsSub: 'Browse and restore archived sessions',
archivedBack: 'Back',
} as const;

View file

@ -12,7 +12,7 @@ export default {
newChat: 'New Chat',
newWorkspace: 'New Workspace',
emptyState: 'No sessions yet · click New Session to start',
archiveConfirm: 'Archive session?',
archiveConfirm: 'Archive this session? You can restore it later from Settings.',
options: 'Options',
rename: 'Rename',
copyPath: 'Copy path',

View file

@ -16,4 +16,7 @@ export default {
permYoloSub: '全部自动批准',
planModeSub: '计划模式',
swarmModeSub: 'Swarm 模式',
archivedSessions: '已归档会话',
archivedSessionsSub: '查看并恢复已归档会话',
archivedBack: '返回',
} as const;

View file

@ -12,7 +12,7 @@ export default {
newChat: '新建对话',
newWorkspace: '新建工作区',
emptyState: '还没有会话 · 点击 新建会话 开始',
archiveConfirm: '归档会话?',
archiveConfirm: '确认归档会话?归档后可以从「设置」中恢复',
options: '选项',
rename: '重命名',
copyPath: '复制路径',