@oh-my-pi/pi-coding-agent 13.17.6 → 13.19.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/CHANGELOG.md +72 -0
- package/package.json +7 -11
- package/src/autoresearch/git.ts +25 -30
- package/src/autoresearch/tools/log-experiment.ts +61 -74
- package/src/cli/args.ts +0 -1
- package/src/commit/agentic/agent.ts +0 -3
- package/src/commit/agentic/index.ts +19 -22
- package/src/commit/agentic/tools/git-file-diff.ts +3 -6
- package/src/commit/agentic/tools/git-hunk.ts +3 -3
- package/src/commit/agentic/tools/git-overview.ts +6 -9
- package/src/commit/agentic/tools/index.ts +6 -8
- package/src/commit/agentic/tools/propose-commit.ts +4 -7
- package/src/commit/agentic/tools/recent-commits.ts +3 -3
- package/src/commit/agentic/tools/split-commit.ts +4 -4
- package/src/commit/changelog/index.ts +5 -9
- package/src/commit/pipeline.ts +10 -12
- package/src/config/keybindings.ts +7 -6
- package/src/config/settings-schema.ts +45 -1
- package/src/extensibility/custom-commands/bundled/ci-green/index.ts +4 -16
- package/src/extensibility/custom-commands/bundled/review/index.ts +43 -41
- package/src/extensibility/custom-tools/types.ts +1 -1
- package/src/extensibility/extensions/types.ts +3 -1
- package/src/extensibility/hooks/types.ts +1 -1
- package/src/extensibility/plugins/marketplace/fetcher.ts +2 -57
- package/src/extensibility/plugins/marketplace/source-resolver.ts +4 -4
- package/src/index.ts +1 -0
- package/src/internal-urls/types.ts +1 -1
- package/src/main.ts +24 -2
- package/src/modes/acp/acp-event-mapper.ts +0 -1
- package/src/modes/components/footer.ts +9 -29
- package/src/modes/components/hook-editor.ts +3 -3
- package/src/modes/components/hook-selector.ts +6 -1
- package/src/modes/components/session-observer-overlay.ts +472 -0
- package/src/modes/components/settings-defs.ts +19 -0
- package/src/modes/components/status-line.ts +15 -61
- package/src/modes/controllers/command-controller.ts +1 -0
- package/src/modes/controllers/event-controller.ts +59 -2
- package/src/modes/controllers/extension-ui-controller.ts +1 -0
- package/src/modes/controllers/input-controller.ts +3 -0
- package/src/modes/controllers/selector-controller.ts +26 -0
- package/src/modes/interactive-mode.ts +195 -43
- package/src/modes/session-observer-registry.ts +146 -0
- package/src/modes/shared.ts +0 -42
- package/src/modes/types.ts +2 -0
- package/src/modes/utils/keybinding-matchers.ts +9 -0
- package/src/prompts/agents/designer.md +1 -1
- package/src/prompts/agents/explore.md +1 -1
- package/src/prompts/agents/librarian.md +1 -1
- package/src/prompts/agents/oracle.md +1 -1
- package/src/prompts/agents/plan.md +1 -1
- package/src/prompts/agents/reviewer.md +1 -1
- package/src/prompts/system/custom-system-prompt.md +5 -0
- package/src/prompts/system/system-prompt.md +6 -0
- package/src/prompts/tools/read.md +27 -18
- package/src/sdk.ts +28 -13
- package/src/secrets/index.ts +1 -1
- package/src/secrets/obfuscator.ts +24 -16
- package/src/session/agent-session.ts +75 -30
- package/src/session/artifacts.ts +2 -2
- package/src/session/session-manager.ts +15 -5
- package/src/system-prompt.ts +4 -0
- package/src/task/executor.ts +28 -0
- package/src/task/index.ts +89 -79
- package/src/task/types.ts +25 -0
- package/src/task/worktree.ts +127 -145
- package/src/tools/exit-plan-mode.ts +1 -0
- package/src/tools/fetch.ts +173 -98
- package/src/tools/gh.ts +120 -297
- package/src/tools/index.ts +0 -4
- package/src/tools/path-utils.ts +12 -1
- package/src/tools/read.ts +74 -85
- package/src/tools/renderers.ts +0 -2
- package/src/utils/external-editor.ts +11 -5
- package/src/utils/git.ts +1400 -0
- package/src/web/search/render.ts +6 -4
- package/src/commit/git/errors.ts +0 -9
- package/src/commit/git/index.ts +0 -210
- package/src/commit/git/operations.ts +0 -54
- package/src/prompts/tools/fetch.md +0 -11
- package/src/tools/gh-cli.ts +0 -125
|
@@ -15,6 +15,7 @@ import { renderPromptTemplate } from "../../../../config/prompt-templates";
|
|
|
15
15
|
import type { CustomCommand, CustomCommandAPI } from "../../../../extensibility/custom-commands/types";
|
|
16
16
|
import type { HookCommandContext } from "../../../../extensibility/hooks/types";
|
|
17
17
|
import reviewRequestTemplate from "../../../../prompts/review-request.md" with { type: "text" };
|
|
18
|
+
import * as git from "../../../../utils/git";
|
|
18
19
|
|
|
19
20
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
20
21
|
// Types
|
|
@@ -258,20 +259,20 @@ export class ReviewCommand implements CustomCommand {
|
|
|
258
259
|
if (!baseBranch) return undefined;
|
|
259
260
|
|
|
260
261
|
const currentBranch = await getCurrentBranch(this.api);
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
ctx.ui.notify(`Failed to get diff: ${
|
|
262
|
+
let diffText: string;
|
|
263
|
+
try {
|
|
264
|
+
diffText = await git.diff(this.api.cwd, { base: `${baseBranch}...${currentBranch}` });
|
|
265
|
+
} catch (err) {
|
|
266
|
+
ctx.ui.notify(`Failed to get diff: ${err instanceof Error ? err.message : String(err)}`, "error");
|
|
266
267
|
return undefined;
|
|
267
268
|
}
|
|
268
269
|
|
|
269
|
-
if (!
|
|
270
|
+
if (!diffText.trim()) {
|
|
270
271
|
ctx.ui.notify(`No changes between ${baseBranch} and ${currentBranch}`, "warning");
|
|
271
272
|
return undefined;
|
|
272
273
|
}
|
|
273
274
|
|
|
274
|
-
const stats = parseDiff(
|
|
275
|
+
const stats = parseDiff(diffText);
|
|
275
276
|
if (stats.files.length === 0) {
|
|
276
277
|
ctx.ui.notify("No reviewable files (all changes filtered out)", "warning");
|
|
277
278
|
return undefined;
|
|
@@ -280,7 +281,7 @@ export class ReviewCommand implements CustomCommand {
|
|
|
280
281
|
return buildReviewPrompt(
|
|
281
282
|
`Reviewing changes between \`${baseBranch}\` and \`${currentBranch}\` (PR-style)`,
|
|
282
283
|
stats,
|
|
283
|
-
|
|
284
|
+
diffText,
|
|
284
285
|
);
|
|
285
286
|
}
|
|
286
287
|
|
|
@@ -292,12 +293,19 @@ export class ReviewCommand implements CustomCommand {
|
|
|
292
293
|
return undefined;
|
|
293
294
|
}
|
|
294
295
|
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
296
|
+
let unstagedDiff: string;
|
|
297
|
+
let stagedDiff: string;
|
|
298
|
+
try {
|
|
299
|
+
[unstagedDiff, stagedDiff] = await Promise.all([
|
|
300
|
+
git.diff(this.api.cwd),
|
|
301
|
+
git.diff(this.api.cwd, { cached: true }),
|
|
302
|
+
]);
|
|
303
|
+
} catch (err) {
|
|
304
|
+
ctx.ui.notify(`Failed to get diff: ${err instanceof Error ? err.message : String(err)}`, "error");
|
|
305
|
+
return undefined;
|
|
306
|
+
}
|
|
299
307
|
|
|
300
|
-
const combinedDiff = [
|
|
308
|
+
const combinedDiff = [unstagedDiff, stagedDiff].filter(Boolean).join("\n");
|
|
301
309
|
|
|
302
310
|
if (!combinedDiff.trim()) {
|
|
303
311
|
ctx.ui.notify("No diff content found", "warning");
|
|
@@ -327,25 +335,26 @@ export class ReviewCommand implements CustomCommand {
|
|
|
327
335
|
// Extract commit hash from selection (format: "abc1234 message")
|
|
328
336
|
const hash = selected.split(" ")[0];
|
|
329
337
|
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
338
|
+
let diffText: string;
|
|
339
|
+
try {
|
|
340
|
+
diffText = await git.show(this.api.cwd, hash, { format: "" });
|
|
341
|
+
} catch (err) {
|
|
342
|
+
ctx.ui.notify(`Failed to get commit: ${err instanceof Error ? err.message : String(err)}`, "error");
|
|
334
343
|
return undefined;
|
|
335
344
|
}
|
|
336
345
|
|
|
337
|
-
if (!
|
|
346
|
+
if (!diffText.trim()) {
|
|
338
347
|
ctx.ui.notify("Commit has no diff content", "warning");
|
|
339
348
|
return undefined;
|
|
340
349
|
}
|
|
341
350
|
|
|
342
|
-
const stats = parseDiff(
|
|
351
|
+
const stats = parseDiff(diffText);
|
|
343
352
|
if (stats.files.length === 0) {
|
|
344
353
|
ctx.ui.notify("No reviewable files in commit (all changes filtered out)", "warning");
|
|
345
354
|
return undefined;
|
|
346
355
|
}
|
|
347
356
|
|
|
348
|
-
return buildReviewPrompt(`Reviewing commit \`${hash}\``, stats,
|
|
357
|
+
return buildReviewPrompt(`Reviewing commit \`${hash}\``, stats, diffText);
|
|
349
358
|
}
|
|
350
359
|
|
|
351
360
|
case 4: {
|
|
@@ -354,16 +363,21 @@ export class ReviewCommand implements CustomCommand {
|
|
|
354
363
|
if (!instructions?.trim()) return undefined;
|
|
355
364
|
|
|
356
365
|
// For custom, we still try to get current diff for context
|
|
357
|
-
|
|
358
|
-
|
|
366
|
+
let diffText: string | undefined;
|
|
367
|
+
try {
|
|
368
|
+
diffText = await git.diff(this.api.cwd, { base: "HEAD" });
|
|
369
|
+
} catch {
|
|
370
|
+
diffText = undefined;
|
|
371
|
+
}
|
|
372
|
+
const reviewDiff = diffText?.trim();
|
|
359
373
|
|
|
360
|
-
if (
|
|
361
|
-
const stats = parseDiff(
|
|
374
|
+
if (reviewDiff) {
|
|
375
|
+
const stats = parseDiff(reviewDiff);
|
|
362
376
|
// Even if all files filtered, include the custom instructions
|
|
363
377
|
return `${buildReviewPrompt(
|
|
364
378
|
`Custom review: ${instructions.split("\n")[0].slice(0, 60)}…`,
|
|
365
379
|
stats,
|
|
366
|
-
|
|
380
|
+
reviewDiff,
|
|
367
381
|
)}\n\n### Additional Instructions\n\n${instructions}`;
|
|
368
382
|
}
|
|
369
383
|
|
|
@@ -388,12 +402,7 @@ Use the Task tool with \`agent: "reviewer"\` to execute this review.`;
|
|
|
388
402
|
|
|
389
403
|
async function getGitBranches(api: CustomCommandAPI): Promise<string[]> {
|
|
390
404
|
try {
|
|
391
|
-
|
|
392
|
-
if (result.code !== 0) return [];
|
|
393
|
-
return result.stdout
|
|
394
|
-
.split("\n")
|
|
395
|
-
.map(b => b.trim())
|
|
396
|
-
.filter(Boolean);
|
|
405
|
+
return await git.branch.list(api.cwd, { all: true });
|
|
397
406
|
} catch {
|
|
398
407
|
return [];
|
|
399
408
|
}
|
|
@@ -401,8 +410,7 @@ async function getGitBranches(api: CustomCommandAPI): Promise<string[]> {
|
|
|
401
410
|
|
|
402
411
|
async function getCurrentBranch(api: CustomCommandAPI): Promise<string> {
|
|
403
412
|
try {
|
|
404
|
-
|
|
405
|
-
return result.stdout.trim() || "HEAD";
|
|
413
|
+
return (await git.branch.current(api.cwd)) ?? "HEAD";
|
|
406
414
|
} catch {
|
|
407
415
|
return "HEAD";
|
|
408
416
|
}
|
|
@@ -410,8 +418,7 @@ async function getCurrentBranch(api: CustomCommandAPI): Promise<string> {
|
|
|
410
418
|
|
|
411
419
|
async function getGitStatus(api: CustomCommandAPI): Promise<string> {
|
|
412
420
|
try {
|
|
413
|
-
|
|
414
|
-
return result.stdout;
|
|
421
|
+
return await git.status(api.cwd);
|
|
415
422
|
} catch {
|
|
416
423
|
return "";
|
|
417
424
|
}
|
|
@@ -419,12 +426,7 @@ async function getGitStatus(api: CustomCommandAPI): Promise<string> {
|
|
|
419
426
|
|
|
420
427
|
async function getRecentCommits(api: CustomCommandAPI, count: number): Promise<string[]> {
|
|
421
428
|
try {
|
|
422
|
-
|
|
423
|
-
if (result.code !== 0) return [];
|
|
424
|
-
return result.stdout
|
|
425
|
-
.split("\n")
|
|
426
|
-
.map(c => c.trim())
|
|
427
|
-
.filter(Boolean);
|
|
429
|
+
return await git.log.onelines(api.cwd, count);
|
|
428
430
|
} catch {
|
|
429
431
|
return [];
|
|
430
432
|
}
|
|
@@ -81,6 +81,8 @@ export interface ExtensionUIDialogOptions {
|
|
|
81
81
|
onLeft?: () => void;
|
|
82
82
|
/** Invoked when user presses right arrow in select dialogs */
|
|
83
83
|
onRight?: () => void;
|
|
84
|
+
/** Invoked when user presses the external editor shortcut in select dialogs */
|
|
85
|
+
onExternalEditor?: () => void;
|
|
84
86
|
/** Optional footer hint text rendered by interactive selector */
|
|
85
87
|
helpText?: string;
|
|
86
88
|
}
|
|
@@ -566,7 +568,7 @@ export interface ToolExecutionEndEvent {
|
|
|
566
568
|
/** Fired when auto-compaction starts */
|
|
567
569
|
export interface AutoCompactionStartEvent {
|
|
568
570
|
type: "auto_compaction_start";
|
|
569
|
-
reason: "threshold" | "overflow";
|
|
571
|
+
reason: "threshold" | "overflow" | "idle";
|
|
570
572
|
action: "context-full" | "handoff";
|
|
571
573
|
}
|
|
572
574
|
|
|
@@ -394,7 +394,7 @@ export interface TurnEndEvent {
|
|
|
394
394
|
/** Event data for auto_compaction_start event. */
|
|
395
395
|
export interface AutoCompactionStartEvent {
|
|
396
396
|
type: "auto_compaction_start";
|
|
397
|
-
reason: "threshold" | "overflow";
|
|
397
|
+
reason: "threshold" | "overflow" | "idle";
|
|
398
398
|
action: "context-full" | "handoff";
|
|
399
399
|
}
|
|
400
400
|
|
|
@@ -8,7 +8,7 @@ import * as fs from "node:fs/promises";
|
|
|
8
8
|
import * as os from "node:os";
|
|
9
9
|
import * as path from "node:path";
|
|
10
10
|
import { isEnoent, logger } from "@oh-my-pi/pi-utils";
|
|
11
|
-
import
|
|
11
|
+
import * as git from "../../../utils/git";
|
|
12
12
|
|
|
13
13
|
import type { MarketplaceCatalog, MarketplaceSourceType } from "./types";
|
|
14
14
|
import { isValidNameSegment } from "./types";
|
|
@@ -274,21 +274,11 @@ export async function fetchMarketplace(source: string, cacheDir: string): Promis
|
|
|
274
274
|
* `promoteCloneToCache` after any duplicate/drift checks pass.
|
|
275
275
|
*/
|
|
276
276
|
async function cloneAndReadCatalog(url: string, cacheDir: string): Promise<FetchResult> {
|
|
277
|
-
if (!Bun.which("git")) {
|
|
278
|
-
throw new Error("git is not installed. Install git to use git-based marketplace sources.");
|
|
279
|
-
}
|
|
280
|
-
|
|
281
277
|
const tmpDir = path.join(cacheDir, `.tmp-clone-${Date.now()}`);
|
|
282
278
|
await fs.mkdir(cacheDir, { recursive: true });
|
|
283
279
|
|
|
284
280
|
logger.debug(`[marketplace] cloning ${url} → ${tmpDir}`);
|
|
285
|
-
|
|
286
|
-
const result = await $`git clone --depth 1 --single-branch ${url} ${tmpDir}`.quiet().nothrow();
|
|
287
|
-
if (result.exitCode !== 0) {
|
|
288
|
-
await fs.rm(tmpDir, { recursive: true, force: true });
|
|
289
|
-
const stderr = result.stderr.toString().trim();
|
|
290
|
-
throw new Error(`git clone failed (exit ${result.exitCode}): ${stderr || "unknown error"}`);
|
|
291
|
-
}
|
|
281
|
+
await git.clone(url, tmpDir);
|
|
292
282
|
|
|
293
283
|
const catalogPath = path.join(tmpDir, CATALOG_RELATIVE_PATH);
|
|
294
284
|
let content: string;
|
|
@@ -325,48 +315,3 @@ export async function promoteCloneToCache(tmpDir: string, cacheDir: string, name
|
|
|
325
315
|
await fs.rename(tmpDir, finalDir);
|
|
326
316
|
return finalDir;
|
|
327
317
|
}
|
|
328
|
-
|
|
329
|
-
/**
|
|
330
|
-
* Clone a git repository to a target directory. Shared by fetcher (marketplace clones)
|
|
331
|
-
* and source-resolver (plugin source clones).
|
|
332
|
-
*
|
|
333
|
-
* @param url - Git clone URL (HTTPS, SSH, or GitHub shorthand expanded to HTTPS)
|
|
334
|
-
* @param targetDir - Directory to clone into (must not exist)
|
|
335
|
-
* @param options.ref - Optional branch/tag to clone
|
|
336
|
-
* @param options.sha - Optional commit SHA to checkout after clone
|
|
337
|
-
*/
|
|
338
|
-
export async function cloneGitRepo(
|
|
339
|
-
url: string,
|
|
340
|
-
targetDir: string,
|
|
341
|
-
options?: { ref?: string; sha?: string },
|
|
342
|
-
): Promise<void> {
|
|
343
|
-
if (!Bun.which("git")) {
|
|
344
|
-
throw new Error("git is not installed. Install git to use git-based plugin sources.");
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
const cloneArgs = ["git", "clone", "--depth", "1"];
|
|
348
|
-
if (options?.ref) {
|
|
349
|
-
cloneArgs.push("--branch", options.ref, "--single-branch");
|
|
350
|
-
} else {
|
|
351
|
-
cloneArgs.push("--single-branch");
|
|
352
|
-
}
|
|
353
|
-
cloneArgs.push(url, targetDir);
|
|
354
|
-
|
|
355
|
-
logger.debug("[marketplace] cloning plugin source", { url, targetDir });
|
|
356
|
-
|
|
357
|
-
const result = await $`${cloneArgs}`.quiet().nothrow();
|
|
358
|
-
if (result.exitCode !== 0) {
|
|
359
|
-
await fs.rm(targetDir, { recursive: true, force: true });
|
|
360
|
-
const stderr = result.stderr.toString().trim();
|
|
361
|
-
throw new Error(`git clone failed (exit ${result.exitCode}): ${stderr || "unknown error"}`);
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
// If a specific SHA is requested, checkout that commit
|
|
365
|
-
if (options?.sha) {
|
|
366
|
-
const checkout = await $`git -C ${targetDir} checkout ${options.sha}`.quiet().nothrow();
|
|
367
|
-
if (checkout.exitCode !== 0) {
|
|
368
|
-
await fs.rm(targetDir, { recursive: true, force: true });
|
|
369
|
-
throw new Error(`Failed to checkout SHA ${options.sha} — shallow clone may not contain this commit`);
|
|
370
|
-
}
|
|
371
|
-
}
|
|
372
|
-
}
|
|
@@ -14,8 +14,8 @@ import * as fs from "node:fs/promises";
|
|
|
14
14
|
import * as path from "node:path";
|
|
15
15
|
|
|
16
16
|
import { isEnoent, pathIsWithin } from "@oh-my-pi/pi-utils";
|
|
17
|
+
import * as git from "../../../utils/git";
|
|
17
18
|
|
|
18
|
-
import { cloneGitRepo } from "./fetcher";
|
|
19
19
|
import type { MarketplaceCatalogMetadata, MarketplacePluginEntry, PluginSource } from "./types";
|
|
20
20
|
|
|
21
21
|
export interface ResolveContext {
|
|
@@ -87,7 +87,7 @@ async function resolveObjectSource(
|
|
|
87
87
|
// { source: "url", url: "https://github.com/owner/repo.git" }
|
|
88
88
|
// Despite the name, this is typically a git clone URL
|
|
89
89
|
const targetDir = path.join(context.tmpDir, `plugin-${crypto.randomUUID()}`);
|
|
90
|
-
await
|
|
90
|
+
await git.clone(source.url, targetDir, { ref: source.ref, sha: source.sha });
|
|
91
91
|
return { dir: targetDir, tempCloneRoot: targetDir };
|
|
92
92
|
}
|
|
93
93
|
|
|
@@ -95,7 +95,7 @@ async function resolveObjectSource(
|
|
|
95
95
|
// { source: "github", repo: "owner/repo" }
|
|
96
96
|
const url = `https://github.com/${source.repo}.git`;
|
|
97
97
|
const targetDir = path.join(context.tmpDir, `plugin-${crypto.randomUUID()}`);
|
|
98
|
-
await
|
|
98
|
+
await git.clone(url, targetDir, { ref: source.ref, sha: source.sha });
|
|
99
99
|
return { dir: targetDir, tempCloneRoot: targetDir };
|
|
100
100
|
}
|
|
101
101
|
|
|
@@ -106,7 +106,7 @@ async function resolveObjectSource(
|
|
|
106
106
|
? source.url
|
|
107
107
|
: `https://github.com/${source.url}.git`;
|
|
108
108
|
const cloneDir = path.join(context.tmpDir, `plugin-repo-${crypto.randomUUID()}`);
|
|
109
|
-
await
|
|
109
|
+
await git.clone(url, cloneDir, { ref: source.ref, sha: source.sha });
|
|
110
110
|
|
|
111
111
|
const subdirPath = path.resolve(cloneDir, source.path);
|
|
112
112
|
if (!pathIsWithin(cloneDir, subdirPath)) {
|
package/src/index.ts
CHANGED
|
@@ -51,6 +51,7 @@ export * from "./task/executor";
|
|
|
51
51
|
export type * from "./task/types";
|
|
52
52
|
// Tools (detail types and utilities)
|
|
53
53
|
export * from "./tools";
|
|
54
|
+
export * from "./utils/git";
|
|
54
55
|
// UI components for extensions
|
|
55
56
|
export {
|
|
56
57
|
HookEditorComponent as ExtensionEditorComponent,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Types for the internal URL routing system.
|
|
3
3
|
*
|
|
4
|
-
* Internal URLs (agent://, artifact://, memory://, skill://, rule://, mcp://, pi://, local://) are resolved by tools like
|
|
4
|
+
* Internal URLs (agent://, artifact://, memory://, skill://, rule://, mcp://, pi://, local://) are resolved by tools like read,
|
|
5
5
|
* providing access to agent outputs and server resources without exposing filesystem paths.
|
|
6
6
|
*/
|
|
7
7
|
|
package/src/main.ts
CHANGED
|
@@ -48,6 +48,7 @@ import type { AgentSession } from "./session/agent-session";
|
|
|
48
48
|
import { resolveResumableSession, type SessionInfo, SessionManager } from "./session/session-manager";
|
|
49
49
|
import { resolvePromptInput } from "./system-prompt";
|
|
50
50
|
import { getChangelogPath, getNewEntries, parseChangelog } from "./utils/changelog";
|
|
51
|
+
import type { EventBus } from "./utils/event-bus";
|
|
51
52
|
|
|
52
53
|
async function checkForNewVersion(currentVersion: string): Promise<string | undefined> {
|
|
53
54
|
if (!settings.get("startup.checkUpdate")) {
|
|
@@ -119,10 +120,19 @@ async function runInteractiveMode(
|
|
|
119
120
|
setExtensionUIContext: (uiContext: ExtensionUIContext, hasUI: boolean) => void,
|
|
120
121
|
lspServers: Array<{ name: string; status: "ready" | "error"; fileTypes: string[]; error?: string }> | undefined,
|
|
121
122
|
mcpManager: MCPManager | undefined,
|
|
123
|
+
eventBus?: EventBus,
|
|
122
124
|
initialMessage?: string,
|
|
123
125
|
initialImages?: ImageContent[],
|
|
124
126
|
): Promise<void> {
|
|
125
|
-
const mode = new InteractiveMode(
|
|
127
|
+
const mode = new InteractiveMode(
|
|
128
|
+
session,
|
|
129
|
+
version,
|
|
130
|
+
changelogMarkdown,
|
|
131
|
+
setExtensionUIContext,
|
|
132
|
+
lspServers,
|
|
133
|
+
mcpManager,
|
|
134
|
+
eventBus,
|
|
135
|
+
);
|
|
126
136
|
|
|
127
137
|
await mode.init();
|
|
128
138
|
|
|
@@ -273,6 +283,17 @@ async function createSessionManager(parsed: Args, cwd: string): Promise<SessionM
|
|
|
273
283
|
if (parsed.sessionDir) {
|
|
274
284
|
return SessionManager.create(cwd, parsed.sessionDir);
|
|
275
285
|
}
|
|
286
|
+
// Auto-resume: behave like --continue if the setting is enabled and a prior
|
|
287
|
+
// session exists. When a prior session is resumed, mark parsed.continue so
|
|
288
|
+
// buildSessionOptions restores the session's model/thinking instead of
|
|
289
|
+
// overriding them with CLI defaults.
|
|
290
|
+
if (settings.get("autoResume")) {
|
|
291
|
+
const manager = await SessionManager.continueRecent(cwd, parsed.sessionDir);
|
|
292
|
+
if (manager.getEntries().length > 0) {
|
|
293
|
+
parsed.continue = true;
|
|
294
|
+
}
|
|
295
|
+
return manager;
|
|
296
|
+
}
|
|
276
297
|
// Default case (new session) returns undefined, SDK will create one
|
|
277
298
|
return undefined;
|
|
278
299
|
}
|
|
@@ -718,7 +739,7 @@ export async function runRootCommand(parsed: Args, rawArgs: string[]): Promise<v
|
|
|
718
739
|
}
|
|
719
740
|
}
|
|
720
741
|
|
|
721
|
-
const { session, setToolUIContext, modelFallbackMessage, lspServers, mcpManager } = await logger.timeAsync(
|
|
742
|
+
const { session, setToolUIContext, modelFallbackMessage, lspServers, mcpManager, eventBus } = await logger.timeAsync(
|
|
722
743
|
"createAgentSession",
|
|
723
744
|
() => createAgentSession(sessionOptions),
|
|
724
745
|
);
|
|
@@ -806,6 +827,7 @@ export async function runRootCommand(parsed: Args, rawArgs: string[]): Promise<v
|
|
|
806
827
|
setToolUIContext,
|
|
807
828
|
lspServers,
|
|
808
829
|
mcpManager,
|
|
830
|
+
eventBus,
|
|
809
831
|
initialMessage,
|
|
810
832
|
initialImages,
|
|
811
833
|
);
|
|
@@ -5,7 +5,8 @@ import { formatNumber, getProjectDir } from "@oh-my-pi/pi-utils";
|
|
|
5
5
|
import { theme } from "../../modes/theme/theme";
|
|
6
6
|
import type { AgentSession } from "../../session/agent-session";
|
|
7
7
|
import { shortenPath } from "../../tools/render-utils";
|
|
8
|
-
import
|
|
8
|
+
import * as git from "../../utils/git";
|
|
9
|
+
import { sanitizeStatusText } from "../shared";
|
|
9
10
|
import { getContextUsageLevel, getContextUsageThemeColor } from "./status-line/context-thresholds";
|
|
10
11
|
|
|
11
12
|
/**
|
|
@@ -55,13 +56,13 @@ export class FooterComponent implements Component {
|
|
|
55
56
|
this.#gitWatcher = null;
|
|
56
57
|
}
|
|
57
58
|
|
|
58
|
-
|
|
59
|
-
if (!
|
|
59
|
+
git.head.resolve(getProjectDir()).then(head => {
|
|
60
|
+
if (!head) {
|
|
60
61
|
return;
|
|
61
62
|
}
|
|
62
63
|
|
|
63
64
|
try {
|
|
64
|
-
this.#gitWatcher = fs.watch(
|
|
65
|
+
this.#gitWatcher = fs.watch(head.headPath, () => {
|
|
65
66
|
this.#cachedBranch = undefined; // Invalidate cache
|
|
66
67
|
if (this.#onBranchChange) {
|
|
67
68
|
this.#onBranchChange();
|
|
@@ -93,35 +94,14 @@ export class FooterComponent implements Component {
|
|
|
93
94
|
* Returns null if not in a git repo, branch name otherwise.
|
|
94
95
|
*/
|
|
95
96
|
#getCurrentBranch(): string | null {
|
|
96
|
-
// Return cached value if available
|
|
97
97
|
if (this.#cachedBranch !== undefined) {
|
|
98
98
|
return this.#cachedBranch;
|
|
99
99
|
}
|
|
100
100
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
this.#cachedBranch = null;
|
|
106
|
-
if (this.#onBranchChange) {
|
|
107
|
-
this.#onBranchChange();
|
|
108
|
-
}
|
|
109
|
-
return;
|
|
110
|
-
}
|
|
111
|
-
const content = result.content.trim();
|
|
112
|
-
|
|
113
|
-
if (content.startsWith("ref: refs/heads/")) {
|
|
114
|
-
this.#cachedBranch = content.slice(16);
|
|
115
|
-
} else {
|
|
116
|
-
this.#cachedBranch = "detached";
|
|
117
|
-
}
|
|
118
|
-
if (this.#onBranchChange) {
|
|
119
|
-
this.#onBranchChange();
|
|
120
|
-
}
|
|
121
|
-
});
|
|
122
|
-
|
|
123
|
-
// Return undefined while loading (will show on next render once loaded)
|
|
124
|
-
return null;
|
|
101
|
+
const headState = git.head.resolveSync(getProjectDir());
|
|
102
|
+
this.#cachedBranch =
|
|
103
|
+
headState === null ? null : headState.kind === "ref" ? (headState.branchName ?? headState.ref) : "detached";
|
|
104
|
+
return this.#cachedBranch;
|
|
125
105
|
}
|
|
126
106
|
|
|
127
107
|
render(width: number): string[] {
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import { Container, Editor, matchesKey, Spacer, Text, type TUI } from "@oh-my-pi/pi-tui";
|
|
10
10
|
import { getEditorTheme, theme } from "../../modes/theme/theme";
|
|
11
|
-
import { matchesAppInterrupt } from "../../modes/utils/keybinding-matchers";
|
|
11
|
+
import { matchesAppExternalEditor, matchesAppInterrupt } from "../../modes/utils/keybinding-matchers";
|
|
12
12
|
import { getEditorCommand, openInEditor } from "../../utils/external-editor";
|
|
13
13
|
import { DynamicBorder } from "./dynamic-border";
|
|
14
14
|
|
|
@@ -87,7 +87,7 @@ export class HookEditorComponent extends Container {
|
|
|
87
87
|
}
|
|
88
88
|
|
|
89
89
|
// Ctrl+G for external editor
|
|
90
|
-
if (
|
|
90
|
+
if (matchesAppExternalEditor(keyData)) {
|
|
91
91
|
void this.#openExternalEditor();
|
|
92
92
|
return;
|
|
93
93
|
}
|
|
@@ -123,7 +123,7 @@ export class HookEditorComponent extends Container {
|
|
|
123
123
|
}
|
|
124
124
|
|
|
125
125
|
// Ctrl+G for external editor
|
|
126
|
-
if (
|
|
126
|
+
if (matchesAppExternalEditor(keyData)) {
|
|
127
127
|
void this.#openExternalEditor();
|
|
128
128
|
return;
|
|
129
129
|
}
|
|
@@ -16,7 +16,7 @@ import {
|
|
|
16
16
|
visibleWidth,
|
|
17
17
|
} from "@oh-my-pi/pi-tui";
|
|
18
18
|
import { getMarkdownTheme, theme } from "../../modes/theme/theme";
|
|
19
|
-
import { matchesSelectCancel } from "../../modes/utils/keybinding-matchers";
|
|
19
|
+
import { matchesAppExternalEditor, matchesSelectCancel } from "../../modes/utils/keybinding-matchers";
|
|
20
20
|
import { CountdownTimer } from "./countdown-timer";
|
|
21
21
|
import { DynamicBorder } from "./dynamic-border";
|
|
22
22
|
|
|
@@ -29,6 +29,7 @@ export interface HookSelectorOptions {
|
|
|
29
29
|
maxVisible?: number;
|
|
30
30
|
onLeft?: () => void;
|
|
31
31
|
onRight?: () => void;
|
|
32
|
+
onExternalEditor?: () => void;
|
|
32
33
|
helpText?: string;
|
|
33
34
|
}
|
|
34
35
|
|
|
@@ -67,6 +68,7 @@ export class HookSelectorComponent extends Container {
|
|
|
67
68
|
#countdown: CountdownTimer | undefined;
|
|
68
69
|
#onLeftCallback: (() => void) | undefined;
|
|
69
70
|
#onRightCallback: (() => void) | undefined;
|
|
71
|
+
#onExternalEditorCallback: (() => void) | undefined;
|
|
70
72
|
constructor(
|
|
71
73
|
title: string,
|
|
72
74
|
options: string[],
|
|
@@ -84,6 +86,7 @@ export class HookSelectorComponent extends Container {
|
|
|
84
86
|
this.#baseTitle = title;
|
|
85
87
|
this.#onLeftCallback = opts?.onLeft;
|
|
86
88
|
this.#onRightCallback = opts?.onRight;
|
|
89
|
+
this.#onExternalEditorCallback = opts?.onExternalEditor;
|
|
87
90
|
|
|
88
91
|
this.addChild(new DynamicBorder());
|
|
89
92
|
this.addChild(new Spacer(1));
|
|
@@ -174,6 +177,8 @@ export class HookSelectorComponent extends Container {
|
|
|
174
177
|
this.#onLeftCallback?.();
|
|
175
178
|
} else if (matchesKey(keyData, "right")) {
|
|
176
179
|
this.#onRightCallback?.();
|
|
180
|
+
} else if (this.#onExternalEditorCallback && matchesAppExternalEditor(keyData)) {
|
|
181
|
+
this.#onExternalEditorCallback();
|
|
177
182
|
} else if (matchesSelectCancel(keyData)) {
|
|
178
183
|
this.#onCancelCallback();
|
|
179
184
|
}
|