Skip to content
Merged
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
22 changes: 22 additions & 0 deletions changelogs/v2.5.4.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# v2.5.4

## Added

- **Drag a notes folder into another folder.** Folders in the notes sidebar are now draggable — drop one onto another folder (or the "Move to root" zone) to reparent the whole subtree; every note inside keeps its relative structure.
- **Drag a notes folder onto another project.** Drop a folder from the notes sidebar onto a project row in the leftmost sidebar to move the entire folder (and all its notes) to that project.
- **Drag a single note onto another project.** Drop an individual note from the notes sidebar onto a project row to move just that note (its folder path is kept).
- **Drag a task card onto another project.** Drop a card from the board onto a project row in the leftmost sidebar to move it there. The card lands in the target project's column of the *same type* (e.g. In Progress → In Progress), falling back to Backlog / the first column if there's no match — so its state is preserved.
- **Subagents in AI chat (per-thread toggle).** A new "Subagents" toggle in the chat toolbar routes a conversation through a dispatch → research/write architecture: a thin dispatcher delegates read-only research and write work to focused sub-agents, each with a smaller tool set. On capable models this cut prompt tokens ~43% on research-heavy tasks at equal output quality. The toggle is per-thread and persisted; hidden on the on-device Llama provider (small models are unreliable with the multi-hop split).
- **Live, expandable subagent trace.** While a subagent-mode turn runs, each sub-agent appears as an inline block you can expand to "step into" it — showing its instruction, streamed findings/confirmation, and its tool calls. Traces persist in thread history.

## Fixed

- **Renaming a project now renames its notes folder on disk immediately** — no app restart required. When an open note autosaved during the rename, the note file was written under the new folder name first, which forced a merge that left a stale duplicate (and the old, now-empty folder) behind on disk until the next launch. Stale same-note duplicates are now removed and the old folder is cleaned up as part of the rename.


## Internal

- Refactored the in-app chat tool-call loop: extracted `runToolLoop` from `electron/ipc/chat.ts` into a shared `electron/lib/chat-loop.ts` (behaviour unchanged) so it can be reused and benchmarked. Added an optional tool-array override to support restricted tool sets, plus an optional (default no-op) pre-execution arg hook used only by tests for fault injection.
- Added an experimental subagent chat variant (`electron/lib/chat-subagent-loop.ts`) and two benchmarks: a single-agent-vs-subagent token/latency harness (`electron/lib/chat-agent-benchmark.test.ts`) and a write-failure recovery / fault-injection harness (`electron/lib/chat-write-recovery-benchmark.test.ts`). Not wired into production — used to evaluate a research/write/dispatch split for chat.
- Subagent variant robustness: added a forced final-synthesis turn (`tool_choice:"none"` nudge) at the subagent and dispatcher boundaries so weak/local models that stop after their last tool call still return a brief instead of empty output. On a local 4-bit Gemma this eliminated dropped answers and cut subagent prompt tokens ~25% and latency ~35%.
- Added an LLM-as-judge output-quality scorer (`electron/lib/chat-quality-scorer.test.ts`) that grades the actual note artifacts written to the DB against a 5-criterion rubric, with an overridable judge endpoint (`JUDGE_BASE_URL`/`JUDGE_MODEL`/`JUDGE_API_KEY`) so a strong model can fairly grade a weak agent. On a capable model, single-agent and subagent quality were statistically equal (23.7 vs 24.0 / 25).
18 changes: 9 additions & 9 deletions electron/db/queries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -651,14 +651,14 @@ export function getChatThreads(db: Database.Database, workspaceId: string) {
}

