@cortexkit/opencode-magic-context 0.4.2 → 0.5.1
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.
- package/README.md +41 -0
- package/dist/cli/config-paths.d.ts +2 -0
- package/dist/cli/config-paths.d.ts.map +1 -1
- package/dist/cli/doctor.d.ts +2 -0
- package/dist/cli/doctor.d.ts.map +1 -0
- package/dist/cli/setup.d.ts.map +1 -1
- package/dist/cli.js +8524 -106
- package/dist/features/builtin-commands/commands.d.ts.map +1 -1
- package/dist/features/magic-context/compartment-storage.d.ts.map +1 -1
- package/dist/features/magic-context/scheduler.d.ts.map +1 -1
- package/dist/features/magic-context/search.d.ts +2 -0
- package/dist/features/magic-context/search.d.ts.map +1 -1
- package/dist/hooks/magic-context/compartment-runner-incremental.d.ts.map +1 -1
- package/dist/hooks/magic-context/compartment-runner-recomp.d.ts.map +1 -1
- package/dist/hooks/magic-context/inject-compartments.d.ts.map +1 -1
- package/dist/hooks/magic-context/send-session-notification.d.ts.map +1 -1
- package/dist/hooks/magic-context/transform.d.ts.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8653 -238
- package/dist/plugin/conflict-warning-hook.d.ts +24 -0
- package/dist/plugin/conflict-warning-hook.d.ts.map +1 -0
- package/dist/shared/conflict-detector.d.ts +29 -0
- package/dist/shared/conflict-detector.d.ts.map +1 -0
- package/dist/shared/conflict-fixer.d.ts +3 -0
- package/dist/shared/conflict-fixer.d.ts.map +1 -0
- package/dist/shared/tui-config.d.ts +10 -0
- package/dist/shared/tui-config.d.ts.map +1 -0
- package/dist/tools/ctx-search/tools.d.ts.map +1 -1
- package/dist/tui/data/context-db.d.ts +54 -0
- package/dist/tui/data/context-db.d.ts.map +1 -0
- package/package.json +20 -1
- package/src/tui/data/context-db.ts +584 -0
- package/src/tui/index.tsx +461 -0
- package/src/tui/slots/sidebar-content.tsx +422 -0
- package/src/tui/types/opencode-plugin-tui.d.ts +232 -0
|
@@ -0,0 +1,461 @@
|
|
|
1
|
+
/** @jsxImportSource @opentui/solid */
|
|
2
|
+
// @ts-nocheck
|
|
3
|
+
import { existsSync, mkdirSync, writeFileSync } from "node:fs"
|
|
4
|
+
import { dirname, join } from "node:path"
|
|
5
|
+
import { createMemo } from "solid-js"
|
|
6
|
+
import type { TuiPlugin, TuiPluginApi, TuiThemeCurrent } from "@opencode-ai/plugin/tui"
|
|
7
|
+
import { createSidebarContentSlot } from "./slots/sidebar-content"
|
|
8
|
+
import { closeDb, loadStatusDetail, type StatusDetail } from "./data/context-db"
|
|
9
|
+
import { detectConflicts } from "../shared/conflict-detector"
|
|
10
|
+
import { fixConflicts } from "../shared/conflict-fixer"
|
|
11
|
+
import { readJsoncFile } from "../shared/jsonc-parser"
|
|
12
|
+
import { getOpenCodeConfigPaths } from "../shared/opencode-config-dir"
|
|
13
|
+
|
|
14
|
+
const PLUGIN_NAME = "@cortexkit/opencode-magic-context"
|
|
15
|
+
|
|
16
|
+
function ensureParentDir(filePath: string) {
|
|
17
|
+
mkdirSync(dirname(filePath), { recursive: true })
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function resolveTuiConfigPath() {
|
|
21
|
+
const configDir = getOpenCodeConfigPaths({ binary: "opencode" }).configDir
|
|
22
|
+
const jsoncPath = join(configDir, "tui.jsonc")
|
|
23
|
+
const jsonPath = join(configDir, "tui.json")
|
|
24
|
+
|
|
25
|
+
if (existsSync(jsoncPath)) {
|
|
26
|
+
return jsoncPath
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (existsSync(jsonPath)) {
|
|
30
|
+
return jsonPath
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return jsonPath
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function readTuiConfig(filePath: string): Record<string, unknown> | null {
|
|
37
|
+
if (!existsSync(filePath)) {
|
|
38
|
+
return {}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return readJsoncFile<Record<string, unknown>>(filePath)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function hasMagicContextTuiPlugin(): boolean {
|
|
45
|
+
const configPath = resolveTuiConfigPath()
|
|
46
|
+
const config = readTuiConfig(configPath)
|
|
47
|
+
if (!config) {
|
|
48
|
+
return false
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const plugins = Array.isArray(config.plugin)
|
|
52
|
+
? config.plugin.filter((plugin): plugin is string => typeof plugin === "string")
|
|
53
|
+
: []
|
|
54
|
+
|
|
55
|
+
return plugins.some((plugin) => plugin === PLUGIN_NAME || plugin.startsWith(`${PLUGIN_NAME}@`))
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function addMagicContextTuiPlugin(): { ok: boolean; updated: boolean } {
|
|
59
|
+
const configPath = resolveTuiConfigPath()
|
|
60
|
+
const config = readTuiConfig(configPath)
|
|
61
|
+
if (!config) {
|
|
62
|
+
return { ok: false, updated: false }
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const plugins = Array.isArray(config.plugin)
|
|
66
|
+
? config.plugin.filter((plugin): plugin is string => typeof plugin === "string")
|
|
67
|
+
: []
|
|
68
|
+
|
|
69
|
+
if (plugins.some((plugin) => plugin === PLUGIN_NAME || plugin.startsWith(`${PLUGIN_NAME}@`))) {
|
|
70
|
+
return { ok: true, updated: false }
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
plugins.push(PLUGIN_NAME)
|
|
74
|
+
config.plugin = plugins
|
|
75
|
+
|
|
76
|
+
ensureParentDir(configPath)
|
|
77
|
+
writeFileSync(configPath, `${JSON.stringify(config, null, 2)}\n`)
|
|
78
|
+
return { ok: true, updated: true }
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function showConflictDialog(api: TuiPluginApi, directory: string, reasons: string[], conflicts: ReturnType<typeof detectConflicts>["conflicts"]) {
|
|
82
|
+
api.ui.dialog.replace(() => (
|
|
83
|
+
<api.ui.DialogConfirm
|
|
84
|
+
title="⚠️ Magic Context Disabled"
|
|
85
|
+
message={`${reasons.join("\n")}\n\nFix these conflicts automatically?`}
|
|
86
|
+
onConfirm={() => {
|
|
87
|
+
const actions = fixConflicts(directory, conflicts)
|
|
88
|
+
const actionSummary = actions.length > 0
|
|
89
|
+
? actions.map(a => `• ${a}`).join("\n")
|
|
90
|
+
: "No changes needed"
|
|
91
|
+
// DialogConfirm calls dialog.clear() after onConfirm, so defer the next dialog
|
|
92
|
+
setTimeout(() => {
|
|
93
|
+
api.ui.dialog.replace(() => (
|
|
94
|
+
<api.ui.DialogAlert
|
|
95
|
+
title="✅ Configuration Fixed"
|
|
96
|
+
message={`${actionSummary}\n\nPlease restart OpenCode for changes to take effect.`}
|
|
97
|
+
onConfirm={() => {
|
|
98
|
+
api.ui.toast({ message: "Restart OpenCode to enable Magic Context", variant: "warning", duration: 10000 })
|
|
99
|
+
}}
|
|
100
|
+
/>
|
|
101
|
+
))
|
|
102
|
+
}, 50)
|
|
103
|
+
}}
|
|
104
|
+
onCancel={() => {
|
|
105
|
+
api.ui.toast({ message: "Magic Context remains disabled. Run: bunx @cortexkit/opencode-magic-context doctor", variant: "warning", duration: 5000 })
|
|
106
|
+
}}
|
|
107
|
+
/>
|
|
108
|
+
))
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function showTuiSetupDialog(api: TuiPluginApi) {
|
|
112
|
+
api.ui.dialog.replace(() => (
|
|
113
|
+
<api.ui.DialogConfirm
|
|
114
|
+
title="✨ Enable Magic Context Sidebar"
|
|
115
|
+
message={[
|
|
116
|
+
"Magic Context can show a sidebar with live context breakdown,",
|
|
117
|
+
"token usage, historian status, memory counts, and dreamer info.",
|
|
118
|
+
"",
|
|
119
|
+
"This requires adding the plugin to your tui.json config",
|
|
120
|
+
"(OpenCode's TUI plugin configuration file).",
|
|
121
|
+
"",
|
|
122
|
+
"Add it now?",
|
|
123
|
+
].join("\n")}
|
|
124
|
+
onConfirm={() => {
|
|
125
|
+
const result = addMagicContextTuiPlugin()
|
|
126
|
+
if (!result.ok) {
|
|
127
|
+
setTimeout(() => {
|
|
128
|
+
api.ui.dialog.replace(() => (
|
|
129
|
+
<api.ui.DialogAlert
|
|
130
|
+
title="❌ Setup Failed"
|
|
131
|
+
message={'Could not update tui.json automatically. Add the plugin manually:\n\n { "plugin": ["@cortexkit/opencode-magic-context"] }'}
|
|
132
|
+
onConfirm={() => {
|
|
133
|
+
api.ui.toast({ message: "Add plugin to tui.json manually", variant: "warning", duration: 5000 })
|
|
134
|
+
}}
|
|
135
|
+
/>
|
|
136
|
+
))
|
|
137
|
+
}, 50)
|
|
138
|
+
return
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
setTimeout(() => {
|
|
142
|
+
api.ui.dialog.replace(() => (
|
|
143
|
+
<api.ui.DialogAlert
|
|
144
|
+
title="✅ Sidebar Enabled"
|
|
145
|
+
message="tui.json updated with Magic Context plugin.\n\nPlease restart OpenCode to see the sidebar."
|
|
146
|
+
onConfirm={() => {
|
|
147
|
+
api.ui.toast({ message: "Restart OpenCode to see the sidebar", variant: "warning", duration: 10000 })
|
|
148
|
+
}}
|
|
149
|
+
/>
|
|
150
|
+
))
|
|
151
|
+
}, 50)
|
|
152
|
+
}}
|
|
153
|
+
onCancel={() => {
|
|
154
|
+
api.ui.toast({ message: "You can add the sidebar later via: bunx @cortexkit/opencode-magic-context doctor", variant: "info", duration: 5000 })
|
|
155
|
+
}}
|
|
156
|
+
/>
|
|
157
|
+
))
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function fmt(n: number): string {
|
|
161
|
+
if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(1)}M`
|
|
162
|
+
if (n >= 1_000) return `${Math.round(n / 1_000)}K`
|
|
163
|
+
return String(n)
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function fmtBytes(n: number): string {
|
|
167
|
+
if (n >= 1_048_576) return `${(n / 1_048_576).toFixed(1)} MB`
|
|
168
|
+
if (n >= 1_024) return `${Math.round(n / 1_024)} KB`
|
|
169
|
+
return `${n} B`
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function relTime(ms: number): string {
|
|
173
|
+
const d = Date.now() - ms
|
|
174
|
+
if (d < 60_000) return "just now"
|
|
175
|
+
if (d < 3_600_000) return `${Math.floor(d / 60_000)}m ago`
|
|
176
|
+
if (d < 86_400_000) return `${Math.floor(d / 3_600_000)}h ago`
|
|
177
|
+
return `${Math.floor(d / 86_400_000)}d ago`
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function getSessionId(api: TuiPluginApi): string | null {
|
|
181
|
+
try {
|
|
182
|
+
const route = api.route.current
|
|
183
|
+
if (route?.name === "session" && route.params?.sessionID) {
|
|
184
|
+
return route.params.sessionID
|
|
185
|
+
}
|
|
186
|
+
} catch {
|
|
187
|
+
// ignore
|
|
188
|
+
}
|
|
189
|
+
return null
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const R = (props: { t: TuiThemeCurrent; l: string; v: string; fg?: string }) => (
|
|
193
|
+
<box width="100%" flexDirection="row" justifyContent="space-between">
|
|
194
|
+
<text fg={props.t.textMuted}>{props.l}</text>
|
|
195
|
+
<text fg={props.fg ?? props.t.text}>{props.v}</text>
|
|
196
|
+
</box>
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
const StatusDialog = (props: { api: TuiPluginApi; s: StatusDetail }) => {
|
|
200
|
+
const theme = createMemo(() => (props.api as any).theme.current)
|
|
201
|
+
const t = () => theme()
|
|
202
|
+
const s = () => props.s
|
|
203
|
+
|
|
204
|
+
const contextLimit = () =>
|
|
205
|
+
s().usagePercentage > 0 ? Math.round(s().inputTokens / (s().usagePercentage / 100)) : 0
|
|
206
|
+
|
|
207
|
+
const elapsed = () => (s().lastResponseTime > 0 ? Date.now() - s().lastResponseTime : 0)
|
|
208
|
+
|
|
209
|
+
// Token breakdown segments — same colors as sidebar
|
|
210
|
+
const COLORS = {
|
|
211
|
+
system: "#c084fc",
|
|
212
|
+
compartments: "#60a5fa",
|
|
213
|
+
facts: "#fbbf24",
|
|
214
|
+
memories: "#34d399",
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const breakdownSegments = () => {
|
|
218
|
+
const d = s()
|
|
219
|
+
const total = d.inputTokens || 1
|
|
220
|
+
const segs: Array<{ label: string; tokens: number; color: string; detail?: string }> = []
|
|
221
|
+
|
|
222
|
+
if (d.systemPromptTokens > 0)
|
|
223
|
+
segs.push({ label: "System", tokens: d.systemPromptTokens, color: COLORS.system })
|
|
224
|
+
if (d.compartmentTokens > 0)
|
|
225
|
+
segs.push({
|
|
226
|
+
label: "Compartments",
|
|
227
|
+
tokens: d.compartmentTokens,
|
|
228
|
+
color: COLORS.compartments,
|
|
229
|
+
detail: `(${d.compartmentCount})`,
|
|
230
|
+
})
|
|
231
|
+
if (d.factTokens > 0)
|
|
232
|
+
segs.push({
|
|
233
|
+
label: "Facts",
|
|
234
|
+
tokens: d.factTokens,
|
|
235
|
+
color: COLORS.facts,
|
|
236
|
+
detail: `(${d.factCount})`,
|
|
237
|
+
})
|
|
238
|
+
if (d.memoryTokens > 0)
|
|
239
|
+
segs.push({
|
|
240
|
+
label: "Memories",
|
|
241
|
+
tokens: d.memoryTokens,
|
|
242
|
+
color: COLORS.memories,
|
|
243
|
+
detail: `(${d.memoryBlockCount})`,
|
|
244
|
+
})
|
|
245
|
+
|
|
246
|
+
const used = d.systemPromptTokens + d.compartmentTokens + d.factTokens + d.memoryTokens
|
|
247
|
+
const convTokens = Math.max(0, d.inputTokens - used)
|
|
248
|
+
if (convTokens > 0)
|
|
249
|
+
segs.push({ label: "Conversation", tokens: convTokens, color: t().textMuted })
|
|
250
|
+
|
|
251
|
+
return { segs, total }
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const barWidth = 56
|
|
255
|
+
const barSegments = () => {
|
|
256
|
+
const { segs, total } = breakdownSegments()
|
|
257
|
+
if (segs.length === 0) return []
|
|
258
|
+
|
|
259
|
+
let widths = segs.map((seg) => Math.max(1, Math.round((seg.tokens / total) * barWidth)))
|
|
260
|
+
let sum = widths.reduce((a, b) => a + b, 0)
|
|
261
|
+
while (sum > barWidth) {
|
|
262
|
+
const maxIdx = widths.indexOf(Math.max(...widths))
|
|
263
|
+
if (widths[maxIdx] > 1) { widths[maxIdx]--; sum-- } else break
|
|
264
|
+
}
|
|
265
|
+
while (sum < barWidth) {
|
|
266
|
+
const maxIdx = widths.indexOf(Math.max(...widths))
|
|
267
|
+
widths[maxIdx]++; sum++
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
return segs.map((seg, i) => ({
|
|
271
|
+
chars: "█".repeat(widths[i] || 0),
|
|
272
|
+
color: seg.color,
|
|
273
|
+
}))
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
return (
|
|
277
|
+
<box flexDirection="column" width="100%" paddingLeft={2} paddingRight={2} paddingTop={1} paddingBottom={1}>
|
|
278
|
+
{/* Title */}
|
|
279
|
+
<box justifyContent="center" width="100%" marginBottom={1}>
|
|
280
|
+
<text fg={t().accent}>
|
|
281
|
+
<b>⚡ Magic Context Status</b>
|
|
282
|
+
</text>
|
|
283
|
+
</box>
|
|
284
|
+
|
|
285
|
+
{/* Context summary line */}
|
|
286
|
+
<box flexDirection="row" justifyContent="space-between" width="100%">
|
|
287
|
+
<text fg={t().text}>Context</text>
|
|
288
|
+
<text fg={s().usagePercentage >= 80 ? t().error : s().usagePercentage >= 65 ? t().warning : t().accent}>
|
|
289
|
+
<b>{s().usagePercentage.toFixed(1)}%</b> · {fmt(s().inputTokens)} / {contextLimit() > 0 ? fmt(contextLimit()) : "?"} tokens
|
|
290
|
+
</text>
|
|
291
|
+
</box>
|
|
292
|
+
|
|
293
|
+
{/* Segmented breakdown bar */}
|
|
294
|
+
<box flexDirection="row">
|
|
295
|
+
{barSegments().map((seg, i) => (
|
|
296
|
+
<text key={i} fg={seg.color}>{seg.chars}</text>
|
|
297
|
+
))}
|
|
298
|
+
</box>
|
|
299
|
+
|
|
300
|
+
{/* Breakdown legend */}
|
|
301
|
+
<box flexDirection="column">
|
|
302
|
+
{breakdownSegments().segs.map((seg) => {
|
|
303
|
+
const pct = ((seg.tokens / breakdownSegments().total) * 100).toFixed(1)
|
|
304
|
+
return (
|
|
305
|
+
<box key={seg.label} width="100%" flexDirection="row" justifyContent="space-between">
|
|
306
|
+
<text fg={seg.color}>{seg.label} {seg.detail ?? ""}</text>
|
|
307
|
+
<text fg={t().textMuted}>{fmt(seg.tokens)} ({pct}%)</text>
|
|
308
|
+
</box>
|
|
309
|
+
)
|
|
310
|
+
})}
|
|
311
|
+
</box>
|
|
312
|
+
|
|
313
|
+
{/* 2-column layout */}
|
|
314
|
+
<box flexDirection="row" width="100%" marginTop={1} gap={4}>
|
|
315
|
+
{/* Left column */}
|
|
316
|
+
<box flexDirection="column" flexGrow={1} flexBasis={0}>
|
|
317
|
+
<text fg={t().text}><b>Tags</b></text>
|
|
318
|
+
<R t={t()} l="Active" v={`${s().activeTags} (~${fmtBytes(s().activeBytes)})`} />
|
|
319
|
+
<R t={t()} l="Dropped" v={String(s().droppedTags)} />
|
|
320
|
+
<R t={t()} l="Total" v={String(s().totalTags)} fg={t().textMuted} />
|
|
321
|
+
|
|
322
|
+
<box marginTop={1}>
|
|
323
|
+
<text fg={t().text}><b>Pending Queue</b></text>
|
|
324
|
+
</box>
|
|
325
|
+
<R t={t()} l="Drops" v={String(s().pendingOpsCount)} fg={s().pendingOpsCount > 0 ? t().warning : t().textMuted} />
|
|
326
|
+
|
|
327
|
+
<box marginTop={1}>
|
|
328
|
+
<text fg={t().text}><b>Cache TTL</b></text>
|
|
329
|
+
</box>
|
|
330
|
+
<R t={t()} l="Configured" v={s().cacheTtl} />
|
|
331
|
+
<R t={t()} l="Last response" v={s().lastResponseTime > 0 ? `${Math.round(elapsed() / 1000)}s ago` : "never"} />
|
|
332
|
+
<R t={t()} l="Remaining" v={s().cacheExpired ? "expired" : `${Math.round(s().cacheRemainingMs / 1000)}s`} fg={s().cacheExpired ? t().warning : t().textMuted} />
|
|
333
|
+
<R t={t()} l="Auto-execute" v={s().cacheExpired ? "yes (expired)" : `at TTL or ≥${s().executeThreshold}%`} fg={t().textMuted} />
|
|
334
|
+
|
|
335
|
+
<box marginTop={1}>
|
|
336
|
+
<text fg={t().text}><b>Memory</b></text>
|
|
337
|
+
</box>
|
|
338
|
+
<R t={t()} l="Active" v={String(s().memoryCount)} fg={t().accent} />
|
|
339
|
+
<R t={t()} l="Injected" v={String(s().memoryBlockCount)} fg={t().textMuted} />
|
|
340
|
+
</box>
|
|
341
|
+
|
|
342
|
+
{/* Right column */}
|
|
343
|
+
<box flexDirection="column" flexGrow={1} flexBasis={0}>
|
|
344
|
+
<text fg={t().text}><b>Rolling Nudges</b></text>
|
|
345
|
+
<R t={t()} l="Execute threshold" v={`${s().executeThreshold}%`} />
|
|
346
|
+
<R t={t()} l="Nudge anchor" v={`${fmt(s().lastNudgeTokens)} tok`} />
|
|
347
|
+
<R t={t()} l="Interval" v={`${fmt(s().nudgeInterval)} tok`} fg={t().textMuted} />
|
|
348
|
+
<R t={t()} l="Next nudge after" v={`${fmt(s().nextNudgeAfter)} tok`} />
|
|
349
|
+
{s().lastNudgeBand ? <R t={t()} l="Current band" v={s().lastNudgeBand} /> : null}
|
|
350
|
+
|
|
351
|
+
<box marginTop={1}>
|
|
352
|
+
<text fg={t().text}><b>Context Details</b></text>
|
|
353
|
+
</box>
|
|
354
|
+
<R t={t()} l="Protected tags" v={String(s().protectedTagCount)} fg={t().textMuted} />
|
|
355
|
+
<R t={t()} l="Subagent" v={s().isSubagent ? "yes" : "no"} fg={t().textMuted} />
|
|
356
|
+
|
|
357
|
+
<box marginTop={1}>
|
|
358
|
+
<text fg={t().text}><b>History Compression</b></text>
|
|
359
|
+
</box>
|
|
360
|
+
<R t={t()} l="History block" v={`~${fmt(s().historyBlockTokens)} tok`} />
|
|
361
|
+
{s().compressionBudget != null && (
|
|
362
|
+
<R t={t()} l="Budget" v={`~${fmt(s().compressionBudget!)} tok (${s().compressionUsage} used)`} />
|
|
363
|
+
)}
|
|
364
|
+
{s().lastDreamerRunAt && (
|
|
365
|
+
<R t={t()} l="Dreamer" v={`last ${relTime(s().lastDreamerRunAt!)}`} fg={t().textMuted} />
|
|
366
|
+
)}
|
|
367
|
+
</box>
|
|
368
|
+
</box>
|
|
369
|
+
|
|
370
|
+
{/* Error (full width, conditional) */}
|
|
371
|
+
{s().lastTransformError && (
|
|
372
|
+
<box marginTop={1} width="100%">
|
|
373
|
+
<text fg={t().error}>⚠ {s().lastTransformError}</text>
|
|
374
|
+
</box>
|
|
375
|
+
)}
|
|
376
|
+
|
|
377
|
+
{/* Footer */}
|
|
378
|
+
<box marginTop={1} justifyContent="flex-end" width="100%">
|
|
379
|
+
<text fg={t().textMuted}>Esc to close</text>
|
|
380
|
+
</box>
|
|
381
|
+
</box>
|
|
382
|
+
)
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
function getModelKeyFromMessages(api: TuiPluginApi, sessionId: string): string | undefined {
|
|
386
|
+
try {
|
|
387
|
+
const msgs = api.state.session.messages(sessionId)
|
|
388
|
+
// Find the last assistant message with model info
|
|
389
|
+
// AssistantMessage has providerID/modelID as top-level fields
|
|
390
|
+
// UserMessage has model: { providerID, modelID }
|
|
391
|
+
for (let i = msgs.length - 1; i >= 0; i--) {
|
|
392
|
+
const msg = msgs[i] as Record<string, unknown>
|
|
393
|
+
if (msg.role === "assistant" && msg.providerID && msg.modelID) {
|
|
394
|
+
return `${msg.providerID}/${msg.modelID}`
|
|
395
|
+
}
|
|
396
|
+
if (msg.role === "user") {
|
|
397
|
+
const model = msg.model as Record<string, unknown> | undefined
|
|
398
|
+
if (model?.providerID && model?.modelID) {
|
|
399
|
+
return `${model.providerID}/${model.modelID}`
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
} catch {
|
|
404
|
+
// messages not available
|
|
405
|
+
}
|
|
406
|
+
return undefined
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
function showStatusDialog(api: TuiPluginApi) {
|
|
410
|
+
const sessionId = getSessionId(api)
|
|
411
|
+
if (!sessionId) {
|
|
412
|
+
api.ui.toast({ message: "No active session", variant: "warning" })
|
|
413
|
+
return
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
const directory = api.state.path.directory ?? ""
|
|
417
|
+
const modelKey = getModelKeyFromMessages(api, sessionId)
|
|
418
|
+
const detail = loadStatusDetail(sessionId, directory, modelKey)
|
|
419
|
+
|
|
420
|
+
api.ui.dialog.replace(() => <StatusDialog api={api} s={detail} />)
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
const tui: TuiPlugin = async (api, _options, meta) => {
|
|
424
|
+
// Register sidebar slot
|
|
425
|
+
api.slots.register(createSidebarContentSlot(api))
|
|
426
|
+
|
|
427
|
+
// Register TUI command palette entries for commands with richer TUI-native UI.
|
|
428
|
+
api.command.register(() => [
|
|
429
|
+
{
|
|
430
|
+
title: "Magic Context: Status",
|
|
431
|
+
value: "magic-context.status",
|
|
432
|
+
category: "Magic Context",
|
|
433
|
+
slash: { name: "ctx-status" },
|
|
434
|
+
onSelect() {
|
|
435
|
+
showStatusDialog(api)
|
|
436
|
+
},
|
|
437
|
+
},
|
|
438
|
+
])
|
|
439
|
+
|
|
440
|
+
// Clean up on dispose
|
|
441
|
+
api.lifecycle.onDispose(() => {
|
|
442
|
+
closeDb()
|
|
443
|
+
})
|
|
444
|
+
|
|
445
|
+
const directory = api.state.path.directory ?? ""
|
|
446
|
+
const conflictResult = detectConflicts(directory)
|
|
447
|
+
if (conflictResult.hasConflict) {
|
|
448
|
+
showConflictDialog(api, directory, conflictResult.reasons, conflictResult.conflicts)
|
|
449
|
+
return
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
// Note: tui.json detection moved to server plugin (src/index.ts) since
|
|
453
|
+
// if tui.json doesn't have our plugin, this TUI code never loads at all.
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
const id = "opencode-magic-context"
|
|
457
|
+
|
|
458
|
+
export default {
|
|
459
|
+
id,
|
|
460
|
+
tui,
|
|
461
|
+
}
|