§
    øžyj3$  ã                  óÂ   — 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/ragecks/.hermes/hermes-agent/hermes_cli/prompt_stash.pyÚbuild_previewr   %   s¦   € ð ð ØˆrØ�<Š<˜ Ñ%Ô%×-Ò-¨d°DÑ9Ô9€DØ�<Š<˜˜gÑ&Ô&×.Ò.¨t°SÑ9Ô9€DØ�8Š8�D—J’J‘L”LÑ!Ô!€DØˆq‚y€y•S˜‘Y”Y Ò&Ð&Ø�K�e˜a‘i�KÔ  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à€I€I�IØ˜¨dÐ3Ñ3Ô3€FÐ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ÚPromptStashz´Session-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   € å�D”KÑ Ô Ð 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   € õ �D”KÑ Ô Ð 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Ð1 T¤[Ð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Ø�Š6ˆ6ØA¨¬°Q¬Ô)?ÐAÐAÐAØ5 1Ð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åØ�˜Ý˜˜ "Ñ%Ô%Ø—{’{‘}”}Ý! $ *¨"Ñ-Ô-Ð@°ð	
ñ 
ô 
ˆð 	Œ×Ò˜1˜eÑ$Ô$Ð$àŒ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¨Ð#@Ð#@Ò#@Ð#@µ°D´KÑ0@Ô0@Ò#@Ð#@Ð#@Ð#@Ø�4Ø”—’ Ñ&Ô&ˆØŒ{ð 	$Ø#ˆDŒOØ ×.Ò.¨tÔ/@ÑAÔAˆÔØŒz�4 ¤Ñ-Ô-Ð-Ð-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¨Ð#@Ð#@Ò#@Ð#@µ°D´KÑ0@Ô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Ý�1•c�#˜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Ô/@Å3ÀuÁ:Ä:Ñ/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Ø#ˆDŒOØ !ˆDÔÐà $× 2Ò 2°3Ñ 7Ô 7ˆDÔØˆ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Ð Ð õ �‰JŒJ€EØ�‚z€zÝ˜DÐ Ð Ø�‚z€zÝ §	¢	¨!¡¤Ð,Ð,Ø	×ÒÑÔÐÝ˜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    