Reviewed linked issue and PR details.
UnderstandAnalyzed code changes in PR.
UnderstandChecked for changelog entry.
UnderstandAdded changelog entry for fix.
EditMerged PR and updated main branch.
CleanupYou are given one or more GitHub PR URLs: https://github.com/badlogic/pi-mono/pull/947
For each PR URL, do the following in order:
packages/*/CHANGELOG.md files. Report whether an entry exists. If missing, state that a changelog entry is required before merge and that you will add it if the user decides to merge. Follow the changelog format rules in AGENTS.md. Verify:
### Breaking Changes, ### Added, ### Fixed, etc.)Fixed foo ([#123](https://github.com/badlogic/pi-mono/pull/123) by [@user](https://github.com/user))### Breaking Changes, not just ### FixedOutput format per PR: PR: Changelog:
If no issues are found, say so under Bad and Ugly.
>cd /Users/badlogic/workspaces/pi-mono && gh pr view 947 --json title,body,comments,commits,files,author,state,labels,baseRefName,headRefName{"author":{"id":"MDQ6VXNlcjEwNDQzMDA=","is_bot":false,"login":"Perlence","name":"Sviatoslav Abakumov"},"baseRefName":"main","body":"### Problem\r\n\r\nExtension shortcuts registered via `registerShortcut()` were not firing when the extension also used `setEditorComponent()`. This happened because `setEditorComponent()` copied `onExtensionShortcut` from `defaultEditor` at creation time, capturing `undefined` because `setupExtensionShortcuts()` hadn't run yet.\r\n\r\nThis regression was introduced in commit b846a4bf #645.\r\n\r\nAn excerpt of the extension:\r\n\r\n```typescript\r\nexport default function (pi: ExtensionAPI) {\r\n\tpi.on(\"session_start\", (_event, ctx) => {\r\n\t\tctx.ui.setEditorComponent((tui, theme, keybindings) => {\r\n\t\t\treturn new MyEditor(tui, theme, keybindings);\r\n\t\t});\r\n\t});\r\n\r\n\tpi.registerShortcut(Key.ctrl(\"x\"), {\r\n\t\tdescription: \"Copy prompt to clipboard\",\r\n\t\thandler: async (ctx) => {\r\n\t\t\t// ...\r\n\t\t},\r\n\t});\r\n}\r\n```\r\n\r\nHere's the flow:\r\n\r\n1. `InteractiveMode.initExtensions()` runs\r\n2. `bindExtensions()` emits `session_start`\r\n3. Extension's `session_start` handler calls `ctx.ui.setEditorComponent(factory)`\r\n4. `setCustomEditorComponent()` runs:\r\n \r\n ```\r\n // Line 1580 in interactive-mode.ts\r\n customEditor.onExtensionShortcut = this.defaultEditor.onExtensionShortcut;\r\n // ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\r\n // This is UNDEFINED at this point!\r\n ```\r\n \r\n5. `this.editor = customEditor` (now using custom editor)\r\n6. `setupExtensionShortcuts()` sets `defaultEditor.onExtensionShortcut = handler`\r\n - But we're not using `defaultEditor` anymore!\r\n7. User presses shortcut → `customEditor.onExtensionShortcut(key)` → `undefined(key)` → nothing happens ✗\r\n\r\n### Solution\r\n\r\nThe fix is to delegate to `defaultEditor.onExtensionShortcut` at call time.\r\n","comments":[],"commits":[{"authoredDate":"2026-01-25T16:39:33Z","authors":[{"email":"[REDACTED]","id":"MDQ6VXNlcjEwNDQzMDA=","login":"Perlence","name":"Sviatoslav Abakumov"}],"committedDate":"2026-01-25T16:58:11Z","messageBody":"Extension shortcuts registered via registerShortcut() were not firing\nwhen the extension also used setEditorComponent(). This happened because\nsetEditorComponent() copied onExtensionShortcut from defaultEditor at\ncreation time, capturing undefined if setupExtensionShortcuts() hadn't\nrun yet.\n\nThe fix is to delegate to defaultEditor.onExtensionShortcut at call\ntime.","messageHeadline":"fix(coding-agent): delegate extension shortcuts instead of copying","oid":"3acb58aabd69f0040efb31995267df5200dadfcd"}],"files":[{"path":"packages/coding-agent/src/modes/interactive/interactive-mode.ts","additions":1,"deletions":1}],"headRefName":"fix/extension-shortcut-timing","labels":[],"state":"OPEN","title":"fix(coding-agent): delegate extension shortcuts instead of copying"}>cd /Users/badlogic/workspaces/pi-mono && gh pr diff 947diff --git a/packages/coding-agent/src/modes/interactive/interactive-mode.ts b/packages/coding-agent/src/modes/interactive/interactive-mode.ts
index d815c7aee..bf2568184 100644
--- a/packages/coding-agent/src/modes/interactive/interactive-mode.ts
+++ b/packages/coding-agent/src/modes/interactive/interactive-mode.ts
@@ -1575,7 +1575,7 @@ export class InteractiveMode {
customEditor.onEscape = this.defaultEditor.onEscape;
customEditor.onCtrlD = this.defaultEditor.onCtrlD;
customEditor.onPasteImage = this.defaultEditor.onPasteImage;
- customEditor.onExtensionShortcut = this.defaultEditor.onExtensionShortcut;
+ customEditor.onExtensionShortcut = (data: string) => this.defaultEditor.onExtensionShortcut?.(data);
// Copy action handlers (clear, suspend, model switching, etc.)
for (const [action, handler] of this.defaultEditor.actionHandlers) {
(customEditor.actionHandlers as Map<string, () => void>).set(action, handler);Let me read the linked issue #645 and the relevant code file:
>cd /Users/badlogic/workspaces/pi-mono && gh issue view 645 --json title,body,comments,labels,state{"body":"## Summary\n\nSupersedes #326 and #562. Comprehensive plan for extension package management, loading, and hot reload.\n\n---\n\n## Architecture Overview\n\n```\n┌─────────────────┐ ┌────────────────┐ ┌──────────────────┐\n│ PackageManager │────▶│ Loader │────▶│ AgentSession │\n│ │ │ │ │ │\n│ - Resolves │ │ - Loads from │ │ - Uses loaded │\n│ sources to │ │ paths │ │ content │\n│ paths │ │ - Applies │ │ - reload() takes │\n│ - Reads pi │ │ filters │ │ LoadResult │\n│ field from │ │ - Returns │ │ - Calls runner │\n│ package.json │ │ LoadResult │ │ initializer │\n└─────────────────┘ └────────────────┘ └──────────────────┘\n```\n\n---\n\n## Phase 1: Package Management\n\n### Sources\n\n| Source | Example | Global Install Location | Local Install Location |\n|--------|---------|------------------------|------------------------|\n| npm | `npm:@foo/bar@1.0.0` | Global node_modules (`npm root -g`) | `<cwd>/.pi/npm/node_modules/` |\n| git | `git:github.com/user/repo@v1` | `~/.pi/agent/git/<host>/<path>/` | `<cwd>/.pi/git/<host>/<path>/` |\n| local | `./my-extension/` | Used directly | Used directly |\n\n### Commands\n\n```bash\n# Global (default)\npi install npm:@foo/bar@1.0.0 # npm install -g @foo/bar@1.0.0\npi install git:github.com/u/r@v1 # clone to ~/.pi/agent/git/github.com/u/r/\npi install ./my-extension/ # add to settings.json, npm install if package.json exists\n\n# Local (-l flag)\npi install -l npm:@foo/bar@1.0.0 # add to .pi/npm/package.json, npm install\npi install -l git:github.com/u/r@v1\n\n# Management\npi remove npm:@foo/bar # uninstall + remove from settings\npi update # update all unpinned packages\npi update npm:@foo/bar # update specific package\n```\n\n### settings.json\n\nSource of truth for what to load:\n\n```json\n{\n \"extensions\": [\n \"npm:@foo/bar@1.0.0\",\n \"git:github.com/user/repo@v1.0\",\n \"./local-extension/\"\n ]\n}\n```\n\n- Global: `~/.pi/agent/settings.json`\n- Local: `<cwd>/.pi/settings.json` (with `-l` flag)\n- Local overrides/merges with global (existing behavior)\n\n### Version Pinning\n\n- `@version` pinned → `pi update` is no-op\n- No version → `pi update` pulls latest\n\n### Package Format\n\n**Auto-discovery (no package.json needed):**\n- `extensions/*.ts` or `extensions/*/index.ts`\n- `skills/*.md` or `skills/*/SKILL.md`\n- `prompts/*.md`\n- `themes/*.json`\n\n**Explicit declaration via package.json `pi` field:**\n```json\n{\n \"name\": \"@foo/pi-extension-pack\",\n \"pi\": {\n \"extensions\": [\"./src/index.ts\", \"./src/other.ts\"],\n \"skills\": [\"./skills/\"],\n \"prompts\": [\"./prompts/\"],\n \"themes\": [\"./themes/dark.json\"]\n }\n}\n```\n\nPackageManager reads the `pi` field and returns those paths. If no `pi` field, returns the directory for default discovery rules.\n\n### PackageManager API\n\n```typescript\ninterface PackageManager {\n resolve(onMissing?: (source: string) => Promise<\"install\" | \"skip\" | \"error\">): Promise<ResolvedPaths>;\n install(source: string, options?: { local?: boolean }): Promise<void>;\n remove(source: string, options?: { local?: boolean }): Promise<void>;\n update(source?: string): Promise<void>;\n}\n\ninterface ResolvedPaths {\n extensions: string[]; // files or folders\n skills: string[];\n prompts: string[];\n themes: string[];\n agentsMd: string[];\n systemMd: string[];\n}\n```\n\n---\n\n## Phase 2: Loader\n\n### Loader API\n\n```typescript\ntype LoaderConfig = ResolvedPaths & { filters?: LoaderFilters };\n\n// Static config OR async callback (re-reads settings on each load)\ntype LoaderConfigProvider = LoaderConfig | (() => LoaderConfig | Promise<LoaderConfig>);\n\nfunction createLoader(configOrProvider: LoaderConfigProvider): Loader;\n\ninterface Loader {\n load(): Promise<LoadResult>;\n}\n```\n\n### LoaderFilters\n\n```typescript\ninterface LoaderFilters {\n extensions?: { enabled?: boolean; include?: string[]; exclude?: string[] };\n skills?: {\n enabled?: boolean;\n sources?: { codexUser?: boolean; claudeUser?: boolean; claudeProject?: boolean; piUser?: boolean; piProject?: boolean };\n include?: string[];\n exclude?: string[];\n };\n prompts?: { enabled?: boolean; include?: string[]; exclude?: string[] };\n themes?: { enabled?: boolean; include?: string[]; exclude?: string[] };\n}\n```\n\n### LoadResult\n\n```typescript\ninterface LoadResult {\n extensions: LoadExtensionsResult;\n skills: Skill[];\n skillWarnings: SkillWarning[];\n promptTemplates: PromptTemplate[];\n themes: Theme[];\n contextFiles: Array<{ path: string; content: string }>;\n}\n```\n\n---\n\n## Phase 3: Bootstrap Flow (main.ts)\n\nExtensions can register CLI flags via `pi.registerFlag()`. This requires loading extensions BEFORE full CLI parsing.\n\n```typescript\n// 1. First pass parse - get extension paths, --no-extensions, etc.\nconst firstPass = parseArgs(args);\n\n// 2. Setup managers\nconst settingsManager = SettingsManager.create(cwd, agentDir);\nconst packageManager = createPackageManager({ cwd, agentDir });\n\n// 3. Create loader with config callback (captures firstPass for filters)\nconst loader = createLoader(async () => {\n const paths = await packageManager.resolve();\n return {\n ...paths,\n filters: {\n extensions: { enabled: !firstPass.noExtensions, additional: firstPass.extensions },\n skills: { enabled: !firstPass.noSkills },\n },\n };\n});\n\n// 4. Load early to discover extension flags\nconst loaded = await loader.load();\n\n// 5. Collect extension flags\nconst extensionFlags = new Map();\nfor (const ext of loaded.extensions.extensions) {\n for (const [name, flag] of ext.flags) {\n extensionFlags.set(name, { type: flag.type });\n }\n}\n\n// 6. Second pass parse with extension flags\nconst parsed = parseArgs(args, extensionFlags);\n\n// 7. Set flag values in runtime\nfor (const [name, value] of parsed.unknownFlags) {\n loaded.extensions.runtime.flagValues.set(name, value);\n}\n\n// 8. Create session\nconst session = await AgentSession.create({ ...loaded, model, sessionManager, settingsManager, modelRegistry, cwd });\n\n// 9. Run mode\nconst mode = new InteractiveMode(session, loader, parsed);\nawait mode.run();\n```\n\n---\n\n## Phase 4: Runner Initialization\n\nThe ExtensionRunner requires mode-specific initialization:\n\n1. `runner.initialize(actions, contextActions, commandContextActions, uiContext)`\n2. `runner.onError(handler)`\n3. `setupExtensionShortcuts(runner)` (interactive only)\n4. `runner.emit({ type: \"session_start\" })`\n\n**Solution: Runner Initializer Callback**\n\nSession stores a runner initializer provided by the mode. Called on create and reload.\n\n```typescript\ntype RunnerInitializer = (runner: ExtensionRunner) => void | Promise<void>;\n\nclass InteractiveMode {\n constructor(session, loader, parsed) {\n session.setRunnerInitializer(async (runner) => {\n runner.initialize(actions, contextActions, commandContextActions, uiContext);\n runner.onError((e) => this.showExtensionError(...));\n this.setupExtensionShortcuts(runner);\n });\n }\n \n async handleReload() {\n const newLoaded = await this.loader.load();\n // Transfer flag values from old runtime to new\n const oldRuntime = this.session.extensionRunner?.runtime;\n if (oldRuntime) {\n for (const [k, v] of oldRuntime.flagValues) {\n newLoaded.extensions.runtime.flagValues.set(k, v);\n }\n }\n // Session handles: shutdown → create runner → call initializer → session_start\n await this.session.reload(newLoaded);\n this.applyThemes(newLoaded.themes);\n }\n}\n```\n\n---\n\n## Phase 5: AgentSession Reload\n\n```typescript\nclass AgentSession {\n private runnerInitializer?: RunnerInitializer;\n \n setRunnerInitializer(init: RunnerInitializer) {\n this.runnerInitializer = init;\n if (this._extensionRunner && init) {\n init(this._extensionRunner).then(() => this._extensionRunner?.emit({ type: \"session_start\" }));\n }\n }\n \n async reload(loaded: LoadResult): Promise<void> {\n // 1. Shutdown old extensions\n await this._extensionRunner?.emit({ type: \"session_shutdown\" });\n \n // 2. Create new runner\n this._extensionRunner = new ExtensionRunner(loaded.extensions.extensions, loaded.extensions.runtime, ...);\n \n // 3. Update content\n this._skills = loaded.skills;\n this._promptTemplates = loaded.promptTemplates;\n // etc.\n \n // 4. Rebuild system prompt\n this._baseSystemPrompt = this._rebuildSystemPrompt(this.getActiveToolNames());\n this.agent.setSystemPrompt(this._baseSystemPrompt);\n \n // 5. Call mode initializer + emit session_start\n if (this.runnerInitializer) await this.runnerInitializer(this._extensionRunner);\n await this._extensionRunner.emit({ type: \"session_start\" });\n }\n}\n```\n\n---\n\n## Phase 6: Watch Mode\n\n`--watch` flag for auto-reload. Watches all paths from loader config. Debounces changes (100-200ms).\n\n---\n\n## CLI Flag Behavior on Reload\n\n- **CLI flags are fixed at startup** (`--no-extensions`, `--no-skills`, `-e`, etc.)\n- **settings.json changes are picked up** on reload\n- **New extension flags**: values undefined (CLI already parsed). Restart to use new flags.\n- **Flag values transfer**: old runtime values copied to new runtime\n\n---\n\n## Implementation Order\n\n1. Extract loader from sdk.ts\n2. Implement PackageManager with npm/git resolution\n3. Refactor main.ts to use new bootstrap flow\n4. Add RunnerInitializer pattern to AgentSession\n5. Implement session.reload()\n6. Package management commands (`pi install/remove/update`)\n7. Interactive mode `/reload` command\n8. Watch mode (`--watch` flag)\n","comments":[{"id":"IC_kwDOPbFNk87euJAd","author":{"login":"badlogic"},"authorAssociation":"OWNER","body":"## Files to Read Before Implementation\n\nBefore starting work on this issue, read the following files in full:\n\n### Core\n- `packages/coding-agent/src/core/sdk.ts` - Current session creation, discovery logic to extract\n- `packages/coding-agent/src/core/agent-session.ts` - Where reload() will be added\n- `packages/coding-agent/src/core/settings-manager.ts` - Settings structure, filters\n- `packages/coding-agent/src/core/extensions/runner.ts` - ExtensionRunner, initialize() pattern\n- `packages/coding-agent/src/core/extensions/index.ts` - Extension loading, discovery\n\n### Modes\n- `packages/coding-agent/src/main.ts` - Bootstrap flow, CLI parsing, two-pass pattern\n- `packages/coding-agent/src/modes/interactive/interactive-mode.ts` - Runner initialization, mode-specific setup\n- `packages/coding-agent/src/modes/print-mode.ts` - Simpler runner initialization\n- `packages/coding-agent/src/modes/rpc/rpc-mode.ts` - RPC runner initialization\n\n### Loading\n- `packages/coding-agent/src/core/skills.ts` - Skill loading, filters\n- `packages/coding-agent/src/core/prompt-templates.ts` - Prompt template loading\n- `packages/coding-agent/src/core/system-prompt.ts` - Context file loading (AGENTS.md)\n\n### CLI\n- `packages/coding-agent/src/cli/args.ts` - CLI argument parsing, extension flags\n\n### Docs\n- `packages/coding-agent/docs/extensions.md` - Extension discovery rules, package.json format\n- `packages/coding-agent/README.md` - Overview of extension locations","createdAt":"2026-01-12T01:55:40Z","includesCreatedEdit":false,"isMinimized":false,"minimizedReason":"","reactionGroups":[],"url":"https://github.com/badlogic/pi-mono/issues/645#issuecomment-3736637469","viewerDidAuthor":true},{"id":"IC_kwDOPbFNk87gX7j8","author":{"login":"ben-vargas"},"authorAssociation":"CONTRIBUTOR","body":"Would still love to have the ability to have skills recursively traverse discovery similar to AGENTS.md recursion from #751 - any timeline for this rework? I am just having to maintain/use a dev branch that rebases and reapplies #751 on top of each release... would love to get back on to main version.","createdAt":"2026-01-17T21:36:28Z","includesCreatedEdit":false,"isMinimized":false,"minimizedReason":"","reactionGroups":[],"url":"https://github.com/badlogic/pi-mono/issues/645#issuecomment-3764369660","viewerDidAuthor":false},{"id":"IC_kwDOPbFNk87gwFGO","author":{"login":"badlogic"},"authorAssociation":"OWNER","body":"Consolidated design and decisions\n\nResourceLoader\n- Interface\n - getExtensions(): LoadExtensionsResult\n - getSkills(): { skills: Skill[]; diagnostics: ResourceDiagnostic[] }\n - getPrompts(): { prompts: PromptTemplate[]; diagnostics: ResourceDiagnostic[] }\n - getThemes(): { themes: Theme[]; diagnostics: ResourceDiagnostic[] }\n - getAgentsFiles(): { agentsFiles: Array<{ path: string; content: string }> }\n - getSystemPrompt(): string | undefined\n - getAppendSystemPrompt(): string[]\n - reload(): Promise<void>\n- Diagnostics shape\n - { type: \"warning\" | \"error\"; message: string; path?: string }\n- Getters return cached data from the last reload. reload recomputes all resources.\n- AgentSession stores ResourceLoader and exposes it. AgentSession.reload uses loader results and rewires extensions, skills, prompts, themes, context files, and system prompt, then emits session_start.\n\nDefaultResourceLoader behavior\n- Owns its PackageManager internally. Callers do not construct one.\n- Discovery always includes ~/.pi/agent and <cwd>/.pi.\n- Settings arrays extend discovery, not replace it. Arrays are merged with dedupe. Warn on conflicts for extensions with different versions or sources.\n- CLI sources are additive even when --no-* flags are set.\n\nDiscovery rules\n- extensions: direct .ts or .js files, one level deep index.ts or index.js, or package.json with pi field.\n- skills: direct .md children in skills/, plus recursive SKILL.md under subfolders.\n- prompts: direct .md children in prompts/.\n- themes: direct .json children in themes/.\n\nSettings.json\n- Remove SkillsSettings and all source flags. Remove include and ignore filters.\n- New arrays: extensions, skills, prompts, themes. Each accepts file or folder paths.\n- --no-skills and --no-extensions still disable those subsystems.\n\nCLI flags\n- Add --skill <path>, --theme <path>, --prompt-template <path>.\n- Add --no-themes and --no-prompt-templates.\n- Keep --extension and --no-extensions.\n- Remove --skills filter flag or repurpose it for explicit sources.\n\nPackageManager\n- npm: use npm root -g for global installs.\n- Project settings npm installs go to <cwd>/.pi/npm/node_modules via npm --prefix.\n- git installs go to ~/.pi/agent/git/<host>/<path> and checkout ref.\n- Pinning: npm with @version pinned. git with @tag or @commit pinned. Pinned sources do not update.\n- No pin means update pulls latest.\n- npm: and git: are only valid for extensions. Packages can provide extensions, skills, prompts, themes via pi field or auto discovery when no pi field exists.\n- CLI npm or git extension sources install into /tmp/pi-extensions/<hash> for that run only. No settings changes.\n- PackageManager is invoked by DefaultResourceLoader on reload. It installs missing sources automatically. Progress callbacks are needed so TUI can show status.\n\nManifest handling\n- pi field means explicit entries only. If pi field exists, do not auto discover inside the package.\n- If no pi field, apply the same discovery rules as ~/.pi/agent layout.\n- pi manifest includes extensions, skills, prompts, themes. No context files in the manifest.\n- Path formats\n - extensions: .ts or .js factory files\n - skills: directory (recursive SKILL.md) or SKILL.md file paths\n - prompts: directory (non recursive .md) or file paths\n - themes: directory (non recursive .json) or file paths\n\nSystem prompt and context\n- ResourceLoader handles AGENTS.md, SYSTEM.md, and APPEND_SYSTEM.md. Default behavior matches current discovery.\n- createAgentSession consumes ResourceLoader results and does no resource discovery.\n\nMain flow\n- First pass parse for disable flags and CLI sources.\n- Construct DefaultResourceLoader and call reload to load extensions and collect flags.\n- Second pass parse with extension flags, apply runtime flag values to extension runtime. No second reload needed.\n\nOpen points to confirm in implementation\n- keep SKILL.md spelling and adjust any current code or docs as needed.\n- remove --skills filter flag or repurpose it for explicit paths.\n","createdAt":"2026-01-20T02:06:32Z","includesCreatedEdit":false,"isMinimized":false,"minimizedReason":"","reactionGroups":[],"url":"https://github.com/badlogic/pi-mono/issues/645#issuecomment-3770700174","viewerDidAuthor":true},{"id":"IC_kwDOPbFNk87gwF2c","author":{"login":"badlogic"},"authorAssociation":"OWNER","body":"Full implementation plan for #645\n\nScope and intent\n- Replace current ad hoc discovery in sdk.ts and main.ts with a ResourceLoader pipeline.\n- DefaultResourceLoader reproduces current behavior and adds package management and reload.\n- ResourceLoader owns resource discovery. createAgentSession and AgentSession consume ResourceLoader output only.\n\nResourceLoader API\n\n```ts\nexport interface ResourceDiagnostic {\n type: \"warning\" | \"error\";\n message: string;\n path?: string;\n}\n\nexport interface ResourceLoader {\n getExtensions(): LoadExtensionsResult;\n getSkills(): { skills: Skill[]; diagnostics: ResourceDiagnostic[] };\n getPrompts(): { prompts: PromptTemplate[]; diagnostics: ResourceDiagnostic[] };\n getThemes(): { themes: Theme[]; diagnostics: ResourceDiagnostic[] };\n getAgentsFiles(): { agentsFiles: Array<{ path: string; content: string }> };\n getSystemPrompt(): string | undefined;\n getAppendSystemPrompt(): string[];\n reload(): Promise<void>;\n}\n```\n\nNotes\n- All getters return cached data from last reload. reload recomputes everything.\n- getAppendSystemPrompt returns zero or more string fragments.\n- AgentSession.reload consumes the loader output and emits session_start.\n\nDefaultResourceLoader behavior\n- Owns PackageManager internally. Callers do not construct it.\n- Takes cwd, agentDir, settingsManager, and CLI resource additions. Also takes disable flags.\n- Always discovers from ~/.pi/agent and <cwd>/.pi.\n- settings.json arrays extend discovery. Arrays are merged with dedupe.\n- Warn on conflicts for extensions from different sources or versions.\n- CLI sources are additive even when disabled flags are set.\n\nDiscovery rules\n- extensions\n - direct .ts or .js files\n - one level deep index.ts or index.js\n - one level deep package.json with pi field\n- skills\n - direct .md children in skills/\n - recursive SKILL.md under subfolders\n- prompts\n - direct .md children in prompts/\n- themes\n - direct .json children in themes/\n\nPackageManager\n- npm: install into global node_modules via npm root -g for global settings.\n- npm: install into <cwd>/.pi/npm/node_modules for project settings.\n- git: clone into ~/.pi/agent/git/<host>/<path> and checkout ref.\n- pinning\n - npm with @version is pinned\n - git with @tag or @commit is pinned\n - pinned sources do not update\n- no ref means update pulls latest\n- npm: and git: are only valid for extensions\n- packages can provide extensions, skills, prompts, and themes via pi field, or via auto discovery if no pi field exists\n- CLI npm or git extensions install into /tmp/pi-extensions/<hash> for that run only, no settings changes\n- PackageManager is invoked by DefaultResourceLoader on reload and installs missing sources automatically\n- PackageManager exposes progress callbacks so TUI can show status\n\nManifest handling\n- package.json pi field means explicit entries only. No auto discovery inside the package when pi exists.\n- if no pi field, apply normal discovery rules.\n- pi field includes extensions, skills, prompts, themes. No context files in manifest.\n- path formats\n - extensions: .ts or .js factory files\n - skills: directory, recursive SKILL.md, or SKILL.md file paths\n - prompts: directory, non recursive .md, or .md file paths\n - themes: directory, non recursive .json, or .json file paths\n\nSystem prompt and context\n- ResourceLoader handles AGENTS.md, SYSTEM.md, and APPEND_SYSTEM.md. Default behavior matches current discovery.\n- createAgentSession uses ResourceLoader data only. No implicit discovery in sdk.ts.\n\nCLI flags\n- add\n - --skill <path> repeatable\n - --theme <path> repeatable\n - --prompt-template <path> repeatable\n - --no-themes\n - --no-prompt-templates\n- keep\n - --extension, --no-extensions\n - --no-skills\n- remove or repurpose current --skills filter flag. It must not be glob filtering anymore.\n\nMain flow\n- first pass parse for disable flags and CLI sources\n- construct DefaultResourceLoader and call reload to load extensions and collect flags\n- second pass parse with extension flags, set runtime flag values on the extensions runtime\n- no second reload needed because flags do not affect resources\n\nAgentSession reload\n- Add runner initializer as described in the original issue. AgentSession stores it.\n- AgentSession.reload calls loader.reload, creates a new ExtensionRunner, updates skills, prompts, themes, context files, rebuilds system prompt, then emits session_start.\n- /reload command and --watch trigger AgentSession.reload.\n- watch should work in all modes by calling AgentSession.reload.\n\nResource diagnostics\n- Each getter returns diagnostics for that resource type only, no aggregated list.\n\nFiles to read and modify\n\nCore\n- packages/coding-agent/src/core/sdk.ts\n- packages/coding-agent/src/core/agent-session.ts\n- packages/coding-agent/src/core/settings-manager.ts\n- packages/coding-agent/src/core/extensions/runner.ts\n- packages/coding-agent/src/core/extensions/loader.ts\n- packages/coding-agent/src/core/extensions/types.ts\n- packages/coding-agent/src/core/skills.ts\n- packages/coding-agent/src/core/prompt-templates.ts\n- packages/coding-agent/src/core/system-prompt.ts\n\nNew core files\n- packages/coding-agent/src/core/resource-loader.ts (ResourceLoader interface)\n- packages/coding-agent/src/core/default-resource-loader.ts\n- packages/coding-agent/src/core/package-manager.ts\n\nModes and CLI\n- packages/coding-agent/src/main.ts\n- packages/coding-agent/src/cli/args.ts\n- packages/coding-agent/src/modes/interactive/interactive-mode.ts\n- packages/coding-agent/src/modes/print-mode.ts\n- packages/coding-agent/src/modes/rpc/rpc-mode.ts\n\nDocs\n- packages/coding-agent/docs/extensions.md\n- packages/coding-agent/README.md\n- packages/coding-agent/CHANGELOG.md\n\nImplementation steps\n1. Introduce ResourceLoader interface and DefaultResourceLoader.\n2. Add PackageManager and npm or git resolve and install.\n3. Update settings manager to use arrays for skills, prompts, themes. Remove SkillsSettings and include ignore filters.\n4. Refactor extension loader to support pi field for prompts, themes, skills.\n5. Refactor sdk.ts to accept ResourceLoader and remove discovery logic.\n6. Update main.ts to use ResourceLoader, two pass parsing, and flag propagation.\n7. Add AgentSession.reload and runner initializer pattern. Update modes to register initializer.\n8. Add /reload and --watch.\n9. Update CLI args and help.\n10. Update docs and changelog.\n","createdAt":"2026-01-20T02:08:27Z","includesCreatedEdit":false,"isMinimized":false,"minimizedReason":"","reactionGroups":[{"content":"HOORAY","users":{"totalCount":3}}],"url":"https://github.com/badlogic/pi-mono/issues/645#issuecomment-3770703260","viewerDidAuthor":true},{"id":"IC_kwDOPbFNk87g3ESJ","author":{"login":"kim0"},"authorAssociation":"CONTRIBUTOR","body":"Would love to see shared pi threads, where you share how you work with agents to plan such larger changes! Would also be nice marketing for pi and educating new contributors 🚀 ","createdAt":"2026-01-20T12:00:03Z","includesCreatedEdit":false,"isMinimized":false,"minimizedReason":"","reactionGroups":[],"url":"https://github.com/badlogic/pi-mono/issues/645#issuecomment-3772531849","viewerDidAuthor":false},{"id":"IC_kwDOPbFNk87hBke2","author":{"login":"badlogic"},"authorAssociation":"OWNER","body":"## Watch Mode Decision\n\nDecided against implementing `--watch` for auto-reload. The problem: extension files may be in a partial state during editing (incomplete syntax, half-written code). Auto-reloading in that state would cause errors and a bad experience.\n\nOnly the user knows when their changes are complete and safe to reload. The `/reload` command gives explicit control.\n\nRemaining work:\n- Progress callbacks for PackageManager (TUI status during install)\n- onMissing prompt in interactive mode (ask user before auto-installing missing sources)\n- CLI temp installs for `-e npm:foo` / `-e git:...` (install to /tmp for that run only)\n- Extension conflict warnings (different versions/sources)\n- Test coverage for package-manager.ts and resource-loader.ts","createdAt":"2026-01-20T22:38:40Z","includesCreatedEdit":false,"isMinimized":false,"minimizedReason":"","reactionGroups":[{"content":"THUMBS_UP","users":{"totalCount":2}}],"url":"https://github.com/badlogic/pi-mono/issues/645#issuecomment-3775285174","viewerDidAuthor":true}],"labels":[{"id":"LA_kwDOPbFNk88AAAACQ8TbnQ","name":"pkg:coding-agent","description":"Coding agent CLI","color":"0e8a16"}],"state":"CLOSED","title":"Extension package management and hot reload"}Let me continue reading the rest of the file, specifically around line 1575-1580 where the change is:
Let me check if there's a changelog entry for this specific fix in the Unreleased section and also check the related code more closely:
>cd /Users/badlogic/workspaces/pi-mono && grep -i "extension shortcut" packages/coding-agent/CHANGELOG.md | head -20- Fixed extension shortcut conflicts to respect user keybindings when built-in actions are remapped. ([#826](https://github.com/badlogic/pi-mono/pull/826) by [@richardgill](https://github.com/richardgill))Now let me check if the README.md or docs need updates for this fix:
>cd /Users/badlogic/workspaces/pi-mono && grep -r "setEditorComponent\|registerShortcut" packages/coding-agent/README.md packages/coding-agent/docs/*.md 2>/dev/null | head -30packages/coding-agent/README.md: pi.registerShortcut("ctrl+shift+d", {
packages/coding-agent/docs/extensions.md: pi.registerShortcut("ctrl+x", { ... });
packages/coding-agent/docs/extensions.md:### pi.registerShortcut(shortcut, options)
packages/coding-agent/docs/extensions.md:pi.registerShortcut("ctrl+shift+p", {
packages/coding-agent/docs/extensions.md:ctx.ui.setEditorComponent((tui, theme, keybindings) => new VimEditor(tui, theme, keybindings));
packages/coding-agent/docs/extensions.md:ctx.ui.setEditorComponent(undefined); // Restore default editor
packages/coding-agent/docs/extensions.md:- `ctx.ui.setEditorComponent()`: [modal-editor.ts](../examples/extensions/modal-editor.ts)
packages/coding-agent/docs/extensions.md: ctx.ui.setEditorComponent((_tui, theme, keybindings) =>
packages/coding-agent/docs/extensions.md:- Pass `undefined` to restore default: `ctx.ui.setEditorComponent(undefined)`
packages/coding-agent/docs/tui.md: ctx.ui.setEditorComponent((tui, theme, keybindings) =>
packages/coding-agent/docs/tui.md:- **Factory pattern**: `setEditorComponent` receives a factory function that gets `tui`, `theme`, and `keybindings`
packages/coding-agent/docs/tui.md:- **Pass `undefined`** to restore the default editor: `ctx.ui.setEditorComponent(undefined)`Now I have enough context. Let me provide the review:
PR: https://github.com/badlogic/pi-mono/pull/947
### Fixed if user decides to merge.setEditorComponent() copied onExtensionShortcut from defaultEditor at creation time, capturing undefined because setupExtensionShortcuts() hadn't run yet.(data: string) => this.defaultEditor.onExtensionShortcut?.(data) ensures the call happens at runtime rather than copy time.?.) is appropriate since onExtensionShortcut may still be undefined and should just be a no-op in that case.registerShortcut() and setEditorComponent(), which would benefit from a test to prevent future regressions.onEscape, onCtrlD, and onPasteImage on lines 1573-1575. Those are set earlier in the initialization flow (via setupKeyHandlers() and setupEditorSubmitHandler() which run in init() before initExtensions()), so they are already defined when setEditorComponent() is called during session_start. However, if the order ever changes, similar bugs could appear. Worth considering whether those should also be delegated for consistency, though this is out of scope for this PR.this.defaultEditor.onExtensionShortcut to a closure that delegates to this.defaultEditor.onExtensionShortcut?.(data) at call time. This ensures that when a custom editor component calls onExtensionShortcut, it correctly invokes the handler that was set up later by setupExtensionShortcuts().registerShortcut() and setEditorComponent() in the session_start handler, then verify the shortcut fires correctly.merge via gh cli, pull from orogin
>cd /Users/badlogic/workspaces/pi-mono && gh pr merge 947 --squash --delete-branch(no output)>cd /Users/badlogic/workspaces/pi-mono && git pull origin mainerror: cannot pull with rebase: You have unstaged changes.
error: Please commit or stash them.
Command exited with code 128>cd /Users/badlogic/workspaces/pi-mono && git status --shortM package-lock.json
M packages/ai/src/models.generated.ts
?? .pi/settings.json>cd /Users/badlogic/workspaces/pi-mono && git stash && git pull origin main && git stash popSaved working directory and index state WIP on main: 62209c4a docs: add tmux testing instructions to AGENTS.md
From https://github.com/badlogic/pi-mono
* branch main -> FETCH_HEAD
62209c4a..f1b1d549 main -> origin/main
Updating 62209c4a..f1b1d549
Fast-forward
packages/coding-agent/src/modes/interactive/interactive-mode.ts | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
On branch main
Your branch is up to date with 'origin/main'.
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
modified: package-lock.json
modified: packages/ai/src/models.generated.ts
Untracked files:
(use "git add <file>..." to include in what will be committed)
.pi/settings.json
no changes added to commit (use "git add" and/or "git commit -a")
Dropped refs/stash@{0} (f02da51878c03d5a56ee02902b6954d92c6801cc)Now let me add the changelog entry for this fix:
>cd /Users/badlogic/workspaces/pi-mono && npm run check 2>&1> pi-monorepo@0.0.3 check
> biome check --write . && tsgo --noEmit && cd packages/web-ui && npm run check
Checked 432 files in 293ms. No fixes applied.
> @mariozechner/pi-web-ui@0.49.3 check
> biome check --write . && tsc --noEmit && cd example && biome check --write . && tsc --noEmit
Checked 73 files in 39ms. No fixes applied.
Checked 3 files in 8ms. No fixes applied.>cd /Users/badlogic/workspaces/pi-mono && git add packages/coding-agent/CHANGELOG.md && git commit -m "docs(coding-agent): add changelog entry for extension shortcut fix (#947)"Running formatting, linting, and type checking...
> pi-monorepo@0.0.3 check
> biome check --write . && tsgo --noEmit && cd packages/web-ui && npm run check
Checked 432 files in 266ms. No fixes applied.
> @mariozechner/pi-web-ui@0.49.3 check
> biome check --write . && tsc --noEmit && cd example && biome check --write . && tsc --noEmit
Checked 73 files in 38ms. No fixes applied.
Checked 3 files in 8ms. No fixes applied.
✅ All pre-commit checks passed!
[main b1211261] docs(coding-agent): add changelog entry for extension shortcut fix (#947)
1 file changed, 1 insertion(+)>cd /Users/badlogic/workspaces/pi-mono && git push origin mainTo https://github.com/badlogic/pi-mono.git
f1b1d549..b1211261 main -> main