Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CONFIGURATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@ Higher-tier models with longer cache windows benefit from a longer TTL. Setting
| `cache_ttl` | `string` or `object` | `"5m"` | Time after a response before applying pending ops. String or per-model map. |
| `protected_tags` | `number` (1–100) | `20` | Last N active tags immune from immediate dropping. |
| `nudge_interval_tokens` | `number` | `10000` | Minimum token growth between rolling nudges. |
| `toast_duration_ms` | `number` (1000–60000) | `5000` | TUI toast lifetime for Magic Context notifications in milliseconds. Increase this if toasts disappear too quickly. |
| `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. |
| `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. |
| `auto_drop_tool_age` | `number` | `100` | Auto-drop tool outputs older than N tags during execution. |
Expand Down Expand Up @@ -617,6 +618,7 @@ Tier boundaries are hardcoded to keep behavior predictable and prevent cache-bus
"protected_tags": 10,
"auto_drop_tool_age": 50,
"drop_tool_structure": true,
"toast_duration_ms": 12000,
"history_budget_percentage": 0.15,
"compaction_markers": true,
"compressor": {
Expand Down
4 changes: 4 additions & 0 deletions packages/plugin/src/config/schema/magic-context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,8 @@ export interface MagicContextConfig {
dreamer?: DreamerConfig;
cache_ttl: string | { default: string; [modelKey: string]: string };
nudge_interval_tokens: number;
/** TUI toast lifetime in milliseconds for Magic Context notifications. Default: 5000. */
toast_duration_ms?: number;
execute_threshold_percentage: number | { default: number; [modelKey: string]: number };
/** Absolute token thresholds per model. When set for a given model (or via `default`),
* this overrides `execute_threshold_percentage` for that model. Useful for hard caps
Expand Down Expand Up @@ -310,6 +312,8 @@ export const MagicContextConfigSchema = z
.default("5m"),
/** Minimum token growth between low-priority rolling nudges (default: DEFAULT_NUDGE_INTERVAL_TOKENS) */
nudge_interval_tokens: z.number().min(1000).default(DEFAULT_NUDGE_INTERVAL_TOKENS),
/** TUI toast lifetime in milliseconds for Magic Context notifications (min: 1000, max: 60000, default: 5000) */
toast_duration_ms: z.number().min(1_000).max(60_000).default(5_000),
/** 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 */
execute_threshold_percentage: z
.union([
Expand Down
28 changes: 21 additions & 7 deletions packages/plugin/src/hooks/magic-context/command-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -289,6 +289,8 @@ export function createMagicContextCommandHandler(deps: {
text: string,
params: NotificationParams,
) => Promise<void>;
/** Configured toast lifetime (ms) forwarded into diagnostics logs. */
toastDurationMs?: number;
sidekick?: {
config: SidekickConfig;
projectPath: string;
Expand Down Expand Up @@ -351,13 +353,25 @@ export function createMagicContextCommandHandler(deps: {
deps.onFlush?.(sessionId);
}

if (isStatus) {
if (isTuiConnected()) {
// In TUI, push an RPC action so the TUI poller shows a native dialog
pushNotification("action", { action: "show-status-dialog" }, sessionId);
sessionLog(sessionId, "command ctx-status: pushed show-status-dialog to TUI");
throwSentinel(input.command);
}
if (isStatus) {
if (isTuiConnected()) {
// In TUI, push an RPC action so the TUI poller shows a native dialog
pushNotification(
"action",
{
action: "show-status-dialog",
toast_duration_ms: deps.toastDurationMs ?? 5000,
},
sessionId,
);
sessionLog(
sessionId,
`command ctx-status: pushed show-status-dialog to TUI (toast_duration_ms=${String(
deps.toastDurationMs ?? 5000,
)})`,
);
throwSentinel(input.command);
}
const liveModelKey = deps.getLiveModelKey?.(sessionId);
const liveContextLimit = deps.getContextLimit?.(sessionId);
const statusOutput = executeStatus(
Expand Down
3 changes: 3 additions & 0 deletions packages/plugin/src/hooks/magic-context/hook-handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -122,11 +122,13 @@ export function getLiveNotificationParams(
liveModelBySession: LiveModelBySession,
variantBySession: VariantBySession,
agentBySession?: AgentBySession,
toastDurationMs?: number,
): {
agent?: string;
variant?: string;
providerId?: string;
modelId?: string;
toastDurationMs?: number;
} {
const model = liveModelBySession.get(sessionId);
const variant = variantBySession.get(sessionId);
Expand All @@ -135,6 +137,7 @@ export function getLiveNotificationParams(
...(agent ? { agent } : {}),
...(variant ? { variant } : {}),
...(model ? { providerId: model.providerID, modelId: model.modelID } : {}),
...(typeof toastDurationMs === "number" ? { toastDurationMs } : {}),
};
}

Expand Down
6 changes: 6 additions & 0 deletions packages/plugin/src/hooks/magic-context/hook.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ export interface MagicContextDeps {
protected_tags: number;
ctx_reduce_enabled?: boolean;
nudge_interval_tokens?: number;
toast_duration_ms?: number;
auto_drop_tool_age?: number;
drop_tool_structure?: boolean;
clear_reasoning_age?: number;
Expand Down Expand Up @@ -345,6 +346,7 @@ export function createMagicContextHook(deps: MagicContextDeps) {
liveModelBySession,
variantBySession,
agentBySession,
deps.config.toast_duration_ms,
),
getModelKey: (sessionId) => {
const model = liveModelBySession.get(sessionId);
Expand Down Expand Up @@ -408,6 +410,7 @@ export function createMagicContextHook(deps: MagicContextDeps) {
liveModelBySession,
variantBySession,
agentBySession,
deps.config.toast_duration_ms,
),
nudgePlacements,
onSessionCacheInvalidated: (sessionId: string) => {
Expand Down Expand Up @@ -470,6 +473,7 @@ export function createMagicContextHook(deps: MagicContextDeps) {
const commandHandler = createMagicContextCommandHandler({
db,
protectedTags: deps.config.protected_tags,
toastDurationMs: deps.config.toast_duration_ms,
nudgeIntervalTokens: deps.config.nudge_interval_tokens ?? DEFAULT_NUDGE_INTERVAL_TOKENS,
executeThresholdPercentage: deps.config.execute_threshold_percentage ?? 65,
executeThresholdTokens: deps.config.execute_threshold_tokens,
Expand Down Expand Up @@ -521,6 +525,7 @@ export function createMagicContextHook(deps: MagicContextDeps) {
liveModelBySession,
variantBySession,
agentBySession,
deps.config.toast_duration_ms,
),
historianTwoPass: deps.config.historian?.two_pass === true,
// Issue #44: respect memory feature gates from /ctx-recomp too.
Expand Down Expand Up @@ -554,6 +559,7 @@ export function createMagicContextHook(deps: MagicContextDeps) {
liveModelBySession,
variantBySession,
agentBySession,
deps.config.toast_duration_ms,
),
...params,
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ export interface NotificationParams {
variant?: string;
providerId?: string;
modelId?: string;
/** TUI toast lifetime in milliseconds (default: 5000). */
toastDurationMs?: number;
}

interface NotificationClient {
Expand Down Expand Up @@ -69,25 +71,21 @@ export async function sendIgnoredMessage(
const { isTuiConnected: checkTui } = await import("../../shared/rpc-notifications");
if (checkTui()) {
try {
const c = client as Record<string, unknown>;
const tui = c?.tui as Record<string, unknown> | undefined;
if (typeof tui?.showToast === "function") {
// Intentional: call via property access to preserve `this` binding on the SDK client.
// The tui object is an SDK-generated client where methods live on the prototype.
const tuiClient = tui as Record<string, (...args: unknown[]) => Promise<unknown>>;
await tuiClient.showToast({
body: {
title: extractToastTitle(text),
message: text.length > 200 ? `${text.slice(0, 200)}…` : text,
variant: inferToastVariant(text),
duration: 5000,
},
});
return;
}
const { pushNotification } = await import("../../shared/rpc-notifications");
pushNotification(
"toast",
{
title: extractToastTitle(text),
message: text.length > 200 ? `${text.slice(0, 200)}…` : text,
variant: inferToastVariant(text),
duration: params.toastDurationMs ?? 5000,
},
sessionId,
);
return;
} catch {
// showToast failed or tui client is unavailable — fall through to ignored message.
sessionLog(sessionId, "TUI showToast failed, falling back to ignored message");
// RPC enqueue failed — fall through to ignored message.
sessionLog(sessionId, "TUI RPC toast enqueue failed, falling back to ignored message");
}
}
const agent = params.agent || undefined;
Expand Down
13 changes: 13 additions & 0 deletions packages/plugin/src/plugin/rpc-handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -479,6 +479,7 @@ export function buildStatusDetail(
historyBlockTokens: 0,
compressionBudget: null,
compressionUsage: null,
toastDurationMs: 5000,
};

try {
Expand Down Expand Up @@ -573,6 +574,12 @@ export function buildStatusDetail(
if (typeof config.history_budget_percentage === "number") {
detail.historyBudgetPercentage = config.history_budget_percentage;
}
detail.toastDurationMs = resolveConfigValue<number>(
config,
"toast_duration_ms",
modelKey,
5000,
);
}

// Derived values
Expand Down Expand Up @@ -635,6 +642,7 @@ export function registerRpcHandlers(
liveSessionState.liveModelBySession,
liveSessionState.variantBySession,
liveSessionState.agentBySession,
config.toast_duration_ms,
);

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

rpcServer.handle("toast-duration", async () => {
const resolved = resolveConfigValue<number>(rawConfig, "toast_duration_ms", undefined, 5000);
return { toastDurationMs: resolved };
});

rpcServer.handle("pending-notifications", async (params) => {
const lastReceivedId = Number(params.lastReceivedId ?? 0);
const notifications = drainNotifications(
Expand Down
2 changes: 2 additions & 0 deletions packages/plugin/src/shared/rpc-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,8 @@ export interface StatusDetail extends SidebarSnapshot {
historyBlockTokens: number;
compressionBudget: number | null;
compressionUsage: string | null;
/** Effective configured toast duration in ms after config resolution. */
toastDurationMs: number;
}

export interface RpcNotificationMessage {
Expand Down
12 changes: 12 additions & 0 deletions packages/plugin/src/tui/data/context-db.ts
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,7 @@ export async function loadStatusDetail(
historyBlockTokens: 0,
compressionBudget: null,
compressionUsage: null,
toastDurationMs: 5000,
};

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

/** Resolve global toast duration from server config via RPC. */
export async function loadToastDurationMs(): Promise<number> {
if (!rpcClient) return 5000;
try {
const result = await rpcClient.call<{ toastDurationMs?: number }>("toast-duration", {});
return typeof result.toastDurationMs === "number" ? result.toastDurationMs : 5000;
} catch {
return 5000;
}
}

export interface TuiMessage {
type: string;
payload: Record<string, unknown>;
Expand Down
Loading