@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
@@ -1,10 +1,35 @@
1
1
  /** @jsxImportSource @opentui/solid */
2
+ import { appendFileSync } from "node:fs"
3
+ import { tmpdir } from "node:os"
4
+ import { join } from "node:path"
2
5
  import { createEffect, createMemo, createSignal, on, onCleanup } from "solid-js"
3
6
  import type { TuiSlotPlugin, TuiPluginApi, TuiThemeCurrent } from "@opencode-ai/plugin/tui"
4
7
  import packageJson from "../../../package.json"
5
8
  import { loadSidebarSnapshot, type SidebarSnapshot } from "../data/context-db"
6
9
  import { formatThresholdPercent } from "../../shared/format-threshold"
7
10
 
11
+ // TEMP recomp-poll instrumentation (dogfood 2026-05-30). Writes to a dedicated
12
+ // file so we can trace the client poll loop the server log can't see. Remove
13
+ // once the freeze is diagnosed.
14
+ const RECOMP_TRACE = join(tmpdir(), "mc-recomp-trace.log")
15
+ function rtrace(msg: string): void {
16
+ try {
17
+ appendFileSync(RECOMP_TRACE, `[${new Date().toISOString()}] ${msg}\n`)
18
+ } catch {
19
+ // ignore
20
+ }
21
+ }
22
+
23
+ // Module-level hook so the upgrade/recomp dialog can kick the sidebar into its
24
+ // fast recomp self-poll the INSTANT the user confirms — without waiting for a
25
+ // parent-session message event (the RPC upgrade/recomp call fires none). The
26
+ // mounted SidebarContent registers its refresh here (dogfood 2026-05-30).
27
+ let activeRecompPollKick: (() => void) | null = null
28
+ export function kickRecompProgressRefresh(): void {
29
+ rtrace(`kickRecompProgressRefresh called; activeKick=${activeRecompPollKick ? "set" : "NULL"}`)
30
+ activeRecompPollKick?.()
31
+ }
32
+
8
33
  const SINGLE_BORDER = { type: "single" } as any
9
34
  const REFRESH_DEBOUNCE_MS = 150
10
35
 
@@ -22,13 +47,22 @@ function relativeTime(ms: number): string {
22
47
  return `${Math.floor(diff / 86_400_000)}d ago`
23
48
  }
24
49
 
50
+ // Text progress bar, e.g. [██████░░░░] for the recomp/upgrade live indicator.
51
+ function progressBar(fraction: number, width = 14): string {
52
+ const clamped = Math.max(0, Math.min(1, fraction))
53
+ const filled = Math.round(clamped * width)
54
+ return `[${"█".repeat(filled)}${"░".repeat(width - filled)}]`
55
+ }
56
+
25
57
  // Token breakdown segment colors (hardcoded hex values)
