@cortexkit/opencode-magic-context 0.24.0 → 0.25.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 (101) hide show
  1. package/README.md +4 -2
  2. package/dist/agents/magic-context-prompt.d.ts.map +1 -1
  3. package/dist/features/magic-context/compartment-chunk-embedding.d.ts +18 -0
  4. package/dist/features/magic-context/compartment-chunk-embedding.d.ts.map +1 -1
  5. package/dist/features/magic-context/memory/embedding-local.d.ts +4 -0
  6. package/dist/features/magic-context/memory/embedding-local.d.ts.map +1 -1
  7. package/dist/features/magic-context/memory/embedding-openai.d.ts +14 -0
  8. package/dist/features/magic-context/memory/embedding-openai.d.ts.map +1 -1
  9. package/dist/features/magic-context/memory/storage-memory-embeddings.d.ts +6 -0
  10. package/dist/features/magic-context/memory/storage-memory-embeddings.d.ts.map +1 -1
  11. package/dist/features/magic-context/project-embedding-registry.d.ts +38 -0
  12. package/dist/features/magic-context/project-embedding-registry.d.ts.map +1 -1
  13. package/dist/features/magic-context/storage-db.d.ts.map +1 -1
  14. package/dist/features/magic-context/storage-meta-session.d.ts +1 -0
  15. package/dist/features/magic-context/storage-meta-session.d.ts.map +1 -1
  16. package/dist/features/magic-context/storage-meta-shared.d.ts +2 -1
  17. package/dist/features/magic-context/storage-meta-shared.d.ts.map +1 -1
  18. package/dist/features/magic-context/storage-meta.d.ts +1 -1
  19. package/dist/features/magic-context/storage-meta.d.ts.map +1 -1
  20. package/dist/features/magic-context/storage-tags.d.ts +20 -1
  21. package/dist/features/magic-context/storage-tags.d.ts.map +1 -1
  22. package/dist/features/magic-context/storage.d.ts +2 -2
  23. package/dist/features/magic-context/storage.d.ts.map +1 -1
  24. package/dist/features/magic-context/types.d.ts +1 -0
  25. package/dist/features/magic-context/types.d.ts.map +1 -1
  26. package/dist/hooks/magic-context/apply-operations.d.ts +3 -2
  27. package/dist/hooks/magic-context/apply-operations.d.ts.map +1 -1
  28. package/dist/hooks/magic-context/caveman-cleanup.d.ts +1 -0
  29. package/dist/hooks/magic-context/caveman-cleanup.d.ts.map +1 -1
  30. package/dist/hooks/magic-context/channel2-delivery.d.ts +2 -0
  31. package/dist/hooks/magic-context/channel2-delivery.d.ts.map +1 -1
  32. package/dist/hooks/magic-context/command-handler.d.ts +7 -5
  33. package/dist/hooks/magic-context/command-handler.d.ts.map +1 -1
  34. package/dist/hooks/magic-context/ctx-reduce-nudge.d.ts +14 -4
  35. package/dist/hooks/magic-context/ctx-reduce-nudge.d.ts.map +1 -1
  36. package/dist/hooks/magic-context/embed-session-state.d.ts +14 -0
  37. package/dist/hooks/magic-context/embed-session-state.d.ts.map +1 -0
  38. package/dist/hooks/magic-context/event-handler.d.ts.map +1 -1
  39. package/dist/hooks/magic-context/format-embed-status.d.ts +9 -0
  40. package/dist/hooks/magic-context/format-embed-status.d.ts.map +1 -0
  41. package/dist/hooks/magic-context/heuristic-cleanup.d.ts +1 -0
  42. package/dist/hooks/magic-context/heuristic-cleanup.d.ts.map +1 -1
  43. package/dist/hooks/magic-context/hook-handlers.d.ts.map +1 -1
  44. package/dist/hooks/magic-context/hook.d.ts.map +1 -1
  45. package/dist/hooks/magic-context/inject-compartments.d.ts.map +1 -1
  46. package/dist/hooks/magic-context/protected-tail-boundary.d.ts.map +1 -1
  47. package/dist/hooks/magic-context/read-session-true-raw-tokens.d.ts +1 -1
  48. package/dist/hooks/magic-context/read-session-true-raw-tokens.d.ts.map +1 -1
  49. package/dist/hooks/magic-context/recomp-orchestrator.d.ts.map +1 -1
  50. package/dist/hooks/magic-context/strip-content.d.ts +0 -1
  51. package/dist/hooks/magic-context/strip-content.d.ts.map +1 -1
  52. package/dist/hooks/magic-context/tag-content-primitives.d.ts +2 -0
  53. package/dist/hooks/magic-context/tag-content-primitives.d.ts.map +1 -1
  54. package/dist/hooks/magic-context/tag-messages.d.ts.map +1 -1
  55. package/dist/hooks/magic-context/tool-drop-target.d.ts +1 -1
  56. package/dist/hooks/magic-context/tool-drop-target.d.ts.map +1 -1
  57. package/dist/hooks/magic-context/tool-reclaim.d.ts +12 -0
  58. package/dist/hooks/magic-context/tool-reclaim.d.ts.map +1 -0
  59. package/dist/hooks/magic-context/transform-operations.d.ts +1 -1
  60. package/dist/hooks/magic-context/transform-operations.d.ts.map +1 -1
  61. package/dist/hooks/magic-context/transform-postprocess-phase.d.ts.map +1 -1
  62. package/dist/hooks/magic-context/transform.d.ts +2 -0
  63. package/dist/hooks/magic-context/transform.d.ts.map +1 -1
  64. package/dist/index.d.ts.map +1 -1
  65. package/dist/index.js +1117 -378
  66. package/dist/plugin/conflict-warning-hook.d.ts.map +1 -1
  67. package/dist/plugin/hooks/create-session-hooks.d.ts.map +1 -1
  68. package/dist/plugin/rpc-handlers.d.ts.map +1 -1
  69. package/dist/shared/announcement.d.ts +1 -1
  70. package/dist/shared/model-suggestion-retry.d.ts.map +1 -1
  71. package/dist/shared/rpc-types.d.ts +20 -0
  72. package/dist/shared/rpc-types.d.ts.map +1 -1
  73. package/dist/shared/sqlite.d.ts +5 -1
  74. package/dist/shared/sqlite.d.ts.map +1 -1
  75. package/dist/shared/tui-preferences.d.ts +32 -0
  76. package/dist/shared/tui-preferences.d.ts.map +1 -0
  77. package/dist/tools/ctx-expand/constants.d.ts +1 -1
  78. package/dist/tools/ctx-expand/constants.d.ts.map +1 -1
  79. package/dist/tools/ctx-expand/render.d.ts +43 -0
  80. package/dist/tools/ctx-expand/render.d.ts.map +1 -0
  81. package/dist/tools/ctx-expand/tools.d.ts.map +1 -1
  82. package/dist/tools/ctx-expand/types.d.ts +6 -2
  83. package/dist/tools/ctx-expand/types.d.ts.map +1 -1
  84. package/dist/tools/ctx-reduce/constants.d.ts +1 -1
  85. package/dist/tools/ctx-reduce/constants.d.ts.map +1 -1
  86. package/dist/tui/data/context-db.d.ts +4 -2
  87. package/dist/tui/data/context-db.d.ts.map +1 -1
  88. package/package.json +1 -1
  89. package/src/shared/announcement.ts +6 -6
  90. package/src/shared/model-suggestion-retry.test.ts +61 -1
  91. package/src/shared/model-suggestion-retry.ts +22 -0
  92. package/src/shared/rpc-types.ts +11 -0
  93. package/src/shared/sqlite-bind-style.test.ts +82 -0
  94. package/src/shared/sqlite.ts +30 -1
  95. package/src/shared/tag-transcript.test.ts +3 -1
  96. package/src/shared/tag-transcript.ts +19 -17
  97. package/src/shared/tui-preferences.test.ts +210 -0
  98. package/src/shared/tui-preferences.ts +303 -0
  99. package/src/tui/data/context-db.ts +34 -2
  100. package/src/tui/index.tsx +58 -4
  101. package/src/tui/slots/sidebar-content.tsx +106 -12
