Skip to content

fix(crx): persist GIF recording frames + bridge screenRecorder - #3

Closed
Postroggy wants to merge 102 commits into
mainfrom
fix/gif-frame-storage-persistence
Closed

fix(crx): persist GIF recording frames + bridge screenRecorder#3
Postroggy wants to merge 102 commits into
mainfrom
fix/gif-frame-storage-persistence

Conversation

@Postroggy

Copy link
Copy Markdown
Owner

Summary

Closes architectural problem C in docs/architectural-improvements-todo.md.

Two related bugs in the GIF recording pipeline:

1. gifFrameStorage lost on SW restart

The gifFrameStorage.storage Map<groupId, {frames, lastUpdated}> and recordingGroups Set are pure in-memory state. After a 30-second SW idle kill + restart, a 5-minute recording the user was in the middle of disappears with no recovery path. recordingGroups also resets, so even if frames were recovered, subsequent calls to gif_creator wouldn't continue recording.

Fix: Persist both to chrome.storage.local via write-through. Public API stays synchronous and reads always come from the in-memory Map. restoreGifFrameStorageFromStorage() is called from service-worker.ts on chrome.runtime.onStartup, hydrating both the frame map and the recordingGroups set with shape-tolerance for missing / wrong-shape payloads.

2. screenRecorder is a no-op stub

The screenRecorder exported from shared.ts has isRecording: () => false and addFrame: () => void. core.ts#recordToolAction (the function that runs after every computer and navigate tool call) calls these — which means it never actually records frames during normal tool execution. The only path that ever captured frames was the explicit gif_creator start_recording pathway.

Fix: Delete the stub entirely. Replace the 5 callers in core.ts#recordToolAction with direct gifFrameStorage calls so the recording pathway is actually wired up. GifFrameData shape extended with optional frameNumber + timestamp for compatibility with the existing recordToolAction shape (fields ignored by storage / restore).

What changed

  • 2 production files modified: mcpRuntime/mediaTools.ts, mcpRuntime/shared.ts
  • 1 new test file: gifFrameStorage.persistence.test.ts (13 tests)
  • 13 new storage contract tests: single/multi-group payloads, recording-set round-trips, shape tolerance, integer-boundary groupIds, missing-payload path

Verification

  • typecheck: 9 errors (unchanged from baseline, all in ContentBlocksRenderer.tsx — unrelated)
  • unit tests: 181/181 passed (+13 new, was 150/150)
  • build: clean
  • lint: 51 errors / 176 warnings (unchanged from baseline)

🤖 Generated with Claude Code

jh0904 and others added 30 commits May 21, 2026 21:17
* fix(crx): fix language submenu clipped when sidebar is narrow

Replace the fly-out submenu (absolute right-full) with an inline
accordion that expands below the Language row.  The previous
positioning overflowed the left edge of the side-panel viewport
whenever the sidebar width was too small, making the list invisible.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* a11y(crx): add aria-expanded and aria-controls to language menu toggle

Addresses Copilot review suggestion on PR superduck-ai#102: the language disclosure
button now exposes its expanded/collapsed state to assistive technology
via aria-expanded, and references the submenu container via aria-controls.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: yueqi.guo <guo_yueqi@qq.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(crx): fix modal backdrop opaque black and close-on-drag-out bugs

Two bugs in the shared Modal component:

1. Backdrop was fully opaque black because bg-always-black (solid) and
   the [background-color:hsl(...)/0.5] arbitrary class were both applied
   and the solid class was winning. Remove bg-always-black so only the
   semi-transparent arbitrary value applies.

2. Modal closed when the user started a drag inside (e.g. selecting
   text in an input) and released the mouse on the backdrop. Replaced
   the onClick-only guard with an onMouseDown tracker: onClose() now
   fires only when both mousedown and mouseup land on the overlay.

Also remove a stale eslint-disable comment referencing the uninstalled
react-hooks plugin, which was causing lint-staged to fail on this file.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(crx): address Copilot review — use onPointerDownCapture for overlay close guard

Two issues raised in code review:

1. onMouseDown fires in the bubble phase, so child elements that call
   stopPropagation() (e.g. SimpleSelect option buttons) prevent the
   overlay from recording the pointer-down target, making the stale ref
   cause incorrect close behaviour. Switch to onPointerDownCapture so
   the overlay always records the originating target regardless of
   stopPropagation in descendants.

2. onMouseDown does not fire for touch or pen input. onPointerDownCapture
   covers mouse, touch, and stylus uniformly.

