pi-orchestrator-config 4.1.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (175) hide show
  1. package/README.md +808 -0
  2. package/agents/api-documenter.md +32 -0
  3. package/agents/bash-expert.md +53 -0
  4. package/agents/code-reviewer-docs.md +145 -0
  5. package/agents/code-reviewer-guidelines.md +97 -0
  6. package/agents/code-reviewer-quality.md +142 -0
  7. package/agents/code-reviewer-security.md +107 -0
  8. package/agents/code-reviewer-spec.md +164 -0
  9. package/agents/debugger.md +63 -0
  10. package/agents/docker-expert.md +40 -0
  11. package/agents/docs-fetcher.md +48 -0
  12. package/agents/git-expert.md +63 -0
  13. package/agents/github-expert.md +83 -0
  14. package/agents/go-expert.md +62 -0
  15. package/agents/issue-reviewer-feasibility.md +60 -0
  16. package/agents/issue-reviewer-scope.md +63 -0
  17. package/agents/issue-reviewer-spec.md +77 -0
  18. package/agents/java-expert.md +31 -0
  19. package/agents/jenkins-expert.md +38 -0
  20. package/agents/kubernetes-expert.md +40 -0
  21. package/agents/planner.md +42 -0
  22. package/agents/python-expert.md +61 -0
  23. package/agents/reviewer.md +26 -0
  24. package/agents/scout.md +40 -0
  25. package/agents/security-auditor.md +254 -0
  26. package/agents/technical-documentation-writer.md +37 -0
  27. package/agents/test-automator.md +55 -0
  28. package/agents/test-runner.md +47 -0
  29. package/agents/ts-expert.md +23 -0
  30. package/agents/worker.md +30 -0
  31. package/extensions/acpx-provider/index.ts +104 -0
  32. package/extensions/acpx-provider/load-runtime.ts +99 -0
  33. package/extensions/acpx-provider/runtime-models.ts +40 -0
  34. package/extensions/cli-provider/agents/claude.ts +128 -0
  35. package/extensions/cli-provider/agents/cursor.ts +84 -0
  36. package/extensions/cli-provider/agents/gemini.ts +100 -0
  37. package/extensions/cli-provider/discover.ts +77 -0
  38. package/extensions/cli-provider/index.ts +57 -0
  39. package/extensions/cli-provider/parsers.ts +304 -0
  40. package/extensions/cli-provider/providers.ts +58 -0
  41. package/extensions/cli-provider/runner.ts +225 -0
  42. package/extensions/cli-provider/runtime-models.ts +32 -0
  43. package/extensions/cli-provider/session-reaper.ts +122 -0
  44. package/extensions/cli-provider/sessions.ts +533 -0
  45. package/extensions/cli-provider/shared/discover-cache.ts +235 -0
  46. package/extensions/cli-provider/types.ts +48 -0
  47. package/extensions/coms/coms-net-server.ts +1642 -0
  48. package/extensions/coms/coms-net-wrapper.ts +521 -0
  49. package/extensions/coms/coms-net.ts +1589 -0
  50. package/extensions/coms/coms-p2p.ts +1879 -0
  51. package/extensions/coms/coms-shared.ts +510 -0
  52. package/extensions/coms/coms-wrapper.ts +138 -0
  53. package/extensions/coms/index.ts +20 -0
  54. package/extensions/coms/themeMap.ts +147 -0
  55. package/extensions/image-gen/image-gen.ts +332 -0
  56. package/extensions/image-gen/index.ts +14 -0
  57. package/extensions/orchestrator/agents.ts +134 -0
  58. package/extensions/orchestrator/ask-user.ts +293 -0
  59. package/extensions/orchestrator/async-agents.ts +1037 -0
  60. package/extensions/orchestrator/async-capability.ts +129 -0
  61. package/extensions/orchestrator/async-runner.ts +324 -0
  62. package/extensions/orchestrator/async-status-parse.ts +31 -0
  63. package/extensions/orchestrator/async-status-ui.ts +201 -0
  64. package/extensions/orchestrator/async-wait.ts +36 -0
  65. package/extensions/orchestrator/btw.ts +210 -0
  66. package/extensions/orchestrator/cron-status-format.ts +126 -0
  67. package/extensions/orchestrator/cron-status-ui.ts +132 -0
  68. package/extensions/orchestrator/cron.ts +730 -0
  69. package/extensions/orchestrator/dreaming.ts +355 -0
  70. package/extensions/orchestrator/enforcement-helpers.ts +530 -0
  71. package/extensions/orchestrator/enforcement-rules.ts +329 -0
  72. package/extensions/orchestrator/enforcement.ts +828 -0
  73. package/extensions/orchestrator/extended-autocomplete.ts +593 -0
  74. package/extensions/orchestrator/git-helpers.ts +420 -0
  75. package/extensions/orchestrator/github-autocomplete.ts +250 -0
  76. package/extensions/orchestrator/icons.ts +17 -0
  77. package/extensions/orchestrator/index.ts +100 -0
  78. package/extensions/orchestrator/memory-embeddings.ts +281 -0
  79. package/extensions/orchestrator/memory-promotion.ts +449 -0
  80. package/extensions/orchestrator/memory-provenance.ts +80 -0
  81. package/extensions/orchestrator/memory-query-class.ts +90 -0
  82. package/extensions/orchestrator/memory-scoring.ts +434 -0
  83. package/extensions/orchestrator/memory-tools.ts +883 -0
  84. package/extensions/orchestrator/memory-tree.ts +212 -0
  85. package/extensions/orchestrator/nvim.ts +169 -0
  86. package/extensions/orchestrator/overlay-dashboard-utils.ts +25 -0
  87. package/extensions/orchestrator/overlay-dashboard.ts +512 -0
  88. package/extensions/orchestrator/pi-config-review-state.ts +303 -0
  89. package/extensions/orchestrator/preference-extractor.ts +106 -0
  90. package/extensions/orchestrator/project-settings.ts +468 -0
  91. package/extensions/orchestrator/promotion-queue.ts +311 -0
  92. package/extensions/orchestrator/resolve-agent-model.ts +32 -0
  93. package/extensions/orchestrator/review-ui.ts +204 -0
  94. package/extensions/orchestrator/rule-placeholders.ts +12 -0
  95. package/extensions/orchestrator/rules.ts +481 -0
  96. package/extensions/orchestrator/session-search.ts +243 -0
  97. package/extensions/orchestrator/session-validation.ts +374 -0
  98. package/extensions/orchestrator/situation-report.ts +370 -0
  99. package/extensions/orchestrator/status-bar.ts +31 -0
  100. package/extensions/orchestrator/status-line.ts +192 -0
  101. package/extensions/orchestrator/status.ts +110 -0
  102. package/extensions/orchestrator/subagent-tool.ts +1748 -0
  103. package/extensions/orchestrator/task-lifecycle.ts +88 -0
  104. package/extensions/orchestrator/utils.ts +227 -0
  105. package/extensions/pidash/index.ts +15 -0
  106. package/extensions/pidash/pidash.ts +941 -0
  107. package/extensions/pidiff/index.ts +13 -0
  108. package/extensions/pidiff/pidiff.ts +369 -0
  109. package/extensions/providers/acpx-driver.ts +479 -0
  110. package/extensions/providers/built-in-drivers.ts +52 -0
  111. package/extensions/providers/claude-driver.ts +319 -0
  112. package/extensions/providers/cursor-acpx-driver.ts +398 -0
  113. package/extensions/providers/cursor-cli-driver.ts +484 -0
  114. package/extensions/providers/gemini-driver.ts +302 -0
  115. package/extensions/providers/index.ts +513 -0
  116. package/extensions/shared/build-system-prompt.ts +57 -0
  117. package/extensions/shared/create-runtime-provider.ts +190 -0
  118. package/extensions/shared/daemon-manager.ts +248 -0
  119. package/extensions/shared/file-logger.ts +115 -0
  120. package/extensions/shared/managed-refresh.ts +211 -0
  121. package/extensions/shared/provider-driver.ts +295 -0
  122. package/extensions/shared/provider-errors.ts +305 -0
  123. package/extensions/shared/provider-registry.ts +388 -0
  124. package/extensions/shared/stream-builder.ts +275 -0
  125. package/extensions/shared/types.ts +20 -0
  126. package/extensions/shared/ui/README.md +10 -0
  127. package/extensions/shared/ui/badge.tsx +52 -0
  128. package/extensions/shared/ui/button.tsx +58 -0
  129. package/extensions/shared/ui/collapsible.tsx +21 -0
  130. package/extensions/shared/ui/dropdown-menu.tsx +266 -0
  131. package/extensions/shared/ui/input.tsx +20 -0
  132. package/extensions/shared/ui/popover.tsx +114 -0
  133. package/extensions/shared/ui/resizable.tsx +48 -0
  134. package/extensions/shared/ui/scroll-area.tsx +53 -0
  135. package/extensions/shared/ui/select.tsx +201 -0
  136. package/extensions/shared/ui/separator.tsx +25 -0
  137. package/extensions/shared/ui/tooltip.tsx +64 -0
  138. package/extensions/shared/ws-client.ts +74 -0
  139. package/package.json +59 -0
  140. package/prompts/coderabbit-rate-limit.md +95 -0
  141. package/prompts/create-coms-feature-manager.md +186 -0
  142. package/prompts/create-skill.md +128 -0
  143. package/prompts/domain-model.md +109 -0
  144. package/prompts/external-ai.md +665 -0
  145. package/prompts/implement-and-review.md +25 -0
  146. package/prompts/implement.md +26 -0
  147. package/prompts/issue-review.md +256 -0
  148. package/prompts/pr-review.md +753 -0
  149. package/prompts/query-db.md +154 -0
  150. package/prompts/refine-review.md +208 -0
  151. package/prompts/release.md +235 -0
  152. package/prompts/remember.md +33 -0
  153. package/prompts/review-handler-status.md +27 -0
  154. package/prompts/review-handler.md +677 -0
  155. package/prompts/review-local.md +159 -0
  156. package/prompts/scout-and-plan.md +21 -0
  157. package/rules/00-orchestrator-core.md +42 -0
  158. package/rules/05-issue-first-workflow.md +119 -0
  159. package/rules/10-agent-routing.md +58 -0
  160. package/rules/15-mcp-launchpad.md +46 -0
  161. package/rules/20-code-review-loop.md +234 -0
  162. package/rules/25-documentation-updates.md +15 -0
  163. package/rules/30-prompt-templates.md +19 -0
  164. package/rules/35-memory.md +240 -0
  165. package/rules/40-critical-rules.md +124 -0
  166. package/rules/45-file-preview.md +29 -0
  167. package/rules/50-agent-bug-reporting.md +90 -0
  168. package/rules/55-coms-protocol.md +63 -0
  169. package/rules/60-task-tracking.md +66 -0
  170. package/skills/conflict-resolver/SKILL.md +213 -0
  171. package/skills/skill-creator/SKILL.md +85 -0
  172. package/skills/systematic-debugging/SKILL.md +353 -0
  173. package/skills/test-driven-development/SKILL.md +335 -0
  174. package/skills/update-agents-md/SKILL.md +282 -0
  175. package/templates/coms-feature-manager-prompt.md +313 -0
