Skip to content

Commit 21db026

Browse files
committed
fix: thread toast duration through session hook config
- preserves toast_duration_ms when building the narrowed hook config - keeps dream notifications on the configured duration instead of falling back to 5s - includes regression coverage - keep restart/setup toasts on 10s fallback
1 parent 041b34e commit 21db026

17 files changed

Lines changed: 224 additions & 42 deletions

CONFIGURATION.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,7 @@ Higher-tier models with longer cache windows benefit from a longer TTL. Setting
113113
| `ctx_reduce_enabled` | `boolean` | `true` | When `false`, hides `ctx_reduce` tool, disables all nudges/reminders, and strips reduction guidance from prompts. Heuristic cleanup, compartments, memory, and search still work. Useful for testing whether automatic cleanup alone is sufficient. |
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. |
116+
| `toast_duration_ms` | `number` (1000–60000) | `5000` | TUI toast lifetime for Magic Context notifications in milliseconds. Increase this if toasts disappear too quickly. |
116117
| `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. |
117118
| `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. |
118119
| `clear_reasoning_age` | `number` | `50` | Clear thinking/reasoning blocks older than N tags. |
@@ -627,6 +628,7 @@ Tier boundaries are hardcoded to keep behavior predictable and prevent cache-bus
627628
"anthropic/claude-opus-4-6": 50
628629
},
629630
"protected_tags": 10,
631+
"toast_duration_ms": 12000,
630632
"history_budget_percentage": 0.15,
631633
"temporal_awareness": true,
632634

