Skip to content

Commit ca2bc04

Browse files
btuckerclaude
andcommitted
Extract originalFile from tool results for remote session support
When viewing sessions from remote URLs (like gists), the local filesystem isn't available to read initial file content before edits. This change extracts the originalFile field from toolUseResult in JSONL sessions and uses it as a fallback when reconstructing file state. - Add original_content field to FileOperation dataclass - Extract originalFile from toolUseResult entries in extract_file_operations() - Use original_content as fallback in build_file_history_repo() when file doesn't exist locally - Add test coverage for originalFile extraction 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent e0a280c commit ca2bc04

File tree

2 files changed

+190
-0
lines changed

2 files changed

+190
-0
lines changed

src/claude_code_transcripts/code_view.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,10 @@ class FileOperation:
7070
new_string: Optional[str] = None
7171
replace_all: bool = False
7272

73+
# Original file content from tool result (for Edit operations)
74+
# This allows reconstruction without local file access
75+
original_content: Optional[str] = None
76+
7377

7478
@dataclass
7579
class FileState:
@@ -152,6 +156,24 @@ def extract_file_operations(
152156
# Store timestamp -> (page_num, msg_id) mapping
153157
msg_to_page[timestamp] = (page_num, msg_id)
154158

159+
# First pass: collect originalFile content from tool results
160+
# These are stored in the toolUseResult field of user messages
161+
tool_id_to_original = {}
162+
for entry in loglines:
163+
tool_use_result = entry.get("toolUseResult", {})
164+
if tool_use_result and "originalFile" in tool_use_result:
165+
# Find the matching tool_use_id from the message content
166+
message = entry.get("message", {})
167+
content = message.get("content", [])
168+
if isinstance(content, list):
169+
for block in content:
170+
if isinstance(block, dict) and block.get("type") == "tool_result":
171+
tool_use_id = block.get("tool_use_id", "")
172+
if tool_use_id:
173+
tool_id_to_original[tool_use_id] = tool_use_result.get(
174+
"originalFile"
175+
)
176+
155177
for entry in loglines:
156178
timestamp = entry.get("timestamp", "")
157179
message = entry.get("message", {})
@@ -199,6 +221,9 @@ def extract_file_operations(
199221
replace_all = tool_input.get("replace_all", False)
200222

201223
if file_path and old_string is not None and new_string is not None:
224+
# Get original file content if available from tool result
225+
original_content = tool_id_to_original.get(tool_id)
226+
202227
operations.append(
203228
FileOperation(
204229
file_path=file_path,
@@ -210,6 +235,7 @@ def extract_file_operations(
210235
old_string=old_string,
211236
new_string=new_string,
212237
replace_all=replace_all,
238+
original_content=original_content,
213239
)
214240
)
215241

@@ -544,6 +570,11 @@ def build_file_history_repo(
544570
except Exception:
545571
pass
546572

573+
# Fallback: use original_content from tool result (for remote sessions)
574+
if not fetched and op.original_content:
575+
full_path.write_text(op.original_content)
576+
fetched = True
577+
547578
# Commit the initial content first (no metadata = pre-session)
548579
# This allows git blame to correctly attribute unchanged lines
549580
if fetched:

tests/test_code_view.py

Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -221,6 +221,165 @@ def test_no_tool_calls(self):
221221
operations = extract_file_operations(loglines, conversations)
222222
assert operations == []
223223

224+
def test_extracts_original_file_content_for_edit(self):
225+
"""Test that originalFile from toolUseResult is extracted for Edit operations.
226+
227+
This enables file reconstruction for remote sessions without local file access.
228+
"""
229+
original_content = "def add(a, b):\n return a + b\n"
230+
231+
loglines = [
232+
# User prompt
233+
{
234+
"type": "user",
235+
"timestamp": "2025-12-24T10:00:00.000Z",
236+
"message": {"content": "Edit the file", "role": "user"},
237+
},
238+
# Assistant makes an Edit
239+
{
240+
"type": "assistant",
241+
"timestamp": "2025-12-24T10:00:05.000Z",
242+
"message": {
243+
"role": "assistant",
244+
"content": [
245+
{
246+
"type": "tool_use",
247+
"id": "toolu_edit_001",
248+
"name": "Edit",
249+
"input": {
250+
"file_path": "/project/math.py",
251+
"old_string": "return a + b",
252+
"new_string": "return a + b # sum",
253+
},
254+
}
255+
],
256+
},
257+
},
258+
# Tool result with originalFile in toolUseResult
259+
{
260+
"type": "user",
261+
"timestamp": "2025-12-24T10:00:10.000Z",
262+
"toolUseResult": {"originalFile": original_content},
263+
"message": {
264+
"role": "user",
265+
"content": [
266+
{
267+
"type": "tool_result",
268+
"tool_use_id": "toolu_edit_001",
269+
"content": "File edited successfully",
270+
"is_error": False,
271+
}
272+
],
273+
},
274+
},
275+
]
276+
277+
conversations = [
278+
{
279+
"user_text": "Edit the file",
280+
"timestamp": "2025-12-24T10:00:00.000Z",
281+
"messages": [
282+
(
283+
"user",
284+
'{"content": "Edit the file", "role": "user"}',
285+
"2025-12-24T10:00:00.000Z",
286+
),
287+
(
288+
"assistant",
289+
'{"content": [{"type": "tool_use", "id": "toolu_edit_001", "name": "Edit", "input": {}}], "role": "assistant"}',
290+
"2025-12-24T10:00:05.000Z",
291+
),
292+
(
293+
"user",
294+
'{"content": [{"type": "tool_result", "tool_use_id": "toolu_edit_001"}], "role": "user"}',
295+
"2025-12-24T10:00:10.000Z",
296+
),
297+
],
298+
}
299+
]
300+
301+
operations = extract_file_operations(loglines, conversations)
302+
303+
# Should have one Edit operation
304+
assert len(operations) == 1
305+
op = operations[0]
306+
assert op.operation_type == "edit"
307+
assert op.file_path == "/project/math.py"
308+
assert op.old_string == "return a + b"
309+
assert op.new_string == "return a + b # sum"
310+
# original_content should be populated from toolUseResult.originalFile
311+
assert op.original_content == original_content
312+
313+
def test_original_file_not_set_for_write(self):
314+
"""Test that original_content is not set for Write operations (only Edit)."""
315+
loglines = [
316+
{
317+
"type": "user",
318+
"timestamp": "2025-12-24T10:00:00.000Z",
319+
"message": {"content": "Create a file", "role": "user"},
320+
},
321+
{
322+
"type": "assistant",
323+
"timestamp": "2025-12-24T10:00:05.000Z",
324+
"message": {
325+
"role": "assistant",
326+
"content": [
327+
{
328+
"type": "tool_use",
329+
"id": "toolu_write_001",
330+
"name": "Write",
331+
"input": {
332+
"file_path": "/project/new.py",
333+
"content": "print('hello')\n",
334+
},
335+
}
336+
],
337+
},
338+
},
339+
{
340+
"type": "user",
341+
"timestamp": "2025-12-24T10:00:10.000Z",
342+
"message": {
343+
"role": "user",
344+
"content": [
345+
{
346+
"type": "tool_result",
347+
"tool_use_id": "toolu_write_001",
348+
"content": "File written",
349+
"is_error": False,
350+
}
351+
],
352+
},
353+
},
354+
]
355+
356+
conversations = [
357+
{
358+
"user_text": "Create a file",
359+
"timestamp": "2025-12-24T10:00:00.000Z",
360+
"messages": [
361+
(
362+
"user",
363+
'{"content": "Create a file", "role": "user"}',
364+
"2025-12-24T10:00:00.000Z",
365+
),
366+
(
367+
"assistant",
368+
'{"content": [], "role": "assistant"}',
369+
"2025-12-24T10:00:05.000Z",
370+
),
371+
],
372+
}
373+
]
374+
375+
operations = extract_file_operations(loglines, conversations)
376+
377+
assert len(operations) == 1
378+
op = operations[0]
379+
assert op.operation_type == "write"
380+
# Write operations don't use original_content
381+
assert op.original_content is None
382+
224383

225384
class TestBuildFileTree:
226385
"""Tests for the build_file_tree function."""

0 commit comments

Comments
 (0)