22
33from __future__ import annotations
44
5- from collections .abc import Callable
5+ from collections .abc import Callable , Sequence
66import dataclasses
7- from datetime import timedelta
7+ from datetime import datetime , timedelta
88import logging
99import re
1010import textwrap
1111import time
1212from typing import Literal
1313
1414from .bots import ActionSummary , Bot , Goal
15- from .common import qualified_class_name , reindent
15+ from .common import (
16+ UnreachableError ,
17+ now ,
18+ qualified_class_name ,
19+ reindent ,
20+ tagged ,
21+ )
1622from .events import (
1723 Event ,
1824 EventConsumer ,
25+ event_decoders ,
1926 event_encoder ,
2027 feedback_events ,
2128 worktree_events ,
@@ -46,8 +53,17 @@ def ref(self) -> str:
4653 return _draft_ref (self .folio .id , self .seqno )
4754
4855
56+ _DRAFT_REF_PREFIX = "refs/drafts/"
57+
58+
4959def _draft_ref (folio_id : int , suffix : int | str ) -> str :
50- return f"refs/drafts/{ folio_id } /{ suffix } "
60+ return f"{ _DRAFT_REF_PREFIX } { folio_id } /{ suffix } "
61+
62+
63+ def _parse_draft_ref (ref : str ) -> tuple [int , int | None ]:
64+ ref = ref .removeprefix (_DRAFT_REF_PREFIX )
65+ parts = ref .split ("/" )
66+ return int (parts [0 ]), int (parts [1 ]) if len (parts ) > 1 else None
5167
5268
5369_FOLIO_BRANCH_NAMESPACE = "draft"
@@ -70,7 +86,7 @@ def upstream_branch_name(self) -> str:
7086 return self .branch_name () + _FOLIO_UPSTREAM_BRANCH_SUFFIX
7187
7288
73- def _active_folio (repo : Repo ) -> Folio | None :
89+ def _maybe_active_folio (repo : Repo ) -> Folio | None :
7490 active_branch = repo .active_branch ()
7591 if not active_branch :
7692 return None
@@ -80,6 +96,13 @@ def _active_folio(repo: Repo) -> Folio | None:
8096 return Folio (int (match [1 ]))
8197
8298
99+ def _active_folio (repo : Repo ) -> Folio :
100+ folio = _maybe_active_folio (repo )
101+ if not folio :
102+ raise RuntimeError ("Not currently on a draft branch" )
103+ return folio
104+
105+
83106#: Select ort strategies.
84107DraftMergeStrategy = Literal [
85108 "ours" ,
@@ -133,7 +156,7 @@ async def generate_draft(
133156 )
134157
135158 # Ensure that we are in a folio.
136- folio = _active_folio (self ._repo )
159+ folio = _maybe_active_folio (self ._repo )
137160 if not folio :
138161 folio = self ._create_folio ()
139162 with self ._store .cursor () as cursor :
@@ -149,7 +172,7 @@ async def generate_draft(
149172 # Run the bot to generate the change.
150173 event_recorder = _EventRecorder (self ._progress )
151174 with self ._progress .spinner ("Running bot..." ) as spinner :
152- feedback = spinner .feedback ()
175+ feedback = spinner .feedback (event_recorder )
153176 change = await self ._generate_change (
154177 bot ,
155178 Goal (prompt_contents ),
@@ -206,11 +229,11 @@ async def generate_draft(
206229 [
207230 {
208231 "prompt_id" : prompt_id ,
209- "occurred_at" : e . at ,
232+ "occurred_at" : dt ,
210233 "class" : e .__class__ .__name__ ,
211234 "data" : encoder .encode (e ),
212235 }
213- for e in event_recorder .events
236+ for ( dt , e ) in event_recorder .events ()
214237 ],
215238 )
216239 spinner .update ("Created draft commit." , ref = draft .ref )
@@ -244,9 +267,6 @@ async def generate_draft(
244267
245268 def quit_folio (self ) -> None :
246269 folio = _active_folio (self ._repo )
247- if not folio :
248- raise RuntimeError ("Not currently on a draft branch" )
249-
250270 with self ._store .cursor () as cursor :
251271 rows = cursor .execute (sql ("get-folio-by-id" ), {"id" : folio .id })
252272 if not rows :
@@ -404,7 +424,7 @@ def _commit_tree(
404424
405425 def latest_draft_prompt (self ) -> str | None :
406426 """Returns the latest prompt for the current draft"""
407- folio = _active_folio (self ._repo )
427+ folio = _maybe_active_folio (self ._repo )
408428 if not folio :
409429 return None
410430 with self ._store .cursor () as cursor :
@@ -422,6 +442,27 @@ def latest_draft_prompt(self) -> str | None:
422442 prompt = "\n \n " .join ([prompt , reindent (question , prefix = "> " )])
423443 return prompt
424444
445+ def list_draft_events (self , draft_ref : str | None = None ) -> Sequence [str ]:
446+ if draft_ref :
447+ folio_id , seqno = _parse_draft_ref (draft_ref )
448+ else :
449+ folio = _active_folio (self ._repo )
450+ folio_id = folio .id
451+ seqno = None
452+ elems = []
453+ with self ._store .cursor () as cursor :
454+ rows = cursor .execute (
455+ sql ("list-action-events" ),
456+ {"folio_id" : folio_id , "seqno" : seqno },
457+ )
458+ decoders = event_decoders ()
459+ for row in rows :
460+ occurred_at , class_name , data = row
461+ event = decoders [class_name ].decode (data )
462+ description = _format_event (event )
463+ elems .append (f"{ occurred_at } \t { class_name } \t { description } " )
464+ return elems
465+
425466
426467@dataclasses .dataclass (frozen = True )
427468class _Change :
@@ -442,34 +483,50 @@ class _EventRecorder(EventConsumer):
442483 """
443484
444485 def __init__ (self , progress : Progress ) -> None :
445- self .events = list [Event ]()
486+ self ._events = list [tuple [ datetime , Event ] ]()
446487 self ._progress = progress
447488
489+ def events (self ) -> Sequence [tuple [datetime , Event ]]:
490+ return sorted (list (self ._events ))
491+
448492 def on_event (self , event : Event ) -> None :
449- self .events .append (event )
450- match event :
451- case worktree_events .ListFiles (_, paths ):
452- self ._progress .report ("Listed files." , count = len (paths ))
453- case worktree_events .ReadFile (_, path , contents ):
454- size = - 1 if contents is None else len (contents )
455- self ._progress .report (f"Read { path } ." , length = size )
456- case worktree_events .WriteFile (_, path , contents ):
457- size = len (contents )
458- self ._progress .report (f"Wrote { path } ." , length = size )
459- case worktree_events .DeleteFile (_, path ):
460- self ._progress .report (f"Deleted { path } ." )
461- case worktree_events .RenameFile (_, src_path , dst_path ):
462- self ._progress .report (f"Renamed { src_path } to { dst_path } ." )
463- case worktree_events .StartEditingFiles (_):
464- self ._progress .report ("Started editing files..." )
465- case worktree_events .StopEditingFiles (_):
466- self ._progress .report ("Stopped editing files." )
467- case (
468- feedback_events .NotifyUser (_, _)
469- | feedback_events .RequestUserGuidance (_, _)
470- | feedback_events .ReceiveUserGuidance (_, _)
471- ):
472- pass
493+ self ._events .append ((now (), event ))
494+ if formatted := _format_internal_event (event ):
495+ self ._progress .report (formatted )
496+
497+
498+ def _format_internal_event (event : Event ) -> str :
499+ match event :
500+ case worktree_events .ListFiles (path_count ):
501+ return f"Listed { path_count } files."
502+ case worktree_events .ReadFile (path , char_count ):
503+ return tagged (f"Read { path } ." , length = char_count )
504+ case worktree_events .WriteFile (path , char_count ):
505+ return tagged (f"Wrote { path } ." , length = char_count )
506+ case worktree_events .DeleteFile (path ):
507+ return f"Deleted { path } ."
508+ case worktree_events .RenameFile (src_path , dst_path ):
509+ return f"Renamed { src_path } to { dst_path } ."
510+ case worktree_events .StartEditingFiles ():
511+ return "Started editing files..."
512+ case worktree_events .StopEditingFiles ():
513+ return "Stopped editing files."
514+ case _:
515+ return ""
516+
517+
518+ def _format_event (event : Event ) -> str :
519+ if formatted := _format_internal_event (event ):
520+ return formatted
521+ match event :
522+ case feedback_events .NotifyUser (update ):
523+ return update
524+ case feedback_events .RequestUserGuidance (question ):
525+ return question
526+ case feedback_events .ReceiveUserGuidance (answer ):
527+ return answer
528+ case _:
529+ raise UnreachableError ()
473530
474531
475532def _default_title (prompt : str ) -> str :
0 commit comments