feat: task persistence, memoized UI components, hook/delete-failure safety

locode-authored (dogfooded) changes:
- TaskStore.toJSON/fromJSON: task_create/list state now survives session
  resume instead of resetting to empty (session.ts, task.ts, sessionStore.ts).
- StatusBar/ChatInput/ThinkingIndicator wrapped in memo() to skip re-renders
  during the ~30fps streaming-text update loop.
- HistoryItemView's todos list keys off t.content instead of array index
  (TodoItem has no id field; content is the best stable key available).
- sessionStore.ts read paths now log (console.warn) on corrupt/unreadable
  files instead of silently swallowing the error.

Fixes on top, found in review:
- safeRunHooksForEvent() wraps the 4 remaining unprotected hook call sites
  (PreToolUse, PermissionRequest, PostToolUse, SubagentStart) — these fire on
  every tool call/sub-agent, far more often than the 3 sites already wrapped
  (Stop/SessionStart/UserPromptSubmit), so a broken hook script was still able
  to abort an in-flight turn.
- deleteSession() no longer reports success when unlinkSync fails for a
  reason other than ENOENT (e.g. EPERM/EBUSY from a Windows file lock) — it
  used to drop the index entry and return true anyway, orphaning the file on
  disk with no way to reference it again. `sessions rm` in cli.ts now
  distinguishes "not found" from "found but couldn't delete" in its message.
- ChatInput's memo() was a no-op: onSubmit={handleSubmit} passed a fresh
  closure every render since handleSubmit isn't wrapped in useCallback.
  Rather than force a ~300-line function (which reads isThinking/streamingText
  — both changing every streaming frame) into a dependency array, added a
  handleSubmitRef + stable wrapper, the same ref-indirection this file
  already uses for phaseRef.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LxiAaGhSD4DRVYYQZ5GJjm
This commit is contained in:
kim
2026-08-24 15:02:34 +09:00
co-authored by Claude Sonnet 5
parent 5bf60c7921
commit c649fdf6ed
12 changed files with 193 additions and 47 deletions
+6 -1
View File
@@ -9,7 +9,7 @@ import { configFilePath, loadStoredConfig, saveStoredConfig, type StoredConfig }
import { loadMergedHooks, userHooksFilePath } from "./hooks/config.js";
import { loadMergedServers, removeUserServer, saveUserServer, userMcpFilePath } from "./mcp/config.js";
import { isHttpServerConfig } from "./mcp/types.js";
import { deleteSession, listSessions, mostRecentSessionId, sessionsDir } from "./persistence/sessionStore.js";
import { deleteSession, listSessions, loadSession, mostRecentSessionId, sessionsDir } from "./persistence/sessionStore.js";
import { addInstalledPlugin, loadInstalledPlugins, pluginsDir, removeInstalledPlugin } from "./plugins/config.js";
import { loadPlugin } from "./plugins/loader.js";
import { runInkApp } from "./ui/ink/index.js";
@@ -230,6 +230,11 @@ sessionsCmd
.action((id: string) => {
if (deleteSession(id)) {
console.log(`Deleted session ${id}.`);
} else if (loadSession(id)) {
// The file exists but deleteSession() couldn't actually remove it (e.g. locked by another
// process) — a different situation from "no such session", so say so distinctly.
console.error(`Could not delete session "${id}" — the file may be in use by another process.`);
process.exit(1);
} else {
console.error(`No saved session found with id "${id}".`);
process.exit(1);