package/src/tui/index.tsx CHANGED
@@ -6,7 +6,7 @@ import { createMemo } from "solid-js"
6
6
  import type { TuiPlugin, TuiPluginApi, TuiThemeCurrent } from "@opencode-ai/plugin/tui"
7
7
  import { createSidebarContentSlot, kickRecompProgressRefresh } from "./slots/sidebar-content"
8
8
  import packageJson from "../../package.json"
9
- import { closeRpc, consumeTuiMessages, dismissUpgradeReminder, getAnnouncement, getCompartmentCount, getRpcGeneration, initRpcClient, loadStatusDetail, markAnnounced, markTuiMessagesHandled, requestRecomp, requestUpgrade, type TuiMessage, type StatusDetail } from "./data/context-db"
9
+ import { closeRpc, consumeTuiMessages, dismissUpgradeReminder, getAnnouncement, getCompartmentCount, getRpcGeneration, initRpcClient, loadEmbedDetail, loadStatusDetail, markAnnounced, markTuiMessagesHandled, requestRecomp, requestUpgrade, type EmbedDetail, 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"
@@ -353,7 +353,7 @@ const StatusDialog = (props: { api: TuiPluginApi; s: StatusDetail }) => {
353
353
  }
354
354
  if (p.phase === "migration") return <R t={t()} l="Status" v={p.note ?? "Migrating memories ⟳"} fg={t().warning} />
355
355
  if (p.phase === "done") return <R t={t()} l="Status" v={`✓ ${verb} complete`} fg={t().accent} />
356
- if (p.phase === "skipped") return <R t={t()} l="Status" v={`${verb} skipped — retry shortly${p.message ? `: ${p.message}` : ""}`} fg={t().textMuted} />
356
+ if (p.phase === "skipped") return <R t={t()} l="Status" v={p.message ?? `${verb} stopped early`} fg={t().textMuted} />
357
357
  return <R t={t()} l="Status" v={`✗ ${verb} failed${p.message ? `: ${p.message}` : ""}`} fg={t().error} />
358
358
  })()}
