@oh-my-pi/pi-coding-agent 13.12.7 → 13.12.8
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/CHANGELOG.md +28 -0
- package/package.json +7 -7
- package/src/config/keybindings.ts +1 -4
- package/src/modes/components/status-line/segments.ts +13 -5
- package/src/modes/controllers/command-controller.ts +8 -46
- package/src/modes/controllers/input-controller.ts +1 -0
- package/src/modes/prompt-action-autocomplete.ts +19 -3
- package/src/modes/utils/hotkeys-markdown.ts +57 -0
- package/src/sdk.ts +6 -2
- package/src/session/session-manager.ts +64 -45
- package/src/tools/ast-edit.ts +3 -2
- package/src/tools/ast-grep.ts +13 -3
- package/src/tools/find.ts +2 -2
- package/src/tools/grep.ts +3 -2
- package/src/tools/path-utils.ts +4 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,34 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [13.12.8] - 2026-03-16
|
|
6
|
+
|
|
7
|
+
### Breaking Changes
|
|
8
|
+
|
|
9
|
+
- Changed `SessionManager.create()` to require explicit `sessionDir` parameter instead of optional—callers must now pass `SessionManager.getDefaultSessionDir(cwd)` to use default behavior
|
|
10
|
+
- Changed `SessionManager.continueRecent()` to require explicit `sessionDir` parameter instead of optional—callers must now pass `SessionManager.getDefaultSessionDir(cwd)` to use default behavior
|
|
11
|
+
- Changed `SessionManager.forkFrom()` to require explicit `sessionDir` parameter instead of optional—callers must now pass `SessionManager.getDefaultSessionDir(cwd)` to use default behavior
|
|
12
|
+
- Changed `SessionManager.list()` signature to accept only `sessionDir` parameter instead of `cwd` and optional `sessionDir`—callers must now compute and pass the session directory explicitly
|
|
13
|
+
|
|
14
|
+
### Added
|
|
15
|
+
|
|
16
|
+
- Added `SessionManager.getDefaultSessionDir()` static method to explicitly resolve the canonical default session directory for a working directory
|
|
17
|
+
- Added support for quoted paths in grep, ast_grep, and find tools to handle directory names with spaces
|
|
18
|
+
- Added `normalizePathLikeInput` utility function to consistently handle quoted and whitespace-trimmed path inputs
|
|
19
|
+
|
|
20
|
+
### Changed
|
|
21
|
+
|
|
22
|
+
- Made `sessionDir` parameter optional in `SessionManager.create()`, `SessionManager.continueRecent()`, and `SessionManager.forkFrom()`—callers can now omit it to use the default session directory
|
|
23
|
+
- Changed `SessionManager.list()` signature to accept `cwd` as the first parameter instead of requiring an explicit `sessionDir`—callers can now omit `sessionDir` to use the default for the given working directory
|
|
24
|
+
- Updated `SessionManager.getDefaultSessionDir()` to accept optional `agentDir` parameter for computing session directories within a custom agent root
|
|
25
|
+
- Improved status line path display to strip display roots using canonical path resolution, correctly handling symlink aliases to home and Projects directories
|
|
26
|
+
- Improved error messaging in ast_grep when no matches are found with parse errors, now suggests narrowing `path`/`glob` or setting `lang` to resolve mis-scoped queries
|
|
27
|
+
|
|
28
|
+
### Fixed
|
|
29
|
+
|
|
30
|
+
- Fixed SDK-created default sessions to honor the configured `agentDir` for session storage, preventing tests from writing stray session directories into the real `~/.omp/agent/sessions` root
|
|
31
|
+
- Fixed session directory resolution to correctly handle symlink-equivalent paths, ensuring aliased home and temp directories resolve to the same session storage location as their real targets
|
|
32
|
+
|
|
5
33
|
## [13.12.7] - 2026-03-16
|
|
6
34
|
### Changed
|
|
7
35
|
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@oh-my-pi/pi-coding-agent",
|
|
4
|
-
"version": "13.12.
|
|
4
|
+
"version": "13.12.8",
|
|
5
5
|
"description": "Coding agent CLI with read, bash, edit, write tools and session management",
|
|
6
6
|
"homepage": "https://github.com/can1357/oh-my-pi",
|
|
7
7
|
"author": "Can Boluk",
|
|
@@ -41,12 +41,12 @@
|
|
|
41
41
|
},
|
|
42
42
|
"dependencies": {
|
|
43
43
|
"@mozilla/readability": "^0.6",
|
|
44
|
-
"@oh-my-pi/omp-stats": "13.12.
|
|
45
|
-
"@oh-my-pi/pi-agent-core": "13.12.
|
|
46
|
-
"@oh-my-pi/pi-ai": "13.12.
|
|
47
|
-
"@oh-my-pi/pi-natives": "13.12.
|
|
48
|
-
"@oh-my-pi/pi-tui": "13.12.
|
|
49
|
-
"@oh-my-pi/pi-utils": "13.12.
|
|
44
|
+
"@oh-my-pi/omp-stats": "13.12.8",
|
|
45
|
+
"@oh-my-pi/pi-agent-core": "13.12.8",
|
|
46
|
+
"@oh-my-pi/pi-ai": "13.12.8",
|
|
47
|
+
"@oh-my-pi/pi-natives": "13.12.8",
|
|
48
|
+
"@oh-my-pi/pi-tui": "13.12.8",
|
|
49
|
+
"@oh-my-pi/pi-utils": "13.12.8",
|
|
50
50
|
"@sinclair/typebox": "^0.34",
|
|
51
51
|
"@xterm/headless": "^6.0",
|
|
52
52
|
"ajv": "^8.18",
|
|
@@ -264,10 +264,7 @@ export class KeybindingsManager {
|
|
|
264
264
|
* Get display string for an action.
|
|
265
265
|
*/
|
|
266
266
|
getDisplayString(action: AppAction): string {
|
|
267
|
-
|
|
268
|
-
if (keys.length === 0) return "";
|
|
269
|
-
if (keys.length === 1) return keys[0]!;
|
|
270
|
-
return keys.join("/");
|
|
267
|
+
return formatKeyHints(this.getKeys(action));
|
|
271
268
|
}
|
|
272
269
|
|
|
273
270
|
/**
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import * as os from "node:os";
|
|
2
|
+
import * as path from "node:path";
|
|
2
3
|
import { ThinkingLevel } from "@oh-my-pi/pi-agent-core";
|
|
3
4
|
import { TERMINAL } from "@oh-my-pi/pi-tui";
|
|
4
|
-
import { formatDuration, formatNumber, getProjectDir } from "@oh-my-pi/pi-utils";
|
|
5
|
+
import { formatDuration, formatNumber, getProjectDir, relativePathWithinRoot } from "@oh-my-pi/pi-utils";
|
|
5
6
|
import { theme } from "../../../modes/theme/theme";
|
|
6
7
|
import { shortenPath } from "../../../tools/render-utils";
|
|
7
8
|
import { getContextUsageLevel, getContextUsageThemeColor } from "./context-thresholds";
|
|
@@ -17,6 +18,14 @@ function withIcon(icon: string, text: string): string {
|
|
|
17
18
|
return icon ? `${icon} ${text}` : text;
|
|
18
19
|
}
|
|
19
20
|
|
|
21
|
+
function stripDisplayRoot(pwd: string): string {
|
|
22
|
+
for (const root of ["/work", path.join(os.homedir(), "Projects")]) {
|
|
23
|
+
const relative = relativePathWithinRoot(root, pwd);
|
|
24
|
+
if (relative) return relative;
|
|
25
|
+
}
|
|
26
|
+
return pwd;
|
|
27
|
+
}
|
|
28
|
+
|
|
20
29
|
function normalizePremiumRequests(value: number): number {
|
|
21
30
|
return Math.round((value + Number.EPSILON) * 100) / 100;
|
|
22
31
|
}
|
|
@@ -87,13 +96,12 @@ const pathSegment: StatusLineSegment = {
|
|
|
87
96
|
|
|
88
97
|
let pwd = getProjectDir();
|
|
89
98
|
|
|
99
|
+
if (opts.stripWorkPrefix !== false) {
|
|
100
|
+
pwd = stripDisplayRoot(pwd);
|
|
101
|
+
}
|
|
90
102
|
if (opts.abbreviate !== false) {
|
|
91
103
|
pwd = shortenPath(pwd);
|
|
92
104
|
}
|
|
93
|
-
if (opts.stripWorkPrefix !== false) {
|
|
94
|
-
if (pwd.startsWith("/work/")) pwd = pwd.slice(6);
|
|
95
|
-
else if (pwd.startsWith("~/Projects/")) pwd = pwd.slice(11);
|
|
96
|
-
}
|
|
97
105
|
|
|
98
106
|
const maxLen = opts.maxLength ?? 40;
|
|
99
107
|
if (pwd.length > maxLen) {
|
|
@@ -24,6 +24,7 @@ import { DynamicBorder } from "../../modes/components/dynamic-border";
|
|
|
24
24
|
import { PythonExecutionComponent } from "../../modes/components/python-execution";
|
|
25
25
|
import { getMarkdownTheme, getSymbolTheme, theme } from "../../modes/theme/theme";
|
|
26
26
|
import type { InteractiveModeContext } from "../../modes/types";
|
|
27
|
+
import { buildHotkeysMarkdown } from "../../modes/utils/hotkeys-markdown";
|
|
27
28
|
import type { AsyncJobSnapshotItem } from "../../session/agent-session";
|
|
28
29
|
import type { AuthStorage } from "../../session/auth-storage";
|
|
29
30
|
import { outputMeta } from "../../tools/output-meta";
|
|
@@ -509,52 +510,13 @@ export class CommandController {
|
|
|
509
510
|
const sttKey = this.ctx.keybindings.getDisplayString("toggleSTT") || "Alt+H";
|
|
510
511
|
const copyLineKey = this.ctx.keybindings.getDisplayString("copyLine") || "Alt+Shift+L";
|
|
511
512
|
const copyPromptKey = this.ctx.keybindings.getDisplayString("copyPrompt") || "Alt+Shift+C";
|
|
512
|
-
const hotkeys =
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
| \`Ctrl+E\` / \`End\` / \`Cmd+Right\` | End of line |
|
|
520
|
-
|
|
521
|
-
**Editing**
|
|
522
|
-
| Key | Action |
|
|
523
|
-
|-----|--------|
|
|
524
|
-
| \`Enter\` | Send message |
|
|
525
|
-
| \`Shift+Enter\` / \`Alt+Enter\` | New line |
|
|
526
|
-
| \`Ctrl+W\` / \`Option+Backspace\` | Delete word backwards |
|
|
527
|
-
| \`Ctrl+U\` | Delete to start of line |
|
|
528
|
-
| \`Ctrl+K\` | Delete to end of line |
|
|
529
|
-
| \`${copyLineKey}\` | Copy current line |
|
|
530
|
-
| \`${copyPromptKey}\` | Copy whole prompt |
|
|
531
|
-
|
|
532
|
-
**Other**
|
|
533
|
-
| Key | Action |
|
|
534
|
-
|-----|--------|
|
|
535
|
-
| \`Tab\` | Path completion / accept autocomplete |
|
|
536
|
-
| \`Escape\` | Cancel autocomplete / abort streaming |
|
|
537
|
-
| \`Ctrl+C\` | Clear editor (first) / exit (second) |
|
|
538
|
-
| \`Ctrl+D\` | Exit (when editor is empty) |
|
|
539
|
-
| \`Ctrl+Z\` | Suspend to background |
|
|
540
|
-
| \`Shift+Tab\` | Cycle thinking level |
|
|
541
|
-
| \`Ctrl+P\` | Cycle role models (slow/default/smol) |
|
|
542
|
-
| \`Shift+Ctrl+P\` | Cycle role models (temporary) |
|
|
543
|
-
| \`Alt+P\` | Select model (temporary) |
|
|
544
|
-
| \`Ctrl+L\` | Select model (set roles) |
|
|
545
|
-
| \`${planModeKey}\` | Toggle plan mode |
|
|
546
|
-
| \`Ctrl+R\` | Search prompt history |
|
|
547
|
-
| \`${expandToolsKey}\` | Toggle tool output expansion |
|
|
548
|
-
| \`Ctrl+T\` | Toggle todo list expansion |
|
|
549
|
-
| \`Ctrl+G\` | Edit message in external editor |
|
|
550
|
-
| \`${sttKey}\` | Toggle speech-to-text recording |
|
|
551
|
-
| \`#\` | Open prompt actions |
|
|
552
|
-
| \`/\` | Slash commands |
|
|
553
|
-
| \`!\` | Run bash command |
|
|
554
|
-
| \`!!\` | Run bash command (excluded from context) |
|
|
555
|
-
| \`$\` | Run Python in shared kernel |
|
|
556
|
-
| \`$$\` | Run Python (excluded from context) |
|
|
557
|
-
`;
|
|
513
|
+
const hotkeys = buildHotkeysMarkdown({
|
|
514
|
+
expandToolsKey,
|
|
515
|
+
planModeKey,
|
|
516
|
+
sttKey,
|
|
517
|
+
copyLineKey,
|
|
518
|
+
copyPromptKey,
|
|
519
|
+
});
|
|
558
520
|
this.ctx.chatContainer.addChild(new Spacer(1));
|
|
559
521
|
this.ctx.chatContainer.addChild(new DynamicBorder());
|
|
560
522
|
this.ctx.chatContainer.addChild(new Text(theme.bold(theme.fg("accent", "Keyboard Shortcuts")), 1, 0));
|
|
@@ -535,6 +535,7 @@ export class InputController {
|
|
|
535
535
|
keybindings: this.ctx.keybindings,
|
|
536
536
|
copyCurrentLine: () => this.handleCopyCurrentLine(),
|
|
537
537
|
copyPrompt: () => this.handleCopyPrompt(),
|
|
538
|
+
undo: prefix => this.ctx.editor.undoPastTransientText(prefix),
|
|
538
539
|
moveCursorToMessageEnd: () => this.ctx.editor.moveToMessageEnd(),
|
|
539
540
|
moveCursorToMessageStart: () => this.ctx.editor.moveToMessageStart(),
|
|
540
541
|
moveCursorToLineStart: () => this.ctx.editor.moveToLineStart(),
|
|
@@ -12,12 +12,12 @@ interface PromptActionDefinition {
|
|
|
12
12
|
label: string;
|
|
13
13
|
description: string;
|
|
14
14
|
keywords: string[];
|
|
15
|
-
execute: () => void;
|
|
15
|
+
execute: (prefix: string) => void;
|
|
16
16
|
}
|
|
17
17
|
|
|
18
18
|
interface PromptActionAutocompleteItem extends AutocompleteItem {
|
|
19
19
|
actionId: string;
|
|
20
|
-
execute: () => void;
|
|
20
|
+
execute: (prefix: string) => void;
|
|
21
21
|
}
|
|
22
22
|
|
|
23
23
|
interface PromptActionAutocompleteOptions {
|
|
@@ -26,6 +26,7 @@ interface PromptActionAutocompleteOptions {
|
|
|
26
26
|
keybindings: KeybindingsManager;
|
|
27
27
|
copyCurrentLine: () => void;
|
|
28
28
|
copyPrompt: () => void;
|
|
29
|
+
undo: (prefix: string) => void;
|
|
29
30
|
moveCursorToMessageEnd: () => void;
|
|
30
31
|
moveCursorToMessageStart: () => void;
|
|
31
32
|
moveCursorToLineStart: () => void;
|
|
@@ -141,6 +142,14 @@ export class PromptActionAutocompleteProvider implements AutocompleteProvider {
|
|
|
141
142
|
onApplied?: () => void;
|
|
142
143
|
} {
|
|
143
144
|
if (prefix.startsWith("#") && isPromptActionItem(item)) {
|
|
145
|
+
if (item.actionId === "undo") {
|
|
146
|
+
return {
|
|
147
|
+
lines,
|
|
148
|
+
cursorLine,
|
|
149
|
+
cursorCol,
|
|
150
|
+
onApplied: () => item.execute(prefix),
|
|
151
|
+
};
|
|
152
|
+
}
|
|
144
153
|
const currentLine = lines[cursorLine] || "";
|
|
145
154
|
const beforePrefix = currentLine.slice(0, cursorCol - prefix.length);
|
|
146
155
|
const afterCursor = currentLine.slice(cursorCol);
|
|
@@ -150,7 +159,7 @@ export class PromptActionAutocompleteProvider implements AutocompleteProvider {
|
|
|
150
159
|
lines: newLines,
|
|
151
160
|
cursorLine,
|
|
152
161
|
cursorCol: beforePrefix.length,
|
|
153
|
-
onApplied: item.execute,
|
|
162
|
+
onApplied: () => item.execute(prefix),
|
|
154
163
|
};
|
|
155
164
|
}
|
|
156
165
|
|
|
@@ -181,6 +190,13 @@ export function createPromptActionAutocompleteProvider(
|
|
|
181
190
|
keywords: ["copy", "prompt", "clipboard", "message"],
|
|
182
191
|
execute: options.copyPrompt,
|
|
183
192
|
},
|
|
193
|
+
{
|
|
194
|
+
id: "undo",
|
|
195
|
+
label: "Undo",
|
|
196
|
+
description: formatKeyHints(editorKeybindings.getKeys("undo")),
|
|
197
|
+
keywords: ["undo", "revert", "edit", "history"],
|
|
198
|
+
execute: options.undo,
|
|
199
|
+
},
|
|
184
200
|
{
|
|
185
201
|
id: "cursor-message-end",
|
|
186
202
|
label: "Move cursor to end of message",
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
export interface HotkeysMarkdownBindings {
|
|
2
|
+
expandToolsKey: string;
|
|
3
|
+
planModeKey: string;
|
|
4
|
+
sttKey: string;
|
|
5
|
+
copyLineKey: string;
|
|
6
|
+
copyPromptKey: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function buildHotkeysMarkdown(bindings: HotkeysMarkdownBindings): string {
|
|
10
|
+
const { expandToolsKey, planModeKey, sttKey, copyLineKey, copyPromptKey } = bindings;
|
|
11
|
+
return [
|
|
12
|
+
"**Navigation**",
|
|
13
|
+
"| Key | Action |",
|
|
14
|
+
"|-----|--------|",
|
|
15
|
+
"| `Arrow keys` | Move cursor / browse history (Up when empty) |",
|
|
16
|
+
"| `Option+Left/Right` | Move by word |",
|
|
17
|
+
"| `Ctrl+A` / `Home` / `Cmd+Left` | Start of line |",
|
|
18
|
+
"| `Ctrl+E` / `End` / `Cmd+Right` | End of line |",
|
|
19
|
+
"",
|
|
20
|
+
"**Editing**",
|
|
21
|
+
"| Key | Action |",
|
|
22
|
+
"|-----|--------|",
|
|
23
|
+
"| `Enter` | Send message |",
|
|
24
|
+
"| `Shift+Enter` / `Alt+Enter` | New line |",
|
|
25
|
+
"| `Ctrl+W` / `Option+Backspace` | Delete word backwards |",
|
|
26
|
+
"| `Ctrl+U` | Delete to start of line |",
|
|
27
|
+
"| `Ctrl+K` | Delete to end of line |",
|
|
28
|
+
`| \`${copyLineKey}\` | Copy current line |`,
|
|
29
|
+
`| \`${copyPromptKey}\` | Copy whole prompt |`,
|
|
30
|
+
"",
|
|
31
|
+
"**Other**",
|
|
32
|
+
"| Key | Action |",
|
|
33
|
+
"|-----|--------|",
|
|
34
|
+
"| `Tab` | Path completion / accept autocomplete |",
|
|
35
|
+
"| `Escape` | Cancel autocomplete / abort streaming |",
|
|
36
|
+
"| `Ctrl+C` | Clear editor (first) / exit (second) |",
|
|
37
|
+
"| `Ctrl+D` | Exit (when editor is empty) |",
|
|
38
|
+
"| `Ctrl+Z` | Suspend to background |",
|
|
39
|
+
"| `Shift+Tab` | Cycle thinking level |",
|
|
40
|
+
"| `Ctrl+P` | Cycle role models (slow/default/smol) |",
|
|
41
|
+
"| `Shift+Ctrl+P` | Cycle role models (temporary) |",
|
|
42
|
+
"| `Alt+P` | Select model (temporary) |",
|
|
43
|
+
"| `Ctrl+L` | Select model (set roles) |",
|
|
44
|
+
`| \`${planModeKey}\` | Toggle plan mode |`,
|
|
45
|
+
"| `Ctrl+R` | Search prompt history |",
|
|
46
|
+
`| \`${expandToolsKey}\` | Toggle tool output expansion |`,
|
|
47
|
+
"| `Ctrl+T` | Toggle todo list expansion |",
|
|
48
|
+
"| `Ctrl+G` | Edit message in external editor |",
|
|
49
|
+
`| \`${sttKey}\` | Toggle speech-to-text recording |`,
|
|
50
|
+
"| `#` | Open prompt actions |",
|
|
51
|
+
"| `/` | Slash commands |",
|
|
52
|
+
"| `!` | Run bash command |",
|
|
53
|
+
"| `!!` | Run bash command (excluded from context) |",
|
|
54
|
+
"| `$` | Run Python in shared kernel |",
|
|
55
|
+
"| `$$` | Run Python (excluded from context) |",
|
|
56
|
+
].join("\n");
|
|
57
|
+
}
|
package/src/sdk.ts
CHANGED
|
@@ -191,7 +191,7 @@ export interface CreateAgentSessionOptions {
|
|
|
191
191
|
/** Parent task ID prefix for nested artifact naming (e.g., "6-Extensions") */
|
|
192
192
|
parentTaskPrefix?: string;
|
|
193
193
|
|
|
194
|
-
/** Session manager. Default:
|
|
194
|
+
/** Session manager. Default: session stored under the configured agentDir sessions root */
|
|
195
195
|
sessionManager?: SessionManager;
|
|
196
196
|
|
|
197
197
|
/** Settings instance. Default: Settings.init({ cwd, agentDir }) */
|
|
@@ -656,7 +656,11 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
656
656
|
setPreferredImageProvider(imageProvider);
|
|
657
657
|
}
|
|
658
658
|
|
|
659
|
-
const sessionManager =
|
|
659
|
+
const sessionManager =
|
|
660
|
+
options.sessionManager ??
|
|
661
|
+
logger.time("sessionManager", () =>
|
|
662
|
+
SessionManager.create(cwd, SessionManager.getDefaultSessionDir(cwd, agentDir)),
|
|
663
|
+
);
|
|
660
664
|
const sessionId = sessionManager.getSessionId();
|
|
661
665
|
const modelApiKeyAvailability = new Map<string, boolean>();
|
|
662
666
|
const getModelAvailabilityKey = (candidate: Model): string =>
|
|
@@ -21,6 +21,8 @@ import {
|
|
|
21
21
|
isEnoent,
|
|
22
22
|
logger,
|
|
23
23
|
parseJsonlLenient,
|
|
24
|
+
pathIsWithin,
|
|
25
|
+
resolveEquivalentPath,
|
|
24
26
|
Snowflake,
|
|
25
27
|
toError,
|
|
26
28
|
} from "@oh-my-pi/pi-utils";
|
|
@@ -345,7 +347,7 @@ export function migrateSessionEntries(entries: FileEntry[]): void {
|
|
|
345
347
|
migrateToCurrentVersion(entries);
|
|
346
348
|
}
|
|
347
349
|
|
|
348
|
-
|
|
350
|
+
const migratedSessionRoots = new Set<string>();
|
|
349
351
|
|
|
350
352
|
/**
|
|
351
353
|
* Merge or rename a legacy session directory into its canonical target.
|
|
@@ -375,29 +377,36 @@ function encodeLegacyAbsoluteSessionDirName(cwd: string): string {
|
|
|
375
377
|
return `--${resolvedCwd.replace(/^[/\\]/, "").replace(/[/\\:]/g, "-")}--`;
|
|
376
378
|
}
|
|
377
379
|
|
|
378
|
-
function pathIsWithin(root: string, candidate: string): boolean {
|
|
379
|
-
const relative = path.relative(root, candidate);
|
|
380
|
-
return relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative));
|
|
381
|
-
}
|
|
382
|
-
|
|
383
380
|
function encodeRelativeSessionDirName(prefix: string, root: string, cwd: string): string {
|
|
384
381
|
const relative = path.relative(root, cwd).replace(/[/\\:]/g, "-");
|
|
385
|
-
return relative ? `${prefix}-${relative}` : prefix;
|
|
382
|
+
return relative ? (prefix.endsWith("-") ? `${prefix}${relative}` : `${prefix}-${relative}`) : prefix;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
function getDefaultSessionDirName(cwd: string): { encodedDirName: string; resolvedCwd: string } {
|
|
386
|
+
const resolvedCwd = path.resolve(cwd);
|
|
387
|
+
const canonicalCwd = resolveEquivalentPath(resolvedCwd);
|
|
388
|
+
const home = resolveEquivalentPath(os.homedir());
|
|
389
|
+
const tempRoot = resolveEquivalentPath(os.tmpdir());
|
|
390
|
+
const encodedDirName = pathIsWithin(home, canonicalCwd)
|
|
391
|
+
? encodeRelativeSessionDirName("-", home, canonicalCwd)
|
|
392
|
+
: pathIsWithin(tempRoot, canonicalCwd)
|
|
393
|
+
? encodeRelativeSessionDirName("-tmp", tempRoot, canonicalCwd)
|
|
394
|
+
: encodeLegacyAbsoluteSessionDirName(canonicalCwd);
|
|
395
|
+
return { encodedDirName, resolvedCwd };
|
|
386
396
|
}
|
|
387
397
|
|
|
388
398
|
/**
|
|
389
399
|
* Migrate old `--<home-encoded>-*--` session dirs to the new `-*` format.
|
|
390
|
-
* Runs once on first access, best-effort.
|
|
400
|
+
* Runs once per sessions root on first access, best-effort.
|
|
391
401
|
*/
|
|
392
|
-
function migrateHomeSessionDirs(): void {
|
|
393
|
-
if (
|
|
394
|
-
|
|
402
|
+
function migrateHomeSessionDirs(sessionsRoot: string): void {
|
|
403
|
+
if (migratedSessionRoots.has(sessionsRoot)) return;
|
|
404
|
+
migratedSessionRoots.add(sessionsRoot);
|
|
395
405
|
|
|
396
406
|
const home = os.homedir();
|
|
397
407
|
const homeEncoded = home.replace(/^[/\\]/, "").replace(/[/\\:]/g, "-");
|
|
398
408
|
const oldPrefix = `--${homeEncoded}-`;
|
|
399
409
|
const oldExact = `--${homeEncoded}--`;
|
|
400
|
-
const sessionsRoot = getSessionsDir();
|
|
401
410
|
|
|
402
411
|
let entries: string[];
|
|
403
412
|
try {
|
|
@@ -428,8 +437,8 @@ function migrateHomeSessionDirs(): void {
|
|
|
428
437
|
}
|
|
429
438
|
}
|
|
430
439
|
|
|
431
|
-
function migrateLegacyAbsoluteSessionDir(cwd: string, sessionDir: string): void {
|
|
432
|
-
const legacyDir = path.join(
|
|
440
|
+
function migrateLegacyAbsoluteSessionDir(cwd: string, sessionDir: string, sessionsRoot: string): void {
|
|
441
|
+
const legacyDir = path.join(sessionsRoot, encodeLegacyAbsoluteSessionDirName(cwd));
|
|
433
442
|
if (legacyDir === sessionDir || !fs.existsSync(legacyDir)) return;
|
|
434
443
|
|
|
435
444
|
try {
|
|
@@ -439,6 +448,15 @@ function migrateLegacyAbsoluteSessionDir(cwd: string, sessionDir: string): void
|
|
|
439
448
|
}
|
|
440
449
|
}
|
|
441
450
|
|
|
451
|
+
function resolveManagedSessionRoot(sessionDir: string, cwd: string): string | undefined {
|
|
452
|
+
const currentDirName = path.basename(sessionDir);
|
|
453
|
+
const { encodedDirName } = getDefaultSessionDirName(cwd);
|
|
454
|
+
if (currentDirName !== encodedDirName && currentDirName !== encodeLegacyAbsoluteSessionDirName(cwd)) {
|
|
455
|
+
return undefined;
|
|
456
|
+
}
|
|
457
|
+
return path.dirname(sessionDir);
|
|
458
|
+
}
|
|
459
|
+
|
|
442
460
|
/** Exported for compaction.test.ts */
|
|
443
461
|
export function parseSessionEntries(content: string): FileEntry[] {
|
|
444
462
|
return parseJsonlLenient<FileEntry>(content);
|
|
@@ -633,34 +651,20 @@ export function buildSessionContext(
|
|
|
633
651
|
return { messages, thinkingLevel, serviceTier, models, injectedTtsrRules, mode, modeData };
|
|
634
652
|
}
|
|
635
653
|
|
|
636
|
-
/**
|
|
637
|
-
* Encode a cwd into a safe directory name for session storage.
|
|
638
|
-
* Home-relative paths use single-dash format: `/Users/x/Projects/pi` → `-Projects-pi`
|
|
639
|
-
* Temp-root paths use `-tmp-` prefixes: `/tmp/foo` → `-tmp-foo`
|
|
640
|
-
* Other absolute paths keep the legacy double-dash format for compatibility.
|
|
641
|
-
*/
|
|
642
|
-
function encodeSessionDirName(cwd: string): string {
|
|
643
|
-
const resolvedCwd = path.resolve(cwd);
|
|
644
|
-
const home = path.resolve(os.homedir());
|
|
645
|
-
if (pathIsWithin(home, resolvedCwd)) {
|
|
646
|
-
return encodeRelativeSessionDirName("-", home, resolvedCwd);
|
|
647
|
-
}
|
|
648
|
-
const tempRoot = path.resolve(os.tmpdir());
|
|
649
|
-
if (pathIsWithin(tempRoot, resolvedCwd)) {
|
|
650
|
-
return encodeRelativeSessionDirName("-tmp", tempRoot, resolvedCwd);
|
|
651
|
-
}
|
|
652
|
-
return encodeLegacyAbsoluteSessionDirName(resolvedCwd);
|
|
653
|
-
}
|
|
654
|
-
|
|
655
654
|
/**
|
|
656
655
|
* Compute the default session directory for a cwd.
|
|
657
|
-
*
|
|
656
|
+
* Classifies cwd by canonical location so symlink/alias paths resolve to the
|
|
657
|
+
* same home-relative or temp-root directory names as their real targets.
|
|
658
658
|
*/
|
|
659
|
-
function
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
659
|
+
function computeDefaultSessionDir(
|
|
660
|
+
cwd: string,
|
|
661
|
+
storage: SessionStorage,
|
|
662
|
+
sessionsRoot: string = getSessionsDir(),
|
|
663
|
+
): string {
|
|
664
|
+
const { encodedDirName, resolvedCwd } = getDefaultSessionDirName(cwd);
|
|
665
|
+
migrateHomeSessionDirs(sessionsRoot);
|
|
666
|
+
const sessionDir = path.join(sessionsRoot, encodedDirName);
|
|
667
|
+
migrateLegacyAbsoluteSessionDir(resolvedCwd, sessionDir, sessionsRoot);
|
|
664
668
|
storage.ensureDirSync(sessionDir);
|
|
665
669
|
return sessionDir;
|
|
666
670
|
}
|
|
@@ -1322,7 +1326,8 @@ export async function resolveResumableSession(
|
|
|
1322
1326
|
sessionDir?: string,
|
|
1323
1327
|
storage: SessionStorage = new FileSessionStorage(),
|
|
1324
1328
|
): Promise<ResolvedSessionMatch | undefined> {
|
|
1325
|
-
const
|
|
1329
|
+
const localSessionDir = sessionDir ?? SessionManager.getDefaultSessionDir(cwd, undefined, storage);
|
|
1330
|
+
const localSessions = await SessionManager.list(cwd, localSessionDir, storage);
|
|
1326
1331
|
const localMatch = localSessions.find(session => sessionMatchesResumeArg(session, sessionArg));
|
|
1327
1332
|
if (localMatch) {
|
|
1328
1333
|
return { session: localMatch, scope: "local" };
|
|
@@ -1489,7 +1494,10 @@ export class SessionManager {
|
|
|
1489
1494
|
const resolvedCwd = path.resolve(newCwd);
|
|
1490
1495
|
if (resolvedCwd === this.cwd) return;
|
|
1491
1496
|
|
|
1492
|
-
const
|
|
1497
|
+
const managedSessionsRoot = resolveManagedSessionRoot(this.sessionDir, this.cwd);
|
|
1498
|
+
const newSessionDir = managedSessionsRoot
|
|
1499
|
+
? computeDefaultSessionDir(resolvedCwd, this.storage, managedSessionsRoot)
|
|
1500
|
+
: computeDefaultSessionDir(resolvedCwd, this.storage);
|
|
1493
1501
|
let hadSessionFile = false;
|
|
1494
1502
|
|
|
1495
1503
|
if (this.persist && this.#sessionFile) {
|
|
@@ -2446,13 +2454,24 @@ export class SessionManager {
|
|
|
2446
2454
|
return undefined;
|
|
2447
2455
|
}
|
|
2448
2456
|
|
|
2457
|
+
/**
|
|
2458
|
+
* Resolve the canonical default session directory for a cwd.
|
|
2459
|
+
*/
|
|
2460
|
+
static getDefaultSessionDir(
|
|
2461
|
+
cwd: string,
|
|
2462
|
+
agentDir?: string,
|
|
2463
|
+
storage: SessionStorage = new FileSessionStorage(),
|
|
2464
|
+
): string {
|
|
2465
|
+
return computeDefaultSessionDir(cwd, storage, getSessionsDir(agentDir));
|
|
2466
|
+
}
|
|
2467
|
+
|
|
2449
2468
|
/**
|
|
2450
2469
|
* Create a new session.
|
|
2451
2470
|
* @param cwd Working directory (stored in session header)
|
|
2452
2471
|
* @param sessionDir Optional session directory. If omitted, uses default (~/.omp/agent/sessions/<encoded-cwd>/).
|
|
2453
2472
|
*/
|
|
2454
2473
|
static create(cwd: string, sessionDir?: string, storage: SessionStorage = new FileSessionStorage()): SessionManager {
|
|
2455
|
-
const dir = sessionDir ?? getDefaultSessionDir(cwd, storage);
|
|
2474
|
+
const dir = sessionDir ?? SessionManager.getDefaultSessionDir(cwd, undefined, storage);
|
|
2456
2475
|
const manager = new SessionManager(cwd, dir, true, storage);
|
|
2457
2476
|
manager.#initNewSession();
|
|
2458
2477
|
return manager;
|
|
@@ -2468,7 +2487,7 @@ export class SessionManager {
|
|
|
2468
2487
|
sessionDir?: string,
|
|
2469
2488
|
storage: SessionStorage = new FileSessionStorage(),
|
|
2470
2489
|
): Promise<SessionManager> {
|
|
2471
|
-
const dir = sessionDir ?? getDefaultSessionDir(cwd, storage);
|
|
2490
|
+
const dir = sessionDir ?? SessionManager.getDefaultSessionDir(cwd, undefined, storage);
|
|
2472
2491
|
const manager = new SessionManager(cwd, dir, true, storage);
|
|
2473
2492
|
const forkEntries = structuredClone(await loadEntriesFromFile(sourcePath, storage)) as FileEntry[];
|
|
2474
2493
|
migrateToCurrentVersion(forkEntries);
|
|
@@ -2516,7 +2535,7 @@ export class SessionManager {
|
|
|
2516
2535
|
sessionDir?: string,
|
|
2517
2536
|
storage: SessionStorage = new FileSessionStorage(),
|
|
2518
2537
|
): Promise<SessionManager> {
|
|
2519
|
-
const dir = sessionDir ?? getDefaultSessionDir(cwd, storage);
|
|
2538
|
+
const dir = sessionDir ?? SessionManager.getDefaultSessionDir(cwd, undefined, storage);
|
|
2520
2539
|
// Prefer terminal-scoped breadcrumb (handles concurrent sessions correctly)
|
|
2521
2540
|
const terminalSession = await readTerminalBreadcrumb(cwd);
|
|
2522
2541
|
const mostRecent = terminalSession ?? (await findMostRecentSession(dir, storage));
|
|
@@ -2549,7 +2568,7 @@ export class SessionManager {
|
|
|
2549
2568
|
sessionDir?: string,
|
|
2550
2569
|
storage: SessionStorage = new FileSessionStorage(),
|
|
2551
2570
|
): Promise<SessionInfo[]> {
|
|
2552
|
-
const dir = sessionDir ?? getDefaultSessionDir(cwd, storage);
|
|
2571
|
+
const dir = sessionDir ?? SessionManager.getDefaultSessionDir(cwd, undefined, storage);
|
|
2553
2572
|
try {
|
|
2554
2573
|
const files = storage.listFilesSync(dir, "*.jsonl");
|
|
2555
2574
|
return await collectSessionsFromFiles(files, storage);
|
package/src/tools/ast-edit.ts
CHANGED
|
@@ -17,6 +17,7 @@ import type { OutputMeta } from "./output-meta";
|
|
|
17
17
|
import {
|
|
18
18
|
combineSearchGlobs,
|
|
19
19
|
hasGlobPathChars,
|
|
20
|
+
normalizePathLikeInput,
|
|
20
21
|
parseSearchPath,
|
|
21
22
|
resolveMultiSearchPath,
|
|
22
23
|
resolveToCwd,
|
|
@@ -110,8 +111,8 @@ export class AstEditTool implements AgentTool<typeof astEditSchema, AstEditToolD
|
|
|
110
111
|
};
|
|
111
112
|
let searchPath: string | undefined;
|
|
112
113
|
let scopePath: string | undefined;
|
|
113
|
-
let globFilter = params.glob
|
|
114
|
-
const rawPath = params.path
|
|
114
|
+
let globFilter = params.glob ? normalizePathLikeInput(params.glob) || undefined : undefined;
|
|
115
|
+
const rawPath = params.path ? normalizePathLikeInput(params.path) || undefined : undefined;
|
|
115
116
|
if (rawPath) {
|
|
116
117
|
const internalRouter = this.session.internalRouter;
|
|
117
118
|
if (internalRouter?.canHandle(rawPath)) {
|
package/src/tools/ast-grep.ts
CHANGED
|
@@ -17,6 +17,7 @@ import type { OutputMeta } from "./output-meta";
|
|
|
17
17
|
import {
|
|
18
18
|
combineSearchGlobs,
|
|
19
19
|
hasGlobPathChars,
|
|
20
|
+
normalizePathLikeInput,
|
|
20
21
|
parseSearchPath,
|
|
21
22
|
resolveMultiSearchPath,
|
|
22
23
|
resolveToCwd,
|
|
@@ -98,8 +99,8 @@ export class AstGrepTool implements AgentTool<typeof astGrepSchema, AstGrepToolD
|
|
|
98
99
|
};
|
|
99
100
|
let searchPath: string | undefined;
|
|
100
101
|
let scopePath: string | undefined;
|
|
101
|
-
let globFilter = params.glob
|
|
102
|
-
const rawPath = params.path
|
|
102
|
+
let globFilter = params.glob ? normalizePathLikeInput(params.glob) || undefined : undefined;
|
|
103
|
+
const rawPath = params.path ? normalizePathLikeInput(params.path) || undefined : undefined;
|
|
103
104
|
if (rawPath) {
|
|
104
105
|
const internalRouter = this.session.internalRouter;
|
|
105
106
|
if (internalRouter?.canHandle(rawPath)) {
|
|
@@ -194,10 +195,13 @@ export class AstGrepTool implements AgentTool<typeof astGrepSchema, AstGrepToolD
|
|
|
194
195
|
};
|
|
195
196
|
|
|
196
197
|
if (result.matches.length === 0) {
|
|
198
|
+
const noMatchMessage = dedupedParseErrors.length
|
|
199
|
+
? "No matches found. Parse issues mean the query may be mis-scoped; narrow `path`/`glob` or set `lang` before concluding absence."
|
|
200
|
+
: "No matches found";
|
|
197
201
|
const parseMessage = dedupedParseErrors.length
|
|
198
202
|
? `\n${formatParseErrors(dedupedParseErrors).join("\n")}`
|
|
199
203
|
: "";
|
|
200
|
-
return toolResult(baseDetails).text(
|
|
204
|
+
return toolResult(baseDetails).text(`${noMatchMessage}${parseMessage}`).done();
|
|
201
205
|
}
|
|
202
206
|
|
|
203
207
|
const useHashLines = resolveFileDisplayMode(this.session).hashLines;
|
|
@@ -343,6 +347,12 @@ export const astGrepToolRenderer = {
|
|
|
343
347
|
const header = renderStatusLine({ icon: "warning", title: "AST Grep", description, meta }, uiTheme);
|
|
344
348
|
const lines = [header, formatEmptyMessage("No matches found", uiTheme)];
|
|
345
349
|
if (details?.parseErrors?.length) {
|
|
350
|
+
lines.push(
|
|
351
|
+
uiTheme.fg(
|
|
352
|
+
"warning",
|
|
353
|
+
"Query may be mis-scoped; narrow `path`/`glob` or set `lang` before concluding absence",
|
|
354
|
+
),
|
|
355
|
+
);
|
|
346
356
|
const capped = details.parseErrors.slice(0, PARSE_ERRORS_LIMIT);
|
|
347
357
|
for (const err of capped) {
|
|
348
358
|
lines.push(uiTheme.fg("warning", ` - ${err}`));
|
package/src/tools/find.ts
CHANGED
|
@@ -24,7 +24,7 @@ import {
|
|
|
24
24
|
import type { ToolSession } from ".";
|
|
25
25
|
import { applyListLimit } from "./list-limit";
|
|
26
26
|
import { formatFullOutputReference, type OutputMeta } from "./output-meta";
|
|
27
|
-
import { parseFindPattern, resolveMultiFindPattern, resolveToCwd } from "./path-utils";
|
|
27
|
+
import { normalizePathLikeInput, parseFindPattern, resolveMultiFindPattern, resolveToCwd } from "./path-utils";
|
|
28
28
|
import { formatCount, formatEmptyMessage, formatErrorMessage, PREVIEW_LIMITS } from "./render-utils";
|
|
29
29
|
import { ToolAbortError, ToolError, throwIfAborted } from "./tool-errors";
|
|
30
30
|
import { toolResult } from "./tool-result";
|
|
@@ -103,7 +103,7 @@ export class FindTool implements AgentTool<typeof findSchema, FindToolDetails> {
|
|
|
103
103
|
const relative = path.relative(this.session.cwd, targetPath).replace(/\\/g, "/");
|
|
104
104
|
return relative.length === 0 ? "." : relative;
|
|
105
105
|
};
|
|
106
|
-
const normalizedPattern = pattern
|
|
106
|
+
const normalizedPattern = normalizePathLikeInput(pattern).replace(/\\/g, "/");
|
|
107
107
|
if (!normalizedPattern) {
|
|
108
108
|
throw new ToolError("Pattern must not be empty");
|
|
109
109
|
}
|
package/src/tools/grep.ts
CHANGED
|
@@ -19,6 +19,7 @@ import { formatFullOutputReference, type OutputMeta } from "./output-meta";
|
|
|
19
19
|
import {
|
|
20
20
|
combineSearchGlobs,
|
|
21
21
|
hasGlobPathChars,
|
|
22
|
+
normalizePathLikeInput,
|
|
22
23
|
parseSearchPath,
|
|
23
24
|
resolveMultiSearchPath,
|
|
24
25
|
resolveToCwd,
|
|
@@ -119,10 +120,10 @@ export class GrepTool implements AgentTool<typeof grepSchema, GrepToolDetails> {
|
|
|
119
120
|
};
|
|
120
121
|
let searchPath: string;
|
|
121
122
|
let scopePath: string;
|
|
122
|
-
let globFilter = glob
|
|
123
|
+
let globFilter = glob ? normalizePathLikeInput(glob) || undefined : undefined;
|
|
123
124
|
const internalRouter = this.session.internalRouter;
|
|
124
125
|
if (searchDir?.trim()) {
|
|
125
|
-
const rawPath = searchDir
|
|
126
|
+
const rawPath = normalizePathLikeInput(searchDir);
|
|
126
127
|
if (internalRouter?.canHandle(rawPath)) {
|
|
127
128
|
if (hasGlobPathChars(rawPath)) {
|
|
128
129
|
throw new ToolError(`Glob patterns are not supported for internal URLs: ${rawPath}`);
|
package/src/tools/path-utils.ts
CHANGED
|
@@ -122,6 +122,10 @@ export function stripOuterDoubleQuotes(input: string): string {
|
|
|
122
122
|
return input.startsWith('"') && input.endsWith('"') && input.length > 1 ? input.slice(1, -1) : input;
|
|
123
123
|
}
|
|
124
124
|
|
|
125
|
+
export function normalizePathLikeInput(input: string): string {
|
|
126
|
+
return stripOuterDoubleQuotes(input.trim());
|
|
127
|
+
}
|
|
128
|
+
|
|
125
129
|
const GLOB_PATH_CHARS = ["*", "?", "[", "{"] as const;
|
|
126
130
|
|
|
127
131
|
export function hasGlobPathChars(filePath: string): boolean {
|