Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions desktop/frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
"dev": "vite",
"build": "tsc --noEmit && vite build",
"preview": "vite preview",
"test:todo-visibility": "node scripts/test-todo-visibility.mjs",
"typecheck": "tsc --noEmit"
},
"dependencies": {
Expand Down
57 changes: 57 additions & 0 deletions desktop/frontend/scripts/test-todo-visibility.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import path from "node:path";
import { performance } from "node:perf_hooks";
import { fileURLToPath } from "node:url";
import ts from "typescript";

const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
const sourcePath = path.join(root, "src", "lib", "todoVisibility.ts");
const source = readFileSync(sourcePath, "utf8");
const transpiled = ts.transpileModule(source, {
compilerOptions: {
module: ts.ModuleKind.ES2022,
target: ts.ScriptTarget.ES2022,
},
}).outputText;

const moduleUrl = `data:text/javascript;base64,${Buffer.from(transpiled).toString("base64")}`;
const { shouldShowTodoPanel } = await import(moduleUrl);

const completedTodos = [
{ content: "Inspect the report", status: "completed" },
{ content: "Ship the fix", status: "completed" },
];

assert.equal(
shouldShowTodoPanel("todo-final", null, completedTodos),
true,
"the final all-completed todo_write must remain visible",
);
assert.equal(
shouldShowTodoPanel("todo-active", null, [{ content: "Run tests", status: "in_progress" }]),
true,
"an active todo_write remains visible",
);
assert.equal(
shouldShowTodoPanel("todo-final", "todo-final", completedTodos),
false,
"a user dismissal still hides that exact todo list",
);
assert.equal(shouldShowTodoPanel(null, null, completedTodos), false, "no canonical todo item means no panel");
assert.equal(shouldShowTodoPanel("todo-empty", null, []), false, "empty todo lists do not render a panel");

const iterations = 200_000;
const started = performance.now();
for (let i = 0; i < iterations; i += 1) {
if (!shouldShowTodoPanel("todo-perf", null, completedTodos)) {
throw new Error("unexpected hidden todo panel during performance loop");
}
}
const elapsed = performance.now() - started;
const perCallUs = (elapsed * 1000) / iterations;

assert.ok(elapsed < 500, `todo visibility check is too slow: ${elapsed.toFixed(2)} ms`);
console.log(
`todo visibility checks: ${iterations} calls in ${elapsed.toFixed(2)} ms (${perCallUs.toFixed(3)} us/call)`,
);
15 changes: 6 additions & 9 deletions desktop/frontend/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import { WorkspacePanel } from "./components/WorkspacePanel";
import { Tooltip } from "./components/Tooltip";
import { OnboardingOverlay } from "./components/OnboardingOverlay";
import { parseTodos } from "./lib/tools";
import { shouldShowTodoPanel } from "./lib/todoVisibility";
import { sessionActivityTime } from "./lib/session";
import type { ComposerInsertRequest, MemoryView, Mode, SessionMeta } from "./lib/types";
import { loadLayoutSize, saveLayoutSize } from "./lib/layoutPreferences";
Expand Down Expand Up @@ -238,10 +239,10 @@ export default function App() {

// The live task list pinned above the composer comes from the most recent
// successful top-level todo_write result; failed or still-running attempts do
// not advance the canonical panel state. It stays visible while work remains,
// clears itself once every item is completed, and can be dismissed by the user
// (the ✕). A dismissal is keyed to that list's id, so a fresh accepted
// todo_write brings the panel back.
// not advance the canonical panel state. It stays visible through the final
// all-completed update, and can be dismissed by the user (the ✕). A dismissal
// is keyed to that list's id, so a fresh accepted todo_write brings the panel
// back.
const todoItem = useMemo(() => {
for (let i = state.items.length - 1; i >= 0; i--) {
const it = state.items[i];
Expand All @@ -251,11 +252,7 @@ export default function App() {
}, [state.items]);
const todos = useMemo(() => (todoItem ? parseTodos(todoItem.args) : []), [todoItem]);
const [dismissedTodo, setDismissedTodo] = useState<string | null>(null);
const showTodos =
!!todoItem &&
todoItem.id !== dismissedTodo &&
todos.length > 0 &&
todos.some((t) => t.status !== "completed");
const showTodos = shouldShowTodoPanel(todoItem?.id, dismissedTodo, todos);

useEffect(() => {
if (!pendingPlanRevision || state.running) return;
Expand Down
9 changes: 9 additions & 0 deletions desktop/frontend/src/lib/todoVisibility.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import type { Todo } from "./tools";

export function shouldShowTodoPanel(
todoId: string | null | undefined,
dismissedTodoId: string | null,
todos: Todo[],
): boolean {
return !!todoId && todoId !== dismissedTodoId && todos.length > 0;
}
Loading