pi-repoprompt-mcp 0.3.0 → 0.4.0
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
CHANGED
|
@@ -1,15 +1,10 @@
|
|
|
1
1
|
# RepoPrompt MCP for Pi (`pi-repoprompt-mcp`)
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
This package provides a single tool (`rp`) that exposes the RepoPrompt MCP tools to Pi, includes branch-safe window and tab binding (automatically detecting the right window by `cwd`, provisioning a safe tab, persisting both across sessions and session tree nodes, and letting you pick windows and tabs interactively) 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 package'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. **Note: this recovery currently requires the original workspace (but not necessarily its original tabs) to be open, not just any workspace containing the same required root(s).**
|
|
11
6
|
|
|
12
|
-
##
|
|
7
|
+
## Installation
|
|
13
8
|
|
|
14
9
|
From npm:
|
|
15
10
|
|
|
@@ -35,28 +30,104 @@ 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 a completely blank tab, the package binds to that tab; if the tab is dirty, then 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 package and need to target different windows or tabs simultaneously.
|
|
46
|
+
|
|
47
|
+
### Output rendering
|
|
48
|
+
|
|
49
|
+
- Syntax highlighting for read files' code blocks and for codemaps
|
|
50
|
+
- Diff highlighting for diff blocks (`delta` when installed, honoring the user's global git/delta color config, graceful fallback otherwise)
|
|
51
|
+
- Markdown-aware styling for headings and lists
|
|
52
|
+
- Collapsed output by default (expand using Pi's standard UI controls)
|
|
53
|
+
|
|
54
|
+
### Safety checks
|
|
55
|
+
|
|
56
|
+
- Delete operations are blocked unless you pass `allowDelete: true`
|
|
57
|
+
- Optional edit confirmation gate for edit-like operations (`confirmEdits`)
|
|
58
|
+
- Warn on in-place workspace switches (when applicable)
|
|
59
|
+
|
|
38
60
|
## Requirements
|
|
39
61
|
|
|
40
|
-
- RepoPrompt
|
|
41
|
-
- RepoPrompt MCP server reachable (stdio transport)
|
|
62
|
+
- RepoPrompt MCP server configured and reachable (stdio transport)
|
|
42
63
|
- If the server is not configured/auto-detected, the package will still load, but `rp(...)` will error until you configure it
|
|
43
|
-
- `rp-cli`
|
|
64
|
+
- `rp-cli` available in `PATH` is recommended (used as a fallback for window discovery)
|
|
65
|
+
|
|
66
|
+
### Compatibility notes
|
|
67
|
+
|
|
68
|
+
This package 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:
|
|
69
|
+
|
|
70
|
+
- **Window discovery**: `list_windows`
|
|
71
|
+
- If `list_windows` is not exposed by the MCP server, the package falls back to `rp-cli -e 'windows'`
|
|
72
|
+
- If neither is available, window listing/binding features will be limited
|
|
73
|
+
- **Workspace root discovery (auto-bind by cwd)**: `get_file_tree` with `{ type: "roots" }` (scoped by `_windowID`)
|
|
74
|
+
- If unavailable (or if parameters/semantics change), auto-binding may be disabled or less accurate
|
|
75
|
+
- Selection summary: `manage_selection` with `{ op: "get", view: "files" }` and `{ op: "get", view: "summary" }`
|
|
76
|
+
- If these are unavailable (or if parameters/semantics change), the status output may omit file/token counts
|
|
77
|
+
|
|
78
|
+
If RepoPrompt renames/removes these tools or changes their required parameters/output formats, this package may need updates
|
|
44
79
|
|
|
45
80
|
## Usage
|
|
46
81
|
|
|
47
|
-
Commands
|
|
48
|
-
|
|
49
|
-
- `/rp
|
|
50
|
-
- `/rp
|
|
51
|
-
- `/rp
|
|
52
|
-
- `/rp
|
|
53
|
-
- `/rp
|
|
82
|
+
### Commands
|
|
83
|
+
|
|
84
|
+
- `/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
|
|
85
|
+
- `/rp windows` — list available RepoPrompt windows
|
|
86
|
+
- `/rp bind` — interactive workflow for choosing the RepoPrompt window
|
|
87
|
+
- `/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 package restores the branch's tab for that window or provisions a fresh background tab once
|
|
88
|
+
- `/rp tab` — interactive tab picker for the current bound window, with `Create new tab` as the first option followed by existing tab names
|
|
89
|
+
- `/rp tab new` — create and bind a fresh tab on the current bound window
|
|
90
|
+
- `/rp tab <name-or-id>` — bind an existing tab on the current bound window by name or id
|
|
91
|
+
- `/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.
|
|
92
|
+
- `/rp reconnect` — reconnect to RepoPrompt
|
|
93
|
+
|
|
94
|
+
### Tool: `rp`
|
|
95
|
+
|
|
96
|
+
Examples:
|
|
54
97
|
|
|
55
|
-
Tool:
|
|
56
98
|
```ts
|
|
99
|
+
// Status (connection + binding)
|
|
100
|
+
rp({ })
|
|
101
|
+
|
|
102
|
+
// List windows (best-effort; uses MCP tool if available, otherwise rp-cli)
|
|
57
103
|
rp({ windows: true })
|
|
104
|
+
|
|
105
|
+
// Bind to a specific window (does not change RepoPrompt active window)
|
|
58
106
|
rp({ bind: { window: 3 } })
|
|
107
|
+
|
|
108
|
+
// Bind to an exact tab in that window
|
|
109
|
+
rp({ bind: { window: 3, tab: "T2" } })
|
|
110
|
+
|
|
111
|
+
// Search or describe tools
|
|
112
|
+
rp({ search: "file" })
|
|
113
|
+
rp({ describe: "apply_edits" })
|
|
114
|
+
|
|
115
|
+
// Call a RepoPrompt tool (binding args are injected automatically)
|
|
59
116
|
rp({ call: "read_file", args: { path: "src/main.ts" } })
|
|
117
|
+
|
|
118
|
+
// Edit confirmation gate (only required if confirmEdits=true in config)
|
|
119
|
+
rp({
|
|
120
|
+
call: "apply_edits",
|
|
121
|
+
args: { path: "file.ts", search: "old", replace: "new" },
|
|
122
|
+
confirmEdits: true
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
// Delete guard override
|
|
126
|
+
rp({
|
|
127
|
+
call: "file_actions",
|
|
128
|
+
args: { action: "delete", path: "temp.txt" },
|
|
129
|
+
allowDelete: true
|
|
130
|
+
})
|
|
60
131
|
```
|
|
61
132
|
|
|
62
133
|
## Configuration
|
|
@@ -65,32 +136,78 @@ Create `~/.pi/agent/extensions/repoprompt-mcp.json`:
|
|
|
65
136
|
|
|
66
137
|
```json
|
|
67
138
|
{
|
|
139
|
+
"command": "rp-mcp-server",
|
|
140
|
+
"args": [],
|
|
141
|
+
|
|
68
142
|
"autoBindOnStart": true,
|
|
69
143
|
"persistBinding": true,
|
|
144
|
+
|
|
70
145
|
"confirmDeletes": true,
|
|
71
146
|
"confirmEdits": false,
|
|
147
|
+
|
|
72
148
|
"readcacheReadFile": true,
|
|
73
|
-
"
|
|
149
|
+
"autoSelectReadSlices": true,
|
|
150
|
+
"oracleDefaultMode": "chat",
|
|
151
|
+
|
|
152
|
+
"collapsedMaxLines": 3,
|
|
153
|
+
"suppressHostDisconnectedLog": true
|
|
74
154
|
}
|
|
75
155
|
```
|
|
76
156
|
|
|
77
|
-
`collapsedMaxLines` controls how many lines of RepoPrompt tool output Pi shows
|
|
157
|
+
`collapsedMaxLines` controls how many lines of RepoPrompt tool output Pi shows before the result is expanded. This applies to the collapsed preview for all `rp(...)` calls, including commands like window listings and file reads. **Recommended setting for maximally compressed** but still informative output: `3`.
|
|
78
158
|
|
|
79
|
-
|
|
159
|
+
Options:
|
|
80
160
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
161
|
+
| Option | Default | Description |
|
|
162
|
+
|---|---:|---|
|
|
163
|
+
| `command` | auto-detect | MCP server command |
|
|
164
|
+
| `args` | `[]` | MCP server args |
|
|
165
|
+
| `env` | unset | Extra environment variables for the MCP server |
|
|
166
|
+
| `autoBindOnStart` | `true` | Auto-detect and bind on session start, then reconcile the branch-safe tab for the chosen window |
|
|
167
|
+
| `persistBinding` | `true` | Persist window and tab bindings in Pi session history for branch-safe replay |
|
|
168
|
+
| `confirmDeletes` | `true` | Block delete operations unless `allowDelete: true` |
|
|
169
|
+
| `confirmEdits` | `false` | Block edit-like operations unless `confirmEdits: true` |
|
|
170
|
+
| `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) |
|
|
171
|
+
| `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 package-owned snapshot replay |
|
|
172
|
+
| `oracleDefaultMode` | `"chat"` | Default mode for `/rp oracle` when `--mode` is omitted (`chat`, `plan`, `edit`, or `review`) |
|
|
173
|
+
| `collapsedMaxLines` | `15` | Lines shown in collapsed view |
|
|
174
|
+
| `suppressHostDisconnectedLog` | `true` | Filter noisy stderr from macOS `repoprompt-mcp` (disconnect/retry bootstrap logs) |
|
|
87
175
|
|
|
88
|
-
|
|
176
|
+
Automatic tab restoration and provisioning is driven by `autoBindOnStart` and `persistBinding`; there is no separate tab-only configuration surface.
|
|
89
177
|
|
|
90
|
-
|
|
178
|
+
Note: when `readcacheReadFile` is enabled, the package may persist UTF-8 file snapshots to an on-disk content-addressed store under
|
|
179
|
+
`<repo-root>/.pi/readcache/objects` to compute diffs/unchanged markers across calls. Common secret filenames (e.g. `.env*`, `*.pem`) are excluded,
|
|
180
|
+
but this is best-effort
|
|
91
181
|
|
|
92
182
|
## Readcache gotchas
|
|
93
183
|
|
|
94
184
|
- `raw: true` disables readcache (and rendering). Don't use unless debugging
|
|
95
185
|
- Need full content? use `bypass_cache: true` in `read_file` args
|
|
96
186
|
- Multi-root: use absolute or specific relative paths (MCP `read_file` has no `RootName:` disambiguation)
|
|
187
|
+
|
|
188
|
+
## Troubleshooting
|
|
189
|
+
|
|
190
|
+
### "Not connected to RepoPrompt"
|
|
191
|
+
- Ensure RepoPrompt is running
|
|
192
|
+
- Verify the MCP server command in config
|
|
193
|
+
- Run `/rp reconnect`
|
|
194
|
+
|
|
195
|
+
### Pi becomes unresponsive after closing/restarting RepoPrompt
|
|
196
|
+
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 package will drop the connection and you can recover with `/rp reconnect`.
|
|
197
|
+
|
|
198
|
+
### "No matching window found"
|
|
199
|
+
- Your `cwd` may not match any RepoPrompt workspace root
|
|
200
|
+
- Use `/rp windows` to list windows
|
|
201
|
+
- Use `/rp bind` to pick one
|
|
202
|
+
|
|
203
|
+
### Window listing doesn't work
|
|
204
|
+
- If the MCP server does not expose a `list_windows` tool, this package uses `rp-cli -e 'windows'`
|
|
205
|
+
- Make sure `rp-cli` is installed and on your `PATH`
|
|
206
|
+
- If RepoPrompt is in single-window mode, `rp-cli -e 'windows'` may report single-window mode
|
|
207
|
+
|
|
208
|
+
### Delete operation blocked
|
|
209
|
+
- Pass `allowDelete: true` on the `rp` call
|
|
210
|
+
|
|
211
|
+
## License
|
|
212
|
+
|
|
213
|
+
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
|
+
}
|