export function upsertChatThread(db: Database.Database, t: {
id: string; scope: string; workspaceId: string; projectId?: string; title?: string;
id: string; scope: string; workspaceId: string; projectId?: string; title?: string; useSubagents?: boolean;
}) {
const now = ts();
db.prepare(`
INSERT INTO chat_threads (id, scope, workspace_id, project_id, title, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?)
ON CONFLICT(id) DO UPDATE SET title = excluded.title, updated_at = excluded.updated_at
`).run(t.id, t.scope, t.workspaceId, t.projectId ?? null, t.title ?? null, now, now);
INSERT INTO chat_threads (id, scope, workspace_id, project_id, title, use_subagents, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
ON CONFLICT(id) DO UPDATE SET title = excluded.title, use_subagents = excluded.use_subagents, updated_at = excluded.updated_at
`).run(t.id, t.scope, t.workspaceId, t.projectId ?? null, t.title ?? null, t.useSubagents ? 1 : 0, now, now);
return toChatThread(db.prepare("SELECT * FROM chat_threads WHERE id = ?").get(t.id));
}

Expand All @@ -672,13 +672,13 @@ export function getChatMessages(db: Database.Database, threadId: string) {
}

export function addChatMessage(db: Database.Database, m: {
id: string; threadId: string; role: string; content: string; contextRefs?: unknown; toolCalls?: unknown; reasoning?: string;
id: string; threadId: string; role: string; content: string; contextRefs?: unknown; toolCalls?: unknown; reasoning?: string; subagents?: unknown;
}) {
const now = ts();
db.prepare(`
INSERT INTO chat_messages (id, thread_id, role, content, context_refs, tool_calls, reasoning, created_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
`).run(m.id, m.threadId, m.role, m.content, m.contextRefs ? JSON.stringify(m.contextRefs) : null, m.toolCalls ? JSON.stringify(m.toolCalls) : null, m.reasoning ?? null, now);
INSERT INTO chat_messages (id, thread_id, role, content, context_refs, tool_calls, reasoning, subagents, created_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
`).run(m.id, m.threadId, m.role, m.content, m.contextRefs ? JSON.stringify(m.contextRefs) : null, m.toolCalls ? JSON.stringify(m.toolCalls) : null, m.reasoning ?? null, m.subagents ? JSON.stringify(m.subagents) : null, now);
return toChatMessage(db.prepare("SELECT * FROM chat_messages WHERE id = ?").get(m.id));
}

Expand Down
39 changes: 39 additions & 0 deletions electron/db/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -740,13 +740,52 @@ const MIGRATIONS: Migration[] = [
db.prepare("DELETE FROM sync_oplog WHERE entity = ?").run(t);
}
},

// v29: Subagent chat mode. `chat_threads.use_subagents` persists the per-thread
// toggle that routes a conversation through the dispatch → research/write
// subagent loop; `chat_messages.subagents` (JSON) persists the expandable
// subagent traces so they survive restarts (mirrors v14 tool_calls / v20 reasoning).
(db) => {
const threadCols = db.prepare("PRAGMA table_info(chat_threads)").all() as { name: string }[];
if (!threadCols.some((c) => c.name === "use_subagents")) {
db.exec("ALTER TABLE chat_threads ADD COLUMN use_subagents INTEGER NOT NULL DEFAULT 0");
}
const msgCols = db.prepare("PRAGMA table_info(chat_messages)").all() as { name: string }[];
if (!msgCols.some((c) => c.name === "subagents")) {
db.exec("ALTER TABLE chat_messages ADD COLUMN subagents TEXT");
}
},
];

export function applySchema(db: Database.Database): void {
// PRAGMA foreign_keys must be set per connection — do it first, before exec.
db.pragma("foreign_keys = ON");
db.exec(SCHEMA_SQL);
runMigrations(db);
ensureColumns(db);
}

/**
* Idempotent, version-independent column guards. Migrations only run for
* `user_version` values below MIGRATIONS.length, so a DB whose user_version was
* already advanced past a migration index (e.g. by an interim/renumbered build)
* can miss a column even though its version looks current. This defensively
* ensures late-added columns exist on every connection, regardless of version.
* Each check is a no-op once the column is present.
*/
function ensureColumns(db: Database.Database): void {
const ensure = (table: string, column: string, ddl: string) => {
try {
const cols = db.prepare(`PRAGMA table_info(${table})`).all() as { name: string }[];
if (cols.length > 0 && !cols.some((c) => c.name === column)) {
db.exec(`ALTER TABLE ${table} ADD COLUMN ${ddl}`);
}
} catch {
/* table may not exist on some connections — safe to skip */
}
};
ensure("chat_threads", "use_subagents", "use_subagents INTEGER NOT NULL DEFAULT 0");
ensure("chat_messages", "subagents", "subagents TEXT");
}

function runMigrations(db: Database.Database): void {
Expand Down
Loading