spiracha 2.1.0 → 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (125) hide show
  1. package/AGENTS.md +27 -12
  2. package/README.md +37 -19
  3. package/apps/ui/AGENTS.md +18 -10
  4. package/apps/ui/README.md +17 -10
  5. package/apps/ui/src/components/analytics-breakdowns.tsx +106 -0
  6. package/apps/ui/src/components/antigravity-conversations-table.tsx +1 -0
  7. package/apps/ui/src/components/app-shell.tsx +45 -3
  8. package/apps/ui/src/components/data-table.tsx +17 -1
  9. package/apps/ui/src/components/delete-confirm-dialog.tsx +4 -3
  10. package/apps/ui/src/components/export-dialog.tsx +77 -62
  11. package/apps/ui/src/components/projects-table.tsx +4 -1
  12. package/apps/ui/src/components/route-error-panel.tsx +13 -0
  13. package/apps/ui/src/components/thread-goals-panel.tsx +50 -0
  14. package/apps/ui/src/components/thread-tools-panel.tsx +142 -0
  15. package/apps/ui/src/components/threads-table.tsx +4 -1
  16. package/apps/ui/src/lib/antigravity-server.ts +70 -17
  17. package/apps/ui/src/lib/antigravity-transcript-events.ts +46 -70
  18. package/apps/ui/src/lib/claude-code-server.ts +14 -4
  19. package/apps/ui/src/lib/codex-queries.ts +3 -3
  20. package/apps/ui/src/lib/codex-server.ts +3 -3
  21. package/apps/ui/src/lib/cursor-server.ts +41 -69
  22. package/apps/ui/src/lib/cursor-transcript-events.ts +11 -86
  23. package/apps/ui/src/lib/delete-batch.ts +52 -0
  24. package/apps/ui/src/lib/error-presentation.ts +43 -0
  25. package/apps/ui/src/lib/export-mutation.ts +20 -0
  26. package/apps/ui/src/lib/export-options.ts +15 -0
  27. package/apps/ui/src/lib/grok-server.ts +2 -3
  28. package/apps/ui/src/lib/kiro-server.ts +14 -2
  29. package/apps/ui/src/lib/mutation-error.ts +15 -0
  30. package/apps/ui/src/lib/opencode-server.ts +13 -2
  31. package/apps/ui/src/lib/path-utils.ts +1 -1
  32. package/apps/ui/src/lib/qoder-queries.ts +15 -0
  33. package/apps/ui/src/lib/qoder-transcript-events.ts +1 -1
  34. package/apps/ui/src/lib/route-state-reset.tsx +10 -0
  35. package/apps/ui/src/lib/settings-server.ts +33 -0
  36. package/apps/ui/src/lib/settings-store.tsx +82 -38
  37. package/apps/ui/src/lib/settings.ts +65 -0
  38. package/apps/ui/src/lib/source-session-export-server.ts +1 -1
  39. package/apps/ui/src/lib/thread-metadata.ts +22 -0
  40. package/apps/ui/src/lib/thread-transcript-stats.ts +10 -1
  41. package/apps/ui/src/lib/workspace-delete-navigation.ts +8 -0
  42. package/apps/ui/src/routeTree.gen.ts +282 -282
  43. package/apps/ui/src/routes/__root.tsx +12 -15
  44. package/apps/ui/src/routes/analytics.tsx +10 -61
  45. package/apps/ui/src/routes/antigravity-conversations.$conversationId.tsx +73 -19
  46. package/apps/ui/src/routes/antigravity.$workspaceKey.tsx +42 -60
  47. package/apps/ui/src/routes/antigravity.index.tsx +2 -2
  48. package/apps/ui/src/routes/claude-code-sessions.$sessionId.tsx +25 -11
  49. package/apps/ui/src/routes/claude-code.$workspaceKey.tsx +27 -32
  50. package/apps/ui/src/routes/claude-code.index.tsx +2 -2
  51. package/apps/ui/src/routes/codex.$project.tsx +33 -31
  52. package/apps/ui/src/routes/codex.index.tsx +5 -10
  53. package/apps/ui/src/routes/cursor-threads.$composerId.tsx +29 -16
  54. package/apps/ui/src/routes/cursor.$workspaceKey.tsx +33 -27
  55. package/apps/ui/src/routes/cursor.index.tsx +5 -2
  56. package/apps/ui/src/routes/grok-sessions.$sessionId.tsx +25 -12
  57. package/apps/ui/src/routes/grok.$workspaceKey.tsx +20 -26
  58. package/apps/ui/src/routes/grok.index.tsx +2 -2
  59. package/apps/ui/src/routes/index.tsx +2 -18
  60. package/apps/ui/src/routes/kiro-sessions.$sessionId.tsx +25 -12
  61. package/apps/ui/src/routes/kiro.$workspaceKey.tsx +27 -32
  62. package/apps/ui/src/routes/kiro.index.tsx +2 -2
  63. package/apps/ui/src/routes/opencode-sessions.$sessionId.tsx +29 -36
  64. package/apps/ui/src/routes/opencode.$workspaceKey.tsx +45 -61
  65. package/apps/ui/src/routes/opencode.index.tsx +8 -17
  66. package/apps/ui/src/routes/qoder-sessions.$sessionId.tsx +12 -11
  67. package/apps/ui/src/routes/qoder.$workspaceKey.tsx +19 -25
  68. package/apps/ui/src/routes/qoder.index.tsx +2 -2
  69. package/apps/ui/src/routes/threads.$threadId.tsx +87 -56
  70. package/apps/ui/vite.config.ts +22 -12
  71. package/bin/spiracha.ts +28 -1
  72. package/package.json +26 -17
  73. package/src/client.ts +61 -16
  74. package/src/lib/antigravity-db.ts +236 -82
  75. package/src/lib/antigravity-keychain.ts +7 -8
  76. package/src/lib/antigravity-transcript-phase.ts +47 -0
  77. package/src/lib/claude-code-db.ts +45 -19
  78. package/src/lib/codex-analytics.ts +32 -5
  79. package/src/lib/codex-browser-db.ts +540 -137
  80. package/src/lib/codex-browser-export.ts +2 -7
  81. package/src/lib/codex-browser-types.ts +21 -1
  82. package/src/lib/codex-thread-cache.ts +88 -33
  83. package/src/lib/codex-thread-parser.ts +78 -35
  84. package/src/lib/codex-thread-recovery.ts +38 -11
  85. package/src/lib/codex-transcript-renderer.ts +49 -61
  86. package/src/lib/concurrency.ts +15 -3
  87. package/src/lib/conversation-api.ts +34 -31
  88. package/src/lib/conversation-data/adapter-helpers.ts +25 -3
  89. package/src/lib/conversation-data/antigravity-adapter.ts +54 -29
  90. package/src/lib/conversation-data/claude-code-adapter.ts +32 -6
  91. package/src/lib/conversation-data/codex-adapter.ts +91 -80
  92. package/src/lib/conversation-data/cursor-adapter.ts +48 -34
  93. package/src/lib/conversation-data/grok-adapter.ts +19 -6
  94. package/src/lib/conversation-data/index.ts +29 -10
  95. package/src/lib/conversation-data/kiro-adapter.ts +18 -5
  96. package/src/lib/conversation-data/message-selector.ts +2 -5
  97. package/src/lib/conversation-data/opencode-adapter.ts +43 -15
  98. package/src/lib/conversation-data/path-match.ts +1 -13
  99. package/src/lib/conversation-data/qoder-adapter.ts +23 -18
  100. package/src/lib/conversation-zip-export.ts +20 -2
  101. package/src/lib/cursor-db.ts +34 -23
  102. package/src/lib/cursor-id.ts +17 -0
  103. package/src/lib/cursor-recovery.ts +146 -35
  104. package/src/lib/cursor-transcript-phase.ts +40 -0
  105. package/src/lib/cursor-transcript.ts +21 -5
  106. package/src/lib/grok-db.ts +76 -34
  107. package/src/lib/grok-transcript-phase.ts +11 -2
  108. package/src/lib/grok-transcript.ts +15 -2
  109. package/src/lib/kiro-db.ts +161 -68
  110. package/src/lib/kiro-transcript.ts +0 -4
  111. package/src/lib/opencode-db.ts +4 -8
  112. package/src/lib/opencode-think-tags.ts +17 -3
  113. package/src/lib/portable-path.ts +9 -0
  114. package/src/lib/qoder-acp-client.ts +11 -2
  115. package/src/lib/qoder-db.ts +51 -17
  116. package/src/lib/qoder-transcript-phase.ts +64 -0
  117. package/src/lib/qoder-transcript.ts +30 -2
  118. package/src/lib/shared.ts +51 -15
  119. package/src/lib/sqlite-error.ts +31 -5
  120. package/src/lib/transcript-load-limiter.ts +8 -3
  121. package/src/lib/ui-cache.ts +152 -17
  122. package/src/lib/ui-export-archive.ts +14 -55
  123. package/src/lib/ui-export-files.ts +75 -15
  124. package/src/lib/ui-export-zip.ts +40 -0
  125. package/apps/ui/package.json +0 -65
