From 434bc159cb2542be7b0d7bb54fec83367564db4e Mon Sep 17 00:00:00 2001 From: Adib Hanna Date: Sun, 18 Oct 2026 14:15:45 -0500 Subject: [PATCH 1/2] Fix(tasks): a finished task note stays in its Custom status column On the board grouped by field:status, a task note with `status: done` vanished, even with `done` listed in kanban_statuses. The field board skipped every checked, cancelled or forwarded task before it read the field, and a task note's checked state comes from that same `status:`: the value that names its column was the value that took it off the board. The desktop app had the same filter (ZenNotes/zennotes#949). A task note whose value is one of the configured columns now stays in that column. Only configured values count: a discovered column would turn every finished task note in the vault into a column of its own. A configured `cancelled` column holds cancelled task notes the same way. Checkbox tasks keep the rule, since a status they inherit from the note's frontmatter would pull every checked subtask of an in-progress note into In progress. One test, which fails without the change. --- internal/tui/tasks_parity_test.go | 27 +++++++++++++++++++++++++++ internal/tui/views_tasks.go | 20 ++++++++++++++++++-- 2 files changed, 45 insertions(+), 2 deletions(-) diff --git a/internal/tui/tasks_parity_test.go b/internal/tui/tasks_parity_test.go index 35c653f..a9353c7 100644 --- a/internal/tui/tasks_parity_test.go +++ b/internal/tui/tasks_parity_test.go @@ -85,6 +85,33 @@ func TestKanbanCustomStatusUsesConfiguredThenDiscoveredColumns(t *testing.T) { } } +// Desktop #949: `status: done` closes a task note and names its column, so a +// configured Done column keeps it; checkbox tasks and unlisted statuses do not. +func TestKanbanCustomStatusKeepsClosedTaskNotesInConfiguredColumns(t *testing.T) { + status := func(s string) map[string]string { return map[string]string{"status": s} } + tasks := []vault.Task{ + {ID: "done-note", Kind: "file", Checked: true, Fields: status("done")}, + {ID: "active-note", Kind: "file", InProgress: true, Fields: status("in-progress")}, + {ID: "cancelled-note", Kind: "file", Cancelled: true, Fields: status("cancelled")}, + {ID: "finished-note", Kind: "file", Checked: true, Fields: status("completed")}, + {ID: "checked-step", Checked: true, Fields: status("in-progress")}, + {ID: "shipped-step", Checked: true, Fields: status("done")}, + {ID: "next-step", Fields: status("in-progress")}, + } + v := &tasksView{groupBy: "field:status", tasks: tasks} + a := &App{prefs: prefsView{Prefs: config.Prefs{KanbanStatuses: []string{"open", "in-progress", "waiting", "done"}}}} + v.buildColumns(a) + assertKanbanColumnIDs(t, v, []string{"open", "in-progress", "waiting", "done", "__none__"}) + assertKanbanCards(t, v, "done", []string{"done-note"}) + assertKanbanCards(t, v, "in-progress", []string{"active-note", "next-step"}) + + v = &tasksView{groupBy: "field:status", tasks: tasks} + a.prefs.KanbanStatuses = []string{"in-progress", "done", "cancelled"} + v.buildColumns(a) + assertKanbanColumnIDs(t, v, []string{"in-progress", "done", "cancelled", "__none__"}) + assertKanbanCards(t, v, "cancelled", []string{"cancelled-note"}) +} + func TestKanbanProjectFieldSeparatesLiteralNoneFromMissingValue(t *testing.T) { v := &tasksView{groupBy: "field:project", tasks: []vault.Task{ {ID: "literal-none", Fields: map[string]string{"project": "none"}}, diff --git a/internal/tui/views_tasks.go b/internal/tui/views_tasks.go index ad79510..0c11ff5 100644 --- a/internal/tui/views_tasks.go +++ b/internal/tui/views_tasks.go @@ -395,8 +395,24 @@ func (v *tasksView) buildColumns(a *App) { } } configured := len(values) - for _, t := range v.tasks { + configuredSet := map[string]bool{} + for _, val := range values { + configuredSet[val] = true + } + // Closed tasks stay off a board, with one exception (desktop #949): a + // task note whose value names a configured column stays in it, done or + // cancelled. A task note's `status:` is its one state, so `status: done` + // both closes it and names its column. Only configured columns count, or + // every finished note would become a column; checkbox tasks keep the rule, + // since a status inherited from the note would pull in checked subtasks. + onBoard := func(t vault.Task) bool { if !t.Checked && !t.Cancelled && !t.Forwarded { + return true + } + return t.Kind == "file" && configuredSet[strings.ToLower(strings.TrimSpace(t.Fields[key]))] + } + for _, t := range v.tasks { + if onBoard(t) { add(t.Fields[key]) } } @@ -406,7 +422,7 @@ func (v *tasksView) buildColumns(a *App) { } cols = append(cols, kanbanColumn{id: "__none__", title: "No " + key}) for _, t := range v.tasks { - if t.Checked || t.Cancelled || t.Forwarded { + if !onBoard(t) { continue } val := strings.ToLower(strings.TrimSpace(t.Fields[key])) From 241abbc32984e7058093155fbe451b3c87b9fb13 Mon Sep 17 00:00:00 2001 From: Adib Hanna Date: Sun, 18 Oct 2026 16:03:00 -0500 Subject: [PATCH 2/2] Fix(mcp): zn mcp gives assistants the desktop's instructions again DefaultInstructions is meant to be a verbatim copy of the desktop MCP server's instructions, so an agent behaves the same whichever binary serves the vault. It had drifted three times: it lacked the rule that Mermaid labels are plain text, the collapsible callout syntax, and the whole section on reviewing a note through comment threads, although zn mcp has had list_comments, add_comment, reply_to_comment and resolve_comment since e5550a3. Desktop 2.71.0 also adds D2 to the renderers an agent should use. The constant is regenerated from the desktop text at ZenNotes/zennotes c4a38631, one quoted line per source line as before (the same generator reproduces the old constant exactly), and zn mcp's initialize now returns that text unchanged. --- internal/mcp/instructions.go | 55 ++++++++++++++++++++++++++++++++---- 1 file changed, 49 insertions(+), 6 deletions(-) diff --git a/internal/mcp/instructions.go b/internal/mcp/instructions.go index 41819db..12ab74c 100644 --- a/internal/mcp/instructions.go +++ b/internal/mcp/instructions.go @@ -23,8 +23,8 @@ func ResolveInstructions() string { // instructions (apps/desktop/src/mcp/instructions.ts), so an agent behaves // the same whichever binary is serving the vault. const DefaultInstructions = "You are connected to a user's ZenNotes vault: plain .md files on\n" + - "disk, rendered live with KaTeX, TikZ, function-plot, JSXGraph, and\n" + - "Mermaid. Treat the vault like a shared filesystem. Prompts will be\n" + + "disk, rendered live with KaTeX, TikZ, function-plot, JSXGraph,\n" + + "Mermaid, and D2. Treat the vault like a shared filesystem. Prompts will be\n" + "short; you must infer the right note shape, voice, and visuals from\n" + "the subject itself.\n" + "\n" + @@ -38,7 +38,7 @@ const DefaultInstructions = "You are connected to a user's ZenNotes vault: plain " bullet-list template onto a subject that wants prose, and never\n" + " force prose onto a subject that wants steps.\n" + "2. **Always use the native renderers for anything visual.** KaTeX\n" + - " for math, TikZ / function-plot / JSXGraph / Mermaid for every\n" + + " for math, TikZ / function-plot / JSXGraph / Mermaid / D2 for every\n" + " diagram. Never ASCII art, never unicode-arrow sketches.\n" + "3. **Structure logically, flow consistently.** Decide the section\n" + " order before writing. Sections carry a reader from orientation\n" + @@ -167,7 +167,7 @@ const DefaultInstructions = "You are connected to a user's ZenNotes vault: plain "made of `/ \\ | - + * ^ . o > < \u2192 \u2190 \u2191 \u2193 \u2022` or\n" + "box-drawing characters, fenced or not, is a bug. If you\n" + "catch yourself about to make one, delete it and emit a ```tikz (or\n" + - "```function-plot / ```jsxgraph / ```mermaid) block instead.\n" + + "```function-plot / ```jsxgraph / ```mermaid / ```d2) block instead.\n" + "Triggers that mean \"use TikZ, not ASCII\": axes, origin, point at,\n" + "arrow from\u2026to, head to tail, parallelogram, rotation, projection,\n" + "span, plane, angle, triangle, unit circle, subspace, tangent, normal,\n" + @@ -180,7 +180,13 @@ const DefaultInstructions = "You are connected to a user's ZenNotes vault: plain " vectors over axes.\n" + "- ```jsxgraph: interactive / draggable constructions.\n" + "- ```mermaid: flow, sequence, class, state, ER, mindmap,\n" + - " Gantt. NOT for coordinate geometry.\n" + + " Gantt. NOT for coordinate geometry. Labels are plain text plus\n" + + " `
` breaks: no other HTML and no `$$` math inside a label\n" + + " (put the formula in a KaTeX block next to the diagram).\n" + + "- ```d2: architecture, infrastructure and system diagrams, nested\n" + + " containers, data models (`shape: sql_table`) and sequence\n" + + " diagrams. Labels may be Markdown (`|md \u2026|`). Icons, images and\n" + + " links are not drawn: a diagram never fetches anything.\n" + "\n" + "Fence body rules:\n" + "- `tikz`: emit the TikZ content itself, usually a bare\n" + @@ -208,7 +214,9 @@ const DefaultInstructions = "You are connected to a user's ZenNotes vault: plain "`due:YYYY-MM-DD`, `!high` / `!med` / `!low`, `@waiting`, and\n" + "`#tag`.\n" + "\n" + - "**Callouts**: `> [!note]`, `> [!tip]`, `> [!warning]`.\n" + + "**Callouts**: `> [!note]`, `> [!tip]`, `> [!warning]`. A `-` right after the\n" + + "type (`> [!example]- Screenshots`) makes the callout start collapsed, a `+`\n" + + "starts it expanded; use `-` for long blocks a reader opens on demand.\n" + "\n" + "## Tool etiquette\n" + "\n" + @@ -227,6 +235,28 @@ const DefaultInstructions = "You are connected to a user's ZenNotes vault: plain " `tasks: false`/`note`, excluded folders). Pass\n" + " includeExcluded: true only when the user asks for everything.\n" + "\n" + + "## Comments: reviewing a note together\n" + + "\n" + + "Notes carry comment threads, kept beside the note and shown in the app's\n" + + "Comments panel. A user who asks you to review, answer, or discuss a note\n" + + "usually means through those threads, like a pull request, not by editing\n" + + "the body.\n" + + "\n" + + "- Start with `list_comments` on the note. Each thread shows the passage it\n" + + " is anchored to, the line it sits on now, who wrote what (`author` is\n" + + " null for the user), and the replies so far.\n" + + "- Answer a thread with `reply_to_comment`; it lands under the user's\n" + + " comment, signed with your name. Reply to every open thread you were asked\n" + + " about, one reply per thread, and keep replies short and concrete.\n" + + "- Raise something new with `add_comment`, passing `anchor_text` copied\n" + + " verbatim from the note so the comment highlights that passage in the app.\n" + + " Omit it only for a note-level remark.\n" + + "- Change the note body only when the user asks for the change; when a thread\n" + + " ends in \"do it\", make the edit with the editing tools, then reply in the\n" + + " thread saying what changed.\n" + + "- `resolve_comment` only when the user says the thread is settled or asks\n" + + " you to close it. Resolved threads stay in the note's history.\n" + + "\n" + "## Self-check before every write\n" + "\n" + "Scan the markdown before sending it. Fix, don\u2019t ship:\n" + @@ -289,6 +319,19 @@ const DefaultInstructions = "You are connected to a user's ZenNotes vault: plain " B -- no --> D[Do Y]\n" + "```\n" + "\n" + + "Architecture:\n" + + "\n" + + "```d2\n" + + "direction: right\n" + + "app: Desktop app {\n" + + " ui: Renderer\n" + + " main: Main process\n" + + " ui -> main: IPC\n" + + "}\n" + + "cloud: Sync server {shape: cloud}\n" + + "app.main -> cloud: sync\n" + + "```\n" + + "\n" + "Adjust these. Don't invent ASCII replacements.\n" + "\n" + "## Intent mapping\n" +