npcsh 1.1.20__py3-none-any.whl → 1.1.22__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (186) hide show
  1. npcsh/_state.py +15 -76
  2. npcsh/benchmark/npcsh_agent.py +22 -14
  3. npcsh/benchmark/templates/install-npcsh.sh.j2 +2 -2
  4. npcsh/diff_viewer.py +3 -3
  5. npcsh/mcp_server.py +9 -1
  6. npcsh/npc_team/alicanto.npc +12 -6
  7. npcsh/npc_team/corca.npc +0 -1
  8. npcsh/npc_team/frederic.npc +2 -3
  9. npcsh/npc_team/jinxs/lib/core/compress.jinx +373 -85
  10. npcsh/npc_team/jinxs/lib/core/edit_file.jinx +83 -61
  11. npcsh/npc_team/jinxs/lib/core/search/db_search.jinx +17 -6
  12. npcsh/npc_team/jinxs/lib/core/search/file_search.jinx +17 -6
  13. npcsh/npc_team/jinxs/lib/core/search/web_search.jinx +52 -14
  14. npcsh/npc_team/jinxs/{bin → lib/utils}/benchmark.jinx +2 -2
  15. npcsh/npc_team/jinxs/{bin → lib/utils}/jinxs.jinx +12 -12
  16. npcsh/npc_team/jinxs/{bin → lib/utils}/models.jinx +7 -7
  17. npcsh/npc_team/jinxs/{bin → lib/utils}/setup.jinx +6 -6
  18. npcsh/npc_team/jinxs/modes/alicanto.jinx +1633 -295
  19. npcsh/npc_team/jinxs/modes/arxiv.jinx +5 -5
  20. npcsh/npc_team/jinxs/modes/build.jinx +378 -0
  21. npcsh/npc_team/jinxs/modes/config_tui.jinx +300 -0
  22. npcsh/npc_team/jinxs/modes/convene.jinx +597 -0
  23. npcsh/npc_team/jinxs/modes/corca.jinx +777 -387
  24. npcsh/npc_team/jinxs/modes/git.jinx +795 -0
  25. {npcsh-1.1.20.data/data/npcsh/npc_team → npcsh/npc_team/jinxs/modes}/kg.jinx +82 -15
  26. npcsh/npc_team/jinxs/modes/memories.jinx +414 -0
  27. npcsh/npc_team/jinxs/{bin → modes}/nql.jinx +10 -21
  28. npcsh/npc_team/jinxs/modes/papers.jinx +578 -0
  29. npcsh/npc_team/jinxs/modes/plonk.jinx +503 -308
  30. npcsh/npc_team/jinxs/modes/reattach.jinx +3 -3
  31. npcsh/npc_team/jinxs/modes/spool.jinx +3 -3
  32. npcsh/npc_team/jinxs/{bin → modes}/team.jinx +12 -12
  33. npcsh/npc_team/jinxs/modes/vixynt.jinx +388 -0
  34. npcsh/npc_team/jinxs/modes/wander.jinx +454 -181
  35. npcsh/npc_team/jinxs/modes/yap.jinx +630 -182
  36. npcsh/npc_team/kadiefa.npc +2 -1
  37. npcsh/npc_team/sibiji.npc +3 -3
  38. npcsh/npcsh.py +112 -47
  39. npcsh/routes.py +4 -1
  40. npcsh/salmon_simulation.py +0 -0
  41. npcsh-1.1.22.data/data/npcsh/npc_team/alicanto.jinx +1694 -0
  42. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/alicanto.npc +12 -6
  43. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/arxiv.jinx +5 -5
  44. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/benchmark.jinx +2 -2
  45. npcsh-1.1.22.data/data/npcsh/npc_team/build.jinx +378 -0
  46. npcsh-1.1.22.data/data/npcsh/npc_team/compress.jinx +428 -0
  47. npcsh-1.1.22.data/data/npcsh/npc_team/config_tui.jinx +300 -0
  48. npcsh-1.1.22.data/data/npcsh/npc_team/corca.jinx +820 -0
  49. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/corca.npc +0 -1
  50. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/db_search.jinx +17 -6
  51. npcsh-1.1.22.data/data/npcsh/npc_team/edit_file.jinx +119 -0
  52. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/file_search.jinx +17 -6
  53. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/frederic.npc +2 -3
  54. npcsh-1.1.22.data/data/npcsh/npc_team/git.jinx +795 -0
  55. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/jinxs.jinx +12 -12
  56. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/kadiefa.npc +2 -1
  57. {npcsh/npc_team/jinxs/bin → npcsh-1.1.22.data/data/npcsh/npc_team}/kg.jinx +82 -15
  58. npcsh-1.1.22.data/data/npcsh/npc_team/memories.jinx +414 -0
  59. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/models.jinx +7 -7
  60. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/nql.jinx +10 -21
  61. npcsh-1.1.22.data/data/npcsh/npc_team/papers.jinx +578 -0
  62. npcsh-1.1.22.data/data/npcsh/npc_team/plonk.jinx +574 -0
  63. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/reattach.jinx +3 -3
  64. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/setup.jinx +6 -6
  65. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/sibiji.npc +3 -3
  66. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/spool.jinx +3 -3
  67. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/team.jinx +12 -12
  68. npcsh-1.1.22.data/data/npcsh/npc_team/vixynt.jinx +388 -0
  69. npcsh-1.1.22.data/data/npcsh/npc_team/wander.jinx +728 -0
  70. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/web_search.jinx +52 -14
  71. npcsh-1.1.22.data/data/npcsh/npc_team/yap.jinx +716 -0
  72. {npcsh-1.1.20.dist-info → npcsh-1.1.22.dist-info}/METADATA +246 -281
  73. npcsh-1.1.22.dist-info/RECORD +240 -0
  74. npcsh-1.1.22.dist-info/entry_points.txt +11 -0
  75. npcsh/npc_team/jinxs/bin/config_tui.jinx +0 -300
  76. npcsh/npc_team/jinxs/bin/memories.jinx +0 -317
  77. npcsh/npc_team/jinxs/bin/vixynt.jinx +0 -122
  78. npcsh/npc_team/jinxs/lib/core/search/kg_search.jinx +0 -418
  79. npcsh/npc_team/jinxs/lib/core/search/mem_review.jinx +0 -73
  80. npcsh/npc_team/jinxs/lib/core/search/mem_search.jinx +0 -388
  81. npcsh/npc_team/jinxs/lib/core/search.jinx +0 -54
  82. npcsh/npc_team/jinxs/lib/research/paper_search.jinx +0 -412
  83. npcsh/npc_team/jinxs/lib/research/semantic_scholar.jinx +0 -386
  84. npcsh/npc_team/jinxs/lib/utils/build.jinx +0 -65
  85. npcsh/npc_team/plonkjr.npc +0 -23
  86. npcsh-1.1.20.data/data/npcsh/npc_team/alicanto.jinx +0 -356
  87. npcsh-1.1.20.data/data/npcsh/npc_team/build.jinx +0 -65
  88. npcsh-1.1.20.data/data/npcsh/npc_team/compress.jinx +0 -140
  89. npcsh-1.1.20.data/data/npcsh/npc_team/config_tui.jinx +0 -300
  90. npcsh-1.1.20.data/data/npcsh/npc_team/corca.jinx +0 -430
  91. npcsh-1.1.20.data/data/npcsh/npc_team/edit_file.jinx +0 -97
  92. npcsh-1.1.20.data/data/npcsh/npc_team/kg_search.jinx +0 -418
  93. npcsh-1.1.20.data/data/npcsh/npc_team/mem_review.jinx +0 -73
  94. npcsh-1.1.20.data/data/npcsh/npc_team/mem_search.jinx +0 -388
  95. npcsh-1.1.20.data/data/npcsh/npc_team/memories.jinx +0 -317
  96. npcsh-1.1.20.data/data/npcsh/npc_team/paper_search.jinx +0 -412
  97. npcsh-1.1.20.data/data/npcsh/npc_team/plonk.jinx +0 -379
  98. npcsh-1.1.20.data/data/npcsh/npc_team/plonkjr.npc +0 -23
  99. npcsh-1.1.20.data/data/npcsh/npc_team/search.jinx +0 -54
  100. npcsh-1.1.20.data/data/npcsh/npc_team/semantic_scholar.jinx +0 -386
  101. npcsh-1.1.20.data/data/npcsh/npc_team/vixynt.jinx +0 -122
  102. npcsh-1.1.20.data/data/npcsh/npc_team/wander.jinx +0 -455
  103. npcsh-1.1.20.data/data/npcsh/npc_team/yap.jinx +0 -268
  104. npcsh-1.1.20.dist-info/RECORD +0 -248
  105. npcsh-1.1.20.dist-info/entry_points.txt +0 -25
  106. /npcsh/npc_team/jinxs/lib/{orchestration → core}/convene.jinx +0 -0
  107. /npcsh/npc_team/jinxs/lib/{orchestration → core}/delegate.jinx +0 -0
  108. /npcsh/npc_team/jinxs/{bin → lib/core}/sample.jinx +0 -0
  109. /npcsh/npc_team/jinxs/lib/{core → utils}/chat.jinx +0 -0
  110. /npcsh/npc_team/jinxs/lib/{core → utils}/cmd.jinx +0 -0
  111. /npcsh/npc_team/jinxs/{bin → lib/utils}/sync.jinx +0 -0
  112. /npcsh/npc_team/jinxs/{bin → modes}/roll.jinx +0 -0
  113. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/add_tab.jinx +0 -0
  114. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/alicanto.png +0 -0
  115. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/browser_action.jinx +0 -0
  116. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/browser_screenshot.jinx +0 -0
  117. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/chat.jinx +0 -0
  118. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/click.jinx +0 -0
  119. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/close_browser.jinx +0 -0
  120. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/close_pane.jinx +0 -0
  121. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/close_tab.jinx +0 -0
  122. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/cmd.jinx +0 -0
  123. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/compile.jinx +0 -0
  124. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/confirm.jinx +0 -0
  125. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/convene.jinx +0 -0
  126. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/corca.png +0 -0
  127. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/corca_example.png +0 -0
  128. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/delegate.jinx +0 -0
  129. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/focus_pane.jinx +0 -0
  130. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/frederic4.png +0 -0
  131. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/guac.jinx +0 -0
  132. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/guac.npc +0 -0
  133. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/guac.png +0 -0
  134. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/help.jinx +0 -0
  135. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/incognide.jinx +0 -0
  136. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/init.jinx +0 -0
  137. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/kadiefa.png +0 -0
  138. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/key_press.jinx +0 -0
  139. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/launch_app.jinx +0 -0
  140. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/list_panes.jinx +0 -0
  141. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/load_file.jinx +0 -0
  142. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/navigate.jinx +0 -0
  143. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/notify.jinx +0 -0
  144. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/npcsh.ctx +0 -0
  145. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/npcsh_sibiji.png +0 -0
  146. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/open_browser.jinx +0 -0
  147. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/open_pane.jinx +0 -0
  148. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/ots.jinx +0 -0
  149. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/paste.jinx +0 -0
  150. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/plonk.npc +0 -0
  151. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/plonk.png +0 -0
  152. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/plonkjr.png +0 -0
  153. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/pti.jinx +0 -0
  154. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/python.jinx +0 -0
  155. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/read_pane.jinx +0 -0
  156. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/roll.jinx +0 -0
  157. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/run_terminal.jinx +0 -0
  158. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/sample.jinx +0 -0
  159. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/screenshot.jinx +0 -0
  160. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/send_message.jinx +0 -0
  161. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/serve.jinx +0 -0
  162. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/set.jinx +0 -0
  163. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/sh.jinx +0 -0
  164. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/shh.jinx +0 -0
  165. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/sibiji.png +0 -0
  166. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/sleep.jinx +0 -0
  167. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/split_pane.jinx +0 -0
  168. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/spool.png +0 -0
  169. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/sql.jinx +0 -0
  170. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/switch.jinx +0 -0
  171. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/switch_npc.jinx +0 -0
  172. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/switch_tab.jinx +0 -0
  173. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/switches.jinx +0 -0
  174. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/sync.jinx +0 -0
  175. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/teamviz.jinx +0 -0
  176. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/trigger.jinx +0 -0
  177. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/type_text.jinx +0 -0
  178. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/usage.jinx +0 -0
  179. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/verbose.jinx +0 -0
  180. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/wait.jinx +0 -0
  181. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/write_file.jinx +0 -0
  182. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/yap.png +0 -0
  183. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/zen_mode.jinx +0 -0
  184. {npcsh-1.1.20.dist-info → npcsh-1.1.22.dist-info}/WHEEL +0 -0
  185. {npcsh-1.1.20.dist-info → npcsh-1.1.22.dist-info}/licenses/LICENSE +0 -0
  186. {npcsh-1.1.20.dist-info → npcsh-1.1.22.dist-info}/top_level.txt +0 -0
