pi-repoprompt-mcp 0.3.0 → 0.4.9
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.
- package/README.md +165 -34
- package/extensions/repoprompt-mcp/src/auto-select.ts +233 -2
- package/extensions/repoprompt-mcp/src/binding.ts +855 -11
- package/extensions/repoprompt-mcp/src/config.ts +21 -2
- package/extensions/repoprompt-mcp/src/diff-presentation.ts +79 -0
- package/extensions/repoprompt-mcp/src/diff-renderer.ts +1421 -0
- package/extensions/repoprompt-mcp/src/file-action-normalization.ts +96 -0
- package/extensions/repoprompt-mcp/src/index.ts +1126 -326
- package/extensions/repoprompt-mcp/src/language-detection.ts +33 -0
- package/extensions/repoprompt-mcp/src/presentation-summary.ts +302 -0
- package/extensions/repoprompt-mcp/src/render.ts +323 -433
- package/extensions/repoprompt-mcp/src/result-normalization.ts +69 -0
- package/extensions/repoprompt-mcp/src/tool-forwarding-policy.ts +17 -0
- package/extensions/repoprompt-mcp/src/types.ts +7 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,15 +1,10 @@
|
|
|
1
1
|
# RepoPrompt MCP for Pi (`pi-repoprompt-mcp`)
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
This extension provides a single tool (`rp`) that exposes RepoPrompt MCP tools to Pi, includes branch-safe window and tab binding (auto-detect and bind to window by `cwd`, auto-bind to safe tab, persist and restore across sessions and session tree nodes, and interactive selection of windows and tabs) and batches of read files (automatically selected as context in the RepoPrompt desktop app), renders RepoPrompt tool outputs (syntax + diff highlighting), and applies guardrails for destructive operations.
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
- window/tab binding (auto-detect by `cwd`, optional persistence); these are branch-safe across navigation of the session DAG via `/tree` and across `/fork`ed sessions
|
|
7
|
-
- output rendering (syntax + diff highlighting; uses `delta` when installed, honoring the user's global git/delta color config, with graceful fallback)
|
|
8
|
-
- safety guardrails (deletes blocked unless explicitly allowed; optional edit confirmation)
|
|
9
|
-
- optional [Gurpartap/pi-readcache](https://github.com/Gurpartap/pi-readcache)-like caching for RepoPrompt `read_file` results (unchanged markers + diffs) to save on tokens
|
|
10
|
-
- optional auto-selection in the RP UI (e.g. for use in RP Chat) of slices/files the agent has read; these selections are also branch-safe across `/tree` navigation and `/fork`ed session
|
|
5
|
+
The extension's window- and tab-related management features allow a workflow where new Pi sessions automatically attach to the required workspace and tab without clobbering your, or other agents', parallel usage of RepoPrompt. Because it recovers the window, tab, and auto-selected read-files context when you rewind via `/tree` or restore a session, all the context the agent has built up (and automatically selected in the RepoPrompt app) by reading files and slices up to that point always remains available in the app for RP Chat (see `/rp oracle` below) or external "oracle" (e.g. GPT-x Pro) use cases. Recovery is based on the required root(s) of the saved selection state, so it can reattach to any open workspace that already contains those roots rather than requiring the original workspace name; if multiple open workspaces satisfy that requirement and `cwd` does not disambiguate them, then you should re-bind with `/rp bind`.
|
|
11
6
|
|
|
12
|
-
##
|
|
7
|
+
## Installation
|
|
13
8
|
|
|
14
9
|
From npm:
|
|
15
10
|
|
|
@@ -35,28 +30,114 @@ Add to `~/.pi/agent/settings.json` (or replace an existing unfiltered `git:githu
|
|
|
35
30
|
}
|
|
36
31
|
```
|
|
37
32
|
|
|
33
|
+
## Features
|
|
34
|
+
|
|
35
|
+
### Window and tab binding
|
|
36
|
+
|
|
37
|
+
- Auto-binds to the RepoPrompt window that matches `process.cwd()` (by workspace roots, resolving symlinks to their real paths before matching)
|
|
38
|
+
- If multiple windows match, you're prompted to pick one
|
|
39
|
+
- Window binding is (optionally) persisted across session reloads and session tree nodes
|
|
40
|
+
- If a bound window has an existing tab with zero selected files and no chats, the extension binds to that tab; otherwise it provisions a new tab and binds to that
|
|
41
|
+
- Deterministically reconciles the session tree node's bound tab, and can restore the tab already associated with that node or provision a new safe tab when needed
|
|
42
|
+
- User-driven binding via `/rp bind` (windows) or `/rp tab` (tabs); agents can use `rp({ bind: ... })`
|
|
43
|
+
- In addition to window bindings, tab bindings and auto-selected read-files context is stored and automatically recovered across node rewinds via `/tree`, different sessions (e.g., created via `/fork`), and resumed sessions
|
|
44
|
+
|
|
45
|
+
Forked sessions inherit the parent session-plus-node's window, tab, and auto-selected context snapshot at the fork point (unless you rewind in the forked session and switch window/tab/etc.), then can diverge independently as later reads or manual tab switches are performed in the child session. Binding is non-invasive, in that it doesn't change RepoPrompt's globally active window, and automatic tab provisioning uses background tabs (`focus=false`) without stealing UI focus. This is to prevent interference when multiple agents (or your manual usage of RepoPrompt in parallel to a Pi session) are using this extension and need to target different windows or tabs simultaneously.
|
|
46
|
+
|
|
47
|
+
### Output rendering
|
|
48
|
+
|
|
49
|
+
- Syntax highlighting for code blocks and codemaps in `read_file`, and for code blocks in outputs of `apply_edits`, `file_actions create/delete`, and `git`
|
|
50
|
+
- Common non-mutating RepoPrompt actions (`read_file`, `file_search`, `get_file_tree`, `get_code_structure`, `workspace_context`, routing helpers like `manage_workspaces`, and control/discovery actions like `windows`/`bind`/`status`/`search`/`describe`) get concise request-driven call/result summaries in collapsed mode. The call line carries intent while the result line carries outcome, so the transcript stays compact without echoing the same label twice. These summaries are derived from the arguments Pi sent, not by parsing RepoPrompt's prose output, and unknown tools fall back to normal collapsed rendering
|
|
51
|
+
<img width="270" height="936" alt="Image" src="https://github.com/user-attachments/assets/142ca6c2-c1cf-4f0b-b41b-3d52d623c78c" />
|
|
52
|
+
- RepoPrompt `apply_edits` calls are forwarded with `verbose: true` by default (unless `raw: true`), while the returned diff is normalized into `details.diff` and presented to the agent as a terse summary. The same is done for `file_actions create/delete` outputs, so you see all edited/created/deleted LOC with rich rendering but the extension prevents the context window from getting bloated by round-tripping tool I/O tokens
|
|
53
|
+
- Adaptive diff rendering for RepoPrompt `git` and `apply_edits` outputs by default (`diffViewMode: "auto"` picks split, unified, compact, or summary at render time based on pane width). This uses the active Pi theme's `toolDiffAdded`, `toolDiffRemoved`, and `toolDiffContext` colors (typically mapped to chosen hues for green and red), and its visual design and rendering logic are indebted to [MasuRii/pi-tool-display](https://github.com/MasuRii/pi-tool-display). Two different examples at different pane widths:
|
|
54
|
+
<img width="1027" height="256" alt="horizontal" src="https://github.com/user-attachments/assets/31943d5b-475c-4254-813b-18bf9bd79d60" />
|
|
55
|
+
<img width="629" height="302" alt="vertical" src="https://github.com/user-attachments/assets/fe4fc253-6bda-49e3-a37e-918244eb9e05" />
|
|
56
|
+
- Generic fenced diff blocks, and adaptive-diff parse failures, fall back to a simpler diff renderer, which uses `delta` if installed or otherwise the built-in highlighter
|
|
57
|
+
- Markdown-aware styling for headings and lists
|
|
58
|
+
|
|
59
|
+
### Safety checks
|
|
60
|
+
|
|
61
|
+
- Delete operations are blocked unless you pass `allowDelete: true`
|
|
62
|
+
- Optional edit confirmation gate for edit-like operations (`confirmEdits`)
|
|
63
|
+
- Warn on in-place workspace switches (when applicable)
|
|
64
|
+
|
|
38
65
|
## Requirements
|
|
39
66
|
|
|
40
|
-
- RepoPrompt
|
|
41
|
-
-
|
|
42
|
-
|
|
43
|
-
|
|
67
|
+
- RepoPrompt MCP server configured and reachable (stdio transport)
|
|
68
|
+
- If the server is not configured/auto-detected, the extension will still load, but `rp(...)` will error until you configure it
|
|
69
|
+
- `rp-cli` available in `PATH` is recommended (used as a fallback for window discovery)
|
|
70
|
+
|
|
71
|
+
### Compatibility notes
|
|
72
|
+
|
|
73
|
+
This extension tries to be tolerant of **tool name prefixing** (e.g. `RepoPrompt_list_windows` vs `list_windows`), but it is still dependent on a small set of capabilities and their semantics remaining reasonably stable across RepoPrompt versions:
|
|
74
|
+
|
|
75
|
+
- **Window discovery**: `list_windows`
|
|
76
|
+
- If `list_windows` is not exposed by the MCP server, the extension falls back to `rp-cli -e 'windows'`
|
|
77
|
+
- If neither is available, window listing/binding features will be limited
|
|
78
|
+
- **Workspace root discovery (auto-bind by cwd)**: `get_file_tree` with `{ type: "roots" }` (scoped by `_windowID`)
|
|
79
|
+
- If unavailable (or if parameters/semantics change), auto-binding may be disabled or less accurate
|
|
80
|
+
- Selection summary: `manage_selection` with `{ op: "get", view: "files" }` and `{ op: "get", view: "summary" }`
|
|
81
|
+
- If these are unavailable (or if parameters/semantics change), the status output may omit file/token counts
|
|
82
|
+
|
|
83
|
+
If RepoPrompt renames/removes these tools or changes their required parameters/output formats, this extension may need updates
|
|
44
84
|
|
|
45
85
|
## Usage
|
|
46
86
|
|
|
47
|
-
Commands
|
|
48
|
-
|
|
49
|
-
- `/rp
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
87
|
+
### Commands
|
|
88
|
+
|
|
89
|
+
- `/rp status` — show status (connection + binding), including the currently bound tab name and a label like `[bound, in-focus]` or `[bound, out-of-focus]`, plus current selected file counts and estimated token counts
|
|
90
|
+
|
|
91
|
+
<p align="center">
|
|
92
|
+
<img width="210" alt="status" src="https://github.com/user-attachments/assets/bd59af9e-7df1-4572-8baf-edb6f8f7a0df" />
|
|
93
|
+
</p>
|
|
94
|
+
|
|
95
|
+
- `/rp windows` — list available RepoPrompt windows
|
|
96
|
+
- `/rp bind` — interactive workflow for choosing the RepoPrompt window
|
|
97
|
+
- `/rp bind <id> [tab]` — direct option if you already know the target window id (and optionally an exact tab name or tab id); when `[tab]` is omitted, the extension restores the branch's tab for that window or provisions a fresh background tab once
|
|
98
|
+
- `/rp tab` — interactive tab picker for the current bound window, with `Create new tab` as the first option followed by existing tab names
|
|
99
|
+
- `/rp tab new` — create and bind a fresh tab on the current bound window
|
|
100
|
+
- `/rp tab <name-or-id>` — bind an existing tab on the current bound window by name or id
|
|
101
|
+
- `/rp oracle [--mode <chat|plan|edit|review>] [--name <chat name>] [--continue|--chat-id <id>] <message>` — ask RepoPrompt chat with current selection context. If `--mode` not specified, uses `oracleDefaultMode` config.
|
|
102
|
+
- `/rp reconnect` — reconnect to RepoPrompt
|
|
103
|
+
|
|
104
|
+
### Tool: `rp`
|
|
105
|
+
|
|
106
|
+
Examples:
|
|
54
107
|
|
|
55
|
-
Tool:
|
|
56
108
|
```ts
|
|
109
|
+
// Status (connection + binding)
|
|
110
|
+
rp({ })
|
|
111
|
+
|
|
112
|
+
// List windows (best-effort; uses MCP tool if available, otherwise rp-cli)
|
|
57
113
|
rp({ windows: true })
|
|
114
|
+
|
|
115
|
+
// Bind to a specific window (does not change RepoPrompt active window)
|
|
58
116
|
rp({ bind: { window: 3 } })
|
|
117
|
+
|
|
118
|
+
// Bind to an exact tab in that window
|
|
119
|
+
rp({ bind: { window: 3, tab: "T2" } })
|
|
120
|
+
|
|
121
|
+
// Search or describe tools
|
|
122
|
+
rp({ search: "file" })
|
|
123
|
+
rp({ describe: "apply_edits" })
|
|
124
|
+
|
|
125
|
+
// Call a RepoPrompt tool (binding args are injected automatically)
|
|
59
126
|
rp({ call: "read_file", args: { path: "src/main.ts" } })
|
|
127
|
+
|
|
128
|
+
// Edit confirmation gate (only required if confirmEdits=true in config)
|
|
129
|
+
rp({
|
|
130
|
+
call: "apply_edits",
|
|
131
|
+
args: { path: "file.ts", search: "old", replace: "new" },
|
|
132
|
+
confirmEdits: true
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
// Delete guard override
|
|
136
|
+
rp({
|
|
137
|
+
call: "file_actions",
|
|
138
|
+
args: { action: "delete", path: "temp.txt" },
|
|
139
|
+
allowDelete: true
|
|
140
|
+
})
|
|
60
141
|
```
|
|
61
142
|
|
|
62
143
|
## Configuration
|
|
@@ -65,32 +146,82 @@ Create `~/.pi/agent/extensions/repoprompt-mcp.json`:
|
|
|
65
146
|
|
|
66
147
|
```json
|
|
67
148
|
{
|
|
149
|
+
"command": "rp-mcp-server",
|
|
150
|
+
"args": [],
|
|
151
|
+
|
|
68
152
|
"autoBindOnStart": true,
|
|
69
153
|
"persistBinding": true,
|
|
154
|
+
|
|
70
155
|
"confirmDeletes": true,
|
|
71
156
|
"confirmEdits": false,
|
|
72
|
-
"readcacheReadFile": true,
|
|
73
|
-
"collapsedMaxLines": 3
|
|
74
|
-
}
|
|
75
|
-
```
|
|
76
|
-
|
|
77
|
-
`collapsedMaxLines` controls how many lines of RepoPrompt tool output Pi shows in collapsed view before you expand the result. It applies across `rp(...)` calls, so it is the main knob for keeping window listings, reads, and other RepoPrompt responses compact in the TUI. `3` is the best default if you want maximally compressed but still informative output.
|
|
78
157
|
|
|
79
|
-
|
|
158
|
+
"readcacheReadFile": true,
|
|
159
|
+
"autoSelectReadSlices": true,
|
|
160
|
+
"oracleDefaultMode": "chat",
|
|
80
161
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
"
|
|
84
|
-
"
|
|
162
|
+
"collapsedMaxLines": 3,
|
|
163
|
+
"diffViewMode": "auto",
|
|
164
|
+
"diffSplitMinWidth": 120,
|
|
165
|
+
"suppressHostDisconnectedLog": true
|
|
85
166
|
}
|
|
86
167
|
```
|
|
87
168
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
169
|
+
`collapsedMaxLines` controls how many rendered lines of RepoPrompt tool output Pi shows before the result is expanded for the generic fallback path. In addition, the extension now emits hand-authored one-line or two-line collapsed summaries for common non-mutating actions like `read_file`, `file_search`, `get_file_tree`, `get_code_structure`, `workspace_context`, `windows`, `bind`, and `status`; these are derived from Pi's own request metadata rather than RepoPrompt's returned prose. Unknown or unsupported tools still fall back to the normal `collapsedMaxLines` behavior. LOC-changing operations are the other exception: verbose RepoPrompt `apply_edits` and rendered `file_actions create/delete` results ignore `collapsedMaxLines` once normalized into `details.diff`, so the full rendered code changes remain visible.
|
|
170
|
+
|
|
171
|
+
Options:
|
|
172
|
+
|
|
173
|
+
| Option | Default | Description |
|
|
174
|
+
|---|---:|---|
|
|
175
|
+
| `command` | auto-detect | MCP server command |
|
|
176
|
+
| `args` | `[]` | MCP server args |
|
|
177
|
+
| `env` | unset | Extra environment variables for the MCP server |
|
|
178
|
+
| `autoBindOnStart` | `true` | Auto-detect and bind on session start, then reconcile the branch-safe tab for the chosen window |
|
|
179
|
+
| `persistBinding` | `true` | Persist window and tab bindings in Pi session history for branch-safe replay |
|
|
180
|
+
| `confirmDeletes` | `true` | Block delete operations unless `allowDelete: true` |
|
|
181
|
+
| `confirmEdits` | `false` | Block edit-like operations unless `confirmEdits: true` |
|
|
182
|
+
| `readcacheReadFile` | `false` | Enable [pi-readcache](https://github.com/Gurpartap/pi-readcache)-like caching for RepoPrompt `read_file` calls (returns unchanged markers/diffs on repeat reads to save on tokens and prevent context bloat) |
|
|
183
|
+
| `autoSelectReadSlices` | `true` | Automatically track `read_file` calls by adding slices/full-file selection via `manage_selection`, so `chat_send` (or a manually created chat in the RP app) uses everything the agent has read as context; these file/slice selections are **branch-safe** across `/tree` rewinds and `/fork`ed session branches via extension-owned snapshot replay |
|
|
184
|
+
| `oracleDefaultMode` | `"chat"` | Default mode for `/rp oracle` when `--mode` is omitted (`chat`, `plan`, `edit`, or `review`) |
|
|
185
|
+
| `collapsedMaxLines` | `3` | Lines shown in collapsed view |
|
|
186
|
+
| `diffViewMode` | `"auto"` | Diff layout for RepoPrompt `git` / `apply_edits` fenced diff output (`auto`, `split`, `unified`) |
|
|
187
|
+
| `diffSplitMinWidth` | `120` | Minimum render width before `diffViewMode: "auto"` uses split diff layout |
|
|
188
|
+
| `suppressHostDisconnectedLog` | `true` | Filter noisy stderr from macOS `repoprompt-mcp` (disconnect/retry bootstrap logs) |
|
|
189
|
+
|
|
190
|
+
Automatic tab restoration and provisioning is driven by `autoBindOnStart` and `persistBinding`; there is no separate tab-only configuration surface. Adaptive diff layout applies only to RepoPrompt `git` and `apply_edits` outputs that arrive as fenced `diff` blocks; other rendered output stays on the existing text-based path.
|
|
191
|
+
|
|
192
|
+
Note: when `readcacheReadFile` is enabled, the extension may persist UTF-8 file snapshots to an on-disk content-addressed store under
|
|
193
|
+
`<repo-root>/.pi/readcache/objects` to compute diffs/unchanged markers across calls. Common secret filenames (e.g. `.env*`, `*.pem`) are excluded,
|
|
194
|
+
but this is best-effort
|
|
91
195
|
|
|
92
196
|
## Readcache gotchas
|
|
93
197
|
|
|
94
198
|
- `raw: true` disables readcache (and rendering). Don't use unless debugging
|
|
95
199
|
- Need full content? use `bypass_cache: true` in `read_file` args
|
|
96
200
|
- Multi-root: use absolute or specific relative paths (MCP `read_file` has no `RootName:` disambiguation)
|
|
201
|
+
|
|
202
|
+
## Troubleshooting
|
|
203
|
+
|
|
204
|
+
### "Not connected to RepoPrompt"
|
|
205
|
+
- Ensure RepoPrompt is running
|
|
206
|
+
- Verify the MCP server command in config
|
|
207
|
+
- Run `/rp reconnect`
|
|
208
|
+
|
|
209
|
+
### Pi becomes unresponsive after closing/restarting RepoPrompt
|
|
210
|
+
If the RepoPrompt MCP server stops responding (for example, if the RepoPrompt app is closed while Pi stays open), tool calls may time out. When that happens, the extension will drop the connection and you can recover with `/rp reconnect`.
|
|
211
|
+
|
|
212
|
+
### "No matching window found"
|
|
213
|
+
- Your `cwd` may not match any RepoPrompt workspace root
|
|
214
|
+
- Use `/rp windows` to list windows
|
|
215
|
+
- Use `/rp bind` to pick one
|
|
216
|
+
|
|
217
|
+
### Window listing doesn't work
|
|
218
|
+
- If the MCP server does not expose a `list_windows` tool, this extension uses `rp-cli -e 'windows'`
|
|
219
|
+
- Make sure `rp-cli` is installed and on your `PATH`
|
|
220
|
+
- If RepoPrompt is in single-window mode, `rp-cli -e 'windows'` may report single-window mode
|
|
221
|
+
|
|
222
|
+
### Delete operation blocked
|
|
223
|
+
- Pass `allowDelete: true` on the `rp` call
|
|
224
|
+
|
|
225
|
+
## License
|
|
226
|
+
|
|
227
|
+
MIT
|
|
@@ -3,6 +3,12 @@
|
|
|
3
3
|
import * as fs from "node:fs";
|
|
4
4
|
import { stat } from "node:fs/promises";
|
|
5
5
|
|
|
6
|
+
import type {
|
|
7
|
+
AutoSelectionEntryData,
|
|
8
|
+
AutoSelectionEntryRangeData,
|
|
9
|
+
AutoSelectionEntrySliceData,
|
|
10
|
+
} from "./types.js";
|
|
11
|
+
|
|
6
12
|
export type SelectionMode = "full" | "slices" | "codemap_only";
|
|
7
13
|
|
|
8
14
|
export interface SelectionStatus {
|
|
@@ -118,8 +124,128 @@ export function inferSelectionStatus(selectionText: string, selectionPath: strin
|
|
|
118
124
|
continue;
|
|
119
125
|
}
|
|
120
126
|
|
|
121
|
-
const parentPrefix = prefixes[indentDepth] ?? baseDir;
|
|
122
|
-
if (
|
|
127
|
+
const parentPrefix = prefixes[indentDepth] ?? baseDir ?? (indentDepth === 0 ? "" : null);
|
|
128
|
+
if (parentPrefix === null) {
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const nodePath = joinPath(parentPrefix, name);
|
|
133
|
+
|
|
134
|
+
if (name.endsWith("/")) {
|
|
135
|
+
prefixes[indentDepth + 1] = normalizeDir(nodePath);
|
|
136
|
+
prefixes.length = indentDepth + 2;
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const match = considerMatch(nodePath, rest);
|
|
141
|
+
if (match) {
|
|
142
|
+
return match;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return null;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export function inferSelectionSliceRanges(
|
|
150
|
+
selectionText: string,
|
|
151
|
+
selectionPath: string
|
|
152
|
+
): AutoSelectionEntryRangeData[] | null {
|
|
153
|
+
if (!selectionText || !selectionPath) {
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const target = toPosixPath(selectionPath).replace(/\/+$/, "");
|
|
158
|
+
|
|
159
|
+
const normalizeDir = (dir: string): string => {
|
|
160
|
+
const normalized = toPosixPath(dir).trim();
|
|
161
|
+
return normalized.endsWith("/") ? normalized : `${normalized}/`;
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
const joinPath = (dir: string, name: string): string => {
|
|
165
|
+
if (!dir) {
|
|
166
|
+
return name;
|
|
167
|
+
}
|
|
168
|
+
return dir.endsWith("/") ? `${dir}${name}` : `${dir}/${name}`;
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
const lines = selectionText.split("\n");
|
|
172
|
+
|
|
173
|
+
let section: "selected" | "codemaps" | null = null;
|
|
174
|
+
|
|
175
|
+
let baseDir: string | null = null;
|
|
176
|
+
const prefixes: string[] = [];
|
|
177
|
+
|
|
178
|
+
const considerMatch = (nodePath: string, rest: string): AutoSelectionEntryRangeData[] | null => {
|
|
179
|
+
const normalizedNode = toPosixPath(nodePath).replace(/\/+$/, "");
|
|
180
|
+
if (normalizedNode !== target) {
|
|
181
|
+
return null;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if (section !== "selected") {
|
|
185
|
+
return null;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const allRangesMatch = rest.match(/\(lines\s+([^)]*)\)/i);
|
|
189
|
+
if (!allRangesMatch) {
|
|
190
|
+
return null;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const rangesText = allRangesMatch[1] ?? "";
|
|
194
|
+
const ranges = [...rangesText.matchAll(/(\d+)\s*[-–—]\s*(\d+)/g)]
|
|
195
|
+
.map((m) => ({
|
|
196
|
+
start_line: Number(m[1]),
|
|
197
|
+
end_line: Number(m[2]),
|
|
198
|
+
}))
|
|
199
|
+
.filter((range) => Number.isFinite(range.start_line) && Number.isFinite(range.end_line))
|
|
200
|
+
.filter((range) => range.start_line > 0 && range.end_line >= range.start_line);
|
|
201
|
+
|
|
202
|
+
return ranges.length > 0 ? ranges : null;
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
for (const rawLine of lines) {
|
|
206
|
+
const line = toPosixPath(rawLine).trimEnd();
|
|
207
|
+
|
|
208
|
+
if (line.includes("### Selected Files")) {
|
|
209
|
+
section = "selected";
|
|
210
|
+
baseDir = null;
|
|
211
|
+
prefixes.length = 0;
|
|
212
|
+
continue;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
if (line.includes("### Codemaps")) {
|
|
216
|
+
section = "codemaps";
|
|
217
|
+
baseDir = null;
|
|
218
|
+
prefixes.length = 0;
|
|
219
|
+
continue;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
if (line.endsWith("/") && !line.includes("──") && !line.includes(" — ")) {
|
|
223
|
+
baseDir = normalizeDir(line);
|
|
224
|
+
prefixes.length = 0;
|
|
225
|
+
prefixes.push(baseDir);
|
|
226
|
+
continue;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const markerMatch = line.match(/(├──|└──)\s+(.*)$/);
|
|
230
|
+
if (!markerMatch) {
|
|
231
|
+
continue;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
const marker = markerMatch[1] ?? "";
|
|
235
|
+
const markerIdx = line.indexOf(marker);
|
|
236
|
+
const indentPrefix = markerIdx >= 0 ? line.slice(0, markerIdx) : "";
|
|
237
|
+
|
|
238
|
+
const indentDepth = Math.floor(indentPrefix.length / 4);
|
|
239
|
+
|
|
240
|
+
const rest = (markerMatch[2] ?? "").trim();
|
|
241
|
+
const name = rest.split(" — ")[0]?.trim() ?? "";
|
|
242
|
+
|
|
243
|
+
if (!name) {
|
|
244
|
+
continue;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const parentPrefix = prefixes[indentDepth] ?? baseDir ?? (indentDepth === 0 ? "" : null);
|
|
248
|
+
if (parentPrefix === null) {
|
|
123
249
|
continue;
|
|
124
250
|
}
|
|
125
251
|
|
|
@@ -203,6 +329,30 @@ export async function countFileLines(absolutePath: string): Promise<number> {
|
|
|
203
329
|
return lines;
|
|
204
330
|
}
|
|
205
331
|
|
|
332
|
+
export function isWholeFileReadFromArgs(
|
|
333
|
+
startLine: number | undefined,
|
|
334
|
+
limit: number | undefined,
|
|
335
|
+
totalLines: number | undefined
|
|
336
|
+
): boolean {
|
|
337
|
+
if (typeof totalLines !== "number" || totalLines <= 0 || typeof startLine !== "number") {
|
|
338
|
+
return false;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
if (startLine > 0) {
|
|
342
|
+
if (typeof limit !== "number" || limit <= 0) {
|
|
343
|
+
return false;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
return startLine === 1 && totalLines <= limit;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
if (startLine < 0) {
|
|
350
|
+
return Math.abs(startLine) >= totalLines;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
return false;
|
|
354
|
+
}
|
|
355
|
+
|
|
206
356
|
export function computeSliceRangeFromReadArgs(
|
|
207
357
|
startLine: number | undefined,
|
|
208
358
|
limit: number | undefined,
|
|
@@ -212,6 +362,10 @@ export function computeSliceRangeFromReadArgs(
|
|
|
212
362
|
return null;
|
|
213
363
|
}
|
|
214
364
|
|
|
365
|
+
if (isWholeFileReadFromArgs(startLine, limit, totalLines)) {
|
|
366
|
+
return null;
|
|
367
|
+
}
|
|
368
|
+
|
|
215
369
|
// Positive range reads
|
|
216
370
|
if (startLine > 0) {
|
|
217
371
|
if (typeof limit !== "number" || limit <= 0) {
|
|
@@ -241,3 +395,80 @@ export function computeSliceRangeFromReadArgs(
|
|
|
241
395
|
|
|
242
396
|
return null;
|
|
243
397
|
}
|
|
398
|
+
|
|
399
|
+
function normalizeSelectionRanges(ranges: AutoSelectionEntryRangeData[]): AutoSelectionEntryRangeData[] {
|
|
400
|
+
const normalized = ranges
|
|
401
|
+
.map((range) => ({
|
|
402
|
+
start_line: Number(range.start_line),
|
|
403
|
+
end_line: Number(range.end_line),
|
|
404
|
+
}))
|
|
405
|
+
.filter((range) => Number.isFinite(range.start_line) && Number.isFinite(range.end_line))
|
|
406
|
+
.filter((range) => range.start_line > 0 && range.end_line >= range.start_line)
|
|
407
|
+
.sort((a, b) => {
|
|
408
|
+
if (a.start_line !== b.start_line) {
|
|
409
|
+
return a.start_line - b.start_line;
|
|
410
|
+
}
|
|
411
|
+
return a.end_line - b.end_line;
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
const merged: AutoSelectionEntryRangeData[] = [];
|
|
415
|
+
for (const range of normalized) {
|
|
416
|
+
const last = merged[merged.length - 1];
|
|
417
|
+
if (!last) {
|
|
418
|
+
merged.push(range);
|
|
419
|
+
continue;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
if (range.start_line <= last.end_line + 1) {
|
|
423
|
+
last.end_line = Math.max(last.end_line, range.end_line);
|
|
424
|
+
continue;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
merged.push(range);
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
return merged;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
export function applyFullReadToSelectionState(
|
|
434
|
+
state: AutoSelectionEntryData,
|
|
435
|
+
selectionPath: string
|
|
436
|
+
): AutoSelectionEntryData {
|
|
437
|
+
const normalizedPath = toPosixPath(selectionPath);
|
|
438
|
+
|
|
439
|
+
return {
|
|
440
|
+
...state,
|
|
441
|
+
fullPaths: [...state.fullPaths, normalizedPath],
|
|
442
|
+
slicePaths: state.slicePaths.filter((entry) => entry.path !== normalizedPath),
|
|
443
|
+
};
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
export function applySliceReadToSelectionState(
|
|
447
|
+
state: AutoSelectionEntryData,
|
|
448
|
+
selectionPath: string,
|
|
449
|
+
range: AutoSelectionEntryRangeData
|
|
450
|
+
): AutoSelectionEntryData {
|
|
451
|
+
const normalizedPath = toPosixPath(selectionPath);
|
|
452
|
+
|
|
453
|
+
if (state.fullPaths.includes(normalizedPath)) {
|
|
454
|
+
return {
|
|
455
|
+
...state,
|
|
456
|
+
fullPaths: [...state.fullPaths],
|
|
457
|
+
slicePaths: [...state.slicePaths],
|
|
458
|
+
};
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
const existing = state.slicePaths.find((entry) => entry.path === normalizedPath);
|
|
462
|
+
|
|
463
|
+
const nextSlicePaths: AutoSelectionEntrySliceData[] = state.slicePaths.filter((entry) => entry.path !== normalizedPath);
|
|
464
|
+
nextSlicePaths.push({
|
|
465
|
+
path: normalizedPath,
|
|
466
|
+
ranges: normalizeSelectionRanges([...(existing?.ranges ?? []), range]),
|
|
467
|
+
});
|
|
468
|
+
|
|
469
|
+
return {
|
|
470
|
+
...state,
|
|
471
|
+
fullPaths: [...state.fullPaths],
|
|
472
|
+
slicePaths: nextSlicePaths,
|
|
473
|
+
};
|
|
474
|
+
}
|