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
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,10 +53,10 @@ After compaction, the model calls `planner_status`, reloads from persisted JSON/

| Command | Purpose |
| --- | --- |
| `/planner-create` | Create a new plan from a multiline request. |
| `/planner-improve` | Discovery-first self-improvement plan. |
| `/planner-preview` | Check out the plan branch in your main repo to browse accumulated files. Run again for status. `/planner-finish` restores your branch automatically. |
| `/planner-resume` | Pick a plan and resume its worktree session. |
| `/planner-create` | Create a new plan from a multiline request. Opens the planner workspace. |
| `/planner-improve` | Discovery-first self-improvement plan. Opens the planner workspace. |
| `/planner-resume` | Pick a plan and resume its worktree session. Opens the planner workspace. |
| `/planner-dashboard` | Open the planner workspace: live stage dashboard, task list, and the model chat in one window. Opens automatically for planner-worktree sessions. |
| `/planner-helper` | Show current effective settings and planner behavior. |
| `/planner-skills` | Search, view, and delete planner-generated skills. |
| `/planner-finish` | Export `output/<plan-id>`, remove temporary planner state, return Pi to the original session. |
Expand Down
47 changes: 47 additions & 0 deletions SETTINGS.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,11 @@ getAgentDir()/extensions/pi-code-planner/settings.json
},
"requireAfterTdd": true,
"requireBeforeEditOutsideChain": true
},
"workspace": {
"enabled": true,
"autoOpen": true,
"footerReserveRows": 3
}
}
```
Expand Down Expand Up @@ -126,6 +131,48 @@ Planner-generated skills are stored under `getAgentDir()/extensions/pi-code-plan
| `contracts.requireAfterTdd` | `true` | Require `execution/contract_check` after a green implementation. |
| `contracts.requireBeforeEditOutsideChain` | `true` | Instruct the model to route/read contracts before leaving declared task scope. |

## Workspace (TUI)

`/planner-dashboard` opens the planner workspace: the stage dashboard and the model chat in one window. It also opens automatically for planner-worktree sessions (after `/planner-create`, `/planner-resume`, `/planner-improve`).

| Setting | Default | Purpose |
| --- | --- | --- |
| `workspace.enabled` | `true` | Master switch for the workspace window. |
| `workspace.autoOpen` | `true` | Open the workspace automatically for planner-worktree sessions. |
| `workspace.footerReserveRows` | `3` | Terminal rows left for Pi's native footer below the workspace overlay. Raise if the footer overlaps; lower if there is a gap. `0`–`20`. |

### Workspace keys

Inside the workspace, `Tab` cycles three focus panes:

| Pane | Keys |
| --- | --- |
| input | type or paste, `Enter` to send to the model |
| chat | `↑`/`↓`, `PageUp`/`PageDown` scroll; `End` jumps back to the live tail, `Home` to the top; `x` toggles expand-all for collapsed tool calls |
| tasks | `↑`/`↓` select a task and reveal the task list + stage timings; `←`/`→` nudge the ticker |

While scrolled up, the transcript stays anchored — new streamed output appends below without moving your view. Press `End` to jump back to the live tail. History is projected as a sliding window over the session (a chunk of trailing entries); scrolling to the top loads the next older chunk, so very long sessions never project the whole conversation at once.

Pasting text into the input works (bracketed paste is handled; newlines fold to spaces). Pasting **images** is not supported in the workspace window — Pi's image paste targets its built-in editor, which the workspace replaces; close the workspace (`Esc`) to use the plain editor for image input.

The workspace also inherits two Pi bindings (work in any pane): `app.thinking.toggle` (default `Ctrl+T`) shows/hides thinking blocks, and `app.tools.expand` (default `Ctrl+O`) expands/collapses tool output. Rebind them in `~/.pi/agent/keybindings.json`.

The workspace's own keys are configurable in planner settings (Pi's `keybindings.json` only accepts Pi's built-in action ids, not ours). Override any of them under `workspace.keys`; omitted actions keep their defaults:

```json
{ "workspace": { "keys": { "jumpBottom": ["end", "ctrl+e"], "expand": ["x", "o"] } } }
```

Actions: `focusNext` (`tab`), `up` (`up`), `down` (`down`), `pageUp` (`pageUp`), `pageDown` (`pageDown`), `jumpBottom` (`end`), `jumpTop` (`home`), `expand` (`x`), `submit` (`enter`), `exit` (`escape`). `Ctrl+C` always exits regardless of overrides.

