From 5ee2d6dcc3332ff81d87827b3bbc86e4c4fe8d67 Mon Sep 17 00:00:00 2001 From: IronRookieCoder Date: Wed, 13 May 2026 16:44:13 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E4=BB=BB=E5=8A=A1?= =?UTF-8?q?=E5=88=97=E8=A1=A8=E6=95=B0=E5=AD=97ID=E4=B9=B1=E5=BA=8F?= =?UTF-8?q?=E5=92=8C=E5=86=85=E5=AD=98=E8=BD=AE=E8=AF=A2=E5=AF=BC=E8=87=B4?= =?UTF-8?q?=E6=BB=9A=E5=8A=A8=E8=B7=B3=E5=8A=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 两个修复: 1. 任务列表数字ID乱序(#32):新增 compareTaskIds 比较函数,纯数字ID按 数值大小排序(1, 2, 3, 10 而非 1, 10, 2, 3),非纯数字回退到 localeCompare 自然排序。listTasks 和 taskStateMessage 两处排序入口 均已修复。 2. 终端 RSS 内存轮询引发滚动跳动(#31):useRssDisplay hook 新增 isLoading 参数,仅在加载中启用定时轮询,闲时仅渲染一次内存占用值,避免 Ink 重渲染 干扰用户滚动查看结果。 附:TaskCreateTool.isConcurrencySafe 改为 false,防止并发任务创建的文件 IO 竞态。 Co-Authored-By: CoStrict-DeepSeek-V4-Pro --- .../src/tools/TaskCreateTool/TaskCreateTool.ts | 2 +- .../PromptInput/PromptInputFooterLeftSide.tsx | 7 ++++--- src/utils/__tests__/tasks.test.ts | 6 ++++++ src/utils/taskStateMessage.ts | 3 ++- src/utils/tasks.ts | 13 +++++++++++++ 5 files changed, 26 insertions(+), 5 deletions(-) diff --git a/packages/builtin-tools/src/tools/TaskCreateTool/TaskCreateTool.ts b/packages/builtin-tools/src/tools/TaskCreateTool/TaskCreateTool.ts index f607c821d..2606e84c6 100644 --- a/packages/builtin-tools/src/tools/TaskCreateTool/TaskCreateTool.ts +++ b/packages/builtin-tools/src/tools/TaskCreateTool/TaskCreateTool.ts @@ -69,7 +69,7 @@ export const TaskCreateTool = buildTool({ return isTodoV2Enabled() }, isConcurrencySafe() { - return true + return false }, toAutoClassifierInput(input) { return input.subject diff --git a/src/components/PromptInput/PromptInputFooterLeftSide.tsx b/src/components/PromptInput/PromptInputFooterLeftSide.tsx index efa289772..0f9fa266d 100644 --- a/src/components/PromptInput/PromptInputFooterLeftSide.tsx +++ b/src/components/PromptInput/PromptInputFooterLeftSide.tsx @@ -58,7 +58,7 @@ const RSS_UPDATE_INTERVAL_MS = 5_000; type RssState = { text: string; level: 'normal' | 'warning' | 'error' }; -function useRssDisplay(): RssState | null { +function useRssDisplay(isLoading: boolean): RssState | null { const [state, setState] = useState(null); useEffect(() => { function update(): void { @@ -68,9 +68,10 @@ function useRssDisplay(): RssState | null { setState(prev => (prev?.text === text ? prev : { text, level })); } update(); + if (!isLoading) return; const timer = setInterval(update, RSS_UPDATE_INTERVAL_MS); return () => clearInterval(timer); - }, []); + }, [isLoading]); return state; } @@ -279,7 +280,7 @@ function ModeIndicator({ } }, [voiceEnabled, voiceHintUnderCap]); const isKillAgentsConfirmShowing = useAppState(s => s.notifications.current?.key === 'kill-agents-confirm'); - const rssState = useRssDisplay(); + const rssState = useRssDisplay(isLoading); // Derive team info from teamContext (no filesystem I/O needed) // Match the same logic as TeamStatus to avoid trailing separator diff --git a/src/utils/__tests__/tasks.test.ts b/src/utils/__tests__/tasks.test.ts index 2895afac6..43493133d 100644 --- a/src/utils/__tests__/tasks.test.ts +++ b/src/utils/__tests__/tasks.test.ts @@ -46,6 +46,7 @@ import { setLeaderTeamName, clearLeaderTeamName, isTodoV2Enabled, + compareTaskIds, type Task, } from '../tasks' @@ -349,6 +350,11 @@ describe('listTasks', () => { const subjects = tasks.map(t => t.subject).sort() expect(subjects).toEqual(['A', 'B']) }) + + test('sorts task IDs numerically', () => { + const ids = ['3', '10', '2', '1'] + expect(ids.sort(compareTaskIds)).toEqual(['1', '2', '3', '10']) + }) }) // --------------------------------------------------------------------------- diff --git a/src/utils/taskStateMessage.ts b/src/utils/taskStateMessage.ts index 95ad686c9..977f00b0d 100644 --- a/src/utils/taskStateMessage.ts +++ b/src/utils/taskStateMessage.ts @@ -1,6 +1,7 @@ import { randomUUID } from 'crypto' import type { SDKMessage } from 'src/entrypoints/agentSdkTypes.js' import type { Task } from './tasks.js' +import { compareTaskIds } from './tasks.js' export type TaskStateItem = Pick< Task, @@ -37,7 +38,7 @@ function toTaskStateItem(task: Task): TaskStateItem { } function compareTaskStateItems(a: TaskStateItem, b: TaskStateItem): number { - return a.id.localeCompare(b.id) + return compareTaskIds(a.id, b.id) } export function buildTaskStateSnapshot( diff --git a/src/utils/tasks.ts b/src/utils/tasks.ts index 90e50e72f..ebab867a7 100644 --- a/src/utils/tasks.ts +++ b/src/utils/tasks.ts @@ -230,6 +230,18 @@ export function getTaskPath(taskListId: string, taskId: string): string { return join(getTasksDir(taskListId), `${sanitizePathComponent(taskId)}.json`) } +export function compareTaskIds(a: string, b: string): number { + const aIsNumeric = /^\d+$/.test(a) + const bIsNumeric = /^\d+$/.test(b) + if (aIsNumeric && bIsNumeric) { + return Number(a) - Number(b) + } + if (aIsNumeric !== bIsNumeric) { + return aIsNumeric ? -1 : 1 + } + return a.localeCompare(b, undefined, { numeric: true }) +} + export async function ensureTasksDir(taskListId: string): Promise { const dir = getTasksDir(taskListId) try { @@ -451,6 +463,7 @@ export async function listTasks(taskListId: string): Promise { const taskIds = files .filter(f => f.endsWith('.json')) .map(f => f.replace('.json', '')) + .sort(compareTaskIds) const results = await Promise.all(taskIds.map(id => getTask(taskListId, id))) return results.filter((t): t is Task => t !== null) }