fix: simplify workbench session list (#1285)

This commit is contained in:
Lyon
2026-06-15 22:10:23 +08:00
committed by GitHub
parent b41bae1c81
commit 05aa30bf3a
4 changed files with 68 additions and 37 deletions
@@ -116,7 +116,7 @@ function traceRecord(trace: RunnerTrace | null | undefined): Record<string, unkn
</template>
</li>
</ol>
<p v-if="readableRows.length === 0" class="trace-empty">等待后端事件</p>
<p v-if="readableRows.length === 0" class="trace-empty">思考中...</p>
</div>
</details>
</section>
@@ -2,7 +2,6 @@
import { computed, ref } from "vue";
import { useWorkbenchStore } from "@/stores/workbench";
import { useClipboard } from "@/composables/useClipboard";
import { formatRelativeTime } from "@/utils";
const workbench = useWorkbenchStore();
const { copied, copy } = useClipboard();
@@ -51,6 +50,18 @@ function readWidth(): number {
return 300;
}
}
function formatSessionUpdatedTime(value: string | null | undefined): string {
if (!value) return "未记录";
const date = new Date(value);
if (!Number.isFinite(date.getTime())) return value;
const delta = Date.now() - date.getTime();
if (delta < 60_000) return "刚刚";
if (delta < 3_600_000) return `${Math.max(1, Math.round(delta / 60_000))}`;
if (delta < 86_400_000) return `${Math.max(1, Math.round(delta / 3_600_000))}`;
if (delta < 604_800_000) return `${Math.max(1, Math.round(delta / 86_400_000))}`;
return date.toLocaleString(undefined, { month: "numeric", day: "numeric", hour: "2-digit", minute: "2-digit" });
}
</script>
<template>
@@ -64,11 +75,9 @@ function readWidth(): number {
</div>
<div class="session-model-channel" id="session-model-channel">模型通道{{ workbench.providerProfile }}</div>
<div class="session-list" id="session-tabs">
<button v-for="tab in workbench.sessionTabs" :key="tab.conversationId" class="session-tab" :data-active="tab.active" type="button" @click="workbench.selectConversation(tab)">
<strong>{{ tab.label || tab.conversationId }}</strong>
<span>{{ tab.sessionId || "未绑定 session" }}</span>
<span>{{ tab.subtitle }}</span>
<small>{{ formatRelativeTime(tab.updatedAt || tab.startedAt) }}</small>
<button v-for="tab in workbench.sessionTabs" :key="tab.conversationId" class="session-tab" :data-active="tab.active" type="button" :title="tab.label || tab.conversationId" @click="workbench.selectConversation(tab)">
<span class="session-tab-title">{{ tab.label || tab.conversationId }}</span>
<time class="session-tab-time" :datetime="tab.updatedAt || tab.startedAt || undefined">{{ formatSessionUpdatedTime(tab.updatedAt || tab.startedAt) }}</time>
</button>
<p v-if="workbench.sessionTabs.length === 0" class="muted-box">没有 session发送前必须显式新建或选择 session</p>
</div>
@@ -93,20 +93,20 @@ export function conversationToSessionTab(conversation: ConversationRecord, activ
const status = firstNonEmptyString(conversation.snapshot?.sessionStatus, conversation.status, conversation.messages?.at(-1)?.status) ?? "source";
const trace = firstNonEmptyString(conversation.lastTraceId, conversation.messages?.at(-1)?.traceId);
const userMessage = conversation.messages?.find((message) => message.role === "user");
const preview = firstReadableText(
const preview = firstReadableSentence(
conversation.firstUserMessagePreview,
conversation.snapshot?.firstUserMessagePreview,
conversation.userPreview,
conversation.title,
conversation.name,
userMessage?.text,
userMessage?.content,
userMessage?.title
userMessage?.title,
conversation.title,
conversation.name
);
return {
...conversation,
key: sessionId ?? conversationId,
label: truncateLabel(preview) ?? `Session ${shortToken(sessionId ?? conversationId, 8)}`,
label: preview ?? `Session ${shortToken(sessionId ?? conversationId, 8)}`,
subtitle: `${status === "running" ? "处理中" : trace ? `trace ${shortToken(trace, 8)}` : "未开始"} · ${conversation.messageCount ?? conversation.messages?.length ?? 0}`,
sessionId,
threadId: firstNonEmptyString(conversation.threadId, conversation.session?.threadId),
@@ -215,31 +215,21 @@ function compareProviderProfileOptions(left: ProviderProfileOption, right: Provi
return left.value.localeCompare(right.value);
}
function truncateLabel(value: string | null): string | null {
if (!value) return null;
const text = value.replace(/\s+/gu, " ").trim();
if (!text || text === OBJECT_OBJECT_TEXT) return null;
return text.length <= 36 ? text : `${text.slice(0, 36).trimEnd()}...`;
}
function firstReadableText(...values: unknown[]): string | null {
function firstReadableSentence(...values: unknown[]): string | null {
for (const value of values) {
const text = readableText(value);
const text = readableSentence(value);
if (text) return text;
}
return null;
}
function readableText(value: unknown): string | null {
if (typeof value === "string") {
const text = value.replace(/\s+/gu, " ").trim();
return text && text !== OBJECT_OBJECT_TEXT ? text : null;
}
function readableSentence(value: unknown): string | null {
if (typeof value === "string") return firstSentenceFromText(value);
if (typeof value === "number" || typeof value === "boolean") return String(value);
if (Array.isArray(value)) return firstReadableText(...value);
if (Array.isArray(value)) return firstReadableSentence(...value);
if (!value || typeof value !== "object") return null;
const record = value as Record<string, unknown>;
return firstReadableText(
return firstReadableSentence(
record.text,
record.content,
record.message,
@@ -252,6 +242,24 @@ function readableText(value: unknown): string | null {
);
}
function firstSentenceFromText(value: string): string | null {
const normalized = value.replace(/\u0000/gu, " ").replace(/\r\n|\r/gu, "\n").replace(/[\t\f\v ]+/gu, " ").trim();
if (!normalized || normalized === OBJECT_OBJECT_TEXT) return null;
const candidates = [
sentenceBoundary(normalized, /[!?]/u, 1),
sentenceBoundary(normalized, /\.(?=\s|$)/u, 1),
sentenceBoundary(normalized, /\n+/u, 0)
].filter((index): index is number => typeof index === "number");
const end = candidates.length > 0 ? Math.min(...candidates) : normalized.length;
const text = normalized.slice(0, end).replace(/\s+/gu, " ").trim();
return text && text !== OBJECT_OBJECT_TEXT ? text : null;
}
function sentenceBoundary(value: string, pattern: RegExp, offset: number): number | null {
const match = pattern.exec(value);
return match?.index === undefined ? null : match.index + offset;
}
function shortToken(value: string | null | undefined, size: number): string {
const text = String(value ?? "");
if (text.length <= size) return text;
+23 -9
View File
@@ -606,13 +606,16 @@
}
.session-tab {
display: grid;
display: flex;
align-items: center;
min-width: 0;
min-height: 34px;
width: 100%;
gap: 4px;
gap: 10px;
border: 1px solid #e2e8f0;
border-radius: 8px;
border-radius: 6px;
background: #f8fafc;
padding: 10px;
padding: 7px 9px;
color: #334155;
text-align: left;
}
@@ -622,16 +625,27 @@
background: #f0f9ff;
}
.session-tab strong,
.session-tab span,
.session-tab small {
.session-tab-title {
min-width: 0;
flex: 1 1 auto;
color: #0f172a;
font-size: 13px;
font-weight: 600;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.session-tab span,
.session-tab small,
.session-tab-time {
flex: 0 0 auto;
color: #64748b;
font-size: 12px;
font-variant-numeric: tabular-nums;
line-height: 1;
text-align: right;
white-space: nowrap;
}
.muted-box {
color: #64748b;
font-size: 12px;