Skip to content

Commit abc0cfb

Browse files
authored
Merge pull request #268 from acunniffe/feat/add-gemini-support-and-hook-cleanup
Add gemini support, gemini tests, clean up other agents' transcript logic
2 parents 0c987ad + 215eac7 commit abc0cfb

File tree

12 files changed

+1875
-139
lines changed

12 files changed

+1875
-139
lines changed

agent-support/vscode/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"name": "git-ai-vscode",
33
"displayName": "git-ai",
44
"description": "Keep track of code generated by AI.",
5-
"version": "0.1.7",
5+
"version": "0.1.8",
66
"icon": "git-ai.png",
77
"publisher": "git-ai",
88
"repository": {

agent-support/vscode/src/ai-edit-manager.ts

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -181,10 +181,11 @@ export class AIEditManager {
181181
console.log('[git-ai] AIEditManager: Dirty files with saved file content:', dirtyFiles);
182182
this.checkpoint("ai", JSON.stringify({
183183
hook_event_name: "after_edit",
184-
chatSessionPath,
185-
sessionId,
186-
workspaceFolder: workspaceFolder.uri.fsPath,
187-
dirtyFiles,
184+
chat_session_path: chatSessionPath,
185+
session_id: sessionId,
186+
edited_filepaths: [filePath],
187+
workspace_folder: workspaceFolder.uri.fsPath,
188+
dirty_files: dirtyFiles,
188189
}));
189190
checkpointTriggered = true;
190191
}
@@ -264,9 +265,9 @@ export class AIEditManager {
264265
// Prepare hook input for human checkpoint (session ID is not reliable, so we skip it)
265266
const hookInput = JSON.stringify({
266267
hook_event_name: "before_edit",
267-
workspaceFolder: workspaceFolder.uri.fsPath,
268+
workspace_folder: workspaceFolder.uri.fsPath,
268269
will_edit_filepaths: filesToCheckpoint,
269-
dirtyFiles: dirtyFiles,
270+
dirty_files: dirtyFiles,
270271
});
271272

272273
this.checkpoint("human", hookInput);

agent-support/vscode/src/consts.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { IDEHostKind } from "./utils/host-kind";
22

3-
export const MIN_GIT_AI_VERSION = "1.0.22";
3+
export const MIN_GIT_AI_VERSION = "1.0.23";
44

55
// Use GitHub URL to avoid VS Code open URL safety prompt
66
export const GIT_AI_INSTALL_DOCS_URL = "https://github.com/acunniffe/git-ai?tab=readme-ov-file#quick-start";

src/authorship/post_commit.rs

Lines changed: 51 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ use crate::authorship::authorship_log_serialization::AuthorshipLog;
33
use crate::authorship::stats::{stats_for_commit_stats, write_stats_to_terminal};
44
use crate::authorship::virtual_attribution::VirtualAttributions;
55
use crate::authorship::working_log::Checkpoint;
6-
use crate::commands::checkpoint_agent::agent_presets::{CursorPreset, GithubCopilotPreset};
6+
use crate::commands::checkpoint_agent::agent_presets::{ClaudePreset, CursorPreset, GeminiPreset, GithubCopilotPreset};
77
use crate::config::Config;
88
use crate::error::GitAiError;
99
use crate::git::refs::notes_add;
@@ -209,7 +209,56 @@ fn update_prompts_to_latest(checkpoints: &mut [Checkpoint]) -> Result<(), GitAiE
209209
None
210210
}
211211
}
212-
// TODO: Implement for other AI agents
212+
"claude" => {
213+
// Try to load transcript from agent_metadata if available
214+
if let Some(metadata) = &checkpoint.agent_metadata {
215+
if let Some(transcript_path) = metadata.get("transcript_path") {
216+
// Try to read and parse the transcript JSONL
217+
match ClaudePreset::transcript_and_model_from_claude_code_jsonl(transcript_path) {
218+
Ok((transcript, model)) => {
219+
// Update to the latest transcript (similar to Cursor behavior)
220+
// This handles both cases: initial load failure and getting latest version
221+
Some((transcript, model.unwrap_or_else(|| agent_id.model.clone())))
222+
}
223+
Err(_e) => {
224+
// TODO Log error to sentry
225+
None
226+
}
227+
}
228+
} else {
229+
// No transcript_path in metadata
230+
None
231+
}
232+
} else {
233+
// No agent_metadata available
234+
None
235+
}
236+
}
237+
"gemini" => {
238+
// Try to load transcript from agent_metadata if available
239+
if let Some(metadata) = &checkpoint.agent_metadata {
240+
if let Some(transcript_path) = metadata.get("transcript_path") {
241+
// Try to read and parse the transcript JSON
242+
match GeminiPreset::transcript_and_model_from_gemini_json(transcript_path) {
243+
Ok((transcript, model)) => {
244+
// Update to the latest transcript (similar to Cursor behavior)
245+
// This handles both cases: initial load failure and getting latest version
246+
Some((transcript, model.unwrap_or_else(|| agent_id.model.clone())))
247+
}
248+
Err(_e) => {
249+
// TODO Log error to sentry
250+
None
251+
}
252+
}
253+
} else {
254+
// No transcript_path in metadata
255+
None
256+
}
257+
} else {
258+
// No agent_metadata available
259+
None
260+
}
261+
}
213262
_ => {
214263
// Unknown tool, skip updating
215264
None

src/authorship/transcript.rs

Lines changed: 0 additions & 86 deletions
Original file line numberDiff line numberDiff line change
@@ -96,92 +96,6 @@ impl AiTranscript {
9696
messages: filtered_messages,
9797
}
9898
}
99-
100-
/// Parse a Claude Code JSONL file into a transcript and extract model info
101-
pub fn from_claude_code_jsonl_with_model(
102-
jsonl_content: &str,
103-
) -> Result<(Self, Option<String>), serde_json::Error> {
104-
let mut transcript = AiTranscript::new();
105-
let mut model = None;
106-
107-
for line in jsonl_content.lines() {
108-
if !line.trim().is_empty() {
109-
// Parse the raw JSONL entry
110-
let raw_entry: serde_json::Value = serde_json::from_str(line)?;
111-
let timestamp = raw_entry["timestamp"].as_str().map(|s| s.to_string());
112-
113-
// Extract model from assistant messages if we haven't found it yet
114-
if model.is_none() && raw_entry["type"].as_str() == Some("assistant") {
115-
if let Some(model_str) = raw_entry["message"]["model"].as_str() {
116-
model = Some(model_str.to_string());
117-
}
118-
}
119-
120-
// Extract messages based on the type
121-
match raw_entry["type"].as_str() {
122-
Some("user") => {
123-
// Handle user messages
124-
if let Some(content) = raw_entry["message"]["content"].as_str() {
125-
if !content.trim().is_empty() {
126-
transcript.add_message(Message::User {
127-
text: content.to_string(),
128-
timestamp: timestamp.clone(),
129-
});
130-
}
131-
} else if let Some(content_array) =
132-
raw_entry["message"]["content"].as_array()
133-
{
134-
// Handle user messages with content array (like tool results)
135-
for item in content_array {
136-
if let Some(text) = item["content"].as_str() {
137-
if !text.trim().is_empty() {
138-
transcript.add_message(Message::User {
139-
text: text.to_string(),
140-
timestamp: timestamp.clone(),
141-
});
142-
}
143-
}
144-
}
145-
}
146-
}
147-
Some("assistant") => {
148-
// Handle assistant messages
149-
if let Some(content_array) = raw_entry["message"]["content"].as_array() {
150-
for item in content_array {
151-
match item["type"].as_str() {
152-
Some("text") => {
153-
if let Some(text) = item["text"].as_str() {
154-
if !text.trim().is_empty() {
155-
transcript.add_message(Message::Assistant {
156-
text: text.to_string(),
157-
timestamp: timestamp.clone(),
158-
});
159-
}
160-
}
161-
}
162-
Some("tool_use") => {
163-
if let (Some(name), Some(_input)) =
164-
(item["name"].as_str(), item["input"].as_object())
165-
{
166-
transcript.add_message(Message::ToolUse {
167-
name: name.to_string(),
168-
input: item["input"].clone(),
169-
timestamp: timestamp.clone(),
170-
});
171-
}
172-
}
173-
_ => continue, // Skip unknown content types
174-
}
175-
}
176-
}
177-
}
178-
_ => continue, // Skip unknown message types
179-
}
180-
}
181-
}
182-
183-
Ok((transcript, model))
184-
}
18599
}
186100

187101
impl Default for AiTranscript {

0 commit comments

Comments
 (0)