@wolfx/opencode-magic-context 0.24.0 → 0.24.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -154609,15 +154609,22 @@ function ownerMessageIdForTagRow(row) {
154609
154609
  }
154610
154610
  return row.message_id.replace(CONTENT_ID_SUFFIX, "");
154611
154611
  }
154612
- function getActiveTagTokenAggregate(db, sessionId) {
154613
- const row = db.prepare(`SELECT
154612
+ function getActiveTagTokenAggregate(db, sessionId, protectedTags = 0) {
154613
+ const toolOutputExpr = protectedTags > 0 ? `COALESCE(SUM(CASE WHEN type = 'tool' AND tag_number < (
154614
+ SELECT tag_number FROM tags
154615
+ WHERE session_id = ? AND status = 'active'
154616
+ ORDER BY tag_number DESC LIMIT 1 OFFSET ?
154617
+ ) THEN COALESCE(token_count, 0) ELSE 0 END), 0)` : `COALESCE(SUM(CASE WHEN type = 'tool' THEN COALESCE(token_count, 0) ELSE 0 END), 0)`;
154618
+ const sql = `SELECT
154614
154619
  COALESCE(SUM(CASE WHEN type != 'tool' THEN COALESCE(token_count, 0) ELSE 0 END), 0)
154615
154620
  + COALESCE(SUM(COALESCE(reasoning_token_count, 0)), 0) AS conversation,
154616
154621
  COALESCE(SUM(CASE WHEN type = 'tool' THEN COALESCE(token_count, 0) + COALESCE(input_token_count, 0) ELSE 0 END), 0) AS tool_call,
154617
- COALESCE(SUM(CASE WHEN type = 'tool' THEN COALESCE(token_count, 0) ELSE 0 END), 0) AS tool_output,
154622
+ ${toolOutputExpr} AS tool_output,
154618
154623
  COALESCE(SUM(CASE WHEN token_count IS NULL THEN 1 ELSE 0 END), 0) AS null_count
154619
154624
  FROM tags
154620
- WHERE session_id = ? AND status = 'active'`).get(sessionId);
154625
+ WHERE session_id = ? AND status = 'active'`;
154626
+ const params = protectedTags > 0 ? [sessionId, protectedTags - 1, sessionId] : [sessionId];
154627
+ const row = db.prepare(sql).get(...params);
154621
154628
  return {
154622
154629
  conversation: row?.conversation ?? 0,
154623
154630
  toolCall: row?.tool_call ?? 0,
@@ -165372,9 +165379,10 @@ function chunkCanonicalText(canonicalText, startOrdinal, endOrdinal, maxInputTok
165372
165379
  if (lines.length === 0 || endOrdinal < startOrdinal)
165373
165380
  return [];
165374
165381
  const normalizedMax = normalizeCompartmentChunkMaxInputTokens(maxInputTokens);
165382
+ const effectiveMax = Math.max(1, Math.floor(normalizedMax * CHUNK_WINDOW_SAFETY_RATIO));
165375
165383
  const fullText = lines.join(`
165376
165384
  `);
165377
- if (estimateTokens(fullText) <= normalizedMax) {
165385
+ if (estimateTokens(fullText) <= effectiveMax) {
165378
165386
  return [
165379
165387
  {
165380
165388
  windowIndex: 0,
@@ -165412,7 +165420,7 @@ function chunkCanonicalText(canonicalText, startOrdinal, endOrdinal, maxInputTok
165412
165420
  const lineStart = range?.start ?? startOrdinal;
165413
165421
  const lineEnd = range?.end ?? lineStart;
165414
165422
  const lineTokens = estimateTokens(line);
165415
- if (currentLines.length > 0 && currentTokens + lineTokens > normalizedMax) {
165423
+ if (currentLines.length > 0 && currentTokens + lineTokens > effectiveMax) {
165416
165424
  flush2();
165417
165425
  }
165418
165426
  if (currentLines.length === 0) {
@@ -165567,7 +165575,7 @@ function countUnembeddedSessionCompartments(db, projectPath, sessionId, modelId)
165567
165575
  )`).get(projectPath, sessionId, projectPath, modelId);
165568
165576
  return typeof row?.n === "number" ? row.n : 0;
165569
165577
  }
165570
- var DEFAULT_COMPARTMENT_CHUNK_MAX_INPUT_TOKENS = 512, loadFtsRowsStatements, existingHashStatements, existingHashByProjectStatements, deleteByCompartmentStatements, insertEmbeddingStatements, distinctModelStatements, clearProjectStatements, clearProjectModelStatements, searchRowsStatements, searchRowsByModelStatements, backfillCandidateStatements, sessionBackfillCandidateStatements;
165578
+ var DEFAULT_COMPARTMENT_CHUNK_MAX_INPUT_TOKENS = 512, CHUNK_WINDOW_SAFETY_RATIO = 0.9, loadFtsRowsStatements, existingHashStatements, existingHashByProjectStatements, deleteByCompartmentStatements, insertEmbeddingStatements, distinctModelStatements, clearProjectStatements, clearProjectModelStatements, searchRowsStatements, searchRowsByModelStatements, backfillCandidateStatements, sessionBackfillCandidateStatements;
165571
165579
  var init_compartment_chunk_embedding = __esm(() => {
165572
165580
  init_read_session_formatting();
165573
165581
  loadFtsRowsStatements = new WeakMap;
@@ -166035,6 +166043,13 @@ var init_embedding_ssrf = __esm(() => {
166035
166043
  function normalizeEndpoint3(endpoint) {
166036
166044
  return endpoint?.trim().replace(/\/+$/, "") ?? "";
166037
166045
  }
166046
+ function embeddingModelsMatch(served, requested) {
166047
+ const a = served.trim().toLowerCase();
166048
+ const b = requested.trim().toLowerCase();
166049
+ if (a.length === 0 || b.length === 0)
166050
+ return true;
166051
+ return a === b || a.includes(b) || b.includes(a);
166052
+ }
166038
166053
 
166039
166054
  class OpenAICompatibleEmbeddingProvider {
166040
166055
  modelId;
@@ -166048,6 +166063,7 @@ class OpenAICompatibleEmbeddingProvider {
166048
166063
  failureTimes = [];
166049
166064
  circuitOpenUntil = 0;
166050
166065
  openLogged = false;
166066
+ modelMismatchLogged = false;
166051
166067
  halfOpenProbeInFlight = false;
166052
166068
  constructor(options) {
166053
166069
  this.endpoint = normalizeEndpoint3(options.endpoint);
@@ -166146,6 +166162,15 @@ class OpenAICompatibleEmbeddingProvider {
166146
166162
  this.recordFailure(isProbe);
166147
166163
  return Array.from({ length: texts.length }, () => null);
166148
166164
  }
166165
+ const servedModel = typeof body.model === "string" ? body.model : "";
166166
+ if (this.model && servedModel && !embeddingModelsMatch(servedModel, this.model)) {
166167
+ if (!this.modelMismatchLogged) {
166168
+ log(`[magic-context] embedding endpoint served a DIFFERENT model than requested — refusing the substituted vectors (they have the wrong dimensions/space). requested="${this.model}" served="${servedModel}". The endpoint likely substituted a loaded model; load/select "${this.model}" on the endpoint, or set embedding.model to the served model.`);
166169
+ this.modelMismatchLogged = true;
166170
+ }
166171
+ this.recordFailure(isProbe);
166172
+ return Array.from({ length: texts.length }, () => null);
166173
+ }
166149
166174
  const items = Array.isArray(body.data) ? body.data : [];
166150
166175
  const results = Array.from({ length: texts.length }, (_, index) => {
166151
166176
  const embedding = items[index]?.embedding;
@@ -166632,7 +166657,7 @@ function getChunkEmbeddingModelId(config2, providerIdentity) {
166632
166657
  }
166633
166658
  const chunkIdentity = {
166634
166659
  providerIdentity,
166635
- chunkerVersion: 1,
166660
+ chunkerVersion: 2,
166636
166661
  maxInputTokens: normalizeCompartmentChunkMaxInputTokens("max_input_tokens" in config2 ? config2.max_input_tokens : undefined),
166637
166662
  truncate: config2.provider === "openai-compatible" ? config2.truncate ?? "" : ""
166638
166663
  };
@@ -169271,7 +169296,7 @@ ${prepared.block}
169271
169296
  if (!firstMessage || !textPart || isDroppedPlaceholder(textPart.text)) {
169272
169297
  messages.unshift({
169273
169298
  info: { role: "user", sessionID: sessionId },
169274
- parts: [{ type: "text", text: historyBlock }]
169299
+ parts: [{ type: "text", text: historyBlock, synthetic: true }]
169275
169300
  });
169276
169301
  } else {
169277
169302
  textPart.text = `${historyBlock}
@@ -170161,10 +170186,16 @@ function softRefreshCachedM1(options) {
170161
170186
  function prependM0M1Messages(sessionId, messages, m0Text, m1Text) {
170162
170187
  messages.unshift({
170163
170188
  info: { role: "user", sessionID: sessionId },
170164
- parts: [{ type: "text", text: m0Text.length > 0 ? m0Text : M0_EMPTY_BODY }]
170189
+ parts: [
170190
+ {
170191
+ type: "text",
170192
+ text: m0Text.length > 0 ? m0Text : M0_EMPTY_BODY,
170193
+ synthetic: true
170194
+ }
170195
+ ]
170165
170196
  }, {
170166
170197
  info: { role: "user", sessionID: sessionId },
170167
- parts: [{ type: "text", text: m1Text }]
170198
+ parts: [{ type: "text", text: m1Text, synthetic: true }]
170168
170199
  });
170169
170200
  }
170170
170201
  function renderFreshM0NonPersisted(options) {
@@ -177054,6 +177085,11 @@ async function runManagedRecomp(ctx, sessionId, options) {
177054
177085
  try {
177055
177086
  const message = await executeContextRecomp(buildRecompDeps(ctx, sessionId), options);
177056
177087
  const terminalPhase = isRecompSkip(message) ? "skipped" : isRecompFailure(message) ? "failed" : "done";
177088
+ if (terminalPhase === "done") {
177089
+ try {
177090
+ clearEmergencyRecovery(ctx.db, sessionId);
177091
+ } catch {}
177092
+ }
177057
177093
  setRecompTerminal(ctx.liveSessionState, sessionId, terminalPhase, extractRecompReason(message));
177058
177094
  return message;
177059
177095
  } catch (error51) {
@@ -177152,6 +177188,7 @@ var RECOMP_DONE_GRACE_MS = 30000;
177152
177188
  var init_recomp_orchestrator = __esm(async () => {
177153
177189
  init_compartment_storage();
177154
177190
  init_project_identity2();
177191
+ init_storage_meta_persisted();
177155
177192
  await __promiseAll([
177156
177193
  init_memory_migration(),
177157
177194
  init_compartment_runner()
@@ -182686,8 +182723,8 @@ var CHANNEL1_SENTINEL = "<system-reminder>";
182686
182723
  var TOKENS_PER_BYTE = 0.25;
182687
182724
  var CHANNEL1_FLOOR_TOKENS = 1e4;
182688
182725
  var CHANNEL1_REFIRE_FLOOR_TOKENS = 1e4;
182689
- function channel1RefireTokens(historyBudgetTokens) {
182690
- const scaled = Math.round(0.05 * Math.max(0, historyBudgetTokens));
182726
+ function channel1RefireTokens(workingWindowTokens) {
182727
+ const scaled = Math.round(0.05 * Math.max(0, workingWindowTokens));
182691
182728
  return Math.max(CHANNEL1_REFIRE_FLOOR_TOKENS, scaled);
182692
182729
  }
182693
182730
  var S_GENTLE = 0.2;
@@ -182757,7 +182794,7 @@ function computeTailTokenEstimate(messages) {
182757
182794
  };
182758
182795
  }
182759
182796
  function decideChannel1(input) {
182760
- const { undroppedTokens, pressure, historyBudgetTokens, hasRecentReduce } = input;
182797
+ const { undroppedTokens, pressure, workingWindowTokens, hasRecentReduce } = input;
182761
182798
  const resetCycle = hasRecentReduce || undroppedTokens < input.lastNudgeUndropped;
182762
182799
  const lastNudge = resetCycle ? 0 : input.lastNudgeUndropped;
182763
182800
  const lastLevel = resetCycle ? "" : input.lastNudgeLevel;
@@ -182772,7 +182809,7 @@ function decideChannel1(input) {
182772
182809
  return quiet();
182773
182810
  if (undroppedTokens < CHANNEL1_FLOOR_TOKENS)
182774
182811
  return quiet();
182775
- const budget = historyBudgetTokens > 0 ? historyBudgetTokens : undroppedTokens || 1;
182812
+ const budget = workingWindowTokens > 0 ? workingWindowTokens : undroppedTokens || 1;
182776
182813
  const severity = undroppedTokens / budget * pressure;
182777
182814
  if (severity < S_GENTLE)
182778
182815
  return quiet();
@@ -182784,7 +182821,7 @@ function decideChannel1(input) {
182784
182821
  else
182785
182822
  level = "gentle";
182786
182823
  if (lastLevel === "") {
182787
- if (undroppedTokens < lastNudge + channel1RefireTokens(historyBudgetTokens)) {
182824
+ if (undroppedTokens < lastNudge + channel1RefireTokens(workingWindowTokens)) {
182788
182825
  return quiet();
182789
182826
  }
182790
182827
  } else if (LEVEL_RANK[level] <= LEVEL_RANK[lastLevel]) {
@@ -182829,13 +182866,13 @@ function buildChannel1Reminder(level, undroppedTokens) {
182829
182866
  let body;
182830
182867
  switch (level) {
182831
182868
  case "gentle":
182832
- body = `You have ~${amount} tokens of tool output you have not reduced. ` + `Once you are done with earlier outputs, drop them with ctx_reduce to keep context lean.`;
182869
+ body = `You have ~${amount} tokens of tool output you have not reduced. ` + `When you are done with earlier outputs, dropping them with ctx_reduce keeps context lean.`;
182833
182870
  break;
182834
182871
  case "firm":
182835
- body = `~${amount} tokens of unreduced tool output is accumulating. ` + `Drop what you have already processed with ctx_reduce before continuing.`;
182872
+ body = `~${amount} tokens of unreduced tool output has built up. ` + `At your next natural stopping point, consider dropping what you have already processed with ctx_reduce.`;
182836
182873
  break;
182837
182874
  case "urgent":
182838
- body = `~${amount} tokens of unreduced tool output remain. ` + `A large span of this session will be comparted soon; drop spent outputs with ctx_reduce first so the archived span is the part that matters.`;
182875
+ body = `~${amount} tokens of unreduced tool output remain, and a large span of this session will be comparted before long. ` + `Consider dropping spent outputs with ctx_reduce so the archived span is the part that matters.`;
182839
182876
  break;
182840
182877
  }
182841
182878
  return `
@@ -184423,8 +184460,55 @@ function appendReminderToUserMessage(message, reminder) {
184423
184460
  }
184424
184461
 
184425
184462
  // src/hooks/magic-context/apply-operations.ts
184426
- init_tag_part_guards();
184427
184463
  await init_storage();
184464
+
184465
+ // src/hooks/magic-context/system-injection-stripper.ts
184466
+ var SYSTEM_INJECTION_MARKERS = [
184467
+ "<!-- OMO_INTERNAL_INITIATOR -->",
184468
+ "[SYSTEM DIRECTIVE: MAGIC-CONTEXT",
184469
+ "[SYSTEM DIRECTIVE: OH-MY-OPENCODE",
184470
+ "[Category+Skill Reminder]",
184471
+ "[EDIT ERROR - IMMEDIATE ACTION REQUIRED]",
184472
+ "[task CALL FAILED - IMMEDIATE RETRY REQUIRED]",
184473
+ "[EMERGENCY CONTEXT WINDOW WARNING]",
184474
+ "Unstable background agent appears idle",
184475
+ "**THE SUBAGENT JUST CLAIMED THIS TASK IS DONE."
184476
+ ];
184477
+ var SYSTEM_REMINDER_REGEX = /<system-reminder>[\s\S]*?<\/system-reminder>/gi;
184478
+ var OMO_MARKER_REGEX = /<!-- OMO_INTERNAL_INITIATOR -->/g;
184479
+ function stripSystemInjection(text) {
184480
+ let hasInjection = false;
184481
+ for (const marker of SYSTEM_INJECTION_MARKERS) {
184482
+ if (text.includes(marker)) {
184483
+ hasInjection = true;
184484
+ break;
184485
+ }
184486
+ }
184487
+ if (SYSTEM_REMINDER_REGEX.test(text))
184488
+ hasInjection = true;
184489
+ SYSTEM_REMINDER_REGEX.lastIndex = 0;
184490
+ if (!hasInjection)
184491
+ return null;
184492
+ let cleaned = text;
184493
+ cleaned = cleaned.replace(SYSTEM_REMINDER_REGEX, "");
184494
+ cleaned = cleaned.replace(OMO_MARKER_REGEX, "");
184495
+ cleaned = cleaned.replace(/\[SYSTEM DIRECTIVE: OH-MY-(?:OPENCODE|CLAUDE)[^\]]*\][\s\S]*?(?=\n\n(?!\s*[-*])|$)/g, "");
184496
+ for (const marker of SYSTEM_INJECTION_MARKERS) {
184497
+ if (marker.startsWith("<!-- ") || marker.startsWith("[SYSTEM DIRECTIVE"))
184498
+ continue;
184499
+ const idx = cleaned.indexOf(marker);
184500
+ if (idx === -1)
184501
+ continue;
184502
+ const blockEnd = cleaned.indexOf(`
184503
+
184504
+ `, idx + marker.length);
184505
+ cleaned = blockEnd !== -1 ? cleaned.slice(0, idx) + cleaned.slice(blockEnd) : cleaned.slice(0, idx);
184506
+ }
184507
+ return cleaned.trim();
184508
+ }
184509
+
184510
+ // src/hooks/magic-context/apply-operations.ts
184511
+ init_tag_part_guards();
184428
184512
  var USER_DROP_PREVIEW_CHARS = 250;
184429
184513
  var RECENT_TOOL_SKELETON_WINDOW = 20;
184430
184514
  function buildReplacementContent(tagId, target) {
@@ -184433,6 +184517,10 @@ function buildReplacementContent(tagId, target) {
184433
184517
  return `[dropped §${tagId}§]`;
184434
184518
  }
184435
184519
  const currentContent = target.getContent?.() ?? "";
184520
+ const strippedInjection = stripSystemInjection(currentContent);
184521
+ if (strippedInjection !== null && stripTagPrefix(strippedInjection).trim().length === 0) {
184522
+ return `[dropped §${tagId}§]`;
184523
+ }
184436
184524
  const originalText = stripTagPrefix(currentContent);
184437
184525
  if (originalText.length <= USER_DROP_PREVIEW_CHARS) {
184438
184526
  return `[truncated §${tagId}§]
@@ -186139,51 +186227,6 @@ function planEmergencyDrop(input) {
186139
186227
  };
186140
186228
  }
186141
186229
 
186142
- // src/hooks/magic-context/system-injection-stripper.ts
186143
- var SYSTEM_INJECTION_MARKERS = [
186144
- "<!-- OMO_INTERNAL_INITIATOR -->",
186145
- "[SYSTEM DIRECTIVE: MAGIC-CONTEXT",
186146
- "[SYSTEM DIRECTIVE: OH-MY-OPENCODE",
186147
- "[Category+Skill Reminder]",
186148
- "[EDIT ERROR - IMMEDIATE ACTION REQUIRED]",
186149
- "[task CALL FAILED - IMMEDIATE RETRY REQUIRED]",
186150
- "[EMERGENCY CONTEXT WINDOW WARNING]",
186151
- "Unstable background agent appears idle",
186152
- "**THE SUBAGENT JUST CLAIMED THIS TASK IS DONE."
186153
- ];
186154
- var SYSTEM_REMINDER_REGEX = /<system-reminder>[\s\S]*?<\/system-reminder>/gi;
186155
- var OMO_MARKER_REGEX = /<!-- OMO_INTERNAL_INITIATOR -->/g;
186156
- function stripSystemInjection(text) {
186157
- let hasInjection = false;
186158
- for (const marker of SYSTEM_INJECTION_MARKERS) {
186159
- if (text.includes(marker)) {
186160
- hasInjection = true;
186161
- break;
186162
- }
186163
- }
186164
- if (SYSTEM_REMINDER_REGEX.test(text))
186165
- hasInjection = true;
186166
- SYSTEM_REMINDER_REGEX.lastIndex = 0;
186167
- if (!hasInjection)
186168
- return null;
186169
- let cleaned = text;
186170
- cleaned = cleaned.replace(SYSTEM_REMINDER_REGEX, "");
186171
- cleaned = cleaned.replace(OMO_MARKER_REGEX, "");
186172
- cleaned = cleaned.replace(/\[SYSTEM DIRECTIVE: OH-MY-(?:OPENCODE|CLAUDE)[^\]]*\][\s\S]*?(?=\n\n(?!\s*[-*])|$)/g, "");
186173
- for (const marker of SYSTEM_INJECTION_MARKERS) {
186174
- if (marker.startsWith("<!-- ") || marker.startsWith("[SYSTEM DIRECTIVE"))
186175
- continue;
186176
- const idx = cleaned.indexOf(marker);
186177
- if (idx === -1)
186178
- continue;
186179
- const blockEnd = cleaned.indexOf(`
186180
-
186181
- `, idx + marker.length);
186182
- cleaned = blockEnd !== -1 ? cleaned.slice(0, idx) + cleaned.slice(blockEnd) : cleaned.slice(0, idx);
186183
- }
186184
- return cleaned.trim();
186185
- }
186186
-
186187
186230
  // src/hooks/magic-context/heuristic-cleanup.ts
186188
186231
  init_tag_part_guards();
186189
186232
  var DEDUP_SAFE_TOOLS = new Set([
@@ -187647,7 +187690,7 @@ Historian previously failed ${historianFailureState.failureCount} time(s), so Ma
187647
187690
  let tailToolTokens;
187648
187691
  let liveTailTokens;
187649
187692
  try {
187650
- const agg = getActiveTagTokenAggregate(db, sessionId);
187693
+ const agg = getActiveTagTokenAggregate(db, sessionId, deps.protectedTags);
187651
187694
  tailToolTokens = agg.toolOutput;
187652
187695
  liveTailTokens = agg.conversation + agg.toolCall;
187653
187696
  } catch {
@@ -188282,10 +188325,11 @@ function maybeInjectChannel1Nudge(args, sessionId, tool, output) {
188282
188325
  contextLimit: state.contextLimit,
188283
188326
  executeThresholdPercentage: state.executeThresholdPercentage
188284
188327
  });
188328
+ const workingWindowTokens = Math.round(state.contextLimit * state.executeThresholdPercentage / 100);
188285
188329
  const decision = decideChannel1({
188286
188330
  undroppedTokens,
188287
188331
  pressure,
188288
- historyBudgetTokens: state.historyBudgetTokens,
188332
+ workingWindowTokens,
188289
188333
  lastNudgeUndropped: getLastNudgeUndropped(args.db, sessionId),
188290
188334
  lastNudgeLevel: getLastNudgeLevel(args.db, sessionId),
188291
188335
  hasRecentReduce: false
@@ -0,0 +1,32 @@
1
+ export declare const TUI_PREFS_FILE_ENV = "OPENCODE_TUI_PREFERENCES_FILE";
2
+ export declare function getTuiPreferencesFile(): string;
3
+ export declare function readTuiPreferencesFile(): Promise<Record<string, unknown>>;
4
+ export declare function readTuiPreferencesFileSync(): Record<string, unknown>;
5
+ export declare const PLUGIN_KEY = "magic-context";
6
+ export declare const DEFAULT_SLOT_ORDER = 200;
7
+ export interface MagicContextTuiPrefs {
8
+ forceToTop: boolean;
9
+ order: number;
10
+ startCollapsed: boolean;
11
+ rememberCollapsed: boolean;
12
+ collapsed: boolean | null;
13
+ header: {
14
+ label: string;
15
+ };
16
+ sections: {
17
+ historian: boolean;
18
+ memory: boolean;
19
+ status: boolean;
20
+ dreamer: boolean;
21
+ stats: boolean;
22
+ };
23
+ }
24
+ export type TuiSections = MagicContextTuiPrefs["sections"];
25
+ export declare const DEFAULT_PREFS: MagicContextTuiPrefs;
26
+ export declare function resolveMagicContextPrefs(root: Record<string, unknown>): MagicContextTuiPrefs;
27
+ export declare function computeEffectiveOrder(root: Record<string, unknown>, pluginKey: string, defaultOrder: number): number;
28
+ type JsonValue = string | number | boolean | null;
29
+ export declare function queueTuiPreferenceUpdate(pluginKey: string, path: string[], value: JsonValue): Promise<void>;
30
+ export declare function watchTuiPreferences(onChange: () => void): () => void;
31
+ export {};
32
+ //# sourceMappingURL=tui-preferences.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tui-preferences.d.ts","sourceRoot":"","sources":["../../src/shared/tui-preferences.ts"],"names":[],"mappings":"AAqBA,eAAO,MAAM,kBAAkB,kCAAkC,CAAC;AAGlE,wBAAgB,qBAAqB,IAAI,MAAM,CAO9C;AAQD,wBAAsB,sBAAsB,IAAI,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAS/E;AAMD,wBAAgB,0BAA0B,IAAI,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CASpE;AAED,eAAO,MAAM,UAAU,kBAAkB,CAAC;AAC1C,eAAO,MAAM,kBAAkB,MAAM,CAAC;AAEtC,MAAM,WAAW,oBAAoB;IACjC,UAAU,EAAE,OAAO,CAAC;IACpB,KAAK,EAAE,MAAM,CAAC;IACd,cAAc,EAAE,OAAO,CAAC;IACxB,iBAAiB,EAAE,OAAO,CAAC;IAE3B,SAAS,EAAE,OAAO,GAAG,IAAI,CAAC;IAC1B,MAAM,EAAE;QACJ,KAAK,EAAE,MAAM,CAAC;KACjB,CAAC;IACF,QAAQ,EAAE;QACN,SAAS,EAAE,OAAO,CAAC;QACnB,MAAM,EAAE,OAAO,CAAC;QAChB,MAAM,EAAE,OAAO,CAAC;QAChB,OAAO,EAAE,OAAO,CAAC;QACjB,KAAK,EAAE,OAAO,CAAC;KAClB,CAAC;CACL;AAED,MAAM,MAAM,WAAW,GAAG,oBAAoB,CAAC,UAAU,CAAC,CAAC;AAE3D,eAAO,MAAM,aAAa,EAAE,oBAc3B,CAAC;AAmBF,wBAAgB,wBAAwB,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,oBAAoB,CAyB5F;AAgBD,wBAAgB,qBAAqB,CACjC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC7B,SAAS,EAAE,MAAM,EACjB,YAAY,EAAE,MAAM,GACrB,MAAM,CAOR;AASD,KAAK,SAAS,GAAG,MAAM,GAAG,MAAM,GAAG,OAAO,GAAG,IAAI,CAAC;AA0DlD,wBAAgB,wBAAwB,CACpC,SAAS,EAAE,MAAM,EACjB,IAAI,EAAE,MAAM,EAAE,EACd,KAAK,EAAE,SAAS,GACjB,OAAO,CAAC,IAAI,CAAC,CAGf;AAeD,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,MAAM,IAAI,GAAG,MAAM,IAAI,CAuCpE"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wolfx/opencode-magic-context",
3
- "version": "0.24.0",
3
+ "version": "0.24.1",
4
4
  "type": "module",
5
5
  "description": "OpenCode plugin for Magic Context — cross-session memory and context management",
6
6
  "main": "dist/index.js",
@@ -0,0 +1,210 @@
1
+ import { afterEach, beforeEach, describe, expect, test } from "bun:test";
2
+ import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+ import { parse } from "comment-json";
6
+ import {
7
+ computeEffectiveOrder,
8
+ DEFAULT_PREFS,
9
+ DEFAULT_SLOT_ORDER,
10
+ getTuiPreferencesFile,
11
+ PLUGIN_KEY,
12
+ queueTuiPreferenceUpdate,
13
+ readTuiPreferencesFile,
14
+ resolveMagicContextPrefs,
15
+ TUI_PREFS_FILE_ENV,
16
+ } from "./tui-preferences";
17
+
18
+ let dir: string;
19
+ let file: string;
20
+ const savedEnv: Record<string, string | undefined> = {};
21
+ const ENV_KEYS = [TUI_PREFS_FILE_ENV, "OPENCODE_CONFIG_DIR", "XDG_CONFIG_HOME"];
22
+
23
+ beforeEach(async () => {
24
+ for (const key of ENV_KEYS) savedEnv[key] = process.env[key];
25
+ dir = await mkdtemp(join(tmpdir(), "mc-tui-prefs-test-"));
26
+ file = join(dir, "tui-preferences.jsonc");
27
+ process.env[TUI_PREFS_FILE_ENV] = file;
28
+ });
29
+
30
+ afterEach(async () => {
31
+ for (const key of ENV_KEYS) {
32
+ if (savedEnv[key] === undefined) delete process.env[key];
33
+ else process.env[key] = savedEnv[key];
34
+ }
35
+ await rm(dir, { recursive: true, force: true });
36
+ });
37
+
38
+ describe("getTuiPreferencesFile", () => {
39
+ test("env override wins", () => {
40
+ expect(getTuiPreferencesFile()).toBe(file);
41
+ });
42
+
43
+ test("falls back to OPENCODE_CONFIG_DIR then XDG then ~/.config", () => {
44
+ delete process.env[TUI_PREFS_FILE_ENV];
45
+ process.env.OPENCODE_CONFIG_DIR = "/tmp/cfgdir";
46
+ expect(getTuiPreferencesFile()).toBe("/tmp/cfgdir/tui-preferences.jsonc");
47
+ delete process.env.OPENCODE_CONFIG_DIR;
48
+ process.env.XDG_CONFIG_HOME = "/tmp/xdg";
49
+ expect(getTuiPreferencesFile()).toBe("/tmp/xdg/opencode/tui-preferences.jsonc");
50
+ });
51
+ });
52
+
53
+ describe("readTuiPreferencesFile (tolerant)", () => {
54
+ test("missing file → {}", async () => {
55
+ expect(await readTuiPreferencesFile()).toEqual({});
56
+ });
57
+
58
+ test("malformed JSON → {}", async () => {
59
+ await writeFile(file, "{ this is not json ", "utf8");
60
+ expect(await readTuiPreferencesFile()).toEqual({});
61
+ });
62
+
63
+ test("non-object root → {}", async () => {
64
+ await writeFile(file, "[1, 2, 3]", "utf8");
65
+ expect(await readTuiPreferencesFile()).toEqual({});
66
+ });
67
+
68
+ test("jsonc with comments + trailing comma parses", async () => {
69
+ await writeFile(
70
+ file,
71
+ `{
72
+ // a comment
73
+ "magic-context": { "order": 205, },
74
+ }`,
75
+ "utf8",
76
+ );
77
+ const root = await readTuiPreferencesFile();
78
+ expect(resolveMagicContextPrefs(root).order).toBe(205);
79
+ });
80
+ });
81
+
82
+ describe("resolveMagicContextPrefs (per-key validation)", () => {
83
+ test("missing key → full defaults clone", () => {
84
+ expect(resolveMagicContextPrefs({})).toEqual(DEFAULT_PREFS);
85
+ // clone, not the shared object
86
+ expect(resolveMagicContextPrefs({})).not.toBe(DEFAULT_PREFS);
87
+ });
88
+
89
+ test("one bad value never poisons the rest", () => {
90
+ const prefs = resolveMagicContextPrefs({
91
+ "magic-context": {
92
+ order: "nope",
93
+ rememberCollapsed: 1,
94
+ collapsed: true,
95
+ sections: { historian: false, memory: "bad" },
96
+ },
97
+ });
98
+ expect(prefs.order).toBe(DEFAULT_SLOT_ORDER); // bad → default
99
+ expect(prefs.rememberCollapsed).toBe(true); // bad → default true
100
+ expect(prefs.collapsed).toBe(true); // valid bool preserved
101
+ expect(prefs.sections.historian).toBe(false); // valid bool preserved
102
+ expect(prefs.sections.memory).toBe(true); // bad → default true
103
+ });
104
+
105
+ test("order clamps to -10000..10000", () => {
106
+ expect(resolveMagicContextPrefs({ "magic-context": { order: 99999 } }).order).toBe(10000);
107
+ expect(resolveMagicContextPrefs({ "magic-context": { order: -99999 } }).order).toBe(-10000);
108
+ });
109
+
110
+ test("collapsed non-boolean → null (seed from startCollapsed)", () => {
111
+ expect(resolveMagicContextPrefs({ "magic-context": {} }).collapsed).toBeNull();
112
+ });
113
+
114
+ test("header label clamps length, empty → default", () => {
115
+ expect(
116
+ resolveMagicContextPrefs({ "magic-context": { header: { label: "" } } }).header.label,
117
+ ).toBe(DEFAULT_PREFS.header.label);
118
+ expect(
119
+ resolveMagicContextPrefs({
120
+ "magic-context": { header: { label: "x".repeat(50) } },
121
+ }).header.label.length,
122
+ ).toBe(24);
123
+ });
124
+ });
125
+
126
+ describe("computeEffectiveOrder (cross-plugin convention)", () => {
127
+ test("default when key missing", () => {
128
+ expect(computeEffectiveOrder({}, PLUGIN_KEY, DEFAULT_SLOT_ORDER)).toBe(DEFAULT_SLOT_ORDER);
129
+ });
130
+
131
+ test("explicit order clamped", () => {
132
+ expect(computeEffectiveOrder({ "magic-context": { order: 250 } }, PLUGIN_KEY, 200)).toBe(
133
+ 250,
134
+ );
135
+ });
136
+
137
+ test("forceToTop sorts below FORCE_TOP_BASE by key position", () => {
138
+ const root = { aft: { forceToTop: true }, "magic-context": { forceToTop: true } };
139
+ expect(computeEffectiveOrder(root, "aft", 200)).toBe(-100000 + 0);
140
+ expect(computeEffectiveOrder(root, "magic-context", 200)).toBe(-100000 + 1);
141
+ // forced always beats any manual order (clamped band is strictly above)
142
+ expect(computeEffectiveOrder(root, "aft", 200)).toBeLessThan(-10000);
143
+ });
144
+ });
145
+
146
+ describe("write path — comment-json full round-trip", () => {
147
+ test("persists a nested key and reads back", async () => {
148
+ await queueTuiPreferenceUpdate(PLUGIN_KEY, ["collapsed"], true);
149
+ const prefs = resolveMagicContextPrefs(await readTuiPreferencesFile());
150
+ expect(prefs.collapsed).toBe(true);
151
+ });
152
+
153
+ test("seeds the file from the template when absent", async () => {
154
+ await queueTuiPreferenceUpdate(PLUGIN_KEY, ["order"], 205);
155
+ const text = await readFile(file, "utf8");
156
+ expect(text).toContain("Shared preferences for OpenCode TUI plugins");
157
+ expect(resolveMagicContextPrefs(await readTuiPreferencesFile()).order).toBe(205);
158
+ });
159
+
160
+ test("INTEROP: a sibling plugin's values AND comments survive MC writing only its key", async () => {
161
+ // A shared file owned partly by anthropic-auth, with comments and an
162
+ // appearance block MC knows nothing about. MC must touch ONLY its key.
163
+ await writeFile(
164
+ file,
165
+ `{
166
+ // anthropic-auth section — DO NOT lose this BLOCK comment
167
+ "anthropic-auth": {
168
+ "order": 160,
169
+ "header": { "label": "CLAUDE" },
170
+ // bar appearance knobs MC has no schema for
171
+ "appearance": { "barWidth": 10, "barFilledChar": "#" },
172
+ "pollMs": 2000 // INLINE trailing comment — must survive too
173
+ },
174
+ "magic-context": { "order": 200 }
175
+ }
176
+ `,
177
+ "utf8",
178
+ );
179
+
180
+ await queueTuiPreferenceUpdate(PLUGIN_KEY, ["collapsed"], true);
181
+
182
+ const text = await readFile(file, "utf8");
183
+ // sibling comments preserved — BOTH block and inline trailing
184
+ // (comment-json round-trips both faithfully; enforce the guarantee).
185
+ expect(text).toContain("anthropic-auth section — DO NOT lose this BLOCK comment");
186
+ expect(text).toContain("bar appearance knobs MC has no schema for");
187
+ expect(text).toContain("INLINE trailing comment — must survive too");
188
+
189
+ // sibling VALUES intact (incl. nested keys MC has no schema for)
190
+ const root = parse(text) as Record<string, Record<string, unknown>>;
191
+ const aa = root["anthropic-auth"] as Record<string, unknown>;
192
+ expect(aa.order).toBe(160);
193
+ expect((aa.header as Record<string, unknown>).label).toBe("CLAUDE");
194
+ const appearance = aa.appearance as Record<string, unknown>;
195
+ expect(appearance.barWidth).toBe(10);
196
+ expect(appearance.barFilledChar).toBe("#");
197
+
198
+ // MC's own change landed
199
+ expect(resolveMagicContextPrefs(root).collapsed).toBe(true);
200
+ expect(resolveMagicContextPrefs(root).order).toBe(200);
201
+ });
202
+
203
+ test("malformed existing file → write is a no-op, sibling content untouched", async () => {
204
+ const broken = `{ "anthropic-auth": { "order": 160 } broken `;
205
+ await writeFile(file, broken, "utf8");
206
+ await queueTuiPreferenceUpdate(PLUGIN_KEY, ["collapsed"], true);
207
+ // unchanged — we never clobber a file we can't safely parse
208
+ expect(await readFile(file, "utf8")).toBe(broken);
209
+ });
210
+ });