@@ -1,288 +1,166 @@
1
1
  jinx_name: plonk
2
- description: Vision-based GUI automation - use vision model to interact with screen elements
2
+ description: Vision-based GUI automation TUI - visual task management and action automation
3
+ interactive: true
3
4
  inputs:
4
5
  - task: null
5
6
  - vmodel: null
6
7
  - vprovider: null
7
8
  - max_iterations: 10
8
- - debug: true
9
- - browse: false
9
+ - debug: false
10
10
 
11
11
  steps:
12
- - name: plonk_execute
12
+ - name: plonk_tui
13
13
  engine: python
14
14
  code: |
15
15
  import os
16
16
  import sys
17
17
  import tty
18
18
  import termios
19
+ import select
19
20
  import time
21
+ import json
20
22
  import platform
21
- from termcolor import colored
23
+ from datetime import datetime
22
24
 
23
25
  from npcpy.llm_funcs import get_llm_response
24
- from npcpy.data.image import capture_screenshot
25
- from npcpy.work.desktop import perform_action
26
+
27
+ try:
28
+ from npcpy.data.image import capture_screenshot
29
+ from npcpy.work.desktop import perform_action
30
+ VISION_AVAILABLE = True
31
+ except ImportError:
32
+ VISION_AVAILABLE = False
26
33
 
