Skip to content

Commit 67efc2f

Browse files
mason: improve Pi todowrite UX
Co-authored-by: Alfonso [Magic Context] <288211368+alfonso-magic-context@users.noreply.github.com>
1 parent ea981bd commit 67efc2f

15 files changed

Lines changed: 1128 additions & 29 deletions

File tree

CONFIGURATION.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,7 @@ Higher-tier models with longer cache windows benefit from a longer TTL. Setting
110110
| `commit_cluster_trigger` | `object` | See below | Controls the commit-cluster historian trigger. |
111111
| `system_prompt_injection` | `object` | See below | Controls whether and where Magic Context augments the system prompt; lets you opt specific agents out. |
112112
| `keep_subagents` | `boolean` | `false` | Debug: keep the child sessions Magic Context spawns for its own subagents (historian, dreamer, sidekick, memory-migration) instead of deleting them on success, so their full transcript stays in the host session store for inspection. Kept sessions accumulate until cleared manually — leave `false` for normal use. |
113+
| `todowrite` | `object` | See below | **Pi only.** Controls Magic Context's built-in `todowrite` tool and persistent task overlay. OpenCode has its own built-in `todowrite`, so this setting has no effect there. |
113114
| `sqlite` | `object` | See below | Per-connection SQLite tuning for Magic Context's own `context.db`. |
114115

