@cortexkit/opencode-magic-context 0.21.7 → 0.22.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 (214) hide show
  1. package/README.md +116 -325
  2. package/dist/agents/magic-context-prompt.d.ts.map +1 -1
  3. package/dist/agents/permissions.d.ts +29 -14
  4. package/dist/agents/permissions.d.ts.map +1 -1
  5. package/dist/config/index.d.ts.map +1 -1
  6. package/dist/config/migrate-experimental.d.ts +29 -0
  7. package/dist/config/migrate-experimental.d.ts.map +1 -0
  8. package/dist/config/schema/magic-context.d.ts +80 -104
  9. package/dist/config/schema/magic-context.d.ts.map +1 -1
  10. package/dist/features/builtin-commands/commands.d.ts.map +1 -1
  11. package/dist/features/magic-context/compartment-embedding.d.ts +34 -0
  12. package/dist/features/magic-context/compartment-embedding.d.ts.map +1 -0
  13. package/dist/features/magic-context/compartment-events.d.ts +50 -0
  14. package/dist/features/magic-context/compartment-events.d.ts.map +1 -0
  15. package/dist/features/magic-context/compartment-storage.d.ts +22 -0
  16. package/dist/features/magic-context/compartment-storage.d.ts.map +1 -1
  17. package/dist/features/magic-context/dreamer/lease.d.ts.map +1 -1
  18. package/dist/features/magic-context/dreamer/queue.d.ts +13 -2
  19. package/dist/features/magic-context/dreamer/queue.d.ts.map +1 -1
  20. package/dist/features/magic-context/dreamer/runner.d.ts +11 -0
  21. package/dist/features/magic-context/dreamer/runner.d.ts.map +1 -1
  22. package/dist/features/magic-context/dreamer/task-prompts.d.ts +1 -1
  23. package/dist/features/magic-context/dreamer/task-prompts.d.ts.map +1 -1
  24. package/dist/features/magic-context/git-commits/git-log-reader.d.ts.map +1 -1
  25. package/dist/features/magic-context/key-files/identify-key-files.d.ts +1 -1
  26. package/dist/features/magic-context/key-files/identify-key-files.d.ts.map +1 -1
  27. package/dist/features/magic-context/key-files/project-key-files.d.ts.map +1 -1
  28. package/dist/features/magic-context/key-files/read-stats.d.ts +1 -1
  29. package/dist/features/magic-context/key-files/read-stats.d.ts.map +1 -1
  30. package/dist/features/magic-context/memory/constants.d.ts +4 -0
  31. package/dist/features/magic-context/memory/constants.d.ts.map +1 -1
  32. package/dist/features/magic-context/memory/embedding-local.d.ts.map +1 -1
  33. package/dist/features/magic-context/memory/index.d.ts +1 -1
  34. package/dist/features/magic-context/memory/index.d.ts.map +1 -1
  35. package/dist/features/magic-context/memory/memory-migration.d.ts +133 -0
  36. package/dist/features/magic-context/memory/memory-migration.d.ts.map +1 -0
  37. package/dist/features/magic-context/memory/project-identity.d.ts +38 -7
  38. package/dist/features/magic-context/memory/project-identity.d.ts.map +1 -1
  39. package/dist/features/magic-context/memory/storage-memory-fts.d.ts.map +1 -1
  40. package/dist/features/magic-context/memory/storage-memory.d.ts +15 -1
  41. package/dist/features/magic-context/memory/storage-memory.d.ts.map +1 -1
  42. package/dist/features/magic-context/memory/types.d.ts +3 -1
  43. package/dist/features/magic-context/memory/types.d.ts.map +1 -1
  44. package/dist/features/magic-context/message-index.d.ts.map +1 -1
  45. package/dist/features/magic-context/migrations.d.ts +7 -0
  46. package/dist/features/magic-context/migrations.d.ts.map +1 -1
  47. package/dist/features/magic-context/project-docs-hash.d.ts +6 -0
  48. package/dist/features/magic-context/project-docs-hash.d.ts.map +1 -0
  49. package/dist/features/magic-context/project-identity.d.ts +2 -0
  50. package/dist/features/magic-context/project-identity.d.ts.map +1 -0
  51. package/dist/features/magic-context/sidekick/agent.d.ts.map +1 -1
  52. package/dist/features/magic-context/storage-db.d.ts +51 -7
  53. package/dist/features/magic-context/storage-db.d.ts.map +1 -1
  54. package/dist/features/magic-context/storage-historian-runs.d.ts +73 -0
  55. package/dist/features/magic-context/storage-historian-runs.d.ts.map +1 -0
  56. package/dist/features/magic-context/storage-identity-rekey-map.d.ts +11 -0
  57. package/dist/features/magic-context/storage-identity-rekey-map.d.ts.map +1 -0
  58. package/dist/features/magic-context/storage-m0-mutation-log.d.ts +22 -0
  59. package/dist/features/magic-context/storage-m0-mutation-log.d.ts.map +1 -0
  60. package/dist/features/magic-context/storage-memory-mutation-log.d.ts +25 -0
  61. package/dist/features/magic-context/storage-memory-mutation-log.d.ts.map +1 -0
  62. package/dist/features/magic-context/storage-meta-persisted.d.ts +5 -0
  63. package/dist/features/magic-context/storage-meta-persisted.d.ts.map +1 -1
  64. package/dist/features/magic-context/storage-meta-session.d.ts.map +1 -1
  65. package/dist/features/magic-context/storage-meta-shared.d.ts +44 -0
  66. package/dist/features/magic-context/storage-meta-shared.d.ts.map +1 -1
  67. package/dist/features/magic-context/storage-meta.d.ts +2 -1
  68. package/dist/features/magic-context/storage-meta.d.ts.map +1 -1
  69. package/dist/features/magic-context/storage-project-state.d.ts +19 -0
  70. package/dist/features/magic-context/storage-project-state.d.ts.map +1 -0
  71. package/dist/features/magic-context/storage-subagent-invocations.d.ts +61 -0
  72. package/dist/features/magic-context/storage-subagent-invocations.d.ts.map +1 -0
  73. package/dist/features/magic-context/storage-tags.d.ts +21 -1
  74. package/dist/features/magic-context/storage-tags.d.ts.map +1 -1
  75. package/dist/features/magic-context/storage-v22-backfill-failures.d.ts +24 -0
  76. package/dist/features/magic-context/storage-v22-backfill-failures.d.ts.map +1 -0
  77. package/dist/features/magic-context/storage.d.ts +13 -3
  78. package/dist/features/magic-context/storage.d.ts.map +1 -1
  79. package/dist/features/magic-context/subagent-token-capture.d.ts +33 -0
  80. package/dist/features/magic-context/subagent-token-capture.d.ts.map +1 -0
  81. package/dist/features/magic-context/tagger.d.ts +15 -1
  82. package/dist/features/magic-context/tagger.d.ts.map +1 -1
  83. package/dist/features/magic-context/types.d.ts +21 -0
  84. package/dist/features/magic-context/types.d.ts.map +1 -1
  85. package/dist/features/magic-context/user-memory/review-user-memories.d.ts.map +1 -1
  86. package/dist/features/magic-context/user-memory/storage-user-memory.d.ts.map +1 -1
  87. package/dist/features/magic-context/v22-deferred-backfill.d.ts +46 -0
  88. package/dist/features/magic-context/v22-deferred-backfill.d.ts.map +1 -0
  89. package/dist/features/magic-context/work-metrics.d.ts +79 -0
  90. package/dist/features/magic-context/work-metrics.d.ts.map +1 -0
  91. package/dist/hooks/auto-update-checker/cache.d.ts.map +1 -1
  92. package/dist/hooks/auto-update-checker/checker.d.ts.map +1 -1
  93. package/dist/hooks/magic-context/cache-busting-signals.d.ts +9 -0
  94. package/dist/hooks/magic-context/cache-busting-signals.d.ts.map +1 -1
  95. package/dist/hooks/magic-context/command-handler.d.ts +13 -1
  96. package/dist/hooks/magic-context/command-handler.d.ts.map +1 -1
  97. package/dist/hooks/magic-context/compartment-parser.d.ts +25 -0
  98. package/dist/hooks/magic-context/compartment-parser.d.ts.map +1 -1
  99. package/dist/hooks/magic-context/compartment-prompt.d.ts +27 -16
  100. package/dist/hooks/magic-context/compartment-prompt.d.ts.map +1 -1
  101. package/dist/hooks/magic-context/compartment-runner-historian.d.ts +2 -0
  102. package/dist/hooks/magic-context/compartment-runner-historian.d.ts.map +1 -1
  103. package/dist/hooks/magic-context/compartment-runner-incremental.d.ts.map +1 -1
  104. package/dist/hooks/magic-context/compartment-runner-mapping.d.ts +6 -2
  105. package/dist/hooks/magic-context/compartment-runner-mapping.d.ts.map +1 -1
  106. package/dist/hooks/magic-context/compartment-runner-partial-recomp.d.ts.map +1 -1
  107. package/dist/hooks/magic-context/compartment-runner-recomp.d.ts +9 -1
  108. package/dist/hooks/magic-context/compartment-runner-recomp.d.ts.map +1 -1
  109. package/dist/hooks/magic-context/compartment-runner-types.d.ts +68 -4
  110. package/dist/hooks/magic-context/compartment-runner-types.d.ts.map +1 -1
  111. package/dist/hooks/magic-context/compartment-runner-validation.d.ts.map +1 -1
  112. package/dist/hooks/magic-context/compartment-runner.d.ts.map +1 -1
  113. package/dist/hooks/magic-context/decay-curve.d.ts +78 -0
  114. package/dist/hooks/magic-context/decay-curve.d.ts.map +1 -0
  115. package/dist/hooks/magic-context/decay-render.d.ts +67 -0
  116. package/dist/hooks/magic-context/decay-render.d.ts.map +1 -0
  117. package/dist/hooks/magic-context/event-handler.d.ts +1 -1
  118. package/dist/hooks/magic-context/event-handler.d.ts.map +1 -1
  119. package/dist/hooks/magic-context/event-resolvers.d.ts +17 -0
  120. package/dist/hooks/magic-context/event-resolvers.d.ts.map +1 -1
  121. package/dist/hooks/magic-context/execute-status.d.ts.map +1 -1
  122. package/dist/hooks/magic-context/historian-prompt.generated.d.ts +2 -0
  123. package/dist/hooks/magic-context/historian-prompt.generated.d.ts.map +1 -0
  124. package/dist/hooks/magic-context/hook-handlers.d.ts +3 -0
  125. package/dist/hooks/magic-context/hook-handlers.d.ts.map +1 -1
  126. package/dist/hooks/magic-context/hook.d.ts +9 -21
  127. package/dist/hooks/magic-context/hook.d.ts.map +1 -1
  128. package/dist/hooks/magic-context/inject-compartments.d.ts +126 -0
  129. package/dist/hooks/magic-context/inject-compartments.d.ts.map +1 -1
  130. package/dist/hooks/magic-context/key-files-block.d.ts.map +1 -1
  131. package/dist/hooks/magic-context/live-session-state.d.ts +9 -0
  132. package/dist/hooks/magic-context/live-session-state.d.ts.map +1 -1
  133. package/dist/hooks/magic-context/m0-token-breakdown.d.ts +35 -0
  134. package/dist/hooks/magic-context/m0-token-breakdown.d.ts.map +1 -0
  135. package/dist/hooks/magic-context/read-session-chunk.d.ts +9 -0
  136. package/dist/hooks/magic-context/read-session-chunk.d.ts.map +1 -1
  137. package/dist/hooks/magic-context/read-session-db.d.ts +7 -0
  138. package/dist/hooks/magic-context/read-session-db.d.ts.map +1 -1
  139. package/dist/hooks/magic-context/recomp-orchestrator.d.ts +104 -0
  140. package/dist/hooks/magic-context/recomp-orchestrator.d.ts.map +1 -0
  141. package/dist/hooks/magic-context/reference-retrieval.d.ts +61 -0
  142. package/dist/hooks/magic-context/reference-retrieval.d.ts.map +1 -0
  143. package/dist/hooks/magic-context/reference-seeds.generated.d.ts +8 -0
  144. package/dist/hooks/magic-context/reference-seeds.generated.d.ts.map +1 -0
  145. package/dist/hooks/magic-context/send-session-notification.d.ts +1 -1
  146. package/dist/hooks/magic-context/send-session-notification.d.ts.map +1 -1
  147. package/dist/hooks/magic-context/system-prompt-hash.d.ts +5 -6
  148. package/dist/hooks/magic-context/system-prompt-hash.d.ts.map +1 -1
  149. package/dist/hooks/magic-context/tag-messages.d.ts.map +1 -1
  150. package/dist/hooks/magic-context/tokenizer-calibration.d.ts +6 -0
  151. package/dist/hooks/magic-context/tokenizer-calibration.d.ts.map +1 -1
  152. package/dist/hooks/magic-context/transform-compartment-phase.d.ts +0 -7
  153. package/dist/hooks/magic-context/transform-compartment-phase.d.ts.map +1 -1
  154. package/dist/hooks/magic-context/transform-postprocess-phase.d.ts +18 -0
  155. package/dist/hooks/magic-context/transform-postprocess-phase.d.ts.map +1 -1
  156. package/dist/hooks/magic-context/transform.d.ts +9 -7
  157. package/dist/hooks/magic-context/transform.d.ts.map +1 -1
  158. package/dist/hooks/magic-context/upgrade-reminder.d.ts +73 -0
  159. package/dist/hooks/magic-context/upgrade-reminder.d.ts.map +1 -0
  160. package/dist/index.d.ts.map +1 -1
  161. package/dist/index.js +22111 -16352
  162. package/dist/plugin/conflict-warning-hook.d.ts +13 -0
  163. package/dist/plugin/conflict-warning-hook.d.ts.map +1 -1
  164. package/dist/plugin/dream-timer.d.ts.map +1 -1
  165. package/dist/plugin/hooks/create-session-hooks.d.ts.map +1 -1
  166. package/dist/plugin/messages-transform.d.ts.map +1 -1
  167. package/dist/plugin/rpc-handlers.d.ts.map +1 -1
  168. package/dist/plugin/tool-registry.d.ts.map +1 -1
  169. package/dist/shared/announcement.d.ts +1 -1
  170. package/dist/shared/announcement.d.ts.map +1 -1
  171. package/dist/shared/rpc-client.d.ts +1 -0
  172. package/dist/shared/rpc-client.d.ts.map +1 -1
  173. package/dist/shared/rpc-notifications.d.ts +27 -5
  174. package/dist/shared/rpc-notifications.d.ts.map +1 -1
  175. package/dist/shared/rpc-server.d.ts +1 -0
  176. package/dist/shared/rpc-server.d.ts.map +1 -1
  177. package/dist/shared/rpc-types.d.ts +32 -2
  178. package/dist/shared/rpc-types.d.ts.map +1 -1
  179. package/dist/shared/rpc-utils.d.ts +9 -0
  180. package/dist/shared/rpc-utils.d.ts.map +1 -1
  181. package/dist/shared/sqlite-helpers.d.ts +7 -7
  182. package/dist/shared/sqlite.d.ts +23 -14
  183. package/dist/shared/sqlite.d.ts.map +1 -1
  184. package/dist/shared/subagent-runner.d.ts +5 -0
  185. package/dist/shared/subagent-runner.d.ts.map +1 -1
  186. package/dist/shared/tag-transcript.d.ts +10 -1
  187. package/dist/shared/tag-transcript.d.ts.map +1 -1
  188. package/dist/tools/ctx-expand/tools.d.ts +5 -1
  189. package/dist/tools/ctx-expand/tools.d.ts.map +1 -1
  190. package/dist/tools/ctx-memory/tools.d.ts.map +1 -1
  191. package/dist/tui/data/context-db.d.ts +16 -1
  192. package/dist/tui/data/context-db.d.ts.map +1 -1
  193. package/package.json +2 -4
  194. package/src/shared/announcement.ts +6 -7
  195. package/src/shared/rpc-client.test.ts +49 -2
  196. package/src/shared/rpc-client.ts +19 -9
  197. package/src/shared/rpc-notifications.test.ts +54 -1
  198. package/src/shared/rpc-notifications.ts +82 -13
  199. package/src/shared/rpc-server.ts +33 -4
  200. package/src/shared/rpc-types.ts +32 -2
  201. package/src/shared/rpc-utils.ts +10 -0
  202. package/src/shared/sqlite-helpers.ts +9 -9
  203. package/src/shared/sqlite.ts +99 -80
  204. package/src/shared/subagent-runner.ts +14 -0
  205. package/src/shared/tag-transcript.test.ts +280 -0
  206. package/src/shared/tag-transcript.ts +162 -33
  207. package/src/tui/data/context-db.ts +77 -11
  208. package/src/tui/index.tsx +240 -57
  209. package/src/tui/slots/sidebar-content.tsx +415 -101
  210. package/dist/hooks/magic-context/compartment-runner-compressor.d.ts +0 -87
  211. package/dist/hooks/magic-context/compartment-runner-compressor.d.ts.map +0 -1
  212. package/dist/shared/native-binding.d.ts +0 -87
  213. package/dist/shared/native-binding.d.ts.map +0 -1
  214. package/src/shared/native-binding.ts +0 -311