27
34
  npc = context.get('npc')
28
35
  team = context.get('team')
29
36
  messages = context.get('messages', [])
30
37
 
31
- # Resolve npc if it's a string (npc name) rather than NPC object
32
38
  if isinstance(npc, str) and team:
33
39
  npc = team.get(npc) if hasattr(team, 'get') else None
34
40
  elif isinstance(npc, str):
35
41
  npc = None
36
42
 
37
- # ========== TUI Helper Functions ==========
38
- def get_terminal_size():
43
+ vision_model = context.get('vmodel') or (npc.model if npc and hasattr(npc, 'model') else None)
44
+ vision_provider = context.get('vprovider') or (npc.provider if npc and hasattr(npc, 'provider') else None)
45
+ max_iterations = int(context.get('max_iterations', 10))
46
+
47
+ system_name = platform.system()
48
+ if system_name == "Windows":
49
+ app_examples = "start firefox, notepad, calc"
50
+ elif system_name == "Darwin":
51
+ app_examples = "open -a Firefox, open -a TextEdit"
52
+ else:
53
+ app_examples = "firefox &, gedit &, gnome-calculator &"
54
+
55
+ json_schema_example = '{"action":"click","x":50,"y":30,"reason":"Click the URL bar"}'
56
+
57
+ # ========== State ==========
58
+ class PlonkState:
59
+ def __init__(self):
60
+ self.tasks = [] # [{"text":str, "status":"pending|running|done|failed", "actions":[]}]
61
+ self.sel = 0
62
+ self.scroll = 0
63
+ self.panel = 0 # 0=tasks, 1=actions
64
+ self.mode = 'idle' # idle, input, running, paused, step
65
+ self.input_buf = ""
66
+ self.input_cursor = 0
67
+ self.current_task = -1
68
+ self.iteration = 0
69
+ self.max_iter = max_iterations
70
+ self.status = "Ready"
71
+ self.action_scroll = 0
72
+ self.last_screenshot = ""
73
+
74
+ ui = PlonkState()
75
+
76
+ # ========== Helpers ==========
77
+ def get_size():
39
78
  try:
40
- size = os.get_terminal_size()
41
- return size.columns, size.lines
79
+ s = os.get_terminal_size()
80
+ return s.columns, s.lines
42
81
  except:
43
82
  return 80, 24
44
83
 
45
- def plonk_tui_browser(summary, click_history):
46
- """Interactive TUI browser for plonk action history"""
47
- if not summary and not click_history:
48
- print(colored("No actions recorded.", "yellow"))
84
+ def add_task(text):
85
+ ui.tasks.append({"text": text.strip(), "status": "pending", "actions": []})
86
+
87
+ def delete_task():
88
+ if ui.tasks and ui.mode == 'idle':
89
+ del ui.tasks[ui.sel]
90
+ if ui.sel >= len(ui.tasks):
91
+ ui.sel = max(0, len(ui.tasks) - 1)
92
+
93
+ def get_current_actions():
94
+ if ui.panel == 1 and 0 <= ui.sel < len(ui.tasks):
95
+ return ui.tasks[ui.sel].get('actions', [])
96
+ if ui.current_task >= 0 and ui.current_task < len(ui.tasks):
97
+ return ui.tasks[ui.current_task].get('actions', [])
98
+ return []
99
+
100
+ # ========== Automation ==========
101
+ def run_one_step():
102
+ if ui.current_task < 0 or ui.current_task >= len(ui.tasks):
103
+ ui.mode = 'idle'
104
+ ui.status = "No task"
49
105
  return
50
106
 
51
- width, height = get_terminal_size()
52
- selected = 0
53
- scroll = 0
54
- list_height = height - 5
55
- mode = 'summary' # summary or history
56
-
57
- fd = sys.stdin.fileno()
58
- old_settings = termios.tcgetattr(fd)
59
-
60
- try:
61
- tty.setcbreak(fd)
62
- sys.stdout.write('\033[?25l')
63
- sys.stdout.write('\033[2J\033[H')
64
-
65
- while True:
66
- width, height = get_terminal_size()
67
- list_height = height - 5
68
-
69
- items = summary if mode == 'summary' else click_history
70
-
71
- if not items:
72
- items = [{'action': 'none', 'reason': 'No actions'}]
73
-
74
- if selected >= len(items):
75
- selected = max(0, len(items) - 1)
76
-
77
- if selected < scroll:
78
- scroll = selected
79
- elif selected >= scroll + list_height:
80
- scroll = selected - list_height + 1
81
-
82
- sys.stdout.write('\033[H')
83
-
84
- # Tab bar
85
- summary_tab = '\033[44;37;1m Summary \033[0m' if mode == 'summary' else '\033[90m Summary \033[0m'
86
- history_tab = '\033[44;37;1m History \033[0m' if mode == 'history' else '\033[90m History \033[0m'
87
- header = f" PLONK ACTION BROWSER {summary_tab} {history_tab} "
88
- sys.stdout.write(f'\033[44;37;1m{" PLONK ACTION BROWSER".ljust(width)}\033[0m\n')
107
+ task = ui.tasks[ui.current_task]
89
108
 