26
58
  const COLORS = {
27
59
  // Cool / structured — injected by the plugin into message[0]
28
60
  system: "#c084fc", // Purple
61
+ docs: "#22d3ee", // Cyan — <project-docs>
29
62
  compartments: "#60a5fa", // Blue
30
63
  facts: "#fbbf24", // Yellow/orange
31
64
  memories: "#34d399", // Green
65
+ profile: "#a3e635", // Lime — <user-profile>
32
66
  // Warm / user-facing — regular chat and tool traffic. Grouped visually
33
67
  // by hue family so the user reads them as a related block.
34
68
  conversation: "#f87171", // Red
@@ -47,17 +81,17 @@ interface TokenSegment {
47
81
  const TokenBreakdown = (props: {
48
82
  theme: TuiThemeCurrent
49
83
  snapshot: SidebarSnapshot
84
+ // Collapsed mode renders only the proportional bar (no per-category legend
85
+ // rows) so the sidebar shrinks to the progress bar + a few summary lines.
86
+ collapsed?: boolean
50
87
  }) => {
51
- // Bar width is hardcoded because the @opencode-ai/plugin/tui slot API does
52
- // not expose the rendered sidebar width to plugins. 24 chars is the safe
53
- // floor that fits every realistic sidebar configuration we've observed
54
- // OpenCode TUI's sidebar narrows with the terminal, and 36 (the previous
55
- // value) overflowed users' actual layouts (issue #90), wrapping the bar
56
- // onto a second line. If/when the slot API surfaces a real width, this
57
- // should become Math.max(20, providedWidth) like the Pi status dialog
58
- // already does (`Math.max(20, innerWidth)`).
59
- const barWidth = 24
60
-
88
+ // The bar is rendered as a flex row of colored boxes, each with
89
+ // flexGrow=tokens and flexBasis=0. opentui distributes the parent
90
+ // container's full width proportionally, so the bar always fills the
91
+ // sidebar regardless of terminal size. No hardcoded width is needed
92
+ // this fixes both the over-wide bar that wrapped onto a second line on
93
+ // narrow sidebars (issue #90) and the under-wide bar that left empty
94
+ // space on the right on wide sidebars.
61
95
  const segments = createMemo<TokenSegment[]>(() => {
62
96
  const s = props.snapshot
63
97
  const total = s.inputTokens || 1
@@ -73,6 +107,16 @@ const TokenBreakdown = (props: {
73
107
  })
74
108
  }
75
109
 
110
+ // Docs (cyan) — injected <project-docs> block (ARCHITECTURE/STRUCTURE)
111
+ if (s.docsTokens > 0) {
112
+ result.push({
113
+ key: "docs",
114
+ tokens: s.docsTokens,
115
+ color: COLORS.docs,
116
+ label: "Docs",
117
+ })
118
+ }
119
+
76
120
  // Compartments (blue)
77
121
  if (s.compartmentTokens > 0) {
78
122
  result.push({
@@ -103,6 +147,16 @@ const TokenBreakdown = (props: {
103
147
  })
104
148
  }
105
149
 
150
+ // User Profile (lime) — injected <user-profile> block (promoted user memories)
151
+ if (s.profileTokens > 0) {
152
+ result.push({
153
+ key: "profile",
154
+ tokens: s.profileTokens,
155
+ color: COLORS.profile,
156
+ label: "User Profile",
157
+ })
158
+ }
159
+
106
160
  // Conversation = real user/assistant text/reasoning/images
107
161
  // (excludes injected session-history and excludes tool call I/O).
108
162
  //
@@ -151,88 +205,57 @@ const TokenBreakdown = (props: {
151
205
 
152
206
  const totalTokens = createMemo(() => props.snapshot.inputTokens || 1)
153
207
 
154
- // Calculate proportional widths for each segment
155
- const segmentWidths = createMemo(() => {
156
- const total = totalTokens()
157
- const segs = segments()
158
- if (segs.length === 0) return []
159
-
160
- // Calculate raw proportions
161
- const proportions = segs.map((seg) => seg.tokens / total)
162
-
163
- // Convert to character widths. Minimum 1 char ONLY when the segment
164
- // has tokens > 0 — zero-token segments (e.g. Conversation when the
165
- // calibrator rounded it to zero) must get width 0 so the bar stays
166
- // proportional. The legend row still renders for zero-token segments
167
- // to keep the row stable.
168
- let widths = segs.map((seg, i) =>
169
- seg.tokens > 0 ? Math.max(1, Math.round(proportions[i] * barWidth)) : 0,
170
- )
171
-
172
- // Adjust to exactly barWidth
173
- const sum = widths.reduce((a, b) => a + b, 0)
174
- if (sum > barWidth) {
175
- // Shrink from the largest segments
176
- let excess = sum - barWidth
177
- while (excess > 0) {
178
- const maxIdx = widths.indexOf(Math.max(...widths))
179
- if (widths[maxIdx] > 1) {
180
- widths[maxIdx]--
181
- excess--
182
- } else {
183
- break
184
- }
185
- }
186
- } else if (sum < barWidth) {
187
- // Expand the largest segments
188
- let deficit = barWidth - sum
189
- while (deficit > 0) {
190
- const maxIdx = widths.indexOf(Math.max(...widths))
191
- widths[maxIdx]++
192
- deficit--
193
- }
194
- }
195
-
196
- return widths
197
- })
198
-
199
- const barSegments = createMemo(() => {
200
- const segs = segments()
201
- const widths = segmentWidths()
202
- return segs.map((seg, i) => ({
203
- chars: "█".repeat(widths[i] || 0),
204
- color: seg.color,
205
- }))
206
- })
208
+ // Render-time segments for the bar. Zero-token segments are filtered out
209
+ // entirely (no flex weight, no rendered box) so they don't claim any
210
+ // width. Non-zero segments still get a Math.max(1, ...) floor on
211
+ // flexGrow so very small contributions remain visible as a thin sliver.
212
+ // The legend rows below show every segment (including zeros) for table
213
+ // stability — only the bar prunes them.
214
+ const barSegments = createMemo(() =>
215
+ segments().filter((seg) => seg.tokens > 0),
216
+ )
207
217
 
208
218
  return (
209
219
  <box width="100%" flexDirection="column">
210
- {/* Segmented bar */}
211
- <box flexDirection="row">
212
- {barSegments().map((seg, i) => (
213
- <text key={i} fg={seg.color}>{seg.chars}</text>
220
+ {/* Segmented bar: a width="100%" flex row of colored boxes,
221
+ each with flexGrow proportional to its token count and
222
+ flexBasis=0. opentui distributes the parent's full width
223
+ proportionally, so the bar always fills the sidebar
224
+ regardless of terminal size. Height is fixed at 1 row;
225
+ backgroundColor renders the colored bar. */}
226
+ <box width="100%" flexDirection="row" height={1}>
227
+ {barSegments().map((seg) => (
228
+ <box
229
+ key={seg.key}
230
+ flexGrow={Math.max(1, seg.tokens)}
231
+ flexBasis={0}
232
+ height={1}
233
+ backgroundColor={seg.color}
234
+ />
214
235
  ))}
215
236
  </box>
216
237
 
217
- {/* Legend rows */}
218
- <box flexDirection="column" marginTop={0}>
219
- {segments().map((seg) => {
220
- const pct = ((seg.tokens / totalTokens()) * 100).toFixed(0)
221
- return (
222
- <box
223
- key={seg.key}
224
- width="100%"
225
- flexDirection="row"
226
- justifyContent="space-between"
227
- >
228
- <text fg={seg.color}>{seg.label}</text>
229
- <text fg={props.theme.textMuted}>
230
- {compactTokens(seg.tokens)} ({pct}%)
231
- </text>
232
- </box>
233
- )
234
- })}
235
- </box>
238
+ {/* Legend rows — suppressed in collapsed mode (bar only) */}
239
+ {!props.collapsed && (
240
+ <box flexDirection="column" marginTop={0}>
241
+ {segments().map((seg) => {
242
+ const pct = ((seg.tokens / totalTokens()) * 100).toFixed(0)
243
+ return (
244
+ <box
245
+ key={seg.key}
246
+ width="100%"
247
+ flexDirection="row"
248
+ justifyContent="space-between"
249
+ >
250
+ <text fg={seg.color}>{seg.label}</text>
251
+ <text fg={props.theme.textMuted}>
252
+ {compactTokens(seg.tokens)} ({pct}%)
253
+ </text>
254
+ </box>
255
+ )
256
+ })}
257
+ </box>
258
+ )}
236
259
  </box>
237
260
  )
238
261
  }
@@ -270,26 +293,144 @@ const SectionHeader = (props: { theme: TuiThemeCurrent; title: string }) => (
270
293
  </box>
271
294
  )
272
295
 
296
+ // Live recomp / session-upgrade progress. Renders while an upgrade runs (and
297
+ // briefly after it finishes) so a multi-minute rebuild is visible instead of a
298
+ // single missed toast (dogfood 2026-05-30).
299
+ const RecompProgressSection = (props: {
300
+ theme: TuiThemeCurrent
301
+ progress: NonNullable<SidebarSnapshot["recompProgress"]>
302
+ }) => {
303
+ // CRITICAL: read `props.progress` reactively on every access — do NOT
304
+ // destructure it into a local `const p = props.progress` at creation time.
305
+ // The parent keeps THIS component instance mounted as the phase advances
306
+ // (recomp → migration → done), so a frozen `p` would render the
307
+ // creation-time phase forever — the sidebar stuck on "upgrading / Running
308
+ // historian (pass 1)…" even though the upgrade finished. Each accessor below
309
+ // tracks the parent signal so the label/bar/note update live (root cause of
310
+ // the dogfood 2026-05-30 "recomp upgrading stays" freeze).
311
+ const phase = () => props.progress.phase
312
+ const fraction = () =>
313
+ props.progress.totalMessages > 0
314
+ ? props.progress.processedMessages / props.progress.totalMessages
315
+ : 0
316
+ const pct = () => Math.round(fraction() * 100)
317
+
318
+ const label = createMemo(() => {
319
+ switch (props.progress.phase) {
320
+ case "recomp":
321
+ return { text: "upgrading ⟳", color: props.theme.warning }
322
+ case "migration":
323
+ return { text: "Migrating memories ⟳", color: props.theme.warning }
324
+ case "done":
325
+ return { text: "✓ Upgrade complete", color: props.theme.success ?? props.theme.accent }
326
+ case "failed":
327
+ return { text: "✗ Upgrade failed", color: props.theme.error }
328
+ }
329
+ })
330
+
331
+ return (
332
+ <>
333
+ <box width="100%" marginTop={1} flexDirection="row" justifyContent="space-between">
334
+ <text fg={props.theme.text}>
335
+ <b>Recomp</b>
336
+ </text>
337
+ <text fg={label().color}>{label().text}</text>
338
+ </box>
339
+ {/* Determinate bar during the compartment-rebuild phase. */}
340
+ {phase() === "recomp" && props.progress.totalMessages > 0 && (
341
+ <box width="100%" flexDirection="row" justifyContent="space-between">
342
+ <text fg={props.theme.accent}>{progressBar(fraction())}</text>
343
+ <text fg={props.theme.textMuted}>{pct()}%</text>
344
+ </box>
345
+ )}
346
+ {/* Transient status note (e.g. "Starting…", "Trying fallback
347
+ sonnet-4-6…", "Repair retry…") — surfaces live activity during a
348
+ long pass, including before the determinate range is known. */}
349
+ {(phase() === "recomp" || phase() === "migration") && props.progress.note && (
350
+ <text fg={props.theme.textMuted}>{props.progress.note}</text>
351
+ )}
352
+ {phase() === "recomp" && (
353
+ <StatRow
354
+ theme={props.theme}
355
+ label="Compartments"
356
+ value={`${props.progress.compartmentsCreated} (${props.progress.passCount} pass${props.progress.passCount === 1 ? "" : "es"})`}
357
+ dim
358
+ />
359
+ )}
360
+ {/* Terminal reason (failed) — kept visible so the user sees WHY. */}
361
+ {phase() === "failed" && props.progress.message && (
362
+ <text fg={props.theme.textMuted}>{props.progress.message}</text>
363
+ )}
364
+ </>
365
+ )
366
+ }
367
+
273
368
  const SidebarContent = (props: {
274
369
  api: TuiPluginApi
275
370
  sessionID: () => string
276
371
  theme: TuiThemeCurrent
277
372
  }) => {
278
373
  const [snapshot, setSnapshot] = createSignal<SidebarSnapshot | null>(null)
374
+ // Collapsed view: progress bar + 3 summary lines (Historian / Memories /
375
+ // Status), no per-category legend or section grid. In-memory only (resets
376
+ // to expanded on TUI restart), mirroring the native MCP sidebar toggle.
377
+ const [collapsed, setCollapsed] = createSignal(false)
279
378
  let refreshTimer: ReturnType<typeof setTimeout> | undefined
379
+ // Self-sustaining poll while a recomp/upgrade is running. Recomp work
380
+ // happens in CHILD sessions whose message events are filtered out of the
381
+ // subscription below, so without this the progress bar would freeze until
382
+ // the next parent-session message. Active only during recomp/migration;
383
+ // stops itself once the phase goes terminal/absent (dogfood 2026-05-30).
384
+ let recompPollTimer: ReturnType<typeof setTimeout> | undefined
385
+ const RECOMP_POLL_MS = 1200
386
+ // Robust recomp poll state. The loop MUST survive a failed/slow snapshot
387
+ // fetch — the server is busy doing the historian LLM call during a recomp,
388
+ // so a poll can reject or return a stale (pre-recomp) cached snapshot. The
389
+ // OLD loop reattached the next timer only inside `.then()`, so any rejection
390
+ // killed it and the bar froze mid-pass (dogfood 2026-05-30). This version
391
+ // reschedules on BOTH success and failure, keyed on `recompActive`, and only
392
+ // stops on a terminal phase, a bounded "never started" probe window, or the
393
+ // entry vanishing after we'd seen it active.
394
+ let recompActive = false
395
+ let recompSawPhase = false
396
+ let recompPollCount = 0
397
+ let recompConsecutiveAbsent = 0
398
+ const RECOMP_PROBE_MAX = 12 // ~15s for the server's "Starting…" to land
399
+ // After we've SEEN an active phase, a momentarily absent snapshot is almost
400
+ // always transient — the server's sticky cache serves a pre-recomp snapshot
401
+ // (no recompProgress) during the token-quiet recomp window, or a concurrent
402
+ // BEGIN-IMMEDIATE publish makes the snapshot DB read throw → bare empty. The
403
+ // entry is held until terminal + a 30s grace, so we keep polling through many
404
+ // absents and only give up after a long run of them (entry truly gone but we
405
+ // somehow missed "done"). This was the freeze: the old logic stopped on the
406
+ // FIRST absent-after-active (dogfood 2026-05-30).
407
+ const RECOMP_ABSENT_GIVEUP = 40 // ~48s of continuous absence → stop
408
+ const RECOMP_MAX_POLLS = 1500 // ~30min absolute safety cap
280
409
 
281
410
  const refresh = () => {
282
411
  const sid = props.sessionID()
283
412
  if (!sid) return
284
413
  const directory = props.api.state.path.directory ?? ""
285
- void loadSidebarSnapshot(sid, directory).then((data) => {
286
- setSnapshot(data)
287
- try {
288
- props.api.renderer.requestRender()
289
- } catch {
290
- // Ignore render errors
291
- }
292
- })
414
+ void loadSidebarSnapshot(sid, directory)
415
+ .then((data) => {
416
+ setSnapshot(data)
417
+ try {
418
+ props.api.renderer.requestRender()
419
+ } catch {
420
+ // Ignore render errors
421
+ }
422
+ // If a recomp/upgrade is running (detected via any refresh, e.g.
423
+ // a /ctx-recomp command not started from the dialog), make sure
424
+ // the dedicated poll loop is running.
425
+ const phase = data?.recompProgress?.phase
426
+ if ((phase === "recomp" || phase === "migration") && !recompActive) {
427
+ kickRecompPoll()
428
+ }
429
+ })
430
+ .catch(() => {
431
+ // one-shot refresh failure is non-fatal; the recomp loop (if any)
432
+ // has its own resilient retry.
433
+ })
293
434
  }
294
435
 
295
436
  const scheduleRefresh = () => {
@@ -300,8 +441,108 @@ const SidebarContent = (props: {
300
441
  }, REFRESH_DEBOUNCE_MS)
301
442
  }
302
443
 
444
+ const scheduleRecompTick = () => {
445
+ if (!recompActive) return
446
+ if (recompPollTimer) clearTimeout(recompPollTimer)
447
+ recompPollTimer = setTimeout(recompTick, RECOMP_POLL_MS)
448
+ }
449
+
450
+ function recompTick(): void {
451
+ if (!recompActive) return
452
+ recompPollCount += 1
453
+ if (recompPollCount > RECOMP_MAX_POLLS) {
454
+ recompActive = false
455
+ return
456
+ }
457
+ const sid = props.sessionID()
458
+ if (!sid) {
459
+ recompActive = false
460
+ return
461
+ }
462
+ const directory = props.api.state.path.directory ?? ""
463
+ void loadSidebarSnapshot(sid, directory)
464
+ .then((data) => {
465
+ const phase = data?.recompProgress?.phase
466
+ rtrace(
467
+ `poll#${recompPollCount} phase=${phase ?? "ABSENT"} passCount=${data?.recompProgress?.passCount ?? "-"} note=${data?.recompProgress?.note ?? "-"} sawPhase=${recompSawPhase} absent=${recompConsecutiveAbsent}`,
468
+ )
469
+ // While a recomp is known-active, a transient snapshot that lost
470
+ // recompProgress (sticky cache / busy-DB empty) must NOT wipe the
471
+ // visible bar — carry the last good progress forward so it stays
472
+ // stable until a real update or the terminal state lands.
473
+ const prevProgress = snapshot()?.recompProgress
474
+ const merged =
475
+ !phase && recompSawPhase && prevProgress
476
+ ? { ...data, recompProgress: prevProgress }
477
+ : data
478
+ setSnapshot(merged)
479
+ try {
480
+ props.api.renderer.requestRender()
481
+ } catch {
482
+ // ignore render errors
483
+ }
484
+ if (phase === "recomp" || phase === "migration") {
485
+ recompSawPhase = true
486
+ recompConsecutiveAbsent = 0
487
+ scheduleRecompTick()
488
+ } else if (phase === "done" || phase === "failed") {
489
+ // Terminal state rendered — stop. The server keeps "done" for
490
+ // a grace window and "failed" until the next run, so the
491
+ // outcome stays visible without further polling.
492
+ rtrace(`STOP: terminal phase=${phase}`)
493
+ recompActive = false
494
+ } else {
495
+ // Phase absent this poll.
496
+ recompConsecutiveAbsent += 1
497
+ if (!recompSawPhase) {
498
+ // Still waiting for the server's first "Starting…".
499
+ if (recompPollCount < RECOMP_PROBE_MAX) scheduleRecompTick()
500
+ else {
501
+ rtrace("STOP: probe window exhausted, never saw phase")
502
+ recompActive = false
503
+ }
504
+ } else if (recompConsecutiveAbsent < RECOMP_ABSENT_GIVEUP) {
505
+ // Seen it active — absent is almost certainly the sticky
506
+ // cache / a transient snapshot read. Keep polling so we
507
+ // still catch the terminal state. DON'T overwrite the
508
+ // last good progress snapshot with this transient empty.
509
+ scheduleRecompTick()
510
+ } else {
511
+ // Long continuous absence — the entry is genuinely gone.
512
+ rtrace("STOP: absent giveup")
513
+ recompActive = false
514
+ }
515
+ }
516
+ })
517
+ .catch((err) => {
518
+ // CRITICAL: a failed/slow fetch must NOT kill the loop — keep
519
+ // polling while active so we still catch the terminal state.
520
+ rtrace(`poll#${recompPollCount} FETCH ERROR: ${String(err)}`)
521
+ scheduleRecompTick()
522
+ })
523
+ }
524
+
525
+ // Kick the resilient recomp poll loop on dialog confirm (or when a refresh
526
+ // first detects an active recomp). The server emits an immediate "Starting…"
527
+ // entry; the probe window covers the brief RPC race before it lands.
528
+ function kickRecompPoll(): void {
529
+ rtrace(`kickRecompPoll: recompActive=${recompActive} (${recompActive ? "SKIP" : "starting"})`)
530
+ if (recompActive) return // already running
531
+ recompActive = true
532
+ recompSawPhase = false
533
+ recompPollCount = 0
534
+ recompConsecutiveAbsent = 0
535
+ recompTick()
536
+ }
537
+
538
+ activeRecompPollKick = kickRecompPoll
539
+ rtrace("SidebarContent mounted; registered activeRecompPollKick")
540
+
303
541
  onCleanup(() => {
304
542
  if (refreshTimer) clearTimeout(refreshTimer)
543
+ if (recompPollTimer) clearTimeout(recompPollTimer)
544
+ recompActive = false
545
+ if (activeRecompPollKick === kickRecompPoll) activeRecompPollKick = null
305
546
  })
306
547
 
307
548
  // Refresh on session change
@@ -358,19 +599,28 @@ const SidebarContent = (props: {
358
599
  paddingLeft={1}
359
600
  paddingRight={1}
360
601
  >
361
- {/* Header */}
362
- <box flexDirection="row" justifyContent="space-between" alignItems="center">
602
+ {/* Header: triangle toggle + badge + version. Clicking the row
603
+ collapses/expands the panel (mirrors OpenCode's native MCP
604
+ sidebar section and AFT's sidebar). */}
605
+ <box
606
+ flexDirection="row"
607
+ justifyContent="space-between"
608
+ alignItems="center"
609
+ onMouseDown={() => setCollapsed((x) => !x)}
610
+ >
363
611
  <box paddingLeft={1} paddingRight={1} backgroundColor={props.theme.accent}>
364
612
  <text fg={props.theme.background}>
365
- <b>Magic Context</b>
613
+ <b>{collapsed() ? "▶ " : "▼ "}Magic Context</b>
366
614
  </text>
367
615
  </box>
368
616
  <text fg={props.theme.textMuted}>v{packageJson.version}</text>
369
617
  </box>
370
618
 
371
- {/* Token breakdown bar */}
619
+ {/* Token breakdown bar. In collapsed mode the header, bar and the
620
+ 3 summary rows stack with no vertical padding for a compact look;
621
+ expanded mode keeps the 1-row gap above the bar. */}
372
622
  {s() && s()!.inputTokens > 0 && (
373
- <box marginTop={1} flexDirection="column">
623
+ <box marginTop={collapsed() ? 0 : 1} flexDirection="column">
374
624
  {(s()?.contextLimit ?? 0) > 0 && (
375
625
  <box width="100%" flexDirection="row" justifyContent="space-between">
376
626
  {/* Left: current usage vs the per-model execute
@@ -390,17 +640,56 @@ const SidebarContent = (props: {
390
640
  </text>
391
641
  </box>
392
642
  )}
393
- <TokenBreakdown theme={props.theme} snapshot={s()!} />
643
+ <TokenBreakdown theme={props.theme} snapshot={s()!} collapsed={collapsed()} />
394
644
  </box>
395
645
  )}
396
646
 
647
+ {/* Collapsed view — progress bar (above) + 3 summary lines:
648
+ Historian (with compartment count), Memories (injected/total),
649
+ Status (Q=queued ops, N=session notes). */}
650
+ {collapsed() && (
651
+ <box width="100%" flexDirection="column">
652
+ {/* Collapsed rows are intentionally uniform faded-grey, not
653
+ bold/accent — they're a glanceable summary, so the label
654
+ and value share the muted tone (matches Memories row). */}
655
+ <box width="100%" flexDirection="row" justifyContent="space-between">
656
+ <text fg={props.theme.textMuted}>Historian</text>
657
+ {s()?.historianRunning ? (
658
+ <text fg={props.theme.warning}>comparting ⟳</text>
659
+ ) : (
660
+ <text fg={props.theme.textMuted}>idle</text>
661
+ )}
662
+ </box>
663
+ <box width="100%" flexDirection="row" justifyContent="space-between">
664
+ <text fg={props.theme.textMuted}>Memories</text>
665
+ <text fg={props.theme.textMuted}>
666
+ {(s()?.memoryBlockCount ?? 0) > 0
667
+ ? `${s()!.memoryBlockCount}/${s()?.memoryCount ?? 0}`
668
+ : String(s()?.memoryCount ?? 0)}
669
+ </text>
670
+ </box>
671
+ <box width="100%" flexDirection="row" justifyContent="space-between">
672
+ <text fg={props.theme.textMuted}>Status</text>
673
+ <text fg={props.theme.textMuted}>
674
+ C:{s()?.compartmentCount ?? 0} Q:{s()?.pendingOpsCount ?? 0} N:{s()?.sessionNoteCount ?? 0}
675
+ </text>
676
+ </box>
677
+ {s()?.recompProgress && (
678
+ <RecompProgressSection theme={props.theme} progress={s()!.recompProgress!} />
679
+ )}
680
+ </box>
681
+ )}
682
+
683
+ {/* Expanded view — full section grid. */}
684
+ {!collapsed() && (
685
+ <>
397
686
  {/* Historian section */}
398
687
  <box width="100%" marginTop={1} flexDirection="row" justifyContent="space-between">
399
688
  <text fg={props.theme.text}>
400
689
  <b>Historian</b>
401
690
  </text>
402
691
  {s()?.historianRunning ? (
403
- <text fg={props.theme.warning}>compacting ⟳</text>
692
+ <text fg={props.theme.warning}>comparting ⟳</text>
404
693
  ) : (
405
694
  <text fg={props.theme.textMuted}>idle</text>
406
695
  )}
@@ -416,6 +705,11 @@ const SidebarContent = (props: {
416
705
  value={String(s()?.factCount ?? 0)}
417
706
  />
418
707
 
708
+ {/* Recomp / session-upgrade live progress */}
709
+ {s()?.recompProgress && (
710
+ <RecompProgressSection theme={props.theme} progress={s()!.recompProgress!} />
711
+ )}
712
+
419
713
  {/* Memory section */}
420
714
  <SectionHeader theme={props.theme} title="Memory" />
421
715
  <StatRow
@@ -477,6 +771,26 @@ const SidebarContent = (props: {
477
771
  />
478
772
  </>
479
773
  )}
774
+
775
+ {/* Stats — v0.21.8 ships a single "Total tokens" number while we
776
+ figure out how to present the new-work / reprocessed
777
+ categorization without confusing users. The underlying
778
+ snapshot fields (newWorkTokens, totalInputTokens) and the
779
+ session_meta columns are still populated; only the UI is
780
+ simplified for now. */}
781
+ {s()?.totalInputTokens != null && (
782
+ <>
783
+ <SectionHeader theme={props.theme} title="Stats" />
784
+ <StatRow
785
+ theme={props.theme}
786
+ label="Total tokens"
787
+ value={compactTokens(s()!.totalInputTokens ?? 0)}
788
+ dim
789
+ />
790
+ </>
791
+ )}
792
+ </>
793
+ )}
480
794
  </box>
481
795
  )
482
796
  }