Skip to content

Commit 9274696

Browse files
committed
Add option for the notification interval
1 parent f26a99c commit 9274696

10 files changed

Lines changed: 131 additions & 37 deletions

File tree

CONFIGURATION.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,7 @@ Higher-tier models with longer cache windows benefit from a longer TTL. Setting
114114
| `cache_ttl` | `string` or `object` | `"5m"` | Time after a response before applying pending ops. String or per-model map. |
115115
| `protected_tags` | `number` (1–100) | `20` | Last N active tags immune from immediate dropping. |
116116
| `nudge_interval_tokens` | `number` | `10000` | Minimum token growth between rolling nudges. |
117+
| `toast_duration_ms` | `number` (1000–60000) | `5000` | TUI toast lifetime for Magic Context notifications in milliseconds. Increase this if toasts disappear too quickly. |
117118
| `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. |
118119
| `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. |
119120
| `auto_drop_tool_age` | `number` | `100` | Auto-drop tool outputs older than N tags during execution. |
@@ -634,6 +635,7 @@ Tier boundaries are hardcoded to keep behavior predictable and prevent cache-bus
634635
"protected_tags": 10,
635636
"auto_drop_tool_age": 50,
636637
"drop_tool_structure": true,
638+
"toast_duration_ms": 12000,
637639
"history_budget_percentage": 0.15,
638640
"temporal_awareness": true,
639641

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

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -236,6 +236,8 @@ export interface MagicContextConfig {
236236
dreamer?: DreamerConfig;
237237
cache_ttl: string | { default: string; [modelKey: string]: string };
238238
nudge_interval_tokens: number;
239+
/** TUI toast lifetime in milliseconds for Magic Context notifications. Default: 5000. */
240+
toast_duration_ms?: number;
239241
execute_threshold_percentage: number | { default: number; [modelKey: string]: number };
240242
/** Absolute token thresholds per model. When set for a given model (or via `default`),
241243
* this overrides `execute_threshold_percentage` for that model. Useful for hard caps
@@ -371,6 +373,14 @@ export const MagicContextConfigSchema = z
371373
.describe(
372374
"Minimum token growth between low-priority rolling nudges (default: DEFAULT_NUDGE_INTERVAL_TOKENS)",
373375
),
376+
toast_duration_ms: z
377+
.number()
378+
.min(1_000)
379+
.max(60_000)
380+
.default(5_000)
381+
.describe(
382+
"TUI toast lifetime in milliseconds for Magic Context notifications (min: 1000, max: 60000, default: 5000)",
383+
),
374384
execute_threshold_percentage: z
375385
.union([
376386
z.number().min(20).max(80, EXECUTE_THRESHOLD_CAP_MESSAGE),

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

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -372,6 +372,8 @@ export function createMagicContextCommandHandler(deps: {
372372
text: string,
373373
params: NotificationParams,
374374
) => Promise<void>;
375+
/** Configured toast lifetime (ms) forwarded into diagnostics logs. */
376+
toastDurationMs?: number;
375377
sidekick?: {
376378
config: SidekickConfig;
377379
projectPath: string;
@@ -440,8 +442,20 @@ export function createMagicContextCommandHandler(deps: {
440442
if (isStatus) {
441443
if (isTuiConnected(sessionId)) {
442444
// In TUI, push an RPC action so the TUI poller shows a native dialog
443-
pushNotification("action", { action: "show-status-dialog" }, sessionId);
444-
sessionLog(sessionId, "command ctx-status: pushed show-status-dialog to TUI");
445+
pushNotification(
446+
"action",
447+
{
448+
action: "show-status-dialog",
449+
toast_duration_ms: deps.toastDurationMs ?? 5000,
450+
},
451+
sessionId,
452+
);
453+
sessionLog(
454+
sessionId,
455+
`command ctx-status: pushed show-status-dialog to TUI (toast_duration_ms=${String(
456+
deps.toastDurationMs ?? 5000,
457+
)})`,
458+
);
445459
throwSentinel(input.command);
446460
}
447461
const liveModelKey = deps.getLiveModelKey?.(sessionId);

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: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ export interface MagicContextDeps {
7878
protected_tags: number;
7979
ctx_reduce_enabled?: boolean;
8080
nudge_interval_tokens?: number;
81+
toast_duration_ms?: number;
8182
auto_drop_tool_age?: number;
8283
drop_tool_structure?: boolean;
8384
clear_reasoning_age?: number;
@@ -334,7 +335,13 @@ export function createMagicContextHook(deps: MagicContextDeps) {
334335
userMemoriesEnabled: dreamerConfig?.user_memories?.enabled === true,
335336
ensureProjectRegistered: ensureProjectRegisteredFromOpenCodeDirectory,
336337
getNotificationParams: (sid) =>
337-
getLiveNotificationParams(sid, liveModelBySession, variantBySession, agentBySession),
338+
getLiveNotificationParams(
339+
sid,
340+
liveModelBySession,
341+
variantBySession,
342+
agentBySession,
343+
deps.config.toast_duration_ms,
344+
),
338345
});
339346
const sidekickRunnable = isSidekickRunnable(deps.config);
340347
const sidekickConfig = sidekickRunnable ? deps.config.sidekick : undefined;
@@ -392,6 +399,7 @@ export function createMagicContextHook(deps: MagicContextDeps) {
392399
liveModelBySession,
393400
variantBySession,
394401
agentBySession,
402+
deps.config.toast_duration_ms,
395403
),
396404
getModelKey: (sessionId) => {
397405
const model = liveModelBySession.get(sessionId);
@@ -445,6 +453,7 @@ export function createMagicContextHook(deps: MagicContextDeps) {
445453
liveModelBySession,
446454
variantBySession,
447455
agentBySession,
456+
deps.config.toast_duration_ms,
448457
),
449458
nudgePlacements,
450459
onSessionCacheInvalidated: (sessionId: string) => {
@@ -526,6 +535,7 @@ export function createMagicContextHook(deps: MagicContextDeps) {
526535
const commandHandler = createMagicContextCommandHandler({
527536
db,
528537
protectedTags: deps.config.protected_tags,
538+
toastDurationMs: deps.config.toast_duration_ms,
529539
nudgeIntervalTokens: deps.config.nudge_interval_tokens ?? DEFAULT_NUDGE_INTERVAL_TOKENS,
530540
executeThresholdPercentage: deps.config.execute_threshold_percentage ?? 65,
531541
executeThresholdTokens: deps.config.execute_threshold_tokens,
@@ -577,6 +587,7 @@ export function createMagicContextHook(deps: MagicContextDeps) {
577587
liveModelBySession,
578588
variantBySession,
579589
agentBySession,
590+
deps.config.toast_duration_ms,
580591
),
581592
...params,
582593
});
@@ -706,6 +717,7 @@ export function createMagicContextHook(deps: MagicContextDeps) {
706717
liveModelBySession,
707718
variantBySession,
708719
agentBySession,
720+
deps.config.toast_duration_ms,
709721
),
710722
isTuiConnected,
711723
pushTuiDialogAction: (sid, resume) =>

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 {
@@ -75,25 +77,21 @@ export async function sendIgnoredMessage(
7577
const { isTuiConnected: checkTui } = await import("../../shared/rpc-notifications");
7678
if (!forcePersist && checkTui(sessionId)) {
7779
try {
78-
const c = client as Record<string, unknown>;
79-
const tui = c?.tui as Record<string, unknown> | undefined;
80-
if (typeof tui?.showToast === "function") {
81-
// Intentional: call via property access to preserve `this` binding on the SDK client.
82-
// The tui object is an SDK-generated client where methods live on the prototype.
83-
const tuiClient = tui as Record<string, (...args: unknown[]) => Promise<unknown>>;
84-
await tuiClient.showToast({
85-
body: {
86-
title: extractToastTitle(text),
87-
message: text.length > 200 ? `${text.slice(0, 200)}…` : text,
88-
variant: inferToastVariant(text),
89-
duration: 5000,
90-
},
91-
});
92-
return;
93-
}
80+
const { pushNotification } = await import("../../shared/rpc-notifications");
81+
pushNotification(
82+
"toast",
83+
{
84+
title: extractToastTitle(text),
85+
message: text.length > 200 ? `${text.slice(0, 200)}…` : text,
86+
variant: inferToastVariant(text),
87+
duration: params.toastDurationMs ?? 5000,
88+
},
89+
sessionId,
90+
);
91+
return;
9492
} catch {
95-
// showToast failed or tui client is unavailable — fall through to ignored message.
96-
sessionLog(sessionId, "TUI showToast failed, falling back to ignored message");
93+
// RPC enqueue failed — fall through to ignored message.
94+
sessionLog(sessionId, "TUI RPC toast enqueue failed, falling back to ignored message");
9795
}
9896
}
9997
const agent = params.agent || undefined;

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

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -535,6 +535,7 @@ export function buildStatusDetail(
535535
historyBlockTokens: 0,
536536
compressionBudget: null,
537537
compressionUsage: null,
538+
toastDurationMs: 5000,
538539
};
539540

540541
try {
@@ -629,6 +630,12 @@ export function buildStatusDetail(
629630
if (typeof config.history_budget_percentage === "number") {
630631
detail.historyBudgetPercentage = config.history_budget_percentage;
631632
}
633+
detail.toastDurationMs = resolveConfigValue<number>(
634+
config,
635+
"toast_duration_ms",
636+
modelKey,
637+
5000,
638+
);
632639
}
633640

634641
// Derived values
@@ -691,6 +698,7 @@ export function registerRpcHandlers(
691698
liveSessionState.liveModelBySession,
692699
liveSessionState.variantBySession,
693700
liveSessionState.agentBySession,
701+
config.toast_duration_ms,
694702
);
695703

696704
const injectionBudgetTokens = config.memory?.injection_budget_tokens;
@@ -861,6 +869,11 @@ export function registerRpcHandlers(
861869
}
862870
});
863871

872+
rpcServer.handle("toast-duration", async () => {
873+
const resolved = resolveConfigValue<number>(rawConfig, "toast_duration_ms", undefined, 5000);
874+
return { toastDurationMs: resolved };
875+
});
876+
864877
rpcServer.handle("pending-notifications", async (params) => {
865878
const lastReceivedId = Number(params.lastReceivedId ?? 0);
866879
// Scope drain to the TUI's active session so a notification tagged for a

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,8 @@ export interface StatusDetail extends SidebarSnapshot {
124124
historyBlockTokens: number;
125125
compressionBudget: number | null;
126126
compressionUsage: string | null;
127+
/** Effective configured toast duration in ms after config resolution. */
128+
toastDurationMs: number;
127129
}
128130

129131
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
@@ -203,6 +203,7 @@ export async function loadStatusDetail(
203203
historyBlockTokens: 0,
204204
compressionBudget: null,
205205
compressionUsage: null,
206+
toastDurationMs: 5000,
206207
};
207208

208209
if (!rpcClient) return emptyDetail;
@@ -270,6 +271,17 @@ export async function dismissUpgradeReminder(sessionId: string): Promise<boolean
270271
}
271272
}
272273

274+
/** Resolve global toast duration from server config via RPC. */
275+
export async function loadToastDurationMs(): Promise<number> {
276+
if (!rpcClient) return 5000;
277+
try {
278+
const result = await rpcClient.call<{ toastDurationMs?: number }>("toast-duration", {});
279+
return typeof result.toastDurationMs === "number" ? result.toastDurationMs : 5000;
280+
} catch {
281+
return 5000;
282+
}
283+
}
284+
273285
export interface TuiMessage {
274286
id: number;
275287
type: string;

0 commit comments

Comments
 (0)