90
- if mode == 'summary':
91
- col_header = f' {"STEP":<6} {"ACTION":<12} {"COORDS":<15} {"REASON":<40}'
92
- else:
93
- col_header = f' {"#":<4} {"ACTION":<10} {"X":<6} {"Y":<6} {"DETAILS":<40}'
94
- sys.stdout.write(f'\033[90m{col_header[:width]}\033[0m\n')
95
-
96
- for i in range(list_height):
97
- idx = scroll + i
98
- sys.stdout.write(f'\033[{3+i};1H\033[K')
99
- if idx >= len(items):
100
- continue
101
-
102
- item = items[idx]
103
-
104
- if mode == 'summary':
105
- step = item.get('iteration', idx + 1)
106
- action = item.get('action', '?')[:12]
107
- coords = item.get('last_click_coords', 'N/A')[:15]
108
- reason = item.get('reason', item.get('error', ''))[:40]
109
- line = f" {step:<6} {action:<12} {coords:<15} {reason}"
110
- else:
111
- action = item.get('action', '?')[:10]
112
- x = str(item.get('x', '-'))[:6]
113
- y = str(item.get('y', '-'))[:6]
114
- details = item.get('reason', item.get('text', item.get('key', item.get('command', ''))))[:40]
115
- line = f" {idx+1:<4} {action:<10} {x:<6} {y:<6} {details}"
116
-
117
- line = line[:width-1]
118
-
119
- # Color by action type
120
- action_type = item.get('action', '')
121
- if action_type == 'done':
122
- color = '\033[32m' # green
123
- elif action_type == 'fail':
124
- color = '\033[31m' # red
125
- elif action_type == 'click':
126
- color = '\033[33m' # yellow
127
- elif action_type == 'type':
128
- color = '\033[36m' # cyan
129
- else:
130
- color = ''
131
-
132
- if idx == selected:
133
- sys.stdout.write(f'\033[7;1m>{line.ljust(width-2)}\033[0m')
134
- elif color:
135
- sys.stdout.write(f'{color}{line}\033[0m')
136
- else:
137
- sys.stdout.write(f' {line}')
138
-
139
- # Status bar
140
- sys.stdout.write(f'\033[{height-2};1H\033[K\033[90m{"─" * width}\033[0m')
141
- stats = f"Actions: {len(summary)} | Clicks: {len([h for h in click_history if h.get('action') == 'click'])}"
142
- sys.stdout.write(f'\033[{height-1};1H\033[K {stats}'.ljust(width)[:width])
143
- sys.stdout.write(f'\033[{height};1H\033[K\033[44;37m j/k:Nav Tab:Toggle Enter:Details q:Quit [{selected+1}/{len(items)}] \033[0m')
144
-
145
- sys.stdout.flush()
146
-
147
- c = sys.stdin.read(1)
148
-
149
- if c == '\x1b':
150
- c2 = sys.stdin.read(1)
151
- if c2 == '[':
152
- c3 = sys.stdin.read(1)
153
- if c3 == 'A' and selected > 0:
154
- selected -= 1
155
- elif c3 == 'B' and selected < len(items) - 1:
156
- selected += 1
157
- else:
158
- return
159
- continue
160
-
161
- if c == 'q' or c == '\x03':
162
- return
163
- elif c == 'k' and selected > 0:
164
- selected -= 1
165
- elif c == 'j' and selected < len(items) - 1:
166
- selected += 1
167
- elif c == '\t': # Tab to toggle
168
- mode = 'history' if mode == 'summary' else 'summary'
169
- selected = 0
170
- scroll = 0
171
- elif c in ('\r', '\n'):
172
- # Show full details
173
- item = items[selected]
174
- details = "\n".join(f"{k}: {v}" for k, v in item.items())
175
- print(f'\033[2J\033[H{details}\n\nPress any key to continue...')
176
- sys.stdout.flush()
177
- sys.stdin.read(1)
178
- sys.stdout.write('\033[2J\033[H')
179
-
180
- finally:
181
- termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
182
- sys.stdout.write('\033[?25h')
183
- sys.stdout.write('\033[2J\033[H')
184
- sys.stdout.flush()
185
-
186
- task = context.get('task')
187
- vision_model = context.get('vmodel') or (npc.model if npc and hasattr(npc, 'model') else 'gpt-4o')
188
- vision_provider = context.get('vprovider') or (npc.provider if npc and hasattr(npc, 'provider') else 'openai')
189
- max_iterations = int(context.get('max_iterations', 10))
190
- debug = context.get('debug', True)
191
-
192
- if not task:
193
- context['output'] = """Usage: /plonk <task description>
194
-
195
- Options:
196
- --vmodel MODEL Vision model to use (default: gpt-4o)
197
- --vprovider PROV Vision provider (default: openai)
198
- --max-iterations N Max steps (default: 10)
109
+ if ui.iteration >= ui.max_iter:
110
+ task['status'] = 'failed'
111
+ task['actions'].append({"action": "fail", "reason": "Max iterations reached"})
112
+ advance_to_next_task()
113
+ return
199
114
 
200
- Example: /plonk Open Firefox and navigate to google.com"""
201
- context['messages'] = messages
202
- exit()
203
-
204
- print(f"""
205
- ██████╗ ██╗ ██████╗ ███╗ ██╗██╗ ██╗
206
- ██╔══██╗██║ ██╔═══██╗████╗ ██║██║ ██╔╝
207
- ██████╔╝██║ ██║ ██║██╔██╗ ██║█████╔╝
208
- ██╔═══╝ ██║ ██║ ██║██║╚██╗██║██╔═██╗
209
- ██║ ███████╗╚██████╔╝██║ ╚████║██║ ██╗
210
- ╚═╝ ╚══════╝ ╚═════╝ ╚═╝ ╚═══╝╚═╝ ╚═╝
211
-
212
- Vision GUI Automation
213
- Task: {task}
214
- Model: {vision_model} | Max iterations: {max_iterations}
215
- """)
216
-
217
- # System-specific examples
218
- system = platform.system()
219
- if system == "Windows":
220
- app_examples = "start firefox, notepad, calc"
221
- elif system == "Darwin":
222
- app_examples = "open -a Firefox, open -a TextEdit"
223
- else:
224
- app_examples = "firefox &, gedit &, gnome-calculator &"
115
+ if not VISION_AVAILABLE:
116
+ task['status'] = 'failed'
117
+ task['actions'].append({"action": "fail", "reason": "Vision/desktop modules not available"})
118
+ advance_to_next_task()
119
+ return
225
120
 