The line under the stage ribbon is a static context line (active task, branch, or a blocking note), clipped with `…` — it does not scroll, so it never forces a repaint.

`Esc` (or `Ctrl+C`) closes the workspace and returns to the plain chat. Streaming assistant output appears live, token by token.

### Pi keybindings

The workspace keys above are handled by the extension. Pi's own shortcuts (cursor movement, model/thinking selectors, tool expansion, etc.) are configured globally in `~/.pi/agent/keybindings.json` using namespaced ids such as `tui.editor.cursorUp` and `app.tools.expand`. Each id maps to one key or an array of keys; run `/reload` after editing. See the Pi keybindings documentation for the full list.

## Instruction Append Files

Place extra instructions (test commands, architecture notes, commit style) under:
Expand Down
59 changes: 58 additions & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,10 +44,12 @@
"test:watch": "vitest"
},
"peerDependencies": {
"@earendil-works/pi-coding-agent": "*"
"@earendil-works/pi-coding-agent": "*",
"@earendil-works/pi-tui": "*"
},
"devDependencies": {
"@earendil-works/pi-coding-agent": "0.75.4",
"@earendil-works/pi-tui": "0.75.4",
"@biomejs/biome": "^2.4.15",
"@types/node": "^24.0.0",
"typescript": "^6.0.3",
Expand Down
89 changes: 38 additions & 51 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,10 @@ import {
PLANNER_CONTRACT_TOOL_NAMES,
type PlannerContractToolName,
} from "./runtime/contracts";
import {
openPlannerWorkspace,
registerPlannerDashboard,
} from "./runtime/dashboard";
import {
DEBUG_INSTRUMENTATION_TYPES,
DEBUG_PROBE_METHODS,
Expand Down Expand Up @@ -1111,13 +1115,47 @@ export default function piCodePlannerExtension(pi: ExtensionAPI): void {
registerPlannerTools(pi, compactRuntime);
registerPlannerIdleWatchdog(pi, idleRuntime);
registerPlannerRuntimeTimer(pi, timerRuntime);
registerPlannerDashboard(pi);
registerPlannerWorkspaceAutoOpen(pi);
registerPlannerBuiltinToolGuard(pi);
registerPlannerCompactEvents(pi, compactRuntime);
registerPlannerSkillResources(pi);
registerInstructionDefaultsSync(pi);
registerPlannerToolVisibility(pi);
}

/**
* Auto-open the planner workspace once per session when the active session is a
* planner worktree session. This covers /planner-create, /planner-resume, and
* /planner-improve (each hands off to a worktree session) without coupling to
* the fragile session-switch flow. The user can reopen anytime with
* /planner-dashboard, or close the workspace to fall back to the plain chat.
*/
function registerPlannerWorkspaceAutoOpen(pi: ExtensionAPI): void {
const openedSessions = new Set<string>();
pi.on("session_start", async (_event, ctx) => {
if (!ctx.hasUI) return;
try {
const sessionId = ctx.sessionManager.getSessionId();
if (openedSessions.has(sessionId)) return;
const fs = createNodeFs();
const projectPaths = await resolveProjectStoragePaths({
fs,
agentDir: getAgentDir(),
cwd: ctx.cwd,
});
const context = await readActivePlanContext({ fs, projectPaths });
if (context.status !== "ready") return;
const worktreePath = context.state.worktreePath;
if (!worktreePath || !isPathInsideOrEqual(ctx.cwd, worktreePath)) return;
openedSessions.add(sessionId);
void openPlannerWorkspace(pi, ctx, { auto: true });
} catch {
// Best-effort: never block session start on the workspace.
}
});
}

function registerPlannerSkillResources(pi: ExtensionAPI): void {
pi.on("resources_discover", async (event) => {
const fs = createNodeFs();
Expand Down Expand Up @@ -1936,57 +1974,6 @@ function registerPlannerCommands(pi: ExtensionAPI): void {
},
});

pi.registerCommand("planner-preview", {
description:
"Show the planner worktree path so you can open it in your editor and browse the current plan files directly.",
handler: async (_args, ctx) => {
await ctx.waitForIdle();
const fs = createNodeFs();
const git = new NodeGitRunner();
try {
const agentDir = getAgentDir();
const projectPaths = await resolveProjectStoragePaths({
fs,
agentDir,
cwd: ctx.cwd,
});
const project = await readProjectRecordIfExists(fs, projectPaths);
const activePlanId = project?.activePlanId;
if (!activePlanId) {
ctx.ui.notify("No active plan to preview.", "info");
return;
}
const planPaths = createPlanStoragePaths(projectPaths, activePlanId);
const state = await readPlanStateIfExists(fs, planPaths);
if (!state?.worktreePath) {
ctx.ui.notify("Plan worktree not found.", "warning");
return;
}
const worktreePath = state.worktreePath;
const currentBranch = await git
.currentBranch({ repoRoot: worktreePath })
.catch(() => state.currentBranch ?? "(unknown)");
const planBranch = state.activeBranches.plan;
const onPlanBranch = currentBranch === planBranch;
ctx.ui.notify(
[
`Planner worktree: ${worktreePath}`,
`Branch: ${currentBranch}`,
onPlanBranch
? "Open this folder in your editor to browse all completed plan work."
: `Currently on a task branch. Plan branch (${planBranch}) contains all completed tasks merged so far.`,
].join("\n"),
"info",
);
} catch (error) {
ctx.ui.notify(
`Planner preview failed: ${errorMessage(error)}`,
"error",
);
}
},
});

pi.registerCommand("planner-finish", {
description:
"Finish the completed planner result, keep one output branch, clean temporary planner state, and return to the original project session.",
Expand Down
29 changes: 28 additions & 1 deletion src/runtime/about.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,21 @@ const SETTING_DESCRIPTORS: SettingDescriptor[] = [
purpose:
"Instruct the model to route/read contracts before leaving declared task scope.",
},
{
path: "workspace.enabled",
purpose:
"Master switch for the /planner-dashboard workspace window (dashboard + model chat).",
},
{
path: "workspace.autoOpen",
purpose:
"Open the workspace automatically for planner-worktree sessions (create/resume/improve).",
},
{
path: "workspace.footerReserveRows",
purpose:
"Terminal rows left for Pi's native footer below the workspace overlay (raise if the footer overlaps).",
},
{
path: "metadata.humanLanguage",
purpose: "Default language for user-facing planner text.",
Expand Down Expand Up @@ -181,6 +196,18 @@ export function buildPlannerAboutReport(input: {
"- /planner-skills shows the saved inventory even when runtime exposure is disabled or capped.",
"- Missing SKILL.md files are ignored by inventory/resource discovery; delete stale index entries through /planner-skills when needed.",
"",
"## Planner Workspace TUI",
"- /planner-dashboard opens the workspace: stage dashboard + the model chat in one window. It also opens automatically for planner-worktree sessions (workspace.autoOpen).",
"- Inside the workspace, Tab cycles three focus panes:",
" - input: type or paste, press Enter to send a message to the model.",
" - chat: ↑↓ / PageUp / PageDown scroll; End jumps to the live tail, Home to the top; x toggles expand-all for tool calls.",
" - tasks: ↑↓ select a task and reveal the task list + stage timings; ←→ nudge the ticker.",
"- Inherits Pi bindings: app.thinking.toggle (Ctrl+T) hides/shows thinking; app.tools.expand (Ctrl+O) expands/collapses tool output.",
"- Esc (or Ctrl+C) closes the workspace and returns to the plain chat.",
"- Streaming assistant output is shown live, token by token, as it is generated.",
"- Pi's own keys (cursor movement, model/thinking selectors, etc.) are configured in ~/.pi/agent/keybindings.json; run /reload after editing. See SETTINGS.md and the Pi keybindings docs.",
"- If Pi's native footer overlaps or leaves a gap below the workspace, tune workspace.footerReserveRows.",
"",
"## Effective Settings",
"Settings merge order: defaults, global settings, then project settings.",
"",
Expand All @@ -192,7 +219,7 @@ export function buildPlannerAboutReport(input: {
"",
"## Notes",
"- worktree and compact settings are captured when a plan is created.",
"- idle, timer, metadata, skills, and contracts settings are read while planner runs.",
"- idle, timer, metadata, skills, contracts, and workspace settings are read while planner runs.",
"- skills.maxActive = 0 means no planner-side limit.",
].join("\n");
}
Expand Down
Loading
Loading