Merge pull request #80 from IronRookieCoder/fix/issues-31-32

fix: 修复任务列表数字ID乱序和内存轮询导致滚动跳动
This commit is contained in:
geroge 2026-05-13 19:37:25 +08:00 committed by GitHub
commit d3d0dd25d2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 26 additions and 5 deletions

View File

@ -69,7 +69,7 @@ export const TaskCreateTool = buildTool({
return isTodoV2Enabled() return isTodoV2Enabled()
}, },
isConcurrencySafe() { isConcurrencySafe() {
return true return false
}, },
toAutoClassifierInput(input) { toAutoClassifierInput(input) {
return input.subject return input.subject

View File

@ -58,7 +58,7 @@ const RSS_UPDATE_INTERVAL_MS = 5_000;
type RssState = { text: string; level: 'normal' | 'warning' | 'error' }; type RssState = { text: string; level: 'normal' | 'warning' | 'error' };
function useRssDisplay(): RssState | null { function useRssDisplay(isLoading: boolean): RssState | null {
const [state, setState] = useState<RssState | null>(null); const [state, setState] = useState<RssState | null>(null);
useEffect(() => { useEffect(() => {
function update(): void { function update(): void {
@ -68,9 +68,10 @@ function useRssDisplay(): RssState | null {
setState(prev => (prev?.text === text ? prev : { text, level })); setState(prev => (prev?.text === text ? prev : { text, level }));
} }
update(); update();
if (!isLoading) return;
const timer = setInterval(update, RSS_UPDATE_INTERVAL_MS); const timer = setInterval(update, RSS_UPDATE_INTERVAL_MS);
return () => clearInterval(timer); return () => clearInterval(timer);
}, []); }, [isLoading]);
return state; return state;
} }
@ -279,7 +280,7 @@ function ModeIndicator({
} }
}, [voiceEnabled, voiceHintUnderCap]); }, [voiceEnabled, voiceHintUnderCap]);
const isKillAgentsConfirmShowing = useAppState(s => s.notifications.current?.key === 'kill-agents-confirm'); 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) // Derive team info from teamContext (no filesystem I/O needed)
// Match the same logic as TeamStatus to avoid trailing separator // Match the same logic as TeamStatus to avoid trailing separator

View File

@ -46,6 +46,7 @@ import {
setLeaderTeamName, setLeaderTeamName,
clearLeaderTeamName, clearLeaderTeamName,
isTodoV2Enabled, isTodoV2Enabled,
compareTaskIds,
type Task, type Task,
} from '../tasks' } from '../tasks'
@ -349,6 +350,11 @@ describe('listTasks', () => {
const subjects = tasks.map(t => t.subject).sort() const subjects = tasks.map(t => t.subject).sort()
expect(subjects).toEqual(['A', 'B']) 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'])
})
}) })
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------

View File

@ -1,6 +1,7 @@
import { randomUUID } from 'crypto' import { randomUUID } from 'crypto'
import type { SDKMessage } from 'src/entrypoints/agentSdkTypes.js' import type { SDKMessage } from 'src/entrypoints/agentSdkTypes.js'
import type { Task } from './tasks.js' import type { Task } from './tasks.js'
import { compareTaskIds } from './tasks.js'
export type TaskStateItem = Pick< export type TaskStateItem = Pick<
Task, Task,
@ -37,7 +38,7 @@ function toTaskStateItem(task: Task): TaskStateItem {
} }
function compareTaskStateItems(a: TaskStateItem, b: TaskStateItem): number { function compareTaskStateItems(a: TaskStateItem, b: TaskStateItem): number {
return a.id.localeCompare(b.id) return compareTaskIds(a.id, b.id)
} }
export function buildTaskStateSnapshot( export function buildTaskStateSnapshot(

View File

@ -230,6 +230,18 @@ export function getTaskPath(taskListId: string, taskId: string): string {
return join(getTasksDir(taskListId), `${sanitizePathComponent(taskId)}.json`) 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<void> { export async function ensureTasksDir(taskListId: string): Promise<void> {
const dir = getTasksDir(taskListId) const dir = getTasksDir(taskListId)
try { try {
@ -451,6 +463,7 @@ export async function listTasks(taskListId: string): Promise<Task[]> {
const taskIds = files const taskIds = files
.filter(f => f.endsWith('.json')) .filter(f => f.endsWith('.json'))
.map(f => f.replace('.json', '')) .map(f => f.replace('.json', ''))
.sort(compareTaskIds)
const results = await Promise.all(taskIds.map(id => getTask(taskListId, id))) const results = await Promise.all(taskIds.map(id => getTask(taskListId, id)))
return results.filter((t): t is Task => t !== null) return results.filter((t): t is Task => t !== null)
} }