226
- # Action types
227
- ACTION_SCHEMA = {
228
- "type": "object",
229
- "properties": {
230
- "action": {
231
- "type": "string",
232
- "enum": ["click", "type", "key", "launch", "wait", "done", "fail"],
233
- "description": "Action to perform"
234
- },
235
- "x": {"type": "number", "description": "X coordinate (0-100 percentage)"},
236
- "y": {"type": "number", "description": "Y coordinate (0-100 percentage)"},
237
- "text": {"type": "string", "description": "Text to type or key to press"},
238
- "command": {"type": "string", "description": "Command to launch"},
239
- "duration": {"type": "number", "description": "Wait duration in seconds"},
240
- "reason": {"type": "string", "description": "Explanation of action"}
241
- },
242
- "required": ["action", "reason"]
243
- }
244
-
245
- click_history = []
246
- summary = []
247
-
248
- for iteration in range(max_iterations):
249
- print(colored(f"\n--- Iteration {iteration + 1}/{max_iterations} ---", "cyan"))
250
-
251
- # Capture screenshot
252
- ss = capture_screenshot()
253
- if not ss or 'file_path' not in ss:
254
- print(colored("Failed to capture screenshot", "red"))
255
- break
256
-
257
- screenshot_path = ss['file_path']
258
- if debug:
259
- print(colored(f"Screenshot: {screenshot_path}", "gray"))
260
-
261
- # Build context from history
262
- history_context = ""
263
- if click_history:
264
- history_context = f"\nPrevious actions ({len(click_history)}):\n"
265
- for i, click in enumerate(click_history[-5:], 1):
266
- history_context += f" {i}. {click.get('action', 'unknown')} at ({click.get('x', '?')}, {click.get('y', '?')}) - {click.get('reason', '')}\n"
267
-
268
- prompt = f"""You are a GUI automation assistant. Analyze this screenshot and determine the next action to complete the task.
269
-
270
- TASK: {task}
271
-
272
- {history_context}
273
-
274
- Available actions:
275
- - click: Click at x,y coordinates (0-100 percentage of screen)
276
- - type: Type text
277
- - key: Press key (enter, tab, escape, etc.)
278
- - launch: Launch application ({app_examples})
279
- - wait: Wait for duration seconds
280
- - done: Task completed successfully
281
- - fail: Task cannot be completed
282
-
283
- Respond with JSON: {{"action": "...", "x": N, "y": N, "text": "...", "command": "...", "duration": N, "reason": "..."}}"""
121
+ ui.iteration += 1
122
+ ui.status = "Capturing screen..."
123
+ render_screen()
284
124
 
285
125
  try:
