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 @@
+
-