package/AGENTS.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  ## Purpose
4
4
 
5
- This repo is a Bun-first local app for browsing, exporting, and exposing agent conversation history from Codex, Claude Code, Kiro, Qoder, Cursor, Antigravity, and OpenCode.
5
+ This repo is a Bun-first local app for browsing, exporting, and exposing agent conversation history from Codex, Claude Code, Grok, Kiro, Qoder, Cursor, Antigravity, and OpenCode.
6
6
 
7
7
  The command-line exporter, MCP server, and Codex plugin were removed in the 2.0 hard cut. Do not add bridge commands, compatibility aliases, or deprecated entrypoints back. New client workflows should use the stable HTTP API exposed by the UI server or the stable `spiracha/client` package export.
8
8
 
@@ -52,10 +52,14 @@ Codex browser/export modules:
52
52
  - project/thread browsing queries, delete flows, dashboard summaries, DB path resolution
53
53
  - `src/lib/codex-browser-export.ts`
54
54
  - UI-facing thread download rendering
55
+ - `src/lib/codex-browser-types.ts`
56
+ - Codex browser query and presentation contracts
55
57
  - `src/lib/codex-thread-types.ts`
56
58
  - Codex DB row and transcript rendering types
57
59
  - `src/lib/codex-transcript-renderer.ts`
58
60
  - Markdown/plain text rendering for Codex session files
61
+ - `src/lib/codex-transcript-filter.ts`
62
+ - centralized hidden bootstrap and transcript-text filtering
59
63
  - `src/lib/codex-thread-parser.ts`
60
64
  - structured Codex event parsing used by analytics and the UI
61
65
  - `src/lib/codex-analytics.ts`
@@ -66,29 +70,35 @@ Codex browser/export modules:
66
70
  - Codex project recovery helpers
67
71
 
68
72
  Source-specific browser/export modules:
69
- - `src/lib/claude-code-db.ts`, `src/lib/claude-code-transcript-phase.ts`, `src/lib/claude-code-transcript.ts`
70
- - `src/lib/kiro-db.ts`, `src/lib/kiro-transcript-phase.ts`, `src/lib/kiro-transcript.ts`
71
- - `src/lib/qoder-db.ts`, `src/lib/qoder-transcript-phase.ts`, `src/lib/qoder-transcript.ts`
72
- - `src/lib/cursor-db.ts`, `src/lib/cursor-recovery.ts`, `src/lib/cursor-transcript.ts`
73
- - `src/lib/antigravity-db.ts`, `src/lib/antigravity-keychain.ts`
74
- - `src/lib/opencode-db.ts`, `src/lib/opencode-transcript-phase.ts`, `src/lib/opencode-think-tags.ts`, `src/lib/opencode-transcript.ts`
73
+ - `src/lib/claude-code-db.ts`, `src/lib/claude-code-exporter-types.ts`, `src/lib/claude-code-transcript-phase.ts`, `src/lib/claude-code-transcript.ts`
74
+ - `src/lib/grok-db.ts`, `src/lib/grok-exporter-types.ts`, `src/lib/grok-transcript-phase.ts`, `src/lib/grok-transcript.ts`
75
+ - `src/lib/kiro-db.ts`, `src/lib/kiro-exporter-types.ts`, `src/lib/kiro-transcript-phase.ts`, `src/lib/kiro-transcript.ts`
76
+ - `src/lib/qoder-db.ts`, `src/lib/qoder-acp-client.ts`, `src/lib/qoder-exporter-types.ts`, `src/lib/qoder-transcript-phase.ts`, `src/lib/qoder-transcript.ts`
77
+ - `src/lib/cursor-db.ts`, `src/lib/cursor-exporter-types.ts`, `src/lib/cursor-recovery.ts`, `src/lib/cursor-transcript-phase.ts`, `src/lib/cursor-transcript.ts`
78
+ - `src/lib/antigravity-db.ts`, `src/lib/antigravity-exporter-types.ts`, `src/lib/antigravity-keychain.ts`, `src/lib/antigravity-projects.ts`, `src/lib/antigravity-transcript-phase.ts`
79
+ - `src/lib/opencode-db.ts`, `src/lib/opencode-exporter-types.ts`, `src/lib/opencode-transcript-phase.ts`, `src/lib/opencode-think-tags.ts`, `src/lib/opencode-transcript.ts`
75
80
 
