
    Pmj3$                        d Z ddlmZ ddlZddlmZmZ ddlmZm	Z	m
Z
mZmZ dZdZefddZe G d d                      Z G d d          ZdZdZdZdZdZ	 dddZdS ) u  Ctrl+S prompt stash — pure state machine for the classic CLI composer.

Park a half-written prompt, send something else, then bring the draft back.
Mirrors Claude Code's ``ctrl + s to stash prompt`` affordance.

The state machine lives here (no prompt_toolkit imports) so it can be unit
tested directly; ``cli.py`` owns only the keybinding and the rendering.

Gesture
-------
- Buffer has content  → push it onto the stash, clear the composer.
- Buffer empty, 1 item → pop it straight back into the composer.
- Buffer empty, 2+ items → open the browse panel (↑↓ / Enter / D / Esc).

Newest-first ordering: index 0 is always the most recently stashed draft, so
the common "undo my last Ctrl+S" case is a single keystroke.

Nothing is written to disk. Drafts frequently contain credentials, prompts
under NDA, or pasted secrets, and a session-scoped stash keeps that material
in memory only. Callers that later want cross-restart persistence must route
through ``get_hermes_home()`` rather than hardcoding ``~/.hermes``.
    )annotationsN)	dataclassfield)AnyListOptionalSequenceTuple<      textstrwidthintreturnc                V   | sdS |                      dd                               dd          }|                     dd                               dd          }d                    |                                          }|dk    r#t          |          |k    r|d	|dz
           d
z   S |S )u   Collapse a possibly multi-line draft into one preview line.

    Newlines and tabs become ``⏎``/space so a 40-line draft still renders as a
    single panel row, and the result is ellipsized to ``width`` display chars.
     z

u    ⏎ 	    Nu   …)replacejoinsplitlen)r   r   flats      =/home/thesage/.hermes/hermes-agent/hermes_cli/prompt_stash.pybuild_previewr   %   s      r<<%%--dD99D<<g&&..tS99D88DJJLL!!DqyySYY&&KeaiK 5((K    c                  d    e Zd ZU dZded<    ee          Zded<   dZded	<   d
Z	ded<   ddZ
dS )
StashEntryz@One parked draft: exact text plus any images that were attached.r   r   )default_factoryz	List[Any]imagesg        float
stashed_atr   previewr   dictc                R    | j         t          | j                  | j        | j        dS )z?Render in the shape ``HermesCLI._render_stash_panel`` consumes.r   r$   r&   r'   )r   listr$   r&   r'   selfs    r   as_dictzStashEntry.as_dict>   s/     I4;''/|	
 
 	
r    N)r   r(   )__name__
__module____qualname____doc____annotations__r   r+   r$   r&   r'   r.    r    r   r"   r"   5   sy         JJIIId333F3333JG
 
 
 
 
 
r    r"   c                      e Zd ZdZeddd&dZd'dZd(d
Zed)d            Z	d*dZ
d+dZd+dZd,d-dZd.d/dZd.d0dZd1dZd2dZd(d Zd1d!Zd3d#Zd(d$Zd4d%ZdS )5PromptStashzSession-scoped stack of parked composer drafts.

    Pure state: no I/O, no prompt_toolkit, no global clock beyond
    ``time.monotonic`` (injectable for tests via ``clock``).
    N)	max_itemsclockr7   r   c                   g | _         t          dt          |                    | _        |pt          j        | _        d| _        d| _        d S )Nr   Fr   )	_itemsmaxr   
_max_itemstime	monotonic_clock
panel_openpanel_cursor)r-   r7   r8   s      r   __init__zPromptStash.__init__O   sC    (*aY00-t~r    r   c                *    t          | j                  S N)r   r:   r,   s    r   __len__zPromptStash.__len__X   s    4;r    boolc                *    t          | j                  S rD   )rF   r:   r,   s    r   __bool__zPromptStash.__bool__[   s    DK   r    List[StashEntry]c                *    t          | j                  S )u=   Newest-first list of entries (a copy — mutate via the API).)r+   r:   r,   s    r   itemszPromptStash.items_   s     DK   r    
List[dict]c                $    d | j         D             S )z.Entries as plain dicts for the panel renderer.c                6    g | ]}|                                 S r4   )r.   ).0es     r   
<listcomp>z*PromptStash.panel_rows.<locals>.<listcomp>f   s     111		111r    )r:   r,   s    r   
panel_rowszPromptStash.panel_rowsd   s    11T[1111r    r   c                V    t          | j                  }|sdS | j        rd| dnd| S )u   Status-bar indicator, or ``""`` when the stash is empty.

        ``📌 2`` when idle, ``📌 2 ▲`` while the browse panel is open, so the
        user can always tell a parked draft exists without opening anything.
        r   u   📌 u    ▲)r   r:   r@   r-   ns     r   	indicatorzPromptStash.indicatorh   s?      	2"&/Bq{q{{Br    c                t    t          | j                  }|sdS |dk    rd| j        d         j         S d| dS )z8Composer placeholder text advertising the stashed draft.r   r   zCtrl+S to restore: r   zCtrl+S to browse z stashed drafts)r   r:   r'   rT   s     r   placeholder_hintzPromptStash.placeholder_hints   sO     	266AQ)?AAA515555r    r   r$   Optional[Sequence[Any]]c                V   t          |          }|pd                                s|sdS t          |pdt          |pg           |                                 t          |pd          pd          }| j                            d|           | j        | j        d= d| _	        d| _
        dS )u  Push a draft. Returns False (no-op) for a blank buffer.

        A buffer that is empty or whitespace-only is not worth parking and
        must stay a no-op, otherwise Ctrl+S on an empty composer would push a
        junk entry instead of triggering the restore half of the gesture.
        Text is stored verbatim — leading/trailing whitespace and newlines are
        preserved so a restore round-trips byte-for-byte.
        r   Fz(images only)r*   r   NT)rF   stripr"   r+   r?   r   r:   insertr<   r@   rA   )r-   r   r$   
