Skip to content

Commit 7e1e48d

Browse files
committed
Add option for the notification interval
1 parent a2984d1 commit 7e1e48d

10 files changed

Lines changed: 110 additions & 37 deletions

File tree

CONFIGURATION.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,7 @@ Higher-tier models with longer cache windows benefit from a longer TTL. Setting
118118
| `cache_ttl` | `string` or `object` | `"5m"` | Time after a response before applying pending ops. String or per-model map. |
119119
| `protected_tags` | `number` (1–100) | `20` | Last N active tags immune from immediate dropping. |
120120
| `nudge_interval_tokens` | `number` | `10000` | Minimum token growth between rolling nudges. |
121+
| `toast_duration_ms` | `number` (1000–60000) | `5000` | TUI toast lifetime for Magic Context notifications in milliseconds. Increase this if toasts disappear too quickly. |
121122
| `execute_threshold_percentage` | `number` (20–80) or `object` | `65` | Context usage that forces queued ops to execute. Capped at 80% max for cache safety. Supports per-model map. |
122123
| `execute_threshold_tokens` | `object` (per-model map) || **Optional absolute-tokens variant of `execute_threshold_percentage`.** Per-model map (e.g. `{ "default": 150000, "github-copilot/gpt-5.2-codex": 40000 }`). When set for a model, overrides the percentage-based threshold for that model. Clamped to `80% × context_limit` with a warn log. Requires a resolvable context limit — falls through to percentage if unavailable. See below. |
123124
| `auto_drop_tool_age` | `number` | `100` | Auto-drop tool outputs older than N tags during execution. |
@@ -617,6 +618,7 @@ Tier boundaries are hardcoded to keep behavior predictable and prevent cache-bus
617618
"protected_tags": 10,
618619
"auto_drop_tool_age": 50,
619620
"drop_tool_structure": true,
621+
"toast_duration_ms": 12000,
620622
"history_budget_percentage": 0.15,
621623
"compaction_markers": true,
622624
"compressor": {

packages/plugin/src/config/schema/magic-context.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,8 @@ export interface MagicContextConfig {
190190
dreamer?: DreamerConfig;
191191
cache_ttl: string | { default: string; [modelKey: string]: string };
192192
nudge_interval_tokens: number;
193+
/** TUI toast lifetime in milliseconds for Magic Context notifications. Default: 5000. */
194+
toast_duration_ms?: number;
193195
execute_threshold_percentage: number | { default: number; [modelKey: string]: number };
194196
/** Absolute token thresholds per model. When set for a given model (or via `default`),
195197
* this overrides `execute_threshold_percentage` for that model. Useful for hard caps
@@ -310,6 +312,8 @@ export const MagicContextConfigSchema = z
310312
.default("5m"),
311313
/** Minimum token growth between low-priority rolling nudges (default: DEFAULT_NUDGE_INTERVAL_TOKENS) */
312314
nudge_interval_tokens: z.number().min(1000).default(DEFAULT_NUDGE_INTERVAL_TOKENS),
315+
/** TUI toast lifetime in milliseconds for Magic Context notifications (min: 1000, max: 60000, default: 5000) */
316+
toast_duration_ms: z.number().min(1_000).max(60_000).default(5_000),
313317
/** Context percentage that forces queued operations to execute. Number or per-model object ({ default: 65, "provider/model": 45 }). Values above 80 are rejected because the runtime caps at 80% for cache safety (MAX_EXECUTE_THRESHOLD). Default: DEFAULT_EXECUTE_THRESHOLD_PERCENTAGE */
314318
execute_threshold_percentage: z
315319
.union([

packages/plugin/src/hooks/magic-context/command-handler.ts

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -289,6 +289,8 @@ export function createMagicContextCommandHandler(deps: {
289289
text: string,
290290
params: NotificationParams,
291291
) => Promise<void>;
292+
/** Configured toast lifetime (ms) forwarded into diagnostics logs. */
293+
toastDurationMs?: number;
292294
sidekick?: {
293295
config: SidekickConfig;
294296
projectPath: string;
@@ -351,13 +353,25 @@ export function createMagicContextCommandHandler(deps: {
351353
deps.onFlush?.(sessionId);
352354
}
353355

354-
if (isStatus) {
355-
if (isTuiConnected()) {
356-
// In TUI, push an RPC action so the TUI poller shows a native dialog
357-
pushNotification("action", { action: "show-status-dialog" }, sessionId);
358-
sessionLog(sessionId, "command ctx-status: pushed show-status-dialog to TUI");
359-
throwSentinel(input.command);
360-
}
356+
if (isStatus) {
357+
if (isTuiConnected()) {
358+
// In TUI, push an RPC action so the TUI poller shows a native dialog
359+
pushNotification(
360+
"action",
361+
{
362+
action: "show-status-dialog",
363+
toast_duration_ms: deps.toastDurationMs ?? 5000,
364+
},
365+
sessionId,
366+
);
367+
sessionLog(
368+
sessionId,
369+
`command ctx-status: pushed show-status-dialog to TUI (toast_duration_ms=${String(
370+
deps.toastDurationMs ?? 5000,
371+
)})`,
372+
);
373+
throwSentinel(input.command);
374+
}
361375
const liveModelKey = deps.getLiveModelKey?.(sessionId);
362376
const liveContextLimit = deps.getContextLimit?.(sessionId);
363377
const statusOutput = executeStatus(

packages/plugin/src/hooks/magic-context/hook-handlers.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,11 +122,13 @@ export function getLiveNotificationParams(
122122
liveModelBySession: LiveModelBySession,
123123
variantBySession: VariantBySession,
124124
agentBySession?: AgentBySession,
125+
toastDurationMs?: number,
125126
): {
126127
agent?: string;
127128
variant?: string;
128129
providerId?: string;
129130
modelId?: string;
131+
toastDurationMs?: number;
130132
} {
131133
const model = liveModelBySession.get(sessionId);
132134
const variant = variantBySession.get(sessionId);
@@ -135,6 +137,7 @@ export function getLiveNotificationParams(
135137
...(agent ? { agent } : {}),
136138
...(variant ? { variant } : {}),
137139
...(model ? { providerId: model.providerID, modelId: model.modelID } : {}),
140+
...(typeof toastDurationMs === "number" ? { toastDurationMs } : {}),
138141
};
139142
}
140143

packages/plugin/src/hooks/magic-context/hook.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ export interface MagicContextDeps {
7474
protected_tags: number;
7575
ctx_reduce_enabled?: boolean;
7676
nudge_interval_tokens?: number;
77+
toast_duration_ms?: number;
7778
auto_drop_tool_age?: number;
7879
drop_tool_structure?: boolean;
7980
clear_reasoning_age?: number;
@@ -345,6 +346,7 @@ export function createMagicContextHook(deps: MagicContextDeps) {
345346
liveModelBySession,
346347
variantBySession,
347348
agentBySession,
349+
deps.config.toast_duration_ms,
348350
),
349351
getModelKey: (sessionId) => {
350352
const model = liveModelBySession.get(sessionId);
@@ -408,6 +410,7 @@ export function createMagicContextHook(deps: MagicContextDeps) {
408410
liveModelBySession,
409411
variantBySession,
410412
agentBySession,
413+
deps.config.toast_duration_ms,
411414
),
412415
nudgePlacements,
413416
onSessionCacheInvalidated: (sessionId: string) => {
@@ -470,6 +473,7 @@ export function createMagicContextHook(deps: MagicContextDeps) {
470473
const commandHandler = createMagicContextCommandHandler({
471474
db,
472475
protectedTags: deps.config.protected_tags,
476+
toastDurationMs: deps.config.toast_duration_ms,
473477
nudgeIntervalTokens: deps.config.nudge_interval_tokens ?? DEFAULT_NUDGE_INTERVAL_TOKENS,
474478
executeThresholdPercentage: deps.config.execute_threshold_percentage ?? 65,
475479
executeThresholdTokens: deps.config.execute_threshold_tokens,
@@ -521,6 +525,7 @@ export function createMagicContextHook(deps: MagicContextDeps) {
521525
liveModelBySession,
522526
variantBySession,
523527
agentBySession,
528+
deps.config.toast_duration_ms,
524529
),
525530
historianTwoPass: deps.config.historian?.two_pass === true,
526531
// Issue #44: respect memory feature gates from /ctx-recomp too.
@@ -554,6 +559,7 @@ export function createMagicContextHook(deps: MagicContextDeps) {
554559
liveModelBySession,
555560
variantBySession,
556561
agentBySession,
562+
deps.config.toast_duration_ms,
557563
),
558564
...params,
559565
});

packages/plugin/src/hooks/magic-context/send-session-notification.ts

Lines changed: 16 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ export interface NotificationParams {
66
variant?: string;
77
providerId?: string;
88
modelId?: string;
9+
/** TUI toast lifetime in milliseconds (default: 5000). */
10+
toastDurationMs?: number;
911
}
1012

1113
interface NotificationClient {
@@ -69,25 +71,21 @@ export async function sendIgnoredMessage(
6971
const { isTuiConnected: checkTui } = await import("../../shared/rpc-notifications");
7072
if (checkTui()) {
7173
try {
72-
const c = client as Record<string, unknown>;
73-
const tui = c?.tui as Record<string, unknown> | undefined;
74-
if (typeof tui?.showToast === "function") {
75-
// Intentional: call via property access to preserve `this` binding on the SDK client.
76-
// The tui object is an SDK-generated client where methods live on the prototype.
77-
const tuiClient = tui as Record<string, (...args: unknown[]) => Promise<unknown>>;
78-
await tuiClient.showToast({
79-
body: {
80-
title: extractToastTitle(text),
81-
message: text.length > 200 ? `${text.slice(0, 200)}…` : text,
82-
variant: inferToastVariant(text),
83-
duration: 5000,
84-
},
85-
});
86-
return;
87-
}
74+
const { pushNotification } = await import("../../shared/rpc-notifications");
75+
pushNotification(
76+
"toast",
77+
{
78+
title: extractToastTitle(text),
79+
message: text.length > 200 ? `${text.slice(0, 200)}…` : text,
80+
variant: inferToastVariant(text),
81+
duration: params.toastDurationMs ?? 5000,
82+
},
83+
sessionId,
84+
);
85+
return;
8886
} catch {
89-
// showToast failed or tui client is unavailable — fall through to ignored message.
90-
sessionLog(sessionId, "TUI showToast failed, falling back to ignored message");
87+
// RPC enqueue failed — fall through to ignored message.
88+
sessionLog(sessionId, "TUI RPC toast enqueue failed, falling back to ignored message");
9189
}
9290
}
9391
const agent = params.agent || undefined;

packages/plugin/src/plugin/rpc-handlers.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -473,6 +473,7 @@ export function buildStatusDetail(
473473
historyBlockTokens: 0,
474474
compressionBudget: null,
475475
compressionUsage: null,
476+
toastDurationMs: 5000,
476477
};
477478

478479
try {
@@ -567,6 +568,12 @@ export function buildStatusDetail(
567568
if (typeof config.history_budget_percentage === "number") {
568569
detail.historyBudgetPercentage = config.history_budget_percentage;
569570
}
571+
detail.toastDurationMs = resolveConfigValue<number>(
572+
config,
573+
"toast_duration_ms",
574+
modelKey,
575+
5000,
576+
);
570577
}
571578

572579
// Derived values
@@ -747,6 +754,11 @@ export function registerRpcHandlers(
747754
return { ok: true };
748755
});
749756

757+
rpcServer.handle("toast-duration", async () => {
758+
const resolved = resolveConfigValue<number>(rawConfig, "toast_duration_ms", undefined, 5000);
759+
return { toastDurationMs: resolved };
760+
});
761+
750762
rpcServer.handle("pending-notifications", async (params) => {
751763
const lastReceivedId = Number(params.lastReceivedId ?? 0);
752764
const notifications = drainNotifications(

packages/plugin/src/shared/rpc-types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,8 @@ export interface StatusDetail extends SidebarSnapshot {
9292
historyBlockTokens: number;
9393
compressionBudget: number | null;
9494
compressionUsage: string | null;
95+
/** Effective configured toast duration in ms after config resolution. */
96+
toastDurationMs: number;
9597
}
9698

9799
export interface RpcNotificationMessage {

packages/plugin/src/tui/data/context-db.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,7 @@ export async function loadStatusDetail(
187187
historyBlockTokens: 0,
188188
compressionBudget: null,
189189
compressionUsage: null,
190+
toastDurationMs: 5000,
190191
};
191192

192193
if (!rpcClient) return emptyDetail;
@@ -227,6 +228,17 @@ export async function requestRecomp(sessionId: string): Promise<boolean> {
227228
}
228229
}
229230

231+
/** Resolve global toast duration from server config via RPC. */
232+
export async function loadToastDurationMs(): Promise<number> {
233+
if (!rpcClient) return 5000;
234+
try {
235+
const result = await rpcClient.call<{ toastDurationMs?: number }>("toast-duration", {});
236+
return typeof result.toastDurationMs === "number" ? result.toastDurationMs : 5000;
237+
} catch {
238+
return 5000;
239+
}
240+
}
241+
230242
export interface TuiMessage {
231243
type: string;
232244
payload: Record<string, unknown>;

packages/plugin/src/tui/index.tsx

Lines changed: 32 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,34 @@ import { createMemo } from "solid-js"
66
import type { TuiPlugin, TuiPluginApi, TuiThemeCurrent } from "@opencode-ai/plugin/tui"
77
import { createSidebarContentSlot } from "./slots/sidebar-content"
88
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, getAnnouncement, getCompartmentCount, initRpcClient, loadStatusDetail, loadToastDurationMs, markAnnounced, requestRecomp, type StatusDetail } from "./data/context-db"
1010
import { formatThresholdPercent } from "../shared/format-threshold"
1111
import { detectConflicts } from "../shared/conflict-detector"
1212
import { fixConflicts } from "../shared/conflict-fixer"
1313
import { readJsoncFile } from "../shared/jsonc-parser"
1414
import { getOpenCodeConfigPaths } from "../shared/opencode-config-dir"
1515

1616
const PLUGIN_NAME = "@cortexkit/opencode-magic-context"
17+
const DEFAULT_TOAST_DURATION_MS = 5000
18+
let unifiedToastDurationMs = DEFAULT_TOAST_DURATION_MS
19+
20+
function getToastDurationMs(): number {
21+
return unifiedToastDurationMs
22+
}
23+
24+
function showToast(
25+
api: TuiPluginApi,
26+
input: {
27+
message: string
28+
variant: "info" | "warning" | "error" | "success"
29+
},
30+
): void {
31+
api.ui.toast({
32+
message: input.message,
33+
variant: input.variant,
34+
duration: getToastDurationMs(),
35+
})
36+
}
1737

1838
function ensureParentDir(filePath: string) {
1939
mkdirSync(dirname(filePath), { recursive: true })
@@ -97,14 +117,14 @@ function showConflictDialog(api: TuiPluginApi, directory: string, reasons: strin
97117
title="✅ Configuration Fixed"
98118
message={`${actionSummary}\n\nPlease restart OpenCode for changes to take effect.`}
99119
onConfirm={() => {
100-
api.ui.toast({ message: "Restart OpenCode to enable Magic Context", variant: "warning", duration: 10000 })
120+
showToast(api, { message: "Restart OpenCode to enable Magic Context", variant: "warning" })
101121
}}
102122
/>
103123
))
104124
}, 50)
105125
}}
106126
onCancel={() => {
107-
api.ui.toast({ message: "Magic Context remains disabled. Run: npx @cortexkit/opencode-magic-context@latest doctor", variant: "warning", duration: 5000 })
127+
showToast(api, { message: "Magic Context remains disabled. Run: npx @cortexkit/opencode-magic-context@latest doctor", variant: "warning" })
108128
}}
109129
/>
110130
))
@@ -132,7 +152,7 @@ function showTuiSetupDialog(api: TuiPluginApi) {
132152
title="❌ Setup Failed"
133153
message={'Could not update tui.json automatically. Add the plugin manually:\n\n { "plugin": ["@cortexkit/opencode-magic-context"] }'}
134154
onConfirm={() => {
135-
api.ui.toast({ message: "Add plugin to tui.json manually", variant: "warning", duration: 5000 })
155+
showToast(api, { message: "Add plugin to tui.json manually", variant: "warning" })
136156
}}
137157
/>
138158
))
@@ -146,14 +166,14 @@ function showTuiSetupDialog(api: TuiPluginApi) {
146166
title="✅ Sidebar Enabled"
147167
message="tui.json updated with Magic Context plugin.\n\nPlease restart OpenCode to see the sidebar."
148168
onConfirm={() => {
149-
api.ui.toast({ message: "Restart OpenCode to see the sidebar", variant: "warning", duration: 10000 })
169+
showToast(api, { message: "Restart OpenCode to see the sidebar", variant: "warning" })
150170
}}
151171
/>
152172
))
153173
}, 50)
154174
}}
155175
onCancel={() => {
156-
api.ui.toast({ message: "You can add the sidebar later via: npx @cortexkit/opencode-magic-context@latest doctor", variant: "info", duration: 5000 })
176+
showToast(api, { message: "You can add the sidebar later via: npx @cortexkit/opencode-magic-context@latest doctor", variant: "info" })
157177
}}
158178
/>
159179
))
@@ -417,7 +437,7 @@ function getModelKeyFromMessages(api: TuiPluginApi, sessionId: string): string |
417437
function showRecompDialog(api: TuiPluginApi) {
418438
const sessionId = getSessionId(api)
419439
if (!sessionId) {
420-
api.ui.toast({ message: "No active session", variant: "warning" })
440+
showToast(api, { message: "No active session", variant: "warning" })
421441
return
422442
}
423443

@@ -435,10 +455,10 @@ function showRecompDialog(api: TuiPluginApi) {
435455
].join("\n")}
436456
onConfirm={() => {
437457
void requestRecomp(sessionId)
438-
api.ui.toast({ message: "Recomp requested — historian will start shortly", variant: "info", duration: 5000 })
458+
showToast(api, { message: "Recomp requested — historian will start shortly", variant: "info" })
439459
}}
440460
onCancel={() => {
441-
api.ui.toast({ message: "Recomp cancelled", variant: "info", duration: 3000 })
461+
showToast(api, { message: "Recomp cancelled", variant: "info" })
442462
}}
443463
/>
444464
))
@@ -448,7 +468,7 @@ function showRecompDialog(api: TuiPluginApi) {
448468
function showStatusDialog(api: TuiPluginApi) {
449469
const sessionId = getSessionId(api)
450470
if (!sessionId) {
451-
api.ui.toast({ message: "No active session", variant: "warning" })
471+
showToast(api, { message: "No active session", variant: "warning" })
452472
return
453473
}
454474

@@ -625,6 +645,7 @@ const tui: TuiPlugin = async (api, _options, meta) => {
625645
// Initialize RPC client for server communication
626646
const directory = api.state.path.directory ?? ""
627647
initRpcClient(directory)
648+
unifiedToastDurationMs = await loadToastDurationMs()
628649

629650
// Register sidebar slot
630651
api.slots.register(createSidebarContentSlot(api))
@@ -650,10 +671,9 @@ const tui: TuiPlugin = async (api, _options, meta) => {
650671
for (const msg of messages) {
651672
if (msg.type === "toast") {
652673
const p = msg.payload
653-
api.ui.toast({
674+
showToast(api, {
654675
message: String(p.message ?? ""),
655676
variant: (p.variant as "info" | "warning" | "error" | "success") ?? "info",
656-
duration: typeof p.duration === "number" ? p.duration : 5000,
657677
})
658678
} else if (msg.type === "action") {
659679
const action = msg.payload?.action

0 commit comments

Comments
 (0)