115116
### `language`
@@ -162,6 +163,26 @@ Controls whether and where Magic Context augments the system prompt (its guidanc
162163
- **`enabled`** — when `false`, NO injection happens for ANY agent. Global escape hatch; Magic Context's transform and compaction still run, but nothing is added to the system prompt.
163164
- **`skip_signatures`** — substring opt-out list. If an agent's system prompt contains any of these strings, Magic Context skips ALL injection for that call. Use it to exempt a specific custom agent by putting the signature (e.g. the default `<!-- magic-context: skip -->`) in that agent's prompt.
164165

166+
### `todowrite` (Pi only)
167+
168+
Pi does not ship a built-in `todowrite` tool, so Magic Context registers an OpenCode-parity task-list tool by default. Disable it if another Pi extension already provides todo UX:
169+
170+
```jsonc
171+
{
172+
"todowrite": {
173+
"enabled": true, // default: true
174+
"overlay": true // default: true
175+
}
176+
}
177+
```
178+
179+
| Field | Type | Default | Description |
180+
|-------|------|---------|-------------|
181+
| `enabled` | `boolean` | `true` | Register Magic Context's Pi `todowrite` tool and `/todos` command. Set `false` when using another todo extension. |
182+
| `overlay` | `boolean` | `true` | Show the persistent todo overlay above the editor while tasks are active. |
183+
184+
Pi registers tools, slash commands, and widgets once at extension boot. If you `/cd` into a project with a different `todowrite.enabled` value, run `/reload` or restart Pi for the tool surface to change.
185+
165186
### `sqlite`
166187

167188
Per-connection PRAGMAs applied to Magic Context's own `context.db` at open. These tune SQLite's runtime behaviour only — they do not change the schema or what is stored, and they do not touch OpenCode's or Pi's databases.

assets/magic-context.schema.json

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1209,6 +1209,26 @@
12091209
"description": "Debug: keep the child sessions Magic Context spawns for its own subagents (historian, dreamer, sidekick, memory-migration) instead of deleting them on success. Useful for short-term inspection/data collection — their full transcript (prompt, tool calls, token usage, output) stays in the host session store. Kept sessions accumulate until manually cleared; leave false for normal use. Requires a restart to take effect.",
12101210
"type": "boolean"
12111211
},
1212+
"todowrite": {
1213+
"default": {
1214+
"enabled": true,
1215+
"overlay": true
1216+
},
1217+
"description": "Pi-only todowrite tool and overlay controls. Pi registers tools and widgets at extension boot, so changing this after /cd requires /reload or restart.",
1218+
"type": "object",
1219+
"properties": {
1220+
"enabled": {
1221+
"default": true,
1222+
"description": "Pi only: register Magic Context's todowrite task-list tool. Disable if you use your own todo extension. OpenCode ships its own built-in todowrite; this setting has no effect there.",
1223+
"type": "boolean"
1224+
},
1225+
"overlay": {
1226+
"default": true,
1227+
"description": "Pi only: show the persistent todo overlay above the editor while tasks are active.",
1228+
"type": "boolean"
1229+
}
1230+
}
1231+
},
12121232
"smart_drops": {
12131233
"default": false,
12141234
"description": "Content-aware reclaim of provably-superseded tool output, layered on the existing execute-pass auto-drop. When on: superseded todowrite (keep newest 1), spent ctx_reduce (keep newest 5), and zero-value meta (bash_status, bash_kill, ctx_note read/dismiss) outputs are dropped; older edits to a file are compressed to a filePath-preserving marker while the newest edit per file stays full. Only acts on passes already busting the cache, so it never originates a cache bust. Honors the protected-tag reserve. Experimental: opt-in, default off until cache stability is proven; when off the wire is byte-identical to the positional-only reclaim. Requires a restart.",

packages/dashboard/src/components/ConfigEditor/ConfigEditor.tsx

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1748,6 +1748,14 @@ function ConfigForm(props: {
17481748
return v == null ? true : Boolean(v);
17491749
};
17501750
const keepSubagents = () => Boolean(getNestedValue(formData(), "keep_subagents"));
1751+
const todowrite = () =>
1752+
(getNestedValue(formData(), "todowrite") as
1753+
| { enabled?: boolean; overlay?: boolean }
1754+
| undefined) ?? {};
1755+
const todowriteEnabled = () => todowrite().enabled ?? true;
1756+
const todowriteOverlay = () => todowrite().overlay ?? true;
1757+
const setTodowrite = (patch: Record<string, unknown>) =>
1758+
handleFieldChange("todowrite", { ...todowrite(), ...patch });
17511759
const smartDrops = () => Boolean(getNestedValue(formData(), "smart_drops"));
17521760
const sqlite = () =>
17531761
(getNestedValue(formData(), "sqlite") as
@@ -1823,6 +1831,55 @@ function ConfigForm(props: {
18231831
</label>
18241832
</div>
18251833

1834+
{/* Pi todowrite */}
1835+
<div class="config-field">
1836+
<div class="config-field-header">
1837+
<span class="config-field-label">Pi Todowrite Tool</span>
1838+
<span class="config-field-key">todowrite.enabled</span>
1839+
</div>
1840+
<span class="config-field-desc">
1841+
Register Magic Context&apos;s Pi <code>todowrite</code> task-list tool.
1842+
Disable this if you use another Pi todo extension. OpenCode has its own
1843+
built-in todowrite, so this setting only affects Pi. Requires /reload or
1844+
restart after changing.
1845+
</span>
1846+
<label class="toggle-switch">
1847+
<input
1848+
type="checkbox"
1849+
checked={todowriteEnabled()}
1850+
onChange={(e) => setTodowrite({ enabled: e.currentTarget.checked })}
1851+
/>
1852+
<span class="toggle-slider" />
1853+
<span class="toggle-label">
1854+
{todowriteEnabled() ? "Enabled" : "Disabled"}
1855+
</span>
1856+
</label>
1857+
</div>
1858+
1859+
<Show when={todowriteEnabled()}>
1860+
<div class="config-field">
1861+
<div class="config-field-header">
1862+
<span class="config-field-label">Pi Todo Overlay</span>
1863+
<span class="config-field-key">todowrite.overlay</span>
1864+
</div>
1865+
<span class="config-field-desc">
1866+
Show the persistent todo overlay above the editor while tasks are active.
1867+
The /todos command and tool remain available when only the overlay is off.
1868+
</span>
1869+
<label class="toggle-switch">
1870+
<input
1871+
type="checkbox"
1872+
checked={todowriteOverlay()}
1873+
onChange={(e) => setTodowrite({ overlay: e.currentTarget.checked })}
1874+
/>
1875+
<span class="toggle-slider" />
1876+
<span class="toggle-label">
1877+
{todowriteOverlay() ? "Enabled" : "Disabled"}
1878+
</span>
1879+
</label>
1880+
</div>
1881+
</Show>
1882+
18261883
{/* Smart drops */}
18271884
<div class="config-field">
18281885
<div class="config-field-header">

packages/dashboard/src/components/ConfigEditor/config-field-coverage.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ export const RENDERED_PREFIXES: readonly string[] = [
6363
// Advanced
6464
"auto_update",
6565
"keep_subagents",
66+
"todowrite",
6667
"smart_drops",
6768
"sqlite",
6869
"system_prompt_injection.enabled",

packages/docs/src/content/docs/reference/configuration.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,9 @@ Global on/off switches for the plugin and its agent-facing surface.
3636
| `language` | string || Output language for Magic Context's generated content and guidance, as a 2-letter ISO 639-1 code (e.g. "tr", "es", "de", "ja", "pt"). When set, the historian, dreamer, sidekick, and the agent-guidance block instruct the model to write its PROSE in this language while keeping all structural tokens (XML tags, the five memory category names, code identifiers, file paths) in English. USER-LEVEL ONLY (ignored in project config for security). Unset = today's behavior (model mirrors the conversation; English scaffolding). Changing it triggers one cache re-materialization; existing compartments/memories keep their original language until naturally rewritten. |
3737
| `auto_update` | boolean || Enable automatic npm self-update checks for the OpenCode plugin. Security: USER-only in config loader, so hostile project configs cannot suppress updates. |
3838
| `keep_subagents` | boolean | `false` | Debug: keep the child sessions Magic Context spawns for its own subagents (historian, dreamer, sidekick, memory-migration) instead of deleting them on success. Useful for short-term inspection/data collection — their full transcript (prompt, tool calls, token usage, output) stays in the host session store. Kept sessions accumulate until manually cleared; leave false for normal use. Requires a restart to take effect. |
39+
| `todowrite` | object || Pi-only todowrite tool and overlay controls. Pi registers tools and widgets at extension boot, so changing this after /cd requires /reload or restart. |
40+
| `todowrite.enabled` | boolean | `true` | Pi only: register Magic Context's todowrite task-list tool. Disable if you use your own todo extension. OpenCode ships its own built-in todowrite; this setting has no effect there. |
41+
| `todowrite.overlay` | boolean | `true` | Pi only: show the persistent todo overlay above the editor while tasks are active. |
3942

4043
## Context management
4144

packages/pi-plugin/src/index.ts

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,11 @@ import {
143143
} from "./system-prompt";
144144
import { withTimeout } from "./timeout";
145145
import { registerMagicContextTools } from "./tools";
146+
import {
147+
registerTodoOverlay,
148+
registerTodoStateLifecycle,
149+
setTodoSnapshot,
150+
} from "./tools/todo-view-pi";
146151

147152
const PREFIX = "[magic-context][pi]";
148153

@@ -737,12 +742,19 @@ export default async function (pi: ExtensionAPI): Promise<void> {
737742

738743
const bootProjectDeps = buildProjectDeps(projectDir, projectIdentity, config);
739744
projectDepsByDir.set(projectDir, bootProjectDeps);
745+
const todowriteEnabled = bootProjectDeps.config.todowrite.enabled !== false;
746+
const todowriteOverlayEnabled =
747+
todowriteEnabled && bootProjectDeps.config.todowrite.overlay !== false;
740748

741749
// Register the agent-facing tools. Reuses the same business logic
742750
// the OpenCode plugin uses (insertMemory, unifiedSearch, addNote, …)
743751
// via the shared cortexkit DB. Cross-harness memory sharing is automatic
744752
// because both plugins resolve the same project identity for the same
745753
// directory.
754+
// Pi registers tools, commands, and widgets once at extension boot. Therefore
755+
// `todowrite.enabled` follows the boot project's config: after `/cd` into a
756+
// project with a different value, users need `/reload` or a Pi restart for the
757+
// tool/command/overlay surface to change, matching Pi's registration lifecycle.
746758
registerMagicContextTools(pi, {
747759
db,
748760
ensureProjectRegistered: ensureProjectRegisteredFromPiDirectory,
@@ -771,9 +783,28 @@ export default async function (pi: ExtensionAPI): Promise<void> {
771783
dreamerEnabled: isDreamerRunnable(config),
772784
resolveDreamerEnabled: (ctx) =>
773785
resolveCurrentProjectDeps(ctx).dreamerEnabled,
786+
todowriteEnabled,
774787
});
775788
info(
776-
"registered tools: ctx_search, ctx_memory, ctx_note, ctx_expand, ctx_reduce",
789+
todowriteEnabled
790+
? "registered tools: ctx_search, ctx_memory, ctx_note, ctx_expand, todowrite, ctx_reduce; registered /todos"
791+
: "registered tools: ctx_search, ctx_memory, ctx_note, ctx_expand, ctx_reduce (todowrite disabled)",
792+
);
793+
794+
const readLastTodoState = (sessionId: string) =>
795+
getOrCreateSessionMeta(db, sessionId).lastTodoState;
796+
if (todowriteEnabled) {
797+
registerTodoStateLifecycle(pi, { readLastTodoState });
798+
}
799+
const todoOverlay = todowriteOverlayEnabled
800+
? registerTodoOverlay(pi, {
801+
readLastTodoState,
802+
})
803+
: undefined;
804+
info(
805+
todowriteOverlayEnabled
806+
? "registered todowrite overlay"
807+
: "registered todowrite overlay: DISABLED (todowrite.enabled=false or todowrite.overlay=false)",
777808
);
778809

779810
// Register the per-LLM-call transform pipeline. Tags eligible message
@@ -1439,6 +1470,10 @@ export default async function (pi: ExtensionAPI): Promise<void> {
14391470
const sessionMeta = Array.isArray(todos)
14401471
? getOrCreateSessionMeta(db, sessionId)
14411472
: null;
1473+
if (todowriteEnabled && Array.isArray(todos)) {
1474+
setTodoSnapshot(sessionId, todos);
1475+
todoOverlay?.update(sessionId);
1476+
}
14421477

14431478
// Synthetic-todowrite snapshot capture (Pi parity with
14441479
// OpenCode hook-handlers.ts:386-401). Persist normalized
@@ -1714,6 +1749,10 @@ export default async function (pi: ExtensionAPI): Promise<void> {
17141749
if (!Array.isArray(todos)) continue;
17151750
const normalized = normalizeTodoStateJson(todos);
17161751
if (normalized === null) continue;
1752+
if (todowriteEnabled) {
1753+
setTodoSnapshot(sessionId, todos);
1754+
todoOverlay?.update(sessionId);
1755+
}
17171756
updateSessionMeta(db, sessionId, {
17181757
lastTodoState: normalized,
17191758
});

packages/pi-plugin/src/subagent-entry.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,10 +110,12 @@ export default function magicContextSubagentExtension(pi: ExtensionAPI): void {
110110
// child session, so session-scoped ctx_note/ctx_expand would write
111111
// orphaned notes / expand an empty transcript. Drop them; keep ctx_search.
112112
sessionScopedToolsDisabled: true,
113+
todowriteEnabled: cfg.todowrite.enabled !== false,
114+
todowriteCommandEnabled: false,
113115
});
114116

115117
log(
116-
`[pi-subagent] registered tools: ctx_search${dreamerActionsEnabled ? ", ctx_memory" : ""}` +
118+
`[pi-subagent] registered tools: ctx_search${dreamerActionsEnabled ? ", ctx_memory" : ""}${cfg.todowrite.enabled !== false ? ", todowrite" : ""}` +
117119
` (ctx_note/ctx_expand omitted: --no-session child;` +
118120
` memory=${cfg.memory.enabled}, embedding=${cfg.embedding.provider !== "off"},` +
119121
` git_commits=${cfg.memory.git_commit_indexing.enabled}, dreamer_actions=${dreamerActionsEnabled})`,

packages/pi-plugin/src/tools/index.test.ts

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,22 +8,29 @@ describe("registerMagicContextTools", () => {
88
const db = createTestDb();
99
try {
1010
const registered: string[] = [];
11+
const commands: string[] = [];
1112
const pi = {
1213
registerTool: (tool: { name: string }) => {
1314
registered.push(tool.name);
1415
},
16+
registerCommand: (name: string) => {
17+
commands.push(name);
18+
},
1519
} as never;
1620

1721
registerMagicContextTools(pi, {
1822
db,
1923
memoryToolEnabled: false,
2024
sessionScopedToolsDisabled: true,
25+
todowriteCommandEnabled: false,
2126
});
2227

2328
expect(registered).toContain("ctx_search");
2429
expect(registered).not.toContain("ctx_memory");
2530
expect(registered).not.toContain("ctx_note");
2631
expect(registered).not.toContain("ctx_expand");
32+
expect(registered).toContain("todowrite");
33+
expect(commands).not.toContain("todos");
2734
} finally {
2835
closeQuietly(db);
2936
}
@@ -43,6 +50,7 @@ describe("registerMagicContextTools", () => {
4350
}) => {
4451
registered.set(tool.name, tool);
4552
},
53+
registerCommand: () => undefined,
4654
} as never;
4755

4856
registerMagicContextTools(pi, {
@@ -75,4 +83,62 @@ describe("registerMagicContextTools", () => {
7583
closeQuietly(db);
7684
}
7785
});
86+
87+
it("registers todowrite and /todos by default", () => {
88+
const db = createTestDb();
89+
try {
90+
const registered: string[] = [];
91+
const commands: string[] = [];
92+
const pi = {
93+
registerTool: (tool: { name: string }) => registered.push(tool.name),
94+
registerCommand: (name: string) => commands.push(name),
95+
} as never;
96+
97+
registerMagicContextTools(pi, { db });
98+
99+
expect(registered).toContain("todowrite");
100+
expect(commands).toContain("todos");
101+
} finally {
102+
closeQuietly(db);
103+
}
104+
});
105+
106+
it("omits todowrite and /todos when todowrite is disabled", () => {
107+
const db = createTestDb();
108+
try {
109+
const registered: string[] = [];
110+
const commands: string[] = [];
111+
const pi = {
112+
registerTool: (tool: { name: string }) => registered.push(tool.name),
113+
registerCommand: (name: string) => commands.push(name),
114+
} as never;
115+
116+
registerMagicContextTools(pi, { db, todowriteEnabled: false });
117+
118+
expect(registered).toContain("ctx_search");
119+
expect(registered).not.toContain("todowrite");
120+
expect(commands).not.toContain("todos");
121+
} finally {
122+
closeQuietly(db);
123+
}
124+
});
125+
126+
it("can keep /todos off for lean subagent entries", () => {
127+
const db = createTestDb();
128+
try {
129+
const registered: string[] = [];
130+
const commands: string[] = [];
131+
const pi = {
132+
registerTool: (tool: { name: string }) => registered.push(tool.name),
133+
registerCommand: (name: string) => commands.push(name),
134+
} as never;
135+
136+
registerMagicContextTools(pi, { db, todowriteCommandEnabled: false });
137+
138+
expect(registered).toContain("todowrite");
139+
expect(commands).not.toContain("todos");
140+
} finally {
141+
closeQuietly(db);
142+
}
143+
});
78144
});

packages/pi-plugin/src/tools/index.ts

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import { createCtxMemoryTool } from "./ctx-memory";
1919
import { createCtxNoteTool } from "./ctx-note";
2020
import { createCtxReduceTool } from "./ctx-reduce";
2121
import { createCtxSearchTool } from "./ctx-search";
22+
import { registerTodosCommand } from "./todo-view-pi";
2223
import { createTodowriteTool } from "./todowrite";
2324

2425
export interface RegisterToolsOptions {
@@ -55,6 +56,10 @@ export interface RegisterToolsOptions {
5556
* child session, so ctx_note would write notes orphaned under the hidden
5657
* child id and ctx_expand would expand the child's empty transcript. */
5758
sessionScopedToolsDisabled?: boolean;
59+
/** When false, omit Magic Context's Pi todowrite tool entirely. */
60+
todowriteEnabled?: boolean;
61+
/** Main Pi entry registers /todos; lean subagent entries keep commands off. */
62+
todowriteCommandEnabled?: boolean;
5863
}
5964

6065
export function registerMagicContextTools(
@@ -100,13 +105,18 @@ export function registerMagicContextTools(
100105
pi.registerTool(createCtxExpandTool({ db: opts.db }));
101106
}
102107

103-
// `todowrite` parity with OpenCode. Pi-coding-agent has no built-in
104-
// task list tool, so without this the synthetic-todowrite injector
105-
// would never have anything to surface. The tool just captures the
106-
// `todos` arg and echoes a pretty-printed JSON ack; `message_end`
107-
// in index.ts snapshots `params.todos` into `session_meta.last_todo_state`
108-
// for downstream synthesis. See `tools/todowrite.ts` header for rationale.
109-
pi.registerTool(createTodowriteTool());
108+
if (opts.todowriteEnabled !== false) {
109+
// `todowrite` parity with OpenCode. Pi-coding-agent has no built-in
110+
// task list tool, so without this the synthetic-todowrite injector
111+
// would never have anything to surface. The tool just captures the
112+
// `todos` arg and echoes a pretty-printed JSON ack; `message_end`
113+
// in index.ts snapshots `params.todos` into `session_meta.last_todo_state`
114+
// for downstream synthesis. See `tools/todowrite.ts` header for rationale.
115+
pi.registerTool(createTodowriteTool());
116+
if (opts.todowriteCommandEnabled !== false) {
117+
registerTodosCommand(pi);
118+
}
119+
}
110120

111121
// ctx_reduce is session-scoped just like ctx_note/ctx_expand: it resolves the
112122
// CURRENT session id at call time. Omit it for `--no-session` children where

0 commit comments

Comments
 (0)