@cortexkit/opencode-magic-context 0.21.6 → 0.21.8

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 (76) hide show
  1. package/README.md +1 -1
  2. package/dist/config/agent-disable.d.ts +26 -0
  3. package/dist/config/agent-disable.d.ts.map +1 -0
  4. package/dist/config/index.d.ts.map +1 -1
  5. package/dist/config/schema/magic-context.d.ts +0 -6
  6. package/dist/config/schema/magic-context.d.ts.map +1 -1
  7. package/dist/features/magic-context/compartment-lease.d.ts +14 -0
  8. package/dist/features/magic-context/compartment-lease.d.ts.map +1 -0
  9. package/dist/features/magic-context/compartment-storage.d.ts +5 -1
  10. package/dist/features/magic-context/compartment-storage.d.ts.map +1 -1
  11. package/dist/features/magic-context/compression-depth-storage.d.ts +2 -1
  12. package/dist/features/magic-context/compression-depth-storage.d.ts.map +1 -1
  13. package/dist/features/magic-context/dreamer/runner.d.ts.map +1 -1
  14. package/dist/features/magic-context/migrations.d.ts.map +1 -1
  15. package/dist/features/magic-context/sidekick/agent.d.ts.map +1 -1
  16. package/dist/features/magic-context/storage-db.d.ts.map +1 -1
  17. package/dist/features/magic-context/storage-meta-persisted.d.ts +5 -0
  18. package/dist/features/magic-context/storage-meta-persisted.d.ts.map +1 -1
  19. package/dist/features/magic-context/storage-meta-session.d.ts.map +1 -1
  20. package/dist/features/magic-context/storage-meta.d.ts +1 -1
  21. package/dist/features/magic-context/storage-meta.d.ts.map +1 -1
  22. package/dist/features/magic-context/storage-subagent-invocations.d.ts +52 -0
  23. package/dist/features/magic-context/storage-subagent-invocations.d.ts.map +1 -0
  24. package/dist/features/magic-context/storage.d.ts +3 -2
  25. package/dist/features/magic-context/storage.d.ts.map +1 -1
  26. package/dist/features/magic-context/subagent-token-capture.d.ts +33 -0
  27. package/dist/features/magic-context/subagent-token-capture.d.ts.map +1 -0
  28. package/dist/features/magic-context/user-memory/review-user-memories.d.ts.map +1 -1
  29. package/dist/features/magic-context/work-metrics.d.ts +13 -0
  30. package/dist/features/magic-context/work-metrics.d.ts.map +1 -0
  31. package/dist/hooks/magic-context/auto-search-runner.d.ts.map +1 -1
  32. package/dist/hooks/magic-context/compartment-runner-compressor.d.ts +4 -0
  33. package/dist/hooks/magic-context/compartment-runner-compressor.d.ts.map +1 -1
  34. package/dist/hooks/magic-context/compartment-runner-historian.d.ts +2 -0
  35. package/dist/hooks/magic-context/compartment-runner-historian.d.ts.map +1 -1
  36. package/dist/hooks/magic-context/compartment-runner-incremental.d.ts.map +1 -1
  37. package/dist/hooks/magic-context/compartment-runner-partial-recomp.d.ts.map +1 -1
  38. package/dist/hooks/magic-context/compartment-runner-recomp.d.ts.map +1 -1
  39. package/dist/hooks/magic-context/compartment-runner-types.d.ts +3 -0
  40. package/dist/hooks/magic-context/compartment-runner-types.d.ts.map +1 -1
  41. package/dist/hooks/magic-context/compartment-runner.d.ts +5 -0
  42. package/dist/hooks/magic-context/compartment-runner.d.ts.map +1 -1
  43. package/dist/hooks/magic-context/hook.d.ts.map +1 -1
  44. package/dist/hooks/magic-context/transform-compartment-phase.d.ts +2 -2
  45. package/dist/hooks/magic-context/transform-compartment-phase.d.ts.map +1 -1
  46. package/dist/hooks/magic-context/transform-postprocess-phase.d.ts +1 -0
  47. package/dist/hooks/magic-context/transform-postprocess-phase.d.ts.map +1 -1
  48. package/dist/hooks/magic-context/transform.d.ts +2 -0
  49. package/dist/hooks/magic-context/transform.d.ts.map +1 -1
  50. package/dist/index.d.ts.map +1 -1
  51. package/dist/index.js +14944 -14084
  52. package/dist/plugin/conflict-warning-hook.d.ts +10 -0
  53. package/dist/plugin/conflict-warning-hook.d.ts.map +1 -1
  54. package/dist/plugin/dream-timer.d.ts.map +1 -1
  55. package/dist/plugin/hooks/create-session-hooks.d.ts.map +1 -1
  56. package/dist/plugin/rpc-handlers.d.ts.map +1 -1
  57. package/dist/plugin/tool-registry.d.ts.map +1 -1
  58. package/dist/shared/announcement.d.ts +55 -0
  59. package/dist/shared/announcement.d.ts.map +1 -0
  60. package/dist/shared/format-threshold.d.ts +24 -0
  61. package/dist/shared/format-threshold.d.ts.map +1 -0
  62. package/dist/shared/rpc-types.d.ts +2 -0
  63. package/dist/shared/rpc-types.d.ts.map +1 -1
  64. package/dist/shared/subagent-runner.d.ts +5 -0
  65. package/dist/shared/subagent-runner.d.ts.map +1 -1
  66. package/dist/tui/data/context-db.d.ts +14 -0
  67. package/dist/tui/data/context-db.d.ts.map +1 -1
  68. package/package.json +1 -1
  69. package/src/shared/announcement.test.ts +143 -0
  70. package/src/shared/announcement.ts +97 -0
  71. package/src/shared/format-threshold.ts +28 -0
  72. package/src/shared/rpc-types.ts +2 -0
  73. package/src/shared/subagent-runner.ts +14 -0
  74. package/src/tui/data/context-db.ts +45 -0
  75. package/src/tui/index.tsx +85 -29
  76. package/src/tui/slots/sidebar-content.tsx +51 -60