359
359
  </box>
@@ -580,14 +580,53 @@ async function showStatusDialog(api: TuiPluginApi, targetSessionId = getSessionI
580
580
  const directory = api.state.path.directory ?? ""
581
581
  const modelKey = getModelKeyFromMessages(api, sessionId)
582
582
  const detail = await loadStatusDetail(sessionId, directory, modelKey)
583
- // Ack only after the dialog is actually shown for the same active session;
584
- // route switches while the RPC detail load is in flight must leave it pending.
585
583
  if (getSessionId(api) !== sessionId) return false
586
584
 
587
585
  api.ui.dialog.replace(() => <StatusDialog api={api} s={detail} />)
588
586
  return true
589
587
  }
590
588
 
589
+ const EmbedDialog = (props: { api: TuiPluginApi; detail: EmbedDetail }) => {
590
+ const theme = createMemo(() => (props.api as any).theme.current)
591
+ const t = () => theme()
592
+ const lines = () => props.detail.statusText.split("\n")
593
+ return (
594
+ <box flexDirection="column" width="100%" paddingLeft={2} paddingRight={2} paddingTop={1} paddingBottom={1}>
595
+ <box justifyContent="center" width="100%" marginBottom={1}>
596
+ <text fg={t().accent}><b>Embedding</b></text>
597
+ </box>
598
+ {lines().map((line) => (
599
+ <text fg={t().text}>{line}</text>
600
+ ))}
601
+ </box>
602
+ )
603
+ }
604
+
605
+ async function showEmbedDialog(api: TuiPluginApi, targetSessionId = getSessionId(api)): Promise<boolean> {
606
+ const sessionId = targetSessionId
607
+ if (!sessionId) {
608
+ api.ui.toast({ message: "No active session", variant: "warning" })
609
+ return false
610
+ }
611
+ const directory = api.state.path.directory ?? ""
612
+ const detail = await loadEmbedDetail(sessionId, directory)
613
+ if (getSessionId(api) !== sessionId) return false
614
+ api.ui.dialog.replace(() => <EmbedDialog api={api} detail={detail} />)
615
+ return true
616
+ }
617
+
618
+ function showResultDialog(api: TuiPluginApi, title: string, message: string): boolean {
619
+ api.ui.dialog.replace(() => (
620
+ <api.ui.DialogAlert
621
+ title={title}
622
+ message={message}
623
+ onConfirm={() => {}}
624
+ />
625
+ ))
626
+ return true
627
+ }
628
+
629
+
591
630
  /**
592
631
  * Register Magic Context command palette entries, preferring the v1.14.42+
593
632
  * `keymap.registerLayer` API and falling back to the legacy
@@ -842,6 +881,21 @@ const tui: TuiPlugin = async (api, _options, meta) => {
842
881
  if (showUpgradeDialog(api, resume, requestedSessionId)) {
843
882
  handledMessageIds.add(msg.id)
844
883
  }
884
+ } else if (action === "show-embed-dialog") {
885
+ if (await showEmbedDialog(api, requestedSessionId)) {
886
+ handledMessageIds.add(msg.id)
887
+ }
888
+ } else if (action === "show-flush-dialog") {
889
+ const flushMsg = String(msg.payload?.message ?? "Flushed.")
890
+ if (showResultDialog(api, "Flush", flushMsg)) {
891
+ handledMessageIds.add(msg.id)
892
+ }
893
+ } else if (action === "show-result-dialog") {
894
+ const title = String(msg.payload?.title ?? "Magic Context")
895
+ const body = String(msg.payload?.message ?? "")
896
+ if (showResultDialog(api, title, body)) {
897
+ handledMessageIds.add(msg.id)
898
+ }
845
899
  }
846
900
  }
847
901
  }
@@ -4,6 +4,17 @@ import type { TuiSlotPlugin, TuiPluginApi, TuiThemeCurrent } from "@opencode-ai/
4
4
  import packageJson from "../../../package.json"
5
5
  import { loadSidebarSnapshot, type SidebarSnapshot } from "../data/context-db"
6
6
  import { formatThresholdPercent } from "../../shared/format-threshold"
7
+ import {
8
+ computeEffectiveOrder,
9
+ DEFAULT_SLOT_ORDER,
10
+ type MagicContextTuiPrefs,
11
+ PLUGIN_KEY,
12
+ queueTuiPreferenceUpdate,
13
+ readTuiPreferencesFile,
14
+ readTuiPreferencesFileSync,
15
+ resolveMagicContextPrefs,
16
+ watchTuiPreferences,
17
+ } from "../../shared/tui-preferences"
7
18
 
8
19
  // Module-level hook so the upgrade/recomp dialog can kick the sidebar into its
9
20
  // fast recomp self-poll the INSTANT the user confirms — without waiting for a
@@ -17,6 +28,64 @@ export function kickRecompProgressRefresh(): void {
17
28
  const SINGLE_BORDER = { type: "single" } as any
18
29
  const REFRESH_DEBOUNCE_MS = 150
19
30
 
31
+ export interface SidebarController {
32
+ prefs: () => MagicContextTuiPrefs
33
+ collapsed: () => boolean
34
+ toggleCollapsed: () => void
35
+ }
36
+
37
+ // The TUI may unmount and remount sidebar_content when the user switches views
38
+ // (main -> subagent -> main). A remount re-runs the component body, so a signal
39
+ // created inside the component would reset to its seed. The controller lives in
40
+ // the slot-factory closure (plugin/process lifetime) and owns the durable
41
+ // prefs/collapse signals plus the single shared file watcher, so collapse state
42
+ // and live pref reloads survive remounts. No Solid effects/memos here — those
43
+ // need an owner; the poll-interval effect stays inside the component.
44
+ function createSidebarController(initialPrefs: MagicContextTuiPrefs): SidebarController {
45
+ const [prefs, setPrefs] = createSignal<MagicContextTuiPrefs>(initialPrefs)
46
+ const seedCollapsed =
47
+ initialPrefs.rememberCollapsed && initialPrefs.collapsed != null
48
+ ? initialPrefs.collapsed
49
+ : initialPrefs.startCollapsed
50
+ const [collapsed, setCollapsed] = createSignal(seedCollapsed)
51
+ let lastPersistedCollapsed: boolean | null = initialPrefs.collapsed
52
+ let lastApplied = JSON.stringify(initialPrefs)
53
+
54
+ // Watcher lives for the process lifetime — intentionally never disposed.
55
+ // Collapse echo guard: lastPersistedCollapsed advances only once our own
56
+ // write lands, so a watcher echo of the value we just wrote is rejected by
57
+ // the `!==` check and cannot revert a user click.
58
+ watchTuiPreferences(() => {
59
+ void (async () => {
60
+ const next = resolveMagicContextPrefs(await readTuiPreferencesFile())
61
+ const serialized = JSON.stringify(next)
62
+ if (serialized === lastApplied) return
63
+ lastApplied = serialized
64
+ setPrefs(next)
65
+ if (
66
+ next.rememberCollapsed &&
67
+ next.collapsed != null &&
68
+ next.collapsed !== lastPersistedCollapsed
69
+ ) {
70
+ lastPersistedCollapsed = next.collapsed
71
+ setCollapsed(next.collapsed)
72
+ }
73
+ })()
74
+ })
75
+
76
+ function toggleCollapsed() {
77
+ const next = !collapsed()
78
+ setCollapsed(next)
79
+ if (prefs().rememberCollapsed) {
80
+ void queueTuiPreferenceUpdate(PLUGIN_KEY, ["collapsed"], next).then(() => {
81
+ lastPersistedCollapsed = next
82
+ })
83
+ }
84
+ }
85
+
86
+ return { prefs, collapsed, toggleCollapsed }
87
+ }
88
+
20
89
  function compactTokens(value: number): string {
21
90
  if (value >= 1_000_000) return `${(value / 1_000_000).toFixed(1)}M`
22
91
  if (value >= 1_000) return `${(value / 1_000).toFixed(0)}K`
@@ -325,8 +394,13 @@ const RecompProgressSection = (props: {
325
394
  case "done":
326
395
  return { text: `✓ ${verb()} complete`, color: props.theme.success ?? props.theme.accent }
327
396
  case "skipped":
328
- // Transient (lease busy) neutral, not an error.
329
- return { text: `${verb()} skipped retry shortly`, color: props.theme.textMuted }
397
+ // Neutral terse status next to the bold verb header; the full,
398
+ // self-contained reason (lease-busy "try again shortly" vs a
399
+ // partial-stall "run /ctx-embed start again") renders on its own
400
+ // line below. Don't re-prepend verb here (it's already the bold
401
+ // header — doing so read as "EmbedEmbed"), and don't hardcode
402
+ // "retry shortly" (wrong for a partial stall).
403
+ return { text: "stopped", color: props.theme.textMuted }
330
404
  case "failed":
331
405
  return { text: `✗ ${verb()} failed`, color: props.theme.error }
332
406
  }
@@ -382,12 +456,15 @@ const SidebarContent = (props: {
382
456
  api: TuiPluginApi
383
457
  sessionID: () => string
384
458
  theme: TuiThemeCurrent
459
+ controller: SidebarController
385
460
  }) => {
386
461
  const [snapshot, setSnapshot] = createSignal<SidebarSnapshot | null>(null)
387
- // Collapsed view: progress bar + 3 summary lines (Historian / Memories /
388
- // Status), no per-category legend or section grid. In-memory only (resets
389
- // to expanded on TUI restart), mirroring the native MCP sidebar toggle.
390
- const [collapsed, setCollapsed] = createSignal(false)
462
+ // Collapse state + section visibility prefs live in the controller (plugin
463
+ // closure), so they survive view-switch remounts and persist across restarts
464
+ // via ~/.config/opencode/tui-preferences.jsonc. Read reactively.
465
+ const collapsed = props.controller.collapsed
466
+ const sections = () => props.controller.prefs().sections
467
+ const headerLabel = () => props.controller.prefs().header.label
391
468
  let refreshTimer: ReturnType<typeof setTimeout> | undefined
392
469
  // Self-sustaining poll while a recomp/upgrade is running. Recomp work
393
470
  // happens in CHILD sessions whose message events are filtered out of the
@@ -610,11 +687,11 @@ const SidebarContent = (props: {
610
687
  flexDirection="row"
611
688
  justifyContent="space-between"
612
689
  alignItems="center"
613
- onMouseDown={() => setCollapsed((x) => !x)}
690
+ onMouseDown={() => props.controller.toggleCollapsed()}
614
691
  >
615
692
  <box paddingLeft={1} paddingRight={1} backgroundColor={props.theme.accent}>
616
693
  <text fg={props.theme.background}>
617
- <b>{collapsed() ? "▶ " : "▼ "}Magic Context</b>
694
+ <b>{collapsed() ? "▶ " : "▼ "}{headerLabel()}</b>
618
695
  </text>
619
696
  </box>
620
697
  <text fg={props.theme.textMuted}>v{packageJson.version}</text>
@@ -688,6 +765,8 @@ const SidebarContent = (props: {
688
765
  {!collapsed() && (
689
766
  <>
690
767
  {/* Historian section */}
