Skip to content

Commit 44415a7

Browse files
authored
Merge pull request #39 from ruslanmv/dev-v0.3.5.24
First commit
2 parents 4ffc893 + f0cf499 commit 44415a7

7 files changed

Lines changed: 358 additions & 21 deletions

File tree

README.md

Lines changed: 84 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
### The first open-source multi-agent AI coding assistant.
88

99
Four specialized agents &mdash; Explorer, Planner, Coder, Reviewer &mdash; collaborate on every task.<br/>
10-
You approve every change. No surprises, no silent commits, no lock-in.
10+
By default, GitPilot asks before every risky action. Switch to Auto or Plan mode anytime.
1111

1212
[![PyPI](https://img.shields.io/pypi/v/gitcopilot?style=flat-square&color=D95C3D&labelColor=1C1C1F&label=pypi)](https://pypi.org/project/gitcopilot/)
1313
[![Python](https://img.shields.io/badge/python-3.11%20%7C%203.12-D95C3D?style=flat-square&labelColor=1C1C1F)](https://www.python.org/)
@@ -39,7 +39,15 @@ Most AI coding tools are a **single model behind a chat box**. GitPilot is funda
3939
| **Coder** | Execution | Writes code, runs your tests, and self-corrects on failure — iterating until the suite passes |
4040
| **Reviewer** | Quality | Validates the output, re-runs the suite, and drafts a commit message and PR summary |
4141

42-
**You approve every change.** Diffs are shown before they're applied. Tests run before anything is committed. No surprises.
42+
**You control how the agent runs.** Three execution modes — selectable per session from the VS Code compose bar or backend API:
43+
44+
| Mode | Default? | Behavior |
45+
|---|---|---|
46+
| **Ask** | Yes | Prompts you before each dangerous action (write, edit, run, commit). You see the diff and click Allow / Deny. |
47+
| **Auto** | | Executes all tools automatically. Fastest for experienced users who trust the plan. |
48+
| **Plan** | | Read-only. Generates and displays the plan but blocks all file writes and commands. |
49+
50+
Diffs are shown before they're applied. Tests run before anything is committed. No surprises.
4351

4452
### What else sets GitPilot apart
4553

@@ -63,9 +71,9 @@ You: "Add input validation to the login form"
6371
GitPilot:
6472
1. Reading src/auth/login.ts...
6573
2. Planning 3 changes...
66-
3. Editing login.ts (Allow? [Yes] [No])
74+
3. Editing login.ts → [Apply Patch] [Revert]
6775
4. Running npm test... 3 passed
68-
5. Done.
76+
5. Done — files written to your workspace.
6977
```
7078

7179
---
@@ -116,12 +124,79 @@ The sidebar panel gives you everything in one place:
116124
| Feature | What it does |
117125
|---|---|
118126
| **Chat** | Ask questions, request changes, review code |
127+
| **Execution Modes** | Bottom bar: `Auto` / `Ask` / `Plan` — controls agent permissions per session |
119128
| **Plan View** | See the step-by-step plan before changes are made |
129+
| **Plan Approval** | "Approve & Execute" / "Dismiss" bar — execution waits for your OK |
130+
| **Tool Approvals** | Per-action Allow / Allow for session / Deny cards (Ask mode) |
120131
| **Diff Preview** | Review proposed edits in VS Code's native diff viewer |
121132
| **Apply / Revert** | One click to apply changes, one click to undo |
122133
| **Quick Actions** | Explain, Review, Fix, Generate Tests, Security Scan |
123134
| **Smart Commit** | AI-generated commit messages |
124135
| **Code Lens** | Inline "Explain / Review" hints on functions |
136+
| **Settings Tab** | Branded settings page (General, Provider, Agent, Editor) |
137+
| **New Chat** | One click to clear chat and start a fresh session |
138+
139+
### Execution modes
140+
141+
The compose bar includes a mode selector that controls how the multi-agent pipeline runs:
142+
143+
```
144+
[ Auto | Ask | Plan ] [ Send ] [ New Chat ]
145+
```
146+
147+
| Mode | VS Code setting | Backend value | What happens |
148+
|---|---|---|---|
149+
| **Ask** (default) | `gitpilot.permissionMode: "normal"` | `"normal"` | Each dangerous tool (write, edit, run, commit) shows an approval card |
150+
| **Auto** | `gitpilot.permissionMode: "auto"` | `"auto"` | Tools execute automatically — no approval prompts |
151+
| **Plan** | `gitpilot.permissionMode: "plan"` | `"plan"` | Plan is generated and displayed, all writes/commands blocked |
152+
153+
Mode changes are persisted to VS Code settings and synced to the backend via `PUT /api/permissions/mode`.
154+
155+
### How approvals work
156+
157+
```
158+
You send a request
159+
→ Explorer reads repo context
160+
→ Planner drafts step-by-step plan
161+
→ Plan appears in sidebar (Approve & Execute / Dismiss)
162+
→ You click Approve
163+
→ Coder begins execution
164+
→ Dangerous tool requested (e.g. write_file)
165+
→ Ask mode: approval card shown (Allow / Allow for session / Deny)
166+
→ Auto mode: executes immediately
167+
→ Plan mode: blocked
168+
→ Tests run, Reviewer validates
169+
→ Done — Apply Patch or Revert
170+
```
171+
172+
> **Note:** Simple questions (e.g. "explain this code") may return a direct answer without generating a multi-step plan. This is expected — the planner activates for tasks that require file changes or multi-step execution.
173+
174+
### Code generation and Apply Patch
175+
176+
When you ask GitPilot to create or edit files, the response includes structured `edits` — not just text. The **Apply Patch** button writes them directly to your workspace.
177+
178+
```
179+
You: "Create a Flask app with app.py, requirements.txt, and README.md"
180+
181+
GitPilot:
182+
→ LLM generates 3 files with content
183+
→ Backend extracts structured edits (path + content)
184+
→ VS Code shows [Apply Patch] [Revert]
185+
→ You click Apply Patch
186+
→ 3 files written to disk
187+
→ Project context refreshes automatically
188+
→ First file opens in the editor
189+
```
190+
191+
How it works under the hood:
192+
- The LLM is instructed to output code blocks with the filename on the fence line (` ```python hello.py`)
193+
- The backend parses these blocks into `ProposedEdit` objects with file path, kind, and content
194+
- All paths are sanitized (rejects `../` traversal, absolute paths, drive letters)
195+
- The extension stores edits in `activeTask.edits` and shows Apply / Revert
196+
- `PatchApplier` writes files via `vscode.workspace.fs.writeFile`
197+
- After apply, project context refreshes and the first file opens
198+
199+
> **Note:** For folder-only sessions (no GitHub remote), code generation uses the LLM directly with structured output instructions. For GitHub-connected sessions, the full CrewAI multi-agent pipeline (Explorer → Planner → Coder → Reviewer) handles planning and execution.
125200
126201
### Supported AI Providers
127202

@@ -165,7 +240,7 @@ GitPilot uses a multi-agent system powered by CrewAI:
165240
3. **Executor** writes code and runs tests, self-correcting on failure
166241
4. **Reviewer** validates the output and summarises what changed
167242

168-
You approve every change before it's applied.
243+
In **Ask** mode (default), you approve every change before it's applied. In **Auto** mode, tools execute without prompts. In **Plan** mode, only the plan is generated — no files are touched.
169244

170245
---
171246

@@ -225,12 +300,15 @@ GitPilot exposes a REST + WebSocket API:
225300
|---|---|
226301
| `GET /api/status` | Server health check |
227302
| `POST /api/chat/send` | Send a message, get a response |
228-
| `POST /api/v2/chat/stream` | Stream agent events (SSE) |
303+
| `POST /api/v2/chat/stream` | Stream agent events (SSE) — accepts `permission_mode` |
229304
| `WS /ws/v2/sessions/{id}` | Real-time WebSocket streaming |
230305
| `POST /api/chat/plan` | Generate an execution plan |
231306
| `POST /api/chat/execute` | Execute a plan |
232307
| `GET /api/repos` | List connected repositories |
233308
| `GET /api/sessions` | List chat sessions |
309+
| `GET /api/permissions` | Current permission policy |
310+
| `PUT /api/permissions/mode` | Set execution mode: `normal` / `auto` / `plan` |
311+
| `POST /api/v2/approval/respond` | Approve or deny a tool execution request |
234312

235313
Full API docs at `http://localhost:8000/docs` (Swagger UI).
236314

extensions/vscode/src/core/stateStore.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import {
2323
ChatMessagePayload,
2424
ChatState,
2525
WorkspaceUiState,
26+
ExecutionMode,
2627
} from "./types";
2728

2829
type StateChangeListener = (state: GitPilotState) => void;
@@ -83,6 +84,7 @@ const DEFAULT_STATE: GitPilotState = {
8384
warnings: [],
8485
},
8586
workflow: { selectedMode: "auto", source: "auto" },
87+
executionMode: "ask",
8688
projectContextSummary: DEFAULT_PROJECT_CONTEXT_SUMMARY,
8789
activeTask: DEFAULT_ACTIVE_TASK,
8890
chat: DEFAULT_CHAT,
@@ -229,6 +231,10 @@ export class StateStore {
229231
this.setChatMessages([...this._state.chat.messages, message]);
230232
}
231233

234+
setExecutionMode(mode: ExecutionMode): void {
235+
this.update({ executionMode: mode } as Partial<GitPilotState>);
236+
}
237+
232238
clearTaskState(): void {
233239
this.update({ activeTask: JSON.parse(JSON.stringify(DEFAULT_ACTIVE_TASK)), ui: { mode: "idle", focusedDiffPath: undefined } });
234240
}

extensions/vscode/src/core/types.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
export type WorkspaceMode = "folder" | "local_git" | "github";
77
export type WorkspaceUiMode = "idle" | "working" | "diff";
8+
export type ExecutionMode = "auto" | "ask" | "plan";
89

910
export type ConnectionState =
1011
| "connected"
@@ -232,6 +233,7 @@ export interface GitPilotState {
232233
session: SessionState;
233234
readiness: ReadinessState;
234235
workflow: WorkflowState;
236+
executionMode: ExecutionMode;
235237
projectContextSummary: ProjectContextSummaryState;
236238
activeTask: ActiveTaskState;
237239
chat: ChatState;
@@ -347,7 +349,8 @@ export type WebviewToExtensionMessage =
347349
| { type: "CANCEL_TASK" }
348350
| { type: "NEW_SESSION" }
349351
| { type: "APPROVE_PLAN" }
350-
| { type: "REJECT_PLAN" };
352+
| { type: "REJECT_PLAN" }
353+
| { type: "SET_EXECUTION_MODE"; payload: { mode: "auto" | "ask" | "plan" } };
351354

352355
export interface StatusResponse {
353356
server_ready: boolean;

extensions/vscode/src/extension.ts

Lines changed: 78 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ import type {
7979
StructuredWorkingSet,
8080
StructuredTaskContext,
8181
PlanSummary,
82+
ProposedEdit,
8283
} from "./core/types";
8384

8485
type DisposableLike = { dispose(): void };
@@ -1050,7 +1051,7 @@ export function activate(context: vscode.ExtensionContext): void {
10501051
body: JSON.stringify({
10511052
session_id: sessionId,
10521053
message,
1053-
permission_mode: "normal",
1054+
permission_mode: ({ auto: "auto", ask: "normal", plan: "plan" } as Record<string, string>)[stateStore.state.executionMode] || "normal",
10541055
}),
10551056
signal,
10561057
});
@@ -1352,12 +1353,26 @@ export function activate(context: vscode.ExtensionContext): void {
13521353
appendChatMessageToState(assistantMessage);
13531354
syncResponsePlanToState(response);
13541355

1356+
// Extract proposed file edits from the backend response.
1357+
// The backend now parses code blocks in the LLM answer and
1358+
// returns them as structured ProposedEdit objects, enabling
1359+
// the VS Code "Apply Patch" button to write files to disk.
1360+
const responseEdits: ProposedEdit[] = Array.isArray(
1361+
(response as unknown as Record<string, unknown>).edits
1362+
)
1363+
? ((response as unknown as Record<string, unknown>).edits as ProposedEdit[])
1364+
: [];
1365+
1366+
const hasEdits = responseEdits.length > 0;
1367+
13551368
const updatedTask = stateStore.state.activeTask || {};
13561369
stateStore.updateActiveTask({
13571370
...updatedTask,
1358-
status: normalizedPlan ? "ready_to_apply" : "done",
1371+
status: normalizedPlan || hasEdits ? "ready_to_apply" : "done",
1372+
edits: hasEdits ? responseEdits : updatedTask.edits,
13591373
summary:
13601374
normalizedPlan?.summary ||
1375+
(hasEdits ? `${responseEdits.length} file(s) ready to apply.` : "") ||
13611376
updatedTask.summary ||
13621377
"GitPilot completed the request.",
13631378
});
@@ -1583,6 +1598,32 @@ export function activate(context: vscode.ExtensionContext): void {
15831598
return;
15841599
}
15851600

1601+
case "SET_EXECUTION_MODE": {
1602+
const execMode = msg.payload.mode;
1603+
output.appendLine(`[GitPilot] Execution mode set to: ${execMode}`);
1604+
stateStore.setExecutionMode(execMode);
1605+
1606+
// Persist to VS Code settings
1607+
const modeMap: Record<string, string> = { auto: "auto", ask: "normal", plan: "plan" };
1608+
void vscode.workspace.getConfiguration("gitpilot").update(
1609+
"permissionMode", modeMap[execMode] || "normal",
1610+
vscode.ConfigurationTarget.Global
1611+
);
1612+
1613+
// Sync to backend
1614+
try {
1615+
const serverUrl = client.serverUrl;
1616+
void fetch(`${serverUrl}/api/permissions/mode`, {
1617+
method: "PUT",
1618+
headers: { "Content-Type": "application/json" },
1619+
body: JSON.stringify({ mode: modeMap[execMode] || "normal" }),
1620+
});
1621+
} catch {
1622+
output.appendLine("[GitPilot] Warning: failed to sync mode to backend");
1623+
}
1624+
return;
1625+
}
1626+
15861627
case "REFRESH_STATUS":
15871628
await vscode.commands.executeCommand("gitpilot.refreshStatus");
15881629
return;
@@ -1880,8 +1921,41 @@ export function activate(context: vscode.ExtensionContext): void {
18801921
const { PatchApplier } = await import("./services/patch/patchApplier");
18811922
const patchApplier = new PatchApplier();
18821923
const result = await patchApplier.apply(folderPath, edits);
1883-
stateStore.setTaskStatus(result.success ? "done" : "failed");
1884-
output.appendLine(`[GitPilot] Apply result: ${result.success ? "success" : "failed"} (${result.appliedFiles?.length ?? 0} files)`);
1924+
1925+
if (result.success) {
1926+
// Post-apply: refresh project context so the tree/index
1927+
// reflects the newly created/modified files, and clear
1928+
// pending edits so the "Apply Patch" button disappears.
1929+
stateStore.updateActiveTask({
1930+
...(stateStore.state.activeTask || {}),
1931+
edits: [],
1932+
status: "done",
1933+
summary: `Applied ${result.appliedFiles?.length ?? edits.length} file(s). Context refreshing...`,
1934+
});
1935+
1936+
// Refresh project context in background
1937+
void vscode.commands.executeCommand("gitpilot.refreshProjectContext");
1938+
1939+
// Open the first applied file so the user sees the result
1940+
if (result.appliedFiles?.length) {
1941+
const firstFile = result.appliedFiles[0] as unknown;
1942+
const firstPath = typeof firstFile === "string"
1943+
? firstFile
1944+
: (firstFile as { path?: string })?.path || "";
1945+
const fileUri = vscode.Uri.file(path.join(folderPath, firstPath));
1946+
void vscode.commands.executeCommand("vscode.open", fileUri);
1947+
}
1948+
1949+
output.appendLine(
1950+
`[GitPilot] Apply success: ${result.appliedFiles?.length ?? 0} files written`
1951+
);
1952+
vscode.window.showInformationMessage(
1953+
`GitPilot applied ${result.appliedFiles?.length ?? edits.length} file(s) successfully.`
1954+
);
1955+
} else {
1956+
stateStore.setTaskStatus("failed");
1957+
output.appendLine("[GitPilot] Apply reported failure");
1958+
}
18851959
} catch (err) {
18861960
stateStore.setTaskStatus("failed");
18871961
appendOutputError("[GitPilot] Apply failed", err);

extensions/vscode/src/ui/webview/gitpilotWorkspace.css

Lines changed: 41 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -691,9 +691,48 @@ details[open] .chevron {
691691
}
692692

693693
.primary-row {
694-
display: grid;
695-
grid-template-columns: 1fr 1fr;
694+
display: flex;
696695
gap: 8px;
696+
align-items: center;
697+
}
698+
699+
/* ─── Execution mode selector (Auto / Ask / Plan) ─── */
700+
.mode-selector {
701+
display: inline-flex;
702+
border-radius: 8px;
703+
border: 1px solid var(--border);
704+
overflow: hidden;
705+
flex-shrink: 0;
706+
}
707+
.mode-btn {
708+
font-family: inherit;
709+
font-size: var(--font-xs);
710+
font-weight: 600;
711+
padding: 5px 10px;
712+
color: var(--muted);
713+
background: transparent;
714+
border: none;
715+
cursor: pointer;
716+
transition: background 100ms ease, color 100ms ease;
717+
line-height: 1;
718+
}
719+
.mode-btn:not(:last-child) {
720+
border-right: 1px solid var(--border);
721+
}
722+
.mode-btn:hover {
723+
color: var(--fg);
724+
background: rgba(255, 255, 255, 0.04);
725+
}
726+
.mode-btn.active {
727+
color: var(--accent-bright, #ff7a3c);
728+
background: rgba(255, 122, 60, 0.1);
729+
}
730+
731+
.primary-row #send-btn {
732+
flex: 1;
733+
}
734+
.primary-row #new-chat-btn {
735+
flex-shrink: 0;
697736
}
698737

699738
.secondary-row {

0 commit comments

Comments
 (0)