@oh-my-pi/pi-coding-agent 13.18.0 → 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 +50 -0
- package/package.json +7 -11
- package/src/autoresearch/git.ts +25 -30
- package/src/autoresearch/tools/log-experiment.ts +61 -74
- 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 +44 -0
- 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/main.ts +24 -2
- 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/system/custom-system-prompt.md +5 -0
- package/src/prompts/system/system-prompt.md +6 -0
- 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/session-manager.ts +15 -5
- package/src/system-prompt.ts +4 -0
- package/src/task/executor.ts +28 -0
- package/src/task/index.ts +88 -78
- 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/gh.ts +120 -297
- package/src/tools/read.ts +13 -79
- 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/tools/gh-cli.ts +0 -125
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Type } from "@sinclair/typebox";
|
|
2
|
-
import type { ControlledGit } from "../../../commit/git";
|
|
3
2
|
import type { DiffHunk, FileHunks } from "../../../commit/types";
|
|
4
3
|
import type { CustomTool } from "../../../extensibility/custom-tools/types";
|
|
4
|
+
import * as git from "../../../utils/git";
|
|
5
5
|
|
|
6
6
|
const gitHunkSchema = Type.Object({
|
|
7
7
|
file: Type.String({ description: "File path" }),
|
|
@@ -15,7 +15,7 @@ function selectHunks(fileHunks: FileHunks, requested?: number[]): DiffHunk[] {
|
|
|
15
15
|
return fileHunks.hunks.filter(hunk => wanted.has(hunk.index + 1));
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
-
export function createGitHunkTool(
|
|
18
|
+
export function createGitHunkTool(cwd: string): CustomTool<typeof gitHunkSchema> {
|
|
19
19
|
return {
|
|
20
20
|
name: "git_hunk",
|
|
21
21
|
label: "Git Hunk",
|
|
@@ -23,7 +23,7 @@ export function createGitHunkTool(git: ControlledGit): CustomTool<typeof gitHunk
|
|
|
23
23
|
parameters: gitHunkSchema,
|
|
24
24
|
async execute(_toolCallId, params) {
|
|
25
25
|
const staged = params.staged ?? true;
|
|
26
|
-
const hunks = await git.
|
|
26
|
+
const hunks = await git.diff.hunks(cwd, [params.file], { cached: staged });
|
|
27
27
|
const fileHunks = hunks.find(entry => entry.filename === params.file) ?? {
|
|
28
28
|
filename: params.file,
|
|
29
29
|
isBinary: false,
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { Type } from "@sinclair/typebox";
|
|
2
2
|
import type { CommitAgentState, GitOverviewSnapshot } from "../../../commit/agentic/state";
|
|
3
3
|
import { extractScopeCandidates } from "../../../commit/analysis/scope";
|
|
4
|
-
import type { ControlledGit } from "../../../commit/git";
|
|
5
4
|
import type { CustomTool } from "../../../extensibility/custom-tools/types";
|
|
5
|
+
import * as git from "../../../utils/git";
|
|
6
6
|
|
|
7
7
|
const EXCLUDED_LOCK_FILES = new Set([
|
|
8
8
|
"Cargo.lock",
|
|
@@ -47,10 +47,7 @@ const gitOverviewSchema = Type.Object({
|
|
|
47
47
|
include_untracked: Type.Optional(Type.Boolean({ description: "Include untracked files when staged=false" })),
|
|
48
48
|
});
|
|
49
49
|
|
|
50
|
-
export function createGitOverviewTool(
|
|
51
|
-
git: ControlledGit,
|
|
52
|
-
state: CommitAgentState,
|
|
53
|
-
): CustomTool<typeof gitOverviewSchema> {
|
|
50
|
+
export function createGitOverviewTool(cwd: string, state: CommitAgentState): CustomTool<typeof gitOverviewSchema> {
|
|
54
51
|
return {
|
|
55
52
|
name: "git_overview",
|
|
56
53
|
label: "Git Overview",
|
|
@@ -58,13 +55,13 @@ export function createGitOverviewTool(
|
|
|
58
55
|
parameters: gitOverviewSchema,
|
|
59
56
|
async execute(_toolCallId, params) {
|
|
60
57
|
const staged = params.staged ?? true;
|
|
61
|
-
const allFiles =
|
|
58
|
+
const allFiles = await git.diff.changedFiles(cwd, { cached: staged });
|
|
62
59
|
const { filtered: files, excluded } = filterExcludedFiles(allFiles);
|
|
63
|
-
const stat = await git.
|
|
64
|
-
const allNumstat = await git.
|
|
60
|
+
const stat = await git.diff(cwd, { stat: true, cached: staged });
|
|
61
|
+
const allNumstat = await git.diff.numstat(cwd, { cached: staged });
|
|
65
62
|
const numstat = allNumstat.filter(entry => !isExcludedFile(entry.path));
|
|
66
63
|
const scopeResult = extractScopeCandidates(numstat);
|
|
67
|
-
const untrackedFiles = !staged && params.include_untracked ? await git.
|
|
64
|
+
const untrackedFiles = !staged && params.include_untracked ? await git.ls.untracked(cwd) : undefined;
|
|
68
65
|
const snapshot: GitOverviewSnapshot = {
|
|
69
66
|
files,
|
|
70
67
|
stat,
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import type { CommitAgentState } from "../../../commit/agentic/state";
|
|
2
|
-
import type { ControlledGit } from "../../../commit/git";
|
|
3
2
|
import type { ModelRegistry } from "../../../config/model-registry";
|
|
4
3
|
import type { Settings } from "../../../config/settings";
|
|
5
4
|
import type { CustomTool } from "../../../extensibility/custom-tools/types";
|
|
@@ -15,7 +14,6 @@ import { createSplitCommitTool } from "./split-commit";
|
|
|
15
14
|
|
|
16
15
|
export interface CommitToolOptions {
|
|
17
16
|
cwd: string;
|
|
18
|
-
git: ControlledGit;
|
|
19
17
|
authStorage: AuthStorage;
|
|
20
18
|
modelRegistry: ModelRegistry;
|
|
21
19
|
settings: Settings;
|
|
@@ -27,10 +25,10 @@ export interface CommitToolOptions {
|
|
|
27
25
|
|
|
28
26
|
export function createCommitTools(options: CommitToolOptions): Array<CustomTool<any, any>> {
|
|
29
27
|
const tools: Array<CustomTool<any, any>> = [
|
|
30
|
-
createGitOverviewTool(options.
|
|
31
|
-
createGitFileDiffTool(options.
|
|
32
|
-
createGitHunkTool(options.
|
|
33
|
-
createRecentCommitsTool(options.
|
|
28
|
+
createGitOverviewTool(options.cwd, options.state),
|
|
29
|
+
createGitFileDiffTool(options.cwd, options.state),
|
|
30
|
+
createGitHunkTool(options.cwd),
|
|
31
|
+
createRecentCommitsTool(options.cwd),
|
|
34
32
|
];
|
|
35
33
|
|
|
36
34
|
if (options.enableAnalyzeFiles ?? true) {
|
|
@@ -48,8 +46,8 @@ export function createCommitTools(options: CommitToolOptions): Array<CustomTool<
|
|
|
48
46
|
|
|
49
47
|
tools.push(
|
|
50
48
|
createProposeChangelogTool(options.state, options.changelogTargets),
|
|
51
|
-
createProposeCommitTool(options.
|
|
52
|
-
createSplitCommitTool(options.
|
|
49
|
+
createProposeCommitTool(options.cwd, options.state),
|
|
50
|
+
createSplitCommitTool(options.cwd, options.state, options.changelogTargets),
|
|
53
51
|
);
|
|
54
52
|
|
|
55
53
|
return tools;
|
|
@@ -9,9 +9,9 @@ import {
|
|
|
9
9
|
validateTypeConsistency,
|
|
10
10
|
} from "../../../commit/agentic/validation";
|
|
11
11
|
import { validateAnalysis } from "../../../commit/analysis/validation";
|
|
12
|
-
import type { ControlledGit } from "../../../commit/git";
|
|
13
12
|
import type { CommitType, ConventionalAnalysis, ConventionalDetail } from "../../../commit/types";
|
|
14
13
|
import type { CustomTool } from "../../../extensibility/custom-tools/types";
|
|
14
|
+
import * as git from "../../../utils/git";
|
|
15
15
|
import { commitTypeSchema, detailSchema } from "./schemas.js";
|
|
16
16
|
|
|
17
17
|
const proposeCommitSchema = Type.Object({
|
|
@@ -49,10 +49,7 @@ function normalizeDetails(
|
|
|
49
49
|
}));
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
-
export function createProposeCommitTool(
|
|
53
|
-
git: ControlledGit,
|
|
54
|
-
state: CommitAgentState,
|
|
55
|
-
): CustomTool<typeof proposeCommitSchema> {
|
|
52
|
+
export function createProposeCommitTool(cwd: string, state: CommitAgentState): CustomTool<typeof proposeCommitSchema> {
|
|
56
53
|
return {
|
|
57
54
|
name: "propose_commit",
|
|
58
55
|
label: "Propose Commit",
|
|
@@ -72,8 +69,8 @@ export function createProposeCommitTool(
|
|
|
72
69
|
|
|
73
70
|
const summaryValidation = validateSummaryRules(summary);
|
|
74
71
|
const analysisValidation = validateAnalysis(analysis);
|
|
75
|
-
const stagedFiles = state.overview?.files ?? (await git.
|
|
76
|
-
const diffText = state.diffText ?? (await git.
|
|
72
|
+
const stagedFiles = state.overview?.files ?? (await git.diff.changedFiles(cwd, { cached: true }));
|
|
73
|
+
const diffText = state.diffText ?? (await git.diff(cwd, { cached: true }));
|
|
77
74
|
const typeValidation = validateTypeConsistency(params.type, stagedFiles, {
|
|
78
75
|
diffText,
|
|
79
76
|
summary,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Type } from "@sinclair/typebox";
|
|
2
|
-
import type { ControlledGit } from "../../../commit/git";
|
|
3
2
|
import type { CustomTool } from "../../../extensibility/custom-tools/types";
|
|
3
|
+
import * as git from "../../../utils/git";
|
|
4
4
|
|
|
5
5
|
const recentCommitsSchema = Type.Object({
|
|
6
6
|
count: Type.Optional(Type.Number({ description: "Number of commits to fetch", minimum: 1, maximum: 50 })),
|
|
@@ -25,7 +25,7 @@ function extractScope(subject: string): string | null {
|
|
|
25
25
|
return match?.[1]?.trim() ?? null;
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
-
export function createRecentCommitsTool(
|
|
28
|
+
export function createRecentCommitsTool(cwd: string): CustomTool<typeof recentCommitsSchema> {
|
|
29
29
|
return {
|
|
30
30
|
name: "recent_commits",
|
|
31
31
|
label: "Recent Commits",
|
|
@@ -33,7 +33,7 @@ export function createRecentCommitsTool(git: ControlledGit): CustomTool<typeof r
|
|
|
33
33
|
parameters: recentCommitsSchema,
|
|
34
34
|
async execute(_toolCallId, params) {
|
|
35
35
|
const count = params.count ?? 8;
|
|
36
|
-
const commits = await git.
|
|
36
|
+
const commits = await git.log.subjects(cwd, count);
|
|
37
37
|
const verbs: Record<string, number> = {};
|
|
38
38
|
const scopes: Record<string, number> = {};
|
|
39
39
|
const lengths: number[] = [];
|
|
@@ -10,9 +10,9 @@ import {
|
|
|
10
10
|
validateTypeConsistency,
|
|
11
11
|
} from "../../../commit/agentic/validation";
|
|
12
12
|
import { validateScope } from "../../../commit/analysis/validation";
|
|
13
|
-
import type { ControlledGit } from "../../../commit/git";
|
|
14
13
|
import type { ConventionalDetail } from "../../../commit/types";
|
|
15
14
|
import type { CustomTool } from "../../../extensibility/custom-tools/types";
|
|
15
|
+
import * as git from "../../../utils/git";
|
|
16
16
|
import { commitTypeSchema, detailSchema } from "./schemas.js";
|
|
17
17
|
|
|
18
18
|
const hunkSelectorSchema = Type.Union([
|
|
@@ -64,7 +64,7 @@ function normalizeDetails(
|
|
|
64
64
|
}
|
|
65
65
|
|
|
66
66
|
export function createSplitCommitTool(
|
|
67
|
-
|
|
67
|
+
cwd: string,
|
|
68
68
|
state: CommitAgentState,
|
|
69
69
|
changelogTargets: string[],
|
|
70
70
|
): CustomTool<typeof splitCommitSchema> {
|
|
@@ -74,13 +74,13 @@ export function createSplitCommitTool(
|
|
|
74
74
|
description: "Propose multiple atomic commits for unrelated changes.",
|
|
75
75
|
parameters: splitCommitSchema,
|
|
76
76
|
async execute(_toolCallId, params) {
|
|
77
|
-
const stagedFiles = state.overview?.files ?? (await git.
|
|
77
|
+
const stagedFiles = state.overview?.files ?? (await git.diff.changedFiles(cwd, { cached: true }));
|
|
78
78
|
const stagedSet = new Set(stagedFiles);
|
|
79
79
|
const changelogSet = new Set(changelogTargets);
|
|
80
80
|
const usedFiles = new Set<string>();
|
|
81
81
|
const errors: string[] = [];
|
|
82
82
|
const warnings: string[] = [];
|
|
83
|
-
const diffText = await git.
|
|
83
|
+
const diffText = await git.diff(cwd, { cached: true });
|
|
84
84
|
|
|
85
85
|
const commits: SplitCommitGroup[] = params.commits.map((commit, index) => {
|
|
86
86
|
const scope = commit.scope?.trim() || null;
|
|
@@ -2,8 +2,8 @@ import * as path from "node:path";
|
|
|
2
2
|
import type { ThinkingLevel } from "@oh-my-pi/pi-agent-core";
|
|
3
3
|
import type { Api, Model } from "@oh-my-pi/pi-ai";
|
|
4
4
|
import { logger } from "@oh-my-pi/pi-utils";
|
|
5
|
-
import type { ControlledGit } from "../../commit/git";
|
|
6
5
|
import { CHANGELOG_CATEGORIES } from "../../commit/types";
|
|
6
|
+
import * as git from "../../utils/git";
|
|
7
7
|
import { detectChangelogBoundaries } from "./detect";
|
|
8
8
|
import { generateChangelogEntries } from "./generate";
|
|
9
9
|
import { parseUnreleasedSection } from "./parse";
|
|
@@ -13,7 +13,6 @@ const CHANGELOG_SECTIONS = CHANGELOG_CATEGORIES;
|
|
|
13
13
|
const DEFAULT_MAX_DIFF_CHARS = 120_000;
|
|
14
14
|
|
|
15
15
|
export interface ChangelogFlowInput {
|
|
16
|
-
git: ControlledGit;
|
|
17
16
|
cwd: string;
|
|
18
17
|
model: Model<Api>;
|
|
19
18
|
apiKey: string;
|
|
@@ -25,7 +24,6 @@ export interface ChangelogFlowInput {
|
|
|
25
24
|
}
|
|
26
25
|
|
|
27
26
|
export interface ChangelogProposalInput {
|
|
28
|
-
git: ControlledGit;
|
|
29
27
|
cwd: string;
|
|
30
28
|
proposals: Array<{
|
|
31
29
|
path: string;
|
|
@@ -40,7 +38,6 @@ export interface ChangelogProposalInput {
|
|
|
40
38
|
* Update CHANGELOG.md entries for staged changes.
|
|
41
39
|
*/
|
|
42
40
|
export async function runChangelogFlow({
|
|
43
|
-
git,
|
|
44
41
|
cwd,
|
|
45
42
|
model,
|
|
46
43
|
apiKey,
|
|
@@ -58,9 +55,9 @@ export async function runChangelogFlow({
|
|
|
58
55
|
const updated: string[] = [];
|
|
59
56
|
for (const boundary of boundaries) {
|
|
60
57
|
onProgress?.(`Generating entries for ${boundary.changelogPath}…`);
|
|
61
|
-
const diff = await git.
|
|
58
|
+
const diff = await git.diff(cwd, { cached: true, files: boundary.files });
|
|
62
59
|
if (!diff.trim()) continue;
|
|
63
|
-
const stat = await git.
|
|
60
|
+
const stat = await git.diff(cwd, { stat: true, cached: true, files: boundary.files });
|
|
64
61
|
const diffForPrompt = truncateDiff(diff, maxDiffChars ?? DEFAULT_MAX_DIFF_CHARS);
|
|
65
62
|
const changelogContent = await Bun.file(boundary.changelogPath).text();
|
|
66
63
|
let unreleased: { startLine: number; endLine: number; entries: Record<string, string[]> };
|
|
@@ -87,7 +84,7 @@ export async function runChangelogFlow({
|
|
|
87
84
|
const updatedContent = applyChangelogEntries(changelogContent, unreleased, generated.entries);
|
|
88
85
|
if (!dryRun) {
|
|
89
86
|
await Bun.write(boundary.changelogPath, updatedContent);
|
|
90
|
-
await git.
|
|
87
|
+
await git.stage.files(cwd, [path.relative(cwd, boundary.changelogPath)]);
|
|
91
88
|
}
|
|
92
89
|
updated.push(boundary.changelogPath);
|
|
93
90
|
}
|
|
@@ -99,7 +96,6 @@ export async function runChangelogFlow({
|
|
|
99
96
|
* Apply changelog entries provided by the commit agent.
|
|
100
97
|
*/
|
|
101
98
|
export async function applyChangelogProposals({
|
|
102
|
-
git,
|
|
103
99
|
cwd,
|
|
104
100
|
proposals,
|
|
105
101
|
dryRun,
|
|
@@ -132,7 +128,7 @@ export async function applyChangelogProposals({
|
|
|
132
128
|
const updatedContent = applyChangelogEntries(changelogContent, unreleased, normalized, normalizedDeletions);
|
|
133
129
|
if (!dryRun) {
|
|
134
130
|
await Bun.write(proposal.path, updatedContent);
|
|
135
|
-
await git.
|
|
131
|
+
await git.stage.files(cwd, [path.relative(cwd, proposal.path)]);
|
|
136
132
|
}
|
|
137
133
|
updated.push(proposal.path);
|
|
138
134
|
}
|
package/src/commit/pipeline.ts
CHANGED
|
@@ -7,6 +7,7 @@ import { renderPromptTemplate } from "../config/prompt-templates";
|
|
|
7
7
|
import { Settings } from "../config/settings";
|
|
8
8
|
import { discoverAuthStorage } from "../sdk";
|
|
9
9
|
import { loadProjectContextFiles } from "../system-prompt";
|
|
10
|
+
import * as git from "../utils/git";
|
|
10
11
|
import { runAgenticCommit } from "./agentic";
|
|
11
12
|
import {
|
|
12
13
|
extractScopeCandidates,
|
|
@@ -16,7 +17,6 @@ import {
|
|
|
16
17
|
validateSummary,
|
|
17
18
|
} from "./analysis";
|
|
18
19
|
import { runChangelogFlow } from "./changelog";
|
|
19
|
-
import { ControlledGit } from "./git";
|
|
20
20
|
import { runMapReduceAnalysis, shouldUseMapReduce } from "./map-reduce";
|
|
21
21
|
import { formatCommitMessage } from "./message";
|
|
22
22
|
import { resolvePrimaryModel, resolveSmolModel } from "./model-selection";
|
|
@@ -57,12 +57,11 @@ async function runLegacyCommitCommand(args: CommitCommandArgs): Promise<void> {
|
|
|
57
57
|
thinkingLevel: smolThinkingLevel,
|
|
58
58
|
} = await resolveSmolModel(settings, modelRegistry, primaryModel, primaryApiKey);
|
|
59
59
|
|
|
60
|
-
|
|
61
|
-
let stagedFiles = await git.getStagedFiles();
|
|
60
|
+
let stagedFiles = await git.diff.changedFiles(cwd, { cached: true });
|
|
62
61
|
if (stagedFiles.length === 0) {
|
|
63
62
|
process.stdout.write("No staged changes detected, staging all changes...\n");
|
|
64
|
-
await git.
|
|
65
|
-
stagedFiles = await git.
|
|
63
|
+
await git.stage.files(cwd);
|
|
64
|
+
stagedFiles = await git.diff.changedFiles(cwd, { cached: true });
|
|
66
65
|
}
|
|
67
66
|
if (stagedFiles.length === 0) {
|
|
68
67
|
process.stderr.write("No changes to commit.\n");
|
|
@@ -71,7 +70,6 @@ async function runLegacyCommitCommand(args: CommitCommandArgs): Promise<void> {
|
|
|
71
70
|
|
|
72
71
|
if (!args.noChangelog) {
|
|
73
72
|
await runChangelogFlow({
|
|
74
|
-
git,
|
|
75
73
|
cwd,
|
|
76
74
|
model: primaryModel,
|
|
77
75
|
apiKey: primaryApiKey,
|
|
@@ -82,11 +80,11 @@ async function runLegacyCommitCommand(args: CommitCommandArgs): Promise<void> {
|
|
|
82
80
|
});
|
|
83
81
|
}
|
|
84
82
|
|
|
85
|
-
const diff = await git.
|
|
86
|
-
const stat = await git.
|
|
87
|
-
const numstat = await git.
|
|
83
|
+
const diff = await git.diff(cwd, { cached: true });
|
|
84
|
+
const stat = await git.diff(cwd, { stat: true, cached: true });
|
|
85
|
+
const numstat = await git.diff.numstat(cwd, { cached: true });
|
|
88
86
|
const scopeCandidates = extractScopeCandidates(numstat).scopeCandidates;
|
|
89
|
-
const recentCommits = await git.
|
|
87
|
+
const recentCommits = await git.log.subjects(cwd, RECENT_COMMITS_COUNT);
|
|
90
88
|
const contextFiles = await loadProjectContextFiles({ cwd });
|
|
91
89
|
const formattedContextFiles = contextFiles.map(file => ({
|
|
92
90
|
path: path.relative(cwd, file.path),
|
|
@@ -131,10 +129,10 @@ async function runLegacyCommitCommand(args: CommitCommandArgs): Promise<void> {
|
|
|
131
129
|
return;
|
|
132
130
|
}
|
|
133
131
|
|
|
134
|
-
await git.commit(commitMessage);
|
|
132
|
+
await git.commit(cwd, commitMessage);
|
|
135
133
|
process.stdout.write("Commit created.\n");
|
|
136
134
|
if (args.push) {
|
|
137
|
-
await git.push();
|
|
135
|
+
await git.push(cwd);
|
|
138
136
|
process.stdout.write("Pushed to remote.\n");
|
|
139
137
|
}
|
|
140
138
|
}
|
|
@@ -37,6 +37,7 @@ interface AppKeybindings {
|
|
|
37
37
|
"app.session.tree": true;
|
|
38
38
|
"app.session.fork": true;
|
|
39
39
|
"app.session.resume": true;
|
|
40
|
+
"app.session.observe": true;
|
|
40
41
|
"app.session.togglePath": true;
|
|
41
42
|
"app.session.toggleSort": true;
|
|
42
43
|
"app.session.rename": true;
|
|
@@ -144,6 +145,10 @@ export const KEYBINDINGS = {
|
|
|
144
145
|
defaultKeys: [],
|
|
145
146
|
description: "Resume session",
|
|
146
147
|
},
|
|
148
|
+
"app.session.observe": {
|
|
149
|
+
defaultKeys: "ctrl+s",
|
|
150
|
+
description: "Observe subagent sessions",
|
|
151
|
+
},
|
|
147
152
|
"app.session.togglePath": {
|
|
148
153
|
defaultKeys: "ctrl+p",
|
|
149
154
|
description: "Toggle session path display",
|
|
@@ -214,6 +219,7 @@ const KEYBINDING_NAME_MIGRATIONS = {
|
|
|
214
219
|
tree: "app.session.tree",
|
|
215
220
|
fork: "app.session.fork",
|
|
216
221
|
resume: "app.session.resume",
|
|
222
|
+
observeSessions: "app.session.observe",
|
|
217
223
|
toggleSTT: "app.stt.toggle",
|
|
218
224
|
// TUI editor (old names for backward compatibility)
|
|
219
225
|
cursorUp: "tui.editor.cursorUp",
|
|
@@ -260,9 +266,6 @@ function isLegacyKeybindingName(key: string): key is keyof typeof KEYBINDING_NAM
|
|
|
260
266
|
return key in KEYBINDING_NAME_MIGRATIONS;
|
|
261
267
|
}
|
|
262
268
|
|
|
263
|
-
/**
|
|
264
|
-
* Normalize input to KeybindingsConfig, validating types.
|
|
265
|
-
*/
|
|
266
269
|
function toKeybindingsConfig(value: unknown): KeybindingsConfig {
|
|
267
270
|
if (typeof value !== "object" || value === null) {
|
|
268
271
|
return {};
|
|
@@ -270,15 +273,13 @@ function toKeybindingsConfig(value: unknown): KeybindingsConfig {
|
|
|
270
273
|
|
|
271
274
|
const config: KeybindingsConfig = {};
|
|
272
275
|
for (const [key, val] of Object.entries(value)) {
|
|
273
|
-
// Allow undefined, string (KeyId), or array of strings
|
|
274
276
|
if (val === undefined) {
|
|
275
277
|
config[key] = undefined;
|
|
276
278
|
} else if (typeof val === "string") {
|
|
277
279
|
config[key] = val as KeyId;
|
|
278
280
|
} else if (Array.isArray(val) && val.every(v => typeof v === "string")) {
|
|
279
|
-
config[key] = val as
|
|
281
|
+
config[key] = val as KeyId[];
|
|
280
282
|
}
|
|
281
|
-
// Silently skip invalid entries
|
|
282
283
|
}
|
|
283
284
|
return config;
|
|
284
285
|
}
|
|
@@ -194,6 +194,15 @@ export const SETTINGS_SCHEMA = {
|
|
|
194
194
|
// ────────────────────────────────────────────────────────────────────────
|
|
195
195
|
lastChangelogVersion: { type: "string", default: undefined },
|
|
196
196
|
|
|
197
|
+
autoResume: {
|
|
198
|
+
type: "boolean",
|
|
199
|
+
default: false,
|
|
200
|
+
ui: {
|
|
201
|
+
tab: "interaction",
|
|
202
|
+
label: "Auto Resume",
|
|
203
|
+
description: "Automatically resume the most recent session in the current directory",
|
|
204
|
+
},
|
|
205
|
+
},
|
|
197
206
|
shellPath: { type: "string", default: undefined },
|
|
198
207
|
|
|
199
208
|
extensions: { type: "array", default: EMPTY_STRING_ARRAY },
|
|
@@ -797,6 +806,38 @@ export const SETTINGS_SCHEMA = {
|
|
|
797
806
|
|
|
798
807
|
"compaction.remoteEndpoint": { type: "string", default: undefined },
|
|
799
808
|
|
|
809
|
+
// Idle compaction
|
|
810
|
+
"compaction.idleEnabled": {
|
|
811
|
+
type: "boolean",
|
|
812
|
+
default: false,
|
|
813
|
+
ui: {
|
|
814
|
+
tab: "context",
|
|
815
|
+
label: "Idle Compaction",
|
|
816
|
+
description: "Compact context while idle when token count exceeds threshold",
|
|
817
|
+
},
|
|
818
|
+
},
|
|
819
|
+
|
|
820
|
+
"compaction.idleThresholdTokens": {
|
|
821
|
+
type: "number",
|
|
822
|
+
default: 200000,
|
|
823
|
+
ui: {
|
|
824
|
+
tab: "context",
|
|
825
|
+
label: "Idle Compaction Threshold",
|
|
826
|
+
description: "Token count above which idle compaction triggers",
|
|
827
|
+
submenu: true,
|
|
828
|
+
},
|
|
829
|
+
},
|
|
830
|
+
|
|
831
|
+
"compaction.idleTimeoutSeconds": {
|
|
832
|
+
type: "number",
|
|
833
|
+
default: 300,
|
|
834
|
+
ui: {
|
|
835
|
+
tab: "context",
|
|
836
|
+
label: "Idle Compaction Delay",
|
|
837
|
+
description: "Seconds to wait while idle before compacting",
|
|
838
|
+
submenu: true,
|
|
839
|
+
},
|
|
840
|
+
},
|
|
800
841
|
// Branch summaries
|
|
801
842
|
"branchSummary.enabled": {
|
|
802
843
|
type: "boolean",
|
|
@@ -1701,6 +1742,9 @@ export interface CompactionSettings {
|
|
|
1701
1742
|
autoContinue: boolean;
|
|
1702
1743
|
remoteEnabled: boolean;
|
|
1703
1744
|
remoteEndpoint: string | undefined;
|
|
1745
|
+
idleEnabled: boolean;
|
|
1746
|
+
idleThresholdTokens: number;
|
|
1747
|
+
idleTimeoutSeconds: number;
|
|
1704
1748
|
}
|
|
1705
1749
|
|
|
1706
1750
|
export interface ContextPromotionSettings {
|
|
@@ -2,26 +2,14 @@ import { renderPromptTemplate } from "../../../../config/prompt-templates";
|
|
|
2
2
|
import type { CustomCommand, CustomCommandAPI } from "../../../../extensibility/custom-commands/types";
|
|
3
3
|
import type { HookCommandContext } from "../../../../extensibility/hooks/types";
|
|
4
4
|
import ciGreenRequestTemplate from "../../../../prompts/ci-green-request.md" with { type: "text" };
|
|
5
|
+
import * as git from "../../../../utils/git";
|
|
5
6
|
|
|
6
7
|
async function getHeadTag(api: CustomCommandAPI): Promise<string | undefined> {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
"HEAD",
|
|
11
|
-
"--sort=-version:refname",
|
|
12
|
-
"--format=%(refname:strip=2)",
|
|
13
|
-
"refs/tags",
|
|
14
|
-
]);
|
|
15
|
-
|
|
16
|
-
if (result.code !== 0 || result.killed) {
|
|
8
|
+
try {
|
|
9
|
+
return (await git.ref.tags(api.cwd))[0];
|
|
10
|
+
} catch {
|
|
17
11
|
return undefined;
|
|
18
12
|
}
|
|
19
|
-
|
|
20
|
-
const tag = result.stdout
|
|
21
|
-
.split("\n")
|
|
22
|
-
.map(line => line.trim())
|
|
23
|
-
.find(Boolean);
|
|
24
|
-
return tag || undefined;
|
|
25
13
|
}
|
|
26
14
|
|
|
27
15
|
export class GreenCommand implements CustomCommand {
|
|
@@ -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
|
}
|