126
+ ss = capture_screenshot(full=True)
127
+ if not ss or 'file_path' not in ss:
128
+ task['actions'].append({"action": "fail", "reason": "Screenshot failed"})
129
+ task['status'] = 'failed'
130
+ advance_to_next_task()
131
+ return
132
+
133
+ screenshot_path = ss['file_path']
134
+ ui.last_screenshot = screenshot_path
135
+
136
+ history_context = ""
137
+ if task['actions']:
138
+ history_context = "\nPrevious actions:\n"
139
+ for i, act in enumerate(task['actions'][-5:], 1):
140
+ history_context += " " + str(i) + ". " + act.get('action', '?')
141
+ if act.get('x'):
142
+ history_context += " at (" + str(act.get('x', '?')) + ", " + str(act.get('y', '?')) + ")"
143
+ history_context += " - " + act.get('reason', '') + "\n"
144
+
145
+ prompt = "You are a GUI automation assistant. Analyze this screenshot and determine the next action.\n\n"
146
+ prompt += "IMPORTANT: Before taking new actions, VERIFY the current screenshot shows the expected result of your previous actions. "
147
+ prompt += "If the page is still loading or hasn't changed yet, use 'wait' with duration 2-3 seconds. "
148
+ prompt += "Do NOT blindly proceed — confirm each step worked before moving on.\n\n"
149
+ prompt += "TASK: " + task['text'] + "\n"
150
+ prompt += history_context + "\n"
151
+ prompt += "Available actions:\n"
152
+ prompt += "- click: Click at x,y coordinates (0-100 percentage of screen)\n"
153
+ prompt += "- type: Type text (use 'text' field)\n"
154
+ prompt += "- key: Press key like enter, tab, escape (use 'text' field)\n"
155
+ prompt += "- launch: Launch application (use 'command' field, e.g. " + app_examples + ")\n"
156
+ prompt += "- wait: Wait for 'duration' seconds (use when page is loading or UI hasn't updated)\n"
157
+ prompt += "- done: Task completed successfully\n"
158
+ prompt += "- fail: Task cannot be completed\n\n"
159
+ prompt += "Respond with JSON, e.g.: " + json_schema_example
160
+
161
+ ui.status = "Thinking... (iter " + str(ui.iteration) + "/" + str(ui.max_iter) + ")"
162
+ render_screen()
163
+
286
164
  resp = get_llm_response(
287
165
  prompt,
288
166
  model=vision_model,
@@ -294,86 +172,403 @@ steps:
294
172
 
295
173
  action_response = resp.get('response', {})
296
174
  if isinstance(action_response, str):
297
- import json
298
175
  try:
299
176
  action_response = json.loads(action_response)
300
177
  except:
301
- print(colored(f"Invalid JSON response: {action_response[:100]}", "red"))
302
- continue
178
+ task['actions'].append({"action": "error", "reason": "Invalid JSON from model"})
179
+ ui.status = "Bad response, retrying..."
180
+ return
303
181
 
304
182
  action = action_response.get('action', 'fail')
305
- reason = action_response.get('reason', 'No reason provided')
306
-
307
- print(colored(f"Action: {action} - {reason}", "yellow"))
183
+ reason = action_response.get('reason', '')
308
184
 
309
185
  if action == 'done':
310
- print(colored("Task completed successfully!", "green"))
311
- summary.append({"iteration": iteration + 1, "action": "done", "reason": reason})
312
- break
186
+ task['status'] = 'done'
187
+ task['actions'].append({"action": "done", "reason": reason})
188
+ advance_to_next_task()
189
+ return
313
190
 
314
191
  if action == 'fail':
315
- print(colored(f"Task failed: {reason}", "red"))
316
- summary.append({"iteration": iteration + 1, "action": "fail", "reason": reason})
317
- break
192
+ task['status'] = 'failed'
193
+ task['actions'].append({"action": "fail", "reason": reason})
194
+ advance_to_next_task()
195
+ return
196
+
197
+ # Execute the action
198
+ act_record = {"action": action, "reason": reason}
318
199
 
319
- # Execute action
320
200
  if action == 'click':
321
201
  x, y = action_response.get('x', 50), action_response.get('y', 50)
322
- perform_action('click', x=x, y=y)
323
- click_history.append({"action": "click", "x": x, "y": y, "reason": reason})
324
- print(colored(f"Clicked at ({x}, {y})", "green"))
325
-
202
+ perform_action({"type": "click", "x": x, "y": y})
203
+ act_record['x'] = x
204
+ act_record['y'] = y
326
205
  elif action == 'type':
327
- text = action_response.get('text', '')
328
- perform_action('type', text=text)
329
- click_history.append({"action": "type", "text": text[:20], "reason": reason})
330
- print(colored(f"Typed: {text[:30]}...", "green"))
331
-
206
+ txt = action_response.get('text', '')
207
+ perform_action({"type": "type", "text": txt})
208
+ act_record['text'] = txt
332
209
  elif action == 'key':
333
210
  key = action_response.get('text', 'enter')
334
- perform_action('key', key=key)
335
- click_history.append({"action": "key", "key": key, "reason": reason})
336
- print(colored(f"Pressed key: {key}", "green"))
337
-
211
+ perform_action({"type": "key", "key": key})
212
+ act_record['key'] = key
338
213
  elif action == 'launch':
339
214
  cmd = action_response.get('command', '')
340
- perform_action('launch', command=cmd)
341
- click_history.append({"action": "launch", "command": cmd, "reason": reason})
342
- print(colored(f"Launched: {cmd}", "green"))
343
- time.sleep(2) # Wait for app to open
344
-
215
+ perform_action({"type": "launch", "command": cmd})
216
+ act_record['command'] = cmd
217
+ time.sleep(2)
345
218
  elif action == 'wait':
346
- duration = action_response.get('duration', 1)
347
- time.sleep(duration)
348
- click_history.append({"action": "wait", "duration": duration, "reason": reason})
349
- print(colored(f"Waited {duration}s", "green"))
350
-
351
- summary.append({
352
- "iteration": iteration + 1,
353
- "action": action,
354
- "last_click_coords": f"({click_history[-1].get('x', 'N/A')}, {click_history[-1].get('y', 'N/A')})" if click_history else "N/A",
355
- "reason": reason
356
- })
219
+ dur = action_response.get('duration', 1)
220
+ time.sleep(dur)
221
+ act_record['duration'] = dur
222
+
223
+ task['actions'].append(act_record)
224
+ ui.status = action + " - " + reason[:40]
225
+ # Wait for UI to settle after state-changing actions
226
+ if action in ('key', 'click'):
227
+ time.sleep(2.0)
228
+ elif action == 'type':
229
+ time.sleep(0.5)
230
+ else:
231
+ time.sleep(0.3)
357
232
 
358
- time.sleep(0.5) # Brief pause between actions
233
+ if ui.mode == 'step':
234
+ ui.mode = 'paused'
235
+ ui.status = "Paused (step done)"
359
236
 
360
237
  except Exception as e:
361
- print(colored(f"Error in iteration {iteration + 1}: {e}", "red"))
362
- summary.append({"iteration": iteration + 1, "error": str(e)})
363
-
364
- # Generate summary
365
- print("\n" + "="*50)
366
- print(colored("PLONK SESSION SUMMARY", "cyan", attrs=['bold']))
367
- print("="*50)
368
- for s in summary:
369
- print(f" Step {s.get('iteration', '?')}: {s.get('action', 'unknown')} - {s.get('reason', s.get('error', ''))[:60]}")
370
-
371
- context['output'] = f"Plonk completed with {len(summary)} actions"
372
- context['messages'] = messages
373
- context['plonk_summary'] = summary
374
- context['click_history'] = click_history
375
-
376
- # Launch interactive browser automatically
377
- if summary:
378
- print(colored("\nLaunching action browser...", "cyan"))
379
- plonk_tui_browser(summary, click_history)
238
+ task['actions'].append({"action": "error", "reason": str(e)})
239
+ ui.status = "Error: " + str(e)[:40]
240
+
241
+ def advance_to_next_task():
242
+ # Find next pending task
243
+ for i in range(len(ui.tasks)):
244
+ if ui.tasks[i]['status'] == 'pending':
245
+ start_task(i)
246
+ return
247
+ # All done
248
+ ui.mode = 'idle'
249
+ ui.current_task = -1
250
+ done_count = sum(1 for t in ui.tasks if t['status'] == 'done')
251
+ fail_count = sum(1 for t in ui.tasks if t['status'] == 'failed')
252
+ ui.status = "Complete: " + str(done_count) + " done, " + str(fail_count) + " failed"
253
+
254
+ def start_task(idx):
255
+ ui.current_task = idx
256
+ ui.tasks[idx]['status'] = 'running'
257
+ ui.iteration = 0
258
+ ui.mode = 'running'
259
+ ui.status = "Running: " + ui.tasks[idx]['text'][:30]
260
+
261
+ def start_selected():
262
+ if not ui.tasks:
263
+ return
264
+ if ui.tasks[ui.sel]['status'] in ('pending', 'failed'):
265
+ ui.tasks[ui.sel]['status'] = 'pending'
266
+ ui.tasks[ui.sel]['actions'] = []
267
+ start_task(ui.sel)
268
+
269
+ def run_all():
270
+ if not ui.tasks:
271
+ return
272
+ for t in ui.tasks:
273
+ if t['status'] != 'done':
274
+ t['status'] = 'pending'
275
+ t['actions'] = []
276
+ # Find first pending
277
+ for i, t in enumerate(ui.tasks):
278
+ if t['status'] == 'pending':
279
+ start_task(i)
280
+ return
281
+
282
+ # ========== Rendering ==========
283
+ def render_screen():
284
+ width, height = get_size()
285
+ out = []
286
+ out.append("\033[H")
287
+
288
+ # Header
289
+ mode_str = ui.mode.upper()
290
+ if ui.mode == 'running' and ui.current_task >= 0:
291
+ mode_str = "RUNNING [" + str(ui.iteration) + "/" + str(ui.max_iter) + "]"
292
+ header = " PLONK - Visual Task Automation "
293
+ mode_display = " [" + mode_str + "] "
294
+ out.append("\033[1;1H\033[7;1m" + header.ljust(width) + "\033[0m")
295
+ out.append("\033[1;" + str(width - len(mode_display) - 1) + "H\033[33;1m" + mode_display + "\033[0m")
296
+
297
+ # Split: top = tasks, bottom = actions
298
+ split = max(6, (height - 4) // 2)
299
+ task_h = split - 2
300
+ action_h = height - split - 4
301
+
302
+ # ── Tasks panel ──
303
+ task_label = " Tasks (" + str(len(ui.tasks)) + ") "
304
+ if ui.panel == 0:
305
+ out.append("\033[3;1H\033[36;1m" + task_label + "\033[90m" + ("-" * (width - len(task_label))) + "\033[0m")
306
+ else:
307
+ out.append("\033[3;1H\033[90m" + task_label + ("-" * (width - len(task_label))) + "\033[0m")
308
+
309
+ if ui.mode == 'input':
310
+ # Show input line
311
+ out.append("\033[4;1H\033[K New task: \033[7m " + ui.input_buf + " \033[0m")
312
+ for i in range(1, task_h):
313
+ out.append("\033[" + str(4+i) + ";1H\033[K")
314
+ elif not ui.tasks:
315
+ out.append("\033[4;1H\033[K\033[90m No tasks. Press 'a' to add a task.\033[0m")
316
+ for i in range(1, task_h):
317
+ out.append("\033[" + str(4+i) + ";1H\033[K")
318
+ else:
319
+ if ui.sel < ui.scroll:
320
+ ui.scroll = ui.sel
321
+ elif ui.sel >= ui.scroll + task_h:
322
+ ui.scroll = ui.sel - task_h + 1
323
+
324
+ for i in range(task_h):
325
+ idx = ui.scroll + i
326
+ row = 4 + i
327
+ out.append("\033[" + str(row) + ";1H\033[K")
328
+ if idx >= len(ui.tasks):
329
+ continue
330
+
331
+ t = ui.tasks[idx]
332
+ status = t['status']
333
+ icon = {"pending": "\033[90m-", "running": "\033[33m>", "done": "\033[32m+", "failed": "\033[31mx"}.get(status, " ")
334
+ action_count = str(len(t.get('actions', [])))
335
+ text = t['text'][:width - 25]
336
+
337
+ line = " " + icon + "\033[0m " + text + " \033[90m[" + status + "] (" + action_count + " acts)\033[0m"
338
+
339
+ if idx == ui.sel and ui.panel == 0:
340
+ out.append("\033[7m>" + line + "\033[0m")
341
+ else:
342
+ out.append(" " + line)
343
+
344
+ # ── Actions panel ──
345
+ action_row = 3 + split
346
+ acts = get_current_actions()
347
+ act_label = " Actions (" + str(len(acts)) + ") "
348
+ if ui.panel == 1:
349
+ out.append("\033[" + str(action_row) + ";1H\033[36;1m" + act_label + "\033[90m" + ("-" * (width - len(act_label))) + "\033[0m")
350
+ else:
351
+ out.append("\033[" + str(action_row) + ";1H\033[90m" + act_label + ("-" * (width - len(act_label))) + "\033[0m")
352
+
353
+ if not acts:
354
+ out.append("\033[" + str(action_row+1) + ";1H\033[K\033[90m No actions yet.\033[0m")
355
+ for i in range(1, action_h):
356
+ out.append("\033[" + str(action_row+1+i) + ";1H\033[K")
357
+ else:
358
+ if ui.action_scroll < 0:
359
+ ui.action_scroll = 0
360
+
361
+ for i in range(action_h):
362
+ idx = ui.action_scroll + i
363
+ row = action_row + 1 + i
364
+ out.append("\033[" + str(row) + ";1H\033[K")
365
+ if idx >= len(acts):
366
+ continue
367
+
368
+ a = acts[idx]
369
+ action = a.get('action', '?')
370
+ reason = a.get('reason', '')[:width - 35]
371
+ coords = ""
372
+ if a.get('x') is not None:
373
+ coords = "(" + str(a.get('x', '')) + "," + str(a.get('y', '')) + ") "
374
+ elif a.get('text'):
375
+ coords = '"' + str(a['text'])[:15] + '" '
376
+ elif a.get('key'):
377
+ coords = '[' + str(a['key']) + '] '
378
+ elif a.get('command'):
379
+ coords = str(a['command'])[:20] + ' '
380
+
381
+ act_color = {"click": "\033[33m", "type": "\033[36m", "key": "\033[35m",
382
+ "launch": "\033[34m", "done": "\033[32m", "fail": "\033[31m",
383
+ "error": "\033[31m", "wait": "\033[90m"}.get(action, "")
384
+
385
+ line = " " + str(idx+1) + ". " + act_color + action + "\033[0m " + coords + "\033[90m" + reason + "\033[0m"
386
+
387
+ if idx == ui.action_scroll and ui.panel == 1:
388
+ out.append("\033[7m " + line + "\033[0m")
389
+ else:
390
+ out.append(" " + line)
391
+
392
+ # ── Status bar ──
393
+ out.append("\033[" + str(height-2) + ";1H\033[K\033[90m" + ("-" * width) + "\033[0m")
394
+ status_line = " " + ui.status + " | Model: " + str(vision_model) + " | Max: " + str(ui.max_iter)
395
+ out.append("\033[" + str(height-1) + ";1H\033[K" + status_line[:width])
396
+
397
+ # ── Footer ──
398
+ if ui.mode == 'input':
399
+ footer = " Type task, Enter:Confirm Esc:Cancel "
400
+ elif ui.mode in ('running', 'step'):
401
+ footer = " p:Pause s:Step Q:Abort Tab:Panel j/k:Scroll "
402
+ elif ui.mode == 'paused':
403
+ footer = " r:Resume s:Step Q:Abort Tab:Panel j/k:Scroll "
404
+ else:
405
+ footer = " a:Add d:Delete Enter:Run R:RunAll Tab:Panel j/k:Nav q:Quit "
406
+ out.append("\033[" + str(height) + ";1H\033[K\033[7m" + footer.ljust(width) + "\033[0m")
407
+
408
+ sys.stdout.write(''.join(out))
409
+ sys.stdout.flush()
410
+
411
+ # ========== Input Handling ==========
412
+ def handle_input(c, fd):
413
+ if ui.mode == 'input':
414
+ return handle_input_mode(c, fd)
415
+
416
+ # Escape sequences
417
+ if c == '\x1b':
418
+ if select.select([fd], [], [], 0.05)[0]:
419
+ c2 = os.read(fd, 1).decode('latin-1')
420
+ if c2 == '[':
421
+ c3 = os.read(fd, 1).decode('latin-1')
422
+ if c3 == 'A': # Up
423
+ nav_up()
424
+ elif c3 == 'B': # Down
425
+ nav_down()
426
+ else:
427
+ # Bare Esc
428
+ if ui.mode == 'paused':
429
+ ui.mode = 'idle'
430
+ ui.status = "Aborted"
431
+ if ui.current_task >= 0 and ui.current_task < len(ui.tasks):
432
+ ui.tasks[ui.current_task]['status'] = 'failed'
433
+ ui.current_task = -1
434
+ return True
435
+
436
+ if c == 'q' and ui.mode == 'idle':
437
+ return False
438
+ if c == 'Q':
439
+ # Abort running
440
+ if ui.mode in ('running', 'paused', 'step'):
441
+ ui.mode = 'idle'
442
+ if ui.current_task >= 0 and ui.current_task < len(ui.tasks):
443
+ ui.tasks[ui.current_task]['status'] = 'failed'
444
+ ui.current_task = -1
445
+ ui.status = "Aborted"
446
+ elif ui.mode == 'idle':
447
+ return False
448
+ return True
449
+
450
+ if c == 'j':
451
+ nav_down()
452
+ elif c == 'k':
453
+ nav_up()
454
+ elif c == '\t':
455
+ ui.panel = 1 - ui.panel
456
+ ui.action_scroll = 0
457
+ elif c == 'a' and ui.mode == 'idle':
458
+ ui.mode = 'input'
459
+ ui.input_buf = ""
460
+ ui.input_cursor = 0
461
+ elif c == 'd' and ui.mode == 'idle':
462
+ delete_task()
463
+ elif c in ('\r', '\n') and ui.mode == 'idle':
464
+ start_selected()
465
+ elif c == 'R' and ui.mode == 'idle':
466
+ run_all()
467
+ elif c == 'p' and ui.mode == 'running':
468
+ ui.mode = 'paused'
469
+ ui.status = "Paused"
470
+ elif c == 'r' and ui.mode == 'paused':
471
+ ui.mode = 'running'
472
+ ui.status = "Resumed"
473
+ elif c == 's' and ui.mode in ('paused', 'idle'):
474
+ if ui.current_task >= 0:
475
+ ui.mode = 'step'
476
+ elif ui.tasks and ui.tasks[ui.sel]['status'] in ('pending', 'failed'):
477
+ ui.tasks[ui.sel]['actions'] = []
478
+ start_task(ui.sel)
479
+ ui.mode = 'step'
480
+
481
+ return True
482
+
483
+ def handle_input_mode(c, fd):
484
+ if c == '\x1b':
485
+ # Cancel input
486
+ if select.select([fd], [], [], 0.05)[0]:
487
+ os.read(fd, 2) # consume rest of escape seq
488
+ ui.mode = 'idle'
489
+ ui.input_buf = ""
490
+ return True
491
+
492
+ if c in ('\r', '\n'):
493
+ if ui.input_buf.strip():
494
+ add_task(ui.input_buf)
495
+ ui.sel = len(ui.tasks) - 1
496
+ ui.mode = 'idle'
497
+ ui.input_buf = ""
498
+ return True
499
+
500
+ if c == '\x7f' or c == '\x08': # Backspace
501
+ if ui.input_cursor > 0:
502
+ ui.input_buf = ui.input_buf[:ui.input_cursor-1] + ui.input_buf[ui.input_cursor:]
503
+ ui.input_cursor -= 1
504
+ elif c >= ' ' and c <= '~':
505
+ ui.input_buf = ui.input_buf[:ui.input_cursor] + c + ui.input_buf[ui.input_cursor:]
506
+ ui.input_cursor += 1
507
+
508
+ return True
509
+
510
+ def nav_up():
511
+ if ui.panel == 0:
512
+ ui.sel = max(0, ui.sel - 1)
513
+ else:
514
+ ui.action_scroll = max(0, ui.action_scroll - 1)
515
+
516
+ def nav_down():
517
+ if ui.panel == 0:
518
+ ui.sel = min(max(0, len(ui.tasks) - 1), ui.sel + 1)
519
+ else:
520
+ acts = get_current_actions()
521
+ ui.action_scroll = min(max(0, len(acts) - 1), ui.action_scroll + 1)
522
+
523
+ # ========== Auto-add task from CLI ==========
524
+ task_arg = context.get('task')
525
+ if task_arg:
526
+ add_task(str(task_arg))
527
+
528
+ # ========== Main Loop ==========
529
+ if not sys.stdin.isatty():
530
+ context['output'] = "Plonk requires an interactive terminal."
531
+ else:
532
+ fd = sys.stdin.fileno()
533
+ old_settings = termios.tcgetattr(fd)
534
+
535
+ try:
536
+ tty.setcbreak(fd)
537
+ sys.stdout.write('\033[?25l')
538
+ sys.stdout.write('\033[2J')
539
+ render_screen()
540
+
541
+ running = True
542
+ while running:
543
+ if ui.mode in ('running', 'step'):
544
+ # Non-blocking check for user input
545
+ if select.select([fd], [], [], 0.05)[0]:
546
+ c = os.read(fd, 1).decode('latin-1')
547
+ running = handle_input(c, fd)
548
+ else:
549
+ run_one_step()
550
+ else:
551
+ # Blocking wait for input
552
+ c = os.read(fd, 1).decode('latin-1')
553
+ running = handle_input(c, fd)
554
+
555
+ render_screen()
556
+
557
+ finally:
558
+ termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
559
+ sys.stdout.write('\033[?25h')
560
+ sys.stdout.write('\033[2J\033[H')
561
+ sys.stdout.flush()
562
+
563
+ # Summary output
564
+ if ui.tasks:
565
+ lines = ["PLONK SESSION SUMMARY", "=" * 40]
566
+ for i, t in enumerate(ui.tasks):
567
+ lines.append(str(i+1) + ". [" + t['status'] + "] " + t['text'])
568
+ for j, a in enumerate(t.get('actions', [])):
569
+ lines.append(" " + str(j+1) + ". " + a.get('action', '?') + " - " + a.get('reason', '')[:50])
570
+ context['output'] = "\n".join(lines)
571
+ else:
572
+ context['output'] = "Plonk session ended."
573
+
574
+ context['messages'] = messages