package/src/tui/index.tsx CHANGED
@@ -6,7 +6,8 @@ import { createMemo } from "solid-js"
6
6
  import type { TuiPlugin, TuiPluginApi, TuiThemeCurrent } from "@opencode-ai/plugin/tui"
7
7
  import { createSidebarContentSlot } from "./slots/sidebar-content"
8
8
  import packageJson from "../../package.json"
9
- import { closeRpc, consumeTuiMessages, getCompartmentCount, initRpcClient, loadStatusDetail, requestRecomp, type StatusDetail } from "./data/context-db"
9
+ import { closeRpc, consumeTuiMessages, getAnnouncement, getCompartmentCount, initRpcClient, loadStatusDetail, markAnnounced, requestRecomp, type StatusDetail } from "./data/context-db"
10
+ import { formatThresholdPercent } from "../shared/format-threshold"
10
11
  import { detectConflicts } from "../shared/conflict-detector"
11
12
  import { fixConflicts } from "../shared/conflict-fixer"
12
13
  import { readJsoncFile } from "../shared/jsonc-parser"
@@ -260,27 +261,12 @@ const StatusDialog = (props: { api: TuiPluginApi; s: StatusDetail }) => {
260
261
  return { segs, total }
261
262
  }
262
263
 
263
- const barWidth = 56
264
- const barSegments = () => {
265
- const { segs, total } = breakdownSegments()
266
- if (segs.length === 0) return []
267
-
268
- let widths = segs.map((seg) => Math.max(1, Math.round((seg.tokens / total) * barWidth)))
269
- let sum = widths.reduce((a, b) => a + b, 0)
270
- while (sum > barWidth) {
271
- const maxIdx = widths.indexOf(Math.max(...widths))
272
- if (widths[maxIdx] > 1) { widths[maxIdx]--; sum-- } else break
273
- }
274
- while (sum < barWidth) {
275
- const maxIdx = widths.indexOf(Math.max(...widths))
276
- widths[maxIdx]++; sum++
277
- }
278
-
279
- return segs.map((seg, i) => ({
280
- chars: "█".repeat(widths[i] || 0),
281
- color: seg.color,
282
- }))
283
- }
264
+ // The status-dialog breakdown bar uses flex layout (same approach as the
265
+ // sidebar breakdown). Each segment becomes a colored box with
266
+ // flexGrow=tokens and flexBasis=0, parent has width="100%", so opentui
267
+ // distributes the dialog's full width proportionally regardless of the
268
+ // dialog's actual rendered width.
269
+ const barSegments = () => breakdownSegments().segs.filter((seg) => seg.tokens > 0)
284
270
 
