feat: add magic note tags and a two-pane notes layout

Keep the notes list beside the open note, compact the detail view with a full-width sticky toolbar, and make Back to overview return to a full-width all-notes grid. Add manual note tags with AND filtering, a tag manager for rename, merge and delete, schema 51 tag tables, and tag support in the note Agent tools.
This commit is contained in:
mesalogo
2026-09-30 19:04:47 +08:00
parent ab85e72f01
commit 6edfc0046b
28 changed files with 2736 additions and 370 deletions
@@ -903,6 +903,18 @@ describe('KnowledgeMcpGateway', () => {
expect(canvasEntry).toMatchObject({ content: 'Canvas text', contentKind: 'paged-canvas', contentVersion: 2, plainTextEditable: false })
expect(() => gateway.updateMagicNoteEntry(writeToken, { entryId: canvasEntry.id, expectedRevision: canvasEntry.revision, content: 'overwrite' })).toThrow('画布记录不能')
expect(database.getMagicNote(canvas.id)).toEqual(canvas)
// Tags: normalized on write, AND-filtered on list, replaced as a whole on update.
const tagged = gateway.createMagicNote(writeToken, { title: 'Tagged', tags: [' Work ', 'work', 'Q4'] })
expect(tagged.tags).toEqual(['Work', 'Q4'])
expect(gateway.listMagicNotes(readToken, { tags: ['WORK', 'q4'] })).toEqual([
expect.objectContaining({ id: tagged.id, tags: ['Work', 'Q4'] })
])
expect(gateway.listMagicNotes(readToken, { tags: ['Work', 'missing'] })).toEqual([])
expect(() => gateway.createMagicNote(writeToken, { title: 'Bad', tags: ['a,b'] })).toThrow()
const cleared = gateway.updateMagicNote(writeToken, { noteId: tagged.id, tags: [], expectedRevision: tagged.revision })
expect(cleared.tags).toEqual([])
expect(database.listMagicNoteTags()).toEqual([])
})
it('searches more than ten notes over MCP and returns recoverable argument errors', async () => {
+16 -5
View File
@@ -62,6 +62,7 @@ import type {
MagicNoteSearchResult,
MagicNoteSummary
} from '../../shared/magic-notes-contracts'
import { magicNoteTagListSchema } from '../../shared/magic-notes-contracts'
import {
magicNotePlainText,
validateMagicNoteRichContent
@@ -136,18 +137,20 @@ const {
} = magicNoteScopedDataToolCatalog
export type MagicNotesDatabase = {
listMagicNotes(): MagicNoteSummary[]
listMagicNotes(input?: { tags?: string[] }): MagicNoteSummary[]
getMagicNote(noteId: string): MagicNoteDetail
getMagicNoteEntry(entryId: string): MagicNoteEntry
searchMagicNotes(query: string, limit: number): MagicNoteSearchResult[]
createMagicNote(input: {
title: string
content?: MagicNoteContent
tags?: string[]
}): MagicNoteDetail
updateMagicNote(input: {
noteId: string
title?: string
pinned?: boolean
tags?: string[]
expectedRevision: number
}): MagicNoteDetail
deleteMagicNote(noteId: string): void
@@ -173,6 +176,7 @@ export type MagicNoteToolSummary = {
preview: string
entryCount: number
pinned: boolean
tags: string[]
revision: number
createdAt: string
updatedAt: string
@@ -278,6 +282,7 @@ function toMagicNoteToolSummary(
preview: note.preview.slice(0, 500),
entryCount: note.entryCount,
pinned: note.pinned,
tags: note.tags ?? [],
revision: note.revision,
createdAt: note.createdAt,
updatedAt: note.updatedAt
@@ -1376,9 +1381,11 @@ export class KnowledgeMcpGateway {
input: unknown = {}
): MagicNoteToolSummary[] {
const { database } = this.requireMagicNotes(token, 'read')
const { limit } = magicNoteListTool.inputSchema.parse(input)
const { limit, tags } = magicNoteListTool.inputSchema.parse(input)
const filter = tags ? magicNoteTagListSchema.parse(tags) : undefined
const notes: MagicNoteToolSummary[] = []
for (const note of database.listMagicNotes().slice(0, limit)) {
const listed = database.listMagicNotes(filter?.length ? { tags: filter } : {})
for (const note of listed.slice(0, limit)) {
const item = toMagicNoteToolSummary(note)
if (
Buffer.byteLength(JSON.stringify({ notes: [...notes, item] })) >
@@ -1467,7 +1474,8 @@ export class KnowledgeMcpGateway {
{
noteId: database.createMagicNote({
title: parsed.title,
...(content ? { content } : {})
...(content ? { content } : {}),
...(parsed.tags ? { tags: magicNoteTagListSchema.parse(parsed.tags) } : {})
}).id
}
)
@@ -1476,7 +1484,10 @@ export class KnowledgeMcpGateway {
updateMagicNote(token: string, input: unknown): MagicNoteToolDetail {
const { database } = this.requireMagicNotes(token, 'write')
const parsed = magicNoteUpdateTool.inputSchema.parse(input)
database.updateMagicNote(parsed)
database.updateMagicNote({
...parsed,
...(parsed.tags ? { tags: magicNoteTagListSchema.parse(parsed.tags) } : {})
})
return this.getMagicNote(token, { noteId: parsed.noteId })
}
@@ -7337,6 +7337,63 @@ describe('AssistantDatabase', () => {
}
})
it('stores manual note tags, filters by all tags, and renames, merges or deletes them', async () => {
const onMagicNotesChanged = vi.fn()
const directory = await mkdtemp(join(tmpdir(), 'goodbuddy-note-tags-'))
temporaryDirectories.push(directory)
const database = new AssistantDatabase(join(directory, 'assistant.sqlite'), { onMagicNotesChanged })
database.initialize('C:\\Workspace')
try {
const work = database.createMagicNote({ title: 'Work', tags: ['Work', 'Project A'] })
const personal = database.createMagicNote({ title: 'Personal', tags: ['personal'] })
const both = database.createMagicNote({ title: 'Both', tags: ['work', 'Personal'] })
expect(work.tags).toEqual(['Work', 'Project A'])
// The first spelling wins; later notes reuse the tag case-insensitively.
expect(both.tags).toEqual(['Work', 'personal'])
expect(database.listMagicNoteTags().map((tag) => [tag.name, tag.noteCount])).toEqual([
['personal', 2], ['Work', 2], ['Project A', 1]
])
expect(database.listMagicNotes({ tags: ['WORK'] }).map((note) => note.title).sort()).toEqual(['Both', 'Work'])
expect(database.listMagicNotes({ tags: ['work', 'personal'] }).map((note) => note.title)).toEqual(['Both'])
expect(database.listMagicNotes({ tags: ['missing'] })).toEqual([])
expect(database.searchMagicNoteSummaries('project').map((note) => note.title)).toEqual(['Work'])
const retagged = database.updateMagicNote({ noteId: work.id, tags: ['Work'], expectedRevision: work.revision })
expect(retagged.tags).toEqual(['Work'])
expect(retagged.revision).toBe(work.revision + 1)
// "Project A" lost its last note and is removed.
expect(database.listMagicNoteTags().map((tag) => tag.name)).not.toContain('Project A')
expect(() => database.updateMagicNote({ noteId: work.id, tags: [], expectedRevision: work.revision })).toThrow()
expect(database.getMagicNote(work.id).tags).toEqual(['Work'])
const workTag = database.listMagicNoteTags().find((tag) => tag.name === 'Work')!
const renamed = database.renameMagicNoteTag({ tagId: workTag.id, name: 'Job' })
expect(renamed).toMatchObject({ merged: false, tag: { name: 'Job', noteCount: 2 } })
expect(database.getMagicNote(both.id).tags).toEqual(['Job', 'personal'])
expect(database.getMagicNote(both.id).revision).toBe(both.revision + 1)
// Renaming onto an existing name merges; a note carrying both keeps one link.
const merged = database.renameMagicNoteTag({ tagId: workTag.id, name: 'Personal' })
expect(merged).toMatchObject({ merged: true, tag: { name: 'personal', noteCount: 3 } })
expect(database.getMagicNote(both.id).tags).toEqual(['personal'])
expect(database.listMagicNoteTags()).toHaveLength(1)
const calls = onMagicNotesChanged.mock.calls.length
database.deleteMagicNoteTag(merged.tag.id)
expect(onMagicNotesChanged).toHaveBeenCalledTimes(calls + 1)
expect(database.listMagicNoteTags()).toEqual([])
expect(database.getMagicNote(personal.id)).toMatchObject({ title: 'Personal', tags: [] })
expect(() => database.deleteMagicNoteTag(merged.tag.id)).toThrow('标签不存在')
expect(() => database.renameMagicNoteTag({ tagId: merged.tag.id, name: 'x' })).toThrow('标签不存在')
database.updateMagicNote({ noteId: personal.id, tags: ['Solo'], expectedRevision: database.getMagicNote(personal.id).revision })
database.deleteMagicNote(personal.id)
expect(database.listMagicNoteTags()).toEqual([])
} finally {
database.close()
}
})
it('counts incomplete todos and publishes todo-changing writes', async () => {
const onMagicTodosChanged = vi.fn()
const database = await createDatabase({ onMagicTodosChanged })
+277 -46
View File
@@ -105,6 +105,9 @@ import {
type MagicNoteContent,
type MagicNoteSearchResult,
type MagicNoteSummary,
type MagicNoteTag,
type MagicNoteTagRenameResult,
magicNoteTagKey,
type MagicTodoItem,
type MagicTodoStatus,
type MagicTodoUpdateInput
@@ -130,7 +133,7 @@ import {
SUBAGENT_PROGRESS_STORAGE_SCHEMA_VERSION
} from './subagent-progress-storage'
export const ASSISTANT_DATABASE_SCHEMA_VERSION = 50
export const ASSISTANT_DATABASE_SCHEMA_VERSION = 51
export type RemoteTaskEventInput = {
taskId: string
@@ -261,8 +264,28 @@ type MagicNoteRow = {
updated_at: string
entry_count: number
latest_plain_text: string | null
tags_json: string | null
}
type MagicNoteTagRow = {
id: string
name: string
note_count: number
}
/** Shared SELECT list for note summaries; the caller supplies FROM/WHERE. */
const MAGIC_NOTE_SUMMARY_COLUMNS = `n.*,
(SELECT COUNT(*) FROM magic_note_entries e
WHERE e.note_id = n.id) AS entry_count,
(SELECT plain_text FROM magic_note_entries e
WHERE e.note_id = n.id
ORDER BY e.created_at DESC, e.rowid DESC LIMIT 1) AS latest_plain_text,
(SELECT json_group_array(name) FROM (
SELECT t.name FROM magic_note_tag_links l
JOIN magic_note_tags t ON t.id = l.tag_id
WHERE l.note_id = n.id
ORDER BY l.position ASC, t.name_key ASC)) AS tags_json`
type MagicNoteEntryRow = {
id: string
note_id: string
@@ -762,12 +785,40 @@ function toMagicNoteSummary(row: MagicNoteRow): MagicNoteSummary {
preview: magicNotePreview(row.latest_plain_text ?? ''),
entryCount: row.entry_count,
pinned: row.pinned === 1,
tags: parseMagicNoteTags(row.tags_json),
revision: row.revision,
createdAt: row.created_at,
updatedAt: row.updated_at
}
}
/** AND-filter: a note must link to every requested tag key. */
function magicNoteTagFilter(tags: string[] | undefined): {
clause: string
params: string[]
} {
const keys = [...new Set((tags ?? []).map(magicNoteTagKey).filter(Boolean))]
if (keys.length === 0) return { clause: '', params: [] }
return {
clause: `(SELECT COUNT(DISTINCT t.name_key) FROM magic_note_tag_links l
JOIN magic_note_tags t ON t.id = l.tag_id
WHERE l.note_id = n.id AND t.name_key IN (${keys.map(() => '?').join(', ')})) = ${keys.length}`,
params: keys
}
}
function parseMagicNoteTags(value: string | null): string[] {
if (!value) return []
const parsed: unknown = JSON.parse(value)
return Array.isArray(parsed)
? parsed.filter((item): item is string => typeof item === 'string')
: []
}
function toMagicNoteTag(row: MagicNoteTagRow): MagicNoteTag {
return { id: row.id, name: row.name, noteCount: row.note_count }
}
function toArtifact(row: ArtifactRow): AssistantArtifact {
return {
id: row.id,
@@ -2305,6 +2356,8 @@ export class AssistantDatabase {
try {
for (const table of [
'magic_todos',
'magic_note_tag_links',
'magic_note_tags',
'magic_note_entries',
'magic_notes',
'heartbeat_configs',
@@ -3866,36 +3919,164 @@ export class AssistantDatabase {
}))
}
listMagicNotes(): MagicNoteSummary[] {
/**
* Lists notes, pinned first. `tags` keeps only notes carrying every listed tag
* (compared by key), so selecting more tags narrows the result.
*/
listMagicNotes(input: { tags?: string[]; limit?: number } = {}): MagicNoteSummary[] {
const database = this.requireDatabase()
const { clause, params } = magicNoteTagFilter(input.tags)
const rows = database
.prepare(
`SELECT n.*,
(SELECT COUNT(*) FROM magic_note_entries e
WHERE e.note_id = n.id) AS entry_count,
(SELECT plain_text FROM magic_note_entries e
WHERE e.note_id = n.id
ORDER BY e.created_at DESC, e.rowid DESC LIMIT 1)
AS latest_plain_text
`SELECT ${MAGIC_NOTE_SUMMARY_COLUMNS}
FROM magic_notes n
${clause ? `WHERE ${clause}` : ''}
ORDER BY n.pinned DESC, n.updated_at DESC, n.rowid DESC
LIMIT 200`
LIMIT ?`
)
.all() as MagicNoteRow[]
.all(...params, input.limit ?? 200) as MagicNoteRow[]
return rows.map(toMagicNoteSummary)
}
listMagicNoteTags(): MagicNoteTag[] {
const rows = this.requireDatabase()
.prepare(
`SELECT t.id, t.name, COUNT(l.note_id) AS note_count
FROM magic_note_tags t
LEFT JOIN magic_note_tag_links l ON l.tag_id = t.id
GROUP BY t.id
HAVING note_count > 0
ORDER BY note_count DESC, t.name_key ASC`
)
.all() as MagicNoteTagRow[]
return rows.map(toMagicNoteTag)
}
/**
* Renames a tag across every note. When the new name matches another tag,
* the two merge: links move to the existing tag and the old tag is removed.
* Affected notes get a new revision so open editors detect the change.
*/
renameMagicNoteTag(input: { tagId: string; name: string }): MagicNoteTagRenameResult {
const database = this.requireDatabase()
const now = new Date().toISOString()
const key = magicNoteTagKey(input.name)
let targetId = input.tagId
let merged = false
database.exec('BEGIN IMMEDIATE')
try {
const current = database
.prepare('SELECT id FROM magic_note_tags WHERE id = ?')
.get(input.tagId) as { id: string } | undefined
if (!current) throw new Error('标签不存在')
const other = database
.prepare('SELECT id FROM magic_note_tags WHERE name_key = ? AND id <> ?')
.get(key, input.tagId) as { id: string } | undefined
const noteIds = (
database
.prepare('SELECT note_id FROM magic_note_tag_links WHERE tag_id = ?')
.all(input.tagId) as Array<{ note_id: string }>
).map((row) => row.note_id)
if (other) {
merged = true
targetId = other.id
// Notes that already had both tags keep the target's position.
database
.prepare(
`INSERT OR IGNORE INTO magic_note_tag_links (note_id, tag_id, position, created_at)
SELECT note_id, ?, position, created_at FROM magic_note_tag_links WHERE tag_id = ?`
)
.run(other.id, input.tagId)
database.prepare('DELETE FROM magic_note_tags WHERE id = ?').run(input.tagId)
database
.prepare('UPDATE magic_note_tags SET updated_at = ? WHERE id = ?')
.run(now, other.id)
} else {
database
.prepare('UPDATE magic_note_tags SET name = ?, name_key = ?, updated_at = ? WHERE id = ?')
.run(input.name, key, now, input.tagId)
}
this.touchMagicNotes(database, noteIds, now)
database.exec('COMMIT')
} catch (error) {
database.exec('ROLLBACK')
throw error
}
const tag = this.listMagicNoteTags().find((item) => item.id === targetId)
this.options.onMagicNotesChanged?.()
return { tag: tag ?? { id: targetId, name: input.name, noteCount: 0 }, merged }
}
/** Removes a tag from every note. Notes and their entries are kept. */
deleteMagicNoteTag(tagId: string): void {
const database = this.requireDatabase()
const now = new Date().toISOString()
database.exec('BEGIN IMMEDIATE')
try {
const noteIds = (
database
.prepare('SELECT note_id FROM magic_note_tag_links WHERE tag_id = ?')
.all(tagId) as Array<{ note_id: string }>
).map((row) => row.note_id)
const result = database.prepare('DELETE FROM magic_note_tags WHERE id = ?').run(tagId)
if (result.changes !== 1) throw new Error('标签不存在')
this.touchMagicNotes(database, noteIds, now)
database.exec('COMMIT')
} catch (error) {
database.exec('ROLLBACK')
throw error
}
this.options.onMagicNotesChanged?.()
}
private touchMagicNotes(database: DatabaseSync, noteIds: string[], now: string): void {
const statement = database.prepare(
'UPDATE magic_notes SET revision = revision + 1, updated_at = ? WHERE id = ?'
)
for (const noteId of noteIds) statement.run(now, noteId)
}
/** Replaces a note's tags, creating missing tags and removing orphans. */
private replaceMagicNoteTags(
database: DatabaseSync,
noteId: string,
tags: string[],
now: string
): void {
database.prepare('DELETE FROM magic_note_tag_links WHERE note_id = ?').run(noteId)
const findTag = database.prepare('SELECT id FROM magic_note_tags WHERE name_key = ?')
const insertTag = database.prepare(
`INSERT INTO magic_note_tags (id, name, name_key, created_at, updated_at)
VALUES (?, ?, ?, ?, ?)`
)
const insertLink = database.prepare(
`INSERT OR IGNORE INTO magic_note_tag_links (note_id, tag_id, position, created_at)
VALUES (?, ?, ?, ?)`
)
tags.forEach((name, position) => {
const key = magicNoteTagKey(name)
let tag = findTag.get(key) as { id: string } | undefined
if (!tag) {
tag = { id: randomUUID() }
insertTag.run(tag.id, name, key, now, now)
}
insertLink.run(noteId, tag.id, position, now)
})
this.deleteOrphanMagicNoteTags(database)
}
private deleteOrphanMagicNoteTags(database: DatabaseSync): void {
database.exec(
`DELETE FROM magic_note_tags
WHERE NOT EXISTS (SELECT 1 FROM magic_note_tag_links l WHERE l.tag_id = magic_note_tags.id)`
)
}
getMagicNote(noteId: string): MagicNoteDetail {
const database = this.requireDatabase()
const row = database
.prepare(
`SELECT n.*,
(SELECT COUNT(*) FROM magic_note_entries e
WHERE e.note_id = n.id) AS entry_count,
(SELECT plain_text FROM magic_note_entries e
WHERE e.note_id = n.id
ORDER BY e.created_at DESC, e.rowid DESC LIMIT 1)
AS latest_plain_text
`SELECT ${MAGIC_NOTE_SUMMARY_COLUMNS}
FROM magic_notes n
WHERE n.id = ?`
)
@@ -3942,6 +4123,7 @@ export class AssistantDatabase {
title: string
content?: MagicNoteContent
source?: MagicNoteSource
tags?: string[]
}): MagicNoteCreateResult {
const id = randomUUID()
const entryId = input.content ? randomUUID() : undefined
@@ -3970,6 +4152,9 @@ export class AssistantDatabase {
VALUES (?, ?, ?, 0, ?, ?, ?)`
)
.run(id, null, input.title, input.content ? 1 : 0, now, now)
if (input.tags?.length) {
this.replaceMagicNoteTags(database, id, input.tags, now)
}
if (input.content && entryId) {
const pointer = this.writeMagicNoteBody(id, entryId, input.content, 0, now)
database
@@ -4016,27 +4201,39 @@ export class AssistantDatabase {
noteId: string
title?: string
pinned?: boolean
tags?: string[]
expectedRevision: number
}): MagicNoteDetail {
const now = new Date().toISOString()
const result = this.requireDatabase()
.prepare(
`UPDATE magic_notes
SET title = COALESCE(?, title),
pinned = COALESCE(?, pinned),
revision = revision + 1,
updated_at = ?
WHERE id = ? AND revision = ?`
)
.run(
input.title ?? null,
input.pinned === undefined ? null : input.pinned ? 1 : 0,
now,
input.noteId,
input.expectedRevision
)
if (result.changes !== 1) {
throw new Error('笔记已被更新,请刷新后重试')
const database = this.requireDatabase()
database.exec('BEGIN IMMEDIATE')
try {
const result = database
.prepare(
`UPDATE magic_notes
SET title = COALESCE(?, title),
pinned = COALESCE(?, pinned),
revision = revision + 1,
updated_at = ?
WHERE id = ? AND revision = ?`
)
.run(
input.title ?? null,
input.pinned === undefined ? null : input.pinned ? 1 : 0,
now,
input.noteId,
input.expectedRevision
)
if (result.changes !== 1) {
throw new Error('笔记已被更新,请刷新后重试')
}
if (input.tags !== undefined) {
this.replaceMagicNoteTags(database, input.noteId, input.tags, now)
}
database.exec('COMMIT')
} catch (error) {
database.exec('ROLLBACK')
throw error
}
const detail = this.getMagicNote(input.noteId)
this.options.onMagicNotesChanged?.()
@@ -4044,11 +4241,20 @@ export class AssistantDatabase {
}
deleteMagicNote(noteId: string): void {
const result = this.requireDatabase()
.prepare('DELETE FROM magic_notes WHERE id = ?')
.run(noteId)
if (result.changes !== 1) {
throw new Error('笔记不存在')
const database = this.requireDatabase()
database.exec('BEGIN IMMEDIATE')
try {
const result = database
.prepare('DELETE FROM magic_notes WHERE id = ?')
.run(noteId)
if (result.changes !== 1) {
throw new Error('笔记不存在')
}
this.deleteOrphanMagicNoteTags(database)
database.exec('COMMIT')
} catch (error) {
database.exec('ROLLBACK')
throw error
}
this.reconcileMagicNoteFiles(noteId)
this.options.onMagicNotesChanged?.()
@@ -4318,17 +4524,17 @@ export class AssistantDatabase {
searchMagicNoteSummaries(query: string, limit = 200): MagicNoteSummary[] {
const pattern = `%${query.replace(/[\\%_]/gu, '\\$&')}%`
const rows = this.requireDatabase().prepare(
`SELECT n.*,
(SELECT COUNT(*) FROM magic_note_entries e WHERE e.note_id = n.id) AS entry_count,
(SELECT plain_text FROM magic_note_entries e WHERE e.note_id = n.id
ORDER BY e.created_at DESC, e.rowid DESC LIMIT 1) AS latest_plain_text
`SELECT ${MAGIC_NOTE_SUMMARY_COLUMNS}
FROM magic_notes n
WHERE n.title LIKE ? ESCAPE '\\'
OR EXISTS (SELECT 1 FROM magic_note_entries e
WHERE e.note_id = n.id AND e.plain_text LIKE ? ESCAPE '\\')
OR EXISTS (SELECT 1 FROM magic_note_tag_links l
JOIN magic_note_tags t ON t.id = l.tag_id
WHERE l.note_id = n.id AND t.name LIKE ? ESCAPE '\\')
ORDER BY n.pinned DESC, n.updated_at DESC, n.rowid DESC
LIMIT ?`
).all(pattern, pattern, limit) as MagicNoteRow[]
).all(pattern, pattern, pattern, limit) as MagicNoteRow[]
return rows.map(toMagicNoteSummary)
}
@@ -11664,6 +11870,31 @@ export class AssistantDatabase {
database.exec('PRAGMA user_version = 50; COMMIT;')
} catch (error) { database.exec('ROLLBACK'); throw error }
}
if (version.user_version < 51) {
database.exec('BEGIN IMMEDIATE')
try {
// Manual note tags. Names compare by name_key (NFC, trimmed, lower-case);
// a tag with no remaining links is removed by the write that orphaned it.
database.exec(`
CREATE TABLE IF NOT EXISTS magic_note_tags (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
name_key TEXT NOT NULL UNIQUE,
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS magic_note_tag_links (
note_id TEXT NOT NULL REFERENCES magic_notes(id) ON DELETE CASCADE,
tag_id TEXT NOT NULL REFERENCES magic_note_tags(id) ON DELETE CASCADE,
position INTEGER NOT NULL,
created_at TEXT NOT NULL,
PRIMARY KEY (note_id, tag_id)
);
CREATE INDEX IF NOT EXISTS magic_note_tag_links_tag_idx ON magic_note_tag_links(tag_id, note_id);
`)
database.exec('PRAGMA user_version = 51; COMMIT;')
} catch (error) { database.exec('ROLLBACK'); throw error }
}
}
supervisionReviewStore(): SupervisionReviewStore {
+17 -1
View File
@@ -8,6 +8,8 @@ import {
magicNoteEntryCreateSchema,
magicNoteEntryDeleteSchema,
magicNoteEntryUpdateSchema,
magicNoteTagDeleteSchema,
magicNoteTagRenameSchema,
magicNoteUpdateSchema,
magicTodoUpdateSchema
} from '../../shared/magic-notes-contracts'
@@ -49,7 +51,21 @@ export function registerMagicNotesIpcHandlers(
registerHandler(ipcChannels.magicNotesList, (event) => {
assertTrustedSender(event, window)
return { notes: assistantDatabase.listMagicNotes() }
return {
notes: assistantDatabase.listMagicNotes(),
tags: assistantDatabase.listMagicNoteTags()
}
})
registerHandler(ipcChannels.magicNotesRenameTag, (event, input: unknown) => {
assertTrustedSender(event, window)
return assistantDatabase.renameMagicNoteTag(magicNoteTagRenameSchema.parse(input))
})
registerHandler(ipcChannels.magicNotesDeleteTag, (event, input: unknown) => {
assertTrustedSender(event, window)
const { tagId } = magicNoteTagDeleteSchema.parse(input)
assistantDatabase.deleteMagicNoteTag(tagId)
})
registerHandler(ipcChannels.magicNotesGet, (event, input: unknown) => {