Also reset overlayMouseDownTarget to null after each onClick evaluation
to prevent stale values from affecting subsequent interactions.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: yueqi.guo <guo_yueqi@qq.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
…tHog flow (superduck-ai#106)

* fix(analytics): unify distinct_id across extension and cli

Co-authored-by: Claude <noreply@anthropic.com>

* fix(crx): 存量用户迁移到 native-host 统一 analytics ID

已有用户的 chrome.storage.local 中存有旧的 randomUUID 格式 anonymousId,
更新扩展后不会自动替换为 native-host 的 anon-* ID。

增加迁移逻辑:如果现有 ID 不是 anon-* 格式,尝试从 native host 获取统一 ID
并覆盖旧值,确保存量用户也能与 CLI 共享同一个 distinct_id。

* fix(analytics): 修复 PR superduck-ai#106 评审问题——import 缺失、native probe 无缓存、禁用时仍写磁盘

1. mcpRuntime/analytics.ts: 恢复 FeatureFlagManager 所需的 import(getConfig、
   getStorageValue、removeStorageValues、StorageKeys),修复编译错误。
2. extensionServices/analytics.ts: 为 getNativeHostAnalyticsId 添加模块级缓存,
   避免 native host 不可用时每次事件发射都触发 3s 超时。
3. posthog.go GetOrCreateDistinctID: 尊重 SUPERDUCK_ANALYTICS_DISABLED / CI
   环境变量,禁用时返回空字符串而不创建持久化文件。

* fix(analytics): 修复 PR superduck-ai#106 第二轮评审问题

1. extensionServices/analytics.ts: fallback UUID 加 `anon-` 前缀,避免每次
   重启都误判为旧 ID 触发迁移逻辑;只缓存成功的 native host 探测结果,
   失败不缓存,允许后装 native host 后重新探测。
2. mcpRuntime/analytics.ts: initializeAnalytics 改为 fire-and-forget,不阻塞
   UI 渲染;FeatureFlagManager.fetchAndStore 在 token 缺失时清除 initPromise,
   允许登录后重新拉取 feature flags。
3. native-host/main.go: 日志文件权限从 0644 改为 0600,防止共享机器上其他
   用户读取 debug 日志中的 distinct_id。
4. AGENTS.md: 修复分号后缺少空格的格式问题。

* fix(analytics): 修复 PR superduck-ai#106 第三轮评审问题

1. extensionServices/analytics.ts: 迁移逻辑改为基于 nativeIdCache 判断,
   不再依赖 `anon-` 前缀,确保后装 native host 时能正确迁移到共享 ID。
2. mcpRuntime/analytics.ts: 401 响应时同步清除 initPromise 和 features,
   避免重新登录后 feature flags 卡在默认值。
3. 将 .github/AGENT_SKILLS/issue-fill.md 和 .factory/skills/issue-fill/SKILL.md
   纳入版本控制,修复 AGENTS.md 中的断链。

* fix(analytics): 并发去重、unhandled rejection、feature flags 重试

1. extensionServices/analytics.ts: getOrCreateAnonymousId 加 in-flight
   去重,并发调用共享同一个 Promise,避免生成多个不同 distinct_id。
2. mcpRuntime/analytics.ts: initializeAnalytics 的 fire-and-forget 加
   .catch(() => {}) 吞掉错误,防止 MV3 SW unhandled rejection。
3. mcpRuntime/analytics.ts: fetchAndStore 在非 200 响应时也清除
   initPromise,允许后续重试而非永久返回默认值。

---------

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Claude <noreply@anthropic.com>
…#109)

* fix(native-host): preserve active cli socket

Co-authored-by: Codex <noreply@openai.com>

* feat(analytics): 统一三端 analytics ID 为 sdid-* 格式,同步扩展与 native-host 身份

- 新增 EnsureInstallID / ConfirmInstallID / AdoptInstallID 机制,确保 CLI、native-host、扩展共享同一个 distinct_id
- CLI 设置 RequireConfirmedID,在扩展与 native-host 完成 ID 同步前不上报,防止 split identity
- 扩展连接 native-host 时通过 sync_analytics_id / get_analytics_id 协议同步 ID
- 自动迁移旧的 anon-* / sdext-* 格式 ID 为 sdid-*
- PostHog 事件按来源区分 $lib(superduck-cli / superduck-mcp / superduck-sidepanel / superduck-bridge / superduck-extension)
- npm wrapper 增加 ensureAnalyticsId 预创建 analytics-id 文件
- 更新 allowed_origins 为新的 extension ID
- 版本升至 0.2.6

* feat(cli): 新增 superduck update 命令和后台版本检查

- 新增 internal/selfupdate 包:semver 解析、npm registry 查询、缓存管理、自更新逻辑
- superduck update: 自动检测安装方式(npm/直接下载),执行对应更新流程
- superduck update --check: 仅检查是否有新版本
- 后台版本检查:每 24h 查询 npm registry,命令执行后非阻塞提示有新版本可用
- 支持 GitHub Release tarball 下载 + 原子替换二进制

* fix: 修复 PR review 指出的问题

- native-host: ID 未确认时降级为警告而非拒绝请求,兼容旧版扩展
- analytics.ts: 先注册 storage listener 再读取,消除竞态窗口
- cmd_update: 使用实际安装版本更新缓存和 telemetry
- selfupdate: tar 解压增加 TypeReg 检查,跳过目录/symlink
- selfupdate_test: TestDetectInstallMethodNPM 实际测试路径检测逻辑

* fix: 修复 PR review 第二轮问题

- native-host: ID sync 等待改为 sync.Once,只在首次请求等待一次,后续不阻塞
- cmd_update: UpdateViaNPM 返回空时 fallback 到已知 latest 版本
- selfupdate: UpdateViaNPM 失败时不再返回字面 "latest" 字符串
- mcp-server: 日志文件权限收紧为 0600

---------

Co-authored-by: Codex <noreply@openai.com>
- README 新增 SuperDuck vs Claude for Chrome 对比表、CLI 命令示例、架构图
- 中文 README 同步更新,修正旧仓库名
- 官网能力列表新增 CLI + MCP server
- 终端演示修正不存在的 fetch 命令
- 架构图从 ASCII art 改为 SVG 绘制
- 版本号更新为 v0.2.5

Co-authored-by: Claude <noreply@anthropic.com>
* feat(crx): agent indicator i18n 实时切换及英文默认值兜底

监听 chrome.storage.onChanged,用户切换语言后 indicator 文案实时
刷新,无需重载页面;同时新增 DEFAULT_I18N_MESSAGES 英文兜底,
避免 JSON 加载失败时显示空白。

* fix(crx): 防止快速切换语言时旧请求覆盖新翻译

加入 i18nLoadVersion 版本守卫,每次 await 后校验版本号,
确保只有最新一次 loadI18n 调用能写入状态。
Remove system.display and declarativeNetRequestWithHostAccess from the MV3 manifest after Chrome Web Store review flagged both as unused.

Co-authored-by: Codex <noreply@openai.com>
* fix(crx): 支持模型配置 API URL 输入纯域名并校验非法输入

- 裸域名自动补全 https://,完整 URL 与 endpoint 后缀裁剪保持兼容
- 新增 isValidProviderBaseURL,非法输入返回空字符串且阻止保存
- 添加/编辑模型弹窗展示错误提示,非法 URL 时不请求模型列表

Fixes superduck-ai#112

Co-authored-by: Cursor <noreply@cursor.com>

* fix(crx): address PR review on provider base URL validation

- Keep invalid API URL on blur instead of clearing to empty string
- Reject URLs with embedded credentials in parseProviderBaseURLInput
- Guard handleSubmit when base URL is invalid

Co-authored-by: Cursor <noreply@cursor.com>

* fix(crx): allow single-label hostnames with explicit http(s) scheme

Docker/local gateway URLs like http://ollama:11434/v1 are valid when the
user provides a scheme; bare single-label names still require a dotted domain.

Co-authored-by: Cursor <noreply@cursor.com>

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cursor <noreply@cursor.com>
* fix(crx): align permission mode menu typography with composer UI

Shrink the side panel permission mode dropdown to match the 11px trigger
button and other compact menus (narrower width, smaller icons/text,
tighter padding). Remove leftover debug logging on the menu toggle.

Fixes superduck-ai#165

Co-authored-by: Cursor <noreply@cursor.com>

* fix(crx): keep permission mode menu descriptions on one line

Remove max-width cap and apply whitespace-nowrap so Chinese descriptions
do not wrap mid-sentence in the dropdown.

Fixes superduck-ai#165

Co-authored-by: Cursor <noreply@cursor.com>

* refactor(crx): extract PermissionModeMenu with docstrings

Move the composer permission mode dropdown into a documented component
so CodeRabbit docstring coverage passes. Cap menu width at 280px and
truncate overflow while keeping single-line labels.

Fixes superduck-ai#165

Co-authored-by: Cursor <noreply@cursor.com>

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cursor <noreply@cursor.com>
* fix(crx): support OpenAI Responses GPT gateways

Co-authored-by: Codex <noreply@openai.com>

* docs: relax PR issue linkage rules

Co-authored-by: Codex <noreply@openai.com>

* docs: default PRs to ready for review

Co-authored-by: Codex <noreply@openai.com>

* fix(crx): tighten OpenAI Responses id handling

Co-authored-by: Codex <noreply@openai.com>

* test(crx): align OpenAI mock constant naming

Co-authored-by: Codex <noreply@openai.com>

---------

Co-authored-by: Codex <noreply@openai.com>
…ai#167)

DatePicker serialized selected dates with toISOString(), which can
shift YYYY-MM-DD by one day for non-UTC timezones. Use local calendar
components for serialize/parse and align today comparisons.

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cursor <noreply@cursor.com>
…ck-ai#172)

* feat(crx): keep agent cursor visible in tool-use screenshots

When HIDE_FOR_TOOL_USE runs for screenshot/computer tools, hide glow,
ripple, stop bar, and blocking overlay but leave the proxy cursor in
the DOM so Page.captureScreenshot can show where the agent is pointing.

Closes superduck-ai#168

Co-authored-by: Cursor <noreply@cursor.com>

* fix(crx): address review feedback on tool-use indicator hide

Use display:none for blocking overlay instead of detaching it, null the
overlay reference on full hide even when already detached, and re-hide
interruptive UI after async showAgentIndicators completes if a screenshot
started during i18n load.

Co-authored-by: Cursor <noreply@cursor.com>

* fix(crx): hide interruptive UI before i18n when tool-use hidden

When showAgentIndicators runs while isHiddenForToolUse is already true,
create glow/ripple/blocking/stop with display:none before awaiting i18n
so screenshots cannot capture decorations in the pre-await window.

docs: add PR review thread closure guidance to AGENTS.md

Co-authored-by: Cursor <noreply@cursor.com>

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cursor <noreply@cursor.com>
- Generate per-session auth token (crypto/rand, 256-bit) written to
  ~/.superduck/uds-token (mode 0600) at native-host startup
- Require UDS clients to authenticate before processing tool requests
- Both CLI client and MCP bridge authenticate automatically
- Restrict UDS socket permissions to 0700 (owner-only)
- Cap concurrent UDS connections at 16 to prevent resource exhaustion
- Remove silent duration ms→s conversion in normalizeArgs, validate only
- Tighten audit.log file permissions from 0644 to 0600

Fixes: UDS unauthenticated access, audit log world-readable,
connection exhaustion, duration conversion masking invalid input

Co-authored-by: Codex <noreply@openai.com>
- cmd_wait: Add duration range validation (0-30 seconds)
- cmd_resize: Add width/height range validation (1-7680, 1-4320 pixels)
- cmd_scroll: Add direction enum validation and amount range check (1-100)
- cmd_key: Add repeat range validation (1-100)
- flags: Support --tab=N syntax in addition to --tab N

Co-authored-by: Codex <noreply@openai.com>
- Extract handleImageCapture helper to cmd_computer.go
- Replaces ~40 lines of duplicate base64 decode, file write, JSON output logic
- cmd_screenshot and cmd_zoom now both call handleImageCapture
- Future bug fixes to image handling only need to be made in one place

Co-authored-by: Codex <noreply@openai.com>
- Download and verify .sha256 checksum file before extracting
- Read entire tarball into memory to verify before extraction
- Reject updates with mismatched checksums
- Protects against MITM attacks and corrupted downloads

Co-authored-by: Codex <noreply@openai.com>
- Add nil check in ToMCPContent to return empty TextContent
- Improve buildCallToolResult to handle []interface{} results
- Ensures MCP clients always receive valid Content even for edge cases

Co-authored-by: Codex <noreply@openai.com>
Previously, 'superduck type --help' would treat '--help' as text to type
into the focused element. Now it uses flag.NewFlagSet to properly parse
flags and display help when requested.

Fixes: 维度二#5 - cmd_type 不支持 --help

Co-authored-by: Codex <noreply@openai.com>
Previously, read_page accepted any string value for --filter without
validation. Now it explicitly checks that the value is either 'interactive'
or 'all', returning a clear error message for invalid values.

Fixes: 维度五superduck-ai#6 - read_page --filter 不校验枚举值

Co-authored-by: Codex <noreply@openai.com>
Previously, cmdLog with --tail N would read the entire file into memory
using a ring buffer, which is inefficient for large audit logs. Now:
- Use reverse reading strategy: seek to end and read backwards
- Read in 8KB chunks from file end
- Only read enough data to collect N lines
- Dramatically reduces memory usage and I/O for large files

Fixes: 维度七#1 - cmdLog tail 性能优化

Co-authored-by: Codex <noreply@openai.com>
Previously, ReadMessage allocated a new byte slice for every message,
causing high GC pressure in long-running processes like native-host. Now:
- Use sync.Pool to reuse byte buffers across message reads
- Start with 64KB default buffer size for most messages
- Automatically grow buffer when larger messages are encountered
- Return buffers to pool after use
- Significantly reduces memory allocations in high-throughput scenarios

Fixes: 维度七#2 - ReadMessage buffer 复用

Co-authored-by: Codex <noreply@openai.com>
Previously, BackgroundCheck spawned a goroutine that could not be cancelled,
potentially blocking process exit if the HTTP request hung. Now:
- Return CancelFunc alongside the channel for explicit cancellation
- Use context.WithTimeout for HTTP requests (5s timeout)
- Support context cancellation throughout the check flow
- Caller can cancel with defer cancelUpdate() to prevent goroutine leaks

Fixes: 维度七#5 - BackgroundCheck goroutine 无法取消

Co-authored-by: Codex <noreply@openai.com>
- InvalidToken: wrong token rejected
- MalformedJSON: malformed JSON rejected
- MissingType: missing type field rejected
- WrongType: wrong type value rejected
- ValidToken: correct token accepted
- ClientDisconnects: client disconnect handled
- EmptyMessage: empty message rejected
- Integration: full end-to-end auth flow

Co-authored-by: Codex <noreply@openai.com>
- Remove 0-30s duration limit from cmd_wait.go (CLI should not enforce MCP schema constraints)
- Remove --tab= syntax support from flags.go (UX improvement, not a bug fix)
Only validate that dimensions are positive numbers. Let Chrome handle
its own window size constraints rather than imposing CLI-level limits
that may not match Chrome's actual capabilities.

This follows the same principle as the wait command fix: CLI should not
enforce MCP schema constraints on end users who may have legitimate use
cases for larger values (multi-monitor setups, virtual displays, etc.).
Postroggy and others added 27 commits June 6, 2026 11:52
* fix: improve Edge and Brave browser compatibility

- cmd_doctor: check Chrome, Edge, and Brave manifest paths (not just Chrome)
- install.sh: install native messaging manifest for all supported browsers
- Extension: add edge:// and brave:// to URL validation blocklists
- Update error messages to mention all supported browsers

This ensures superduck works correctly when installed in Edge or Brave
browsers, not just Chrome.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix: address PR superduck-ai#204 review feedback

- Restore accidentally deleted chrome-native-host/superduck symlink
- Make error message browser-agnostic ('in your browser' instead of 'in Chrome')
- Fix cmd_doctor.go: skip manifest check on unsupported OS instead of false failure
- Add brave://extensions/ to install.sh instructions
- Replace 'Chrome Native Host' with browser-agnostic wording in install.sh

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: yueqi.guo <guo_yueqi@qq.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
…ent hook (superduck-ai#205)

* refactor(crx): extract provider client management into useProviderClient hook

Move provider client lifecycle (MessagesClient creation, tier-specific fallback,
server model info fetching) from SidepanelApp.tsx into a dedicated useProviderClient
hook in src/sidepanel/provider/. No logic changes — pure extraction.

- Created src/sidepanel/provider/useProviderClient.ts
- Created src/sidepanel/provider/index.ts
- Replaced ~75 lines of state/effects in SidepanelApp.tsx with single hook call

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(crx): derive hasProviderConfig and fix React type import

Address review feedback from PR superduck-ai#205:

- Derive hasProviderConfig from effectiveMessagesClient !== null instead
  of using separate state, fixing a critical bug where direct config
  (apiKey+apiBaseUrl) left hasProviderConfig as false and could trigger
  SetupGate incorrectly.
- Import MutableRefObject type directly from 'react' instead of using
  the React namespace, which is not imported in this module.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: yueqi.guo <guo_yueqi@qq.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Extract all type definitions and interfaces from SidepanelApp.tsx into a
dedicated types.ts file. This reduces SidepanelApp.tsx by ~190 lines
and improves type discoverability.

Extracted types:
- ChatRole, VisibleChatRole, ChatMessage
- PermissionPromptData, PermissionGrantScope
- RuntimeMessage, PairingPromptState, PendingPromptPayload
- BlockedTabInfo, SessionSnapshot, SessionIndexEntry
- ToolUseBlock, ToolInputRecord, Base64ImageSource, Base64ImageBlock
- ToolResultDisplayContent, LightningContentArray, etc.
- MessageGroup, TimelineGroupData, GroupedContentBlock
- StreamingTextStore, AnnouncementConfig
- PERMISSION_ACTION_TYPES constant

Co-authored-by: yueqi.guo <guo_yueqi@qq.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
superduck-ai#208)

* refactor(sidepanel): extract UserMessageRow and StreamingTextBlock components

Extract pure display components from SidepanelApp.tsx into a new
MessageComponents directory:

- UserMessageRow.tsx: User message bubble with image preview, copy,
  expand/collapse, and shortcut chip rendering (~170 lines)
- StreamingTextBlock.tsx: Streaming text renderer using
  useSyncExternalStore (~50 lines)
- index.ts: Barrel exports

Also moved utility functions needed by these components to
sidepanelUtils.ts:
- getTextFromBlockContent
- getBase64ImageBlocks
- isBase64ImageSource
- isBase64ImageBlock

SidepanelApp.tsx reduced by ~240 lines.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* refactor(sidepanel): extract tool display components from SidepanelApp

Extract ~1500 lines of tool display and message rendering components into
MessageComponents/ContentBlocksRenderer.tsx:
- getStringField helper
- PermissionActionButton (shared with InlinePermissionPrompt)
- PlanApprovalModal (plan approval/rejection modal)
- UpdatePlanCell (plan display with portal)
- BrowserToolCell (browser tool display with screenshot thumbnails)
- ToolUseItem (generic tool display with Request/Result badges)
- isTimelineBlock type guard
- ContentBlocksRenderer (splits blocks at turn_answer_start)
- BlockRenderer (dispatches to right renderer per block type)
- AssistantMessageRow (assistant response with copy + feedback)
- MessageList (groups and renders all messages)

Also clean up ~50 unused imports from SidepanelApp.tsx.

SidepanelApp.tsx reduced from 8502 to 6567 lines across PR 1-3.

---------

Co-authored-by: yueqi.guo <guo_yueqi@qq.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
- Break barrel cycle: import StreamingTextBlock and UserMessageRow
  directly from their files instead of ./index to avoid circular
  dependency in MessageComponents
- Fix read-only PlanApprovalModal backdrop click: add stopPropagation
  on modal content wrapper so clicking backdrop triggers handleBackdropClick
  while clicking modal content does not close the modal
- Security: add noopener,noreferrer to window.open() call to prevent
  reverse tabnabbing when opening external URLs
…ponents (superduck-ai#211)

* refactor(sidepanel): extract SidepanelHeader and SidepanelBanners components

Extract two major JSX sub-components from SidepanelApp.tsx into dedicated files:

- SidepanelHeader (265 lines): Model selector dropdown, quick mode toggle,
  clear chat button, and header menu with settings/language/convert options
- SidepanelBanners (265 lines): Error, refusal, message limit, high risk,
  notification, and announcement banners with AnimatePresence, plus the
  ModelFallbackCard

SidepanelApp.tsx reduced from 6557 to 6239 lines (318 lines extracted).

All tests pass (78/78), build succeeds, typecheck clean for new code.

* fix(sidepanel): tighten type safety in extracted JSX components

- SidepanelBanners: Use proper types for announcementConfig
  (AnnouncementConfig), fallbackConfig (ModelFallbackConfig | undefined),
  and modelConfig (ModelsConfigFeatureValue) instead of any
- SidepanelBanners: Narrow activeBanner to string literal union instead of
  generic string
- SidepanelHeader: Use SupportedLocale for SUPPORTED_LOCALES,
  LOCALE_DISPLAY_NAMES, and locale instead of string
- SidepanelHeader: Use minimal IntlShape interface for intl instead of any

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: yueqi.guo <guo_yueqi@qq.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
…Messages hook (superduck-ai#210)

* refactor(sidepanel): extract permission prompt components

Move InlinePermissionPrompt and isPermissionPromptData to a dedicated
PermissionPrompt.tsx file to improve code organization and reduce the
size of SidepanelApp.tsx.

Extracted:
- InlinePermissionPrompt component (~300 lines)
- isPermissionPromptData type guard

SidepanelApp.tsx reduced from ~6567 to ~6239 lines.

* refactor(sidepanel): extract utility functions and type guards

Move utility functions, type guards, and constants to a dedicated
sidepanelGuards.ts file to improve code organization and reduce the
size of SidepanelApp.tsx.

Extracted:
- Type guards: isChatRole, isChatMessage, isApiConversationMessage,
  isSessionSnapshot, isStringRecord
- Utility functions: getLightningScreenshotReminder, normalizeToolResultContent,
  getStreamHeaders, getRuntimeEvaluateValue, normalizeImageMediaType
- Hooks: createStreamingTextStore, usePrefersReducedMotion
- Constants: SESSION_CONVERSATION_MAP_KEY, SESSION_REMOTE_MAP_KEY,
  SESSION_INDEX_KEY, CUSTOM_API_URL_KEY, CUSTOM_API_KEY_KEY

SidepanelApp.tsx reduced from ~6239 to ~6107 lines.

* refactor(sidepanel): extract useLightningMode hook

Move the useLightningMode hook (the largest single extraction at ~1305
lines) to a dedicated file to improve code organization.

Extracted:
- UseLightningModeProps interface
- useLightningMode hook: config management, system prompt building,
  API client initialization, sendMessage main loop (streaming,
  command parsing, execution, screenshots, page settle), cancel,
  clearMessages, clearError, createApiMessage, trackToolCall

SidepanelApp.tsx reduced from ~6107 to ~4753 lines.

* refactor(sidepanel): extract auth and model config hooks

Move authentication and model configuration logic to dedicated hooks
to improve code organization and testability.

Extracted:
- useAuth hook: apiKey, apiBaseUrl, authLoading, authError state
  management, refreshAuth callback, storage change listeners
- useModelConfig hook: selectedModel, modelMapping state,
  storage loading/listening, handleModelChange callback

SidepanelApp.tsx reduced from ~4753 to ~4627 lines.

* refactor(sidepanel): extract session persistence hook

Extract session persistence logic into useSessionPersistence hook:
- upsertSessionIndex helper function
- loadSnapshotForSession callback
- restoreSnapshotFromRemoteSession callback
- Session-loading effect (activeSessionId change)
- Session persistence effect (debounced 2000ms writes)

SidepanelApp.tsx reduced from 4627 to 4339 lines.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* refactor(sidepanel): extract agent loop into useAgentLoop hook

Extract the main agent loop logic into useAgentLoop hook:
- sendPrompt function (message sending + tool execution loop)
- compactConversation callback
- sendCompletionNotification callback
- generateStatusSummary callback
- generateConversationTitle callback

SidepanelApp.tsx reduced from 4339 to 3558 lines (~780 lines extracted).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* refactor(sidepanel): extract runtime message listener into useRuntimeMessages hook

Extract the Chrome runtime message listener logic from SidepanelApp.tsx
into a dedicated useRuntimeMessages hook. This includes:

- PANEL_OPENED/PANEL_CLOSED messaging effects
- Visibility change listener
- shouldHandleTaskForCurrentContext callback
- Main runtime.onMessage listener handling:
  - PING_SIDEPANEL
  - show_pairing_prompt
  - MAIN_TAB_ACK_REQUEST
  - POPULATE_INPUT_TEXT (with attachment decoding and auto-send)
  - LOAD_CONVERSATION (with session map lookups)
  - EXECUTE_TASK (with context filtering)
  - STOP_AGENT (with abort controller)

SidepanelApp.tsx reduced from 3558 to 3289 lines (~270 lines extracted).
All 78 tests pass, build succeeds.

* fix(sidepanel): address bot review issues in useRuntimeMessages

Fix two issues identified by CodeRabbit and ChatGPT Codex:

1. Clear timeout on effect unmount: Store the setTimeout ID and clear it
   in the cleanup function to prevent state updates after unmount.

2. Fix LOAD_CONVERSATION race condition: Move sendResponse inside the
   async IIFE and return true to indicate async response, ensuring the
   caller knows when the conversation is fully loaded.

All 78 tests pass, build succeeds.

* fix(sidepanel): address bot review feedback for PR superduck-ai#210

- Remove debug console.log in useSessionPersistence, use selectedModelRef
  instead of selectedModel in session load effect to avoid stale closure
- Add intl.locale to compactConversation dependency array
- Replace hardcoded Chinese text with i18n in useAgentLoop
- Add queryTabId to sendPrompt dependency array to prevent stale tab IDs
- Add user feedback for /share command instead of silent return
- Replace non-null assertion with explicit null guard in useLightningMode
- Align platform detection in PermissionPrompt with useLightningMode
  (check both navigator.platform and navigator.userAgent for Mac detection)
- Add i18n keys for new messages in en-US and zh-CN locale files

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(sidepanel): restore full event message parsing in useSessionPersistence

Import pickEventMessage from sessionHistory.ts instead of using a simplified
local version. The original implementation handles nested event structures
(event.data.message, event.payload.message, event.item.message) which are
needed for restoring remote sessions with various API response formats.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: yueqi.guo <guo_yueqi@qq.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
…perduck-ai#212)

- Fix read-only PlanApprovalModal backdrop click: add onClick handler
  to the backdrop div itself (e.target === e.currentTarget on wrapper
  never fired because backdrop is absolute-positioned child)
- Add noopener,noreferrer to window.open() calls in MarkdownComponents
  to prevent reverse tabnabbing attacks

Co-authored-by: yueqi.guo <guo_yueqi@qq.com>
superduck-ai#213)

- Extract 551-line ChatInputArea component into components/ChatInputArea.tsx
- Reduces SidepanelApp.tsx from 2969 to 2471 lines (17% reduction)
- Manages internal state: permission menu, actions menu, command menu
- Props interface groups related dependencies logically
- All 78 tests pass, no new TypeScript errors introduced

Co-authored-by: yueqi.guo <guo_yueqi@qq.com>
* feat: add Edge extension build support

- Add build script with target parameter (chrome/edge)
- Transform manifest.json for Edge:
  - Remove Chrome-specific 'key' field
  - Remove Chrome 'update_url'
  - Change 'minimum_chrome_version' to 'minimum_edge_version'
  - Update description to mention Edge
- Add build:chrome and build:edge npm scripts
- Update install.sh to support configurable extension IDs
- Document environment variables for custom IDs

Now both Chrome Web Store and Edge Add-ons can be built from the same codebase.

* fix: move manifest transformation to vite.config.ts

Move manifest transformation logic into vite.config.ts so that
@crxjs/vite-plugin sees the correct manifest from the start of the
build process, not after. This prevents the plugin from using
Chrome-specific fields (like 'key') during Edge builds.

Changes:
- vite.config.ts: Add transformManifest() that runs before crx() plugin
- scripts/build.mjs: Simplified to just set BUILD_TARGET env var
- No longer modifies dist/manifest.json post-build

* fix(install): validate extension ID format before writing native host manifest

Addresses Factory Droid P1 security concern: validate EXTENSION_ID against
Chromium extension ID format (32 lowercase letters a-p) before interpolating
into JSON manifest to prevent injection attacks.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(edge): correct manifest keys and use bun for build scripts

- Remove incorrect minimum_chrome_version → minimum_edge_version rename
  (Edge supports minimum_chrome_version as a Chromium-based browser)
- Use bun instead of node for build script invocation (CodeRabbit suggestion)
- Update shebang to #!/usr/bin/env bun

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix: handle signal termination and validate BUILD_TARGET at runtime

- build.mjs: propagate non-zero exit when Vite is killed by signal (SIGINT etc.)
- vite.config.ts: validate BUILD_TARGET at runtime, throw on invalid values
  instead of silently falling back to chrome behavior

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix: update usage comment to reflect bun instead of node

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: yueqi.guo <guo_yueqi@qq.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
…tool calls (superduck-ai#215)

clearAllPendingToolCalls resolved all pending permission requests with
false when the bridge disconnected, making 'user denied' indistinguishable
from 'infrastructure failed.' The model sees a permission denial and may
prompt the user, when the real issue is the bridge connection.

Per RoboCFO: structured actionable errors for model self-correction.

Changes:
- Add reason parameter to clearAllPendingToolCalls (bridge_disconnected
  vs manual_disconnect)
- Add console.warn on each cleared request with the reason
- Pass explicit reason at both call sites (ws.onclose and reconnectMcp)

Reference: RoboCFO 'Inside an Agent Harness: Technical Guide' —
tool errors must provide enough context for the model to self-correct

Co-authored-by: yueqi.guo <guo_yueqi@qq.com>
…ignoring (superduck-ai#216)

The navigate tool's domain category check (pageTools.ts:419) had a bare
catch {} that silently swallowed all errors. If the category service
becomes unavailable, the safety gate is bypassed with no diagnostic.

Per RoboCFO: 'tool errors must never be silently swallowed.'

Changes:
- Replace empty catch with console.warn that logs the URL and error
- Add explanatory comment about fail-open rationale (permission check
  below still enforces per-host grants as a second line of defense)
- Improve URL validation error message to be model-actionable:
  includes the invalid input, expected format, and supported schemes

Reference: RoboCFO 'Inside an Agent Harness: Technical Guide' —
structured actionable errors for model self-correction

Co-authored-by: yueqi.guo <guo_yueqi@qq.com>
…nt ignore (superduck-ai#217)

When enableConsoleTracking or enableNetworkTracking failed (CDP attach
error, debugger detached, etc.), the error was silently swallowed. The
subsequent getConsoleMessages/getNetworkRequests returned [], and the
model saw 'No messages found' — a misleading response that hides the
real failure.

Per Addy Osmani: 'success is silent, failures are verbose.'

Changes:
- Console tracking: replace empty catch with error return including
  the failure reason and recovery suggestion
- Network tracking: same pattern

Reference: Addy Osmani 'Agent Harness Engineering' — hooks and
feedback loops must make failures actionable for the model

Co-authored-by: yueqi.guo <guo_yueqi@qq.com>
…superduck-ai#218)

Add chrome.tabs.onRemoved listener in ChromeDebuggerProtocol that cleans
up tabLocks, consoleMessagesByTab, networkRequestsByTab, consoleTrackingEnabled,
and networkTrackingEnabled when a tab is closed.

Also fix withTabLock to self-delete the map entry when no successor is queued,
preventing unbounded promise chain accumulation per tab.

Without this fix, every tab that ever used CDP features (screenshots, console
tracking, network tracking) leaks Map/Set entries for the entire service worker
lifetime. With MAX_LOGS_PER_TAB=10000 and MAX_REQUESTS_PER_TAB=1000, a single
leaked tab can hold significant memory.

Agent harness principle: Deterministic Lifecycle Hooks — per-resource state
must be cleaned up when the resource is destroyed.

Co-authored-by: yueqi.guo <guo_yueqi@qq.com>
…teTool (superduck-ai#221)

Between reconnect() releasing connMu and ExecuteTool re-acquiring it,
a concurrent Close() call could set b.conn to nil. The subsequent
b.conn.SetDeadline() would then panic with a nil pointer dereference.

Add a nil check on b.conn after acquiring the lock, returning a clear
error instead of panicking.

Agent harness principle: Structured actionable errors — surface real
failures with clear messages instead of crashing.

Co-authored-by: yueqi.guo <guo_yueqi@qq.com>
…k-ai#224)

The browser_batch tool accepted an unbounded actions array with no
upper limit. A model generating a large batch (e.g., 100+ actions)
could consume excessive resources and create long-running,
impossible-to-cancel operations.

Now caps batches at 20 actions (MAX_BATCH_ACTIONS), returns a clear
error when exceeded, and advertises the limit in the tool schema
(maxItems) and description. The tool already stopped on first error,
so smaller batches fail fast.

Inspired by agent harness best practices: deterministic input
validation at the harness layer prevents runaway agent loops.

Co-authored-by: yueqi.guo <guo_yueqi@qq.com>
The tool call audit events (superduck.chat.tool_called and
superduck.mcp.tool_called) recorded the tool name, session, and
success/failure, but NOT the tool's input arguments. This made it
impossible to answer 'what did the agent do' from telemetry alone.

Per RoboCFO: 'record every action, tool call, approval decision.'

Changes:
- Add safe, low-cardinality input fields to both audit event paths:
  action (computer), filter, depth, limit, clear, diff, newTab,
  full, allowCrossOrigin
- Intentionally exclude PII-bearing fields (URLs, selectors, code,
  text) — these are available in session history if needed
- Apply to both the sidepanel (processToolResults) and MCP
  (executeToolInner) code paths

Reference: RoboCFO 'Inside an Agent Harness: Technical Guide' —
audit trail must capture what the agent did, with what data, and why

Co-authored-by: yueqi.guo <guo_yueqi@qq.com>
…duck-ai#222)

validateComputerArgs only logged warnings for out-of-range duration
values (>30s or <0) but still forwarded them to the Chrome extension.
This meant invalid tool calls consumed bridge and extension resources
before failing with a confusing downstream error.

Now returns a clear validation error at the bridge layer so the agent
receives an actionable message ('duration 45.0 exceeds schema maximum
of 30 seconds') and can self-correct. This follows the agent harness
principle of rejecting rather than clamping to avoid 'over-shackling'
where the model never learns correct bounds.

Co-authored-by: yueqi.guo <guo_yueqi@qq.com>
…perduck-ai#223)

The Bridge WebSocket reconnection had no upper limit — retryCount grew
indefinitely with exponential backoff capped at 20s. This caused
endless reconnection attempts that wasted resources and masked
permanent disconnections.

Now stops after MAX_BRIDGE_RETRIES (15) attempts (~4-5 minutes) and
emits a 'reconnect_exhausted' telemetry event. Manual connectBridge()
calls reset the counter so users can restart the cycle.

Inspired by agent harness best practices: retry budgets per turn are
not optional in production.

Co-authored-by: yueqi.guo <guo_yueqi@qq.com>
)

Previously, when CDP debugger attachment failed (e.g., user clicked
Cancel on Chrome's debugger prompt), the error was silently swallowed.
This caused all downstream CDP-dependent tools (screenshot, click,
read_page) to fail with confusing errors like 'No debugger attached'.

Now reports attach failures as actionable errors so the agent can
decide to retry or inform the user. Chrome internal pages (chrome://,
edge://, etc.) are still silently skipped since they cannot be
debugged by design.

Inspired by agent harness best practices: tool errors must be
structured and actionable, never silent.

Co-authored-by: yueqi.guo <guo_yueqi@qq.com>
…i#219)

* fix(crx): clean up screenshotContextManager on tab close

Add chrome.tabs.onRemoved listener to clear screenshotContextManager
entries when tabs are closed. Without this, every tab that takes a
screenshot leaks its viewport/screenshot dimensions in the Map for
the entire service worker lifetime. Stale entries could also cause
incorrect click coordinate mappings if Chrome reuses tab IDs.

Agent harness principle: Deterministic Lifecycle Hooks — per-resource
state must be cleaned up when the resource is destroyed.

* test: add coverage for chrome.tabs.onRemoved listener

Add test file to verify the screenshot context cleanup listener
registered at module load time. Uses dynamic import to ensure chrome
mock is set up before shared.ts is loaded.

This brings branch coverage from 83.33% back above the 85% threshold.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: yueqi.guo <guo_yueqi@qq.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
…perduck-ai#226)

The attachDebuggerInner method was unconditionally detaching and
re-attaching the debugger on every call, even when already attached.
Each chrome.debugger.attach() triggers Chrome to display a new
"X is debugging this browser" banner, causing overlapping banners
when switching tabs during agent operation.

Now check wasAttached first and skip the detach/attach cycle if the
debugger is already attached. Only perform the initial attach when
the debugger is not yet attached to the tab.

This prevents the banner duplication issue while preserving the
correct behavior for console/network tracking re-enablement.

Co-authored-by: yueqi.guo <guo_yueqi@qq.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Document the step-by-step process for batch-submitting low-risk PRs:
risk assessment, sequential submission, bot comment monitoring,
10-minute silent window before merge, CI pass requirement, and cleanup.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…uck-ai#227)

The visual_test.sh used --amount 15 which exceeds the schema maximum
of 10 wheel ticks. This was valid when CLI allowed 1-100 but became
silently broken after validation was tightened to match the MCP schema.

Changed to --amount 10 (schema maximum) to maintain strong scroll effect.

Co-authored-by: yueqi.guo <guo_yueqi@qq.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
… fixes (superduck-ai#229)

* feat(sidepanel): add session history panel + persistence

- Restore chat history on sidepanel reopen (close + reopen shows
  the prior assistant reply, fixes reported Bug 1)
- New SessionHistoryPanel for browsing / loading past conversations
- Persist session state when the sidepanel iframe is destroyed on
  tab switch (instead of relying on React state surviving)
- Guard Enter / Escape handlers against IME composition to avoid
  accidental submits in CJK input
- Hoist useTabEvent properties array to module scope to prevent
  infinite re-render loop on tab switch
- Drop 7 broken P0 specs that fail due to a Playwright sidepanel
  fixture bug; see e2e/STATUS.md for context and recovery path

* feat(crx): make sidepanel window-bound to survive tab switches

- Sidepanel is now window-scoped, not tab-scoped, so it stays open
  when the user switches tabs
- useActiveTabId dynamically tracks chrome.tabs.onActivated to
  re-target the conversation to the new active tab
- elementSelectorInjector adapts to the new active tab context
  (useModelConfig no longer captures a stale tabId)

* fix(crx): tighten tab group management and sidePanel gesture handling

- Lock the tab ID during agent execution so the tab group can't
  get reassigned when state refreshes
- Stop refreshSecondaryState from creating a new tab group on
  tab switch
- Set lockedTabIdRef synchronously in effectiveSendPrompt
- Stop the detach / attach cycle that stacks debugger banners
- When the user explicitly ungroups the main tab, don't rebuild
  the group from cached metadata
- PANEL_READY now promotes the active tab to main if it opens
  inside an existing group (matches pre-setPanelBehavior flow)
- chrome.sidePanel.open() is now fire-and-forget so it stays in
  the user-gesture chain (otherwise Ctrl+E rejects with
  'must be called in response to a user gesture')
- Add side_panel.default_path so the action click uses
  setPanelBehavior({openPanelOnActionClick}) and never hits the
  gesture rejection in the first place

* test(e2e): add regression specs, headless default, helpers, and STATUS.md

Adds three regression specs (perf render, sidepanel open flow, session history),
a headless default that keeps --load-extension working, an initialTabId param
on the openSidepanel helper for fixture-based tab targeting, and STATUS.md
documenting the 7 P0 specs that were dropped because of an upstream Playwright
sidepanel fixture bug.

* fix(sidepanel): tighten type predicates and screenshot options

- sidepanelGuards.normalizeToolResultContent: narrow the type
  predicate from ApiToolResultContentBlock to
  ApiTextContentBlock | ApiImageContentBlock, which are the only
  blocks the filter actually accepts and which are cleanly a
  subset of BetaContentBlockParam. The wider union triggered
  TS2677 + TS2322 because SDK v1 / v2 type definitions don't
  fully overlap.
- hooks.useActiveTabId: inline the onActivated listener shape
  ({ tabId: number; windowId: number }) because the @types/chrome
  version pinned in this project does not export TabActiveInfo.
- useLightningMode: drop the format / quality fields from the
  screenshot() options object — they are no-ops (cdp.screenshot
  hard-codes jpeg / INITIAL_JPEG_QUALITY) and ScreenshotOptions
  doesn't declare them.

* fix(sidepanel): address P2 codex reviews on hooks and SessionHistoryPanel

- hooks.ts: track sidepanel's windowId on mount and ignore
  chrome.tabs.onActivated events from other windows, so the
  panel cannot retarget to a different window's tab when the
  user switches tabs elsewhere
- SessionHistoryPanel.tsx: replace the row's outer <button>
  with a div+role=button+tabIndex+onKeyDown so the nested
  delete <button> is no longer invalid HTML and stays
  reachable for keyboard / screen-reader users

* fix(sidepanel): avoid remapping prior tab session + clean up aliases on delete

- SidepanelApp.tsx: drop the standalone tab→session write effect.
  Writing on every (activeSessionId, query.tabId) change would rebind
  the *new* tab to the *old* tab's session whenever the user switched
  tabs while the sidepanel stayed open, silently overwriting the
  new tab's prior mapping. The tab→session mapping is now written
  only inside the resolver, at the moment a session is actually
  chosen / restored / created for the current tab.
- sidepanelGuards.ts: add collectTabSessionKeysToRemove() helper that
  scans chrome.storage.local for sidepanel_tab_session_<n> entries
  whose value equals a given sessionId.
- SessionHistoryPanel.tsx: on delete, also remove the tab→session
  aliases (and the global last-active key) pointing at the deleted
  session so future sidepanel opens on those tabs do not resurrect
  an empty session with the same id.

---------

Co-authored-by: yueqi.guo <guo_yueqi@qq.com>
Co-authored-by: yueqi.guo <yueqi.guo@users.noreply.github.com>
…FrameStorage

Closes architectural problem C in docs/architectural-improvements-todo.md.

Two related bugs in the GIF recording pipeline:

1. gifFrameStorage.storage + recordingGroups are pure in-memory state.
   After a 30-second SW idle kill + restart, a 5-minute recording the
   user was in the middle of disappears with no recovery path.

2. The shared screenRecorder stub is a no-op — isRecording always
   returns false, addFrame is a void. core.ts#recordToolAction calls
   these and never actually records anything during normal tool
   execution. The only path that ever captured frames was the explicit
   'gif_creator start_recording' pathway.

Fixes:

- Persist gifFrameStorage to chrome.storage.local via write-through:
  every addFrame / startRecording / stopRecording / clearFrames / clearAll
  schedules a fire-and-forget write. Public API stays synchronous and
  reads always come from the in-memory Map.
- Add restoreGifFrameStorageFromStorage(), called from
  service-worker.ts onStartup, that hydrates both the frame map and
  the recordingGroups set with shape-tolerance for missing / wrong-shape
  payloads.
- Delete the screenRecorder stub from shared.ts. Replace its 5 callers
  in core.ts#recordToolAction with direct gifFrameStorage calls so the
  recording pathway is actually wired up.

GifFrameData shape extended with optional frameNumber + timestamp for
compatibility with the existing recordToolAction shape (fields ignored
by storage / restore).

13 new storage contract tests in gifFrameStorage.persistence.test.ts
cover single/multi-group payloads, recording-set round-trips, shape
tolerance, integer-boundary groupIds, and the missing-payload path.

typecheck 9 errors (unchanged baseline), 181/181 unit tests pass.
@Postroggy Postroggy closed this Jun 9, 2026
@Postroggy
Postroggy deleted the fix/gif-frame-storage-persistence branch June 9, 2026 09:48
@Postroggy
Postroggy restored the fix/gif-frame-storage-persistence branch June 9, 2026 10:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants