Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
1b24718
fix(ui): give the task rail's status dot one meaning
Astro-Han Aug 15, 2026
850c34c
refactor(ui): rebuild the task row on two signal slots
Astro-Han Aug 15, 2026
d8b93fe
refactor(ui): give the rail one top region and one grouping switch
Astro-Han Aug 15, 2026
53bd296
feat(desktop): move task import out of the rail into settings
Astro-Han Aug 15, 2026
ed2b6d0
refactor: name the thing a user works on 任务 / task everywhere
Astro-Han Aug 15, 2026
4bfc99d
test(ui): make the narrow-rail story actually narrow
Astro-Han Aug 15, 2026
10ca8e5
fix(ui): keep the rail's 任务 row as the way back to the list
Astro-Han Aug 15, 2026
09b1f90
fix(ui): route task status through the shared semantic layer
Astro-Han Aug 15, 2026
b90b60a
fix(ui): keep the task row's facts when it collapses to two slots
Astro-Han Aug 15, 2026
550f954
fix(desktop): refresh the catalog when an import's outcome is unknown
Astro-Han Aug 15, 2026
e275726
fix(desktop): re-point the structural gates the surface move broke
Astro-Han Aug 15, 2026
a17a01a
refactor: finish the 任务 rename where the rule missed
Astro-Han Aug 15, 2026
971bef3
fix(ui): route the task row's last two dots through the shared vocabu…
Astro-Han Aug 15, 2026
60d2631
docs: say what each session-status-presentation file actually owns
Astro-Han Aug 15, 2026
74b2e80
test: make the restored-task and import fixtures answer like their so…
Astro-Han Aug 15, 2026
f531b98
fix(desktop): tidy the two places the 任务 rename passed through twice
Astro-Han Aug 15, 2026
aea0707
chore: regenerate the Astryx surface inventory
Astro-Han Aug 15, 2026
4b17102
test(ui): show every task status in the story that exists to show them
Astro-Han Aug 15, 2026
642d524
refactor(ui): drop the rail's 任务 row
Astro-Han Aug 15, 2026
f2dfdda
fix(runtime-host): coalesce a repeat import while the first is still …
Astro-Han Aug 15, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion apps/desktop/e2e/composer-skill-invocation.spec.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -30,7 +30,7 @@ test('staged Skills come back as chips after leaving and returning', async ({
await expect(workspaceChip).toContainText('Workspace Only');

await page.getByRole('button', { name: '展开侧边栏' }).click();
const sidebar = page.getByRole('navigation', { name: '对话列表' });
const sidebar = page.getByRole('navigation', { name: '任务列表' });
await sidebar.getByRole('button', { name: '新任务', exact: true }).click();
await expect(composer).toHaveText('');

Expand Down
8 changes: 4 additions & 4 deletions apps/desktop/e2e/session-workbar.spec.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -8,7 +8,7 @@ async function openGitChanges(page: Page) {
await composer.fill('create review session');
await composer.press('Enter');
await expect(page.getByText(/Fake backend received: create review session/)).toBeVisible();
await page.getByRole('button', { name: '展开会话工作栏' }).click();
await page.getByRole('button', { name: '展开任务工作栏' }).click();
await expect(page.getByRole('list', { name: '打开工具' })).toBeVisible();
await page.getByRole('button', { name: /变更.*查看当前 Git 工作区变化/ }).click();
return page.getByRole('region', { name: 'Git 变更' });
Expand All@@ -20,14 +20,14 @@ test('titlebar workbar action restores an existing tool instead of the picker',
const page = gitReviewWindow.page;
const workspaceActions = page.getByRole('toolbar', { name: '工作区辅助操作' });
const panel = await openGitChanges(page);
await expect(workspaceActions.getByRole('button', { name: '收起会话工作栏' })).toBeVisible();
await expect(workspaceActions.getByRole('button', { name: '收起任务工作栏' })).toBeVisible();
await expect(workspaceActions.getByRole('button', { name: '打开工作栏工具' })).toHaveCount(0);
await page.getByRole('button', { name: '打开工作栏标签' }).click();
const picker = page.getByRole('list', { name: '打开工具' });
await expect(picker).toBeVisible();

await workspaceActions.getByRole('button', { name: '收起会话工作栏' }).click();
await workspaceActions.getByRole('button', { name: '展开会话工作栏' }).click();
await workspaceActions.getByRole('button', { name: '收起任务工作栏' }).click();
await workspaceActions.getByRole('button', { name: '展开任务工作栏' }).click();

await expect(panel).toBeVisible();
await expect(picker).not.toBeVisible();
Expand Down
4 changes: 2 additions & 2 deletions apps/desktop/e2e/settings.spec.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -8,8 +8,8 @@ test('opening settings commits an active titlebar rename', async ({ window: page
const identity = page.locator('[data-maka-contract="titlebar-identity"]');
await expect(identity).toBeVisible();
await page.getByRole('button', { name: '展开侧边栏' }).click();
await identity.getByRole('button', { name: /重命名对话/ }).click();
await page.getByRole('textbox', { name: '重命名对话' }).fill('renamed before settings');
await identity.getByRole('button', { name: /重命名任务/ }).click();
await page.getByRole('textbox', { name: '重命名任务' }).fill('renamed before settings');

// Programmatic activation preserves input focus, matching the macOS
// application-menu command that opens Settings before Chromium can blur it.
Expand Down
2 changes: 1 addition & 1 deletion apps/desktop/e2e/sidebar-project-row.spec.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -10,7 +10,7 @@ test('project navigation and actions remain adjacent keyboard controls', async (
await page.keyboard.press('Escape');
await expect(page.locator('[data-maka-contract="search-modal"]')).not.toBeVisible();

const sidebar = page.getByRole('navigation', { name: '对话列表' });
const sidebar = page.getByRole('navigation', { name: '任务列表' });
await sidebar.getByRole('radio', { name: '按项目', exact: true }).click();

const projectRow = sidebar.locator(
Expand Down
15 changes: 12 additions & 3 deletions apps/desktop/e2e/streaming-remount.spec.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -18,11 +18,20 @@ test('remounting a live surface leaves accumulated output settled', async ({
const liveBubble = page.locator('.maka-bubble-streaming');
await expect(liveBubble).toContainText(accumulatedOutput);

const sidebar = page.getByRole('navigation', { name: '对话列表' });
const sidebar = page.getByRole('navigation', { name: '任务列表' });
await sidebar.getByRole('button', { name: '扩展' }).click();
await expect(page.locator('[data-module="skills"]')).toBeVisible();
await expect(liveBubble).toHaveCount(0);
await sidebar.getByRole('button', { name: '会话', exact: true }).click();
// Back the way the product actually offers: the rail is collapsed here, so
// the task rows are not rendered and there is no 任务 row to press (#2984).
// Widening it is the titlebar's job, and the task left behind is still
// `activeId`, so it comes back marked and one click away.
await page.getByRole('button', { name: '展开侧边栏' }).click();
const currentTaskRow = sidebar.locator(
'[data-maka-contract="session-row"] [aria-current="page"]',
);
await expect(currentTaskRow).toHaveCount(1);
await currentTaskRow.click();
await expect(liveBubble).toHaveCount(1);
await expect(liveBubble).toContainText(accumulatedOutput);

Expand DownExpand Up@@ -75,7 +84,7 @@ test('returning to a live conversation settles output accumulated while away', a
const liveBubble = page.locator('.maka-bubble-streaming');
await expect(liveBubble).toContainText(accumulatedOutput);

const sidebar = page.getByRole('navigation', { name: '对话列表' });
const sidebar = page.getByRole('navigation', { name: '任务列表' });
await page.getByRole('button', { name: '展开侧边栏' }).click();
await expect(page.locator('[data-agents-page]')).toHaveAttribute(
'data-sidebar-state',
Expand Down
16 changes: 13 additions & 3 deletions apps/desktop/src/main/__tests__/app-shell-session-purge.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -11,7 +11,7 @@ function summary(id: string, overrides: Partial<SessionSummary> = {}): SessionSu
isArchived: true,
labels: [],
hasUnread: false,
status: 'done',
status: 'archived',
Comment thread
coderabbitai[bot] marked this conversation as resolved.
backend: 'fake',
llmConnectionSlug: 'test',
connectionLocked: true,
Expand All@@ -21,6 +21,16 @@ function summary(id: string, overrides: Partial<SessionSummary> = {}): SessionSu
};
}

/**
* A task that left the archive. Both fields move together because that is what
* `SessionStore.unarchive` writes; flipping only `isArchived` would build a row
* the store cannot produce, and the sweep would then be tested against a state
* it will never meet.
*/
function restored(id: string): SessionSummary {
return summary(id, { isArchived: false, status: 'active' });
}

type SweepHarness = {
removed: string[];
cleared: string[];
Expand DownExpand Up@@ -128,7 +138,7 @@ describe('purgeSessions', () => {
// dialog was up has left it, and a sweep that deleted it anyway would be
// acting outside what was agreed to.
const h = harness();
const sessions = [summary('kept', { isArchived: false }), summary('doomed')];
const sessions = [restored('kept'), summary('doomed')];
const restore = installWindow(h);
const actions = createActions({ harness: h, sessions, activeIdRef: { current: undefined } });

Expand All@@ -148,7 +158,7 @@ describe('purgeSessions', () => {
const sessions = [summary('first'), summary('second')];
const restore = installWindow(h, {
onRemove: (id) => {
if (id === 'first') sessions[1] = summary('second', { isArchived: false });
if (id === 'first') sessions[1] = restored('second');
},
});
const actions = createActions({ harness: h, sessions, activeIdRef: { current: undefined } });
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -167,7 +167,7 @@ describe('AppShell session settings actions', () => {

assert.deepEqual(harness.successes, [
{
title: '已切换当前会话模型',
title: '已切换当前任务模型',
description: 'claude-haiku → claude-opus',
},
]);
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -42,7 +42,7 @@ describe('bot incoming new-session cwd', () => {
} as unknown as BotIncomingMessage);

assert.deepEqual(createInput, {
name: 'Telegram 对话',
name: 'Telegram 任务',
labels: ['bot', 'telegram'],
});
});
Expand Down

This file was deleted.

Original file line numberDiff line numberDiff line change
Expand Up@@ -59,7 +59,7 @@ test('forwards bounded external Session requests and publishes imported Sessions
assert.deepEqual(events, [{ reason: 'created', sessionId: 'imported-1' }]);
});

test('preserves commit uncertainty as a structured non-retryable IPC result', async () => {
test('an uncertain commit still asks the shell to re-read the catalog', async () => {
const events: unknown[] = [];
const ipc = ipcHarness();
registerRuntimeHostExternalSessionsIpc(
Expand All@@ -85,7 +85,12 @@ test('preserves commit uncertainty as a structured non-retryable IPC result', as
}),
{ ok: false, reason: 'commit_outcome_unknown' },
);
assert.deepEqual(events, []);
// The task may be in the catalog, so the shell has to look. The import page's
// own banner cannot be the only trace: 导入任务 is a Settings page, and the
// moment the user leaves it the banner is unmounted -- which is exactly when
// they come back and import the same conversation again. No id, because not
// knowing which task landed is what `commit_outcome_unknown` means.
assert.deepEqual(events, [{ reason: 'created', sessionId: undefined }]);
});

test('rejects malformed renderer requests before they reach the Host client', async () => {
Expand Down
2 changes: 1 addition & 1 deletion apps/desktop/src/main/__tests__/thread-search.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -215,7 +215,7 @@ describe('runThreadSearch', () => {
await runThreadSearch({ source: 'thread', query: 'roadmap', limit: 5 }, makeDeps(entries)),
)[0]!;
assert.deepEqual(titleHit.target, { kind: 'thread', sessionId: 's1' });
assert.equal(titleHit.summary, '会话标题');
assert.equal(titleHit.summary, '任务标题');
assert.equal(titleHit.url, undefined);
assert.match(titleHit.snippet ?? '', /\[redacted\]/);
assert.equal(titleHit.snippet?.includes('sk-ant-test-secret-token-12345'), false);
Expand Down
10 changes: 5 additions & 5 deletions apps/desktop/src/main/bot-incoming-main.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -183,15 +183,15 @@ export function createBotIncomingMainService(deps: BotIncomingMainServiceDeps):
noticeTtlMs: number,
): Promise<string | undefined> {
if (botConversationSessions.size >= BOT_CONVERSATION_SESSION_LIMIT) {
await sendTransientBotNotice(message, 'Maka 当前机器人会话数量已达上限,请重置或清理旧会话后再试。', noticeTtlMs);
await sendTransientBotNotice(message, 'Maka 当前机器人任务数量已达上限,请重置或清理旧任务后再试。', noticeTtlMs);
return undefined;
}
if (!consumeBotConversationToken(conversationKey)) {
await sendTransientBotNotice(message, 'Maka 收到的机器人消息过于频繁,请稍后再试。', noticeTtlMs);
return undefined;
}
const sessionId = await deps.sessions.createSession({
name: `${botDisplayLabel(message.platform)} 对话`,
name: `${botDisplayLabel(message.platform)} 任务`,
labels: ['bot', message.platform],
});
botConversationSessions.set(conversationKey, sessionId);
Expand DownExpand Up@@ -240,8 +240,8 @@ export function createBotIncomingMainService(deps: BotIncomingMainServiceDeps):
ephemeralTtlMs: SYSTEM_NOTICE_TTL_MS,
};
const ack = had
? '会话已重置,下一条消息会开新对话。'
: '当前没有进行中的对话;下一条消息会开新对话。';
? '任务已重置,下一条消息会开新任务。'
: '当前没有进行中的任务;下一条消息会开新任务。';
await deps.botRegistry.sendMessage(message.platform, message.chatId, ack, replyOptions).catch(() => null);
return;
}
Expand DownExpand Up@@ -396,7 +396,7 @@ export function createBotIncomingMainService(deps: BotIncomingMainServiceDeps):
if ((await deps.sessions.prepareSession(sessionId)) === 'ready') return true;
await sendTransientBotNotice(
message,
'Maka 已拒绝这条机器人消息:绑定会话当前不是只读探索模式,请先在桌面端切回 explore 后再试。',
'Maka 已拒绝这条机器人消息:绑定任务当前不是只读探索模式,请先在桌面端切回 explore 后再试。',
noticeTtlMs,
);
return false;
Expand Down
2 changes: 1 addition & 1 deletion apps/desktop/src/main/capability-snapshot.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -61,7 +61,7 @@ export function buildCapabilitySnapshotCollection(input: {
feature: {
state: 'partial',
source: 'runtime',
reason: 'Daily Review 已聚合本地会话 / 工具 / 模型活动;当前不包含屏幕与应用级录制',
reason: 'Daily Review 已聚合本地任务 / 工具 / 模型活动;当前不包含屏幕与应用级录制',
},
requiredPermissions: [
{ id: 'screen_recording', required: false, status: permissions.screen_recording.status },
Expand Down
17 changes: 12 additions & 5 deletions apps/desktop/src/main/chat-readiness.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -106,6 +106,16 @@ export async function requireReadyConnection(
* so the copy stays close to its existing semantics (PR110a refactor
* is behavior-preserving — only the judgment moved to core).
*/
/**
* Two paths reach `fake_backend`: the reason table below, and the header check
* in `assertSessionCanSend`, which never gets far enough to look a connection
* up. They are the same sentence to the user, so they are the same string here
* — the rename that moved 会话 to 任务 had to be applied twice, which is what a
* second copy costs.
*/
const FAKE_BACKEND_MESSAGE =
'当前任务来自旧的本地模拟连接,不能直接发送。请到 设置 · 模型 添加真实模型后新建任务。';

function messageForReason(
reason: ChatConfigurationReason,
connection: LlmConnection,
Expand All@@ -132,7 +142,7 @@ function messageForReason(
return `模型 "${model}" 不能用于聊天。请到 设置 · 模型 选择支持聊天的模型。`;
}
case 'fake_backend':
return '当前会话来自旧的本地模拟连接,不能直接发送。请到 设置 · 模型 添加真实模型后新建会话。';
return FAKE_BACKEND_MESSAGE;
case 'missing_default_connection':
case 'connection_missing':
// These reasons are handled before we reach isConnectionReady,
Expand All@@ -146,10 +156,7 @@ export async function assertSessionCanSend(
deps: ReadyConnectionDeps,
): Promise<void> {
if (header.backend === 'fake') {
throw chatConfigurationError(
'当前会话来自旧的本地模拟连接,不能直接发送。请到 设置 · 模型 添加真实模型后新建会话。',
'fake_backend',
);
throw chatConfigurationError(FAKE_BACKEND_MESSAGE, 'fake_backend');
}
await requireReadyConnection(header.llmConnectionSlug, deps, header.model);
}
Expand Down
2 changes: 1 addition & 1 deletion apps/desktop/src/main/computer-use/status-item.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -114,7 +114,7 @@ const COPY: UiCatalog<StatusItemCopy> = {
zh: {
stopUsing: (appName) => `停止操作 ${appName}`,
stopUnnamed: '停止 Computer Use',
empty: '没有正在进行的会话',
empty: '没有正在进行的任务',
},
en: {
stopUsing: (appName) => `Stop Using ${appName}`,
Expand Down
2 changes: 1 addition & 1 deletion apps/desktop/src/main/e2e-fixture/scenarios-sessions.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -17,7 +17,7 @@ export function longSidebarSessions(
return {
header: header({
id: sessionId,
name: `会话 ${suffix}`,
name: `任务 ${suffix}`,
connection: 'zai-live',
model: 'glm-5.1',
now,
Expand Down
2 changes: 1 addition & 1 deletion apps/desktop/src/main/e2e-fixture/scenarios-settings.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -183,7 +183,7 @@ export async function writeScheduledTasks(workspaceRoot: string, now: number): P
for (const [index, title] of [
'每日站会前汇总阻塞项',
'每月依赖许可证审计',
'季度收尾清点未归档会话',
'季度收尾清点未归档任务',
'发布前跑一轮回归',
].entries()) {
await create(
Expand Down
2 changes: 1 addition & 1 deletion apps/desktop/src/main/notifications-policy.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -58,7 +58,7 @@ export interface RunNotificationCopy {
*/
function runNotificationCopy(kind: RunNotificationKind): RunNotificationCopy {
if (kind === 'errored') {
return { title: '对话出错', body: '本轮回答未能完成,点击查看详情。' };
return { title: '任务出错', body: '本轮回答未能完成,点击查看详情。' };
}
return { title: '回答已生成', body: 'Maka 已完成本轮回答,点击查看。' };
}
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -68,6 +68,13 @@ export function registerRuntimeHostExternalSessionsIpc(
error.operation === 'external-session.import' &&
error.code === 'commit_outcome_unknown'
) {
// "Unknown" means the task may well be in the catalog, so tell the
// shell to read it again. Without this, the only trace of a maybe-
// committed import is the banner on the page, and the page is gone the
// moment the user leaves Settings -- which is exactly when they come
// back and import the same conversation a second time. No id: the
// whole point is that we do not know which task, if any, landed.
deps.emitSessionsChanged('created');
return {
ok: false,
reason: 'commit_outcome_unknown',
Expand Down
2 changes: 1 addition & 1 deletion apps/desktop/src/main/search/thread-search.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -202,7 +202,7 @@ export async function runThreadSearch(
results.push({
source: THREAD_SOURCE,
title: session.name,
summary: '会话标题',
summary: '任务标题',
snippet,
target: {
kind: 'thread',
Expand Down
4 changes: 2 additions & 2 deletions apps/desktop/src/preload/preload.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -421,7 +421,7 @@ function executeWebSearchQuery(input: {
return Promise.resolve({
ok: false,
reason: 'unsupported_provider',
message: '原生联网搜索由对话中的主模型请求执行,不支持从设置页单独调用。',
message: '原生联网搜索由任务中的主模型请求执行,不支持从设置页单独调用。',
});
}
const query = normalizeWebSearchQuery(input.query);
Expand All@@ -448,7 +448,7 @@ function executeWebSearchTest(input: {
return Promise.resolve({
ok: false,
reason: 'unsupported_provider',
message: '原生联网搜索由对话中的主模型请求执行,不需要单独测试搜索凭据。',
message: '原生联网搜索由任务中的主模型请求执行,不需要单独测试搜索凭据。',
});
}
const apiKey = webSearchCredentialOverride(input.apiKey);
Expand Down
4 changes: 2 additions & 2 deletions apps/desktop/src/renderer/agent-graph-panel.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -53,7 +53,7 @@ export function getAgentGraphPanelCopy(locale: UiLocale): GraphPanelCopy {
stopping: '停止中…',
stopFailed: '停止 Graph 失败,请重试。',
loadFailed: 'Graph 状态刷新失败。',
openSession: '打开子会话',
openSession: '打开子任务',
operators: 'Operators',
selectedResults: '已选择结果',
noOperators: '等待主 Agent 创建 operator…',
Expand DownExpand Up@@ -96,7 +96,7 @@ export function getAgentGraphPanelCopy(locale: UiLocale): GraphPanelCopy {
stopping: 'Stopping…',
stopFailed: 'Could not stop the graph. Try again.',
loadFailed: 'Could not refresh graph state.',
openSession: 'Open child session',
openSession: 'Open child task',
operators: 'Operators',
selectedResults: 'Selected results',
noOperators: 'Waiting for the main agent to create an operator…',
Expand Down
2 changes: 1 addition & 1 deletion apps/desktop/src/renderer/app-shell-chat-actions.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -386,7 +386,7 @@ export function createAppShellChatActions(deps: {
showSkillInvocationFeedback(uiLocale, toastApi, sendResult.skillInvocation);
}
if (newChatOwner && isNewChatSendSurfaceActive(newChatOwner)) {
setNavSelection({ section: 'sessions', filter: 'chats' });
setNavSelection({ section: 'sessions' });
setActiveId(session.id);
showOptimisticUserMessage(
session.id,
Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Add copy buttons to all
 blocks
(function() {
function addCopyButtons() {
document.querySelectorAll('pre code').forEach(function(codeBlock) {
if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;
codeBlock.parentElement.setAttribute('data-copy-added', 'true');
var btn = document.createElement('button');
btn.textContent = 'Copy';
btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';
btn.onmouseover = function() { this.style.opacity = '1'; };
btn.onmouseout = function() { this.style.opacity = '0.7'; };
btn.onclick = function() {
navigator.clipboard.writeText(codeBlock.textContent).then(function() {
btn.textContent = 'Copied!';
setTimeout(function() { btn.textContent = 'Copy'; }, 1500);
});
};
codeBlock.parentElement.style.position = 'relative';
codeBlock.parentElement.appendChild(btn);
});
}
addCopyButtons();
// Re-run on dynamic content
var observer = new MutationObserver(addCopyButtons);
observer.observe(document.body, { childList: true, subtree: true });
})();
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
1b24718
fix(ui): give the task rail's status dot one meaning
Astro-Han Aug 15, 2026
850c34c
refactor(ui): rebuild the task row on two signal slots
Astro-Han Aug 15, 2026
d8b93fe
refactor(ui): give the rail one top region and one grouping switch
Astro-Han Aug 15, 2026
53bd296
feat(desktop): move task import out of the rail into settings
Astro-Han Aug 15, 2026
ed2b6d0
refactor: name the thing a user works on 任务 / task everywhere
Astro-Han Aug 15, 2026
4bfc99d
test(ui): make the narrow-rail story actually narrow
Astro-Han Aug 15, 2026
10ca8e5
fix(ui): keep the rail's 任务 row as the way back to the list
Astro-Han Aug 15, 2026
09b1f90
fix(ui): route task status through the shared semantic layer
Astro-Han Aug 15, 2026
b90b60a
fix(ui): keep the task row's facts when it collapses to two slots
Astro-Han Aug 15, 2026
550f954
fix(desktop): refresh the catalog when an import's outcome is unknown
Astro-Han Aug 15, 2026
e275726
fix(desktop): re-point the structural gates the surface move broke
Astro-Han Aug 15, 2026
a17a01a
refactor: finish the 任务 rename where the rule missed
Astro-Han Aug 15, 2026
971bef3
fix(ui): route the task row's last two dots through the shared vocabu…
Astro-Han Aug 15, 2026
60d2631
docs: say what each session-status-presentation file actually owns
Astro-Han Aug 15, 2026
74b2e80
test: make the restored-task and import fixtures answer like their so…
Astro-Han Aug 15, 2026
f531b98
fix(desktop): tidy the two places the 任务 rename passed through twice
Astro-Han Aug 15, 2026
aea0707
chore: regenerate the Astryx surface inventory
Astro-Han Aug 15, 2026
4b17102
test(ui): show every task status in the story that exists to show them
Astro-Han Aug 15, 2026
642d524
refactor(ui): drop the rail's 任务 row
Astro-Han Aug 15, 2026
f2dfdda
fix(runtime-host): coalesce a repeat import while the first is still …
Astro-Han Aug 15, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion apps/desktop/e2e/composer-skill-invocation.spec.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -30,7 +30,7 @@ test('staged Skills come back as chips after leaving and returning', async ({
await expect(workspaceChip).toContainText('Workspace Only');

await page.getByRole('button', { name: '展开侧边栏' }).click();
const sidebar = page.getByRole('navigation', { name: '对话列表' });
const sidebar = page.getByRole('navigation', { name: '任务列表' });
await sidebar.getByRole('button', { name: '新任务', exact: true }).click();
await expect(composer).toHaveText('');

Expand Down
8 changes: 4 additions & 4 deletions apps/desktop/e2e/session-workbar.spec.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -8,7 +8,7 @@ async function openGitChanges(page: Page) {
await composer.fill('create review session');
await composer.press('Enter');
await expect(page.getByText(/Fake backend received: create review session/)).toBeVisible();
await page.getByRole('button', { name: '展开会话工作栏' }).click();
await page.getByRole('button', { name: '展开任务工作栏' }).click();
await expect(page.getByRole('list', { name: '打开工具' })).toBeVisible();
await page.getByRole('button', { name: /变更.*查看当前 Git 工作区变化/ }).click();
return page.getByRole('region', { name: 'Git 变更' });
Expand All@@ -20,14 +20,14 @@ test('titlebar workbar action restores an existing tool instead of the picker',
const page = gitReviewWindow.page;
const workspaceActions = page.getByRole('toolbar', { name: '工作区辅助操作' });
const panel = await openGitChanges(page);
await expect(workspaceActions.getByRole('button', { name: '收起会话工作栏' })).toBeVisible();
await expect(workspaceActions.getByRole('button', { name: '收起任务工作栏' })).toBeVisible();
await expect(workspaceActions.getByRole('button', { name: '打开工作栏工具' })).toHaveCount(0);
await page.getByRole('button', { name: '打开工作栏标签' }).click();
const picker = page.getByRole('list', { name: '打开工具' });
await expect(picker).toBeVisible();

await workspaceActions.getByRole('button', { name: '收起会话工作栏' }).click();
await workspaceActions.getByRole('button', { name: '展开会话工作栏' }).click();
await workspaceActions.getByRole('button', { name: '收起任务工作栏' }).click();
await workspaceActions.getByRole('button', { name: '展开任务工作栏' }).click();

await expect(panel).toBeVisible();
await expect(picker).not.toBeVisible();
Expand Down
4 changes: 2 additions & 2 deletions apps/desktop/e2e/settings.spec.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -8,8 +8,8 @@ test('opening settings commits an active titlebar rename', async ({ window: page
const identity = page.locator('[data-maka-contract="titlebar-identity"]');
await expect(identity).toBeVisible();
await page.getByRole('button', { name: '展开侧边栏' }).click();
await identity.getByRole('button', { name: /重命名对话/ }).click();
await page.getByRole('textbox', { name: '重命名对话' }).fill('renamed before settings');
await identity.getByRole('button', { name: /重命名任务/ }).click();
await page.getByRole('textbox', { name: '重命名任务' }).fill('renamed before settings');

// Programmatic activation preserves input focus, matching the macOS
// application-menu command that opens Settings before Chromium can blur it.
Expand Down
2 changes: 1 addition & 1 deletion apps/desktop/e2e/sidebar-project-row.spec.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -10,7 +10,7 @@ test('project navigation and actions remain adjacent keyboard controls', async (
await page.keyboard.press('Escape');
await expect(page.locator('[data-maka-contract="search-modal"]')).not.toBeVisible();

const sidebar = page.getByRole('navigation', { name: '对话列表' });
const sidebar = page.getByRole('navigation', { name: '任务列表' });
await sidebar.getByRole('radio', { name: '按项目', exact: true }).click();

const projectRow = sidebar.locator(
Expand Down
15 changes: 12 additions & 3 deletions apps/desktop/e2e/streaming-remount.spec.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -18,11 +18,20 @@ test('remounting a live surface leaves accumulated output settled', async ({
const liveBubble = page.locator('.maka-bubble-streaming');
await expect(liveBubble).toContainText(accumulatedOutput);

const sidebar = page.getByRole('navigation', { name: '对话列表' });
const sidebar = page.getByRole('navigation', { name: '任务列表' });
await sidebar.getByRole('button', { name: '扩展' }).click();
await expect(page.locator('[data-module="skills"]')).toBeVisible();
await expect(liveBubble).toHaveCount(0);
await sidebar.getByRole('button', { name: '会话', exact: true }).click();
// Back the way the product actually offers: the rail is collapsed here, so
// the task rows are not rendered and there is no 任务 row to press (#2984).
// Widening it is the titlebar's job, and the task left behind is still
// `activeId`, so it comes back marked and one click away.
await page.getByRole('button', { name: '展开侧边栏' }).click();
const currentTaskRow = sidebar.locator(
'[data-maka-contract="session-row"] [aria-current="page"]',
);
await expect(currentTaskRow).toHaveCount(1);
await currentTaskRow.click();
await expect(liveBubble).toHaveCount(1);
await expect(liveBubble).toContainText(accumulatedOutput);

Expand DownExpand Up@@ -75,7 +84,7 @@ test('returning to a live conversation settles output accumulated while away', a
const liveBubble = page.locator('.maka-bubble-streaming');
await expect(liveBubble).toContainText(accumulatedOutput);

const sidebar = page.getByRole('navigation', { name: '对话列表' });
const sidebar = page.getByRole('navigation', { name: '任务列表' });
await page.getByRole('button', { name: '展开侧边栏' }).click();
await expect(page.locator('[data-agents-page]')).toHaveAttribute(
'data-sidebar-state',
Expand Down
16 changes: 13 additions & 3 deletions apps/desktop/src/main/__tests__/app-shell-session-purge.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -11,7 +11,7 @@ function summary(id: string, overrides: Partial<SessionSummary> = {}): SessionSu
isArchived: true,
labels: [],
hasUnread: false,
status: 'done',
status: 'archived',
Comment thread
coderabbitai[bot] marked this conversation as resolved.
backend: 'fake',
llmConnectionSlug: 'test',
connectionLocked: true,
Expand All@@ -21,6 +21,16 @@ function summary(id: string, overrides: Partial<SessionSummary> = {}): SessionSu
};
}

/**
* A task that left the archive. Both fields move together because that is what
* `SessionStore.unarchive` writes; flipping only `isArchived` would build a row
* the store cannot produce, and the sweep would then be tested against a state
* it will never meet.
*/
function restored(id: string): SessionSummary {
return summary(id, { isArchived: false, status: 'active' });
}

type SweepHarness = {
removed: string[];
cleared: string[];
Expand DownExpand Up@@ -128,7 +138,7 @@ describe('purgeSessions', () => {
// dialog was up has left it, and a sweep that deleted it anyway would be
// acting outside what was agreed to.
const h = harness();
const sessions = [summary('kept', { isArchived: false }), summary('doomed')];
const sessions = [restored('kept'), summary('doomed')];
const restore = installWindow(h);
const actions = createActions({ harness: h, sessions, activeIdRef: { current: undefined } });

Expand All@@ -148,7 +158,7 @@ describe('purgeSessions', () => {
const sessions = [summary('first'), summary('second')];
const restore = installWindow(h, {
onRemove: (id) => {
if (id === 'first') sessions[1] = summary('second', { isArchived: false });
if (id === 'first') sessions[1] = restored('second');
},
});
const actions = createActions({ harness: h, sessions, activeIdRef: { current: undefined } });
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -167,7 +167,7 @@ describe('AppShell session settings actions', () => {

assert.deepEqual(harness.successes, [
{
title: '已切换当前会话模型',
title: '已切换当前任务模型',
description: 'claude-haiku → claude-opus',
},
]);
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -42,7 +42,7 @@ describe('bot incoming new-session cwd', () => {
} as unknown as BotIncomingMessage);

assert.deepEqual(createInput, {
name: 'Telegram 对话',
name: 'Telegram 任务',
labels: ['bot', 'telegram'],
});
});
Expand Down

This file was deleted.

Original file line numberDiff line numberDiff line change
Expand Up@@ -59,7 +59,7 @@ test('forwards bounded external Session requests and publishes imported Sessions
assert.deepEqual(events, [{ reason: 'created', sessionId: 'imported-1' }]);
});

test('preserves commit uncertainty as a structured non-retryable IPC result', async () => {
test('an uncertain commit still asks the shell to re-read the catalog', async () => {
const events: unknown[] = [];
const ipc = ipcHarness();
registerRuntimeHostExternalSessionsIpc(
Expand All@@ -85,7 +85,12 @@ test('preserves commit uncertainty as a structured non-retryable IPC result', as
}),
{ ok: false, reason: 'commit_outcome_unknown' },
);
assert.deepEqual(events, []);
// The task may be in the catalog, so the shell has to look. The import page's
// own banner cannot be the only trace: 导入任务 is a Settings page, and the
// moment the user leaves it the banner is unmounted -- which is exactly when
// they come back and import the same conversation again. No id, because not
// knowing which task landed is what `commit_outcome_unknown` means.
assert.deepEqual(events, [{ reason: 'created', sessionId: undefined }]);
});

test('rejects malformed renderer requests before they reach the Host client', async () => {
Expand Down
2 changes: 1 addition & 1 deletion apps/desktop/src/main/__tests__/thread-search.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -215,7 +215,7 @@ describe('runThreadSearch', () => {
await runThreadSearch({ source: 'thread', query: 'roadmap', limit: 5 }, makeDeps(entries)),
)[0]!;
assert.deepEqual(titleHit.target, { kind: 'thread', sessionId: 's1' });
assert.equal(titleHit.summary, '会话标题');
assert.equal(titleHit.summary, '任务标题');
assert.equal(titleHit.url, undefined);
assert.match(titleHit.snippet ?? '', /\[redacted\]/);
assert.equal(titleHit.snippet?.includes('sk-ant-test-secret-token-12345'), false);
Expand Down
10 changes: 5 additions & 5 deletions apps/desktop/src/main/bot-incoming-main.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -183,15 +183,15 @@ export function createBotIncomingMainService(deps: BotIncomingMainServiceDeps):
noticeTtlMs: number,
): Promise<string | undefined> {
if (botConversationSessions.size >= BOT_CONVERSATION_SESSION_LIMIT) {
await sendTransientBotNotice(message, 'Maka 当前机器人会话数量已达上限,请重置或清理旧会话后再试。', noticeTtlMs);
await sendTransientBotNotice(message, 'Maka 当前机器人任务数量已达上限,请重置或清理旧任务后再试。', noticeTtlMs);
return undefined;
}
if (!consumeBotConversationToken(conversationKey)) {
await sendTransientBotNotice(message, 'Maka 收到的机器人消息过于频繁,请稍后再试。', noticeTtlMs);
return undefined;
}
const sessionId = await deps.sessions.createSession({
name: `${botDisplayLabel(message.platform)} 对话`,
name: `${botDisplayLabel(message.platform)} 任务`,
labels: ['bot', message.platform],
});
botConversationSessions.set(conversationKey, sessionId);
Expand DownExpand Up@@ -240,8 +240,8 @@ export function createBotIncomingMainService(deps: BotIncomingMainServiceDeps):
ephemeralTtlMs: SYSTEM_NOTICE_TTL_MS,
};
const ack = had
? '会话已重置,下一条消息会开新对话。'
: '当前没有进行中的对话;下一条消息会开新对话。';
? '任务已重置,下一条消息会开新任务。'
: '当前没有进行中的任务;下一条消息会开新任务。';
await deps.botRegistry.sendMessage(message.platform, message.chatId, ack, replyOptions).catch(() => null);
return;
}
Expand DownExpand Up@@ -396,7 +396,7 @@ export function createBotIncomingMainService(deps: BotIncomingMainServiceDeps):
if ((await deps.sessions.prepareSession(sessionId)) === 'ready') return true;
await sendTransientBotNotice(
message,
'Maka 已拒绝这条机器人消息:绑定会话当前不是只读探索模式,请先在桌面端切回 explore 后再试。',
'Maka 已拒绝这条机器人消息:绑定任务当前不是只读探索模式,请先在桌面端切回 explore 后再试。',
noticeTtlMs,
);
return false;
Expand Down
2 changes: 1 addition & 1 deletion apps/desktop/src/main/capability-snapshot.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -61,7 +61,7 @@ export function buildCapabilitySnapshotCollection(input: {
feature: {
state: 'partial',
source: 'runtime',
reason: 'Daily Review 已聚合本地会话 / 工具 / 模型活动;当前不包含屏幕与应用级录制',
reason: 'Daily Review 已聚合本地任务 / 工具 / 模型活动;当前不包含屏幕与应用级录制',
},
requiredPermissions: [
{ id: 'screen_recording', required: false, status: permissions.screen_recording.status },
Expand Down
17 changes: 12 additions & 5 deletions apps/desktop/src/main/chat-readiness.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -106,6 +106,16 @@ export async function requireReadyConnection(
* so the copy stays close to its existing semantics (PR110a refactor
* is behavior-preserving — only the judgment moved to core).
*/
/**
* Two paths reach `fake_backend`: the reason table below, and the header check
* in `assertSessionCanSend`, which never gets far enough to look a connection
* up. They are the same sentence to the user, so they are the same string here
* — the rename that moved 会话 to 任务 had to be applied twice, which is what a
* second copy costs.
*/
const FAKE_BACKEND_MESSAGE =
'当前任务来自旧的本地模拟连接,不能直接发送。请到 设置 · 模型 添加真实模型后新建任务。';

function messageForReason(
reason: ChatConfigurationReason,
connection: LlmConnection,
Expand All@@ -132,7 +142,7 @@ function messageForReason(
return `模型 "${model}" 不能用于聊天。请到 设置 · 模型 选择支持聊天的模型。`;
}
case 'fake_backend':
return '当前会话来自旧的本地模拟连接,不能直接发送。请到 设置 · 模型 添加真实模型后新建会话。';
return FAKE_BACKEND_MESSAGE;
case 'missing_default_connection':
case 'connection_missing':
// These reasons are handled before we reach isConnectionReady,
Expand All@@ -146,10 +156,7 @@ export async function assertSessionCanSend(
deps: ReadyConnectionDeps,
): Promise<void> {
if (header.backend === 'fake') {
throw chatConfigurationError(
'当前会话来自旧的本地模拟连接,不能直接发送。请到 设置 · 模型 添加真实模型后新建会话。',
'fake_backend',
);
throw chatConfigurationError(FAKE_BACKEND_MESSAGE, 'fake_backend');
}
await requireReadyConnection(header.llmConnectionSlug, deps, header.model);
}
Expand Down
2 changes: 1 addition & 1 deletion apps/desktop/src/main/computer-use/status-item.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -114,7 +114,7 @@ const COPY: UiCatalog<StatusItemCopy> = {
zh: {
stopUsing: (appName) => `停止操作 ${appName}`,
stopUnnamed: '停止 Computer Use',
empty: '没有正在进行的会话',
empty: '没有正在进行的任务',
},
en: {
stopUsing: (appName) => `Stop Using ${appName}`,
Expand Down
2 changes: 1 addition & 1 deletion apps/desktop/src/main/e2e-fixture/scenarios-sessions.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -17,7 +17,7 @@ export function longSidebarSessions(
return {
header: header({
id: sessionId,
name: `会话 ${suffix}`,
name: `任务 ${suffix}`,
connection: 'zai-live',
model: 'glm-5.1',
now,
Expand Down
2 changes: 1 addition & 1 deletion apps/desktop/src/main/e2e-fixture/scenarios-settings.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -183,7 +183,7 @@ export async function writeScheduledTasks(workspaceRoot: string, now: number): P
for (const [index, title] of [
'每日站会前汇总阻塞项',
'每月依赖许可证审计',
'季度收尾清点未归档会话',
'季度收尾清点未归档任务',
'发布前跑一轮回归',
].entries()) {
await create(
Expand Down
2 changes: 1 addition & 1 deletion apps/desktop/src/main/notifications-policy.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -58,7 +58,7 @@ export interface RunNotificationCopy {
*/
function runNotificationCopy(kind: RunNotificationKind): RunNotificationCopy {
if (kind === 'errored') {
return { title: '对话出错', body: '本轮回答未能完成,点击查看详情。' };
return { title: '任务出错', body: '本轮回答未能完成,点击查看详情。' };
}
return { title: '回答已生成', body: 'Maka 已完成本轮回答,点击查看。' };
}
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -68,6 +68,13 @@ export function registerRuntimeHostExternalSessionsIpc(
error.operation === 'external-session.import' &&
error.code === 'commit_outcome_unknown'
) {
// "Unknown" means the task may well be in the catalog, so tell the
// shell to read it again. Without this, the only trace of a maybe-
// committed import is the banner on the page, and the page is gone the
// moment the user leaves Settings -- which is exactly when they come
// back and import the same conversation a second time. No id: the
// whole point is that we do not know which task, if any, landed.
deps.emitSessionsChanged('created');
return {
ok: false,
reason: 'commit_outcome_unknown',
Expand Down
2 changes: 1 addition & 1 deletion apps/desktop/src/main/search/thread-search.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -202,7 +202,7 @@ export async function runThreadSearch(
results.push({
source: THREAD_SOURCE,
title: session.name,
summary: '会话标题',
summary: '任务标题',
snippet,
target: {
kind: 'thread',
Expand Down
4 changes: 2 additions & 2 deletions apps/desktop/src/preload/preload.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -421,7 +421,7 @@ function executeWebSearchQuery(input: {
return Promise.resolve({
ok: false,
reason: 'unsupported_provider',
message: '原生联网搜索由对话中的主模型请求执行,不支持从设置页单独调用。',
message: '原生联网搜索由任务中的主模型请求执行,不支持从设置页单独调用。',
});
}
const query = normalizeWebSearchQuery(input.query);
Expand All@@ -448,7 +448,7 @@ function executeWebSearchTest(input: {
return Promise.resolve({
ok: false,
reason: 'unsupported_provider',
message: '原生联网搜索由对话中的主模型请求执行,不需要单独测试搜索凭据。',
message: '原生联网搜索由任务中的主模型请求执行,不需要单独测试搜索凭据。',
});
}
const apiKey = webSearchCredentialOverride(input.apiKey);
Expand Down
4 changes: 2 additions & 2 deletions apps/desktop/src/renderer/agent-graph-panel.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -53,7 +53,7 @@ export function getAgentGraphPanelCopy(locale: UiLocale): GraphPanelCopy {
stopping: '停止中…',
stopFailed: '停止 Graph 失败,请重试。',
loadFailed: 'Graph 状态刷新失败。',
openSession: '打开子会话',
openSession: '打开子任务',
operators: 'Operators',
selectedResults: '已选择结果',
noOperators: '等待主 Agent 创建 operator…',
Expand DownExpand Up@@ -96,7 +96,7 @@ export function getAgentGraphPanelCopy(locale: UiLocale): GraphPanelCopy {
stopping: 'Stopping…',
stopFailed: 'Could not stop the graph. Try again.',
loadFailed: 'Could not refresh graph state.',
openSession: 'Open child session',
openSession: 'Open child task',
operators: 'Operators',
selectedResults: 'Selected results',
noOperators: 'Waiting for the main agent to create an operator…',
Expand Down
2 changes: 1 addition & 1 deletion apps/desktop/src/renderer/app-shell-chat-actions.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -386,7 +386,7 @@ export function createAppShellChatActions(deps: {
showSkillInvocationFeedback(uiLocale, toastApi, sendResult.skillInvocation);
}
if (newChatOwner && isNewChatSendSurfaceActive(newChatOwner)) {
setNavSelection({ section: 'sessions', filter: 'chats' });
setNavSelection({ section: 'sessions' });
setActiveId(session.id);
showOptimisticUserMessage(
session.id,
Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Force GitHub README to respect dark mode (function() { var style = document.createElement('style'); style.textContent = ' .markdown-body { color-scheme: dark light; } .markdown-body pre { background: #161b22 !important; } .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; } .markdown-body table th, .markdown-body table td { border-color: #30363d !important; } .markdown-body img { background: #0d1117; } .markdown-body blockquote { border-left-color: #8b949e; } .markdown-body hr { border-color: #30363d; } '; document.head.appendChild(style); })(); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
1b24718
fix(ui): give the task rail's status dot one meaning
Astro-Han Aug 15, 2026
850c34c
refactor(ui): rebuild the task row on two signal slots
Astro-Han Aug 15, 2026
d8b93fe
refactor(ui): give the rail one top region and one grouping switch
Astro-Han Aug 15, 2026
53bd296
feat(desktop): move task import out of the rail into settings
Astro-Han Aug 15, 2026
ed2b6d0
refactor: name the thing a user works on 任务 / task everywhere
Astro-Han Aug 15, 2026
4bfc99d
test(ui): make the narrow-rail story actually narrow
Astro-Han Aug 15, 2026
10ca8e5
fix(ui): keep the rail's 任务 row as the way back to the list
Astro-Han Aug 15, 2026
09b1f90
fix(ui): route task status through the shared semantic layer
Astro-Han Aug 15, 2026
b90b60a
fix(ui): keep the task row's facts when it collapses to two slots
Astro-Han Aug 15, 2026
550f954
fix(desktop): refresh the catalog when an import's outcome is unknown
Astro-Han Aug 15, 2026
e275726
fix(desktop): re-point the structural gates the surface move broke
Astro-Han Aug 15, 2026
a17a01a
refactor: finish the 任务 rename where the rule missed
Astro-Han Aug 15, 2026
971bef3
fix(ui): route the task row's last two dots through the shared vocabu…
Astro-Han Aug 15, 2026
60d2631
docs: say what each session-status-presentation file actually owns
Astro-Han Aug 15, 2026
74b2e80
test: make the restored-task and import fixtures answer like their so…
Astro-Han Aug 15, 2026
f531b98
fix(desktop): tidy the two places the 任务 rename passed through twice
Astro-Han Aug 15, 2026
aea0707
chore: regenerate the Astryx surface inventory
Astro-Han Aug 15, 2026
4b17102
test(ui): show every task status in the story that exists to show them
Astro-Han Aug 15, 2026
642d524
refactor(ui): drop the rail's 任务 row
Astro-Han Aug 15, 2026
f2dfdda
fix(runtime-host): coalesce a repeat import while the first is still …
Astro-Han Aug 15, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion apps/desktop/e2e/composer-skill-invocation.spec.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -30,7 +30,7 @@ test('staged Skills come back as chips after leaving and returning', async ({
await expect(workspaceChip).toContainText('Workspace Only');

await page.getByRole('button', { name: '展开侧边栏' }).click();
const sidebar = page.getByRole('navigation', { name: '对话列表' });
const sidebar = page.getByRole('navigation', { name: '任务列表' });
await sidebar.getByRole('button', { name: '新任务', exact: true }).click();
await expect(composer).toHaveText('');

Expand Down
8 changes: 4 additions & 4 deletions apps/desktop/e2e/session-workbar.spec.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -8,7 +8,7 @@ async function openGitChanges(page: Page) {
await composer.fill('create review session');
await composer.press('Enter');
await expect(page.getByText(/Fake backend received: create review session/)).toBeVisible();
await page.getByRole('button', { name: '展开会话工作栏' }).click();
await page.getByRole('button', { name: '展开任务工作栏' }).click();
await expect(page.getByRole('list', { name: '打开工具' })).toBeVisible();
await page.getByRole('button', { name: /变更.*查看当前 Git 工作区变化/ }).click();
return page.getByRole('region', { name: 'Git 变更' });
Expand All@@ -20,14 +20,14 @@ test('titlebar workbar action restores an existing tool instead of the picker',
const page = gitReviewWindow.page;
const workspaceActions = page.getByRole('toolbar', { name: '工作区辅助操作' });
const panel = await openGitChanges(page);
await expect(workspaceActions.getByRole('button', { name: '收起会话工作栏' })).toBeVisible();
await expect(workspaceActions.getByRole('button', { name: '收起任务工作栏' })).toBeVisible();
await expect(workspaceActions.getByRole('button', { name: '打开工作栏工具' })).toHaveCount(0);
await page.getByRole('button', { name: '打开工作栏标签' }).click();
const picker = page.getByRole('list', { name: '打开工具' });
await expect(picker).toBeVisible();

await workspaceActions.getByRole('button', { name: '收起会话工作栏' }).click();
await workspaceActions.getByRole('button', { name: '展开会话工作栏' }).click();
await workspaceActions.getByRole('button', { name: '收起任务工作栏' }).click();
await workspaceActions.getByRole('button', { name: '展开任务工作栏' }).click();

await expect(panel).toBeVisible();
await expect(picker).not.toBeVisible();
Expand Down
4 changes: 2 additions & 2 deletions apps/desktop/e2e/settings.spec.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -8,8 +8,8 @@ test('opening settings commits an active titlebar rename', async ({ window: page
const identity = page.locator('[data-maka-contract="titlebar-identity"]');
await expect(identity).toBeVisible();
await page.getByRole('button', { name: '展开侧边栏' }).click();
await identity.getByRole('button', { name: /重命名对话/ }).click();
await page.getByRole('textbox', { name: '重命名对话' }).fill('renamed before settings');
await identity.getByRole('button', { name: /重命名任务/ }).click();
await page.getByRole('textbox', { name: '重命名任务' }).fill('renamed before settings');

// Programmatic activation preserves input focus, matching the macOS
// application-menu command that opens Settings before Chromium can blur it.
Expand Down
2 changes: 1 addition & 1 deletion apps/desktop/e2e/sidebar-project-row.spec.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -10,7 +10,7 @@ test('project navigation and actions remain adjacent keyboard controls', async (
await page.keyboard.press('Escape');
await expect(page.locator('[data-maka-contract="search-modal"]')).not.toBeVisible();

const sidebar = page.getByRole('navigation', { name: '对话列表' });
const sidebar = page.getByRole('navigation', { name: '任务列表' });
await sidebar.getByRole('radio', { name: '按项目', exact: true }).click();

const projectRow = sidebar.locator(
Expand Down
15 changes: 12 additions & 3 deletions apps/desktop/e2e/streaming-remount.spec.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -18,11 +18,20 @@ test('remounting a live surface leaves accumulated output settled', async ({
const liveBubble = page.locator('.maka-bubble-streaming');
await expect(liveBubble).toContainText(accumulatedOutput);

const sidebar = page.getByRole('navigation', { name: '对话列表' });
const sidebar = page.getByRole('navigation', { name: '任务列表' });
await sidebar.getByRole('button', { name: '扩展' }).click();
await expect(page.locator('[data-module="skills"]')).toBeVisible();
await expect(liveBubble).toHaveCount(0);
await sidebar.getByRole('button', { name: '会话', exact: true }).click();
// Back the way the product actually offers: the rail is collapsed here, so
// the task rows are not rendered and there is no 任务 row to press (#2984).
// Widening it is the titlebar's job, and the task left behind is still
// `activeId`, so it comes back marked and one click away.
await page.getByRole('button', { name: '展开侧边栏' }).click();
const currentTaskRow = sidebar.locator(
'[data-maka-contract="session-row"] [aria-current="page"]',
);
await expect(currentTaskRow).toHaveCount(1);
await currentTaskRow.click();
await expect(liveBubble).toHaveCount(1);
await expect(liveBubble).toContainText(accumulatedOutput);

Expand DownExpand Up@@ -75,7 +84,7 @@ test('returning to a live conversation settles output accumulated while away', a
const liveBubble = page.locator('.maka-bubble-streaming');
await expect(liveBubble).toContainText(accumulatedOutput);

const sidebar = page.getByRole('navigation', { name: '对话列表' });
const sidebar = page.getByRole('navigation', { name: '任务列表' });
await page.getByRole('button', { name: '展开侧边栏' }).click();
await expect(page.locator('[data-agents-page]')).toHaveAttribute(
'data-sidebar-state',
Expand Down
16 changes: 13 additions & 3 deletions apps/desktop/src/main/__tests__/app-shell-session-purge.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -11,7 +11,7 @@ function summary(id: string, overrides: Partial<SessionSummary> = {}): SessionSu
isArchived: true,
labels: [],
hasUnread: false,
status: 'done',
status: 'archived',
Comment thread
coderabbitai[bot] marked this conversation as resolved.
backend: 'fake',
llmConnectionSlug: 'test',
connectionLocked: true,
Expand All@@ -21,6 +21,16 @@ function summary(id: string, overrides: Partial<SessionSummary> = {}): SessionSu
};
}

/**
* A task that left the archive. Both fields move together because that is what
* `SessionStore.unarchive` writes; flipping only `isArchived` would build a row
* the store cannot produce, and the sweep would then be tested against a state
* it will never meet.
*/
function restored(id: string): SessionSummary {
return summary(id, { isArchived: false, status: 'active' });
}

type SweepHarness = {
removed: string[];
cleared: string[];
Expand DownExpand Up@@ -128,7 +138,7 @@ describe('purgeSessions', () => {
// dialog was up has left it, and a sweep that deleted it anyway would be
// acting outside what was agreed to.
const h = harness();
const sessions = [summary('kept', { isArchived: false }), summary('doomed')];
const sessions = [restored('kept'), summary('doomed')];
const restore = installWindow(h);
const actions = createActions({ harness: h, sessions, activeIdRef: { current: undefined } });

Expand All@@ -148,7 +158,7 @@ describe('purgeSessions', () => {
const sessions = [summary('first'), summary('second')];
const restore = installWindow(h, {
onRemove: (id) => {
if (id === 'first') sessions[1] = summary('second', { isArchived: false });
if (id === 'first') sessions[1] = restored('second');
},
});
const actions = createActions({ harness: h, sessions, activeIdRef: { current: undefined } });
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -167,7 +167,7 @@ describe('AppShell session settings actions', () => {

assert.deepEqual(harness.successes, [
{
title: '已切换当前会话模型',
title: '已切换当前任务模型',
description: 'claude-haiku → claude-opus',
},
]);
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -42,7 +42,7 @@ describe('bot incoming new-session cwd', () => {
} as unknown as BotIncomingMessage);

assert.deepEqual(createInput, {
name: 'Telegram 对话',
name: 'Telegram 任务',
labels: ['bot', 'telegram'],
});
});
Expand Down

This file was deleted.

Original file line numberDiff line numberDiff line change
Expand Up@@ -59,7 +59,7 @@ test('forwards bounded external Session requests and publishes imported Sessions
assert.deepEqual(events, [{ reason: 'created', sessionId: 'imported-1' }]);
});

test('preserves commit uncertainty as a structured non-retryable IPC result', async () => {
test('an uncertain commit still asks the shell to re-read the catalog', async () => {
const events: unknown[] = [];
const ipc = ipcHarness();
registerRuntimeHostExternalSessionsIpc(
Expand All@@ -85,7 +85,12 @@ test('preserves commit uncertainty as a structured non-retryable IPC result', as
}),
{ ok: false, reason: 'commit_outcome_unknown' },
);
assert.deepEqual(events, []);
// The task may be in the catalog, so the shell has to look. The import page's
// own banner cannot be the only trace: 导入任务 is a Settings page, and the
// moment the user leaves it the banner is unmounted -- which is exactly when
// they come back and import the same conversation again. No id, because not
// knowing which task landed is what `commit_outcome_unknown` means.
assert.deepEqual(events, [{ reason: 'created', sessionId: undefined }]);
});

test('rejects malformed renderer requests before they reach the Host client', async () => {
Expand Down
2 changes: 1 addition & 1 deletion apps/desktop/src/main/__tests__/thread-search.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -215,7 +215,7 @@ describe('runThreadSearch', () => {
await runThreadSearch({ source: 'thread', query: 'roadmap', limit: 5 }, makeDeps(entries)),
)[0]!;
assert.deepEqual(titleHit.target, { kind: 'thread', sessionId: 's1' });
assert.equal(titleHit.summary, '会话标题');
assert.equal(titleHit.summary, '任务标题');
assert.equal(titleHit.url, undefined);
assert.match(titleHit.snippet ?? '', /\[redacted\]/);
assert.equal(titleHit.snippet?.includes('sk-ant-test-secret-token-12345'), false);
Expand Down
10 changes: 5 additions & 5 deletions apps/desktop/src/main/bot-incoming-main.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -183,15 +183,15 @@ export function createBotIncomingMainService(deps: BotIncomingMainServiceDeps):
noticeTtlMs: number,
): Promise<string | undefined> {
if (botConversationSessions.size >= BOT_CONVERSATION_SESSION_LIMIT) {
await sendTransientBotNotice(message, 'Maka 当前机器人会话数量已达上限,请重置或清理旧会话后再试。', noticeTtlMs);
await sendTransientBotNotice(message, 'Maka 当前机器人任务数量已达上限,请重置或清理旧任务后再试。', noticeTtlMs);
return undefined;
}
if (!consumeBotConversationToken(conversationKey)) {
await sendTransientBotNotice(message, 'Maka 收到的机器人消息过于频繁,请稍后再试。', noticeTtlMs);
return undefined;
}
const sessionId = await deps.sessions.createSession({
name: `${botDisplayLabel(message.platform)} 对话`,
name: `${botDisplayLabel(message.platform)} 任务`,
labels: ['bot', message.platform],
});
botConversationSessions.set(conversationKey, sessionId);
Expand DownExpand Up@@ -240,8 +240,8 @@ export function createBotIncomingMainService(deps: BotIncomingMainServiceDeps):
ephemeralTtlMs: SYSTEM_NOTICE_TTL_MS,
};
const ack = had
? '会话已重置,下一条消息会开新对话。'
: '当前没有进行中的对话;下一条消息会开新对话。';
? '任务已重置,下一条消息会开新任务。'
: '当前没有进行中的任务;下一条消息会开新任务。';
await deps.botRegistry.sendMessage(message.platform, message.chatId, ack, replyOptions).catch(() => null);
return;
}
Expand DownExpand Up@@ -396,7 +396,7 @@ export function createBotIncomingMainService(deps: BotIncomingMainServiceDeps):
if ((await deps.sessions.prepareSession(sessionId)) === 'ready') return true;
await sendTransientBotNotice(
message,
'Maka 已拒绝这条机器人消息:绑定会话当前不是只读探索模式,请先在桌面端切回 explore 后再试。',
'Maka 已拒绝这条机器人消息:绑定任务当前不是只读探索模式,请先在桌面端切回 explore 后再试。',
noticeTtlMs,
);
return false;
Expand Down
2 changes: 1 addition & 1 deletion apps/desktop/src/main/capability-snapshot.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -61,7 +61,7 @@ export function buildCapabilitySnapshotCollection(input: {
feature: {
state: 'partial',
source: 'runtime',
reason: 'Daily Review 已聚合本地会话 / 工具 / 模型活动;当前不包含屏幕与应用级录制',
reason: 'Daily Review 已聚合本地任务 / 工具 / 模型活动;当前不包含屏幕与应用级录制',
},
requiredPermissions: [
{ id: 'screen_recording', required: false, status: permissions.screen_recording.status },
Expand Down
17 changes: 12 additions & 5 deletions apps/desktop/src/main/chat-readiness.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -106,6 +106,16 @@ export async function requireReadyConnection(
* so the copy stays close to its existing semantics (PR110a refactor
* is behavior-preserving — only the judgment moved to core).
*/
/**
* Two paths reach `fake_backend`: the reason table below, and the header check
* in `assertSessionCanSend`, which never gets far enough to look a connection
* up. They are the same sentence to the user, so they are the same string here
* — the rename that moved 会话 to 任务 had to be applied twice, which is what a
* second copy costs.
*/
const FAKE_BACKEND_MESSAGE =
'当前任务来自旧的本地模拟连接,不能直接发送。请到 设置 · 模型 添加真实模型后新建任务。';

function messageForReason(
reason: ChatConfigurationReason,
connection: LlmConnection,
Expand All@@ -132,7 +142,7 @@ function messageForReason(
return `模型 "${model}" 不能用于聊天。请到 设置 · 模型 选择支持聊天的模型。`;
}
case 'fake_backend':
return '当前会话来自旧的本地模拟连接,不能直接发送。请到 设置 · 模型 添加真实模型后新建会话。';
return FAKE_BACKEND_MESSAGE;
case 'missing_default_connection':
case 'connection_missing':
// These reasons are handled before we reach isConnectionReady,
Expand All@@ -146,10 +156,7 @@ export async function assertSessionCanSend(
deps: ReadyConnectionDeps,
): Promise<void> {
if (header.backend === 'fake') {
throw chatConfigurationError(
'当前会话来自旧的本地模拟连接,不能直接发送。请到 设置 · 模型 添加真实模型后新建会话。',
'fake_backend',
);
throw chatConfigurationError(FAKE_BACKEND_MESSAGE, 'fake_backend');
}
await requireReadyConnection(header.llmConnectionSlug, deps, header.model);
}
Expand Down
2 changes: 1 addition & 1 deletion apps/desktop/src/main/computer-use/status-item.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -114,7 +114,7 @@ const COPY: UiCatalog<StatusItemCopy> = {
zh: {
stopUsing: (appName) => `停止操作 ${appName}`,
stopUnnamed: '停止 Computer Use',
empty: '没有正在进行的会话',
empty: '没有正在进行的任务',
},
en: {
stopUsing: (appName) => `Stop Using ${appName}`,
Expand Down
2 changes: 1 addition & 1 deletion apps/desktop/src/main/e2e-fixture/scenarios-sessions.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -17,7 +17,7 @@ export function longSidebarSessions(
return {
header: header({
id: sessionId,
name: `会话 ${suffix}`,
name: `任务 ${suffix}`,
connection: 'zai-live',
model: 'glm-5.1',
now,
Expand Down
2 changes: 1 addition & 1 deletion apps/desktop/src/main/e2e-fixture/scenarios-settings.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -183,7 +183,7 @@ export async function writeScheduledTasks(workspaceRoot: string, now: number): P
for (const [index, title] of [
'每日站会前汇总阻塞项',
'每月依赖许可证审计',
'季度收尾清点未归档会话',
'季度收尾清点未归档任务',
'发布前跑一轮回归',
].entries()) {
await create(
Expand Down
2 changes: 1 addition & 1 deletion apps/desktop/src/main/notifications-policy.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -58,7 +58,7 @@ export interface RunNotificationCopy {
*/
function runNotificationCopy(kind: RunNotificationKind): RunNotificationCopy {
if (kind === 'errored') {
return { title: '对话出错', body: '本轮回答未能完成,点击查看详情。' };
return { title: '任务出错', body: '本轮回答未能完成,点击查看详情。' };
}
return { title: '回答已生成', body: 'Maka 已完成本轮回答,点击查看。' };
}
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -68,6 +68,13 @@ export function registerRuntimeHostExternalSessionsIpc(
error.operation === 'external-session.import' &&
error.code === 'commit_outcome_unknown'
) {
// "Unknown" means the task may well be in the catalog, so tell the
// shell to read it again. Without this, the only trace of a maybe-
// committed import is the banner on the page, and the page is gone the
// moment the user leaves Settings -- which is exactly when they come
// back and import the same conversation a second time. No id: the
// whole point is that we do not know which task, if any, landed.
deps.emitSessionsChanged('created');
return {
ok: false,
reason: 'commit_outcome_unknown',
Expand Down
2 changes: 1 addition & 1 deletion apps/desktop/src/main/search/thread-search.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -202,7 +202,7 @@ export async function runThreadSearch(
results.push({
source: THREAD_SOURCE,
title: session.name,
summary: '会话标题',
summary: '任务标题',
snippet,
target: {
kind: 'thread',
Expand Down
4 changes: 2 additions & 2 deletions apps/desktop/src/preload/preload.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -421,7 +421,7 @@ function executeWebSearchQuery(input: {
return Promise.resolve({
ok: false,
reason: 'unsupported_provider',
message: '原生联网搜索由对话中的主模型请求执行,不支持从设置页单独调用。',
message: '原生联网搜索由任务中的主模型请求执行,不支持从设置页单独调用。',
});
}
const query = normalizeWebSearchQuery(input.query);
Expand All@@ -448,7 +448,7 @@ function executeWebSearchTest(input: {
return Promise.resolve({
ok: false,
reason: 'unsupported_provider',
message: '原生联网搜索由对话中的主模型请求执行,不需要单独测试搜索凭据。',
message: '原生联网搜索由任务中的主模型请求执行,不需要单独测试搜索凭据。',
});
}
const apiKey = webSearchCredentialOverride(input.apiKey);
Expand Down
4 changes: 2 additions & 2 deletions apps/desktop/src/renderer/agent-graph-panel.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -53,7 +53,7 @@ export function getAgentGraphPanelCopy(locale: UiLocale): GraphPanelCopy {
stopping: '停止中…',
stopFailed: '停止 Graph 失败,请重试。',
loadFailed: 'Graph 状态刷新失败。',
openSession: '打开子会话',
openSession: '打开子任务',
operators: 'Operators',
selectedResults: '已选择结果',
noOperators: '等待主 Agent 创建 operator…',
Expand DownExpand Up@@ -96,7 +96,7 @@ export function getAgentGraphPanelCopy(locale: UiLocale): GraphPanelCopy {
stopping: 'Stopping…',
stopFailed: 'Could not stop the graph. Try again.',
loadFailed: 'Could not refresh graph state.',
openSession: 'Open child session',
openSession: 'Open child task',
operators: 'Operators',
selectedResults: 'Selected results',
noOperators: 'Waiting for the main agent to create an operator…',
Expand Down
2 changes: 1 addition & 1 deletion apps/desktop/src/renderer/app-shell-chat-actions.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -386,7 +386,7 @@ export function createAppShellChatActions(deps: {
showSkillInvocationFeedback(uiLocale, toastApi, sendResult.skillInvocation);
}
if (newChatOwner && isNewChatSendSurfaceActive(newChatOwner)) {
setNavSelection({ section: 'sessions', filter: 'chats' });
setNavSelection({ section: 'sessions' });
setActiveId(session.id);
showOptimisticUserMessage(
session.id,
Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Highlight search terms from Google/DuckDuckGo/Bing referrer (function() { var ref = document.referrer; var terms = []; if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) { var url = new URL(ref); var q = url.searchParams.get('q') || url.searchParams.get('p'); if (q) { terms = q.split(/\s+/).filter(function(t) { return t.length > 2; }); } } if (terms.length === 0) return; var style = document.createElement('style'); style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }'; document.head.appendChild(style); function highlight(node) { if (node.nodeType === 3) { // text node var text = node.textContent; var found = false; terms.forEach(function(term) { var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\]\\]/g, '\\') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
1b24718
fix(ui): give the task rail's status dot one meaning
Astro-Han Aug 15, 2026
850c34c
refactor(ui): rebuild the task row on two signal slots
Astro-Han Aug 15, 2026
d8b93fe
refactor(ui): give the rail one top region and one grouping switch
Astro-Han Aug 15, 2026
53bd296
feat(desktop): move task import out of the rail into settings
Astro-Han Aug 15, 2026
ed2b6d0
refactor: name the thing a user works on 任务 / task everywhere
Astro-Han Aug 15, 2026
4bfc99d
test(ui): make the narrow-rail story actually narrow
Astro-Han Aug 15, 2026
10ca8e5
fix(ui): keep the rail's 任务 row as the way back to the list
Astro-Han Aug 15, 2026
09b1f90
fix(ui): route task status through the shared semantic layer
Astro-Han Aug 15, 2026
b90b60a
fix(ui): keep the task row's facts when it collapses to two slots
Astro-Han Aug 15, 2026
550f954
fix(desktop): refresh the catalog when an import's outcome is unknown
Astro-Han Aug 15, 2026
e275726
fix(desktop): re-point the structural gates the surface move broke
Astro-Han Aug 15, 2026
a17a01a
refactor: finish the 任务 rename where the rule missed
Astro-Han Aug 15, 2026
971bef3
fix(ui): route the task row's last two dots through the shared vocabu…
Astro-Han Aug 15, 2026
60d2631
docs: say what each session-status-presentation file actually owns
Astro-Han Aug 15, 2026
74b2e80
test: make the restored-task and import fixtures answer like their so…
Astro-Han Aug 15, 2026
f531b98
fix(desktop): tidy the two places the 任务 rename passed through twice
Astro-Han Aug 15, 2026
aea0707
chore: regenerate the Astryx surface inventory
Astro-Han Aug 15, 2026
4b17102
test(ui): show every task status in the story that exists to show them
Astro-Han Aug 15, 2026
642d524
refactor(ui): drop the rail's 任务 row
Astro-Han Aug 15, 2026
f2dfdda
fix(runtime-host): coalesce a repeat import while the first is still …
Astro-Han Aug 15, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion apps/desktop/e2e/composer-skill-invocation.spec.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -30,7 +30,7 @@ test('staged Skills come back as chips after leaving and returning', async ({
await expect(workspaceChip).toContainText('Workspace Only');

await page.getByRole('button', { name: '展开侧边栏' }).click();
const sidebar = page.getByRole('navigation', { name: '对话列表' });
const sidebar = page.getByRole('navigation', { name: '任务列表' });
await sidebar.getByRole('button', { name: '新任务', exact: true }).click();
await expect(composer).toHaveText('');

Expand Down
8 changes: 4 additions & 4 deletions apps/desktop/e2e/session-workbar.spec.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -8,7 +8,7 @@ async function openGitChanges(page: Page) {
await composer.fill('create review session');
await composer.press('Enter');
await expect(page.getByText(/Fake backend received: create review session/)).toBeVisible();
await page.getByRole('button', { name: '展开会话工作栏' }).click();
await page.getByRole('button', { name: '展开任务工作栏' }).click();
await expect(page.getByRole('list', { name: '打开工具' })).toBeVisible();
await page.getByRole('button', { name: /变更.*查看当前 Git 工作区变化/ }).click();
return page.getByRole('region', { name: 'Git 变更' });
Expand All@@ -20,14 +20,14 @@ test('titlebar workbar action restores an existing tool instead of the picker',
const page = gitReviewWindow.page;
const workspaceActions = page.getByRole('toolbar', { name: '工作区辅助操作' });
const panel = await openGitChanges(page);
await expect(workspaceActions.getByRole('button', { name: '收起会话工作栏' })).toBeVisible();
await expect(workspaceActions.getByRole('button', { name: '收起任务工作栏' })).toBeVisible();
await expect(workspaceActions.getByRole('button', { name: '打开工作栏工具' })).toHaveCount(0);
await page.getByRole('button', { name: '打开工作栏标签' }).click();
const picker = page.getByRole('list', { name: '打开工具' });
await expect(picker).toBeVisible();

await workspaceActions.getByRole('button', { name: '收起会话工作栏' }).click();
await workspaceActions.getByRole('button', { name: '展开会话工作栏' }).click();
await workspaceActions.getByRole('button', { name: '收起任务工作栏' }).click();
await workspaceActions.getByRole('button', { name: '展开任务工作栏' }).click();

await expect(panel).toBeVisible();
await expect(picker).not.toBeVisible();
Expand Down
4 changes: 2 additions & 2 deletions apps/desktop/e2e/settings.spec.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -8,8 +8,8 @@ test('opening settings commits an active titlebar rename', async ({ window: page
const identity = page.locator('[data-maka-contract="titlebar-identity"]');
await expect(identity).toBeVisible();
await page.getByRole('button', { name: '展开侧边栏' }).click();
await identity.getByRole('button', { name: /重命名对话/ }).click();
await page.getByRole('textbox', { name: '重命名对话' }).fill('renamed before settings');
await identity.getByRole('button', { name: /重命名任务/ }).click();
await page.getByRole('textbox', { name: '重命名任务' }).fill('renamed before settings');

// Programmatic activation preserves input focus, matching the macOS
// application-menu command that opens Settings before Chromium can blur it.
Expand Down
2 changes: 1 addition & 1 deletion apps/desktop/e2e/sidebar-project-row.spec.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -10,7 +10,7 @@ test('project navigation and actions remain adjacent keyboard controls', async (
await page.keyboard.press('Escape');
await expect(page.locator('[data-maka-contract="search-modal"]')).not.toBeVisible();

const sidebar = page.getByRole('navigation', { name: '对话列表' });
const sidebar = page.getByRole('navigation', { name: '任务列表' });
await sidebar.getByRole('radio', { name: '按项目', exact: true }).click();

const projectRow = sidebar.locator(
Expand Down
15 changes: 12 additions & 3 deletions apps/desktop/e2e/streaming-remount.spec.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -18,11 +18,20 @@ test('remounting a live surface leaves accumulated output settled', async ({
const liveBubble = page.locator('.maka-bubble-streaming');
await expect(liveBubble).toContainText(accumulatedOutput);

const sidebar = page.getByRole('navigation', { name: '对话列表' });
const sidebar = page.getByRole('navigation', { name: '任务列表' });
await sidebar.getByRole('button', { name: '扩展' }).click();
await expect(page.locator('[data-module="skills"]')).toBeVisible();
await expect(liveBubble).toHaveCount(0);
await sidebar.getByRole('button', { name: '会话', exact: true }).click();
// Back the way the product actually offers: the rail is collapsed here, so
// the task rows are not rendered and there is no 任务 row to press (#2984).
// Widening it is the titlebar's job, and the task left behind is still
// `activeId`, so it comes back marked and one click away.
await page.getByRole('button', { name: '展开侧边栏' }).click();
const currentTaskRow = sidebar.locator(
'[data-maka-contract="session-row"] [aria-current="page"]',
);
await expect(currentTaskRow).toHaveCount(1);
await currentTaskRow.click();
await expect(liveBubble).toHaveCount(1);
await expect(liveBubble).toContainText(accumulatedOutput);

Expand DownExpand Up@@ -75,7 +84,7 @@ test('returning to a live conversation settles output accumulated while away', a
const liveBubble = page.locator('.maka-bubble-streaming');
await expect(liveBubble).toContainText(accumulatedOutput);

const sidebar = page.getByRole('navigation', { name: '对话列表' });
const sidebar = page.getByRole('navigation', { name: '任务列表' });
await page.getByRole('button', { name: '展开侧边栏' }).click();
await expect(page.locator('[data-agents-page]')).toHaveAttribute(
'data-sidebar-state',
Expand Down
16 changes: 13 additions & 3 deletions apps/desktop/src/main/__tests__/app-shell-session-purge.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -11,7 +11,7 @@ function summary(id: string, overrides: Partial<SessionSummary> = {}): SessionSu
isArchived: true,
labels: [],
hasUnread: false,
status: 'done',
status: 'archived',
Comment thread
coderabbitai[bot] marked this conversation as resolved.
backend: 'fake',
llmConnectionSlug: 'test',
connectionLocked: true,
Expand All@@ -21,6 +21,16 @@ function summary(id: string, overrides: Partial<SessionSummary> = {}): SessionSu
};
}

/**
* A task that left the archive. Both fields move together because that is what
* `SessionStore.unarchive` writes; flipping only `isArchived` would build a row
* the store cannot produce, and the sweep would then be tested against a state
* it will never meet.
*/
function restored(id: string): SessionSummary {
return summary(id, { isArchived: false, status: 'active' });
}

type SweepHarness = {
removed: string[];
cleared: string[];
Expand DownExpand Up@@ -128,7 +138,7 @@ describe('purgeSessions', () => {
// dialog was up has left it, and a sweep that deleted it anyway would be
// acting outside what was agreed to.
const h = harness();
const sessions = [summary('kept', { isArchived: false }), summary('doomed')];
const sessions = [restored('kept'), summary('doomed')];
const restore = installWindow(h);
const actions = createActions({ harness: h, sessions, activeIdRef: { current: undefined } });

Expand All@@ -148,7 +158,7 @@ describe('purgeSessions', () => {
const sessions = [summary('first'), summary('second')];
const restore = installWindow(h, {
onRemove: (id) => {
if (id === 'first') sessions[1] = summary('second', { isArchived: false });
if (id === 'first') sessions[1] = restored('second');
},
});
const actions = createActions({ harness: h, sessions, activeIdRef: { current: undefined } });
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -167,7 +167,7 @@ describe('AppShell session settings actions', () => {

assert.deepEqual(harness.successes, [
{
title: '已切换当前会话模型',
title: '已切换当前任务模型',
description: 'claude-haiku → claude-opus',
},
]);
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -42,7 +42,7 @@ describe('bot incoming new-session cwd', () => {
} as unknown as BotIncomingMessage);

assert.deepEqual(createInput, {
name: 'Telegram 对话',
name: 'Telegram 任务',
labels: ['bot', 'telegram'],
});
});
Expand Down

This file was deleted.

Original file line numberDiff line numberDiff line change
Expand Up@@ -59,7 +59,7 @@ test('forwards bounded external Session requests and publishes imported Sessions
assert.deepEqual(events, [{ reason: 'created', sessionId: 'imported-1' }]);
});

test('preserves commit uncertainty as a structured non-retryable IPC result', async () => {
test('an uncertain commit still asks the shell to re-read the catalog', async () => {
const events: unknown[] = [];
const ipc = ipcHarness();
registerRuntimeHostExternalSessionsIpc(
Expand All@@ -85,7 +85,12 @@ test('preserves commit uncertainty as a structured non-retryable IPC result', as
}),
{ ok: false, reason: 'commit_outcome_unknown' },
);
assert.deepEqual(events, []);
// The task may be in the catalog, so the shell has to look. The import page's
// own banner cannot be the only trace: 导入任务 is a Settings page, and the
// moment the user leaves it the banner is unmounted -- which is exactly when
// they come back and import the same conversation again. No id, because not
// knowing which task landed is what `commit_outcome_unknown` means.
assert.deepEqual(events, [{ reason: 'created', sessionId: undefined }]);
});

test('rejects malformed renderer requests before they reach the Host client', async () => {
Expand Down
2 changes: 1 addition & 1 deletion apps/desktop/src/main/__tests__/thread-search.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -215,7 +215,7 @@ describe('runThreadSearch', () => {
await runThreadSearch({ source: 'thread', query: 'roadmap', limit: 5 }, makeDeps(entries)),
)[0]!;
assert.deepEqual(titleHit.target, { kind: 'thread', sessionId: 's1' });
assert.equal(titleHit.summary, '会话标题');
assert.equal(titleHit.summary, '任务标题');
assert.equal(titleHit.url, undefined);
assert.match(titleHit.snippet ?? '', /\[redacted\]/);
assert.equal(titleHit.snippet?.includes('sk-ant-test-secret-token-12345'), false);
Expand Down
10 changes: 5 additions & 5 deletions apps/desktop/src/main/bot-incoming-main.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -183,15 +183,15 @@ export function createBotIncomingMainService(deps: BotIncomingMainServiceDeps):
noticeTtlMs: number,
): Promise<string | undefined> {
if (botConversationSessions.size >= BOT_CONVERSATION_SESSION_LIMIT) {
await sendTransientBotNotice(message, 'Maka 当前机器人会话数量已达上限,请重置或清理旧会话后再试。', noticeTtlMs);
await sendTransientBotNotice(message, 'Maka 当前机器人任务数量已达上限,请重置或清理旧任务后再试。', noticeTtlMs);
return undefined;
}
if (!consumeBotConversationToken(conversationKey)) {
await sendTransientBotNotice(message, 'Maka 收到的机器人消息过于频繁,请稍后再试。', noticeTtlMs);
return undefined;
}
const sessionId = await deps.sessions.createSession({
name: `${botDisplayLabel(message.platform)} 对话`,
name: `${botDisplayLabel(message.platform)} 任务`,
labels: ['bot', message.platform],
});
botConversationSessions.set(conversationKey, sessionId);
Expand DownExpand Up@@ -240,8 +240,8 @@ export function createBotIncomingMainService(deps: BotIncomingMainServiceDeps):
ephemeralTtlMs: SYSTEM_NOTICE_TTL_MS,
};
const ack = had
? '会话已重置,下一条消息会开新对话。'
: '当前没有进行中的对话;下一条消息会开新对话。';
? '任务已重置,下一条消息会开新任务。'
: '当前没有进行中的任务;下一条消息会开新任务。';
await deps.botRegistry.sendMessage(message.platform, message.chatId, ack, replyOptions).catch(() => null);
return;
}
Expand DownExpand Up@@ -396,7 +396,7 @@ export function createBotIncomingMainService(deps: BotIncomingMainServiceDeps):
if ((await deps.sessions.prepareSession(sessionId)) === 'ready') return true;
await sendTransientBotNotice(
message,
'Maka 已拒绝这条机器人消息:绑定会话当前不是只读探索模式,请先在桌面端切回 explore 后再试。',
'Maka 已拒绝这条机器人消息:绑定任务当前不是只读探索模式,请先在桌面端切回 explore 后再试。',
noticeTtlMs,
);
return false;
Expand Down
2 changes: 1 addition & 1 deletion apps/desktop/src/main/capability-snapshot.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -61,7 +61,7 @@ export function buildCapabilitySnapshotCollection(input: {
feature: {
state: 'partial',
source: 'runtime',
reason: 'Daily Review 已聚合本地会话 / 工具 / 模型活动;当前不包含屏幕与应用级录制',
reason: 'Daily Review 已聚合本地任务 / 工具 / 模型活动;当前不包含屏幕与应用级录制',
},
requiredPermissions: [
{ id: 'screen_recording', required: false, status: permissions.screen_recording.status },
Expand Down
17 changes: 12 additions & 5 deletions apps/desktop/src/main/chat-readiness.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -106,6 +106,16 @@ export async function requireReadyConnection(
* so the copy stays close to its existing semantics (PR110a refactor
* is behavior-preserving — only the judgment moved to core).
*/
/**
* Two paths reach `fake_backend`: the reason table below, and the header check
* in `assertSessionCanSend`, which never gets far enough to look a connection
* up. They are the same sentence to the user, so they are the same string here
* — the rename that moved 会话 to 任务 had to be applied twice, which is what a
* second copy costs.
*/
const FAKE_BACKEND_MESSAGE =
'当前任务来自旧的本地模拟连接,不能直接发送。请到 设置 · 模型 添加真实模型后新建任务。';

function messageForReason(
reason: ChatConfigurationReason,
connection: LlmConnection,
Expand All@@ -132,7 +142,7 @@ function messageForReason(
return `模型 "${model}" 不能用于聊天。请到 设置 · 模型 选择支持聊天的模型。`;
}
case 'fake_backend':
return '当前会话来自旧的本地模拟连接,不能直接发送。请到 设置 · 模型 添加真实模型后新建会话。';
return FAKE_BACKEND_MESSAGE;
case 'missing_default_connection':
case 'connection_missing':
// These reasons are handled before we reach isConnectionReady,
Expand All@@ -146,10 +156,7 @@ export async function assertSessionCanSend(
deps: ReadyConnectionDeps,
): Promise<void> {
if (header.backend === 'fake') {
throw chatConfigurationError(
'当前会话来自旧的本地模拟连接,不能直接发送。请到 设置 · 模型 添加真实模型后新建会话。',
'fake_backend',
);
throw chatConfigurationError(FAKE_BACKEND_MESSAGE, 'fake_backend');
}
await requireReadyConnection(header.llmConnectionSlug, deps, header.model);
}
Expand Down
2 changes: 1 addition & 1 deletion apps/desktop/src/main/computer-use/status-item.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -114,7 +114,7 @@ const COPY: UiCatalog<StatusItemCopy> = {
zh: {
stopUsing: (appName) => `停止操作 ${appName}`,
stopUnnamed: '停止 Computer Use',
empty: '没有正在进行的会话',
empty: '没有正在进行的任务',
},
en: {
stopUsing: (appName) => `Stop Using ${appName}`,
Expand Down
2 changes: 1 addition & 1 deletion apps/desktop/src/main/e2e-fixture/scenarios-sessions.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -17,7 +17,7 @@ export function longSidebarSessions(
return {
header: header({
id: sessionId,
name: `会话 ${suffix}`,
name: `任务 ${suffix}`,
connection: 'zai-live',
model: 'glm-5.1',
now,
Expand Down
2 changes: 1 addition & 1 deletion apps/desktop/src/main/e2e-fixture/scenarios-settings.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -183,7 +183,7 @@ export async function writeScheduledTasks(workspaceRoot: string, now: number): P
for (const [index, title] of [
'每日站会前汇总阻塞项',
'每月依赖许可证审计',
'季度收尾清点未归档会话',
'季度收尾清点未归档任务',
'发布前跑一轮回归',
].entries()) {
await create(
Expand Down
2 changes: 1 addition & 1 deletion apps/desktop/src/main/notifications-policy.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -58,7 +58,7 @@ export interface RunNotificationCopy {
*/
function runNotificationCopy(kind: RunNotificationKind): RunNotificationCopy {
if (kind === 'errored') {
return { title: '对话出错', body: '本轮回答未能完成,点击查看详情。' };
return { title: '任务出错', body: '本轮回答未能完成,点击查看详情。' };
}
return { title: '回答已生成', body: 'Maka 已完成本轮回答,点击查看。' };
}
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -68,6 +68,13 @@ export function registerRuntimeHostExternalSessionsIpc(
error.operation === 'external-session.import' &&
error.code === 'commit_outcome_unknown'
) {
// "Unknown" means the task may well be in the catalog, so tell the
// shell to read it again. Without this, the only trace of a maybe-
// committed import is the banner on the page, and the page is gone the
// moment the user leaves Settings -- which is exactly when they come
// back and import the same conversation a second time. No id: the
// whole point is that we do not know which task, if any, landed.
deps.emitSessionsChanged('created');
return {
ok: false,
reason: 'commit_outcome_unknown',
Expand Down
2 changes: 1 addition & 1 deletion apps/desktop/src/main/search/thread-search.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -202,7 +202,7 @@ export async function runThreadSearch(
results.push({
source: THREAD_SOURCE,
title: session.name,
summary: '会话标题',
summary: '任务标题',
snippet,
target: {
kind: 'thread',
Expand Down
4 changes: 2 additions & 2 deletions apps/desktop/src/preload/preload.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -421,7 +421,7 @@ function executeWebSearchQuery(input: {
return Promise.resolve({
ok: false,
reason: 'unsupported_provider',
message: '原生联网搜索由对话中的主模型请求执行,不支持从设置页单独调用。',
message: '原生联网搜索由任务中的主模型请求执行,不支持从设置页单独调用。',
});
}
const query = normalizeWebSearchQuery(input.query);
Expand All@@ -448,7 +448,7 @@ function executeWebSearchTest(input: {
return Promise.resolve({
ok: false,
reason: 'unsupported_provider',
message: '原生联网搜索由对话中的主模型请求执行,不需要单独测试搜索凭据。',
message: '原生联网搜索由任务中的主模型请求执行,不需要单独测试搜索凭据。',
});
}
const apiKey = webSearchCredentialOverride(input.apiKey);
Expand Down
4 changes: 2 additions & 2 deletions apps/desktop/src/renderer/agent-graph-panel.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -53,7 +53,7 @@ export function getAgentGraphPanelCopy(locale: UiLocale): GraphPanelCopy {
stopping: '停止中…',
stopFailed: '停止 Graph 失败,请重试。',
loadFailed: 'Graph 状态刷新失败。',
openSession: '打开子会话',
openSession: '打开子任务',
operators: 'Operators',
selectedResults: '已选择结果',
noOperators: '等待主 Agent 创建 operator…',
Expand DownExpand Up@@ -96,7 +96,7 @@ export function getAgentGraphPanelCopy(locale: UiLocale): GraphPanelCopy {
stopping: 'Stopping…',
stopFailed: 'Could not stop the graph. Try again.',
loadFailed: 'Could not refresh graph state.',
openSession: 'Open child session',
openSession: 'Open child task',
operators: 'Operators',
selectedResults: 'Selected results',
noOperators: 'Waiting for the main agent to create an operator…',
Expand Down
2 changes: 1 addition & 1 deletion apps/desktop/src/renderer/app-shell-chat-actions.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -386,7 +386,7 @@ export function createAppShellChatActions(deps: {
showSkillInvocationFeedback(uiLocale, toastApi, sendResult.skillInvocation);
}
if (newChatOwner && isNewChatSendSurfaceActive(newChatOwner)) {
setNavSelection({ section: 'sessions', filter: 'chats' });
setNavSelection({ section: 'sessions' });
setActiveId(session.id);
showOptimisticUserMessage(
session.id,
Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
1b24718
fix(ui): give the task rail's status dot one meaning
Astro-Han Aug 15, 2026
850c34c
refactor(ui): rebuild the task row on two signal slots
Astro-Han Aug 15, 2026
d8b93fe
refactor(ui): give the rail one top region and one grouping switch
Astro-Han Aug 15, 2026
53bd296
feat(desktop): move task import out of the rail into settings
Astro-Han Aug 15, 2026
ed2b6d0
refactor: name the thing a user works on 任务 / task everywhere
Astro-Han Aug 15, 2026
4bfc99d
test(ui): make the narrow-rail story actually narrow
Astro-Han Aug 15, 2026
10ca8e5
fix(ui): keep the rail's 任务 row as the way back to the list
Astro-Han Aug 15, 2026
09b1f90
fix(ui): route task status through the shared semantic layer
Astro-Han Aug 15, 2026
b90b60a
fix(ui): keep the task row's facts when it collapses to two slots
Astro-Han Aug 15, 2026
550f954
fix(desktop): refresh the catalog when an import's outcome is unknown
Astro-Han Aug 15, 2026
e275726
fix(desktop): re-point the structural gates the surface move broke
Astro-Han Aug 15, 2026
a17a01a
refactor: finish the 任务 rename where the rule missed
Astro-Han Aug 15, 2026
971bef3
fix(ui): route the task row's last two dots through the shared vocabu…
Astro-Han Aug 15, 2026
60d2631
docs: say what each session-status-presentation file actually owns
Astro-Han Aug 15, 2026
74b2e80
test: make the restored-task and import fixtures answer like their so…
Astro-Han Aug 15, 2026
f531b98
fix(desktop): tidy the two places the 任务 rename passed through twice
Astro-Han Aug 15, 2026
aea0707
chore: regenerate the Astryx surface inventory
Astro-Han Aug 15, 2026
4b17102
test(ui): show every task status in the story that exists to show them
Astro-Han Aug 15, 2026
642d524
refactor(ui): drop the rail's 任务 row
Astro-Han Aug 15, 2026
f2dfdda
fix(runtime-host): coalesce a repeat import while the first is still …
Astro-Han Aug 15, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion apps/desktop/e2e/composer-skill-invocation.spec.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -30,7 +30,7 @@ test('staged Skills come back as chips after leaving and returning', async ({
await expect(workspaceChip).toContainText('Workspace Only');

await page.getByRole('button', { name: '展开侧边栏' }).click();
const sidebar = page.getByRole('navigation', { name: '对话列表' });
const sidebar = page.getByRole('navigation', { name: '任务列表' });
await sidebar.getByRole('button', { name: '新任务', exact: true }).click();
await expect(composer).toHaveText('');

Expand Down
8 changes: 4 additions & 4 deletions apps/desktop/e2e/session-workbar.spec.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -8,7 +8,7 @@ async function openGitChanges(page: Page) {
await composer.fill('create review session');
await composer.press('Enter');
await expect(page.getByText(/Fake backend received: create review session/)).toBeVisible();
await page.getByRole('button', { name: '展开会话工作栏' }).click();
await page.getByRole('button', { name: '展开任务工作栏' }).click();
await expect(page.getByRole('list', { name: '打开工具' })).toBeVisible();
await page.getByRole('button', { name: /变更.*查看当前 Git 工作区变化/ }).click();
return page.getByRole('region', { name: 'Git 变更' });
Expand All@@ -20,14 +20,14 @@ test('titlebar workbar action restores an existing tool instead of the picker',
const page = gitReviewWindow.page;
const workspaceActions = page.getByRole('toolbar', { name: '工作区辅助操作' });
const panel = await openGitChanges(page);
await expect(workspaceActions.getByRole('button', { name: '收起会话工作栏' })).toBeVisible();
await expect(workspaceActions.getByRole('button', { name: '收起任务工作栏' })).toBeVisible();
await expect(workspaceActions.getByRole('button', { name: '打开工作栏工具' })).toHaveCount(0);
await page.getByRole('button', { name: '打开工作栏标签' }).click();
const picker = page.getByRole('list', { name: '打开工具' });
await expect(picker).toBeVisible();

await workspaceActions.getByRole('button', { name: '收起会话工作栏' }).click();
await workspaceActions.getByRole('button', { name: '展开会话工作栏' }).click();
await workspaceActions.getByRole('button', { name: '收起任务工作栏' }).click();
await workspaceActions.getByRole('button', { name: '展开任务工作栏' }).click();

await expect(panel).toBeVisible();
await expect(picker).not.toBeVisible();
Expand Down
4 changes: 2 additions & 2 deletions apps/desktop/e2e/settings.spec.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -8,8 +8,8 @@ test('opening settings commits an active titlebar rename', async ({ window: page
const identity = page.locator('[data-maka-contract="titlebar-identity"]');
await expect(identity).toBeVisible();
await page.getByRole('button', { name: '展开侧边栏' }).click();
await identity.getByRole('button', { name: /重命名对话/ }).click();
await page.getByRole('textbox', { name: '重命名对话' }).fill('renamed before settings');
await identity.getByRole('button', { name: /重命名任务/ }).click();
await page.getByRole('textbox', { name: '重命名任务' }).fill('renamed before settings');

// Programmatic activation preserves input focus, matching the macOS
// application-menu command that opens Settings before Chromium can blur it.
Expand Down
2 changes: 1 addition & 1 deletion apps/desktop/e2e/sidebar-project-row.spec.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -10,7 +10,7 @@ test('project navigation and actions remain adjacent keyboard controls', async (
await page.keyboard.press('Escape');
await expect(page.locator('[data-maka-contract="search-modal"]')).not.toBeVisible();

const sidebar = page.getByRole('navigation', { name: '对话列表' });
const sidebar = page.getByRole('navigation', { name: '任务列表' });
await sidebar.getByRole('radio', { name: '按项目', exact: true }).click();

const projectRow = sidebar.locator(
Expand Down
15 changes: 12 additions & 3 deletions apps/desktop/e2e/streaming-remount.spec.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -18,11 +18,20 @@ test('remounting a live surface leaves accumulated output settled', async ({
const liveBubble = page.locator('.maka-bubble-streaming');
await expect(liveBubble).toContainText(accumulatedOutput);

const sidebar = page.getByRole('navigation', { name: '对话列表' });
const sidebar = page.getByRole('navigation', { name: '任务列表' });
await sidebar.getByRole('button', { name: '扩展' }).click();
await expect(page.locator('[data-module="skills"]')).toBeVisible();
await expect(liveBubble).toHaveCount(0);
await sidebar.getByRole('button', { name: '会话', exact: true }).click();
// Back the way the product actually offers: the rail is collapsed here, so
// the task rows are not rendered and there is no 任务 row to press (#2984).
// Widening it is the titlebar's job, and the task left behind is still
// `activeId`, so it comes back marked and one click away.
await page.getByRole('button', { name: '展开侧边栏' }).click();
const currentTaskRow = sidebar.locator(
'[data-maka-contract="session-row"] [aria-current="page"]',
);
await expect(currentTaskRow).toHaveCount(1);
await currentTaskRow.click();
await expect(liveBubble).toHaveCount(1);
await expect(liveBubble).toContainText(accumulatedOutput);

Expand DownExpand Up@@ -75,7 +84,7 @@ test('returning to a live conversation settles output accumulated while away', a
const liveBubble = page.locator('.maka-bubble-streaming');
await expect(liveBubble).toContainText(accumulatedOutput);

const sidebar = page.getByRole('navigation', { name: '对话列表' });
const sidebar = page.getByRole('navigation', { name: '任务列表' });
await page.getByRole('button', { name: '展开侧边栏' }).click();
await expect(page.locator('[data-agents-page]')).toHaveAttribute(
'data-sidebar-state',
Expand Down
16 changes: 13 additions & 3 deletions apps/desktop/src/main/__tests__/app-shell-session-purge.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -11,7 +11,7 @@ function summary(id: string, overrides: Partial<SessionSummary> = {}): SessionSu
isArchived: true,
labels: [],
hasUnread: false,
status: 'done',
status: 'archived',
Comment thread
coderabbitai[bot] marked this conversation as resolved.
backend: 'fake',
llmConnectionSlug: 'test',
connectionLocked: true,
Expand All@@ -21,6 +21,16 @@ function summary(id: string, overrides: Partial<SessionSummary> = {}): SessionSu
};
}

/**
* A task that left the archive. Both fields move together because that is what
* `SessionStore.unarchive` writes; flipping only `isArchived` would build a row
* the store cannot produce, and the sweep would then be tested against a state
* it will never meet.
*/
function restored(id: string): SessionSummary {
return summary(id, { isArchived: false, status: 'active' });
}

type SweepHarness = {
removed: string[];
cleared: string[];
Expand DownExpand Up@@ -128,7 +138,7 @@ describe('purgeSessions', () => {
// dialog was up has left it, and a sweep that deleted it anyway would be
// acting outside what was agreed to.
const h = harness();
const sessions = [summary('kept', { isArchived: false }), summary('doomed')];
const sessions = [restored('kept'), summary('doomed')];
const restore = installWindow(h);
const actions = createActions({ harness: h, sessions, activeIdRef: { current: undefined } });

Expand All@@ -148,7 +158,7 @@ describe('purgeSessions', () => {
const sessions = [summary('first'), summary('second')];
const restore = installWindow(h, {
onRemove: (id) => {
if (id === 'first') sessions[1] = summary('second', { isArchived: false });
if (id === 'first') sessions[1] = restored('second');
},
});
const actions = createActions({ harness: h, sessions, activeIdRef: { current: undefined } });
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -167,7 +167,7 @@ describe('AppShell session settings actions', () => {

assert.deepEqual(harness.successes, [
{
title: '已切换当前会话模型',
title: '已切换当前任务模型',
description: 'claude-haiku → claude-opus',
},
]);
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -42,7 +42,7 @@ describe('bot incoming new-session cwd', () => {
} as unknown as BotIncomingMessage);

assert.deepEqual(createInput, {
name: 'Telegram 对话',
name: 'Telegram 任务',
labels: ['bot', 'telegram'],
});
});
Expand Down

This file was deleted.

Original file line numberDiff line numberDiff line change
Expand Up@@ -59,7 +59,7 @@ test('forwards bounded external Session requests and publishes imported Sessions
assert.deepEqual(events, [{ reason: 'created', sessionId: 'imported-1' }]);
});

test('preserves commit uncertainty as a structured non-retryable IPC result', async () => {
test('an uncertain commit still asks the shell to re-read the catalog', async () => {
const events: unknown[] = [];
const ipc = ipcHarness();
registerRuntimeHostExternalSessionsIpc(
Expand All@@ -85,7 +85,12 @@ test('preserves commit uncertainty as a structured non-retryable IPC result', as
}),
{ ok: false, reason: 'commit_outcome_unknown' },
);
assert.deepEqual(events, []);
// The task may be in the catalog, so the shell has to look. The import page's
// own banner cannot be the only trace: 导入任务 is a Settings page, and the
// moment the user leaves it the banner is unmounted -- which is exactly when
// they come back and import the same conversation again. No id, because not
// knowing which task landed is what `commit_outcome_unknown` means.
assert.deepEqual(events, [{ reason: 'created', sessionId: undefined }]);
});

test('rejects malformed renderer requests before they reach the Host client', async () => {
Expand Down
2 changes: 1 addition & 1 deletion apps/desktop/src/main/__tests__/thread-search.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -215,7 +215,7 @@ describe('runThreadSearch', () => {
await runThreadSearch({ source: 'thread', query: 'roadmap', limit: 5 }, makeDeps(entries)),
)[0]!;
assert.deepEqual(titleHit.target, { kind: 'thread', sessionId: 's1' });
assert.equal(titleHit.summary, '会话标题');
assert.equal(titleHit.summary, '任务标题');
assert.equal(titleHit.url, undefined);
assert.match(titleHit.snippet ?? '', /\[redacted\]/);
assert.equal(titleHit.snippet?.includes('sk-ant-test-secret-token-12345'), false);
Expand Down
10 changes: 5 additions & 5 deletions apps/desktop/src/main/bot-incoming-main.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -183,15 +183,15 @@ export function createBotIncomingMainService(deps: BotIncomingMainServiceDeps):
noticeTtlMs: number,
): Promise<string | undefined> {
if (botConversationSessions.size >= BOT_CONVERSATION_SESSION_LIMIT) {
await sendTransientBotNotice(message, 'Maka 当前机器人会话数量已达上限,请重置或清理旧会话后再试。', noticeTtlMs);
await sendTransientBotNotice(message, 'Maka 当前机器人任务数量已达上限,请重置或清理旧任务后再试。', noticeTtlMs);
return undefined;
}
if (!consumeBotConversationToken(conversationKey)) {
await sendTransientBotNotice(message, 'Maka 收到的机器人消息过于频繁,请稍后再试。', noticeTtlMs);
return undefined;
}
const sessionId = await deps.sessions.createSession({
name: `${botDisplayLabel(message.platform)} 对话`,
name: `${botDisplayLabel(message.platform)} 任务`,
labels: ['bot', message.platform],
});
botConversationSessions.set(conversationKey, sessionId);
Expand DownExpand Up@@ -240,8 +240,8 @@ export function createBotIncomingMainService(deps: BotIncomingMainServiceDeps):
ephemeralTtlMs: SYSTEM_NOTICE_TTL_MS,
};
const ack = had
? '会话已重置,下一条消息会开新对话。'
: '当前没有进行中的对话;下一条消息会开新对话。';
? '任务已重置,下一条消息会开新任务。'
: '当前没有进行中的任务;下一条消息会开新任务。';
await deps.botRegistry.sendMessage(message.platform, message.chatId, ack, replyOptions).catch(() => null);
return;
}
Expand DownExpand Up@@ -396,7 +396,7 @@ export function createBotIncomingMainService(deps: BotIncomingMainServiceDeps):
if ((await deps.sessions.prepareSession(sessionId)) === 'ready') return true;
await sendTransientBotNotice(
message,
'Maka 已拒绝这条机器人消息:绑定会话当前不是只读探索模式,请先在桌面端切回 explore 后再试。',
'Maka 已拒绝这条机器人消息:绑定任务当前不是只读探索模式,请先在桌面端切回 explore 后再试。',
noticeTtlMs,
);
return false;
Expand Down
2 changes: 1 addition & 1 deletion apps/desktop/src/main/capability-snapshot.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -61,7 +61,7 @@ export function buildCapabilitySnapshotCollection(input: {
feature: {
state: 'partial',
source: 'runtime',
reason: 'Daily Review 已聚合本地会话 / 工具 / 模型活动;当前不包含屏幕与应用级录制',
reason: 'Daily Review 已聚合本地任务 / 工具 / 模型活动;当前不包含屏幕与应用级录制',
},
requiredPermissions: [
{ id: 'screen_recording', required: false, status: permissions.screen_recording.status },
Expand Down
17 changes: 12 additions & 5 deletions apps/desktop/src/main/chat-readiness.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -106,6 +106,16 @@ export async function requireReadyConnection(
* so the copy stays close to its existing semantics (PR110a refactor
* is behavior-preserving — only the judgment moved to core).
*/
/**
* Two paths reach `fake_backend`: the reason table below, and the header check
* in `assertSessionCanSend`, which never gets far enough to look a connection
* up. They are the same sentence to the user, so they are the same string here
* — the rename that moved 会话 to 任务 had to be applied twice, which is what a
* second copy costs.
*/
const FAKE_BACKEND_MESSAGE =
'当前任务来自旧的本地模拟连接,不能直接发送。请到 设置 · 模型 添加真实模型后新建任务。';

function messageForReason(
reason: ChatConfigurationReason,
connection: LlmConnection,
Expand All@@ -132,7 +142,7 @@ function messageForReason(
return `模型 "${model}" 不能用于聊天。请到 设置 · 模型 选择支持聊天的模型。`;
}
case 'fake_backend':
return '当前会话来自旧的本地模拟连接,不能直接发送。请到 设置 · 模型 添加真实模型后新建会话。';
return FAKE_BACKEND_MESSAGE;
case 'missing_default_connection':
case 'connection_missing':
// These reasons are handled before we reach isConnectionReady,
Expand All@@ -146,10 +156,7 @@ export async function assertSessionCanSend(
deps: ReadyConnectionDeps,
): Promise<void> {
if (header.backend === 'fake') {
throw chatConfigurationError(
'当前会话来自旧的本地模拟连接,不能直接发送。请到 设置 · 模型 添加真实模型后新建会话。',
'fake_backend',
);
throw chatConfigurationError(FAKE_BACKEND_MESSAGE, 'fake_backend');
}
await requireReadyConnection(header.llmConnectionSlug, deps, header.model);
}
Expand Down
2 changes: 1 addition & 1 deletion apps/desktop/src/main/computer-use/status-item.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -114,7 +114,7 @@ const COPY: UiCatalog<StatusItemCopy> = {
zh: {
stopUsing: (appName) => `停止操作 ${appName}`,
stopUnnamed: '停止 Computer Use',
empty: '没有正在进行的会话',
empty: '没有正在进行的任务',
},
en: {
stopUsing: (appName) => `Stop Using ${appName}`,
Expand Down
2 changes: 1 addition & 1 deletion apps/desktop/src/main/e2e-fixture/scenarios-sessions.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -17,7 +17,7 @@ export function longSidebarSessions(
return {
header: header({
id: sessionId,
name: `会话 ${suffix}`,
name: `任务 ${suffix}`,
connection: 'zai-live',
model: 'glm-5.1',
now,
Expand Down
2 changes: 1 addition & 1 deletion apps/desktop/src/main/e2e-fixture/scenarios-settings.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -183,7 +183,7 @@ export async function writeScheduledTasks(workspaceRoot: string, now: number): P
for (const [index, title] of [
'每日站会前汇总阻塞项',
'每月依赖许可证审计',
'季度收尾清点未归档会话',
'季度收尾清点未归档任务',
'发布前跑一轮回归',
].entries()) {
await create(
Expand Down
2 changes: 1 addition & 1 deletion apps/desktop/src/main/notifications-policy.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -58,7 +58,7 @@ export interface RunNotificationCopy {
*/
function runNotificationCopy(kind: RunNotificationKind): RunNotificationCopy {
if (kind === 'errored') {
return { title: '对话出错', body: '本轮回答未能完成,点击查看详情。' };
return { title: '任务出错', body: '本轮回答未能完成,点击查看详情。' };
}
return { title: '回答已生成', body: 'Maka 已完成本轮回答,点击查看。' };
}
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -68,6 +68,13 @@ export function registerRuntimeHostExternalSessionsIpc(
error.operation === 'external-session.import' &&
error.code === 'commit_outcome_unknown'
) {
// "Unknown" means the task may well be in the catalog, so tell the
// shell to read it again. Without this, the only trace of a maybe-
// committed import is the banner on the page, and the page is gone the
// moment the user leaves Settings -- which is exactly when they come
// back and import the same conversation a second time. No id: the
// whole point is that we do not know which task, if any, landed.
deps.emitSessionsChanged('created');
return {
ok: false,
reason: 'commit_outcome_unknown',
Expand Down
2 changes: 1 addition & 1 deletion apps/desktop/src/main/search/thread-search.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -202,7 +202,7 @@ export async function runThreadSearch(
results.push({
source: THREAD_SOURCE,
title: session.name,
summary: '会话标题',
summary: '任务标题',
snippet,
target: {
kind: 'thread',
Expand Down
4 changes: 2 additions & 2 deletions apps/desktop/src/preload/preload.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -421,7 +421,7 @@ function executeWebSearchQuery(input: {
return Promise.resolve({
ok: false,
reason: 'unsupported_provider',
message: '原生联网搜索由对话中的主模型请求执行,不支持从设置页单独调用。',
message: '原生联网搜索由任务中的主模型请求执行,不支持从设置页单独调用。',
});
}
const query = normalizeWebSearchQuery(input.query);
Expand All@@ -448,7 +448,7 @@ function executeWebSearchTest(input: {
return Promise.resolve({
ok: false,
reason: 'unsupported_provider',
message: '原生联网搜索由对话中的主模型请求执行,不需要单独测试搜索凭据。',
message: '原生联网搜索由任务中的主模型请求执行,不需要单独测试搜索凭据。',
});
}
const apiKey = webSearchCredentialOverride(input.apiKey);
Expand Down
4 changes: 2 additions & 2 deletions apps/desktop/src/renderer/agent-graph-panel.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -53,7 +53,7 @@ export function getAgentGraphPanelCopy(locale: UiLocale): GraphPanelCopy {
stopping: '停止中…',
stopFailed: '停止 Graph 失败,请重试。',
loadFailed: 'Graph 状态刷新失败。',
openSession: '打开子会话',
openSession: '打开子任务',
operators: 'Operators',
selectedResults: '已选择结果',
noOperators: '等待主 Agent 创建 operator…',
Expand DownExpand Up@@ -96,7 +96,7 @@ export function getAgentGraphPanelCopy(locale: UiLocale): GraphPanelCopy {
stopping: 'Stopping…',
stopFailed: 'Could not stop the graph. Try again.',
loadFailed: 'Could not refresh graph state.',
openSession: 'Open child session',
openSession: 'Open child task',
operators: 'Operators',
selectedResults: 'Selected results',
noOperators: 'Waiting for the main agent to create an operator…',
Expand Down
2 changes: 1 addition & 1 deletion apps/desktop/src/renderer/app-shell-chat-actions.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -386,7 +386,7 @@ export function createAppShellChatActions(deps: {
showSkillInvocationFeedback(uiLocale, toastApi, sendResult.skillInvocation);
}
if (newChatOwner && isNewChatSendSurfaceActive(newChatOwner)) {
setNavSelection({ section: 'sessions', filter: 'chats' });
setNavSelection({ section: 'sessions' });
setActiveId(session.id);
showOptimisticUserMessage(
session.id,
Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
1b24718
fix(ui): give the task rail's status dot one meaning
Astro-Han Aug 15, 2026
850c34c
refactor(ui): rebuild the task row on two signal slots
Astro-Han Aug 15, 2026
d8b93fe
refactor(ui): give the rail one top region and one grouping switch
Astro-Han Aug 15, 2026
53bd296
feat(desktop): move task import out of the rail into settings
Astro-Han Aug 15, 2026
ed2b6d0
refactor: name the thing a user works on 任务 / task everywhere
Astro-Han Aug 15, 2026
4bfc99d
test(ui): make the narrow-rail story actually narrow
Astro-Han Aug 15, 2026
10ca8e5
fix(ui): keep the rail's 任务 row as the way back to the list
Astro-Han Aug 15, 2026
09b1f90
fix(ui): route task status through the shared semantic layer
Astro-Han Aug 15, 2026
b90b60a
fix(ui): keep the task row's facts when it collapses to two slots
Astro-Han Aug 15, 2026
550f954
fix(desktop): refresh the catalog when an import's outcome is unknown
Astro-Han Aug 15, 2026
e275726
fix(desktop): re-point the structural gates the surface move broke
Astro-Han Aug 15, 2026
a17a01a
refactor: finish the 任务 rename where the rule missed
Astro-Han Aug 15, 2026
971bef3
fix(ui): route the task row's last two dots through the shared vocabu…
Astro-Han Aug 15, 2026
60d2631
docs: say what each session-status-presentation file actually owns
Astro-Han Aug 15, 2026
74b2e80
test: make the restored-task and import fixtures answer like their so…
Astro-Han Aug 15, 2026
f531b98
fix(desktop): tidy the two places the 任务 rename passed through twice
Astro-Han Aug 15, 2026
aea0707
chore: regenerate the Astryx surface inventory
Astro-Han Aug 15, 2026
4b17102
test(ui): show every task status in the story that exists to show them
Astro-Han Aug 15, 2026
642d524
refactor(ui): drop the rail's 任务 row
Astro-Han Aug 15, 2026
f2dfdda
fix(runtime-host): coalesce a repeat import while the first is still …
Astro-Han Aug 15, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion apps/desktop/e2e/composer-skill-invocation.spec.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -30,7 +30,7 @@ test('staged Skills come back as chips after leaving and returning', async ({
await expect(workspaceChip).toContainText('Workspace Only');

await page.getByRole('button', { name: '展开侧边栏' }).click();
const sidebar = page.getByRole('navigation', { name: '对话列表' });
const sidebar = page.getByRole('navigation', { name: '任务列表' });
await sidebar.getByRole('button', { name: '新任务', exact: true }).click();
await expect(composer).toHaveText('');

Expand Down
8 changes: 4 additions & 4 deletions apps/desktop/e2e/session-workbar.spec.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -8,7 +8,7 @@ async function openGitChanges(page: Page) {
await composer.fill('create review session');
await composer.press('Enter');
await expect(page.getByText(/Fake backend received: create review session/)).toBeVisible();
await page.getByRole('button', { name: '展开会话工作栏' }).click();
await page.getByRole('button', { name: '展开任务工作栏' }).click();
await expect(page.getByRole('list', { name: '打开工具' })).toBeVisible();
await page.getByRole('button', { name: /变更.*查看当前 Git 工作区变化/ }).click();
return page.getByRole('region', { name: 'Git 变更' });
Expand All@@ -20,14 +20,14 @@ test('titlebar workbar action restores an existing tool instead of the picker',
const page = gitReviewWindow.page;
const workspaceActions = page.getByRole('toolbar', { name: '工作区辅助操作' });
const panel = await openGitChanges(page);
await expect(workspaceActions.getByRole('button', { name: '收起会话工作栏' })).toBeVisible();
await expect(workspaceActions.getByRole('button', { name: '收起任务工作栏' })).toBeVisible();
await expect(workspaceActions.getByRole('button', { name: '打开工作栏工具' })).toHaveCount(0);
await page.getByRole('button', { name: '打开工作栏标签' }).click();
const picker = page.getByRole('list', { name: '打开工具' });
await expect(picker).toBeVisible();

await workspaceActions.getByRole('button', { name: '收起会话工作栏' }).click();
await workspaceActions.getByRole('button', { name: '展开会话工作栏' }).click();
await workspaceActions.getByRole('button', { name: '收起任务工作栏' }).click();
await workspaceActions.getByRole('button', { name: '展开任务工作栏' }).click();

await expect(panel).toBeVisible();
await expect(picker).not.toBeVisible();
Expand Down
4 changes: 2 additions & 2 deletions apps/desktop/e2e/settings.spec.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -8,8 +8,8 @@ test('opening settings commits an active titlebar rename', async ({ window: page
const identity = page.locator('[data-maka-contract="titlebar-identity"]');
await expect(identity).toBeVisible();
await page.getByRole('button', { name: '展开侧边栏' }).click();
await identity.getByRole('button', { name: /重命名对话/ }).click();
await page.getByRole('textbox', { name: '重命名对话' }).fill('renamed before settings');
await identity.getByRole('button', { name: /重命名任务/ }).click();
await page.getByRole('textbox', { name: '重命名任务' }).fill('renamed before settings');

// Programmatic activation preserves input focus, matching the macOS
// application-menu command that opens Settings before Chromium can blur it.
Expand Down
2 changes: 1 addition & 1 deletion apps/desktop/e2e/sidebar-project-row.spec.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -10,7 +10,7 @@ test('project navigation and actions remain adjacent keyboard controls', async (
await page.keyboard.press('Escape');
await expect(page.locator('[data-maka-contract="search-modal"]')).not.toBeVisible();

const sidebar = page.getByRole('navigation', { name: '对话列表' });
const sidebar = page.getByRole('navigation', { name: '任务列表' });
await sidebar.getByRole('radio', { name: '按项目', exact: true }).click();

const projectRow = sidebar.locator(
Expand Down
15 changes: 12 additions & 3 deletions apps/desktop/e2e/streaming-remount.spec.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -18,11 +18,20 @@ test('remounting a live surface leaves accumulated output settled', async ({
const liveBubble = page.locator('.maka-bubble-streaming');
await expect(liveBubble).toContainText(accumulatedOutput);

const sidebar = page.getByRole('navigation', { name: '对话列表' });
const sidebar = page.getByRole('navigation', { name: '任务列表' });
await sidebar.getByRole('button', { name: '扩展' }).click();
await expect(page.locator('[data-module="skills"]')).toBeVisible();
await expect(liveBubble).toHaveCount(0);
await sidebar.getByRole('button', { name: '会话', exact: true }).click();
// Back the way the product actually offers: the rail is collapsed here, so
// the task rows are not rendered and there is no 任务 row to press (#2984).
// Widening it is the titlebar's job, and the task left behind is still
// `activeId`, so it comes back marked and one click away.
await page.getByRole('button', { name: '展开侧边栏' }).click();
const currentTaskRow = sidebar.locator(
'[data-maka-contract="session-row"] [aria-current="page"]',
);
await expect(currentTaskRow).toHaveCount(1);
await currentTaskRow.click();
await expect(liveBubble).toHaveCount(1);
await expect(liveBubble).toContainText(accumulatedOutput);

Expand DownExpand Up@@ -75,7 +84,7 @@ test('returning to a live conversation settles output accumulated while away', a
const liveBubble = page.locator('.maka-bubble-streaming');
await expect(liveBubble).toContainText(accumulatedOutput);

const sidebar = page.getByRole('navigation', { name: '对话列表' });
const sidebar = page.getByRole('navigation', { name: '任务列表' });
await page.getByRole('button', { name: '展开侧边栏' }).click();
await expect(page.locator('[data-agents-page]')).toHaveAttribute(
'data-sidebar-state',
Expand Down
16 changes: 13 additions & 3 deletions apps/desktop/src/main/__tests__/app-shell-session-purge.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -11,7 +11,7 @@ function summary(id: string, overrides: Partial<SessionSummary> = {}): SessionSu
isArchived: true,
labels: [],
hasUnread: false,
status: 'done',
status: 'archived',
Comment thread
coderabbitai[bot] marked this conversation as resolved.
backend: 'fake',
llmConnectionSlug: 'test',
connectionLocked: true,
Expand All@@ -21,6 +21,16 @@ function summary(id: string, overrides: Partial<SessionSummary> = {}): SessionSu
};
}

/**
* A task that left the archive. Both fields move together because that is what
* `SessionStore.unarchive` writes; flipping only `isArchived` would build a row
* the store cannot produce, and the sweep would then be tested against a state
* it will never meet.
*/
function restored(id: string): SessionSummary {
return summary(id, { isArchived: false, status: 'active' });
}

type SweepHarness = {
removed: string[];
cleared: string[];
Expand DownExpand Up@@ -128,7 +138,7 @@ describe('purgeSessions', () => {
// dialog was up has left it, and a sweep that deleted it anyway would be
// acting outside what was agreed to.
const h = harness();
const sessions = [summary('kept', { isArchived: false }), summary('doomed')];
const sessions = [restored('kept'), summary('doomed')];
const restore = installWindow(h);
const actions = createActions({ harness: h, sessions, activeIdRef: { current: undefined } });

Expand All@@ -148,7 +158,7 @@ describe('purgeSessions', () => {
const sessions = [summary('first'), summary('second')];
const restore = installWindow(h, {
onRemove: (id) => {
if (id === 'first') sessions[1] = summary('second', { isArchived: false });
if (id === 'first') sessions[1] = restored('second');
},
});
const actions = createActions({ harness: h, sessions, activeIdRef: { current: undefined } });
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -167,7 +167,7 @@ describe('AppShell session settings actions', () => {

assert.deepEqual(harness.successes, [
{
title: '已切换当前会话模型',
title: '已切换当前任务模型',
description: 'claude-haiku → claude-opus',
},
]);
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -42,7 +42,7 @@ describe('bot incoming new-session cwd', () => {
} as unknown as BotIncomingMessage);

assert.deepEqual(createInput, {
name: 'Telegram 对话',
name: 'Telegram 任务',
labels: ['bot', 'telegram'],
});
});
Expand Down

This file was deleted.

Original file line numberDiff line numberDiff line change
Expand Up@@ -59,7 +59,7 @@ test('forwards bounded external Session requests and publishes imported Sessions
assert.deepEqual(events, [{ reason: 'created', sessionId: 'imported-1' }]);
});

test('preserves commit uncertainty as a structured non-retryable IPC result', async () => {
test('an uncertain commit still asks the shell to re-read the catalog', async () => {
const events: unknown[] = [];
const ipc = ipcHarness();
registerRuntimeHostExternalSessionsIpc(
Expand All@@ -85,7 +85,12 @@ test('preserves commit uncertainty as a structured non-retryable IPC result', as
}),
{ ok: false, reason: 'commit_outcome_unknown' },
);
assert.deepEqual(events, []);
// The task may be in the catalog, so the shell has to look. The import page's
// own banner cannot be the only trace: 导入任务 is a Settings page, and the
// moment the user leaves it the banner is unmounted -- which is exactly when
// they come back and import the same conversation again. No id, because not
// knowing which task landed is what `commit_outcome_unknown` means.
assert.deepEqual(events, [{ reason: 'created', sessionId: undefined }]);
});

test('rejects malformed renderer requests before they reach the Host client', async () => {
Expand Down
2 changes: 1 addition & 1 deletion apps/desktop/src/main/__tests__/thread-search.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -215,7 +215,7 @@ describe('runThreadSearch', () => {
await runThreadSearch({ source: 'thread', query: 'roadmap', limit: 5 }, makeDeps(entries)),
)[0]!;
assert.deepEqual(titleHit.target, { kind: 'thread', sessionId: 's1' });
assert.equal(titleHit.summary, '会话标题');
assert.equal(titleHit.summary, '任务标题');
assert.equal(titleHit.url, undefined);
assert.match(titleHit.snippet ?? '', /\[redacted\]/);
assert.equal(titleHit.snippet?.includes('sk-ant-test-secret-token-12345'), false);
Expand Down
10 changes: 5 additions & 5 deletions apps/desktop/src/main/bot-incoming-main.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -183,15 +183,15 @@ export function createBotIncomingMainService(deps: BotIncomingMainServiceDeps):
noticeTtlMs: number,
): Promise<string | undefined> {
if (botConversationSessions.size >= BOT_CONVERSATION_SESSION_LIMIT) {
await sendTransientBotNotice(message, 'Maka 当前机器人会话数量已达上限,请重置或清理旧会话后再试。', noticeTtlMs);
await sendTransientBotNotice(message, 'Maka 当前机器人任务数量已达上限,请重置或清理旧任务后再试。', noticeTtlMs);
return undefined;
}
if (!consumeBotConversationToken(conversationKey)) {
await sendTransientBotNotice(message, 'Maka 收到的机器人消息过于频繁,请稍后再试。', noticeTtlMs);
return undefined;
}
const sessionId = await deps.sessions.createSession({
name: `${botDisplayLabel(message.platform)} 对话`,
name: `${botDisplayLabel(message.platform)} 任务`,
labels: ['bot', message.platform],
});
botConversationSessions.set(conversationKey, sessionId);
Expand DownExpand Up@@ -240,8 +240,8 @@ export function createBotIncomingMainService(deps: BotIncomingMainServiceDeps):
ephemeralTtlMs: SYSTEM_NOTICE_TTL_MS,
};
const ack = had
? '会话已重置,下一条消息会开新对话。'
: '当前没有进行中的对话;下一条消息会开新对话。';
? '任务已重置,下一条消息会开新任务。'
: '当前没有进行中的任务;下一条消息会开新任务。';
await deps.botRegistry.sendMessage(message.platform, message.chatId, ack, replyOptions).catch(() => null);
return;
}
Expand DownExpand Up@@ -396,7 +396,7 @@ export function createBotIncomingMainService(deps: BotIncomingMainServiceDeps):
if ((await deps.sessions.prepareSession(sessionId)) === 'ready') return true;
await sendTransientBotNotice(
message,
'Maka 已拒绝这条机器人消息:绑定会话当前不是只读探索模式,请先在桌面端切回 explore 后再试。',
'Maka 已拒绝这条机器人消息:绑定任务当前不是只读探索模式,请先在桌面端切回 explore 后再试。',
noticeTtlMs,
);
return false;
Expand Down
2 changes: 1 addition & 1 deletion apps/desktop/src/main/capability-snapshot.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -61,7 +61,7 @@ export function buildCapabilitySnapshotCollection(input: {
feature: {
state: 'partial',
source: 'runtime',
reason: 'Daily Review 已聚合本地会话 / 工具 / 模型活动;当前不包含屏幕与应用级录制',
reason: 'Daily Review 已聚合本地任务 / 工具 / 模型活动;当前不包含屏幕与应用级录制',
},
requiredPermissions: [
{ id: 'screen_recording', required: false, status: permissions.screen_recording.status },
Expand Down
17 changes: 12 additions & 5 deletions apps/desktop/src/main/chat-readiness.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -106,6 +106,16 @@ export async function requireReadyConnection(
* so the copy stays close to its existing semantics (PR110a refactor
* is behavior-preserving — only the judgment moved to core).
*/
/**
* Two paths reach `fake_backend`: the reason table below, and the header check
* in `assertSessionCanSend`, which never gets far enough to look a connection
* up. They are the same sentence to the user, so they are the same string here
* — the rename that moved 会话 to 任务 had to be applied twice, which is what a
* second copy costs.
*/
const FAKE_BACKEND_MESSAGE =
'当前任务来自旧的本地模拟连接,不能直接发送。请到 设置 · 模型 添加真实模型后新建任务。';

function messageForReason(
reason: ChatConfigurationReason,
connection: LlmConnection,
Expand All@@ -132,7 +142,7 @@ function messageForReason(
return `模型 "${model}" 不能用于聊天。请到 设置 · 模型 选择支持聊天的模型。`;
}
case 'fake_backend':
return '当前会话来自旧的本地模拟连接,不能直接发送。请到 设置 · 模型 添加真实模型后新建会话。';
return FAKE_BACKEND_MESSAGE;
case 'missing_default_connection':
case 'connection_missing':
// These reasons are handled before we reach isConnectionReady,
Expand All@@ -146,10 +156,7 @@ export async function assertSessionCanSend(
deps: ReadyConnectionDeps,
): Promise<void> {
if (header.backend === 'fake') {
throw chatConfigurationError(
'当前会话来自旧的本地模拟连接,不能直接发送。请到 设置 · 模型 添加真实模型后新建会话。',
'fake_backend',
);
throw chatConfigurationError(FAKE_BACKEND_MESSAGE, 'fake_backend');
}
await requireReadyConnection(header.llmConnectionSlug, deps, header.model);
}
Expand Down
2 changes: 1 addition & 1 deletion apps/desktop/src/main/computer-use/status-item.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -114,7 +114,7 @@ const COPY: UiCatalog<StatusItemCopy> = {
zh: {
stopUsing: (appName) => `停止操作 ${appName}`,
stopUnnamed: '停止 Computer Use',
empty: '没有正在进行的会话',
empty: '没有正在进行的任务',
},
en: {
stopUsing: (appName) => `Stop Using ${appName}`,
Expand Down
2 changes: 1 addition & 1 deletion apps/desktop/src/main/e2e-fixture/scenarios-sessions.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -17,7 +17,7 @@ export function longSidebarSessions(
return {
header: header({
id: sessionId,
name: `会话 ${suffix}`,
name: `任务 ${suffix}`,
connection: 'zai-live',
model: 'glm-5.1',
now,
Expand Down
2 changes: 1 addition & 1 deletion apps/desktop/src/main/e2e-fixture/scenarios-settings.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -183,7 +183,7 @@ export async function writeScheduledTasks(workspaceRoot: string, now: number): P
for (const [index, title] of [
'每日站会前汇总阻塞项',
'每月依赖许可证审计',
'季度收尾清点未归档会话',
'季度收尾清点未归档任务',
'发布前跑一轮回归',
].entries()) {
await create(
Expand Down
2 changes: 1 addition & 1 deletion apps/desktop/src/main/notifications-policy.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -58,7 +58,7 @@ export interface RunNotificationCopy {
*/
function runNotificationCopy(kind: RunNotificationKind): RunNotificationCopy {
if (kind === 'errored') {
return { title: '对话出错', body: '本轮回答未能完成,点击查看详情。' };
return { title: '任务出错', body: '本轮回答未能完成,点击查看详情。' };
}
return { title: '回答已生成', body: 'Maka 已完成本轮回答,点击查看。' };
}
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -68,6 +68,13 @@ export function registerRuntimeHostExternalSessionsIpc(
error.operation === 'external-session.import' &&
error.code === 'commit_outcome_unknown'
) {
// "Unknown" means the task may well be in the catalog, so tell the
// shell to read it again. Without this, the only trace of a maybe-
// committed import is the banner on the page, and the page is gone the
// moment the user leaves Settings -- which is exactly when they come
// back and import the same conversation a second time. No id: the
// whole point is that we do not know which task, if any, landed.
deps.emitSessionsChanged('created');
return {
ok: false,
reason: 'commit_outcome_unknown',
Expand Down
2 changes: 1 addition & 1 deletion apps/desktop/src/main/search/thread-search.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -202,7 +202,7 @@ export async function runThreadSearch(
results.push({
source: THREAD_SOURCE,
title: session.name,
summary: '会话标题',
summary: '任务标题',
snippet,
target: {
kind: 'thread',
Expand Down
4 changes: 2 additions & 2 deletions apps/desktop/src/preload/preload.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -421,7 +421,7 @@ function executeWebSearchQuery(input: {
return Promise.resolve({
ok: false,
reason: 'unsupported_provider',
message: '原生联网搜索由对话中的主模型请求执行,不支持从设置页单独调用。',
message: '原生联网搜索由任务中的主模型请求执行,不支持从设置页单独调用。',
});
}
const query = normalizeWebSearchQuery(input.query);
Expand All@@ -448,7 +448,7 @@ function executeWebSearchTest(input: {
return Promise.resolve({
ok: false,
reason: 'unsupported_provider',
message: '原生联网搜索由对话中的主模型请求执行,不需要单独测试搜索凭据。',
message: '原生联网搜索由任务中的主模型请求执行,不需要单独测试搜索凭据。',
});
}
const apiKey = webSearchCredentialOverride(input.apiKey);
Expand Down
4 changes: 2 additions & 2 deletions apps/desktop/src/renderer/agent-graph-panel.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -53,7 +53,7 @@ export function getAgentGraphPanelCopy(locale: UiLocale): GraphPanelCopy {
stopping: '停止中…',
stopFailed: '停止 Graph 失败,请重试。',
loadFailed: 'Graph 状态刷新失败。',
openSession: '打开子会话',
openSession: '打开子任务',
operators: 'Operators',
selectedResults: '已选择结果',
noOperators: '等待主 Agent 创建 operator…',
Expand DownExpand Up@@ -96,7 +96,7 @@ export function getAgentGraphPanelCopy(locale: UiLocale): GraphPanelCopy {
stopping: 'Stopping…',
stopFailed: 'Could not stop the graph. Try again.',
loadFailed: 'Could not refresh graph state.',
openSession: 'Open child session',
openSession: 'Open child task',
operators: 'Operators',
selectedResults: 'Selected results',
noOperators: 'Waiting for the main agent to create an operator…',
Expand Down
2 changes: 1 addition & 1 deletion apps/desktop/src/renderer/app-shell-chat-actions.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -386,7 +386,7 @@ export function createAppShellChatActions(deps: {
showSkillInvocationFeedback(uiLocale, toastApi, sendResult.skillInvocation);
}
if (newChatOwner && isNewChatSendSurfaceActive(newChatOwner)) {
setNavSelection({ section: 'sessions', filter: 'chats' });
setNavSelection({ section: 'sessions' });
setActiveId(session.id);
showOptimisticUserMessage(
session.id,
Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
1b24718
fix(ui): give the task rail's status dot one meaning
Astro-Han Aug 15, 2026
850c34c
refactor(ui): rebuild the task row on two signal slots
Astro-Han Aug 15, 2026
d8b93fe
refactor(ui): give the rail one top region and one grouping switch
Astro-Han Aug 15, 2026
53bd296
feat(desktop): move task import out of the rail into settings
Astro-Han Aug 15, 2026
ed2b6d0
refactor: name the thing a user works on 任务 / task everywhere
Astro-Han Aug 15, 2026
4bfc99d
test(ui): make the narrow-rail story actually narrow
Astro-Han Aug 15, 2026
10ca8e5
fix(ui): keep the rail's 任务 row as the way back to the list
Astro-Han Aug 15, 2026
09b1f90
fix(ui): route task status through the shared semantic layer
Astro-Han Aug 15, 2026
b90b60a
fix(ui): keep the task row's facts when it collapses to two slots
Astro-Han Aug 15, 2026
550f954
fix(desktop): refresh the catalog when an import's outcome is unknown
Astro-Han Aug 15, 2026
e275726
fix(desktop): re-point the structural gates the surface move broke
Astro-Han Aug 15, 2026
a17a01a
refactor: finish the 任务 rename where the rule missed
Astro-Han Aug 15, 2026
971bef3
fix(ui): route the task row's last two dots through the shared vocabu…
Astro-Han Aug 15, 2026
60d2631
docs: say what each session-status-presentation file actually owns
Astro-Han Aug 15, 2026
74b2e80
test: make the restored-task and import fixtures answer like their so…
Astro-Han Aug 15, 2026
f531b98
fix(desktop): tidy the two places the 任务 rename passed through twice
Astro-Han Aug 15, 2026
aea0707
chore: regenerate the Astryx surface inventory
Astro-Han Aug 15, 2026
4b17102
test(ui): show every task status in the story that exists to show them
Astro-Han Aug 15, 2026
642d524
refactor(ui): drop the rail's 任务 row
Astro-Han Aug 15, 2026
f2dfdda
fix(runtime-host): coalesce a repeat import while the first is still …
Astro-Han Aug 15, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion apps/desktop/e2e/composer-skill-invocation.spec.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -30,7 +30,7 @@ test('staged Skills come back as chips after leaving and returning', async ({
await expect(workspaceChip).toContainText('Workspace Only');

await page.getByRole('button', { name: '展开侧边栏' }).click();
const sidebar = page.getByRole('navigation', { name: '对话列表' });
const sidebar = page.getByRole('navigation', { name: '任务列表' });
await sidebar.getByRole('button', { name: '新任务', exact: true }).click();
await expect(composer).toHaveText('');

Expand Down
8 changes: 4 additions & 4 deletions apps/desktop/e2e/session-workbar.spec.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -8,7 +8,7 @@ async function openGitChanges(page: Page) {
await composer.fill('create review session');
await composer.press('Enter');
await expect(page.getByText(/Fake backend received: create review session/)).toBeVisible();
await page.getByRole('button', { name: '展开会话工作栏' }).click();
await page.getByRole('button', { name: '展开任务工作栏' }).click();
await expect(page.getByRole('list', { name: '打开工具' })).toBeVisible();
await page.getByRole('button', { name: /变更.*查看当前 Git 工作区变化/ }).click();
return page.getByRole('region', { name: 'Git 变更' });
Expand All@@ -20,14 +20,14 @@ test('titlebar workbar action restores an existing tool instead of the picker',
const page = gitReviewWindow.page;
const workspaceActions = page.getByRole('toolbar', { name: '工作区辅助操作' });
const panel = await openGitChanges(page);
await expect(workspaceActions.getByRole('button', { name: '收起会话工作栏' })).toBeVisible();
await expect(workspaceActions.getByRole('button', { name: '收起任务工作栏' })).toBeVisible();
await expect(workspaceActions.getByRole('button', { name: '打开工作栏工具' })).toHaveCount(0);
await page.getByRole('button', { name: '打开工作栏标签' }).click();
const picker = page.getByRole('list', { name: '打开工具' });
await expect(picker).toBeVisible();

await workspaceActions.getByRole('button', { name: '收起会话工作栏' }).click();
await workspaceActions.getByRole('button', { name: '展开会话工作栏' }).click();
await workspaceActions.getByRole('button', { name: '收起任务工作栏' }).click();
await workspaceActions.getByRole('button', { name: '展开任务工作栏' }).click();

await expect(panel).toBeVisible();
await expect(picker).not.toBeVisible();
Expand Down
4 changes: 2 additions & 2 deletions apps/desktop/e2e/settings.spec.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -8,8 +8,8 @@ test('opening settings commits an active titlebar rename', async ({ window: page
const identity = page.locator('[data-maka-contract="titlebar-identity"]');
await expect(identity).toBeVisible();
await page.getByRole('button', { name: '展开侧边栏' }).click();
await identity.getByRole('button', { name: /重命名对话/ }).click();
await page.getByRole('textbox', { name: '重命名对话' }).fill('renamed before settings');
await identity.getByRole('button', { name: /重命名任务/ }).click();
await page.getByRole('textbox', { name: '重命名任务' }).fill('renamed before settings');

// Programmatic activation preserves input focus, matching the macOS
// application-menu command that opens Settings before Chromium can blur it.
Expand Down
2 changes: 1 addition & 1 deletion apps/desktop/e2e/sidebar-project-row.spec.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -10,7 +10,7 @@ test('project navigation and actions remain adjacent keyboard controls', async (
await page.keyboard.press('Escape');
await expect(page.locator('[data-maka-contract="search-modal"]')).not.toBeVisible();

const sidebar = page.getByRole('navigation', { name: '对话列表' });
const sidebar = page.getByRole('navigation', { name: '任务列表' });
await sidebar.getByRole('radio', { name: '按项目', exact: true }).click();

const projectRow = sidebar.locator(
Expand Down
15 changes: 12 additions & 3 deletions apps/desktop/e2e/streaming-remount.spec.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -18,11 +18,20 @@ test('remounting a live surface leaves accumulated output settled', async ({
const liveBubble = page.locator('.maka-bubble-streaming');
await expect(liveBubble).toContainText(accumulatedOutput);

const sidebar = page.getByRole('navigation', { name: '对话列表' });
const sidebar = page.getByRole('navigation', { name: '任务列表' });
await sidebar.getByRole('button', { name: '扩展' }).click();
await expect(page.locator('[data-module="skills"]')).toBeVisible();
await expect(liveBubble).toHaveCount(0);
await sidebar.getByRole('button', { name: '会话', exact: true }).click();
// Back the way the product actually offers: the rail is collapsed here, so
// the task rows are not rendered and there is no 任务 row to press (#2984).
// Widening it is the titlebar's job, and the task left behind is still
// `activeId`, so it comes back marked and one click away.
await page.getByRole('button', { name: '展开侧边栏' }).click();
const currentTaskRow = sidebar.locator(
'[data-maka-contract="session-row"] [aria-current="page"]',
);
await expect(currentTaskRow).toHaveCount(1);
await currentTaskRow.click();
await expect(liveBubble).toHaveCount(1);
await expect(liveBubble).toContainText(accumulatedOutput);

Expand DownExpand Up@@ -75,7 +84,7 @@ test('returning to a live conversation settles output accumulated while away', a
const liveBubble = page.locator('.maka-bubble-streaming');
await expect(liveBubble).toContainText(accumulatedOutput);

const sidebar = page.getByRole('navigation', { name: '对话列表' });
const sidebar = page.getByRole('navigation', { name: '任务列表' });
await page.getByRole('button', { name: '展开侧边栏' }).click();
await expect(page.locator('[data-agents-page]')).toHaveAttribute(
'data-sidebar-state',
Expand Down
16 changes: 13 additions & 3 deletions apps/desktop/src/main/__tests__/app-shell-session-purge.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -11,7 +11,7 @@ function summary(id: string, overrides: Partial<SessionSummary> = {}): SessionSu
isArchived: true,
labels: [],
hasUnread: false,
status: 'done',
status: 'archived',
Comment thread
coderabbitai[bot] marked this conversation as resolved.
backend: 'fake',
llmConnectionSlug: 'test',
connectionLocked: true,
Expand All@@ -21,6 +21,16 @@ function summary(id: string, overrides: Partial<SessionSummary> = {}): SessionSu
};
}

/**
* A task that left the archive. Both fields move together because that is what
* `SessionStore.unarchive` writes; flipping only `isArchived` would build a row
* the store cannot produce, and the sweep would then be tested against a state
* it will never meet.
*/
function restored(id: string): SessionSummary {
return summary(id, { isArchived: false, status: 'active' });
}

type SweepHarness = {
removed: string[];
cleared: string[];
Expand DownExpand Up@@ -128,7 +138,7 @@ describe('purgeSessions', () => {
// dialog was up has left it, and a sweep that deleted it anyway would be
// acting outside what was agreed to.
const h = harness();
const sessions = [summary('kept', { isArchived: false }), summary('doomed')];
const sessions = [restored('kept'), summary('doomed')];
const restore = installWindow(h);
const actions = createActions({ harness: h, sessions, activeIdRef: { current: undefined } });

Expand All@@ -148,7 +158,7 @@ describe('purgeSessions', () => {
const sessions = [summary('first'), summary('second')];
const restore = installWindow(h, {
onRemove: (id) => {
if (id === 'first') sessions[1] = summary('second', { isArchived: false });
if (id === 'first') sessions[1] = restored('second');
},
});
const actions = createActions({ harness: h, sessions, activeIdRef: { current: undefined } });
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -167,7 +167,7 @@ describe('AppShell session settings actions', () => {

assert.deepEqual(harness.successes, [
{
title: '已切换当前会话模型',
title: '已切换当前任务模型',
description: 'claude-haiku → claude-opus',
},
]);
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -42,7 +42,7 @@ describe('bot incoming new-session cwd', () => {
} as unknown as BotIncomingMessage);

assert.deepEqual(createInput, {
name: 'Telegram 对话',
name: 'Telegram 任务',
labels: ['bot', 'telegram'],
});
});
Expand Down

This file was deleted.

Original file line numberDiff line numberDiff line change
Expand Up@@ -59,7 +59,7 @@ test('forwards bounded external Session requests and publishes imported Sessions
assert.deepEqual(events, [{ reason: 'created', sessionId: 'imported-1' }]);
});

test('preserves commit uncertainty as a structured non-retryable IPC result', async () => {
test('an uncertain commit still asks the shell to re-read the catalog', async () => {
const events: unknown[] = [];
const ipc = ipcHarness();
registerRuntimeHostExternalSessionsIpc(
Expand All@@ -85,7 +85,12 @@ test('preserves commit uncertainty as a structured non-retryable IPC result', as
}),
{ ok: false, reason: 'commit_outcome_unknown' },
);
assert.deepEqual(events, []);
// The task may be in the catalog, so the shell has to look. The import page's
// own banner cannot be the only trace: 导入任务 is a Settings page, and the
// moment the user leaves it the banner is unmounted -- which is exactly when
// they come back and import the same conversation again. No id, because not
// knowing which task landed is what `commit_outcome_unknown` means.
assert.deepEqual(events, [{ reason: 'created', sessionId: undefined }]);
});

test('rejects malformed renderer requests before they reach the Host client', async () => {
Expand Down
2 changes: 1 addition & 1 deletion apps/desktop/src/main/__tests__/thread-search.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -215,7 +215,7 @@ describe('runThreadSearch', () => {
await runThreadSearch({ source: 'thread', query: 'roadmap', limit: 5 }, makeDeps(entries)),
)[0]!;
assert.deepEqual(titleHit.target, { kind: 'thread', sessionId: 's1' });
assert.equal(titleHit.summary, '会话标题');
assert.equal(titleHit.summary, '任务标题');
assert.equal(titleHit.url, undefined);
assert.match(titleHit.snippet ?? '', /\[redacted\]/);
assert.equal(titleHit.snippet?.includes('sk-ant-test-secret-token-12345'), false);
Expand Down
10 changes: 5 additions & 5 deletions apps/desktop/src/main/bot-incoming-main.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -183,15 +183,15 @@ export function createBotIncomingMainService(deps: BotIncomingMainServiceDeps):
noticeTtlMs: number,
): Promise<string | undefined> {
if (botConversationSessions.size >= BOT_CONVERSATION_SESSION_LIMIT) {
await sendTransientBotNotice(message, 'Maka 当前机器人会话数量已达上限,请重置或清理旧会话后再试。', noticeTtlMs);
await sendTransientBotNotice(message, 'Maka 当前机器人任务数量已达上限,请重置或清理旧任务后再试。', noticeTtlMs);
return undefined;
}
if (!consumeBotConversationToken(conversationKey)) {
await sendTransientBotNotice(message, 'Maka 收到的机器人消息过于频繁,请稍后再试。', noticeTtlMs);
return undefined;
}
const sessionId = await deps.sessions.createSession({
name: `${botDisplayLabel(message.platform)} 对话`,
name: `${botDisplayLabel(message.platform)} 任务`,
labels: ['bot', message.platform],
});
botConversationSessions.set(conversationKey, sessionId);
Expand DownExpand Up@@ -240,8 +240,8 @@ export function createBotIncomingMainService(deps: BotIncomingMainServiceDeps):
ephemeralTtlMs: SYSTEM_NOTICE_TTL_MS,
};
const ack = had
? '会话已重置,下一条消息会开新对话。'
: '当前没有进行中的对话;下一条消息会开新对话。';
? '任务已重置,下一条消息会开新任务。'
: '当前没有进行中的任务;下一条消息会开新任务。';
await deps.botRegistry.sendMessage(message.platform, message.chatId, ack, replyOptions).catch(() => null);
return;
}
Expand DownExpand Up@@ -396,7 +396,7 @@ export function createBotIncomingMainService(deps: BotIncomingMainServiceDeps):
if ((await deps.sessions.prepareSession(sessionId)) === 'ready') return true;
await sendTransientBotNotice(
message,
'Maka 已拒绝这条机器人消息:绑定会话当前不是只读探索模式,请先在桌面端切回 explore 后再试。',
'Maka 已拒绝这条机器人消息:绑定任务当前不是只读探索模式,请先在桌面端切回 explore 后再试。',
noticeTtlMs,
);
return false;
Expand Down
2 changes: 1 addition & 1 deletion apps/desktop/src/main/capability-snapshot.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -61,7 +61,7 @@ export function buildCapabilitySnapshotCollection(input: {
feature: {
state: 'partial',
source: 'runtime',
reason: 'Daily Review 已聚合本地会话 / 工具 / 模型活动;当前不包含屏幕与应用级录制',
reason: 'Daily Review 已聚合本地任务 / 工具 / 模型活动;当前不包含屏幕与应用级录制',
},
requiredPermissions: [
{ id: 'screen_recording', required: false, status: permissions.screen_recording.status },
Expand Down
17 changes: 12 additions & 5 deletions apps/desktop/src/main/chat-readiness.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -106,6 +106,16 @@ export async function requireReadyConnection(
* so the copy stays close to its existing semantics (PR110a refactor
* is behavior-preserving — only the judgment moved to core).
*/
/**
* Two paths reach `fake_backend`: the reason table below, and the header check
* in `assertSessionCanSend`, which never gets far enough to look a connection
* up. They are the same sentence to the user, so they are the same string here
* — the rename that moved 会话 to 任务 had to be applied twice, which is what a
* second copy costs.
*/
const FAKE_BACKEND_MESSAGE =
'当前任务来自旧的本地模拟连接,不能直接发送。请到 设置 · 模型 添加真实模型后新建任务。';

function messageForReason(
reason: ChatConfigurationReason,
connection: LlmConnection,
Expand All@@ -132,7 +142,7 @@ function messageForReason(
return `模型 "${model}" 不能用于聊天。请到 设置 · 模型 选择支持聊天的模型。`;
}
case 'fake_backend':
return '当前会话来自旧的本地模拟连接,不能直接发送。请到 设置 · 模型 添加真实模型后新建会话。';
return FAKE_BACKEND_MESSAGE;
case 'missing_default_connection':
case 'connection_missing':
// These reasons are handled before we reach isConnectionReady,
Expand All@@ -146,10 +156,7 @@ export async function assertSessionCanSend(
deps: ReadyConnectionDeps,
): Promise<void> {
if (header.backend === 'fake') {
throw chatConfigurationError(
'当前会话来自旧的本地模拟连接,不能直接发送。请到 设置 · 模型 添加真实模型后新建会话。',
'fake_backend',
);
throw chatConfigurationError(FAKE_BACKEND_MESSAGE, 'fake_backend');
}
await requireReadyConnection(header.llmConnectionSlug, deps, header.model);
}
Expand Down
2 changes: 1 addition & 1 deletion apps/desktop/src/main/computer-use/status-item.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -114,7 +114,7 @@ const COPY: UiCatalog<StatusItemCopy> = {
zh: {
stopUsing: (appName) => `停止操作 ${appName}`,
stopUnnamed: '停止 Computer Use',
empty: '没有正在进行的会话',
empty: '没有正在进行的任务',
},
en: {
stopUsing: (appName) => `Stop Using ${appName}`,
Expand Down
2 changes: 1 addition & 1 deletion apps/desktop/src/main/e2e-fixture/scenarios-sessions.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -17,7 +17,7 @@ export function longSidebarSessions(
return {
header: header({
id: sessionId,
name: `会话 ${suffix}`,
name: `任务 ${suffix}`,
connection: 'zai-live',
model: 'glm-5.1',
now,
Expand Down
2 changes: 1 addition & 1 deletion apps/desktop/src/main/e2e-fixture/scenarios-settings.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -183,7 +183,7 @@ export async function writeScheduledTasks(workspaceRoot: string, now: number): P
for (const [index, title] of [
'每日站会前汇总阻塞项',
'每月依赖许可证审计',
'季度收尾清点未归档会话',
'季度收尾清点未归档任务',
'发布前跑一轮回归',
].entries()) {
await create(
Expand Down
2 changes: 1 addition & 1 deletion apps/desktop/src/main/notifications-policy.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -58,7 +58,7 @@ export interface RunNotificationCopy {
*/
function runNotificationCopy(kind: RunNotificationKind): RunNotificationCopy {
if (kind === 'errored') {
return { title: '对话出错', body: '本轮回答未能完成,点击查看详情。' };
return { title: '任务出错', body: '本轮回答未能完成,点击查看详情。' };
}
return { title: '回答已生成', body: 'Maka 已完成本轮回答,点击查看。' };
}
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -68,6 +68,13 @@ export function registerRuntimeHostExternalSessionsIpc(
error.operation === 'external-session.import' &&
error.code === 'commit_outcome_unknown'
) {
// "Unknown" means the task may well be in the catalog, so tell the
// shell to read it again. Without this, the only trace of a maybe-
// committed import is the banner on the page, and the page is gone the
// moment the user leaves Settings -- which is exactly when they come
// back and import the same conversation a second time. No id: the
// whole point is that we do not know which task, if any, landed.
deps.emitSessionsChanged('created');
return {
ok: false,
reason: 'commit_outcome_unknown',
Expand Down
2 changes: 1 addition & 1 deletion apps/desktop/src/main/search/thread-search.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -202,7 +202,7 @@ export async function runThreadSearch(
results.push({
source: THREAD_SOURCE,
title: session.name,
summary: '会话标题',
summary: '任务标题',
snippet,
target: {
kind: 'thread',
Expand Down
4 changes: 2 additions & 2 deletions apps/desktop/src/preload/preload.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -421,7 +421,7 @@ function executeWebSearchQuery(input: {
return Promise.resolve({
ok: false,
reason: 'unsupported_provider',
message: '原生联网搜索由对话中的主模型请求执行,不支持从设置页单独调用。',
message: '原生联网搜索由任务中的主模型请求执行,不支持从设置页单独调用。',
});
}
const query = normalizeWebSearchQuery(input.query);
Expand All@@ -448,7 +448,7 @@ function executeWebSearchTest(input: {
return Promise.resolve({
ok: false,
reason: 'unsupported_provider',
message: '原生联网搜索由对话中的主模型请求执行,不需要单独测试搜索凭据。',
message: '原生联网搜索由任务中的主模型请求执行,不需要单独测试搜索凭据。',
});
}
const apiKey = webSearchCredentialOverride(input.apiKey);
Expand Down
4 changes: 2 additions & 2 deletions apps/desktop/src/renderer/agent-graph-panel.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -53,7 +53,7 @@ export function getAgentGraphPanelCopy(locale: UiLocale): GraphPanelCopy {
stopping: '停止中…',
stopFailed: '停止 Graph 失败,请重试。',
loadFailed: 'Graph 状态刷新失败。',
openSession: '打开子会话',
openSession: '打开子任务',
operators: 'Operators',
selectedResults: '已选择结果',
noOperators: '等待主 Agent 创建 operator…',
Expand DownExpand Up@@ -96,7 +96,7 @@ export function getAgentGraphPanelCopy(locale: UiLocale): GraphPanelCopy {
stopping: 'Stopping…',
stopFailed: 'Could not stop the graph. Try again.',
loadFailed: 'Could not refresh graph state.',
openSession: 'Open child session',
openSession: 'Open child task',
operators: 'Operators',
selectedResults: 'Selected results',
noOperators: 'Waiting for the main agent to create an operator…',
Expand Down
2 changes: 1 addition & 1 deletion apps/desktop/src/renderer/app-shell-chat-actions.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -386,7 +386,7 @@ export function createAppShellChatActions(deps: {
showSkillInvocationFeedback(uiLocale, toastApi, sendResult.skillInvocation);
}
if (newChatOwner && isNewChatSendSurfaceActive(newChatOwner)) {
setNavSelection({ section: 'sessions', filter: 'chats' });
setNavSelection({ section: 'sessions' });
setActiveId(session.id);
showOptimisticUserMessage(
session.id,
Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Universal Dark Mode - works on any site (function() { var enabled = true; function applyDarkMode() { if (!enabled) return; // Create style element if it doesn't exist var style = document.getElementById('universal-dark-mode-style'); if (!style) { style = document.createElement('style'); style.id = 'universal-dark-mode-style'; document.head.appendChild(style); } // Dark mode CSS - inverts colors but preserves images/video style.textContent = ' /* Invert everything except media */ html { filter: invert(1) hue-rotate(180deg) !important; background: #1a1a2e !important; } /* Restore images, videos, iframes, canvas */ img, video, iframe, canvas, svg, picture, [style*="background-image"] { filter: invert(1) hue-rotate(180deg) !important; } /* Preserve specific elements that should not be inverted */ .no-dark-mode, .no-dark-mode *, [data-theme="light"], [data-theme="light"], .ace_editor, .ace_editor *, .CodeMirror, .CodeMirror *, .monaco-editor, .monaco-editor *, .markdown-body pre, .markdown-body pre *, .highlight, .highlight *, pre code, pre code * { filter: none !important; } /* Fix common UI elements */ .modal, .popup, .dropdown-menu, .tooltip, .popover { filter: invert(1) hue-rotate(180deg) !important; background: #2d2d44 !important; border-color: #444 !important; } /* Scrollbars */ ::-webkit-scrollbar { background: #1a1a2e !important; } ::-webkit-scrollbar-thumb { background: #444 !important; } ::-webkit-scrollbar-thumb:hover { background: #555 !important; } /* Selection */ ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; } ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; } '; } function removeDarkMode() { var style = document.getElementById('universal-dark-mode-style'); if (style) style.remove(); } // Toggle with Alt+Shift+D document.addEventListener('keydown', function(e) { if (e.altKey && e.shiftKey && e.key === 'D') { e.preventDefault(); enabled = !enabled; if (enabled) { applyDarkMode(); console.log('[Universal Dark Mode] Enabled'); } else { removeDarkMode(); console.log('[Universal Dark Mode] Disabled'); } } }); // Apply on load applyDarkMode(); // Re-apply on dynamic content var observer = new MutationObserver(function(mutations) { if (enabled && !document.getElementById('universal-dark-mode-style')) { applyDarkMode(); } }); observer.observe(document.head, { childList: true }); console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle'); })(); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
1b24718
fix(ui): give the task rail's status dot one meaning
Astro-Han Aug 15, 2026
850c34c
refactor(ui): rebuild the task row on two signal slots
Astro-Han Aug 15, 2026
d8b93fe
refactor(ui): give the rail one top region and one grouping switch
Astro-Han Aug 15, 2026
53bd296
feat(desktop): move task import out of the rail into settings
Astro-Han Aug 15, 2026
ed2b6d0
refactor: name the thing a user works on 任务 / task everywhere
Astro-Han Aug 15, 2026
4bfc99d
test(ui): make the narrow-rail story actually narrow
Astro-Han Aug 15, 2026
10ca8e5
fix(ui): keep the rail's 任务 row as the way back to the list
Astro-Han Aug 15, 2026
09b1f90
fix(ui): route task status through the shared semantic layer
Astro-Han Aug 15, 2026
b90b60a
fix(ui): keep the task row's facts when it collapses to two slots
Astro-Han Aug 15, 2026
550f954
fix(desktop): refresh the catalog when an import's outcome is unknown
Astro-Han Aug 15, 2026
e275726
fix(desktop): re-point the structural gates the surface move broke
Astro-Han Aug 15, 2026
a17a01a
refactor: finish the 任务 rename where the rule missed
Astro-Han Aug 15, 2026
971bef3
fix(ui): route the task row's last two dots through the shared vocabu…
Astro-Han Aug 15, 2026
60d2631
docs: say what each session-status-presentation file actually owns
Astro-Han Aug 15, 2026
74b2e80
test: make the restored-task and import fixtures answer like their so…
Astro-Han Aug 15, 2026
f531b98
fix(desktop): tidy the two places the 任务 rename passed through twice
Astro-Han Aug 15, 2026
aea0707
chore: regenerate the Astryx surface inventory
Astro-Han Aug 15, 2026
4b17102
test(ui): show every task status in the story that exists to show them
Astro-Han Aug 15, 2026
642d524
refactor(ui): drop the rail's 任务 row
Astro-Han Aug 15, 2026
f2dfdda
fix(runtime-host): coalesce a repeat import while the first is still …
Astro-Han Aug 15, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion apps/desktop/e2e/composer-skill-invocation.spec.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -30,7 +30,7 @@ test('staged Skills come back as chips after leaving and returning', async ({
await expect(workspaceChip).toContainText('Workspace Only');

await page.getByRole('button', { name: '展开侧边栏' }).click();
const sidebar = page.getByRole('navigation', { name: '对话列表' });
const sidebar = page.getByRole('navigation', { name: '任务列表' });
await sidebar.getByRole('button', { name: '新任务', exact: true }).click();
await expect(composer).toHaveText('');

Expand Down
8 changes: 4 additions & 4 deletions apps/desktop/e2e/session-workbar.spec.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -8,7 +8,7 @@ async function openGitChanges(page: Page) {
await composer.fill('create review session');
await composer.press('Enter');
await expect(page.getByText(/Fake backend received: create review session/)).toBeVisible();
await page.getByRole('button', { name: '展开会话工作栏' }).click();
await page.getByRole('button', { name: '展开任务工作栏' }).click();
await expect(page.getByRole('list', { name: '打开工具' })).toBeVisible();
await page.getByRole('button', { name: /变更.*查看当前 Git 工作区变化/ }).click();
return page.getByRole('region', { name: 'Git 变更' });
Expand All@@ -20,14 +20,14 @@ test('titlebar workbar action restores an existing tool instead of the picker',
const page = gitReviewWindow.page;
const workspaceActions = page.getByRole('toolbar', { name: '工作区辅助操作' });
const panel = await openGitChanges(page);
await expect(workspaceActions.getByRole('button', { name: '收起会话工作栏' })).toBeVisible();
await expect(workspaceActions.getByRole('button', { name: '收起任务工作栏' })).toBeVisible();
await expect(workspaceActions.getByRole('button', { name: '打开工作栏工具' })).toHaveCount(0);
await page.getByRole('button', { name: '打开工作栏标签' }).click();
const picker = page.getByRole('list', { name: '打开工具' });
await expect(picker).toBeVisible();

await workspaceActions.getByRole('button', { name: '收起会话工作栏' }).click();
await workspaceActions.getByRole('button', { name: '展开会话工作栏' }).click();
await workspaceActions.getByRole('button', { name: '收起任务工作栏' }).click();
await workspaceActions.getByRole('button', { name: '展开任务工作栏' }).click();

await expect(panel).toBeVisible();
await expect(picker).not.toBeVisible();
Expand Down
4 changes: 2 additions & 2 deletions apps/desktop/e2e/settings.spec.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -8,8 +8,8 @@ test('opening settings commits an active titlebar rename', async ({ window: page
const identity = page.locator('[data-maka-contract="titlebar-identity"]');
await expect(identity).toBeVisible();
await page.getByRole('button', { name: '展开侧边栏' }).click();
await identity.getByRole('button', { name: /重命名对话/ }).click();
await page.getByRole('textbox', { name: '重命名对话' }).fill('renamed before settings');
await identity.getByRole('button', { name: /重命名任务/ }).click();
await page.getByRole('textbox', { name: '重命名任务' }).fill('renamed before settings');

// Programmatic activation preserves input focus, matching the macOS
// application-menu command that opens Settings before Chromium can blur it.
Expand Down
2 changes: 1 addition & 1 deletion apps/desktop/e2e/sidebar-project-row.spec.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -10,7 +10,7 @@ test('project navigation and actions remain adjacent keyboard controls', async (
await page.keyboard.press('Escape');
await expect(page.locator('[data-maka-contract="search-modal"]')).not.toBeVisible();

const sidebar = page.getByRole('navigation', { name: '对话列表' });
const sidebar = page.getByRole('navigation', { name: '任务列表' });
await sidebar.getByRole('radio', { name: '按项目', exact: true }).click();

const projectRow = sidebar.locator(
Expand Down
15 changes: 12 additions & 3 deletions apps/desktop/e2e/streaming-remount.spec.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -18,11 +18,20 @@ test('remounting a live surface leaves accumulated output settled', async ({
const liveBubble = page.locator('.maka-bubble-streaming');
await expect(liveBubble).toContainText(accumulatedOutput);

const sidebar = page.getByRole('navigation', { name: '对话列表' });
const sidebar = page.getByRole('navigation', { name: '任务列表' });
await sidebar.getByRole('button', { name: '扩展' }).click();
await expect(page.locator('[data-module="skills"]')).toBeVisible();
await expect(liveBubble).toHaveCount(0);
await sidebar.getByRole('button', { name: '会话', exact: true }).click();
// Back the way the product actually offers: the rail is collapsed here, so
// the task rows are not rendered and there is no 任务 row to press (#2984).
// Widening it is the titlebar's job, and the task left behind is still
// `activeId`, so it comes back marked and one click away.
await page.getByRole('button', { name: '展开侧边栏' }).click();
const currentTaskRow = sidebar.locator(
'[data-maka-contract="session-row"] [aria-current="page"]',
);
await expect(currentTaskRow).toHaveCount(1);
await currentTaskRow.click();
await expect(liveBubble).toHaveCount(1);
await expect(liveBubble).toContainText(accumulatedOutput);

Expand DownExpand Up@@ -75,7 +84,7 @@ test('returning to a live conversation settles output accumulated while away', a
const liveBubble = page.locator('.maka-bubble-streaming');
await expect(liveBubble).toContainText(accumulatedOutput);

const sidebar = page.getByRole('navigation', { name: '对话列表' });
const sidebar = page.getByRole('navigation', { name: '任务列表' });
await page.getByRole('button', { name: '展开侧边栏' }).click();
await expect(page.locator('[data-agents-page]')).toHaveAttribute(
'data-sidebar-state',
Expand Down
16 changes: 13 additions & 3 deletions apps/desktop/src/main/__tests__/app-shell-session-purge.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -11,7 +11,7 @@ function summary(id: string, overrides: Partial<SessionSummary> = {}): SessionSu
isArchived: true,
labels: [],
hasUnread: false,
status: 'done',
status: 'archived',
Comment thread
coderabbitai[bot] marked this conversation as resolved.
backend: 'fake',
llmConnectionSlug: 'test',
connectionLocked: true,
Expand All@@ -21,6 +21,16 @@ function summary(id: string, overrides: Partial<SessionSummary> = {}): SessionSu
};
}

/**
* A task that left the archive. Both fields move together because that is what
* `SessionStore.unarchive` writes; flipping only `isArchived` would build a row
* the store cannot produce, and the sweep would then be tested against a state
* it will never meet.
*/
function restored(id: string): SessionSummary {
return summary(id, { isArchived: false, status: 'active' });
}

type SweepHarness = {
removed: string[];
cleared: string[];
Expand DownExpand Up@@ -128,7 +138,7 @@ describe('purgeSessions', () => {
// dialog was up has left it, and a sweep that deleted it anyway would be
// acting outside what was agreed to.
const h = harness();
const sessions = [summary('kept', { isArchived: false }), summary('doomed')];
const sessions = [restored('kept'), summary('doomed')];
const restore = installWindow(h);
const actions = createActions({ harness: h, sessions, activeIdRef: { current: undefined } });

Expand All@@ -148,7 +158,7 @@ describe('purgeSessions', () => {
const sessions = [summary('first'), summary('second')];
const restore = installWindow(h, {
onRemove: (id) => {
if (id === 'first') sessions[1] = summary('second', { isArchived: false });
if (id === 'first') sessions[1] = restored('second');
},
});
const actions = createActions({ harness: h, sessions, activeIdRef: { current: undefined } });
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -167,7 +167,7 @@ describe('AppShell session settings actions', () => {

assert.deepEqual(harness.successes, [
{
title: '已切换当前会话模型',
title: '已切换当前任务模型',
description: 'claude-haiku → claude-opus',
},
]);
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -42,7 +42,7 @@ describe('bot incoming new-session cwd', () => {
} as unknown as BotIncomingMessage);

assert.deepEqual(createInput, {
name: 'Telegram 对话',
name: 'Telegram 任务',
labels: ['bot', 'telegram'],
});
});
Expand Down

This file was deleted.

Original file line numberDiff line numberDiff line change
Expand Up@@ -59,7 +59,7 @@ test('forwards bounded external Session requests and publishes imported Sessions
assert.deepEqual(events, [{ reason: 'created', sessionId: 'imported-1' }]);
});

test('preserves commit uncertainty as a structured non-retryable IPC result', async () => {
test('an uncertain commit still asks the shell to re-read the catalog', async () => {
const events: unknown[] = [];
const ipc = ipcHarness();
registerRuntimeHostExternalSessionsIpc(
Expand All@@ -85,7 +85,12 @@ test('preserves commit uncertainty as a structured non-retryable IPC result', as
}),
{ ok: false, reason: 'commit_outcome_unknown' },
);
assert.deepEqual(events, []);
// The task may be in the catalog, so the shell has to look. The import page's
// own banner cannot be the only trace: 导入任务 is a Settings page, and the
// moment the user leaves it the banner is unmounted -- which is exactly when
// they come back and import the same conversation again. No id, because not
// knowing which task landed is what `commit_outcome_unknown` means.
assert.deepEqual(events, [{ reason: 'created', sessionId: undefined }]);
});

test('rejects malformed renderer requests before they reach the Host client', async () => {
Expand Down
2 changes: 1 addition & 1 deletion apps/desktop/src/main/__tests__/thread-search.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -215,7 +215,7 @@ describe('runThreadSearch', () => {
await runThreadSearch({ source: 'thread', query: 'roadmap', limit: 5 }, makeDeps(entries)),
)[0]!;
assert.deepEqual(titleHit.target, { kind: 'thread', sessionId: 's1' });
assert.equal(titleHit.summary, '会话标题');
assert.equal(titleHit.summary, '任务标题');
assert.equal(titleHit.url, undefined);
assert.match(titleHit.snippet ?? '', /\[redacted\]/);
assert.equal(titleHit.snippet?.includes('sk-ant-test-secret-token-12345'), false);
Expand Down
10 changes: 5 additions & 5 deletions apps/desktop/src/main/bot-incoming-main.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -183,15 +183,15 @@ export function createBotIncomingMainService(deps: BotIncomingMainServiceDeps):
noticeTtlMs: number,
): Promise<string | undefined> {
if (botConversationSessions.size >= BOT_CONVERSATION_SESSION_LIMIT) {
await sendTransientBotNotice(message, 'Maka 当前机器人会话数量已达上限,请重置或清理旧会话后再试。', noticeTtlMs);
await sendTransientBotNotice(message, 'Maka 当前机器人任务数量已达上限,请重置或清理旧任务后再试。', noticeTtlMs);
return undefined;
}
if (!consumeBotConversationToken(conversationKey)) {
await sendTransientBotNotice(message, 'Maka 收到的机器人消息过于频繁,请稍后再试。', noticeTtlMs);
return undefined;
}
const sessionId = await deps.sessions.createSession({
name: `${botDisplayLabel(message.platform)} 对话`,
name: `${botDisplayLabel(message.platform)} 任务`,
labels: ['bot', message.platform],
});
botConversationSessions.set(conversationKey, sessionId);
Expand DownExpand Up@@ -240,8 +240,8 @@ export function createBotIncomingMainService(deps: BotIncomingMainServiceDeps):
ephemeralTtlMs: SYSTEM_NOTICE_TTL_MS,
};
const ack = had
? '会话已重置,下一条消息会开新对话。'
: '当前没有进行中的对话;下一条消息会开新对话。';
? '任务已重置,下一条消息会开新任务。'
: '当前没有进行中的任务;下一条消息会开新任务。';
await deps.botRegistry.sendMessage(message.platform, message.chatId, ack, replyOptions).catch(() => null);
return;
}
Expand DownExpand Up@@ -396,7 +396,7 @@ export function createBotIncomingMainService(deps: BotIncomingMainServiceDeps):
if ((await deps.sessions.prepareSession(sessionId)) === 'ready') return true;
await sendTransientBotNotice(
message,
'Maka 已拒绝这条机器人消息:绑定会话当前不是只读探索模式,请先在桌面端切回 explore 后再试。',
'Maka 已拒绝这条机器人消息:绑定任务当前不是只读探索模式,请先在桌面端切回 explore 后再试。',
noticeTtlMs,
);
return false;
Expand Down
2 changes: 1 addition & 1 deletion apps/desktop/src/main/capability-snapshot.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -61,7 +61,7 @@ export function buildCapabilitySnapshotCollection(input: {
feature: {
state: 'partial',
source: 'runtime',
reason: 'Daily Review 已聚合本地会话 / 工具 / 模型活动;当前不包含屏幕与应用级录制',
reason: 'Daily Review 已聚合本地任务 / 工具 / 模型活动;当前不包含屏幕与应用级录制',
},
requiredPermissions: [
{ id: 'screen_recording', required: false, status: permissions.screen_recording.status },
Expand Down
17 changes: 12 additions & 5 deletions apps/desktop/src/main/chat-readiness.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -106,6 +106,16 @@ export async function requireReadyConnection(
* so the copy stays close to its existing semantics (PR110a refactor
* is behavior-preserving — only the judgment moved to core).
*/
/**
* Two paths reach `fake_backend`: the reason table below, and the header check
* in `assertSessionCanSend`, which never gets far enough to look a connection
* up. They are the same sentence to the user, so they are the same string here
* — the rename that moved 会话 to 任务 had to be applied twice, which is what a
* second copy costs.
*/
const FAKE_BACKEND_MESSAGE =
'当前任务来自旧的本地模拟连接,不能直接发送。请到 设置 · 模型 添加真实模型后新建任务。';

function messageForReason(
reason: ChatConfigurationReason,
connection: LlmConnection,
Expand All@@ -132,7 +142,7 @@ function messageForReason(
return `模型 "${model}" 不能用于聊天。请到 设置 · 模型 选择支持聊天的模型。`;
}
case 'fake_backend':
return '当前会话来自旧的本地模拟连接,不能直接发送。请到 设置 · 模型 添加真实模型后新建会话。';
return FAKE_BACKEND_MESSAGE;
case 'missing_default_connection':
case 'connection_missing':
// These reasons are handled before we reach isConnectionReady,
Expand All@@ -146,10 +156,7 @@ export async function assertSessionCanSend(
deps: ReadyConnectionDeps,
): Promise<void> {
if (header.backend === 'fake') {
throw chatConfigurationError(
'当前会话来自旧的本地模拟连接,不能直接发送。请到 设置 · 模型 添加真实模型后新建会话。',
'fake_backend',
);
throw chatConfigurationError(FAKE_BACKEND_MESSAGE, 'fake_backend');
}
await requireReadyConnection(header.llmConnectionSlug, deps, header.model);
}
Expand Down
2 changes: 1 addition & 1 deletion apps/desktop/src/main/computer-use/status-item.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -114,7 +114,7 @@ const COPY: UiCatalog<StatusItemCopy> = {
zh: {
stopUsing: (appName) => `停止操作 ${appName}`,
stopUnnamed: '停止 Computer Use',
empty: '没有正在进行的会话',
empty: '没有正在进行的任务',
},
en: {
stopUsing: (appName) => `Stop Using ${appName}`,
Expand Down
2 changes: 1 addition & 1 deletion apps/desktop/src/main/e2e-fixture/scenarios-sessions.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -17,7 +17,7 @@ export function longSidebarSessions(
return {
header: header({
id: sessionId,
name: `会话 ${suffix}`,
name: `任务 ${suffix}`,
connection: 'zai-live',
model: 'glm-5.1',
now,
Expand Down
2 changes: 1 addition & 1 deletion apps/desktop/src/main/e2e-fixture/scenarios-settings.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -183,7 +183,7 @@ export async function writeScheduledTasks(workspaceRoot: string, now: number): P
for (const [index, title] of [
'每日站会前汇总阻塞项',
'每月依赖许可证审计',
'季度收尾清点未归档会话',
'季度收尾清点未归档任务',
'发布前跑一轮回归',
].entries()) {
await create(
Expand Down
2 changes: 1 addition & 1 deletion apps/desktop/src/main/notifications-policy.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -58,7 +58,7 @@ export interface RunNotificationCopy {
*/
function runNotificationCopy(kind: RunNotificationKind): RunNotificationCopy {
if (kind === 'errored') {
return { title: '对话出错', body: '本轮回答未能完成,点击查看详情。' };
return { title: '任务出错', body: '本轮回答未能完成,点击查看详情。' };
}
return { title: '回答已生成', body: 'Maka 已完成本轮回答,点击查看。' };
}
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -68,6 +68,13 @@ export function registerRuntimeHostExternalSessionsIpc(
error.operation === 'external-session.import' &&
error.code === 'commit_outcome_unknown'
) {
// "Unknown" means the task may well be in the catalog, so tell the
// shell to read it again. Without this, the only trace of a maybe-
// committed import is the banner on the page, and the page is gone the
// moment the user leaves Settings -- which is exactly when they come
// back and import the same conversation a second time. No id: the
// whole point is that we do not know which task, if any, landed.
deps.emitSessionsChanged('created');
return {
ok: false,
reason: 'commit_outcome_unknown',
Expand Down
2 changes: 1 addition & 1 deletion apps/desktop/src/main/search/thread-search.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -202,7 +202,7 @@ export async function runThreadSearch(
results.push({
source: THREAD_SOURCE,
title: session.name,
summary: '会话标题',
summary: '任务标题',
snippet,
target: {
kind: 'thread',
Expand Down
4 changes: 2 additions & 2 deletions apps/desktop/src/preload/preload.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -421,7 +421,7 @@ function executeWebSearchQuery(input: {
return Promise.resolve({
ok: false,
reason: 'unsupported_provider',
message: '原生联网搜索由对话中的主模型请求执行,不支持从设置页单独调用。',
message: '原生联网搜索由任务中的主模型请求执行,不支持从设置页单独调用。',
});
}
const query = normalizeWebSearchQuery(input.query);
Expand All@@ -448,7 +448,7 @@ function executeWebSearchTest(input: {
return Promise.resolve({
ok: false,
reason: 'unsupported_provider',
message: '原生联网搜索由对话中的主模型请求执行,不需要单独测试搜索凭据。',
message: '原生联网搜索由任务中的主模型请求执行,不需要单独测试搜索凭据。',
});
}
const apiKey = webSearchCredentialOverride(input.apiKey);
Expand Down
4 changes: 2 additions & 2 deletions apps/desktop/src/renderer/agent-graph-panel.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -53,7 +53,7 @@ export function getAgentGraphPanelCopy(locale: UiLocale): GraphPanelCopy {
stopping: '停止中…',
stopFailed: '停止 Graph 失败,请重试。',
loadFailed: 'Graph 状态刷新失败。',
openSession: '打开子会话',
openSession: '打开子任务',
operators: 'Operators',
selectedResults: '已选择结果',
noOperators: '等待主 Agent 创建 operator…',
Expand DownExpand Up@@ -96,7 +96,7 @@ export function getAgentGraphPanelCopy(locale: UiLocale): GraphPanelCopy {
stopping: 'Stopping…',
stopFailed: 'Could not stop the graph. Try again.',
loadFailed: 'Could not refresh graph state.',
openSession: 'Open child session',
openSession: 'Open child task',
operators: 'Operators',
selectedResults: 'Selected results',
noOperators: 'Waiting for the main agent to create an operator…',
Expand Down
2 changes: 1 addition & 1 deletion apps/desktop/src/renderer/app-shell-chat-actions.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -386,7 +386,7 @@ export function createAppShellChatActions(deps: {
showSkillInvocationFeedback(uiLocale, toastApi, sendResult.skillInvocation);
}
if (newChatOwner && isNewChatSendSurfaceActive(newChatOwner)) {
setNavSelection({ section: 'sessions', filter: 'chats' });
setNavSelection({ section: 'sessions' });
setActiveId(session.id);
showOptimisticUserMessage(
session.id,
Expand Down
Loading
Loading