768
+ {sections().historian && (
769
+ <>
691
770
  <box width="100%" marginTop={1} flexDirection="row" justifyContent="space-between">
692
771
  <text fg={props.theme.text}>
693
772
  <b>Historian</b>
@@ -713,8 +792,12 @@ const SidebarContent = (props: {
713
792
  {s()?.recompProgress && (
714
793
  <RecompProgressSection theme={props.theme} progress={s()!.recompProgress!} />
715
794
  )}
795
+ </>
796
+ )}
716
797
 
717
798
  {/* Memory section */}
799
+ {sections().memory && (
800
+ <>
718
801
  <SectionHeader theme={props.theme} title="Memory" />
719
802
  <StatRow
720
803
  theme={props.theme}
@@ -730,9 +813,12 @@ const SidebarContent = (props: {
730
813
  dim
731
814
  />
732
815
  )}
816
+ </>
817
+ )}
733
818
 
734
819
  {/* Queue & Status */}
735
- {((s()?.pendingOpsCount ?? 0) > 0 ||
820
+ {sections().status &&
821
+ ((s()?.pendingOpsCount ?? 0) > 0 ||
736
822
  (s()?.sessionNoteCount ?? 0) > 0 ||
737
823
  (s()?.readySmartNoteCount ?? 0) > 0) && (
738
824
  <>
@@ -764,7 +850,7 @@ const SidebarContent = (props: {
764
850
  )}
765
851
 
766
852
  {/* Dreamer */}
767
- {s()?.lastDreamerRunAt && (
853
+ {sections().dreamer && s()?.lastDreamerRunAt && (
768
854
  <>
769
855
  <SectionHeader theme={props.theme} title="Dreamer" />
770
856
  <StatRow
@@ -782,7 +868,7 @@ const SidebarContent = (props: {
782
868
  snapshot fields (newWorkTokens, totalInputTokens) and the
783
869
  session_meta columns are still populated; only the UI is
784
870
  simplified for now. */}
785
- {s()?.totalInputTokens != null && (
871
+ {sections().stats && s()?.totalInputTokens != null && (
786
872
  <>
787
873
  <SectionHeader theme={props.theme} title="Stats" />
788
874
  <StatRow
@@ -800,8 +886,15 @@ const SidebarContent = (props: {
800
886
  }
801
887
 
802
888
  export function createSidebarContentSlot(api: TuiPluginApi): TuiSlotPlugin {
889
+ // Seed synchronously at slot construction so the sidebar renders at its
890
+ // final collapse state + order on the first paint (no async flicker). The
891
+ // controller lives here in the factory closure for the plugin lifetime, so
892
+ // collapse state and live pref reloads survive sidebar_content remounts.
893
+ const seedRoot = readTuiPreferencesFileSync()
894
+ const controller = createSidebarController(resolveMagicContextPrefs(seedRoot))
895
+ const effectiveOrder = computeEffectiveOrder(seedRoot, PLUGIN_KEY, DEFAULT_SLOT_ORDER)
803
896
  return {
804
- order: 150,
897
+ order: effectiveOrder,
805
898
  slots: {
806
899
  sidebar_content: (ctx, value) => {
807
900
  const theme = createMemo(() => ctx.theme.current)
@@ -810,6 +903,7 @@ export function createSidebarContentSlot(api: TuiPluginApi): TuiSlotPlugin {
810
903
  api={api}
811
904
  sessionID={() => value.session_id}
812
905
  theme={theme()}
906
+ controller={controller}
813
907
  />
814
908
  )
815
909
  },