285
271
  return (
286
272
  <box flexDirection="column" width="100%" paddingLeft={2} paddingRight={2} paddingTop={1} paddingBottom={1}>
@@ -296,17 +282,24 @@ const StatusDialog = (props: { api: TuiPluginApi; s: StatusDetail }) => {
296
282
  them how close they are to compaction triggering. */}
297
283
  <box flexDirection="row" justifyContent="space-between" width="100%">
298
284
  <text fg={s().usagePercentage >= 80 ? t().error : s().usagePercentage >= 65 ? t().warning : t().accent}>
299
- <b>{s().usagePercentage.toFixed(1)}%</b> / {s().executeThreshold}%
285
+ <b>{s().usagePercentage.toFixed(1)}%</b> / {formatThresholdPercent(s().executeThreshold)}%
300
286
  </text>
301
287
  <text fg={s().usagePercentage >= 80 ? t().error : s().usagePercentage >= 65 ? t().warning : t().accent}>
302
288
  {fmt(s().inputTokens)} / {contextLimit() > 0 ? fmt(contextLimit()) : "?"} tokens
303
289
  </text>
304
290
  </box>
305
291
 
306
- {/* Segmented breakdown bar */}
307
- <box flexDirection="row">
308
- {barSegments().map((seg, i) => (
309
- <text key={i} fg={seg.color}>{seg.chars}</text>
292
+ {/* Segmented breakdown bar: flex row of colored boxes filling
293
+ the dialog width. See barSegments comment above. */}
294
+ <box width="100%" flexDirection="row" height={1}>
295
+ {barSegments().map((seg) => (
296
+ <box
297
+ key={seg.label}
298
+ flexGrow={Math.max(1, seg.tokens)}
299
+ flexBasis={0}
300
+ height={1}
301
+ backgroundColor={seg.color}
302
+ />
310
303
  ))}
311
304
  </box>
312
305
 
@@ -341,7 +334,7 @@ const StatusDialog = (props: { api: TuiPluginApi; s: StatusDetail }) => {
341
334
  <R t={t()} l="Configured" v={s().cacheTtl} />
342
335
  <R t={t()} l="Last response" v={s().lastResponseTime > 0 ? `${Math.round(elapsed() / 1000)}s ago` : "never"} />
343
336
  <R t={t()} l="Remaining" v={s().cacheExpired ? "expired" : `${Math.round(s().cacheRemainingMs / 1000)}s`} fg={s().cacheExpired ? t().warning : t().textMuted} />
344
- <R t={t()} l="Auto-execute" v={s().cacheExpired ? "yes (expired)" : `at TTL or ≥${s().executeThreshold}%`} fg={t().textMuted} />
337
+ <R t={t()} l="Auto-execute" v={s().cacheExpired ? "yes (expired)" : `at TTL or ≥${formatThresholdPercent(s().executeThreshold)}%`} fg={t().textMuted} />
345
338
  <box marginTop={1}>
346
339
  <text fg={t().text}><b>Memory</b></text>
347
340
  </box>
@@ -351,7 +344,7 @@ const StatusDialog = (props: { api: TuiPluginApi; s: StatusDetail }) => {
351
344
  {/* Right column */}
352
345
  <box flexDirection="column" flexGrow={1} flexBasis={0}>
353
346
  <text fg={t().text}><b>Rolling Nudges</b></text>
354
- <R t={t()} l="Execute threshold" v={`${s().executeThreshold}%`} />
347
+ <R t={t()} l="Execute threshold" v={`${formatThresholdPercent(s().executeThreshold)}%`} />
355
348
  <R t={t()} l="Nudge anchor" v={`${fmt(s().lastNudgeTokens)} tok`} />
356
349
  <R t={t()} l="Interval" v={`${fmt(s().nudgeInterval)} tok`} fg={t().textMuted} />
357
350
  <R t={t()} l="Next nudge after" v={`${fmt(s().nextNudgeAfter)} tok`} />
@@ -563,6 +556,63 @@ function registerCommandPaletteEntries(api: TuiPluginApi): void {
563
556
  // via RPC.
564
557
  }
565
558
 
559
+ /**
560
+ * Show the one-shot "What's new" dialog on TUI startup if the server tells us
561
+ * to. The server is the source of truth: it has the version + features
562
+ * constants AND owns the persistence file. We just render and report back.
563
+ *
564
+ * Failure-tolerant by design — if the server isn't ready or the RPC fails,
565
+ * we silently skip (the next TUI launch will retry).
566
+ */
567
+ /**
568
+ * URLs render as plain text. Modern terminals (iTerm2, kitty, WezTerm, Ghostty,
569
+ * recent macOS Terminal) auto-detect URLs and let users Cmd-click; older
570
+ * terminals require manual copy. We tried opentui's `<a href>` JSX intrinsic
571
+ * for application-level OSC 8 clickability, but it's a span-like element that
572
+ * forced text out of opentui's word-wrap mode, causing bullets to bleed past
573
+ * the dialog border. Pure-string children of `<text>` wrap correctly, so the
574
+ * AFT-style DialogAlert + plain string is the right surface here.
575
+ */
576
+ async function showStartupAnnouncement(api: TuiPluginApi): Promise<void> {
577
+ try {
578
+ const ann = await getAnnouncement()
579
+ if (!ann.show || !ann.version || !ann.features || ann.features.length === 0) return
580
+
581
+ const title = `Magic Context v${ann.version}`
582
+ const lines: string[] = [
583
+ "What's new:",
584
+ "",
585
+ ...ann.features.map((line) => ` • ${line}`),
586
+ ]
587
+ if (ann.footer && ann.footer.trim().length > 0) {
588
+ // Blank-line separator keeps the persistent footer (Discord invite,
589
+ // etc.) visually distinct from the version-specific bullets.
590
+ lines.push("", ann.footer)
591
+ }
592
+ const message = lines.join("\n")
593
+
594
+ api.ui.dialog.replace(
595
+ () => (
596
+ <api.ui.DialogAlert
597
+ title={title}
598
+ message={message}
599
+ onConfirm={() => {
600
+ void markAnnounced()
601
+ }}
602
+ />
603
+ ),
604
+ () => {
605
+ // User dismissed via Escape rather than confirming. Mark
606
+ // dismissed anyway — they saw the dialog, that's the contract.
607
+ void markAnnounced()
608
+ },
609
+ )
610
+ } catch {
611
+ // RPC not ready yet (port file missing or transient HTTP failure) —
612
+ // silently skip. The next TUI start re-checks.
613
+ }
614
+ }
615
+
566
616
  const tui: TuiPlugin = async (api, _options, meta) => {
567
617
  // Initialize RPC client for server communication
568
618
  const directory = api.state.path.directory ?? ""
@@ -623,6 +673,12 @@ const tui: TuiPlugin = async (api, _options, meta) => {
623
673
  return
624
674
  }
625
675
 
676
+ // Show one-shot release announcement after conflict gate.
677
+ // Fire-and-forget: if the server isn't ready or RPC fails, the next TUI
678
+ // launch will retry. Dialog only appears once per ANNOUNCEMENT_VERSION
679
+ // (persisted via mark-announced RPC writing last_announced_version).
680
+ void showStartupAnnouncement(api)
681
+
626
682
  // Note: if TUI plugin is loaded, tui.json already has our entry.
627
683
  // But if the user added it manually and later removes it, or if they
628
684
  // use setup/doctor which handles tui.json, this code is already running.
@@ -3,6 +3,7 @@ import { createEffect, createMemo, createSignal, on, onCleanup } from "solid-js"
3
3
  import type { TuiSlotPlugin, TuiPluginApi, TuiThemeCurrent } from "@opencode-ai/plugin/tui"
4
4
  import packageJson from "../../../package.json"
5
5
  import { loadSidebarSnapshot, type SidebarSnapshot } from "../data/context-db"
6
+ import { formatThresholdPercent } from "../../shared/format-threshold"
6
7
 
7
8
  const SINGLE_BORDER = { type: "single" } as any
8
9
  const REFRESH_DEBOUNCE_MS = 150
@@ -47,8 +48,13 @@ const TokenBreakdown = (props: {
47
48
  theme: TuiThemeCurrent
48
49
  snapshot: SidebarSnapshot
49
50
  }) => {
50
- const barWidth = 36
51
-
51
+ // The bar is rendered as a flex row of colored boxes, each with
52
+ // flexGrow=tokens and flexBasis=0. opentui distributes the parent
53
+ // container's full width proportionally, so the bar always fills the
54
+ // sidebar regardless of terminal size. No hardcoded width is needed —
55
+ // this fixes both the over-wide bar that wrapped onto a second line on
56
+ // narrow sidebars (issue #90) and the under-wide bar that left empty
57
+ // space on the right on wide sidebars.
52
58
  const segments = createMemo<TokenSegment[]>(() => {
53
59
  const s = props.snapshot
54
60
  const total = s.inputTokens || 1
@@ -142,66 +148,33 @@ const TokenBreakdown = (props: {
142
148
 
143
149
  const totalTokens = createMemo(() => props.snapshot.inputTokens || 1)
144
150
 
145
- // Calculate proportional widths for each segment
146
- const segmentWidths = createMemo(() => {
147
- const total = totalTokens()
148
- const segs = segments()
149
- if (segs.length === 0) return []
150
-
151
- // Calculate raw proportions
152
- const proportions = segs.map((seg) => seg.tokens / total)
153
-
154
- // Convert to character widths. Minimum 1 char ONLY when the segment
155
- // has tokens > 0 — zero-token segments (e.g. Conversation when the
156
- // calibrator rounded it to zero) must get width 0 so the bar stays
157
- // proportional. The legend row still renders for zero-token segments
158
- // to keep the row stable.
159
- let widths = segs.map((seg, i) =>
160
- seg.tokens > 0 ? Math.max(1, Math.round(proportions[i] * barWidth)) : 0,
161
- )
162
-
163
- // Adjust to exactly barWidth
164
- const sum = widths.reduce((a, b) => a + b, 0)
165
- if (sum > barWidth) {
166
- // Shrink from the largest segments
167
- let excess = sum - barWidth
168
- while (excess > 0) {
169
- const maxIdx = widths.indexOf(Math.max(...widths))
170
- if (widths[maxIdx] > 1) {
171
- widths[maxIdx]--
172
- excess--
173
- } else {
174
- break
175
- }
176
- }
177
- } else if (sum < barWidth) {
178
- // Expand the largest segments
179
- let deficit = barWidth - sum
180
- while (deficit > 0) {
181
- const maxIdx = widths.indexOf(Math.max(...widths))
182
- widths[maxIdx]++
183
- deficit--
184
- }
185
- }
186
-
187
- return widths
188
- })
189
-
190
- const barSegments = createMemo(() => {
191
- const segs = segments()
192
- const widths = segmentWidths()
193
- return segs.map((seg, i) => ({
194
- chars: "█".repeat(widths[i] || 0),
195
- color: seg.color,
196
- }))
197
- })
151
+ // Render-time segments for the bar. Zero-token segments are filtered out
152
+ // entirely (no flex weight, no rendered box) so they don't claim any
153
+ // width. Non-zero segments still get a Math.max(1, ...) floor on
154
+ // flexGrow so very small contributions remain visible as a thin sliver.
155
+ // The legend rows below show every segment (including zeros) for table
156
+ // stability — only the bar prunes them.
157
+ const barSegments = createMemo(() =>
158
+ segments().filter((seg) => seg.tokens > 0),
159
+ )
198
160
 
199
161
  return (
200
162
  <box width="100%" flexDirection="column">
201
- {/* Segmented bar */}
202
- <box flexDirection="row">
203
- {barSegments().map((seg, i) => (
204
- <text key={i} fg={seg.color}>{seg.chars}</text>
163
+ {/* Segmented bar: a width="100%" flex row of colored boxes,
164
+ each with flexGrow proportional to its token count and
165
+ flexBasis=0. opentui distributes the parent's full width
166
+ proportionally, so the bar always fills the sidebar
167
+ regardless of terminal size. Height is fixed at 1 row;
168
+ backgroundColor renders the colored bar. */}
169
+ <box width="100%" flexDirection="row" height={1}>
170
+ {barSegments().map((seg) => (
171
+ <box
172
+ key={seg.key}
173
+ flexGrow={Math.max(1, seg.tokens)}
174
+ flexBasis={0}
175
+ height={1}
176
+ backgroundColor={seg.color}
177
+ />
205
178
  ))}
206
179
  </box>
207
180
 
@@ -370,7 +343,7 @@ const SidebarContent = (props: {
370
343
  "47.5% / 65%" tells the user how close they
371
344
  are to the next compaction trigger. */}
372
345
  <text fg={contextSummaryColor()}>
373
- <b>{s()!.usagePercentage.toFixed(1)}%</b> / {s()!.executeThreshold}%
346
+ <b>{s()!.usagePercentage.toFixed(1)}%</b> / {formatThresholdPercent(s()!.executeThreshold)}%
374
347
  </text>
375
348
  {/* Right: absolute token usage vs the model's
376
349
  full context window (separate from the
@@ -468,6 +441,24 @@ const SidebarContent = (props: {
468
441
  />
469
442
  </>
470
443
  )}
444
+
445
+ {/* Stats — v0.21.8 ships a single "Total tokens" number while we
446
+ figure out how to present the new-work / reprocessed
447
+ categorization without confusing users. The underlying
448
+ snapshot fields (newWorkTokens, totalInputTokens) and the
449
+ session_meta columns are still populated; only the UI is
450
+ simplified for now. */}
451
+ {s()?.totalInputTokens != null && (
452
+ <>
453
+ <SectionHeader theme={props.theme} title="Stats" />
454
+ <StatRow
455
+ theme={props.theme}
456
+ label="Total tokens"
457
+ value={compactTokens(s()!.totalInputTokens ?? 0)}
458
+ dim
459
+ />
460
+ </>
461
+ )}
471
462
  </box>
472
463
  )
473
464
  }