package/src/tui/index.tsx CHANGED
@@ -4,9 +4,9 @@ import { existsSync, mkdirSync, writeFileSync } from "node:fs"
4
4
  import { dirname, join } from "node:path"
5
5
  import { createMemo } from "solid-js"
6
6
  import type { TuiPlugin, TuiPluginApi, TuiThemeCurrent } from "@opencode-ai/plugin/tui"
7
- import { createSidebarContentSlot } from "./slots/sidebar-content"
7
+ import { createSidebarContentSlot, kickRecompProgressRefresh } from "./slots/sidebar-content"
8
8
  import packageJson from "../../package.json"
9
- import { closeRpc, consumeTuiMessages, getAnnouncement, getCompartmentCount, initRpcClient, loadStatusDetail, markAnnounced, requestRecomp, type StatusDetail } from "./data/context-db"
9
+ import { closeRpc, consumeTuiMessages, dismissUpgradeReminder, getAnnouncement, getCompartmentCount, getRpcGeneration, initRpcClient, loadStatusDetail, markAnnounced, markTuiMessagesHandled, requestRecomp, requestUpgrade, type TuiMessage, type StatusDetail } from "./data/context-db"
10
10
  import { formatThresholdPercent } from "../shared/format-threshold"
11
11
  import { detectConflicts } from "../shared/conflict-detector"