assets/magic-context.schema.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -461,6 +461,13 @@
461461
}
462462
]
463463
},
464+
"toast_duration_ms": {
465+
"default": 5000,
466+
"description": "TUI toast lifetime in milliseconds for Magic Context notifications (min: 1000, max: 60000, default: 5000)",
467+
"type": "number",
468+
"minimum": 1000,
469+
"maximum": 60000
470+
},
464471
"execute_threshold_percentage": {
465472
"default": 65,
466473
"description": "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",

packages/dashboard/src/components/ConfigEditor/ConfigEditor.tsx

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -204,6 +204,13 @@ const FIELD_DEFS: FieldDef[] = [
204204
"Enable agent-controlled reductions via the ctx_reduce tool. When enabled, the agent is prompted and nudged to choose what messages and tool calls to drop. When disabled, the system still manages context automatically via historian comparting and the tiered emergency drop under pressure.",
205205
section: "General",
206206
},
207+
{
208+
key: "toast_duration_ms",
209+
label: "Toast Duration (ms)",
210+
type: "number",
211+
description: "How long Magic Context TUI toasts stay visible.",
212+
section: "General",
213+
},
207214
// Thresholds
208215
// cache_ttl and execute_threshold_percentage are rendered as custom PerModelField components
209216
// Tags & cleanup

packages/dashboard/src/components/ConfigEditor/config-field-coverage.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ export const RENDERED_PREFIXES: readonly string[] = [
2020
// General
2121
"enabled",
2222
"ctx_reduce_enabled",
23+
"toast_duration_ms",
2324
// Thresholds (custom PerModelField widgets)
2425
"cache_ttl",
2526
"execute_threshold_percentage",

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ describe("MagicContextConfigSchema", () => {
4343
enabled: true,
4444
auto_update: false,
4545
ctx_reduce_enabled: true,
46+
toast_duration_ms: 5000,
4647
cache_ttl: "10m",
4748
protected_tags: 3,
4849
execute_threshold_percentage: 75,

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

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -244,6 +244,8 @@ export interface MagicContextConfig {
244244
historian?: HistorianConfig;
245245
dreamer?: DreamerConfig;
246246
cache_ttl: string | { default: string; [modelKey: string]: string };
247+
/** TUI toast lifetime in milliseconds for Magic Context notifications. Default: 5000. */
248+
toast_duration_ms?: number;
247249
execute_threshold_percentage: number | { default: number; [modelKey: string]: number };
248250
/** Absolute token thresholds per model. When set for a given model (or via `default`),
249251
* this overrides `execute_threshold_percentage` for that model. Useful for hard caps
@@ -374,6 +376,14 @@ export const MagicContextConfigSchema = z
374376
.describe(
375377
'Cache TTL: string (e.g. "5m") or per-model object ({ default: "5m", "model-id": "10m" })',
376378
),
379+
toast_duration_ms: z
380+
.number()
381+
.min(1_000)
382+
.max(60_000)
383+
.default(5_000)
384+
.describe(
385+
"TUI toast lifetime in milliseconds for Magic Context notifications (min: 1000, max: 60000, default: 5000)",
386+
),
377387
execute_threshold_percentage: z
378388
.union([
379389
z.number().min(20).max(80, EXECUTE_THRESHOLD_CAP_MESSAGE),

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -721,13 +721,13 @@ describe("createMagicContextCommandHandler", () => {
721721
1,
722722
"ses-dream",
723723
"Starting dream run...",
724-
{},
724+
{ toastDurationMs: 5000 },
725725
);
726726
expect(sendNotification).toHaveBeenNthCalledWith(
727727
2,
728728
"ses-dream",
729729
expect.stringContaining("### Tasks"),
730-
{},
730+
{ toastDurationMs: 5000 },
731731
);
732732
});
733733

@@ -775,7 +775,7 @@ describe("createMagicContextCommandHandler", () => {
775775
3,
776776
"ses-dream",
777777
"Dream already queued for this project",
778-
{},
778+
{ toastDurationMs: 5000 },
779779
);
780780
expect(executeDream).toHaveBeenCalledTimes(1);
781781
});

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

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -270,6 +270,7 @@ async function executeDreaming(
270270
text: string,
271271
params: NotificationParams,
272272
) => Promise<void>;
273+
toastDurationMs?: number;
273274
dreamer?: {
274275
config: DreamerConfig;
275276
projectPath: string;
@@ -288,11 +289,15 @@ async function executeDreaming(
288289
},
289290
sessionId: string,
290291
): Promise<never> {
292+
const dreamNotificationParams: NotificationParams = {
293+
toastDurationMs: deps.toastDurationMs ?? 5000,
294+
};
295+
291296
if (!deps.dreamer?.config?.tasks?.length) {
292297
await deps.sendNotification(
293298
sessionId,
294299
"## /ctx-dream\n\nDreaming is not configured for this project.",
295-
{},
300+
dreamNotificationParams,
296301
);
297302
throwSentinel("CTX-DREAM");
298303
}
@@ -303,11 +308,15 @@ async function executeDreaming(
303308
// runner with an unexpired lease is never deleted just because it is older than 2m.
304309
const entry = enqueueDream(deps.db, deps.dreamer.projectPath, "manual", true);
305310
if (!entry) {
306-
await deps.sendNotification(sessionId, "Dream already queued for this project", {});
311+
await deps.sendNotification(
312+
sessionId,
313+
"Dream already queued for this project",
314+
dreamNotificationParams,
315+
);
307316
throwSentinel("CTX-DREAM");
308317
}
309318

310-
await deps.sendNotification(sessionId, "Starting dream run...", {});
319+
await deps.sendNotification(sessionId, "Starting dream run...", dreamNotificationParams);
311320

312321
const result = deps.dreamer.executeDream
313322
? await deps.dreamer.executeDream(sessionId)
@@ -334,7 +343,7 @@ async function executeDreaming(
334343
result
335344
? summarizeDreamResult(result)
336345
: "Dream queued, but another worker is already processing the queue.",
337-
{},
346+
dreamNotificationParams,
338347
);
339348
throwSentinel("CTX-DREAM");
340349
}
@@ -371,6 +380,8 @@ export function createMagicContextCommandHandler(deps: {
371380
text: string,
372381
params: NotificationParams,
373382
) => Promise<void>;
383+
/** Configured toast lifetime (ms) forwarded into diagnostics logs. */
384+
toastDurationMs?: number;
374385
sidekick?: {
375386
config: SidekickConfig;
376387
projectPath: string;

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,11 +130,13 @@ export function getLiveNotificationParams(
130130
liveModelBySession: LiveModelBySession,
131131
variantBySession: VariantBySession,
132132
agentBySession?: AgentBySession,
133+
toastDurationMs?: number,
133134
): {
134135
agent?: string;
135136
variant?: string;
136137
providerId?: string;
137138
modelId?: string;
139+
toastDurationMs?: number;
138140
} {
139141
const model = liveModelBySession.get(sessionId);
140142
const variant = variantBySession.get(sessionId);
@@ -143,6 +145,7 @@ export function getLiveNotificationParams(
143145
...(agent ? { agent } : {}),
144146
...(variant ? { variant } : {}),
145147
...(model ? { providerId: model.providerID, modelId: model.modelID } : {}),
148+
...(typeof toastDurationMs === "number" ? { toastDurationMs } : {}),
146149
};
147150
}
148151

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

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ export interface MagicContextDeps {
8484
config: {
8585
protected_tags: number;
8686
ctx_reduce_enabled?: boolean;
87+
toast_duration_ms?: number;
8788
clear_reasoning_age?: number;
8889
execute_threshold_percentage?: number | { default: number; [modelKey: string]: number };
8990
execute_threshold_tokens?: { default?: number; [modelKey: string]: number | undefined };
@@ -338,7 +339,13 @@ export function createMagicContextHook(deps: MagicContextDeps) {
338339
userMemoriesEnabled: dreamerConfig?.user_memories?.enabled === true,
339340
ensureProjectRegistered: ensureProjectRegisteredFromOpenCodeDirectory,
340341
getNotificationParams: (sid) =>
341-
getLiveNotificationParams(sid, liveModelBySession, variantBySession, agentBySession),
342+
getLiveNotificationParams(
343+
sid,
344+
liveModelBySession,
345+
variantBySession,
346+
agentBySession,
347+
deps.config.toast_duration_ms,
348+
),
342349
});
343350
// /ctx-embed-history: backfill ALL of this session's compartment chunk
344351
// embeddings in one pass, reusing the recomp progress surface (sidebar +
@@ -453,6 +460,7 @@ export function createMagicContextHook(deps: MagicContextDeps) {
453460
liveModelBySession,
454461
variantBySession,
455462
agentBySession,
463+
deps.config.toast_duration_ms,
456464
),
457465
getModelKey: (sessionId) => {
458466
const model = liveModelBySession.get(sessionId);
@@ -509,6 +517,7 @@ export function createMagicContextHook(deps: MagicContextDeps) {
509517
liveModelBySession,
510518
variantBySession,
511519
agentBySession,
520+
deps.config.toast_duration_ms,
512521
),
513522
onSessionCacheInvalidated: (sessionId: string) => {
514523
clearInjectionCache(sessionId);
@@ -588,6 +597,7 @@ export function createMagicContextHook(deps: MagicContextDeps) {
588597
const commandHandler = createMagicContextCommandHandler({
589598
db,
590599
protectedTags: deps.config.protected_tags,
600+
toastDurationMs: deps.config.toast_duration_ms,
591601
executeThresholdPercentage: deps.config.execute_threshold_percentage ?? 65,
592602
executeThresholdTokens: deps.config.execute_threshold_tokens,
593603
historyBudgetPercentage: deps.config.history_budget_percentage,
@@ -639,6 +649,7 @@ export function createMagicContextHook(deps: MagicContextDeps) {
639649
liveModelBySession,
640650
variantBySession,
641651
agentBySession,
652+
deps.config.toast_duration_ms,
642653
),
643654
...params,
644655
});
@@ -763,6 +774,7 @@ export function createMagicContextHook(deps: MagicContextDeps) {
763774
liveModelBySession,
764775
variantBySession,
765776
agentBySession,
777+
deps.config.toast_duration_ms,
766778
),
767779
isTuiConnected,
768780
pushTuiDialogAction: (sid, resume) =>

0 commit comments

Comments
 (0)