Skip to content

Commit 32c4a5c

Browse files
committed
feat(plugin): handle message.removed for revert support
When users revert messages in OpenCode, magic-context now properly invalidates affected state: tags, reasoning watermark, nudge anchors, note nudge state, sticky reminders, stripped placeholder IDs, and FTS index entries. Compartments and facts are preserved since they represent compressed history that should survive reverts. Also adds monochrome tray icon for macOS menu bar.
1 parent 3baa356 commit 32c4a5c

File tree

13 files changed

+600
-6
lines changed

13 files changed

+600
-6
lines changed
564 Bytes
Loading

packages/dashboard/src-tauri/tauri.conf.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
}
2525
],
2626
"trayIcon": {
27-
"iconPath": "icons/icon.png",
27+
"iconPath": "icons/tray-icon.png",
2828
"iconAsTemplate": true
2929
},
3030
"security": {

packages/plugin/src/features/magic-context/message-index.ts

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ const insertMessageStatements = new WeakMap<Database, PreparedStatement>();
1818
const upsertIndexStatements = new WeakMap<Database, PreparedStatement>();
1919
const deleteFtsStatements = new WeakMap<Database, PreparedStatement>();
2020
const deleteIndexStatements = new WeakMap<Database, PreparedStatement>();
21+
const countIndexedMessageStatements = new WeakMap<Database, PreparedStatement>();
22+
const deleteIndexedMessageStatements = new WeakMap<Database, PreparedStatement>();
2123

2224
function normalizeIndexText(text: string): string {
2325
return text.replace(/\s+/g, " ").trim();
@@ -74,11 +76,48 @@ function getDeleteIndexStatement(db: Database): PreparedStatement {
7476
return stmt;
7577
}
7678

79+
function getCountIndexedMessageStatement(db: Database): PreparedStatement {
80+
let stmt = countIndexedMessageStatements.get(db);
81+
if (!stmt) {
82+
stmt = db.prepare(
83+
"SELECT COUNT(*) AS count FROM message_history_fts WHERE session_id = ? AND message_id = ?",
84+
);
85+
countIndexedMessageStatements.set(db, stmt);
86+
}
87+
return stmt;
88+
}
89+
90+
function getDeleteIndexedMessageStatement(db: Database): PreparedStatement {
91+
let stmt = deleteIndexedMessageStatements.get(db);
92+
if (!stmt) {
93+
stmt = db.prepare(
94+
"DELETE FROM message_history_fts WHERE session_id = ? AND message_id = ?",
95+
);
96+
deleteIndexedMessageStatements.set(db, stmt);
97+
}
98+
return stmt;
99+
}
100+
101+
interface CountRow {
102+
count: number;
103+
}
104+
77105
function getLastIndexedOrdinal(db: Database, sessionId: string): number {
78106
const row = getLastIndexedStatement(db).get(sessionId) as MessageHistoryIndexRow | null;
79107
return typeof row?.last_indexed_ordinal === "number" ? row.last_indexed_ordinal : 0;
80108
}
81109

110+
export function deleteIndexedMessage(db: Database, sessionId: string, messageId: string): number {
111+
const row = getCountIndexedMessageStatement(db).get(sessionId, messageId) as CountRow | null;
112+
const count = typeof row?.count === "number" ? row.count : 0;
113+
if (count > 0) {
114+
getDeleteIndexedMessageStatement(db).run(sessionId, messageId);
115+
}
116+
117+
getDeleteIndexStatement(db).run(sessionId);
118+
return count;
119+
}
120+
82121
export function clearIndexedMessages(db: Database, sessionId: string): void {
83122
db.transaction(() => {
84123
getDeleteFtsStatement(db).run(sessionId);

packages/plugin/src/features/magic-context/storage-meta-persisted.ts

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,10 @@ interface PersistedUsageRow {
88
last_response_time: number;
99
}
1010

11+
interface PersistedReasoningWatermarkRow {
12+
cleared_reasoning_through_tag: number;
13+
}
14+
1115
interface PersistedNudgePlacementRow {
1216
nudge_anchor_message_id: string;
1317
nudge_anchor_text: string;
@@ -47,6 +51,12 @@ function isPersistedUsageRow(row: unknown): row is PersistedUsageRow {
4751
);
4852
}
4953

54+
function isPersistedReasoningWatermarkRow(row: unknown): row is PersistedReasoningWatermarkRow {
55+
if (row === null || typeof row !== "object") return false;
56+
const r = row as Record<string, unknown>;
57+
return typeof r.cleared_reasoning_through_tag === "number";
58+
}
59+
5060
function isPersistedNudgePlacementRow(row: unknown): row is PersistedNudgePlacementRow {
5161
if (row === null || typeof row !== "object") return false;
5262
const r = row as Record<string, unknown>;
@@ -108,6 +118,25 @@ export function loadPersistedUsage(
108118
};
109119
}
110120

121+
export function getPersistedReasoningWatermark(db: Database, sessionId: string): number {
122+
const result = db
123+
.prepare("SELECT cleared_reasoning_through_tag FROM session_meta WHERE session_id = ?")
124+
.get(sessionId);
125+
126+
return isPersistedReasoningWatermarkRow(result) ? result.cleared_reasoning_through_tag : 0;
127+
}
128+
129+
export function setPersistedReasoningWatermark(
130+
db: Database,
131+
sessionId: string,
132+
tagNumber: number,
133+
): void {
134+
ensureSessionMetaRow(db, sessionId);
135+
db.prepare(
136+
"UPDATE session_meta SET cleared_reasoning_through_tag = ? WHERE session_id = ?",
137+
).run(tagNumber, sessionId);
138+
}
139+
111140
export function getPersistedNudgePlacement(
112141
db: Database,
113142
sessionId: string,
@@ -296,3 +325,17 @@ export function setStrippedPlaceholderIds(db: Database, sessionId: string, ids:
296325
sessionId,
297326
);
298327
}
328+
329+
export function removeStrippedPlaceholderId(
330+
db: Database,
331+
sessionId: string,
332+
messageId: string,
333+
): boolean {
334+
const ids = getStrippedPlaceholderIds(db, sessionId);
335+
if (!ids.delete(messageId)) {
336+
return false;
337+
}
338+
339+
setStrippedPlaceholderIds(db, sessionId, ids);
340+
return true;
341+
}

packages/plugin/src/features/magic-context/storage-meta.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,16 @@
11
export {
2+
clearPersistedNoteNudge,
23
clearPersistedNudgePlacement,
34
clearPersistedStickyTurnReminder,
5+
getPersistedNoteNudge,
46
getPersistedNudgePlacement,
7+
getPersistedReasoningWatermark,
58
getPersistedStickyTurnReminder,
69
getStrippedPlaceholderIds,
710
loadPersistedUsage,
11+
removeStrippedPlaceholderId,
812
setPersistedNudgePlacement,
13+
setPersistedReasoningWatermark,
914
setPersistedStickyTurnReminder,
1015
setStrippedPlaceholderIds,
1116
} from "./storage-meta-persisted";

packages/plugin/src/features/magic-context/storage-tags.ts

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@ type PreparedStatement = ReturnType<Database["prepare"]>;
66
const insertTagStatements = new WeakMap<Database, PreparedStatement>();
77
const updateTagStatusStatements = new WeakMap<Database, PreparedStatement>();
88
const updateTagMessageIdStatements = new WeakMap<Database, PreparedStatement>();
9+
const getTagNumbersByMessageIdStatements = new WeakMap<Database, PreparedStatement>();
10+
const deleteTagsByMessageIdStatements = new WeakMap<Database, PreparedStatement>();
11+
const getMaxTagNumberBySessionStatements = new WeakMap<Database, PreparedStatement>();
912

1013
function getInsertTagStatement(db: Database): PreparedStatement {
1114
let stmt = insertTagStatements.get(db);
@@ -36,6 +39,39 @@ function getUpdateTagMessageIdStatement(db: Database): PreparedStatement {
3639
return stmt;
3740
}
3841

42+
function getTagNumbersByMessageIdStatement(db: Database): PreparedStatement {
43+
let stmt = getTagNumbersByMessageIdStatements.get(db);
44+
if (!stmt) {
45+
stmt = db.prepare(
46+
"SELECT tag_number FROM tags WHERE session_id = ? AND (message_id = ? OR message_id LIKE ? ESCAPE '\\' OR message_id LIKE ? ESCAPE '\\') ORDER BY tag_number ASC",
47+
);
48+
getTagNumbersByMessageIdStatements.set(db, stmt);
49+
}
50+
return stmt;
51+
}
52+
53+
function getDeleteTagsByMessageIdStatement(db: Database): PreparedStatement {
54+
let stmt = deleteTagsByMessageIdStatements.get(db);
55+
if (!stmt) {
56+
stmt = db.prepare(
57+
"DELETE FROM tags WHERE session_id = ? AND (message_id = ? OR message_id LIKE ? ESCAPE '\\' OR message_id LIKE ? ESCAPE '\\')",
58+
);
59+
deleteTagsByMessageIdStatements.set(db, stmt);
60+
}
61+
return stmt;
62+
}
63+
64+
function getMaxTagNumberBySessionStatement(db: Database): PreparedStatement {
65+
let stmt = getMaxTagNumberBySessionStatements.get(db);
66+
if (!stmt) {
67+
stmt = db.prepare(
68+
"SELECT COALESCE(MAX(tag_number), 0) AS max_tag_number FROM tags WHERE session_id = ?",
69+
);
70+
getMaxTagNumberBySessionStatements.set(db, stmt);
71+
}
72+
return stmt;
73+
}
74+
3975
interface TagRow {
4076
id: number;
4177
message_id: string;
@@ -46,6 +82,14 @@ interface TagRow {
4682
tag_number: number;
4783
}
4884

85+
interface TagNumberRow {
86+
tag_number: number;
87+
}
88+
89+
interface MaxTagNumberRow {
90+
max_tag_number: number;
91+
}
92+
4993
function isTagRow(row: unknown): row is TagRow {
5094
if (row === null || typeof row !== "object") return false;
5195
const r = row as Record<string, unknown>;
@@ -74,6 +118,22 @@ function toTagEntry(row: TagRow): TagEntry {
74118
};
75119
}
76120

121+
function isTagNumberRow(row: unknown): row is TagNumberRow {
122+
if (row === null || typeof row !== "object") return false;
123+
const r = row as Record<string, unknown>;
124+
return typeof r.tag_number === "number";
125+
}
126+
127+
function isMaxTagNumberRow(row: unknown): row is MaxTagNumberRow {
128+
if (row === null || typeof row !== "object") return false;
129+
const r = row as Record<string, unknown>;
130+
return typeof r.max_tag_number === "number";
131+
}
132+
133+
function escapeLikePattern(value: string): string {
134+
return value.replaceAll("\\", "\\\\").replaceAll("%", "\\%").replaceAll("_", "\\_");
135+
}
136+
77137
export function insertTag(
78138
db: Database,
79139
sessionId: string,
@@ -105,6 +165,37 @@ export function updateTagMessageId(
105165
getUpdateTagMessageIdStatement(db).run(messageId, sessionId, tagId);
106166
}
107167

168+
export function deleteTagsByMessageId(
169+
db: Database,
170+
sessionId: string,
171+
messageId: string,
172+
): number[] {
173+
const escapedMessageId = escapeLikePattern(messageId);
174+
const textPartPattern = `${escapedMessageId}:p%`;
175+
const filePartPattern = `${escapedMessageId}:file%`;
176+
const tagNumbers = getTagNumbersByMessageIdStatement(db)
177+
.all(sessionId, messageId, textPartPattern, filePartPattern)
178+
.filter(isTagNumberRow)
179+
.map((row) => row.tag_number);
180+
181+
if (tagNumbers.length === 0) {
182+
return [];
183+
}
184+
185+
getDeleteTagsByMessageIdStatement(db).run(
186+
sessionId,
187+
messageId,
188+
textPartPattern,
189+
filePartPattern,
190+
);
191+
return tagNumbers;
192+
}
193+
194+
export function getMaxTagNumberBySession(db: Database, sessionId: string): number {
195+
const row = getMaxTagNumberBySessionStatement(db).get(sessionId);
196+
return isMaxTagNumberRow(row) ? row.max_tag_number : 0;
197+
}
198+
108199
export function getTagsBySession(db: Database, sessionId: string): TagEntry[] {
109200
const rows = db
110201
.prepare(

packages/plugin/src/features/magic-context/storage.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@ export {
77
replaceAllCompartmentState,
88
type SessionFact,
99
} from "./compartment-storage";
10+
export {
11+
clearIndexedMessages,
12+
deleteIndexedMessage,
13+
} from "./message-index";
1014
export {
1115
type ContextDatabase,
1216
closeDatabase,
@@ -15,15 +19,20 @@ export {
1519
openDatabase,
1620
} from "./storage-db";
1721
export {
22+
clearPersistedNoteNudge,
1823
clearPersistedNudgePlacement,
1924
clearPersistedStickyTurnReminder,
2025
clearSession,
2126
getOrCreateSessionMeta,
27+
getPersistedNoteNudge,
2228
getPersistedNudgePlacement,
29+
getPersistedReasoningWatermark,
2330
getPersistedStickyTurnReminder,
2431
getStrippedPlaceholderIds,
2532
loadPersistedUsage,
33+
removeStrippedPlaceholderId,
2634
setPersistedNudgePlacement,
35+
setPersistedReasoningWatermark,
2736
setPersistedStickyTurnReminder,
2837
setStrippedPlaceholderIds,
2938
updateSessionMeta,
@@ -59,6 +68,8 @@ export {
5968
saveSourceContent,
6069
} from "./storage-source";
6170
export {
71+
deleteTagsByMessageId,
72+
getMaxTagNumberBySession,
6273
getTagById,
6374
getTagsBySession,
6475
getTopNBySize,

0 commit comments

Comments
 (0)