- 问题1: 修复重复 session - 按 threadId 去重 conversation - 问题2: session sidebar 默认宽度改为150% (330px),移除上限 - 问题3: 删除重复的命令栏「新建 Session」按钮 - 问题4: session 报错(status=failed/error/canceled/timeout)后允许继续重试 - 问题5: session tabs 上方显示当前模型通道 - 问题6: 所有时间统一使用北京时间(UTC+8),移除「北京时间」字样 - 问题7: 注销按钮移入 activity-rail 设置标签 Closes #744 Co-authored-by: Codex <codex@local>
This commit is contained in:
@@ -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) {
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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 = {}) {
|
||||
|
||||
@@ -38,6 +38,7 @@
|
||||
<button class="rail-button" type="button" data-route="gate" title="内部复核" aria-label="内部复核">内部复核</button>
|
||||
<button class="rail-button" type="button" data-route="help" title="使用说明" aria-label="使用说明">使用说明</button>
|
||||
<span class="rail-spacer" aria-hidden="true"></span>
|
||||
<button class="rail-button" type="button" data-route="settings" title="设置" aria-label="SZ">设置</button>
|
||||
<button
|
||||
class="rail-button sidebar-toggle"
|
||||
id="left-sidebar-toggle"
|
||||
@@ -56,6 +57,7 @@
|
||||
</div>
|
||||
<button class="session-icon-button" id="session-create" type="button" aria-label="创建 session" title="创建 session">+</button>
|
||||
</header>
|
||||
<div class="session-model-channel" id="session-model-channel" aria-live="polite">模型通道</div>
|
||||
<div class="session-tabs" id="session-tabs" role="tablist" aria-label="Code Agent sessions"></div>
|
||||
<div class="session-sidebar-foot">
|
||||
<span class="session-status" id="session-status">等待 workspace</span>
|
||||
@@ -206,7 +208,21 @@
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section
|
||||
<section class="view settings-view" id="settings" data-view="settings" aria-labelledby="settings-title" hidden>
|
||||
<div class="workspace-panel settings-panel">
|
||||
<div class="panel-title-row">
|
||||
<div>
|
||||
<p class="eyebrow">账号</p>
|
||||
<h2 id="settings-title">设置</h2>
|
||||
</div>
|
||||
</div>
|
||||
<div class="settings-content">
|
||||
<button class="logout-button" id="logout-button" type="button">注销登录</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
>
|
||||
|
||||
<form class="command-bar" id="command-form" aria-label="Agent 输入">
|
||||
<label class="agent-timeout-control" for="code-agent-provider-profile">
|
||||
@@ -248,7 +264,6 @@
|
||||
placeholder="输入要发送给 Code Agent 的消息;不会直接触发硬件变更。"
|
||||
></textarea>
|
||||
</div>
|
||||
<button class="command-button secondary" id="command-new-session" type="button">新建 Session</button>
|
||||
<button class="command-button" id="command-send" type="submit">发送</button>
|
||||
<button class="command-button secondary" id="command-clear" type="button">清空</button>
|
||||
</form>
|
||||
@@ -265,7 +280,6 @@
|
||||
aria-label="折叠右侧 Device Pod 看板"
|
||||
title="折叠右侧 Device Pod 看板"
|
||||
>收起看板</button>
|
||||
<button class="logout-button" id="logout-button" type="button">退出</button>
|
||||
</div>
|
||||
<header class="sidebar-workbench-header">
|
||||
<p class="eyebrow">用户工作台</p>
|
||||
|
||||
@@ -68,10 +68,8 @@ assert.ok(rightSidebar, "Device Pod right sidebar must exist");
|
||||
assert.match(html, /data-route="skills"/u, "Cloud Web must expose the skills activity route");
|
||||
assert.match(html, /id="session-tabs"[^>]*role="tablist"/u, "Cloud Web workspace must expose session tabs");
|
||||
assert.match(html, /class="session-sidebar"[^>]*id="session-sidebar"/u, "Cloud Web must place sessions in a second-level sidebar");
|
||||
assert.match(html, /id="command-new-session"/u, "Cloud Web command bar must expose explicit Code Agent session creation");
|
||||
assertIncludes(app, "/v1/agent/sessions", "Cloud Web must create/select Code Agent sessions explicitly");
|
||||
assertIncludes(app, "session_required", "Cloud Web must block normal turns until a session is explicitly created");
|
||||
assertIncludes(app, "session_not_usable", "Cloud Web must block failed/stale Code Agent sessions instead of auto rolling");
|
||||
assert.match(html, /id="skills"[^>]*data-view="skills"/u, "Cloud Web must expose a skills view");
|
||||
assert.match(html, /id="skill-upload-input"[^>]*webkitdirectory/u, "Skills view must support directory upload");
|
||||
assertIncludes(app, "/v1/skills", "skills frontend must call /v1/skills");
|
||||
|
||||
@@ -307,6 +307,16 @@ body > [data-app-shell][hidden] {
|
||||
outline: 0;
|
||||
}
|
||||
|
||||
.session-model-channel {
|
||||
padding: 4px 8px;
|
||||
font-family: var(--mono);
|
||||
font-size: 11px;
|
||||
color: var(--accent);
|
||||
background: rgba(19, 22, 20, 0.98);
|
||||
border-bottom: 1px solid var(--line);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.session-tabs {
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
@@ -1369,6 +1379,8 @@ h3 {
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.session-tabs {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
|
||||
Reference in New Issue
Block a user