has_imagesentrys        r   stashzPromptStash.stash~   s     &\\

!!## 	J 	5"%%{{}}!$*"--@	
 
 
 	1e$$$K(()tr    r   indexOptional[Tuple[str, List[Any]]]c                   | j         r!d|cxk    rt          | j                   k     sn dS | j                             |          }| j         sd| _        |                     | j                  | _        |j        t          |j                  fS )z;Remove and return ``(text, images)`` at ``index``, or None.r   NF)	r:   r   popr@   _clamp_cursorrA   r   r+   r$   )r-   r`   r^   s      r   rc   zPromptStash.pop   s    { 	1#@#@#@#@DK0@0@#@#@#@#@4&&{ 	$#DO ..t/@AAz4----r    Optional[StashEntry]c                p    | j         r!d|cxk    rt          | j                   k     sn dS | j         |         S )z2Return the entry at ``index`` without removing it.r   N)r:   r   )r-   r`   s     r   peekzPromptStash.peek   sF    { 	1#@#@#@#@DK0@0@#@#@#@#@4{5!!r    Nonec                T    | j                                          d| _        d| _        d S NFr   )r:   clearr@   rA   r,   s    r   rk   zPromptStash.clear   s+    r    valuec           	         | j         sdS t          dt          t          |          t	          | j                   dz
                      S )Nr   r   )r:   r;   minr   r   )r-   rl   s     r   rd   zPromptStash._clamp_cursor   s@    { 	11c#e**c$+&6&6&:;;<<<r    c                4    | j         sdS d| _        d| _        dS )z=Open the browse panel. False when there is nothing to browse.FTr   )r:   r@   rA   r,   s    r   
open_panelzPromptStash.open_panel   s%    { 	5tr    c                "    d| _         d| _        d S rj   )r@   rA   r,   s    r   close_panelzPromptStash.close_panel   s    r    deltac                n    |                      | j        t          |          z             | _        | j        S )z2Move the panel cursor, clamped to the list bounds.)rd   rA   r   )r-   rs   s     r   move_cursorzPromptStash.move_cursor   s/     ..t/@3u::/MNN  r    c                    | j         sdS |                     | j                  }| j                             |           | j         sd| _        d| _        n|                     |          | _        dS )zCDelete the highlighted entry. False when there was nothing to drop.Fr   T)r:   rd   rA   rc   r@   )r-   idxs     r   delete_at_cursorzPromptStash.delete_at_cursor   sr    { 	5  !233{ 	8#DO !D $ 2 23 7 7Dtr    c                    | j         sdS |                     |                     | j                            }|                                  |S )z.Pop the highlighted entry and close the panel.N)r:   rc   rd   rA   rr   )r-   results     r   restore_at_cursorzPromptStash.restore_at_cursor   sJ    { 	4$,,T->??@@r    )r7   r   )r   r   )r   rF   )r   rI   )r   rL   )r   r   rD   )r   r   r$   rY   r   rF   )r   )r`   r   r   ra   )r`   r   r   re   )r   rh   )rl   r   r   r   )rs   r   r   r   )r   ra   )r/   r0   r1   r2   MAX_STASH_ITEMSrB   rE   rH   propertyrK   rR   rV   rX   r_   rc   rg   rk   rd   rp   rr   ru   rx   r{   r4   r    r   r6   r6   H   s         ,;$             ! ! ! ! ! ! ! X!2 2 2 2	C 	C 	C 	C6 6 6 6    6. . . . ." " " " "   = = = =
      ! ! ! !
        r    r6   noopstashedrestoredrp   rr   r_   buffer_textr$   rY   +Tuple[str, Optional[Tuple[str, List[Any]]]]c                   | j         r|                                  t          dfS |pd                                s|r(|                     ||          r	t
          dfS t          dfS t          |           }|dk    r	t          dfS |dk    rt          | 	                    d          fS | 
                                 t          dfS )a  Decide what one Ctrl+S press does. Returns ``(action, payload)``.

    ``payload`` carries ``(text, images)`` for :data:`ACTION_RESTORED`, else
    None. This is the whole decision table in one pure function so the
    keybinding handler in ``cli.py`` stays a thin adapter.
    Nr   r   r   )r@   rr   ACTION_CLOSE_PANELr[   r_   ACTION_STASHEDACTION_NOOPr   ACTION_RESTOREDrc   rp   ACTION_OPEN_PANEL)r_   r   r$   counts       r   resolve_ctrl_sr      s      (!4'' 	r  "" !f !;;{F++ 	(!4''D   JJEzzD  zz		!,,	d""r    )r   r   r   r   r   r   rD   )r_   r6   r   r   r$   rY   r   r   )r2   
__future__r   r=   dataclassesr   r   typingr   r   r   r	   r
   PREVIEW_WIDTHr|   r   r"   r6   r   r   r   r   r   r   r4   r    r   <module>r      sD   . # " " " " "  ( ( ( ( ( ( ( ( 7 7 7 7 7 7 7 7 7 7 7 7 7 7   +8       
 
 
 
 
 
 
 
$Q Q Q Q Q Q Q Qn   "  '+# # # # # # #r    