12
12
  import { fixConflicts } from "../shared/conflict-fixer"
@@ -213,9 +213,11 @@ const StatusDialog = (props: { api: TuiPluginApi; s: StatusDetail }) => {
213
213
  const COLORS = {
214
214
  // Cool / structured — injected by the plugin into message[0]
215
215
  system: "#c084fc",
216
+ docs: "#22d3ee",
216
217
  compartments: "#60a5fa",
217
218
  facts: "#fbbf24",
218
219
  memories: "#34d399",
220
+ profile: "#a3e635",
219
221
  // Warm / user-facing — chat and tool traffic
220
222
  conversation: "#f87171",
221
223
  toolCalls: "#fb923c",
@@ -229,6 +231,8 @@ const StatusDialog = (props: { api: TuiPluginApi; s: StatusDetail }) => {
229
231
 
230
232
  if (d.systemPromptTokens > 0)
231
233
  segs.push({ label: "System", tokens: d.systemPromptTokens, color: COLORS.system })
234
+ if (d.docsTokens > 0)
235
+ segs.push({ label: "Docs", tokens: d.docsTokens, color: COLORS.docs })
232
236
  if (d.compartmentTokens > 0)
233
237
  segs.push({
234
238
  label: "Compartments",
@@ -250,6 +254,8 @@ const StatusDialog = (props: { api: TuiPluginApi; s: StatusDetail }) => {
250
254
  color: COLORS.memories,
251
255
  detail: `(${d.memoryBlockCount})`,
252
256
  })
257
+ if (d.profileTokens > 0)
258
+ segs.push({ label: "User Profile", tokens: d.profileTokens, color: COLORS.profile })
253
259
 
254
260
  if (d.conversationTokens > 0)
255
261
  segs.push({ label: "Conversation", tokens: d.conversationTokens, color: COLORS.conversation })
@@ -261,27 +267,12 @@ const StatusDialog = (props: { api: TuiPluginApi; s: StatusDetail }) => {
261
267
  return { segs, total }
262
268
  }
263
269
 
264
- const barWidth = 56
265
- const barSegments = () => {
266
- const { segs, total } = breakdownSegments()
267
- if (segs.length === 0) return []
268
-
269
- let widths = segs.map((seg) => Math.max(1, Math.round((seg.tokens / total) * barWidth)))
270
- let sum = widths.reduce((a, b) => a + b, 0)
271
- while (sum > barWidth) {
272
- const maxIdx = widths.indexOf(Math.max(...widths))
273
- if (widths[maxIdx] > 1) { widths[maxIdx]--; sum-- } else break
274
- }
275
- while (sum < barWidth) {
276
- const maxIdx = widths.indexOf(Math.max(...widths))
277
- widths[maxIdx]++; sum++
278
- }
279
-
280
- return segs.map((seg, i) => ({
281
- chars: "█".repeat(widths[i] || 0),
282
- color: seg.color,
283
- }))
284
- }
270
+ // The status-dialog breakdown bar uses flex layout (same approach as the
271
+ // sidebar breakdown). Each segment becomes a colored box with
272
+ // flexGrow=tokens and flexBasis=0, parent has width="100%", so opentui
273
+ // distributes the dialog's full width proportionally regardless of the
274
+ // dialog's actual rendered width.
275
+ const barSegments = () => breakdownSegments().segs.filter((seg) => seg.tokens > 0)
285
276
 
286
277
  return (
287
278
  <box flexDirection="column" width="100%" paddingLeft={2} paddingRight={2} paddingTop={1} paddingBottom={1}>
@@ -304,10 +295,17 @@ const StatusDialog = (props: { api: TuiPluginApi; s: StatusDetail }) => {
304
295
  </text>
305
296
  </box>
306
297
 
307
- {/* Segmented breakdown bar */}
308
- <box flexDirection="row">
309
- {barSegments().map((seg, i) => (
310
- <text key={i} fg={seg.color}>{seg.chars}</text>
298
+ {/* Segmented breakdown bar: flex row of colored boxes filling
299
+ the dialog width. See barSegments comment above. */}
300
+ <box width="100%" flexDirection="row" height={1}>
301
+ {barSegments().map((seg) => (
302
+ <box
303
+ key={seg.label}
304
+ flexGrow={Math.max(1, seg.tokens)}
305
+ flexBasis={0}
306
+ height={1}
307
+ backgroundColor={seg.color}
308
+ />
311
309
  ))}
312
310
  </box>
313
311
 
@@ -324,6 +322,35 @@ const StatusDialog = (props: { api: TuiPluginApi; s: StatusDetail }) => {
324
322
  })}
325
323
  </box>
326
324
 
325
+ {/* Recomp / session-upgrade live progress (full width, only while
326
+ running or just finished — dogfood 2026-05-30). */}
327
+ {s().recompProgress && (
328
+ <box marginTop={1} width="100%" flexDirection="column">
329
+ <text fg={t().text}><b>Recomp / Upgrade</b></text>
330
+ {(() => {
331
+ const p = s().recompProgress!
332
+ if (p.phase === "recomp") {
333
+ const frac = p.totalMessages > 0 ? p.processedMessages / p.totalMessages : 0
334
+ const width = 24
335
+ const filled = Math.round(Math.max(0, Math.min(1, frac)) * width)
336
+ const bar = p.totalMessages > 0
337
+ ? `[${"█".repeat(filled)}${"░".repeat(width - filled)}]`
338
+ : "(starting…)"
339
+ return (
340
+ <>
341
+ <R t={t()} l="upgrading" v={p.totalMessages > 0 ? `${bar} ${Math.round(frac * 100)}%` : bar} fg={t().warning} />
342
+ {p.note ? <R t={t()} l="Status" v={p.note} fg={t().textMuted} /> : null}
343
+ <R t={t()} l="Compartments" v={`${p.compartmentsCreated} (${p.passCount} pass${p.passCount === 1 ? "" : "es"})`} fg={t().textMuted} />
344
+ </>
345
+ )
346
+ }
347
+ if (p.phase === "migration") return <R t={t()} l="Status" v={p.note ?? "Migrating memories ⟳"} fg={t().warning} />
348
+ if (p.phase === "done") return <R t={t()} l="Status" v="✓ Upgrade complete" fg={t().accent} />
349
+ return <R t={t()} l="Status" v={`✗ Failed${p.message ? `: ${p.message}` : ""}`} fg={t().error} />
350
+ })()}
351
+ </box>
352
+ )}
353
+
327
354
  {/* 2-column layout */}
328
355
  <box flexDirection="row" width="100%" marginTop={1} gap={4}>
329
356
  {/* Left column */}
@@ -414,49 +441,139 @@ function getModelKeyFromMessages(api: TuiPluginApi, sessionId: string): string |
414
441
  return undefined
415
442
  }
416
443
 
417
- function showRecompDialog(api: TuiPluginApi) {
418
- const sessionId = getSessionId(api)
444
+ async function showRecompDialog(api: TuiPluginApi, targetSessionId = getSessionId(api)): Promise<boolean> {
445
+ const sessionId = targetSessionId
419
446
  if (!sessionId) {
420
447
  api.ui.toast({ message: "No active session", variant: "warning" })
421
- return
448
+ return false
449
+ }
450
+
451
+ const count = await getCompartmentCount(sessionId)
452
+ // Ack only after the dialog is actually shown for the same active session;
453
+ // route switches while the RPC detail load is in flight must leave it pending.
454
+ if (getSessionId(api) !== sessionId) return false
455
+
456
+ api.ui.dialog.replace(() => (
457
+ <api.ui.DialogConfirm
458
+ title="⚠️ Recomp Confirmation"
459
+ message={[
460
+ `You have ${count} compartments.`,
461
+ "",
462
+ "Recomp will regenerate all compartments and facts from raw history.",
463
+ "This may take a long time and consume significant tokens.",
464
+ "",
465
+ "Proceed?",
466
+ ].join("\n")}
467
+ onConfirm={() => {
468
+ void requestRecomp(sessionId)
469
+ kickRecompProgressRefresh()
470
+ api.ui.toast({ message: "Recomp requested — historian will start shortly", variant: "info", duration: 5000 })
471
+ }}
472
+ onCancel={() => {
473
+ api.ui.toast({ message: "Recomp cancelled", variant: "info", duration: 3000 })
474
+ }}
475
+ />
476
+ ))
477
+ return true
478
+ }
479
+
480
+ function showUpgradeDialog(
481
+ api: TuiPluginApi,
482
+ resume?: { stagedCount: number; stagedThrough: number },
483
+ targetSessionId = getSessionId(api),
484
+ ): boolean {
485
+ const sessionId = targetSessionId
486
+ if (!sessionId) {
487
+ // No active session — nothing to upgrade. Silently skip (the server only
488
+ // enqueues this for sessions with legacy compartments, but the TUI may
489
+ // have switched sessions before the poller fired).
490
+ return false
422
491
  }
423
492
 
424
- void getCompartmentCount(sessionId).then((count) => {
425
- api.ui.dialog.replace(() => (
493
+ if (getSessionId(api) !== sessionId) return false
494
+
495
+ const title = resume ? "🎆 Resume the interrupted upgrade?" : "🎆 Historian V2 is released!"
496
+ const message = resume
497
+ ? [
498
+ `An earlier upgrade to the new historian format was interrupted. ${resume.stagedCount} compartment${resume.stagedCount === 1 ? " was" : "s were"} already rebuilt (through message ${resume.stagedThrough}). Resuming continues from where it left off — nothing already rebuilt is reprocessed.`,
499
+ "",
500
+ "Resuming will:",
501
+ "• Rebuild the remaining compartments into the new layered format",
502
+ "• Re-organize this project's memories into the new taxonomy (once per project)",
503
+ "",
504
+ "The historian runs in the background and you can keep working. You can also resume via /ctx-session-upgrade later.",
505
+ "",
506
+ "Resume the upgrade now?",
507
+ ].join("\n")
508
+ : [
509
+ "This session's compartments are written by the old historian. The session is still usable with its old compartments, however it's strongly advised to upgrade them to the new format. This means every compartment needs to be reprocessed by the new historian, which might take a while depending on how big your session is.",
510
+ "",
511
+ "Running the upgrade will:",
512
+ "• Rebuild this session's compartments into the new layered format",
513
+ "• Re-organize this project's memories into the new taxonomy (once per project)",
514
+ "",
515
+ "The historian runs in the background and you can keep working while older compartments are reprocessed. You can also upgrade via /ctx-session-upgrade later.",
516
+ "",
517
+ "Run the upgrade now?",
518
+ ].join("\n")
519
+
520
+ api.ui.dialog.replace(
521
+ () => (
426
522
  <api.ui.DialogConfirm
427
- title="⚠️ Recomp Confirmation"
428
- message={[
429
- `You have ${count} compartments.`,
430
- "",
431
- "Recomp will regenerate all compartments and facts from raw history.",
432
- "This may take a long time and consume significant tokens.",
433
- "",
434
- "Proceed?",
435
- ].join("\n")}
523
+ title={title}
524
+ message={message}
436
525
  onConfirm={() => {
437
- void requestRecomp(sessionId)
438
- api.ui.toast({ message: "Recomp requested historian will start shortly", variant: "info", duration: 5000 })
526
+ // Explicit choice → dismiss the fresh reminder durably so it
527
+ // won't re-show. (Resume prompts are staging-driven and still
528
+ // fire if this run is later interrupted.)
529
+ void dismissUpgradeReminder(sessionId)
530
+ void requestUpgrade(sessionId)
531
+ // Start the sidebar's recomp self-poll immediately — the RPC
532
+ // call fires no message event, so without this the progress
533
+ // bar wouldn't appear until the upgrade finished.
534
+ kickRecompProgressRefresh()
535
+ api.ui.toast({
536
+ message: resume
537
+ ? "Resuming session upgrade — running in the background"
538
+ : "Session upgrade started — running in the background",
539
+ variant: "info",
540
+ duration: 5000,
541
+ })
439
542
  }}
440
543
  onCancel={() => {
441
- api.ui.toast({ message: "Recomp cancelled", variant: "info", duration: 3000 })
544
+ // Explicit decline set the durable stamp so we don't re-prompt
545
+ // on every restart. The fix for stamp-on-display trapping a
546
+ // never-upgraded session (dogfood 2026-05-30) relies on THIS
547
+ // being the only place the TUI path stamps.
548
+ void dismissUpgradeReminder(sessionId)
549
+ api.ui.toast({
550
+ message: "Upgrade skipped — run /ctx-session-upgrade anytime",
551
+ variant: "info",
552
+ duration: 4000,
553
+ })
442
554
  }}
443
555
  />
444
- ))
445
- })
556
+ ),
557
+ )
558
+ return true
446
559
  }
447
560
 
448
- function showStatusDialog(api: TuiPluginApi) {
449
- const sessionId = getSessionId(api)
561
+ async function showStatusDialog(api: TuiPluginApi, targetSessionId = getSessionId(api)): Promise<boolean> {
562
+ const sessionId = targetSessionId
450
563
  if (!sessionId) {
451
564
  api.ui.toast({ message: "No active session", variant: "warning" })
452
- return
565
+ return false
453
566
  }
454
567
 
455
568
  const directory = api.state.path.directory ?? ""
456
569
  const modelKey = getModelKeyFromMessages(api, sessionId)
457
- void loadStatusDetail(sessionId, directory, modelKey).then((detail) => {
458
- api.ui.dialog.replace(() => <StatusDialog api={api} s={detail} />)
459
- })
570
+ const detail = await loadStatusDetail(sessionId, directory, modelKey)
571
+ // Ack only after the dialog is actually shown for the same active session;
572
+ // route switches while the RPC detail load is in flight must leave it pending.
573
+ if (getSessionId(api) !== sessionId) return false
574
+
575
+ api.ui.dialog.replace(() => <StatusDialog api={api} s={detail} />)
576
+ return true
460
577
  }
461
578
 
462
579
  /**
@@ -644,10 +761,46 @@ const tui: TuiPlugin = async (api, _options, meta) => {
644
761
  registerCommandPaletteEntries(api)
645
762
 
646
763
  // Poll for server→TUI messages: toasts and dialog requests.
647
- // Single poller because consumeTuiMessages() is destructive (deletes consumed rows).
764
+ // The poller owns cursor advancement so notifications are acked only after
765
+ // they are accepted for the still-active session and delivered to the UI.
766
+ let pollInFlight = false
648
767
  const messagePoller = setInterval(() => {
649
- void consumeTuiMessages().then((messages) => {
650
- for (const msg of messages) {
768
+ // Scope the drain to the TUI's active session so notifications tagged for
769
+ // a different session (served by the same RPC process) are not consumed
770
+ // here. Do not poll on non-session routes: a session-scoped action fetched
771
+ // while sessionless could otherwise be acked without being shown.
772
+ // Avoid overlapping read-only drains: the server re-delivers until acked,
773
+ // so a second in-flight poll can fetch and dispatch the same batch twice.
774
+ if (pollInFlight) return
775
+
776
+ const requestedSessionId = getSessionId(api)
777
+ if (!requestedSessionId) return
778
+
779
+ pollInFlight = true
780
+ const pollGeneration = getRpcGeneration()
781
+ void consumeTuiMessages(requestedSessionId).then(async (messages) => {
782
+ // The dialog handlers read the current session when they run. If the
783
+ // user switched routes while the RPC was in flight, drop this whole
784
+ // batch without advancing the cursor; the next poll for the new
785
+ // session will fetch the right notifications.
786
+ // Ignore late responses from an older RPC client generation; close/init
787
+ // clears cursors and stale callbacks must not recreate them.
788
+ if (getRpcGeneration() !== pollGeneration) return
789
+
790
+ if (getSessionId(api) !== requestedSessionId) return
791
+
792
+ const orderedMessages = [...messages].sort((a, b) => a.id - b.id)
793
+ const handledMessageIds = new Set<number>()
794
+ for (const msg of orderedMessages) {
795
+ // Drop any action/dialog whose sessionId doesn't match this TUI's
796
+ // active session (session-less/global notifications still apply).
797
+ if (
798
+ msg.type === "action" &&
799
+ msg.sessionId &&
800
+ msg.sessionId !== requestedSessionId
801
+ ) {
802
+ continue
803
+ }
651
804
  if (msg.type === "toast") {
652
805
  const p = msg.payload
653
806
  api.ui.toast({
@@ -655,17 +808,47 @@ const tui: TuiPlugin = async (api, _options, meta) => {
655
808
  variant: (p.variant as "info" | "warning" | "error" | "success") ?? "info",
656
809
  duration: typeof p.duration === "number" ? p.duration : 5000,
657
810
  })
811
+ handledMessageIds.add(msg.id)
658
812
  } else if (msg.type === "action") {
659
813
  const action = msg.payload?.action
660
814
  if (action === "show-status-dialog") {
661
- showStatusDialog(api)
815
+ if (await showStatusDialog(api, requestedSessionId)) {
816
+ handledMessageIds.add(msg.id)
817
+ }
662
818
  } else if (action === "show-recomp-dialog") {
663
- showRecompDialog(api)
819
+ if (await showRecompDialog(api, requestedSessionId)) {
820
+ handledMessageIds.add(msg.id)
821
+ }
822
+ } else if (action === "show-upgrade-dialog") {
823
+ const resume =
824
+ msg.payload?.resume === true
825
+ ? {
826
+ stagedCount: Number(msg.payload?.stagedCount ?? 0),
827
+ stagedThrough: Number(msg.payload?.stagedThrough ?? 0),
828
+ }
829
+ : undefined
830
+ if (showUpgradeDialog(api, resume, requestedSessionId)) {
831
+ handledMessageIds.add(msg.id)
832
+ }
664
833
  }
665
834
  }
666
835
  }
836
+ const handledPrefixMessages: TuiMessage[] = []
837
+ for (const msg of orderedMessages) {
838
+ if (!handledMessageIds.has(msg.id)) break
839
+ handledPrefixMessages.push(msg)
840
+ }
841
+ // A dialog helper may have awaited more RPC work; re-check before
842
+ // acking so a dispose/reinit or route switch during that await cannot
843
+ // advance a stale cursor.
844
+ if (getRpcGeneration() !== pollGeneration) return
845
+ if (getSessionId(api) !== requestedSessionId) return
846
+
847
+ markTuiMessagesHandled(requestedSessionId, handledPrefixMessages)
667
848
  }).catch(() => {
668
849
  // Intentional: message polling should never crash the TUI
850
+ }).finally(() => {
851
+ pollInFlight = false
669
852
  })
670
853
  }, 500)
671
854