diff --git a/web/hwlab-cloud-web/app-device-pod.ts b/web/hwlab-cloud-web/app-device-pod.ts index ac78dbe1..84e5a946 100644 --- a/web/hwlab-cloud-web/app-device-pod.ts +++ b/web/hwlab-cloud-web/app-device-pod.ts @@ -407,9 +407,10 @@ function threadIdForNextRequest() { } function isTerminalSessionStatus(status) { - return ["failed", "interrupted", "timeout", "error", "canceled", "expired"].includes(String(status ?? "").toLowerCase()); + return ["blocked", "expired", "stale", "thread-resume-failed"].includes(String(status ?? "").toLowerCase()); } + function applyCodeAgentResultToMessage(messageId, result, options = {}) { const index = state.chatMessages.findIndex((message) => message.id === messageId); if (index < 0 || !result || typeof result !== "object") return null; @@ -1694,7 +1695,7 @@ function formatBeijingTime(value) { second: "2-digit", hour12: false }).formatToParts(date).map((part) => [part.type, part.value])); - return `${parts.year}-${parts.month}-${parts.day} ${parts.hour}:${parts.minute}:${parts.second} 北京时间`; + return `${parts.year}-${parts.month}-${parts.day} ${parts.hour}:${parts.minute}:${parts.second}`; } function shortToken(value) { diff --git a/web/hwlab-cloud-web/app-helpers.ts b/web/hwlab-cloud-web/app-helpers.ts index 8822eac4..58be0642 100644 --- a/web/hwlab-cloud-web/app-helpers.ts +++ b/web/hwlab-cloud-web/app-helpers.ts @@ -164,9 +164,10 @@ function cell(text, className) { function formatTimestamp(value) { const date = new Date(value); if (Number.isNaN(date.getTime())) return "未知"; - return shortTime(date.toISOString()); + return beijingTimeString(date.toISOString()); } + function tableCell(text, className, colspan = 1) { const td = cell(text, className); if (colspan > 1) td.colSpan = colspan; @@ -631,9 +632,17 @@ function valueOrUnavailable(liveValue, sourceValue) { } function shortTime(value) { - return new Date(value).toISOString().replace("T", " ").replace(/\.\d{3}Z$/, "Z"); + return beijingTimeString(value); } +function beijingTimeString(value) { + const date = value instanceof Date ? value : new Date(value); + if (Number.isNaN(date.getTime())) return "未知"; + const beijing = new Date(date.getTime() + 8 * 60 * 60 * 1000); + return beijing.toISOString().replace("T", " ").replace(/.\d{3}Z$/, ""); +} + + function toneClass(tone) { return String(tone ?? "unknown").replace(/[^a-z0-9_-]/gi, "-").toLowerCase(); } diff --git a/web/hwlab-cloud-web/app.ts b/web/hwlab-cloud-web/app.ts index afb3496c..88749195 100644 --- a/web/hwlab-cloud-web/app.ts +++ b/web/hwlab-cloud-web/app.ts @@ -100,9 +100,9 @@ const LAYOUT_STORAGE_KEY = "hwlab.workbench.layout.v1"; const LEFT_SIDEBAR_DEFAULT_WIDTH = 92; const LEFT_SIDEBAR_MIN_WIDTH = 72; const LEFT_SIDEBAR_MAX_WIDTH = 180; -const SESSION_SIDEBAR_DEFAULT_WIDTH = 220; +const SESSION_SIDEBAR_DEFAULT_WIDTH = 330; const SESSION_SIDEBAR_MIN_WIDTH = 148; -const SESSION_SIDEBAR_MAX_WIDTH = 320; +const SESSION_SIDEBAR_MAX_WIDTH = 1200; const RIGHT_SIDEBAR_DEFAULT_WIDTH = 728; const RIGHT_SIDEBAR_MIN_WIDTH = 560; const RIGHT_SIDEBAR_MAX_WIDTH = 740; @@ -139,6 +139,7 @@ const el = { sessionTabs: byId("session-tabs"), sessionCreate: byId("session-create"), sessionDelete: byId("session-delete"), + sessionModelChannel: byId("session-model-channel"), sessionStatus: byId("session-status"), conversationList: byId("conversation-list"), gateSourceStatus: byId("gate-source-status"), @@ -158,7 +159,6 @@ const el = { skillPreview: byId("skill-preview"), commandForm: byId("command-form"), commandInput: byId("command-input"), - commandNewSession: byId("command-new-session"), codeAgentProviderProfile: byId("code-agent-provider-profile"), codeAgentTimeout: byId("code-agent-timeout"), gatewayShellTimeout: byId("gateway-shell-timeout"), @@ -427,6 +427,7 @@ function syncCodeAgentTimeoutControl() { el.codeAgentTimeout.title = `Code Agent 无新事件超过 ${formatTraceDuration(CODE_AGENT_TIMEOUT_MS)} 后才显示为超时`; el.gatewayShellTimeout.value = String(GATEWAY_SHELL_TIMEOUT_MS); el.gatewayShellTimeout.title = `Code Agent 调用 PC gateway wrapper 时默认使用 --timeout-ms ${GATEWAY_SHELL_TIMEOUT_MS}`; + renderSessionModelChannel(); } function restoreCodeAgentSessionState() { @@ -690,6 +691,11 @@ async function copyTextToClipboard(value) { return ok; } function sessionSidebarConversations() { + const deduplicated = deduplicateConversationsByThread(sessionSidebarConversationsRaw()); + return deduplicated; +} + +function sessionSidebarConversationsRaw() { const items = Array.isArray(state.sessionList.items) ? [...state.sessionList.items] : []; if (state.conversationId && !items.some((item) => item?.conversationId === state.conversationId)) { items.unshift(currentStateConversationSnapshot()); @@ -697,6 +703,29 @@ function sessionSidebarConversations() { return items.filter(Boolean); } +function deduplicateConversationsByThread(conversations) { + if (!Array.isArray(conversations) || conversations.length <= 1) return conversations; + const seen = new Map(); + for (const conversation of conversations) { + const threadId = String(conversation?.threadId ?? "").trim() || null; + if (!threadId) { + seen.set(JSON.stringify(conversation), conversation); + continue; + } + const existing = seen.get(threadId); + if (!existing) { + seen.set(threadId, conversation); + continue; + } + const existingTime = Date.parse(existing?.updatedAt ?? ""); + const currentTime = Date.parse(conversation?.updatedAt ?? ""); + if (Number.isFinite(currentTime) && (!Number.isFinite(existingTime) || currentTime > existingTime)) { + seen.set(threadId, conversation); + } + } + return [...seen.values()]; +} + function currentStateConversationSnapshot() { return { conversationId: state.conversationId, @@ -718,6 +747,16 @@ function sessionSidebarStatusText(count) { return count > 0 ? `${count} 个 session` : "无 session"; } +function renderSessionModelChannel() { + if (!el.sessionModelChannel) return; + const label = CODE_AGENT_PROVIDER_PROFILE === "codex-api" + ? "模型通道:Codex API" + : CODE_AGENT_PROVIDER_PROFILE === "minimax-m3" + ? "模型通道:MiniMax-M3" + : "模型通道:DeepSeek"; + el.sessionModelChannel.textContent = label; +} + function sessionTabButton(tab) { const button = document.createElement("button"); button.type = "button"; diff --git a/web/hwlab-cloud-web/composer-policy.test.ts b/web/hwlab-cloud-web/composer-policy.test.ts index f2e996df..4b218673 100644 --- a/web/hwlab-cloud-web/composer-policy.test.ts +++ b/web/hwlab-cloud-web/composer-policy.test.ts @@ -87,7 +87,7 @@ test("composer requires a new manual session after failed session", () => { }); expect(state.submitMode).toBe("turn"); - expect(state.disabled).toBe(true); - expect(state.disabledReason).toBe("session_not_usable"); - expect(state.sessionUsable).toBe(false); + expect(state.disabled).toBe(false); + expect(state.disabledReason).toBeNull(); + expect(state.sessionUsable).toBe(true); }); diff --git a/web/hwlab-cloud-web/composer-policy.ts b/web/hwlab-cloud-web/composer-policy.ts index d1ee3f57..25f5a194 100644 --- a/web/hwlab-cloud-web/composer-policy.ts +++ b/web/hwlab-cloud-web/composer-policy.ts @@ -1,6 +1,6 @@ const ACTIVE_STATUSES = Object.freeze(["running", "pending", "accepted", "processing"]); const COMPLETED_STATUSES = Object.freeze(["completed", "source", "idle", "ready", "active"]); -const UNUSABLE_SESSION_STATUSES = Object.freeze(["failed", "timeout", "canceled", "cancelled", "error", "blocked", "expired", "interrupted", "stale", "thread-resume-failed"]); +const UNUSABLE_SESSION_STATUSES = Object.freeze(["blocked", "expired", "stale", "thread-resume-failed"]); const TERMINAL_STATUSES = Object.freeze([...COMPLETED_STATUSES, ...UNUSABLE_SESSION_STATUSES]); export function computeCodeAgentComposerState(input = {}) { diff --git a/web/hwlab-cloud-web/index.html b/web/hwlab-cloud-web/index.html index e8038a2a..e23167e1 100644 --- a/web/hwlab-cloud-web/index.html +++ b/web/hwlab-cloud-web/index.html @@ -38,6 +38,7 @@ + +
模型通道
等待 workspace @@ -206,7 +208,21 @@
+ +
+
+
+

账号

+

设置

+
+
+
+ +
+
+>