76
81
  Shared utilities:
77
82
  - `src/lib/concurrency.ts`
78
83
  - `src/lib/model-label.ts`
79
84
  - `src/lib/path-transforms.ts`
85
+ - `src/lib/portable-path.ts`
80
86
  - `src/lib/shared.ts`
81
87
  - `src/lib/sqlite-error.ts`
82
88
  - `src/lib/sqlite-retry.ts`
83
89
  - `src/lib/ui-cache.ts`
84
90
  - `src/lib/ui-export-archive.ts`
85
91
  - `src/lib/ui-export-files.ts`
92
+ - `src/lib/ui-export-zip.ts`
93
+ - `src/lib/conversation-zip-export.ts`
94
+ - `src/lib/transcript-load-limiter.ts`
95
+ - `src/coverage-check.ts`
86
96
 
87
- UI package:
97
+ UI source tree:
88
98
  - `apps/ui/`
89
99
  - TanStack Start browser UI
90
100
  - API routes live under `apps/ui/src/routes/api.v1.*.ts`
91
- - source routes include `/threads/$threadId`, `/claude-code-sessions/$sessionId`, `/kiro-sessions/$sessionId`, `/qoder-sessions/$sessionId`, `/cursor-threads/$composerId`, `/antigravity-conversations/$conversationId`, and `/opencode-sessions/$sessionId`
101
+ - source routes include `/threads/$threadId`, `/claude-code-sessions/$sessionId`, `/grok-sessions/$sessionId`, `/kiro-sessions/$sessionId`, `/qoder-sessions/$sessionId`, `/cursor-threads/$composerId`, `/antigravity-conversations/$conversationId`, and `/opencode-sessions/$sessionId`
92
102
 
93
103
  ## Stable API Contract
94
104
 
@@ -105,6 +115,9 @@ The local UI server exposes:
105
115
  - `POST /api/v1/conversation-query`
106
116
  - `GET /api/v1/conversations/:source/:id`
107
117
  - `GET /api/v1/conversations/:source/:id/export`
118
+ - `DELETE /api/v1/conversations/:source/:id`
119
+ - `POST /api/v1/conversations/delete`
120
+ - `POST /api/v1/conversations/export`
108
121
  - `GET /api/v1/resolve?ref=<url-or-deeplink>`
109
122
 
110
123
  Defaults:
@@ -126,8 +139,9 @@ Current tests cover:
126
139
  - Cursor recovery/prune behavior
127
140
  - Antigravity discovery, transcript parsing, Keychain state, and artifact export rendering
128
141
  - OpenCode MiniMax `<think>` tag extraction, including code-literal preservation
129
- - UI component and adapter behavior through the Vitest suite wrapped by `src/ui-package.test.ts`
142
+ - UI component and adapter behavior through the Vitest suite wrapped by `src/ui-suite.test.ts`
130
143
  - package manifest hard-cut guarantees through `src/package-manifest.test.ts`
144
+ - a 90% line-coverage gate for both the root Bun suite and UI Vitest suite, with function and hotspot reporting
131
145
 
132
146
  When changing risky areas:
133
147
  - Stable API changes: update `src/lib/conversation-api.test.ts` and focused tests under `src/lib/conversation-data/`.
@@ -147,13 +161,14 @@ rtk bun run build
147
161
  rtk bun run coverage
148
162
  rtk bun start
149
163
  rtk bun run ui:preview
