Skip to content

Commit 3f1a681

Browse files
committed
Add option for the notification interval
1 parent d9728ce commit 3f1a681

10 files changed

Lines changed: 120 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: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -479,6 +479,7 @@ export function buildStatusDetail(
479479
historyBlockTokens: 0,
480480
compressionBudget: null,
481481
compressionUsage: null,
482+
toastDurationMs: 5000,
482483
};
483484

484485
try {
@@ -573,6 +574,12 @@ export function buildStatusDetail(
573574
if (typeof config.history_budget_percentage === "number") {
574575
detail.historyBudgetPercentage = config.history_budget_percentage;
575576
}
577+
detail.toastDurationMs = resolveConfigValue<number>(
578+
config,
579+
"toast_duration_ms",
580+
modelKey,
581+
5000,
582+
);
576583
}
577584

578585
// Derived values
@@ -635,6 +642,7 @@ export function registerRpcHandlers(
635642
liveSessionState.liveModelBySession,
636643
liveSessionState.variantBySession,
637644
liveSessionState.agentBySession,
645+
config.toast_duration_ms,
638646
);
639647

640648
const injectionBudgetTokens = config.memory?.injection_budget_tokens;
@@ -753,6 +761,11 @@ export function registerRpcHandlers(
753761
return { ok: true };
754762
});
755763

764+
rpcServer.handle("toast-duration", async () => {
765+
const resolved = resolveConfigValue<number>(rawConfig, "toast_duration_ms", undefined, 5000);
766+
return { toastDurationMs: resolved };
767+
});
768+
756769
rpcServer.handle("pending-notifications", async (params) => {
757770
const lastReceivedId = Number(params.lastReceivedId ?? 0);
758771
const notifications = drainNotifications(

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,8 @@ export interface StatusDetail extends SidebarSnapshot {
9494
historyBlockTokens: number;
9595
compressionBudget: number | null;
9696
compressionUsage: string | null;
97+
/** Effective configured toast duration in ms after config resolution. */
98+
toastDurationMs: number;
9799
}
98100

99101
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
@@ -189,6 +189,7 @@ export async function loadStatusDetail(
189189
historyBlockTokens: 0,
190190
compressionBudget: null,
191191
compressionUsage: null,
192+
toastDurationMs: 5000,
192193
};
193194

194195
if (!rpcClient) return emptyDetail;
@@ -229,6 +230,17 @@ export async function requestRecomp(sessionId: string): Promise<boolean> {
229230
}
230231
}
231232

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

0 commit comments

Comments
 (0)