@cortexkit/opencode-magic-context 0.21.8 → 0.22.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 +124 -323
- package/dist/agents/magic-context-prompt.d.ts.map +1 -1
- package/dist/agents/permissions.d.ts +29 -14
- package/dist/agents/permissions.d.ts.map +1 -1
- package/dist/config/index.d.ts.map +1 -1
- package/dist/config/migrate-experimental.d.ts +29 -0
- package/dist/config/migrate-experimental.d.ts.map +1 -0
- package/dist/config/schema/agent-overrides.d.ts.map +1 -1
- package/dist/config/schema/magic-context.d.ts +95 -104
- package/dist/config/schema/magic-context.d.ts.map +1 -1
- package/dist/features/builtin-commands/commands.d.ts.map +1 -1
- package/dist/features/magic-context/compartment-embedding.d.ts +34 -0
- package/dist/features/magic-context/compartment-embedding.d.ts.map +1 -0
- package/dist/features/magic-context/compartment-events.d.ts +50 -0
- package/dist/features/magic-context/compartment-events.d.ts.map +1 -0
- package/dist/features/magic-context/compartment-storage.d.ts +22 -0
- package/dist/features/magic-context/compartment-storage.d.ts.map +1 -1
- package/dist/features/magic-context/dreamer/lease.d.ts.map +1 -1
- package/dist/features/magic-context/dreamer/queue.d.ts +13 -2
- package/dist/features/magic-context/dreamer/queue.d.ts.map +1 -1
- package/dist/features/magic-context/dreamer/runner.d.ts +11 -0
- package/dist/features/magic-context/dreamer/runner.d.ts.map +1 -1
- package/dist/features/magic-context/dreamer/task-prompts.d.ts +1 -1
- package/dist/features/magic-context/dreamer/task-prompts.d.ts.map +1 -1
- package/dist/features/magic-context/git-commits/git-log-reader.d.ts.map +1 -1
- package/dist/features/magic-context/key-files/identify-key-files.d.ts +1 -1
- package/dist/features/magic-context/key-files/identify-key-files.d.ts.map +1 -1
- package/dist/features/magic-context/key-files/project-key-files.d.ts.map +1 -1
- package/dist/features/magic-context/key-files/read-stats.d.ts +1 -1
- package/dist/features/magic-context/key-files/read-stats.d.ts.map +1 -1
- package/dist/features/magic-context/memory/constants.d.ts +4 -0
- package/dist/features/magic-context/memory/constants.d.ts.map +1 -1
- package/dist/features/magic-context/memory/embedding-identity.d.ts.map +1 -1
- package/dist/features/magic-context/memory/embedding-local.d.ts.map +1 -1
- package/dist/features/magic-context/memory/embedding-openai.d.ts +6 -0
- package/dist/features/magic-context/memory/embedding-openai.d.ts.map +1 -1
- package/dist/features/magic-context/memory/embedding-probe.d.ts +5 -0
- package/dist/features/magic-context/memory/embedding-probe.d.ts.map +1 -1
- package/dist/features/magic-context/memory/embedding.d.ts.map +1 -1
- package/dist/features/magic-context/memory/index.d.ts +1 -1
- package/dist/features/magic-context/memory/index.d.ts.map +1 -1
- package/dist/features/magic-context/memory/memory-migration.d.ts +133 -0
- package/dist/features/magic-context/memory/memory-migration.d.ts.map +1 -0
- package/dist/features/magic-context/memory/project-identity.d.ts +38 -7
- package/dist/features/magic-context/memory/project-identity.d.ts.map +1 -1
- package/dist/features/magic-context/memory/storage-memory-fts.d.ts.map +1 -1
- package/dist/features/magic-context/memory/storage-memory.d.ts +15 -1
- package/dist/features/magic-context/memory/storage-memory.d.ts.map +1 -1
- package/dist/features/magic-context/memory/types.d.ts +3 -1
- package/dist/features/magic-context/memory/types.d.ts.map +1 -1
- package/dist/features/magic-context/message-index.d.ts.map +1 -1
- package/dist/features/magic-context/migrations.d.ts +7 -0
- package/dist/features/magic-context/migrations.d.ts.map +1 -1
- package/dist/features/magic-context/project-docs-hash.d.ts +6 -0
- package/dist/features/magic-context/project-docs-hash.d.ts.map +1 -0
- package/dist/features/magic-context/project-identity.d.ts +2 -0
- package/dist/features/magic-context/project-identity.d.ts.map +1 -0
- package/dist/features/magic-context/storage-db.d.ts +51 -7
- package/dist/features/magic-context/storage-db.d.ts.map +1 -1
- package/dist/features/magic-context/storage-historian-runs.d.ts +73 -0
- package/dist/features/magic-context/storage-historian-runs.d.ts.map +1 -0
- package/dist/features/magic-context/storage-identity-rekey-map.d.ts +11 -0
- package/dist/features/magic-context/storage-identity-rekey-map.d.ts.map +1 -0
- package/dist/features/magic-context/storage-m0-mutation-log.d.ts +22 -0
- package/dist/features/magic-context/storage-m0-mutation-log.d.ts.map +1 -0
- package/dist/features/magic-context/storage-memory-mutation-log.d.ts +25 -0
- package/dist/features/magic-context/storage-memory-mutation-log.d.ts.map +1 -0
- package/dist/features/magic-context/storage-meta-persisted.d.ts.map +1 -1
- package/dist/features/magic-context/storage-meta-session.d.ts.map +1 -1
- package/dist/features/magic-context/storage-meta-shared.d.ts +44 -0
- package/dist/features/magic-context/storage-meta-shared.d.ts.map +1 -1
- package/dist/features/magic-context/storage-meta.d.ts +1 -0
- package/dist/features/magic-context/storage-meta.d.ts.map +1 -1
- package/dist/features/magic-context/storage-project-state.d.ts +19 -0
- package/dist/features/magic-context/storage-project-state.d.ts.map +1 -0
- package/dist/features/magic-context/storage-subagent-invocations.d.ts +9 -0
- package/dist/features/magic-context/storage-subagent-invocations.d.ts.map +1 -1
- package/dist/features/magic-context/storage-tags.d.ts +21 -1
- package/dist/features/magic-context/storage-tags.d.ts.map +1 -1
- package/dist/features/magic-context/storage-v22-backfill-failures.d.ts +24 -0
- package/dist/features/magic-context/storage-v22-backfill-failures.d.ts.map +1 -0
- package/dist/features/magic-context/storage.d.ts +12 -3
- package/dist/features/magic-context/storage.d.ts.map +1 -1
- package/dist/features/magic-context/subagent-token-capture.d.ts +1 -1
- package/dist/features/magic-context/subagent-token-capture.d.ts.map +1 -1
- package/dist/features/magic-context/tagger.d.ts +15 -1
- package/dist/features/magic-context/tagger.d.ts.map +1 -1
- package/dist/features/magic-context/types.d.ts +21 -0
- package/dist/features/magic-context/types.d.ts.map +1 -1
- package/dist/features/magic-context/user-memory/review-user-memories.d.ts.map +1 -1
- package/dist/features/magic-context/user-memory/storage-user-memory.d.ts.map +1 -1
- package/dist/features/magic-context/v22-deferred-backfill.d.ts +46 -0
- package/dist/features/magic-context/v22-deferred-backfill.d.ts.map +1 -0
- package/dist/features/magic-context/work-metrics.d.ts +66 -0
- package/dist/features/magic-context/work-metrics.d.ts.map +1 -1
- package/dist/hooks/auto-update-checker/cache.d.ts.map +1 -1
- package/dist/hooks/auto-update-checker/checker.d.ts.map +1 -1
- package/dist/hooks/magic-context/cache-busting-signals.d.ts +9 -0
- package/dist/hooks/magic-context/cache-busting-signals.d.ts.map +1 -1
- package/dist/hooks/magic-context/command-handler.d.ts +13 -1
- package/dist/hooks/magic-context/command-handler.d.ts.map +1 -1
- package/dist/hooks/magic-context/compartment-parser.d.ts +25 -0
- package/dist/hooks/magic-context/compartment-parser.d.ts.map +1 -1
- package/dist/hooks/magic-context/compartment-prompt.d.ts +27 -16
- package/dist/hooks/magic-context/compartment-prompt.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-mapping.d.ts +6 -2
- package/dist/hooks/magic-context/compartment-runner-mapping.d.ts.map +1 -1
- package/dist/hooks/magic-context/compartment-runner-partial-recomp.d.ts.map +1 -1
- package/dist/hooks/magic-context/compartment-runner-recomp.d.ts +9 -1
- package/dist/hooks/magic-context/compartment-runner-recomp.d.ts.map +1 -1
- package/dist/hooks/magic-context/compartment-runner-types.d.ts +67 -4
- package/dist/hooks/magic-context/compartment-runner-types.d.ts.map +1 -1
- package/dist/hooks/magic-context/compartment-runner-validation.d.ts.map +1 -1
- package/dist/hooks/magic-context/compartment-runner.d.ts.map +1 -1
- package/dist/hooks/magic-context/decay-curve.d.ts +78 -0
- package/dist/hooks/magic-context/decay-curve.d.ts.map +1 -0
- package/dist/hooks/magic-context/decay-render.d.ts +67 -0
- package/dist/hooks/magic-context/decay-render.d.ts.map +1 -0
- package/dist/hooks/magic-context/event-handler.d.ts +1 -1
- package/dist/hooks/magic-context/event-handler.d.ts.map +1 -1
- package/dist/hooks/magic-context/event-resolvers.d.ts +17 -0
- package/dist/hooks/magic-context/event-resolvers.d.ts.map +1 -1
- package/dist/hooks/magic-context/execute-status.d.ts.map +1 -1
- package/dist/hooks/magic-context/historian-prompt.generated.d.ts +2 -0
- package/dist/hooks/magic-context/historian-prompt.generated.d.ts.map +1 -0
- package/dist/hooks/magic-context/hook-handlers.d.ts +3 -0
- package/dist/hooks/magic-context/hook-handlers.d.ts.map +1 -1
- package/dist/hooks/magic-context/hook.d.ts +9 -21
- package/dist/hooks/magic-context/hook.d.ts.map +1 -1
- package/dist/hooks/magic-context/inject-compartments.d.ts +126 -0
- package/dist/hooks/magic-context/inject-compartments.d.ts.map +1 -1
- package/dist/hooks/magic-context/key-files-block.d.ts.map +1 -1
- package/dist/hooks/magic-context/live-session-state.d.ts +9 -0
- package/dist/hooks/magic-context/live-session-state.d.ts.map +1 -1
- package/dist/hooks/magic-context/m0-token-breakdown.d.ts +35 -0
- package/dist/hooks/magic-context/m0-token-breakdown.d.ts.map +1 -0
- package/dist/hooks/magic-context/read-session-chunk.d.ts +9 -0
- package/dist/hooks/magic-context/read-session-chunk.d.ts.map +1 -1
- package/dist/hooks/magic-context/read-session-db.d.ts +7 -0
- package/dist/hooks/magic-context/read-session-db.d.ts.map +1 -1
- package/dist/hooks/magic-context/recomp-orchestrator.d.ts +104 -0
- package/dist/hooks/magic-context/recomp-orchestrator.d.ts.map +1 -0
- package/dist/hooks/magic-context/reference-retrieval.d.ts +61 -0
- package/dist/hooks/magic-context/reference-retrieval.d.ts.map +1 -0
- package/dist/hooks/magic-context/reference-seeds.generated.d.ts +8 -0
- package/dist/hooks/magic-context/reference-seeds.generated.d.ts.map +1 -0
- package/dist/hooks/magic-context/send-session-notification.d.ts +1 -1
- package/dist/hooks/magic-context/send-session-notification.d.ts.map +1 -1
- package/dist/hooks/magic-context/system-prompt-hash.d.ts +5 -6
- package/dist/hooks/magic-context/system-prompt-hash.d.ts.map +1 -1
- package/dist/hooks/magic-context/tag-messages.d.ts.map +1 -1
- package/dist/hooks/magic-context/tokenizer-calibration.d.ts +6 -0
- package/dist/hooks/magic-context/tokenizer-calibration.d.ts.map +1 -1
- package/dist/hooks/magic-context/transform-compartment-phase.d.ts +0 -7
- package/dist/hooks/magic-context/transform-compartment-phase.d.ts.map +1 -1
- package/dist/hooks/magic-context/transform-postprocess-phase.d.ts +18 -0
- package/dist/hooks/magic-context/transform-postprocess-phase.d.ts.map +1 -1
- package/dist/hooks/magic-context/transform.d.ts +9 -7
- package/dist/hooks/magic-context/transform.d.ts.map +1 -1
- package/dist/hooks/magic-context/upgrade-reminder.d.ts +73 -0
- package/dist/hooks/magic-context/upgrade-reminder.d.ts.map +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +9435 -4001
- package/dist/plugin/conflict-warning-hook.d.ts +13 -0
- package/dist/plugin/conflict-warning-hook.d.ts.map +1 -1
- package/dist/plugin/dream-timer.d.ts.map +1 -1
- package/dist/plugin/event.d.ts +10 -0
- package/dist/plugin/event.d.ts.map +1 -1
- package/dist/plugin/hooks/create-session-hooks.d.ts.map +1 -1
- package/dist/plugin/messages-transform.d.ts.map +1 -1
- package/dist/plugin/rpc-handlers.d.ts.map +1 -1
- package/dist/plugin/tool-registry.d.ts.map +1 -1
- package/dist/shared/announcement.d.ts +17 -1
- package/dist/shared/announcement.d.ts.map +1 -1
- package/dist/shared/models-dev-cache.d.ts.map +1 -1
- package/dist/shared/rpc-client.d.ts +1 -0
- package/dist/shared/rpc-client.d.ts.map +1 -1
- package/dist/shared/rpc-notifications.d.ts +27 -5
- package/dist/shared/rpc-notifications.d.ts.map +1 -1
- package/dist/shared/rpc-server.d.ts +1 -0
- package/dist/shared/rpc-server.d.ts.map +1 -1
- package/dist/shared/rpc-types.d.ts +30 -2
- package/dist/shared/rpc-types.d.ts.map +1 -1
- package/dist/shared/rpc-utils.d.ts +9 -0
- package/dist/shared/rpc-utils.d.ts.map +1 -1
- package/dist/shared/sqlite-helpers.d.ts +7 -7
- package/dist/shared/sqlite.d.ts +23 -14
- package/dist/shared/sqlite.d.ts.map +1 -1
- package/dist/shared/tag-transcript.d.ts +10 -1
- package/dist/shared/tag-transcript.d.ts.map +1 -1
- package/dist/tools/ctx-expand/tools.d.ts +5 -1
- package/dist/tools/ctx-expand/tools.d.ts.map +1 -1
- package/dist/tools/ctx-memory/tools.d.ts.map +1 -1
- package/dist/tui/data/context-db.d.ts +16 -1
- package/dist/tui/data/context-db.d.ts.map +1 -1
- package/package.json +5 -7
- package/src/shared/announcement.test.ts +23 -7
- package/src/shared/announcement.ts +30 -8
- package/src/shared/conflict-detector.test.ts +15 -2
- package/src/shared/conflict-fixer.test.ts +5 -1
- package/src/shared/models-dev-cache.test.ts +72 -4
- package/src/shared/models-dev-cache.ts +47 -8
- package/src/shared/opencode-compaction-detector.test.ts +10 -2
- package/src/shared/rpc-client.test.ts +54 -3
- package/src/shared/rpc-client.ts +19 -9
- package/src/shared/rpc-notifications.test.ts +54 -1
- package/src/shared/rpc-notifications.ts +82 -13
- package/src/shared/rpc-server.ts +33 -4
- package/src/shared/rpc-types.ts +30 -2
- package/src/shared/rpc-utils.ts +10 -0
- package/src/shared/sqlite-helpers.ts +9 -9
- package/src/shared/sqlite.ts +99 -80
- package/src/shared/tag-transcript.test.ts +280 -0
- package/src/shared/tag-transcript.ts +162 -33
- package/src/tui/data/context-db.ts +75 -11
- package/src/tui/index.tsx +223 -32
- package/src/tui/slots/sidebar-content.tsx +366 -34
- package/dist/hooks/magic-context/compartment-runner-compressor.d.ts +0 -87
- package/dist/hooks/magic-context/compartment-runner-compressor.d.ts.map +0 -1
- package/dist/shared/native-binding.d.ts +0 -87
- package/dist/shared/native-binding.d.ts.map +0 -1
- package/src/shared/native-binding.ts +0 -311
|
@@ -10,7 +10,8 @@ import type { RpcNotificationMessage, SidebarSnapshot, StatusDetail } from "../.
|
|
|
10
10
|
export type { SidebarSnapshot, StatusDetail };
|
|
11
11
|
|
|
12
12
|
let rpcClient: MagicContextRpcClient | null = null;
|
|
13
|
-
let
|
|
13
|
+
let rpcGeneration = 0;
|
|
14
|
+
const lastReceivedNotificationIdBySession = new Map<string, number>();
|
|
14
15
|
|
|
15
16
|
function getStorageDir(): string {
|
|
16
17
|
// Plugin v0.16+ uses the shared cortexkit/magic-context path so OpenCode
|
|
@@ -24,14 +25,25 @@ function getStorageDir(): string {
|
|
|
24
25
|
/** Initialize the RPC client. Call once on TUI startup. */
|
|
25
26
|
export function initRpcClient(directory: string): void {
|
|
26
27
|
const storageDir = getStorageDir();
|
|
28
|
+
// Bump the generation before replacing the client so late notification
|
|
29
|
+
// responses from a disposed client cannot repopulate cleared cursors.
|
|
30
|
+
rpcGeneration += 1;
|
|
31
|
+
lastReceivedNotificationIdBySession.clear();
|
|
27
32
|
rpcClient = new MagicContextRpcClient(storageDir, directory);
|
|
28
33
|
}
|
|
29
34
|
|
|
35
|
+
export function getRpcGeneration(): number {
|
|
36
|
+
return rpcGeneration;
|
|
37
|
+
}
|
|
38
|
+
|
|
30
39
|
/** Clean up the RPC client. */
|
|
31
40
|
export function closeRpc(): void {
|
|
41
|
+
// Closing invalidates any already-issued RPC calls; their callbacks must
|
|
42
|
+
// observe the new generation and avoid advancing stale notification cursors.
|
|
43
|
+
rpcGeneration += 1;
|
|
32
44
|
rpcClient?.reset();
|
|
33
45
|
rpcClient = null;
|
|
34
|
-
|
|
46
|
+
lastReceivedNotificationIdBySession.clear();
|
|
35
47
|
}
|
|
36
48
|
|
|
37
49
|
const EMPTY_SNAPSHOT: SidebarSnapshot = {
|
|
@@ -55,6 +67,8 @@ const EMPTY_SNAPSHOT: SidebarSnapshot = {
|
|
|
55
67
|
compartmentTokens: 0,
|
|
56
68
|
factTokens: 0,
|
|
57
69
|
memoryTokens: 0,
|
|
70
|
+
docsTokens: 0,
|
|
71
|
+
profileTokens: 0,
|
|
58
72
|
conversationTokens: 0,
|
|
59
73
|
toolCallTokens: 0,
|
|
60
74
|
toolDefinitionTokens: 0,
|
|
@@ -229,7 +243,35 @@ export async function requestRecomp(sessionId: string): Promise<boolean> {
|
|
|
229
243
|
}
|
|
230
244
|
}
|
|
231
245
|
|
|
246
|
+
/** Run `/ctx-session-upgrade` for the session (full recomp + once-per-project
|
|
247
|
+
* memory migration). Fired from the upgrade dialog's "Run upgrade now" action. */
|
|
248
|
+
export async function requestUpgrade(sessionId: string): Promise<boolean> {
|
|
249
|
+
if (!rpcClient) return false;
|
|
250
|
+
try {
|
|
251
|
+
const result = await rpcClient.call<{ ok: boolean }>("upgrade", { sessionId });
|
|
252
|
+
return result.ok ?? false;
|
|
253
|
+
} catch {
|
|
254
|
+
return false;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/** Mark the upgrade reminder dismissed (the user made an explicit Confirm/Cancel
|
|
259
|
+
* choice), setting the durable stamp so the FRESH dialog won't re-show. Resume
|
|
260
|
+
* prompts are staging-driven and unaffected. */
|
|
261
|
+
export async function dismissUpgradeReminder(sessionId: string): Promise<boolean> {
|
|
262
|
+
if (!rpcClient) return false;
|
|
263
|
+
try {
|
|
264
|
+
const result = await rpcClient.call<{ ok: boolean }>("dismiss-upgrade-reminder", {
|
|
265
|
+
sessionId,
|
|
266
|
+
});
|
|
267
|
+
return result.ok ?? false;
|
|
268
|
+
} catch {
|
|
269
|
+
return false;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
232
273
|
export interface TuiMessage {
|
|
274
|
+
id: number;
|
|
233
275
|
type: string;
|
|
234
276
|
payload: Record<string, unknown>;
|
|
235
277
|
sessionId?: string;
|
|
@@ -279,20 +321,24 @@ export async function markAnnounced(): Promise<boolean> {
|
|
|
279
321
|
}
|
|
280
322
|
|
|
281
323
|
/** Poll for pending server→TUI notifications via RPC. */
|
|
282
|
-
export async function consumeTuiMessages(): Promise<TuiMessage[]> {
|
|
324
|
+
export async function consumeTuiMessages(sessionId: string): Promise<TuiMessage[]> {
|
|
283
325
|
if (!rpcClient) return [];
|
|
284
326
|
try {
|
|
285
327
|
const result = await rpcClient.call<{ messages: RpcNotificationMessage[] }>(
|
|
286
328
|
"pending-notifications",
|
|
287
|
-
|
|
329
|
+
// Pass the TUI's active session so the server only drains
|
|
330
|
+
// notifications scoped to it (or global ones). Without this, a
|
|
331
|
+
// notification for another session served by the same process (e.g.
|
|
332
|
+
// OpenCode Desktop on the same project) could surface here. The
|
|
333
|
+
// cursor is per-session and is advanced by the poller only after it
|
|
334
|
+
// has delivered the returned batch.
|
|
335
|
+
{
|
|
336
|
+
lastReceivedId: lastReceivedNotificationIdBySession.get(sessionId) ?? 0,
|
|
337
|
+
sessionId,
|
|
338
|
+
},
|
|
288
339
|
);
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
if (message.id > lastReceivedNotificationId) {
|
|
292
|
-
lastReceivedNotificationId = message.id;
|
|
293
|
-
}
|
|
294
|
-
}
|
|
295
|
-
return messages.map((m) => ({
|
|
340
|
+
return (result.messages ?? []).map((m) => ({
|
|
341
|
+
id: m.id,
|
|
296
342
|
type: m.type,
|
|
297
343
|
payload: m.payload,
|
|
298
344
|
sessionId: m.sessionId,
|
|
@@ -301,3 +347,21 @@ export async function consumeTuiMessages(): Promise<TuiMessage[]> {
|
|
|
301
347
|
return [];
|
|
302
348
|
}
|
|
303
349
|
}
|
|
350
|
+
|
|
351
|
+
/**
|
|
352
|
+
* Advance the delivered-message cursor for one active TUI session.
|
|
353
|
+
* Callers must pass only the contiguous handled prefix of the drained batch;
|
|
354
|
+
* this helper remains empty-safe and monotonic for that prefix.
|
|
355
|
+
*/
|
|
356
|
+
export function markTuiMessagesHandled(sessionId: string, messages: TuiMessage[]): void {
|
|
357
|
+
const previous = lastReceivedNotificationIdBySession.get(sessionId) ?? 0;
|
|
358
|
+
let next = previous;
|
|
359
|
+
for (const message of messages) {
|
|
360
|
+
if (message.id > next) {
|
|
361
|
+
next = message.id;
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
if (next > previous) {
|
|
365
|
+
lastReceivedNotificationIdBySession.set(sessionId, next);
|
|
366
|
+
}
|
|
367
|
+
}
|
package/src/tui/index.tsx
CHANGED
|
@@ -4,9 +4,9 @@ import { existsSync, mkdirSync, writeFileSync } from "node:fs"
|
|
|
4
4
|
import { dirname, join } from "node:path"
|
|
5
5
|
import { createMemo } from "solid-js"
|
|
6
6
|
import type { TuiPlugin, TuiPluginApi, TuiThemeCurrent } from "@opencode-ai/plugin/tui"
|
|
7
|
-
import { createSidebarContentSlot } from "./slots/sidebar-content"
|
|
7
|
+
import { createSidebarContentSlot, kickRecompProgressRefresh } from "./slots/sidebar-content"
|
|
8
8
|
import packageJson from "../../package.json"
|
|
9
|
-
import { closeRpc, consumeTuiMessages, getAnnouncement, getCompartmentCount, initRpcClient, loadStatusDetail, markAnnounced, requestRecomp, type StatusDetail } from "./data/context-db"
|
|
9
|
+
import { closeRpc, consumeTuiMessages, dismissUpgradeReminder, getAnnouncement, getCompartmentCount, getRpcGeneration, initRpcClient, loadStatusDetail, markAnnounced, markTuiMessagesHandled, requestRecomp, requestUpgrade, type TuiMessage, type StatusDetail } from "./data/context-db"
|
|
10
10
|
import { formatThresholdPercent } from "../shared/format-threshold"
|
|
11
11
|
import { detectConflicts } from "../shared/conflict-detector"
|
|
12
12
|
import { fixConflicts } from "../shared/conflict-fixer"
|
|
@@ -213,9 +213,11 @@ const StatusDialog = (props: { api: TuiPluginApi; s: StatusDetail }) => {
|
|
|
213
213
|
const COLORS = {
|
|
214
214
|
// Cool / structured — injected by the plugin into message[0]
|
|
215
215
|
system: "#c084fc",
|
|
216
|
+
docs: "#22d3ee",
|
|
216
217
|
compartments: "#60a5fa",
|
|
217
218
|
facts: "#fbbf24",
|
|
218
219
|
memories: "#34d399",
|
|
220
|
+
profile: "#a3e635",
|
|
219
221
|
// Warm / user-facing — chat and tool traffic
|
|
220
222
|
conversation: "#f87171",
|
|
221
223
|
toolCalls: "#fb923c",
|
|
@@ -229,6 +231,8 @@ const StatusDialog = (props: { api: TuiPluginApi; s: StatusDetail }) => {
|
|
|
229
231
|
|
|
230
232
|
if (d.systemPromptTokens > 0)
|
|
231
233
|
segs.push({ label: "System", tokens: d.systemPromptTokens, color: COLORS.system })
|
|
234
|
+
if (d.docsTokens > 0)
|
|
235
|
+
segs.push({ label: "Docs", tokens: d.docsTokens, color: COLORS.docs })
|
|
232
236
|
if (d.compartmentTokens > 0)
|
|
233
237
|
segs.push({
|
|
234
238
|
label: "Compartments",
|
|
@@ -250,6 +254,8 @@ const StatusDialog = (props: { api: TuiPluginApi; s: StatusDetail }) => {
|
|
|
250
254
|
color: COLORS.memories,
|
|
251
255
|
detail: `(${d.memoryBlockCount})`,
|
|
252
256
|
})
|
|
257
|
+
if (d.profileTokens > 0)
|
|
258
|
+
segs.push({ label: "User Profile", tokens: d.profileTokens, color: COLORS.profile })
|
|
253
259
|
|
|
254
260
|
if (d.conversationTokens > 0)
|
|
255
261
|
segs.push({ label: "Conversation", tokens: d.conversationTokens, color: COLORS.conversation })
|
|
@@ -316,6 +322,35 @@ const StatusDialog = (props: { api: TuiPluginApi; s: StatusDetail }) => {
|
|
|
316
322
|
})}
|
|
317
323
|
</box>
|
|
318
324
|
|
|
325
|
+
{/* Recomp / session-upgrade live progress (full width, only while
|
|
326
|
+
running or just finished — dogfood 2026-05-30). */}
|
|
327
|
+
{s().recompProgress && (
|
|
328
|
+
<box marginTop={1} width="100%" flexDirection="column">
|
|
329
|
+
<text fg={t().text}><b>Recomp / Upgrade</b></text>
|
|
330
|
+
{(() => {
|
|
331
|
+
const p = s().recompProgress!
|
|
332
|
+
if (p.phase === "recomp") {
|
|
333
|
+
const frac = p.totalMessages > 0 ? p.processedMessages / p.totalMessages : 0
|
|
334
|
+
const width = 24
|
|
335
|
+
const filled = Math.round(Math.max(0, Math.min(1, frac)) * width)
|
|
336
|
+
const bar = p.totalMessages > 0
|
|
337
|
+
? `[${"█".repeat(filled)}${"░".repeat(width - filled)}]`
|
|
338
|
+
: "(starting…)"
|
|
339
|
+
return (
|
|
340
|
+
<>
|
|
341
|
+
<R t={t()} l="upgrading" v={p.totalMessages > 0 ? `${bar} ${Math.round(frac * 100)}%` : bar} fg={t().warning} />
|
|
342
|
+
{p.note ? <R t={t()} l="Status" v={p.note} fg={t().textMuted} /> : null}
|
|
343
|
+
<R t={t()} l="Compartments" v={`${p.compartmentsCreated} (${p.passCount} pass${p.passCount === 1 ? "" : "es"})`} fg={t().textMuted} />
|
|
344
|
+
</>
|
|
345
|
+
)
|
|
346
|
+
}
|
|
347
|
+
if (p.phase === "migration") return <R t={t()} l="Status" v={p.note ?? "Migrating memories ⟳"} fg={t().warning} />
|
|
348
|
+
if (p.phase === "done") return <R t={t()} l="Status" v="✓ Upgrade complete" fg={t().accent} />
|
|
349
|
+
return <R t={t()} l="Status" v={`✗ Failed${p.message ? `: ${p.message}` : ""}`} fg={t().error} />
|
|
350
|
+
})()}
|
|
351
|
+
</box>
|
|
352
|
+
)}
|
|
353
|
+
|
|
319
354
|
{/* 2-column layout */}
|
|
320
355
|
<box flexDirection="row" width="100%" marginTop={1} gap={4}>
|
|
321
356
|
{/* Left column */}
|
|
@@ -406,49 +441,139 @@ function getModelKeyFromMessages(api: TuiPluginApi, sessionId: string): string |
|
|
|
406
441
|
return undefined
|
|
407
442
|
}
|
|
408
443
|
|
|
409
|
-
function showRecompDialog(api: TuiPluginApi) {
|
|
410
|
-
const sessionId =
|
|
444
|
+
async function showRecompDialog(api: TuiPluginApi, targetSessionId = getSessionId(api)): Promise<boolean> {
|
|
445
|
+
const sessionId = targetSessionId
|
|
411
446
|
if (!sessionId) {
|
|
412
447
|
api.ui.toast({ message: "No active session", variant: "warning" })
|
|
413
|
-
return
|
|
448
|
+
return false
|
|
414
449
|
}
|
|
415
450
|
|
|
416
|
-
|
|
417
|
-
|
|
451
|
+
const count = await getCompartmentCount(sessionId)
|
|
452
|
+
// Ack only after the dialog is actually shown for the same active session;
|
|
453
|
+
// route switches while the RPC detail load is in flight must leave it pending.
|
|
454
|
+
if (getSessionId(api) !== sessionId) return false
|
|
455
|
+
|
|
456
|
+
api.ui.dialog.replace(() => (
|
|
457
|
+
<api.ui.DialogConfirm
|
|
458
|
+
title="⚠️ Recomp Confirmation"
|
|
459
|
+
message={[
|
|
460
|
+
`You have ${count} compartments.`,
|
|
461
|
+
"",
|
|
462
|
+
"Recomp will regenerate all compartments and facts from raw history.",
|
|
463
|
+
"This may take a long time and consume significant tokens.",
|
|
464
|
+
"",
|
|
465
|
+
"Proceed?",
|
|
466
|
+
].join("\n")}
|
|
467
|
+
onConfirm={() => {
|
|
468
|
+
void requestRecomp(sessionId)
|
|
469
|
+
kickRecompProgressRefresh()
|
|
470
|
+
api.ui.toast({ message: "Recomp requested — historian will start shortly", variant: "info", duration: 5000 })
|
|
471
|
+
}}
|
|
472
|
+
onCancel={() => {
|
|
473
|
+
api.ui.toast({ message: "Recomp cancelled", variant: "info", duration: 3000 })
|
|
474
|
+
}}
|
|
475
|
+
/>
|
|
476
|
+
))
|
|
477
|
+
return true
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
function showUpgradeDialog(
|
|
481
|
+
api: TuiPluginApi,
|
|
482
|
+
resume?: { stagedCount: number; stagedThrough: number },
|
|
483
|
+
targetSessionId = getSessionId(api),
|
|
484
|
+
): boolean {
|
|
485
|
+
const sessionId = targetSessionId
|
|
486
|
+
if (!sessionId) {
|
|
487
|
+
// No active session — nothing to upgrade. Silently skip (the server only
|
|
488
|
+
// enqueues this for sessions with legacy compartments, but the TUI may
|
|
489
|
+
// have switched sessions before the poller fired).
|
|
490
|
+
return false
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
if (getSessionId(api) !== sessionId) return false
|
|
494
|
+
|
|
495
|
+
const title = resume ? "🎆 Resume the interrupted upgrade?" : "🎆 Historian V2 is released!"
|
|
496
|
+
const message = resume
|
|
497
|
+
? [
|
|
498
|
+
`An earlier upgrade to the new historian format was interrupted. ${resume.stagedCount} compartment${resume.stagedCount === 1 ? " was" : "s were"} already rebuilt (through message ${resume.stagedThrough}). Resuming continues from where it left off — nothing already rebuilt is reprocessed.`,
|
|
499
|
+
"",
|
|
500
|
+
"Resuming will:",
|
|
501
|
+
"• Rebuild the remaining compartments into the new layered format",
|
|
502
|
+
"• Re-organize this project's memories into the new taxonomy (once per project)",
|
|
503
|
+
"",
|
|
504
|
+
"The historian runs in the background and you can keep working. You can also resume via /ctx-session-upgrade later.",
|
|
505
|
+
"",
|
|
506
|
+
"Resume the upgrade now?",
|
|
507
|
+
].join("\n")
|
|
508
|
+
: [
|
|
509
|
+
"This session's compartments are written by the old historian. The session is still usable with its old compartments, however it's strongly advised to upgrade them to the new format. This means every compartment needs to be reprocessed by the new historian, which might take a while depending on how big your session is.",
|
|
510
|
+
"",
|
|
511
|
+
"Running the upgrade will:",
|
|
512
|
+
"• Rebuild this session's compartments into the new layered format",
|
|
513
|
+
"• Re-organize this project's memories into the new taxonomy (once per project)",
|
|
514
|
+
"",
|
|
515
|
+
"The historian runs in the background and you can keep working while older compartments are reprocessed. You can also upgrade via /ctx-session-upgrade later.",
|
|
516
|
+
"",
|
|
517
|
+
"Run the upgrade now?",
|
|
518
|
+
].join("\n")
|
|
519
|
+
|
|
520
|
+
api.ui.dialog.replace(
|
|
521
|
+
() => (
|
|
418
522
|
<api.ui.DialogConfirm
|
|
419
|
-
title=
|
|
420
|
-
message={
|
|
421
|
-
`You have ${count} compartments.`,
|
|
422
|
-
"",
|
|
423
|
-
"Recomp will regenerate all compartments and facts from raw history.",
|
|
424
|
-
"This may take a long time and consume significant tokens.",
|
|
425
|
-
"",
|
|
426
|
-
"Proceed?",
|
|
427
|
-
].join("\n")}
|
|
523
|
+
title={title}
|
|
524
|
+
message={message}
|
|
428
525
|
onConfirm={() => {
|
|
429
|
-
|
|
430
|
-
|
|
526
|
+
// Explicit choice → dismiss the fresh reminder durably so it
|
|
527
|
+
// won't re-show. (Resume prompts are staging-driven and still
|
|
528
|
+
// fire if this run is later interrupted.)
|
|
529
|
+
void dismissUpgradeReminder(sessionId)
|
|
530
|
+
void requestUpgrade(sessionId)
|
|
531
|
+
// Start the sidebar's recomp self-poll immediately — the RPC
|
|
532
|
+
// call fires no message event, so without this the progress
|
|
533
|
+
// bar wouldn't appear until the upgrade finished.
|
|
534
|
+
kickRecompProgressRefresh()
|
|
535
|
+
api.ui.toast({
|
|
536
|
+
message: resume
|
|
537
|
+
? "Resuming session upgrade — running in the background"
|
|
538
|
+
: "Session upgrade started — running in the background",
|
|
539
|
+
variant: "info",
|
|
540
|
+
duration: 5000,
|
|
541
|
+
})
|
|
431
542
|
}}
|
|
432
543
|
onCancel={() => {
|
|
433
|
-
|
|
544
|
+
// Explicit decline → set the durable stamp so we don't re-prompt
|
|
545
|
+
// on every restart. The fix for stamp-on-display trapping a
|
|
546
|
+
// never-upgraded session (dogfood 2026-05-30) relies on THIS
|
|
547
|
+
// being the only place the TUI path stamps.
|
|
548
|
+
void dismissUpgradeReminder(sessionId)
|
|
549
|
+
api.ui.toast({
|
|
550
|
+
message: "Upgrade skipped — run /ctx-session-upgrade anytime",
|
|
551
|
+
variant: "info",
|
|
552
|
+
duration: 4000,
|
|
553
|
+
})
|
|
434
554
|
}}
|
|
435
555
|
/>
|
|
436
|
-
)
|
|
437
|
-
|
|
556
|
+
),
|
|
557
|
+
)
|
|
558
|
+
return true
|
|
438
559
|
}
|
|
439
560
|
|
|
440
|
-
function showStatusDialog(api: TuiPluginApi) {
|
|
441
|
-
const sessionId =
|
|
561
|
+
async function showStatusDialog(api: TuiPluginApi, targetSessionId = getSessionId(api)): Promise<boolean> {
|
|
562
|
+
const sessionId = targetSessionId
|
|
442
563
|
if (!sessionId) {
|
|
443
564
|
api.ui.toast({ message: "No active session", variant: "warning" })
|
|
444
|
-
return
|
|
565
|
+
return false
|
|
445
566
|
}
|
|
446
567
|
|
|
447
568
|
const directory = api.state.path.directory ?? ""
|
|
448
569
|
const modelKey = getModelKeyFromMessages(api, sessionId)
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
570
|
+
const detail = await loadStatusDetail(sessionId, directory, modelKey)
|
|
571
|
+
// Ack only after the dialog is actually shown for the same active session;
|
|
572
|
+
// route switches while the RPC detail load is in flight must leave it pending.
|
|
573
|
+
if (getSessionId(api) !== sessionId) return false
|
|
574
|
+
|
|
575
|
+
api.ui.dialog.replace(() => <StatusDialog api={api} s={detail} />)
|
|
576
|
+
return true
|
|
452
577
|
}
|
|
453
578
|
|
|
454
579
|
/**
|
|
@@ -636,10 +761,46 @@ const tui: TuiPlugin = async (api, _options, meta) => {
|
|
|
636
761
|
registerCommandPaletteEntries(api)
|
|
637
762
|
|
|
638
763
|
// Poll for server→TUI messages: toasts and dialog requests.
|
|
639
|
-
//
|
|
764
|
+
// The poller owns cursor advancement so notifications are acked only after
|
|
765
|
+
// they are accepted for the still-active session and delivered to the UI.
|
|
766
|
+
let pollInFlight = false
|
|
640
767
|
const messagePoller = setInterval(() => {
|
|
641
|
-
|
|
642
|
-
|
|
768
|
+
// Scope the drain to the TUI's active session so notifications tagged for
|
|
769
|
+
// a different session (served by the same RPC process) are not consumed
|
|
770
|
+
// here. Do not poll on non-session routes: a session-scoped action fetched
|
|
771
|
+
// while sessionless could otherwise be acked without being shown.
|
|
772
|
+
// Avoid overlapping read-only drains: the server re-delivers until acked,
|
|
773
|
+
// so a second in-flight poll can fetch and dispatch the same batch twice.
|
|
774
|
+
if (pollInFlight) return
|
|
775
|
+
|
|
776
|
+
const requestedSessionId = getSessionId(api)
|
|
777
|
+
if (!requestedSessionId) return
|
|
778
|
+
|
|
779
|
+
pollInFlight = true
|
|
780
|
+
const pollGeneration = getRpcGeneration()
|
|
781
|
+
void consumeTuiMessages(requestedSessionId).then(async (messages) => {
|
|
782
|
+
// The dialog handlers read the current session when they run. If the
|
|
783
|
+
// user switched routes while the RPC was in flight, drop this whole
|
|
784
|
+
// batch without advancing the cursor; the next poll for the new
|
|
785
|
+
// session will fetch the right notifications.
|
|
786
|
+
// Ignore late responses from an older RPC client generation; close/init
|
|
787
|
+
// clears cursors and stale callbacks must not recreate them.
|
|
788
|
+
if (getRpcGeneration() !== pollGeneration) return
|
|
789
|
+
|
|
790
|
+
if (getSessionId(api) !== requestedSessionId) return
|
|
791
|
+
|
|
792
|
+
const orderedMessages = [...messages].sort((a, b) => a.id - b.id)
|
|
793
|
+
const handledMessageIds = new Set<number>()
|
|
794
|
+
for (const msg of orderedMessages) {
|
|
795
|
+
// Drop any action/dialog whose sessionId doesn't match this TUI's
|
|
796
|
+
// active session (session-less/global notifications still apply).
|
|
797
|
+
if (
|
|
798
|
+
msg.type === "action" &&
|
|
799
|
+
msg.sessionId &&
|
|
800
|
+
msg.sessionId !== requestedSessionId
|
|
801
|
+
) {
|
|
802
|
+
continue
|
|
803
|
+
}
|
|
643
804
|
if (msg.type === "toast") {
|
|
644
805
|
const p = msg.payload
|
|
645
806
|
api.ui.toast({
|
|
@@ -647,17 +808,47 @@ const tui: TuiPlugin = async (api, _options, meta) => {
|
|
|
647
808
|
variant: (p.variant as "info" | "warning" | "error" | "success") ?? "info",
|
|
648
809
|
duration: typeof p.duration === "number" ? p.duration : 5000,
|
|
649
810
|
})
|
|
811
|
+
handledMessageIds.add(msg.id)
|
|
650
812
|
} else if (msg.type === "action") {
|
|
651
813
|
const action = msg.payload?.action
|
|
652
814
|
if (action === "show-status-dialog") {
|
|
653
|
-
showStatusDialog(api)
|
|
815
|
+
if (await showStatusDialog(api, requestedSessionId)) {
|
|
816
|
+
handledMessageIds.add(msg.id)
|
|
817
|
+
}
|
|
654
818
|
} else if (action === "show-recomp-dialog") {
|
|
655
|
-
showRecompDialog(api)
|
|
819
|
+
if (await showRecompDialog(api, requestedSessionId)) {
|
|
820
|
+
handledMessageIds.add(msg.id)
|
|
821
|
+
}
|
|
822
|
+
} else if (action === "show-upgrade-dialog") {
|
|
823
|
+
const resume =
|
|
824
|
+
msg.payload?.resume === true
|
|
825
|
+
? {
|
|
826
|
+
stagedCount: Number(msg.payload?.stagedCount ?? 0),
|
|
827
|
+
stagedThrough: Number(msg.payload?.stagedThrough ?? 0),
|
|
828
|
+
}
|
|
829
|
+
: undefined
|
|
830
|
+
if (showUpgradeDialog(api, resume, requestedSessionId)) {
|
|
831
|
+
handledMessageIds.add(msg.id)
|
|
832
|
+
}
|
|
656
833
|
}
|
|
657
834
|
}
|
|
658
835
|
}
|
|
836
|
+
const handledPrefixMessages: TuiMessage[] = []
|
|
837
|
+
for (const msg of orderedMessages) {
|
|
838
|
+
if (!handledMessageIds.has(msg.id)) break
|
|
839
|
+
handledPrefixMessages.push(msg)
|
|
840
|
+
}
|
|
841
|
+
// A dialog helper may have awaited more RPC work; re-check before
|
|
842
|
+
// acking so a dispose/reinit or route switch during that await cannot
|
|
843
|
+
// advance a stale cursor.
|
|
844
|
+
if (getRpcGeneration() !== pollGeneration) return
|
|
845
|
+
if (getSessionId(api) !== requestedSessionId) return
|
|
846
|
+
|
|
847
|
+
markTuiMessagesHandled(requestedSessionId, handledPrefixMessages)
|
|
659
848
|
}).catch(() => {
|
|
660
849
|
// Intentional: message polling should never crash the TUI
|
|
850
|
+
}).finally(() => {
|
|
851
|
+
pollInFlight = false
|
|
661
852
|
})
|
|
662
853
|
}, 500)
|
|
663
854
|
|