@oh-my-pi/pi-coding-agent 14.5.14 → 14.6.1
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 +49 -0
- package/package.json +7 -7
- package/src/autoresearch/command-resume.md +5 -8
- package/src/autoresearch/git.ts +41 -51
- package/src/autoresearch/helpers.ts +43 -359
- package/src/autoresearch/index.ts +281 -273
- package/src/autoresearch/prompt-setup.md +43 -0
- package/src/autoresearch/prompt.md +52 -193
- package/src/autoresearch/resume-message.md +2 -8
- package/src/autoresearch/state.ts +59 -166
- package/src/autoresearch/storage.ts +687 -0
- package/src/autoresearch/tools/init-experiment.ts +201 -290
- package/src/autoresearch/tools/log-experiment.ts +304 -517
- package/src/autoresearch/tools/run-experiment.ts +117 -296
- package/src/autoresearch/tools/update-notes.ts +116 -0
- package/src/autoresearch/types.ts +16 -66
- package/src/cli/list-models.ts +66 -0
- package/src/config/settings-schema.ts +1 -1
- package/src/config/settings.ts +20 -1
- package/src/cursor.ts +1 -1
- package/src/edit/index.ts +9 -31
- package/src/edit/line-hash.ts +70 -43
- package/src/edit/modes/hashline.lark +26 -0
- package/src/edit/modes/hashline.ts +898 -1099
- package/src/edit/modes/patch.ts +0 -7
- package/src/edit/modes/replace.ts +0 -4
- package/src/edit/renderer.ts +22 -20
- package/src/edit/streaming.ts +8 -28
- package/src/eval/eval.lark +24 -30
- package/src/eval/js/context-manager.ts +5 -162
- package/src/eval/js/prelude.txt +0 -12
- package/src/eval/parse.ts +129 -129
- package/src/eval/py/prelude.py +1 -219
- package/src/export/html/template.generated.ts +1 -1
- package/src/export/html/template.js +2 -2
- package/src/internal-urls/docs-index.generated.ts +2 -2
- package/src/main.ts +18 -3
- package/src/modes/components/session-observer-overlay.ts +5 -2
- package/src/modes/components/status-line/segments.ts +1 -1
- package/src/modes/components/status-line.ts +3 -5
- package/src/modes/components/tree-selector.ts +4 -5
- package/src/modes/components/welcome.ts +11 -1
- package/src/modes/controllers/command-controller.ts +2 -6
- package/src/modes/controllers/event-controller.ts +7 -5
- package/src/modes/controllers/extension-ui-controller.ts +3 -15
- package/src/modes/controllers/input-controller.ts +0 -1
- package/src/modes/controllers/selector-controller.ts +1 -1
- package/src/modes/interactive-mode.ts +5 -7
- package/src/prompts/system/system-prompt.md +14 -38
- package/src/prompts/tools/ast-edit.md +8 -8
- package/src/prompts/tools/ast-grep.md +10 -10
- package/src/prompts/tools/eval.md +13 -31
- package/src/prompts/tools/find.md +2 -1
- package/src/prompts/tools/hashline.md +66 -57
- package/src/prompts/tools/search.md +2 -2
- package/src/session/agent-session.ts +1 -1
- package/src/session/session-manager.ts +17 -13
- package/src/tools/ast-edit.ts +141 -44
- package/src/tools/ast-grep.ts +112 -36
- package/src/tools/eval.ts +2 -53
- package/src/tools/find.ts +16 -15
- package/src/tools/gh-renderer.ts +184 -59
- package/src/tools/path-utils.ts +36 -196
- package/src/tools/search.ts +56 -35
- package/src/utils/edit-mode.ts +2 -11
- package/src/utils/file-display-mode.ts +1 -1
- package/src/utils/git.ts +59 -24
- package/src/utils/session-color.ts +0 -12
- package/src/utils/title-generator.ts +22 -38
- package/src/autoresearch/apply-contract-to-state.ts +0 -24
- package/src/autoresearch/contract.ts +0 -288
- package/src/edit/modes/atom.lark +0 -29
- package/src/edit/modes/atom.ts +0 -1773
- package/src/prompts/tools/atom.md +0 -150
package/src/tools/search.ts
CHANGED
|
@@ -22,7 +22,7 @@ import {
|
|
|
22
22
|
hasGlobPathChars,
|
|
23
23
|
normalizePathLikeInput,
|
|
24
24
|
parseSearchPath,
|
|
25
|
-
|
|
25
|
+
resolveExplicitSearchPaths,
|
|
26
26
|
resolveToCwd,
|
|
27
27
|
} from "./path-utils";
|
|
28
28
|
import {
|
|
@@ -37,9 +37,10 @@ import { toolResult } from "./tool-result";
|
|
|
37
37
|
|
|
38
38
|
const searchSchema = Type.Object({
|
|
39
39
|
pattern: Type.String({ description: "regex pattern", examples: ["function\\s+\\w+", "TODO"] }),
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
40
|
+
paths: Type.Array(Type.String({ description: "file, directory, glob, or internal URL to search" }), {
|
|
41
|
+
minItems: 1,
|
|
42
|
+
description: "files, directories, globs, or internal URLs to search",
|
|
43
|
+
examples: [["src/"], ["src/foo.ts"], ["src/**/*.ts"], ["src/", "packages/"]],
|
|
43
44
|
}),
|
|
44
45
|
i: Type.Optional(Type.Boolean({ description: "case-insensitive search", default: false })),
|
|
45
46
|
gitignore: Type.Optional(Type.Boolean({ description: "respect gitignore", default: true })),
|
|
@@ -48,7 +49,7 @@ const searchSchema = Type.Object({
|
|
|
48
49
|
|
|
49
50
|
export type SearchToolInput = Static<typeof searchSchema>;
|
|
50
51
|
|
|
51
|
-
const DEFAULT_MATCH_LIMIT =
|
|
52
|
+
const DEFAULT_MATCH_LIMIT = 500;
|
|
52
53
|
|
|
53
54
|
export interface SearchToolDetails {
|
|
54
55
|
truncation?: TruncationResult;
|
|
@@ -93,7 +94,7 @@ export class SearchTool implements AgentTool<typeof searchSchema, SearchToolDeta
|
|
|
93
94
|
_onUpdate?: AgentToolUpdateCallback<SearchToolDetails>,
|
|
94
95
|
_toolContext?: AgentToolContext,
|
|
95
96
|
): Promise<AgentToolResult<SearchToolDetails>> {
|
|
96
|
-
const { pattern,
|
|
97
|
+
const { pattern, paths, i, gitignore, skip } = params;
|
|
97
98
|
|
|
98
99
|
return untilAborted(signal, async () => {
|
|
99
100
|
const normalizedPattern = pattern.trim();
|
|
@@ -117,13 +118,19 @@ export class SearchTool implements AgentTool<typeof searchSchema, SearchToolDeta
|
|
|
117
118
|
let searchPath: string;
|
|
118
119
|
let scopePath: string;
|
|
119
120
|
let exactFilePaths: string[] | undefined;
|
|
121
|
+
let multiTargets: Array<{ basePath: string; glob?: string }> | undefined;
|
|
120
122
|
let globFilter: string | undefined;
|
|
121
|
-
const
|
|
122
|
-
if (rawPath.length === 0) {
|
|
123
|
-
throw new ToolError("`
|
|
123
|
+
const rawPaths = paths.map(normalizePathLikeInput);
|
|
124
|
+
if (rawPaths.some(rawPath => rawPath.length === 0)) {
|
|
125
|
+
throw new ToolError("`paths` must contain non-empty paths or globs");
|
|
124
126
|
}
|
|
125
127
|
const internalRouter = this.session.internalRouter;
|
|
126
|
-
|
|
128
|
+
const resolvedPathInputs: string[] = [];
|
|
129
|
+
for (const rawPath of rawPaths) {
|
|
130
|
+
if (!internalRouter?.canHandle(rawPath)) {
|
|
131
|
+
resolvedPathInputs.push(rawPath);
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
127
134
|
if (hasGlobPathChars(rawPath)) {
|
|
128
135
|
throw new ToolError(`Glob patterns are not supported for internal URLs: ${rawPath}`);
|
|
129
136
|
}
|
|
@@ -131,28 +138,30 @@ export class SearchTool implements AgentTool<typeof searchSchema, SearchToolDeta
|
|
|
131
138
|
if (!resource.sourcePath) {
|
|
132
139
|
throw new ToolError(`Cannot search internal URL without a backing file: ${rawPath}`);
|
|
133
140
|
}
|
|
134
|
-
|
|
141
|
+
resolvedPathInputs.push(resource.sourcePath);
|
|
142
|
+
}
|
|
143
|
+
if (resolvedPathInputs.length === 1) {
|
|
144
|
+
const parsedPath = parseSearchPath(resolvedPathInputs[0] ?? ".");
|
|
145
|
+
searchPath = resolveToCwd(parsedPath.basePath, this.session.cwd);
|
|
146
|
+
globFilter = parsedPath.glob;
|
|
135
147
|
scopePath = formatScopePath(searchPath);
|
|
136
148
|
} else {
|
|
137
|
-
const multiSearchPath = await
|
|
138
|
-
if (multiSearchPath) {
|
|
139
|
-
|
|
140
|
-
globFilter = multiSearchPath.exactFilePaths ? undefined : multiSearchPath.glob;
|
|
141
|
-
exactFilePaths = multiSearchPath.exactFilePaths;
|
|
142
|
-
scopePath = multiSearchPath.scopePath;
|
|
143
|
-
} else {
|
|
144
|
-
const parsedPath = parseSearchPath(rawPath);
|
|
145
|
-
searchPath = resolveToCwd(parsedPath.basePath, this.session.cwd);
|
|
146
|
-
globFilter = parsedPath.glob;
|
|
147
|
-
scopePath = formatScopePath(searchPath);
|
|
149
|
+
const multiSearchPath = await resolveExplicitSearchPaths(resolvedPathInputs, this.session.cwd, globFilter);
|
|
150
|
+
if (!multiSearchPath) {
|
|
151
|
+
throw new ToolError("`paths` must contain at least one path or glob");
|
|
148
152
|
}
|
|
153
|
+
searchPath = multiSearchPath.basePath;
|
|
154
|
+
exactFilePaths = multiSearchPath.exactFilePaths;
|
|
155
|
+
multiTargets = multiSearchPath.targets;
|
|
156
|
+
globFilter = exactFilePaths || multiTargets ? undefined : multiSearchPath.glob;
|
|
157
|
+
scopePath = multiSearchPath.scopePath;
|
|
149
158
|
}
|
|
150
159
|
let isDirectory: boolean;
|
|
151
160
|
try {
|
|
152
161
|
const stat = await Bun.file(searchPath).stat();
|
|
153
162
|
isDirectory = stat.isDirectory();
|
|
154
163
|
} catch {
|
|
155
|
-
const hint =
|
|
164
|
+
const hint = rawPaths.length > 1 ? " (`paths` entries must each exist relative to cwd)" : "";
|
|
156
165
|
throw new ToolError(`Path not found: ${scopePath}${hint}`);
|
|
157
166
|
}
|
|
158
167
|
|
|
@@ -163,19 +172,26 @@ export class SearchTool implements AgentTool<typeof searchSchema, SearchToolDeta
|
|
|
163
172
|
// Run grep
|
|
164
173
|
let result: GrepResult;
|
|
165
174
|
try {
|
|
166
|
-
if (exactFilePaths) {
|
|
175
|
+
if (exactFilePaths || multiTargets) {
|
|
167
176
|
const matches: GrepMatch[] = [];
|
|
168
177
|
let limitReached = false;
|
|
169
|
-
|
|
170
|
-
|
|
178
|
+
let totalMatches = 0;
|
|
179
|
+
let filesSearched = 0;
|
|
180
|
+
const targets = exactFilePaths
|
|
181
|
+
? exactFilePaths.map(filePath => ({ basePath: filePath, glob: undefined as string | undefined }))
|
|
182
|
+
: (multiTargets ?? []);
|
|
183
|
+
for (const target of targets) {
|
|
184
|
+
const targetResult = await grep(
|
|
171
185
|
{
|
|
172
186
|
pattern: normalizedPattern,
|
|
173
|
-
path:
|
|
187
|
+
path: target.basePath,
|
|
188
|
+
glob: target.glob,
|
|
174
189
|
ignoreCase,
|
|
175
190
|
multiline: effectiveMultiline,
|
|
176
191
|
hidden: true,
|
|
177
192
|
gitignore: useGitignore,
|
|
178
193
|
cache: false,
|
|
194
|
+
maxCount: exactFilePaths ? undefined : internalLimit,
|
|
179
195
|
contextBefore: normalizedContextBefore,
|
|
180
196
|
contextAfter: normalizedContextAfter,
|
|
181
197
|
maxColumns: DEFAULT_MAX_COLUMN,
|
|
@@ -183,16 +199,21 @@ export class SearchTool implements AgentTool<typeof searchSchema, SearchToolDeta
|
|
|
183
199
|
},
|
|
184
200
|
undefined,
|
|
185
201
|
);
|
|
186
|
-
limitReached = limitReached || Boolean(
|
|
187
|
-
|
|
188
|
-
|
|
202
|
+
limitReached = limitReached || Boolean(targetResult.limitReached);
|
|
203
|
+
totalMatches += targetResult.totalMatches;
|
|
204
|
+
filesSearched += targetResult.filesSearched;
|
|
205
|
+
for (const match of targetResult.matches) {
|
|
206
|
+
const absolute = path.resolve(target.basePath, match.path);
|
|
207
|
+
const rebased = path.relative(searchPath, absolute).replace(/\\/g, "/");
|
|
208
|
+
matches.push({ ...match, path: rebased });
|
|
209
|
+
}
|
|
189
210
|
}
|
|
190
211
|
const offsetMatches = matches.slice(normalizedSkip);
|
|
191
212
|
result = {
|
|
192
213
|
matches: offsetMatches,
|
|
193
|
-
totalMatches: offsetMatches.length,
|
|
214
|
+
totalMatches: exactFilePaths ? offsetMatches.length : totalMatches,
|
|
194
215
|
filesWithMatches: new Set(offsetMatches.map(match => match.path)).size,
|
|
195
|
-
filesSearched: exactFilePaths.length,
|
|
216
|
+
filesSearched: exactFilePaths ? exactFilePaths.length : filesSearched,
|
|
196
217
|
limitReached,
|
|
197
218
|
};
|
|
198
219
|
} else {
|
|
@@ -261,7 +282,7 @@ export class SearchTool implements AgentTool<typeof searchSchema, SearchToolDeta
|
|
|
261
282
|
: result.matches.slice(0, effectiveLimit);
|
|
262
283
|
const matchLimitReached = result.matches.length > effectiveLimit;
|
|
263
284
|
const nextSkip = normalizedSkip + selectedMatches.length;
|
|
264
|
-
const limitMessage = `Result limit reached; narrow
|
|
285
|
+
const limitMessage = `Result limit reached; narrow paths or use skip=${nextSkip}.`;
|
|
265
286
|
const { record: recordFile, list: fileList } = createFileRecorder();
|
|
266
287
|
const fileMatchCounts = new Map<string, number>();
|
|
267
288
|
if (selectedMatches.length === 0) {
|
|
@@ -381,7 +402,7 @@ export class SearchTool implements AgentTool<typeof searchSchema, SearchToolDeta
|
|
|
381
402
|
|
|
382
403
|
interface SearchRenderArgs {
|
|
383
404
|
pattern: string;
|
|
384
|
-
|
|
405
|
+
paths?: string[];
|
|
385
406
|
i?: boolean;
|
|
386
407
|
gitignore?: boolean;
|
|
387
408
|
skip?: number;
|
|
@@ -393,7 +414,7 @@ export const searchToolRenderer = {
|
|
|
393
414
|
inline: true,
|
|
394
415
|
renderCall(args: SearchRenderArgs, _options: RenderResultOptions, uiTheme: Theme): Component {
|
|
395
416
|
const meta: string[] = [];
|
|
396
|
-
if (args.
|
|
417
|
+
if (args.paths?.length) meta.push(`in ${args.paths.join(", ")}`);
|
|
397
418
|
if (args.i) meta.push("case:insensitive");
|
|
398
419
|
if (args.gitignore === false) meta.push("gitignore:false");
|
|
399
420
|
if (args.skip !== undefined && args.skip > 0) meta.push(`skip:${args.skip}`);
|
package/src/utils/edit-mode.ts
CHANGED
|
@@ -1,12 +1,11 @@
|
|
|
1
|
-
import { $env
|
|
1
|
+
import { $env } from "@oh-my-pi/pi-utils";
|
|
2
2
|
|
|
3
|
-
export type EditMode = "replace" | "patch" | "hashline" | "vim" | "apply_patch"
|
|
3
|
+
export type EditMode = "replace" | "patch" | "hashline" | "vim" | "apply_patch";
|
|
4
4
|
|
|
5
5
|
export const DEFAULT_EDIT_MODE: EditMode = "hashline";
|
|
6
6
|
|
|
7
7
|
const EDIT_MODE_IDS = {
|
|
8
8
|
apply_patch: "apply_patch",
|
|
9
|
-
atom: "atom",
|
|
10
9
|
hashline: "hashline",
|
|
11
10
|
patch: "patch",
|
|
12
11
|
replace: "replace",
|
|
@@ -38,14 +37,6 @@ export function resolveEditMode(session: EditModeSessionLike): EditMode {
|
|
|
38
37
|
const envMode = normalizeEditMode($env.PI_EDIT_VARIANT);
|
|
39
38
|
if (envMode) return envMode;
|
|
40
39
|
|
|
41
|
-
if (!$flag("PI_STRICT_EDIT_MODE")) {
|
|
42
|
-
if (activeModel?.includes("spark")) return "apply_patch";
|
|
43
|
-
if (activeModel?.includes("nano")) return "replace";
|
|
44
|
-
if (activeModel?.includes("mini")) return "replace";
|
|
45
|
-
if (activeModel?.includes("haiku")) return "replace";
|
|
46
|
-
if (activeModel?.includes("flash")) return "replace";
|
|
47
|
-
}
|
|
48
|
-
|
|
49
40
|
const settingsMode = normalizeEditMode(String(session.settings.get("edit.mode") ?? ""));
|
|
50
41
|
return settingsMode ?? DEFAULT_EDIT_MODE;
|
|
51
42
|
}
|
|
@@ -29,7 +29,7 @@ export function resolveFileDisplayMode(session: FileDisplayModeSession, options?
|
|
|
29
29
|
const { settings } = session;
|
|
30
30
|
const hasEditTool = session.hasEditTool ?? true;
|
|
31
31
|
const editMode = resolveEditMode(session);
|
|
32
|
-
const usesHashLineAnchors = editMode === "hashline"
|
|
32
|
+
const usesHashLineAnchors = editMode === "hashline";
|
|
33
33
|
const raw = options?.raw === true;
|
|
34
34
|
const hashLines = !raw && hasEditTool && usesHashLineAnchors && settings.get("readHashLines") !== false;
|
|
35
35
|
return {
|
package/src/utils/git.ts
CHANGED
|
@@ -368,50 +368,68 @@ async function writeTempPatch(content: string): Promise<string> {
|
|
|
368
368
|
|
|
369
369
|
type EntryType = "directory" | "file";
|
|
370
370
|
|
|
371
|
-
function
|
|
372
|
-
|
|
371
|
+
function shouldRetry(err: unknown, n: number) {
|
|
372
|
+
if (isEnoent(err) || hasFsCode(err, "ENFILE") || hasFsCode(err, "EMFILE")) return false;
|
|
373
|
+
if (hasFsCode(err, "EINTR")) return n < EINTR_MAX_RETRIES;
|
|
374
|
+
if (n > EINTR_MAX_RETRIES) throw err;
|
|
375
|
+
throw err;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
/**
|
|
379
|
+
* Bounded retry for synchronous I/O against `EINTR`. POSIX permits short syscalls
|
|
380
|
+
* to be interrupted by signals; when that happens libc traditionally retries.
|
|
381
|
+
* Node's sync wrappers surface the raw `EINTR` so we replicate the retry locally.
|
|
382
|
+
* Any other error (and persistent EINTR after `EINTR_MAX_RETRIES`) is rethrown
|
|
383
|
+
* for the caller's normal "optional metadata" classifier to handle.
|
|
384
|
+
*/
|
|
385
|
+
const EINTR_MAX_RETRIES = 3;
|
|
386
|
+
function retryOnEintrSync<T>(op: () => T): T | null {
|
|
387
|
+
for (let attempt = 0; attempt <= EINTR_MAX_RETRIES; attempt += 1) {
|
|
388
|
+
try {
|
|
389
|
+
return op();
|
|
390
|
+
} catch (err) {
|
|
391
|
+
if (shouldRetry(err, attempt)) continue;
|
|
392
|
+
return null;
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
throw new Error("retryOnEintrSync: exhausted without resolution");
|
|
396
|
+
}
|
|
397
|
+
async function retryOnEintr<T>(op: () => Promise<T>): Promise<T | null> {
|
|
398
|
+
for (let attempt = 0; attempt <= EINTR_MAX_RETRIES; attempt += 1) {
|
|
399
|
+
try {
|
|
400
|
+
return await op();
|
|
401
|
+
} catch (err) {
|
|
402
|
+
if (shouldRetry(err, attempt)) continue;
|
|
403
|
+
return null;
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
throw new Error("retryOnEintr: exhausted without resolution");
|
|
373
407
|
}
|
|
374
408
|
|
|
375
409
|
function getEntryTypeSync(gitEntryPath: string): EntryType | null {
|
|
376
|
-
|
|
410
|
+
return retryOnEintrSync(() => {
|
|
377
411
|
const stat = fs.statSync(gitEntryPath);
|
|
378
412
|
if (stat.isDirectory()) return "directory";
|
|
379
413
|
if (stat.isFile()) return "file";
|
|
380
414
|
return null;
|
|
381
|
-
}
|
|
382
|
-
if (isOptionalGitMetadataUnavailable(err)) return null;
|
|
383
|
-
throw err;
|
|
384
|
-
}
|
|
415
|
+
});
|
|
385
416
|
}
|
|
386
417
|
|
|
387
418
|
async function getEntryType(gitEntryPath: string): Promise<EntryType | null> {
|
|
388
|
-
|
|
419
|
+
return retryOnEintr(async () => {
|
|
389
420
|
const stat = await fs.promises.stat(gitEntryPath);
|
|
390
421
|
if (stat.isDirectory()) return "directory";
|
|
391
422
|
if (stat.isFile()) return "file";
|
|
392
423
|
return null;
|
|
393
|
-
}
|
|
394
|
-
if (isOptionalGitMetadataUnavailable(err)) return null;
|
|
395
|
-
throw err;
|
|
396
|
-
}
|
|
424
|
+
});
|
|
397
425
|
}
|
|
398
426
|
|
|
399
427
|
function readOptionalTextSync(filePath: string): string | null {
|
|
400
|
-
|
|
401
|
-
return fs.readFileSync(filePath, "utf8");
|
|
402
|
-
} catch (err) {
|
|
403
|
-
if (isOptionalGitMetadataUnavailable(err)) return null;
|
|
404
|
-
throw err;
|
|
405
|
-
}
|
|
428
|
+
return retryOnEintrSync(() => fs.readFileSync(filePath, "utf8"));
|
|
406
429
|
}
|
|
407
430
|
|
|
408
431
|
async function readOptionalText(filePath: string): Promise<string | null> {
|
|
409
|
-
|
|
410
|
-
return await Bun.file(filePath).text();
|
|
411
|
-
} catch (err) {
|
|
412
|
-
if (isOptionalGitMetadataUnavailable(err)) return null;
|
|
413
|
-
throw err;
|
|
414
|
-
}
|
|
432
|
+
return retryOnEintr(async () => await Bun.file(filePath).text());
|
|
415
433
|
}
|
|
416
434
|
|
|
417
435
|
function parseGitDirPointer(content: string): string | null {
|
|
@@ -1258,6 +1276,23 @@ export async function restore(cwd: string, options: RestoreOptions = {}): Promis
|
|
|
1258
1276
|
await runEffect(cwd, args, { signal: options.signal });
|
|
1259
1277
|
}
|
|
1260
1278
|
|
|
1279
|
+
/**
|
|
1280
|
+
* Run `git reset` with options. Default is a soft reset (no flag); pass `hard: true` for a destructive reset.
|
|
1281
|
+
*
|
|
1282
|
+
* NOTE: stage.reset() handles the per-file unstaging case. This helper exists for tree-wide resets.
|
|
1283
|
+
*/
|
|
1284
|
+
export async function reset(
|
|
1285
|
+
cwd: string,
|
|
1286
|
+
options: { hard?: boolean; mixed?: boolean; soft?: boolean; target?: string; signal?: AbortSignal } = {},
|
|
1287
|
+
): Promise<void> {
|
|
1288
|
+
const args = ["reset"];
|
|
1289
|
+
if (options.hard) args.push("--hard");
|
|
1290
|
+
else if (options.mixed) args.push("--mixed");
|
|
1291
|
+
else if (options.soft) args.push("--soft");
|
|
1292
|
+
if (options.target) args.push(options.target);
|
|
1293
|
+
await runEffect(cwd, args, { signal: options.signal });
|
|
1294
|
+
}
|
|
1295
|
+
|
|
1261
1296
|
export async function clean(
|
|
1262
1297
|
cwd: string,
|
|
1263
1298
|
options: { ignoredOnly?: boolean; paths?: readonly string[]; signal?: AbortSignal } = {},
|
|
@@ -33,18 +33,6 @@ export function getSessionAccentHex(name: string): string {
|
|
|
33
33
|
return hslToHex(nameToHue(name), 0.9, 0.72);
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
-
/**
|
|
37
|
-
* Auto-generated titles should not drive the session accent.
|
|
38
|
-
* Legacy sessions with unknown title source keep the old behavior.
|
|
39
|
-
*/
|
|
40
|
-
export function getSessionAccentHexForTitle(
|
|
41
|
-
name: string | undefined,
|
|
42
|
-
titleSource: "auto" | "user" | undefined,
|
|
43
|
-
): string | undefined {
|
|
44
|
-
if (!name || titleSource === "auto") return undefined;
|
|
45
|
-
return getSessionAccentHex(name);
|
|
46
|
-
}
|
|
47
|
-
|
|
48
36
|
/**
|
|
49
37
|
* Convert a hex accent color to an ANSI-16m foreground escape sequence.
|
|
50
38
|
* Returns `undefined` if `hex` is nullish or Bun.color conversion fails.
|
|
@@ -2,15 +2,13 @@
|
|
|
2
2
|
* Generate session titles using a smol, fast model.
|
|
3
3
|
*/
|
|
4
4
|
import * as path from "node:path";
|
|
5
|
-
|
|
6
|
-
import type
|
|
7
|
-
import { completeSimple } from "@oh-my-pi/pi-ai";
|
|
5
|
+
|
|
6
|
+
import { type Api, completeSimple, type Model } from "@oh-my-pi/pi-ai";
|
|
8
7
|
import { logger, prompt } from "@oh-my-pi/pi-utils";
|
|
9
8
|
import type { ModelRegistry } from "../config/model-registry";
|
|
10
9
|
import { resolveRoleSelection } from "../config/model-resolver";
|
|
11
10
|
import type { Settings } from "../config/settings";
|
|
12
11
|
import titleSystemPrompt from "../prompts/system/title-system.md" with { type: "text" };
|
|
13
|
-
import { toReasoningEffort } from "../thinking";
|
|
14
12
|
|
|
15
13
|
const TITLE_SYSTEM_PROMPT = prompt.render(titleSystemPrompt);
|
|
16
14
|
|
|
@@ -19,22 +17,14 @@ const TERMINAL_TITLE_CONTROL_CHARS = /[\u0000-\u001f\u007f-\u009f]/g;
|
|
|
19
17
|
|
|
20
18
|
const MAX_INPUT_CHARS = 2000;
|
|
21
19
|
|
|
22
|
-
function getTitleModel(
|
|
23
|
-
registry: ModelRegistry,
|
|
24
|
-
settings: Settings,
|
|
25
|
-
currentModel?: Model<Api>,
|
|
26
|
-
): { model: Model<Api>; thinkingLevel?: ThinkingLevel } | undefined {
|
|
20
|
+
function getTitleModel(registry: ModelRegistry, settings: Settings, currentModel?: Model<Api>): Model<Api> | undefined {
|
|
27
21
|
const availableModels = registry.getAvailable();
|
|
28
22
|
if (availableModels.length === 0) return undefined;
|
|
29
23
|
|
|
30
|
-
const titleModel = resolveRoleSelection(["commit", "smol"], settings, availableModels, registry);
|
|
31
|
-
if (titleModel)
|
|
32
|
-
return { model: titleModel.model, thinkingLevel: titleModel.thinkingLevel };
|
|
33
|
-
}
|
|
24
|
+
const titleModel = resolveRoleSelection(["commit", "smol"], settings, availableModels, registry)?.model;
|
|
25
|
+
if (titleModel) return titleModel;
|
|
34
26
|
|
|
35
|
-
if (currentModel)
|
|
36
|
-
return { model: currentModel };
|
|
37
|
-
}
|
|
27
|
+
if (currentModel) return currentModel;
|
|
38
28
|
|
|
39
29
|
return undefined;
|
|
40
30
|
}
|
|
@@ -44,7 +34,7 @@ function getTitleModel(
|
|
|
44
34
|
*
|
|
45
35
|
* @param firstMessage The first user message
|
|
46
36
|
* @param registry Model registry
|
|
47
|
-
* @param settings Settings used to resolve the smol role
|
|
37
|
+
* @param settings Settings used to resolve the smol role
|
|
48
38
|
* @param sessionId Optional session id for sticky API key selection
|
|
49
39
|
*/
|
|
50
40
|
export async function generateSessionTitle(
|
|
@@ -54,8 +44,8 @@ export async function generateSessionTitle(
|
|
|
54
44
|
sessionId?: string,
|
|
55
45
|
currentModel?: Model<Api>,
|
|
56
46
|
): Promise<string | null> {
|
|
57
|
-
const
|
|
58
|
-
if (!
|
|
47
|
+
const model = getTitleModel(registry, settings, currentModel);
|
|
48
|
+
if (!model) {
|
|
59
49
|
logger.debug("title-generator: no title model found");
|
|
60
50
|
return null;
|
|
61
51
|
}
|
|
@@ -67,17 +57,20 @@ export async function generateSessionTitle(
|
|
|
67
57
|
${truncatedMessage}
|
|
68
58
|
</user-message>`;
|
|
69
59
|
|
|
70
|
-
const apiKey = await registry.getApiKey(
|
|
60
|
+
const apiKey = await registry.getApiKey(model, sessionId);
|
|
71
61
|
if (!apiKey) {
|
|
72
62
|
logger.debug("title-generator: no API key for smol model", {
|
|
73
|
-
provider:
|
|
74
|
-
id:
|
|
63
|
+
provider: model.provider,
|
|
64
|
+
id: model.id,
|
|
75
65
|
});
|
|
76
66
|
return null;
|
|
77
67
|
}
|
|
78
68
|
|
|
69
|
+
// Title generation is a 3-6 word task; force reasoning off so reasoning models
|
|
70
|
+
// don't burn the entire output budget on internal thinking and return an empty
|
|
71
|
+
// string. With reasoning disabled, 30 tokens of output is plenty.
|
|
79
72
|
const request = {
|
|
80
|
-
model: `${
|
|
73
|
+
model: `${model.provider}/${model.id}`,
|
|
81
74
|
systemPrompt: TITLE_SYSTEM_PROMPT,
|
|
82
75
|
userMessage,
|
|
83
76
|
maxTokens: 30,
|
|
@@ -86,7 +79,7 @@ ${truncatedMessage}
|
|
|
86
79
|
|
|
87
80
|
try {
|
|
88
81
|
const response = await completeSimple(
|
|
89
|
-
|
|
82
|
+
model,
|
|
90
83
|
{
|
|
91
84
|
systemPrompt: request.systemPrompt,
|
|
92
85
|
messages: [{ role: "user", content: request.userMessage, timestamp: Date.now() }],
|
|
@@ -94,7 +87,7 @@ ${truncatedMessage}
|
|
|
94
87
|
{
|
|
95
88
|
apiKey,
|
|
96
89
|
maxTokens: 30,
|
|
97
|
-
|
|
90
|
+
disableReasoning: true,
|
|
98
91
|
},
|
|
99
92
|
);
|
|
100
93
|
|
|
@@ -153,13 +146,8 @@ function getFallbackTerminalTitle(cwd: string | undefined): string | undefined {
|
|
|
153
146
|
return sanitizeTerminalTitlePart(baseName);
|
|
154
147
|
}
|
|
155
148
|
|
|
156
|
-
export function formatSessionTerminalTitle(
|
|
157
|
-
sessionName
|
|
158
|
-
cwd?: string,
|
|
159
|
-
titleSource?: "auto" | "user" | undefined,
|
|
160
|
-
): string {
|
|
161
|
-
const label =
|
|
162
|
-
sanitizeTerminalTitlePart(titleSource === "auto" ? undefined : sessionName) ?? getFallbackTerminalTitle(cwd);
|
|
149
|
+
export function formatSessionTerminalTitle(sessionName: string | undefined, cwd?: string): string {
|
|
150
|
+
const label = sanitizeTerminalTitlePart(sessionName) ?? getFallbackTerminalTitle(cwd);
|
|
163
151
|
return label ? `${DEFAULT_TERMINAL_TITLE}: ${label}` : DEFAULT_TERMINAL_TITLE;
|
|
164
152
|
}
|
|
165
153
|
|
|
@@ -170,12 +158,8 @@ export function setTerminalTitle(title: string): void {
|
|
|
170
158
|
process.stdout.write(`\x1b]0;${sanitizeTerminalTitlePart(title) ?? DEFAULT_TERMINAL_TITLE}\x07`);
|
|
171
159
|
}
|
|
172
160
|
|
|
173
|
-
export function setSessionTerminalTitle(
|
|
174
|
-
sessionName
|
|
175
|
-
cwd?: string,
|
|
176
|
-
titleSource?: "auto" | "user" | undefined,
|
|
177
|
-
): void {
|
|
178
|
-
setTerminalTitle(formatSessionTerminalTitle(sessionName, cwd, titleSource));
|
|
161
|
+
export function setSessionTerminalTitle(sessionName: string | undefined, cwd?: string): void {
|
|
162
|
+
setTerminalTitle(formatSessionTerminalTitle(sessionName, cwd));
|
|
179
163
|
}
|
|
180
164
|
|
|
181
165
|
/**
|
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
import { inferMetricUnitFromName } from "./helpers";
|
|
2
|
-
import type { AutoresearchContract, ExperimentState } from "./types";
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Updates session fields from a validated `autoresearch.md` parse (same fields as `init_experiment`).
|
|
6
|
-
* Does not touch `name`, `currentSegment`, `results`, `bestMetric`, `confidence`, or `maxExperiments`.
|
|
7
|
-
*/
|
|
8
|
-
export function applyAutoresearchContractToExperimentState(
|
|
9
|
-
contract: AutoresearchContract,
|
|
10
|
-
state: ExperimentState,
|
|
11
|
-
): void {
|
|
12
|
-
const benchmarkContract = contract.benchmark;
|
|
13
|
-
state.metricName = benchmarkContract.primaryMetric ?? state.metricName;
|
|
14
|
-
state.metricUnit = benchmarkContract.metricUnit;
|
|
15
|
-
state.bestDirection = benchmarkContract.direction ?? "lower";
|
|
16
|
-
state.secondaryMetrics = benchmarkContract.secondaryMetrics.map(name => ({
|
|
17
|
-
name,
|
|
18
|
-
unit: inferMetricUnitFromName(name),
|
|
19
|
-
}));
|
|
20
|
-
state.benchmarkCommand = benchmarkContract.command?.trim() ?? state.benchmarkCommand;
|
|
21
|
-
state.scopePaths = [...contract.scopePaths];
|
|
22
|
-
state.offLimits = [...contract.offLimits];
|
|
23
|
-
state.constraints = [...contract.constraints];
|
|
24
|
-
}
|