150
- rtk bun run --cwd apps/ui test
164
+ rtk bun run test:ui
151
165
  ```
152
166
 
153
167
  ## Notes
154
168
 
155
169
  - Keep root-package source modules imported by the UI available through `@spiracha/lib/*`.
156
- - The UI package runs Vite through `bun --bun` because server functions depend on Bun-only modules like `bun:sqlite`.
170
+ - The repository has one package manifest. Keep UI runtime dependencies needed by packaged `bunx spiracha` in root `dependencies` and build/test-only tooling in root `devDependencies`.
171
+ - Root-owned UI Vite commands use `apps/ui` as their internal working directory and run through `bun --bun` because TanStack and server functions depend on that application root and Bun-only modules like `bun:sqlite`. Do not add a nested manifest to achieve this. UI Vitest commands use the normal Node runtime.
157
172
  - TanStack Start server functions should use `.validator(...)`, not deprecated `.inputValidator(...)`.
158
173
  - API routes should use route-level `server.handlers`.
159
174
  - Keep `*-transcript-phase.ts` modules browser-safe; UI client adapters import them directly.
package/README.md CHANGED
@@ -7,7 +7,7 @@
7
7
  [![license](https://img.shields.io/npm/l/spiracha)](LICENSE.md)
8
8
  [![runtime](https://img.shields.io/badge/runtime-Bun-000000?logo=bun)](https://bun.sh)
9
9
 
10
- Spiracha is a local TanStack Start app for browsing and exporting agent conversation history from Codex, Claude Code, Grok, Kiro, Qoder, Cursor, Antigravity, and OpenCode.
10
+ Spiracha is a Bun package with a local TanStack Start UI and a direct data client for browsing and exporting agent conversation history from Codex, Claude Code, Grok, Kiro, Qoder, Cursor, Antigravity, and OpenCode.
11
11
 
12
12
  The legacy CLI, MCP server, and Codex plugin surfaces have been removed in the 2.0 hard cut. Spiracha now exposes the UI and a stable local data API; client-specific workflows such as review collection belong in the client that calls the API.
13
13
 
@@ -24,17 +24,20 @@ Spiracha asks Vite for port 3000 and automatically uses the next available port
24
24
  For repository development:
25
25
 
26
26
  ```bash
27
- rtk bun install
28
- rtk bun start
27
+ bun install
28
+ bun start
29
29
  ```
30
30
 
31
31
  Open the local URL printed by Vite.
32
32
 
33
+ Spiracha requires Bun 1.3.14 or newer. Set `PORT` to request a different starting port, for example `PORT=4100 bunx spiracha`; the launcher uses the next available port if that one is occupied.
34
+
33
35
  ## What It Does
34
36
 
35
- - Browse local conversations across Codex, Claude Code, Kiro, Qoder, Cursor, Antigravity, and OpenCode.
36
- - Inspect source-specific detail pages with transcript, tool, reasoning, metadata, raw event, export, and delete flows where supported by the source.
37
- - Export transcripts from the UI as Markdown, text, or zip bundles with source-specific commentary/final-answer filtering.
37
+ - Browse local conversations across Codex, Claude Code, Grok, Kiro, Qoder, Cursor, Antigravity, and OpenCode.
38
+ - Search Codex projects from the app shell, with results delegated to the shareable `/codex?q=...` inventory filter.
39
+ - Inspect source-specific detail pages with transcript, tool, reasoning, metadata, raw event, export, and delete flows where supported by the source. Codex thread detail includes a tool-focused activity view plus recorded goals and sandbox policy.
40
+ - Export transcripts from the UI as Markdown, text, or zip bundles with source-specific commentary/final-answer filtering. The last submitted export choices persist across dialog openings; canceled drafts do not.
38
41
  - Expose a stable API for local clients that need normalized conversation metadata and message payloads.
39
42
  - Resolve Spiracha UI links and native source links into normalized `{ source, id }` references for cross-thread context lookup.
40
43
 
@@ -95,13 +98,13 @@ Response envelope:
95
98
  }
96
99
  ],
97
100
  "meta": {
98
- "hasNext": false,
101
+ "has_next": false,
99
102
  "next_cursor": null
100
103
  }
101
104
  }
102
105
  ```
103
106
 
104
- For serverless access from Bun, use the public client export:
107
+ For direct access from Bun scripts and CLIs, use the public client export. Local mode reads the source data without starting the TanStack server:
105
108
 
106
109
  ```ts
107
110
  import { createConversationClient } from "spiracha/client";
@@ -114,12 +117,16 @@ const page = await client.listConversations({
114
117
  });
115
118
  ```
116
119
 
120
+ Library and CLI use is quiet by default. Set `SPIRACHA_TRANSCRIPT_LOAD_LOGS=1` or
121
+ `SPIRACHA_OPENCODE_DB_LOGS=1` only when diagnosing loader or OpenCode database timing.
122
+
117
123
  ## Source Locations
118
124
 
119
125
  | Source | Default location | Primary override |
120
126
  | --- | --- | --- |
121
127
  | Codex | shared Codex DB probe list | `SPIRACHA_CODEX_DB` |
122
128
  | Claude Code | `~/.claude/projects` | `SPIRACHA_CLAUDE_CODE_PROJECTS_DIR` |
129
+ | Grok | `~/.grok/sessions` | `SPIRACHA_GROK_SESSIONS_DIR` |
123
130
  | Kiro | `~/Library/Application Support/Kiro/User/globalStorage/kiro.kiroagent/workspace-sessions` | `SPIRACHA_KIRO_WORKSPACE_SESSIONS_DIR` |
124
131
  | Qoder | `~/Library/Application Support/Qoder/User/globalStorage/state.vscdb` and `~/Library/Application Support/Qoder/User/workspaceStorage` | `SPIRACHA_QODER_GLOBAL_STATE_DB`, `SPIRACHA_QODER_WORKSPACE_STORAGE_DIR` |
125
132
  | Cursor | `~/Library/Application Support/Cursor/User` on macOS | `SPIRACHA_CURSOR_USER_DIR`, `SPIRACHA_CURSOR_PROJECTS_DIR` |
@@ -127,27 +134,38 @@ const page = await client.listConversations({
127
134
  | OpenCode | `${XDG_DATA_HOME:-~/.local/share}/opencode/opencode.db` | `SPIRACHA_OPENCODE_DB` |
128
135
  | UI exports | OS temp directory under `spiracha-ui-exports` | `SPIRACHA_UI_EXPORT_DIR` |
129
136
 
137
+ ### Qoder live ACP hydration
138
+
139
+ Qoder detail/export reads first use persisted state and CLI transcript files. When those do not contain assistant messages and Qoder is running, Spiracha can connect to Qoder's local JSON-RPC ACP Unix socket, issue `initialize` and `session/load`, and collect the streamed session updates. The default socket is the Qoder `SharedClientCache/qoder.sock`; override it with `SPIRACHA_QODER_SOCKET_PATH` (or the legacy environment spelling `SPIRACHA_QODER_SOCKET`). Connection failures and timeouts fall back to the persisted transcript rather than preventing the session from loading.
140
+
130
141
  ## UI Routes
131
142
 
132
143
  - `/codex` and `/codex/$project` for Codex inventory and project threads.
133
144
  - `/threads/$threadId` for Codex thread detail.
134
- - `/claude-code`, `/kiro`, `/qoder`, `/cursor`, `/antigravity`, and `/opencode` for source inventories.
135
- - Source detail routes include `/claude-code-sessions/$sessionId`, `/kiro-sessions/$sessionId`, `/qoder-sessions/$sessionId`, `/cursor-threads/$composerId`, `/antigravity-conversations/$conversationId`, and `/opencode-sessions/$sessionId`.
136
- - `/analytics` for Codex token and tool-call analytics.
145
+ - `/claude-code`, `/grok`, `/kiro`, `/qoder`, `/cursor`, `/antigravity`, and `/opencode` for source inventories.
146
+ - Source detail routes include `/claude-code-sessions/$sessionId`, `/grok-sessions/$sessionId`, `/kiro-sessions/$sessionId`, `/qoder-sessions/$sessionId`, `/cursor-threads/$composerId`, `/antigravity-conversations/$conversationId`, and `/opencode-sessions/$sessionId`.
147
+ - `/analytics` for project-scoped Codex token totals, average and median thread size, archive counts, tool usage, model tokens, client sources, and reasoning-effort breakdowns.
148
+ - `/settings` for transcript path conversion and username redaction. Export dialogs remember their own last submitted options.
137
149
 
138
150
  ## Development
139
151
 
140
152
  ```bash
141
- rtk bun test
142
- rtk bun run lint
143
- rtk bun run typecheck
144
- rtk bun run build
145
- rtk bun run coverage
146
- rtk bun start
147
- rtk bun run --cwd apps/ui test
153
+ bun test
154
+ bun run lint
155
+ bun run typecheck
156
+ bun run build
157
+ bun run coverage
158
+ bun start
159
+ bun run test:ui
148
160
  ```
149
161
 
150
- The UI package runs Vite through `bun --bun` because server functions import Bun-only modules such as `bun:sqlite`.
162
+ `bun run coverage` enforces at least 90% line coverage independently for the root Bun suite and the UI Vitest suite, and reports function coverage and per-file hotspots for follow-up.
163
+
164
+ Run one root test file with `bun test src/lib/shared.test.ts`. Run one UI test file with `bun run test:ui --run apps/ui/src/components/export-dialog.vitest.tsx`.
165
+
166
+ Spiracha has one package manifest. The `apps/ui` directory remains a source boundary, but all UI and direct-client commands and dependencies are owned at the repository root. Root-owned Vite commands use `apps/ui` as their internal working directory and run through `bun --bun` because TanStack server functions import Bun-only modules such as `bun:sqlite`; Vitest uses its normal Node runtime.
167
+
168
+ TanStack Router generates `apps/ui/src/routeTree.gen.ts` during development/build. Do not edit it manually; after adding or renaming route files, run `bun run build` (or start the dev server) and include the generated update.
151
169
 
152
170
  ## Breaking Consequences
153
171
 
package/apps/ui/AGENTS.md CHANGED
@@ -2,22 +2,26 @@
2
2
 
3
3
  ## Purpose
4
4
 
5
- This package is the local browser UI for Spiracha. It reads Codex, Claude Code, Kiro, Qoder, Cursor, Antigravity, and OpenCode local data through TanStack Start server functions and shared root-package helpers.
5
+ This directory is the local browser UI source tree for Spiracha. It reads Codex, Claude Code, Grok, Kiro, Qoder, Cursor, Antigravity, and OpenCode local data through TanStack Start server functions and shared root-package helpers.
6
6
 
7
7
  ## Commands
8
8
 
9
9
  ```bash
10
- rtk bun run dev
10
+ rtk bun start
11
11
  rtk bun run build
12
- rtk bun run test
12
+ rtk bun run test:ui
13
13
  rtk bun run typecheck
14
14
  rtk bun run coverage
15
15
  ```
16
16
 
17
+ Run these commands from the repository root. There is intentionally no nested UI package manifest.
18
+
17
19
  Important:
18
20
 
19
- - `dev`, `build`, and `preview` run through `bun --bun ...` on purpose. Do not switch them back to plain `vite` or Node execution, because the server functions import Bun-only modules such as `bun:sqlite`.
20
- - The root package mirrors UI runtime dependencies needed by the packaged `bunx spiracha` launcher. Keep `apps/ui/package.json` and the root `dependencies` aligned when browser/runtime dependencies change.
21
+ - `start`, `build`, and `ui:preview` run Vite through `bun --bun ...` on purpose. Do not switch them back to Node execution, because the server functions import Bun-only modules such as `bun:sqlite`.
22
+ - Those root-owned Vite scripts use `apps/ui` as their internal working directory because TanStack Start derives part of its dev-server plan from the process working directory.
23
+ - Keep UI runtime dependencies needed by the packaged `bunx spiracha` launcher in the root `dependencies`. Keep build and test tooling in the root `devDependencies`.
24
+ - Keep Vitest on its normal Node runtime; forcing it through `bun --bun` breaks test-environment module and global behavior.
21
25
 
22
26
  ## Routing
23
27
 
@@ -29,12 +33,13 @@ Important:
29
33
  - Codex project inventory and project-thread search use route search params. `/codex` and `/codex/$project` use `q`.
30
34
  - Codex analytics uses the `project` route search param so filtered analytics links can be bookmarked and reloaded.
31
35
  - Claude Code session detail lives at `/claude-code-sessions/$sessionId`.
36
+ - Grok session detail lives at `/grok-sessions/$sessionId`.
32
37
  - Kiro session detail lives at `/kiro-sessions/$sessionId`.
33
38
  - Qoder session detail lives at `/qoder-sessions/$sessionId`.
34
39
  - Cursor thread detail lives at `/cursor-threads/$composerId`.
35
40
  - Antigravity conversation detail lives at `/antigravity-conversations/$conversationId`.
36
41
  - OpenCode session detail lives at `/opencode-sessions/$sessionId`.
37
- - Keep the Codex, Claude Code, Kiro, Qoder, Cursor, Antigravity, and OpenCode list/detail pages aligned around the same table-driven index/detail pattern when adding new source integrations.
42
+ - Keep the Codex, Claude Code, Grok, Kiro, Qoder, Cursor, Antigravity, and OpenCode list/detail pages aligned around the same table-driven index/detail pattern when adding new source integrations.
38
43
 
39
44
  ## Shared Data Layer
40
45
 
@@ -48,6 +53,9 @@ The UI depends on root-package helpers via `@spiracha/*` path aliases:
48
53
  - `@spiracha/lib/claude-code-db`
49
54
  - `@spiracha/lib/claude-code-transcript-phase`
50
55
  - `@spiracha/lib/claude-code-transcript`
56
+ - `@spiracha/lib/grok-db`
57
+ - `@spiracha/lib/grok-transcript-phase`
58
+ - `@spiracha/lib/grok-transcript`
51
59
  - `@spiracha/lib/kiro-db`
52
60
  - `@spiracha/lib/kiro-transcript-phase`
53
61
  - `@spiracha/lib/kiro-transcript`
@@ -73,10 +81,10 @@ The `*-transcript-phase` helpers are intentionally browser-safe and may be impor
73
81
 
74
82
  Use the existing layers consistently:
75
83
 
76
- - TanStack Start server functions in `src/lib/codex-server.ts`, `src/lib/claude-code-server.ts`, `src/lib/kiro-server.ts`, `src/lib/qoder-server.ts`, `src/lib/cursor-server.ts`, `src/lib/antigravity-server.ts`, and `src/lib/opencode-server.ts`
84
+ - TanStack Start server functions in `src/lib/codex-server.ts`, `src/lib/claude-code-server.ts`, `src/lib/grok-server.ts`, `src/lib/kiro-server.ts`, `src/lib/qoder-server.ts`, `src/lib/cursor-server.ts`, `src/lib/antigravity-server.ts`, and `src/lib/opencode-server.ts`
77
85
  - Use for any browser-triggered read/write that needs Bun-only modules, DB access, filesystem access, Keychain access, or shared root-package helpers.
78
86
  - Use `.validator(...)` for input validation. Do not add new `.inputValidator(...)` calls.
79
- - TanStack Query query options in `src/lib/codex-queries.ts`, `src/lib/claude-code-queries.ts`, `src/lib/kiro-queries.ts`, `src/lib/qoder-queries.ts`, `src/lib/cursor-queries.ts`, `src/lib/antigravity-queries.ts`, and `src/lib/opencode-queries.ts`
87
+ - TanStack Query query options in `src/lib/codex-queries.ts`, `src/lib/claude-code-queries.ts`, `src/lib/grok-queries.ts`, `src/lib/kiro-queries.ts`, `src/lib/qoder-queries.ts`, `src/lib/cursor-queries.ts`, `src/lib/antigravity-queries.ts`, and `src/lib/opencode-queries.ts`
80
88
  - Use for client-side fetching, caching, retries, and invalidation of server-function results.
81
89
  - Shared root-package helpers under `@spiracha/lib/*`
82
90
  - Extend these when the behavior should stay shared between the UI and the stable data API.
@@ -103,7 +111,7 @@ For URL-backed route state, use `src/lib/route-search.ts` instead of ad hoc pars
103
111
  - UI component tests live under `src/**/*.vitest.tsx`.
104
112
  - Source-specific transcript adapter tests live next to their adapter files under `src/lib/*.vitest.ts`.
105
113
  - Route search parsing tests live next to the helper in `src/lib/route-search.vitest.ts`.
106
- - The root package wraps this Vitest suite from `src/ui-package.test.ts` so `rtk bun test` at the repo root exercises both the Bun suite and the UI suite.
114
+ - The root package wraps this Vitest suite from `src/ui-suite.test.ts` so `rtk bun test` exercises both the Bun suite and the UI suite.
107
115
 
108
116
  ## Design
109
117
 
@@ -113,5 +121,5 @@ For URL-backed route state, use `src/lib/route-search.ts` instead of ad hoc pars
113
121
  ## Constraints
114
122
 
115
123
  - Do not add a second database.
116
- - Do not duplicate transcript parsing or export rendering in this package.
124
+ - Do not duplicate transcript parsing or export rendering in the UI source tree.
117
125
  - Use the shared root-package helpers instead.
package/apps/ui/README.md CHANGED
@@ -17,8 +17,9 @@ The browser UI for browsing local Codex, Claude Code, Grok, Kiro, Qoder, Cursor,
17
17
 
18
18
  - lists derived Codex projects from the Codex SQLite database
19
19
  - lists Codex threads within a project in chronological order
20
- - shows Codex thread timelines, tool calls, metadata, and raw event context
21
- - exports Codex, Claude Code, Grok, Kiro, Qoder, Cursor, and OpenCode sessions or threads as Markdown, plain text, or optional zip archives with optional metadata, commentary, and tool-call inclusion
20
+ - shows Codex thread timelines, a dedicated tool activity and definition view, recorded goals, readable sandbox policy, metadata, and raw event context
21
+ - searches Codex projects from the app shell through the URL-backed `/codex?q=...` inventory filter
22
+ - exports Codex, Claude Code, Grok, Kiro, Qoder, Cursor, and OpenCode sessions or threads as Markdown, plain text, or optional zip archives with optional metadata, commentary, and tool-call inclusion; the last submitted choices persist while canceled drafts are discarded
22
23
  - lists Claude Code workspaces and sessions from local `~/.claude/projects` JSONL files
23
24
  - shows dedicated Claude Code session detail pages with reasoning, tool calls, token metadata, and export actions
24
25
  - lists Grok workspaces and sessions from local Grok session archives
@@ -35,26 +36,26 @@ The browser UI for browsing local Codex, Claude Code, Grok, Kiro, Qoder, Cursor,
35
36
  - unlocks Antigravity transcript export through macOS Keychain and exports conversations or artifacts as Markdown
36
37
  - lists OpenCode workspaces and sessions from the local OpenCode SQLite database
37
38
  - shows dedicated OpenCode session detail pages with reasoning, tool parts, MiniMax `<think>` blocks, token metadata, and export actions
38
- - shows dashboard and analytics summaries, including Codex token totals and tool-call frequency
39
+ - shows dashboard and project-scoped Codex analytics for token totals, average and median thread size, archive counts, tool usage, model tokens, client sources, and reasoning effort
39
40
  - keeps Codex inventory search and analytics project filters in URL search params for reloadable and shareable views
40
41
  - keeps source-specific commentary hidden by default while preserving final answers, with matching export filtering for Claude Code, Kiro, Qoder, and OpenCode
41
42
 
42
43
  ## Commands
43
44
 
44
45
  ```bash
45
- rtk bun run dev
46
+ rtk bun start
46
47
  rtk bun run build
47
- rtk bun run test
48
+ rtk bun run test:ui
48
49
  rtk bun run typecheck
49
50
  ```
50
51
 
51
52
  ## Runtime Note
52
53
 
53
- This package runs `vite` through `bun --bun ...`.
54
+ Run these commands from the repository root. The UI is part of the root package and intentionally has no nested package manifest.
54
55
 
55
- That is required because the TanStack Start server functions import shared root-package modules that use Bun-only features such as `bun:sqlite`.
56
+ Root-owned Vite commands use `apps/ui` as their internal working directory and run through `bun --bun`. That is required because TanStack Start derives part of its server plan from the application directory and the server functions use Bun-only modules such as `bun:sqlite`.
56
57
 
57
- If you change the scripts back to plain `vite` or a Node execution path, the UI server functions will fail at runtime.
58
+ Vitest uses its normal Node runtime.
58
59
 
59
60
  ## Configuration
60
61
 
@@ -72,6 +73,8 @@ Runtime configuration is intentionally small:
72
73
  - `SPIRACHA_TRANSCRIPT_LOAD_CONCURRENCY`
73
74
  - Optional positive integer for detail-page transcript loading concurrency across sources.
74
75
  - Defaults to `3` and is capped at `16` to protect the server from excessive parallel disk and database work.
76
+ - `SPIRACHA_TRANSCRIPT_LOAD_LOGS`
77
+ - Set to `1` to log transcript-loader queue and timing diagnostics. Disabled by default so library and CLI consumers stay quiet.
75
78
  - `SPIRACHA_CLAUDE_CODE_PROJECTS_DIR`
76
79
  - Optional absolute path to the Claude Code projects directory.
77
80
  - If unset, Spiracha reads `${SPIRACHA_CLAUDE_CODE_DATA_DIR:-~/.claude}/projects`. `SPIRACHA_CLAUDE_CODE_DIR` and `SPIRACHA_CLAUDE_HOME` are also accepted aliases for the Claude Code data directory.
@@ -93,6 +96,8 @@ Runtime configuration is intentionally small:
93
96
  - `SPIRACHA_OPENCODE_DB_CONCURRENCY`
94
97
  - Optional positive integer for concurrent OpenCode database reads.
95
98
  - Defaults to `2`.
99
+ - `SPIRACHA_OPENCODE_DB_LOGS`
100
+ - Set to `1` to log OpenCode database queue and timing diagnostics. Disabled by default.
96
101
  - `SPIRACHA_CURSOR_USER_DIR`
97
102
  - Optional absolute path to Cursor's `User` directory.
98
103
  - If unset, Spiracha reads the platform default Cursor user-data directory.
@@ -121,6 +126,8 @@ Default source locations:
121
126
 
122
127
  Codex analytics cache keys are based on Codex DB row metadata instead of statting every rollout file before cache hits. That keeps large histories responsive. The tradeoff is that manual JSONL edits outside Codex do not invalidate analytics unless DB row metadata changes or the temporary UI cache is cleared.
123
128
 
129
+ The temporary UI cache is pruned opportunistically at read/write boundaries, with cleanup scans throttled to once per minute. Entries expire after 24 hours and the retained cache is capped at 256 MiB; recently accessed entries refresh their age so hot data survives eviction.
130
+
124
131
  Transcript detail pages expose the same display controls across sources: user messages, commentary, tool calls, extra events, and raw JSON. Claude Code assistant lead-ins are classified from `stop_reason`, Kiro assistant phases are classified per user turn from session and execution files, Qoder shows local prompt history plus checkpoint file operations, and OpenCode assistant phases are classified per assistant run after stripping MiniMax `<think>` blocks into commentary. OpenCode think-tag extraction preserves literal `<think>` examples inside Markdown code spans and fenced code blocks.
125
132
 
126
133
  ## Routes
@@ -178,11 +185,11 @@ Transcript detail pages expose the same display controls across sources: user me
178
185
  - `/$threadId`
179
186
  - shortcut redirect to the thread detail page for pasted Codex thread UUIDs
180
187
  - `/analytics`
181
- - Codex token and tool-call analytics, with `project` as the route search param
188
+ - project-scoped Codex token, archive, tool, model, client-source, and reasoning-effort analytics, with `project` as the route search param
182
189
 
183
190
  ## Testing
184
191
 
185
192
  - UI component tests live under `src/**/*.vitest.tsx`.
186
193
  - Source-specific transcript event adapter tests live under `src/lib/*.vitest.ts`.
187
194
  - Route search parsing tests live in `src/lib/route-search.vitest.ts`.
188
- - The repo root wraps this Vitest suite from `src/ui-package.test.ts`, so `rtk bun test` at the root covers both the Bun tests and the UI tests.
195
+ - The repo root wraps this Vitest suite from `src/ui-suite.test.ts`, so `rtk bun test` at the root covers both the Bun tests and the UI tests.
@@ -0,0 +1,106 @@
1
+ import type {
2
+ CodexAnalytics,
3
+ DistributionItem,
4
+ ModelTokenSummary,
5
+ ToolUsageSummary,
6
+ } from '@spiracha/lib/codex-browser-types';
7
+ import { createColumnHelper } from '@tanstack/react-table';
8
+ import { formatNumber, formatTokens } from '#/lib/formatters';
9
+ import { DataTable } from './data-table';
10
+
11
+ const toolUsageColumnHelper = createColumnHelper<ToolUsageSummary>();
12
+ const toolUsageColumns = [
13
+ toolUsageColumnHelper.accessor('name', {
14
+ cell: (info) => <span className="font-mono text-sm">{info.getValue()}</span>,
15
+ header: 'Tool',
16
+ }),
17
+ toolUsageColumnHelper.accessor('count', {
18
+ cell: (info) => <span className="font-mono text-sm">{formatNumber(info.getValue())}</span>,
19
+ header: 'Calls',
20
+ }),
21
+ ] as const;
22
+
23
+ const modelColumnHelper = createColumnHelper<ModelTokenSummary>();
24
+ const modelColumns = [
25
+ modelColumnHelper.accessor('model', {
26
+ cell: (info) => <span className="font-mono text-sm">{info.getValue()}</span>,
27
+ header: 'Model',
28
+ }),
29
+ modelColumnHelper.accessor('threadCount', {
30
+ cell: (info) => <span className="font-mono text-sm">{formatNumber(info.getValue())}</span>,
31
+ header: 'Threads',
32
+ }),
33
+ modelColumnHelper.accessor('totalTokens', {
34
+ cell: (info) => <span className="font-mono text-sm">{formatTokens(info.getValue())}</span>,
35
+ header: 'Tokens',
36
+ }),
37
+ ] as const;
38
+
39
+ const distributionColumnHelper = createColumnHelper<DistributionItem>();
40
+ const distributionColumns = [
41
+ distributionColumnHelper.accessor('label', {
42
+ cell: (info) => <span className="font-mono text-sm">{info.getValue()}</span>,
43
+ header: 'Value',
44
+ }),
45
+ distributionColumnHelper.accessor('count', {
46
+ cell: (info) => <span className="font-mono text-sm">{formatNumber(info.getValue())}</span>,
47
+ header: 'Threads',
48
+ }),
49
+ ] as const;
50
+
51
+ type AnalyticsBreakdownsProps = Pick<CodexAnalytics, 'modelsByTokens' | 'reasoningEfforts' | 'sources' | 'toolUsage'>;
52
+
53
+ export const AnalyticsBreakdowns = ({
54
+ modelsByTokens,
55
+ reasoningEfforts,
56
+ sources,
57
+ toolUsage,
58
+ }: AnalyticsBreakdownsProps) => {
59
+ return (
60
+ <div className="grid gap-6 xl:grid-cols-2">
61
+ <section className="space-y-4">
62
+ <div>
63
+ <h3 className="font-semibold text-sm">Most frequent tool calls</h3>
64
+ <p className="mt-1 text-[var(--muted-foreground)] text-sm">
65
+ Useful for future prompt and tool optimization work.
66
+ </p>
67
+ </div>
68
+ <DataTable columns={toolUsageColumns} data={toolUsage} emptyMessage="No tool calls recorded." />
69
+ </section>
70
+
71
+ <section className="space-y-4">
72
+ <div>
73
+ <h3 className="font-semibold text-sm">Model token breakdown</h3>
74
+ <p className="mt-1 text-[var(--muted-foreground)] text-sm">
75
+ Compare model usage and token concentration within the current project scope.
76
+ </p>
77
+ </div>
78
+ <DataTable columns={modelColumns} data={modelsByTokens} emptyMessage="No model usage recorded." />
79
+ </section>
80
+
81
+ <section className="space-y-4">
82
+ <div>
83
+ <h3 className="font-semibold text-sm">Client source breakdown</h3>
84
+ <p className="mt-1 text-[var(--muted-foreground)] text-sm">
85
+ See which Codex clients created threads in the current scope.
86
+ </p>
87
+ </div>
88
+ <DataTable columns={distributionColumns} data={sources} emptyMessage="No client sources recorded." />
89
+ </section>
90
+
91
+ <section className="space-y-4">
92
+ <div>
93
+ <h3 className="font-semibold text-sm">Reasoning effort breakdown</h3>
94
+ <p className="mt-1 text-[var(--muted-foreground)] text-sm">
95
+ Compare configured reasoning effort across threads.
96
+ </p>
97
+ </div>
98
+ <DataTable
99
+ columns={distributionColumns}
100
+ data={reasoningEfforts}
101
+ emptyMessage="No reasoning effort recorded."
102
+ />
103
+ </section>
104
+ </div>
105
+ );
106
+ };
@@ -155,6 +155,7 @@ const columns = (
155
155
  <DropdownMenu>
156
156
  <DropdownMenuTrigger asChild>
157
157
  <Button
158
+ aria-label={`Actions for ${info.row.original.title}`}
158
159
  className="rounded-full"
159
160
  size="icon"
160
161
  type="button"
@@ -1,4 +1,4 @@
1
- import { Link, useRouterState } from '@tanstack/react-router';
1
+ import { Link, useNavigate, useRouterState } from '@tanstack/react-router';
2
2
  import {
3
3
  BarChart3,
4
4
  Bot,
@@ -6,12 +6,13 @@ import {
6
6
  Code2,
7
7
  FolderOpen,
8
8
  LayoutDashboard,
9
+ Search,
9
10
  Settings2,
10
11
  Sparkles,
11
12
  SquareTerminal,
12
13
  Workflow,
13
14
  } from 'lucide-react';
14
- import type { PropsWithChildren } from 'react';
15
+ import { type FormEvent, type PropsWithChildren, useEffect, useState } from 'react';
15
16
  import { packageMetadata } from '#/lib/package-metadata';
16
17
  import { cn } from '#/lib/utils';
17
18
  import { ThemeToggle } from './theme-toggle';
@@ -59,9 +60,33 @@ const GitHubIcon = ({ className }: { className?: string }) => (
59
60
  );
60
61
 
61
62
  export function AppShell({ children }: PropsWithChildren) {
63
+ const navigate = useNavigate();
62
64
  const pathname = useRouterState({
63
65
  select: (state) => state.location.pathname,
64
66
  });
67
+ const routeProjectQuery = useRouterState({
68
+ select: (state) => {
69
+ if (state.location.pathname !== '/codex' && state.location.pathname !== '/codex/') {
70
+ return '';
71
+ }
72
+ const query = (state.location.search as Record<string, unknown>).q;
73
+ return typeof query === 'string' ? query : '';
74
+ },
75
+ });
76
+ const [projectQuery, setProjectQuery] = useState(routeProjectQuery);
77
+
78
+ useEffect(() => {
79
+ setProjectQuery(routeProjectQuery);
80
+ }, [routeProjectQuery]);
81
+
82
+ const handleProjectSearch = (event: FormEvent<HTMLFormElement>) => {
83
+ event.preventDefault();
84
+ const query = projectQuery.trim();
85
+ void navigate({
86
+ search: query ? { q: query } : {},
87
+ to: '/codex',
88
+ });
89
+ };
65
90
 
66
91
  return (
67
92
  <div className="min-h-screen bg-[var(--background)] text-[var(--foreground)]">
@@ -92,7 +117,24 @@ export function AppShell({ children }: PropsWithChildren) {
92
117
  <ThemeToggle />
93
118
  </div>
94
119
 
95
- <nav className="mt-5 grid gap-1">
120
+ <search aria-label="Global project search" className="mt-5">
121
+ <form className="relative" onSubmit={handleProjectSearch}>
122
+ <Search
123
+ aria-hidden="true"
124
+ className="pointer-events-none absolute top-1/2 left-3 size-4 -translate-y-1/2 text-[var(--muted-foreground)]"
125
+ />
126
+ <input
127
+ aria-label="Search Codex projects"
128
+ className="h-9 w-full rounded-lg border border-[var(--border)] bg-[var(--panel-secondary)] pr-3 pl-9 text-sm outline-none transition placeholder:text-[var(--muted-foreground)] focus:border-[var(--accent)] focus:ring-2 focus:ring-[var(--accent)]/20"
129
+ placeholder="Search Codex projects"
130
+ type="search"
131
+ value={projectQuery}
132
+ onChange={(event) => setProjectQuery(event.target.value)}
133
+ />
134
+ </form>
135
+ </search>
136
+
137
+ <nav className="mt-3 grid gap-1">
96
138
  {navItems.map((item) => {
97
139
  const active = isNavItemActive(pathname, item);
98
140
  const Icon = item.icon;
@@ -9,7 +9,7 @@ import {
9
9
  useReactTable,
10
10
  } from '@tanstack/react-table';
11
11
  import { ArrowDownUp } from 'lucide-react';
12
- import { type ReactNode, useRef, useState } from 'react';
12
+ import { type ReactNode, useEffect, useMemo, useRef, useState } from 'react';
13
13
  import { Checkbox } from '#/components/ui/checkbox';
14
14
  import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '#/components/ui/table';
15
15
  import { cn } from '#/lib/utils';
@@ -80,6 +80,22 @@ export function DataTable<TData>({
80
80
  const [rowSelection, setRowSelection] = useState<RowSelectionState>({});
81
81
  const lastSelectedRowIdRef = useRef<string | null>(null);
82
82
  const pendingShiftSelectionRowIdRef = useRef<string | null>(null);
83
+ const currentRowIds = useMemo(
84
+ () => new Set(data.map((row, index) => (getRowId ? getRowId(row, index) : String(index)))),
85
+ [data, getRowId],
86
+ );
87
+
88
+ useEffect(() => {
89
+ setRowSelection((selection) => {
90
+ const next = Object.fromEntries(
91
+ Object.entries(selection).filter(([rowId, selected]) => selected && currentRowIds.has(rowId)),
92
+ );
93
+ return Object.keys(next).length === Object.keys(selection).length ? selection : next;
94
+ });
95
+ if (lastSelectedRowIdRef.current && !currentRowIds.has(lastSelectedRowIdRef.current)) {
96
+ lastSelectedRowIdRef.current = null;
97
+ }
98
+ }, [currentRowIds]);
83
99
 
84
100
  const updateSelectionForRow = (rowId: string, checked: boolean, shiftKey: boolean) => {
85
101
  const visibleRowIds = table.getRowModel().rows.map((row) => row.id);