@@ -0,0 +1,212 @@
1
+ /**
2
+ * Memory Tree — hierarchical topic-based memory organization.
3
+ *
4
+ * Organizes memory entries into topic files under .pi/memory/topics/.
5
+ * Each topic file is a Markdown chunk limited to ~3000 tokens.
6
+ * Topics have hotness scores based on reinforcement frequency.
7
+ * The situation report pulls from hot topics first.
8
+ *
9
+ * Architecture inspired by OpenHuman (https://github.com/tinyhumansai/openhuman).
10
+ * Clean-room TypeScript implementation under MIT — not a code translation.
11
+ */
12
+
13
+ import { existsSync, readFileSync, writeFileSync, readdirSync, mkdirSync, unlinkSync } from "node:fs";
14
+ import { join } from "node:path";
15
+ import {
16
+ type MemoryCategory,
17
+ loadScores,
18
+ entryHash,
19
+ } from "./memory-scoring.js";
20
+
21
+ // ── Constants ──────────────────────────────────────────────────────────────
22
+
23
+ /** Max chars per topic file (~3000 tokens × 4 chars/token) */
24
+ export const MAX_TOPIC_CHARS = 12000;
25
+
26
+ /** Topic mapping: which categories go into which topic files */
27
+ export const CATEGORY_TO_TOPIC: Record<MemoryCategory, string> = {
28
+ preference: "preferences",
29
+ lesson: "lessons",
30
+ pattern: "patterns",
31
+ decision: "decisions",
32
+ done: "completions",
33
+ mistake: "mistakes",
34
+ };
35
+
36
+ /** How old a topic's newest entry must be (relative to half-life) before archival */
37
+ const ARCHIVAL_MULTIPLIER = 2;
38
+
39
+ /** Half-lives in ms for archival check */
40
+ const HALF_LIVES_MS: Record<string, number> = {
41
+ preferences: 90 * 86400 * 1000,
42
+ lessons: 60 * 86400 * 1000,
43
+ patterns: 30 * 86400 * 1000,
44
+ decisions: 30 * 86400 * 1000,
45
+ completions: 14 * 86400 * 1000,
46
+ mistakes: 14 * 86400 * 1000,
47
+ };
48
+
49
+ // ── Types ──────────────────────────────────────────────────────────────────
50
+
51
+ export interface TopicInfo {
52
+ name: string;
53
+ path: string;
54
+ entryCount: number;
55
+ hotness: number;
56
+ newestEntry: string; // ISO date
57
+ chars: number;
58
+ }
59
+
60
+ // ── Topic Directory ────────────────────────────────────────────────────────
61
+
62
+ function getTopicsDir(cwd: string): string {
63
+ return join(cwd, ".pi", "memory", "topics");
64
+ }
65
+
66
+ function ensureTopicsDir(cwd: string): string {
67
+ const dir = getTopicsDir(cwd);
68
+ if (!existsSync(dir)) {
69
+ mkdirSync(dir, { recursive: true });
70
+ }
71
+ return dir;
72
+ }
73
+
74
+ // ── Topic File I/O ─────────────────────────────────────────────────────────
75
+
76
+ interface TopicFile {
77
+ entries: { text: string; category: MemoryCategory; pinned: boolean }[];
78
+ }
79
+
80
+ function readTopicFile(path: string): TopicFile {
81
+ if (!existsSync(path)) return { entries: [] };
82
+ try {
83
+ const content = readFileSync(path, "utf-8");
84
+ const entries: TopicFile["entries"] = [];
85
+ for (const line of content.split("\n")) {
86
+ const match = line.match(/^- \[(preference|lesson|pattern|decision|done|mistake)\] (.+)$/);
87
+ if (match) {
88
+ // Strip *(pinned)* and *(enforced)* tags from text (may have duplicates from legacy data)
89
+ const rawText = match[2]!.replace(/\s*\*\(pinned\)\*/g, "").replace(/\s*\*\(enforced\)\*/g, "").trim();
90
+ entries.push({
91
+ text: rawText,
92
+ category: match[1] as MemoryCategory,
93
+ pinned: line.includes("*(pinned)*"),
94
+ });
95
+ }
96
+ }
97
+ return { entries };
98
+ } catch {
99
+ return { entries: [] };
100
+ }
101
+ }
102
+
103
+ function writeTopicFile(
104
+ path: string,
105
+ topicName: string,
106
+ entries: TopicFile["entries"],
107
+ ): void {
108
+ const header = `# ${topicName.charAt(0).toUpperCase() + topicName.slice(1)}\n\n`;
109
+ const lines = entries.map((e) => {
110
+ const pinTag = e.pinned ? " *(pinned)*" : "";
111
+ return `- [${e.category}] ${e.text}${pinTag}`;
112
+ });
113
+ const content = header + lines.join("\n") + "\n";
114
+
115
+ // Enforce token limit — truncate if too long
116
+ if (content.length > MAX_TOPIC_CHARS) {
117
+ const truncated = content.slice(0, MAX_TOPIC_CHARS);
118
+ const lastNewline = truncated.lastIndexOf("\n");
119
+ writeFileSync(path, truncated.slice(0, lastNewline + 1), "utf-8");
120
+ } else {
121
+ writeFileSync(path, content, "utf-8");
122
+ }
123
+ }
124
+
125
+ // ── Archival ───────────────────────────────────────────────────────────────
126
+
127
+ function archiveColdTopics(topicsDir: string, topics: TopicInfo[], now: number): void {
128
+ for (const topic of topics) {
129
+ const halfLifeMs = HALF_LIVES_MS[topic.name] || 30 * 86400 * 1000;
130
+ const threshold = halfLifeMs * ARCHIVAL_MULTIPLIER;
131
+ const newestMs = new Date(topic.newestEntry).getTime();
132
+ const age = now - newestMs;
133
+
134
+ if (age > threshold && topic.entryCount > 0) {
135
+ // Check if all entries are non-pinned before archiving
136
+ const topicFile = readTopicFile(topic.path);
137
+ const hasPinned = topicFile.entries.some((e) => e.pinned);
138
+ if (!hasPinned) {
139
+ try {
140
+ unlinkSync(topic.path);
141
+ } catch (e: any) { console.debug("[memory-tree] archive topic delete failed:", e?.message || e); }
142
+ }
143
+ }
144
+ }
145
+ }
146
+
147
+ // ── Read All Topic Entries ──────────────────────────────────────────────────
148
+
149
+ /**
150
+ * Read all entries from all topic files.
151
+ * This is the primary read path — topics are the source of truth.
152
+ */
153
+ export function readAllTopicEntries(cwd: string): { text: string; category: MemoryCategory; pinned: boolean }[] {
154
+ const topicsDir = getTopicsDir(cwd);
155
+ if (!existsSync(topicsDir)) return [];
156
+
157
+ const all: { text: string; category: MemoryCategory; pinned: boolean }[] = [];
158
+ try {
159
+ const files = readdirSync(topicsDir).filter((f) => f.endsWith(".md"));
160
+ for (const file of files) {
161
+ const topicFile = readTopicFile(join(topicsDir, file));
162
+ all.push(...topicFile.entries);
163
+ }
164
+ } catch (e: any) { console.debug("[memory-tree] read topic entries failed:", e?.message || e); }
165
+ return all;
166
+ }
167
+
168
+ // ── List Topics ────────────────────────────────────────────────────────────
169
+
170
+ /**
171
+ * List all topic files with their info.
172
+ */
173
+ export function listTopics(cwd: string): TopicInfo[] {
174
+ const topicsDir = getTopicsDir(cwd);
175
+ if (!existsSync(topicsDir)) return [];
176
+
177
+ const scores = loadScores(cwd);
178
+ const infos: TopicInfo[] = [];
179
+
180
+ try {
181
+ const files = readdirSync(topicsDir).filter((f) => f.endsWith(".md"));
182
+ for (const file of files) {
183
+ const path = join(topicsDir, file);
184
+ const topicFile = readTopicFile(path);
185
+ const topicName = file.replace(/\.md$/, "");
186
+
187
+ let hotness = 0;
188
+ let newestDate = 0;
189
+ for (const entry of topicFile.entries) {
190
+ const line = `- [${entry.category}] ${entry.text}`;
191
+ const hash = entryHash(line);
192
+ const scored = scores.entries[hash];
193
+ if (scored) {
194
+ hotness += scored.score;
195
+ const reinforced = new Date(scored.lastReinforced).getTime();
196
+ if (reinforced > newestDate) newestDate = reinforced;
197
+ }
198
+ }
199
+
200
+ infos.push({
201
+ name: topicName,
202
+ path,
203
+ entryCount: topicFile.entries.length,
204
+ hotness,
205
+ newestEntry: newestDate ? new Date(newestDate).toISOString() : "",
206
+ chars: readFileSync(path, "utf-8").length,
207
+ });
208
+ }
209
+ } catch (e: any) { console.debug("[memory-tree] list topics failed:", e?.message || e); }
210
+
211
+ return infos.sort((a, b) => b.hotness - a.hotness);
212
+ }
@@ -0,0 +1,169 @@
1
+ /**
2
+ * Neovim integration — communicate with the parent nvim instance via RPC.
3
+ * Only registers when running inside nvim (NVIM env var is set).
4
+ */
5
+
6
+ import { execFileSync } from "node:child_process";
7
+ import * as fs from "node:fs";
8
+ import * as os from "node:os";
9
+ import * as path from "node:path";
10
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
11
+ import { runGit } from "./git-helpers.js";
12
+ import { getProjectTmpDir } from "./utils.js";
13
+
14
+ const NVIM_SOCKET = process.env.NVIM;
15
+
16
+ /**
17
+ * Check if we're running inside a neovim terminal.
18
+ */
19
+ function isInsideNvim(): boolean {
20
+ return !!NVIM_SOCKET;
21
+ }
22
+
23
+ /**
24
+ * Execute a lua file in the parent nvim instance via --remote-expr.
25
+ * Returns the JSON-parsed result, or null on failure.
26
+ */
27
+ function nvimExecLua(luaCode: string, cwd?: string): any | null {
28
+ if (!NVIM_SOCKET) return null;
29
+
30
+ const tmpFile = path.join(getProjectTmpDir(cwd || process.cwd()), `nvim-${process.pid}-${Date.now()}.lua`);
31
+ try {
32
+ fs.writeFileSync(tmpFile, luaCode, "utf-8");
33
+ const result = execFileSync(
34
+ "nvim",
35
+ ["--server", NVIM_SOCKET, "--remote-expr", `luaeval("dofile('${tmpFile.replace(/'/g, "\\'")}')")` ],
36
+ { timeout: 5000, stdio: ["pipe", "pipe", "pipe"] },
37
+ ).toString().trim();
38
+
39
+ if (!result) return null;
40
+ return JSON.parse(result);
41
+ } catch {
42
+ return null;
43
+ } finally {
44
+ try { fs.unlinkSync(tmpFile); } catch {}
45
+ }
46
+ }
47
+
48
+ /**
49
+ * Send a quickfix list to nvim and open the quickfix window.
50
+ * Each entry: { filename, lnum?, col?, text }
51
+ */
52
+ function nvimSetQuickfix(entries: Array<{ filename: string; lnum?: number; col?: number; text?: string }>, title?: string, cwd?: string): boolean {
53
+ if (!NVIM_SOCKET || entries.length === 0) return false;
54
+
55
+ const dataFile = path.join(getProjectTmpDir(cwd || process.cwd()), `nvim-qf-${process.pid}-${Date.now()}.json`);
56
+ try {
57
+ fs.writeFileSync(dataFile, JSON.stringify(entries), "utf-8");
58
+
59
+ const titleLine = title ? `vim.fn.setqflist({}, "a", {title = "${title.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"})` : "";
60
+ const safeDataFile = dataFile.replace(/"/g, '\\"');
61
+ const lua = `
62
+ local f = io.open("${safeDataFile}", "r")
63
+ if not f then return vim.fn.json_encode({status = "error", message = "no data file"}) end
64
+ local raw = f:read("*a")
65
+ f:close()
66
+ os.remove("${safeDataFile}")
67
+ local entries = vim.fn.json_decode(raw)
68
+ local items = {}
69
+ for _, e in ipairs(entries) do
70
+ table.insert(items, {
71
+ filename = e.filename,
72
+ lnum = e.lnum or 1,
73
+ col = e.col or 1,
74
+ text = e.text or "",
75
+ })
76
+ end
77
+ vim.fn.setqflist(items)
78
+ ${titleLine}
79
+ vim.cmd("copen")
80
+ return vim.fn.json_encode({status = "ok", count = #items})
81
+ `;
82
+
83
+ const result = nvimExecLua(lua, cwd);
84
+ // Clean up data file if lua didn't remove it (nvim unreachable)
85
+ if (result?.status !== "ok") {
86
+ try { fs.unlinkSync(dataFile); } catch {}
87
+ }
88
+ return result?.status === "ok";
89
+ } catch {
90
+ try { fs.unlinkSync(dataFile); } catch {}
91
+ return false;
92
+ }
93
+ }
94
+
95
+ /**
96
+ * Get git changed files relative to origin/main (or HEAD if on main).
97
+ */
98
+ function getChangedFiles(cwd: string): Array<{ filename: string; status: string }> {
99
+ const branchResult = runGit(["rev-parse", "--abbrev-ref", "HEAD"], cwd);
100
+ if (branchResult.code !== 0) return [];
101
+ const branch = branchResult.stdout?.trim() || "";
102
+
103
+ const seen = new Set<string>();
104
+ const files: Array<{ filename: string; status: string }> = [];
105
+
106
+ const parseDiffOutput = (output: string) => {
107
+ for (const line of output.trim().split("\n")) {
108
+ const parts = line.split("\t");
109
+ if (parts.length >= 2) {
110
+ const statusCode = parts[0].charAt(0);
111
+ const filename = parts[parts.length - 1];
112
+ if (seen.has(filename)) continue;
113
+ seen.add(filename);
114
+ const statusMap: Record<string, string> = {
115
+ M: "modified", A: "added", D: "deleted", R: "renamed", C: "copied",
116
+ };
117
+ files.push({ filename, status: statusMap[statusCode] || statusCode });
118
+ }
119
+ }
120
+ };
121
+
122
+ if (branch === "main" || branch === "master") {
123
+ const result = runGit(["diff", "--name-status", "HEAD"], cwd);
124
+ if (result.code === 0 && result.stdout?.trim()) parseDiffOutput(result.stdout);
125
+ } else {
126
+ // Committed changes vs default branch
127
+ const hasMain = runGit(["rev-parse", "--verify", "origin/main"], cwd).code === 0;
128
+ const base = hasMain ? "origin/main" : "origin/master";
129
+ const committed = runGit(["diff", "--name-status", `${base}...HEAD`], cwd);
130
+ if (committed.code === 0 && committed.stdout?.trim()) parseDiffOutput(committed.stdout);
131
+ // Uncommitted changes (working tree + staged)
132
+ const uncommitted = runGit(["diff", "--name-status", "HEAD"], cwd);
133
+ if (uncommitted.code === 0 && uncommitted.stdout?.trim()) parseDiffOutput(uncommitted.stdout);
134
+ }
135
+
136
+ return files;
137
+ }
138
+
139
+ export function registerNvim(pi: ExtensionAPI): void {
140
+ if (!isInsideNvim()) return;
141
+ if (process.env.PI_SUBAGENT_CHILD === "1") return;
142
+
143
+ pi.registerCommand("nvim-changed-files", {
144
+ description: "Open git changed files in nvim's quickfix list",
145
+ handler: async (_args, ctx) => {
146
+ if (!ctx.hasUI) return;
147
+
148
+ const files = getChangedFiles(ctx.cwd);
149
+ if (files.length === 0) {
150
+ ctx.ui.notify("No changed files found.", "info");
151
+ return;
152
+ }
153
+
154
+ const entries = files.map(f => ({
155
+ filename: path.resolve(ctx.cwd, f.filename),
156
+ lnum: 1,
157
+ text: f.status,
158
+ }));
159
+
160
+ const ok = nvimSetQuickfix(entries, "pi: changed files", ctx.cwd);
161
+ if (ok) {
162
+ ctx.ui.notify(`Sent ${entries.length} changed file(s) to nvim quickfix.`, "info");
163
+ } else {
164
+ ctx.ui.notify("Failed to send quickfix to nvim.", "warning");
165
+ }
166
+ },
167
+ });
168
+
169
+ }
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Pure overlay dashboard helpers — no TUI deps (unit-testable).
3
+ */
4
+
5
+ export type OverlayId = string | number;
6
+
7
+ export interface OverlaySelection<TId extends OverlayId = OverlayId> {
8
+ id?: TId;
9
+ index: number;
10
+ }
11
+
12
+ export function reconcileSelection<TId extends OverlayId>(
13
+ selection: OverlaySelection<TId>,
14
+ items: ReadonlyArray<{ id: TId }>,
15
+ ): void {
16
+ const stableIndex =
17
+ selection.id !== undefined && selection.id !== null
18
+ ? items.findIndex((item) => item.id === selection.id)
19
+ : -1;
20
+ selection.index =
21
+ stableIndex >= 0
22
+ ? stableIndex
23
+ : Math.min(Math.max(0, selection.index), Math.max(0, items.length - 1));
24
+ selection.id = items[selection.index]?.id;
25
+ }