npcsh 1.1.22__py3-none-any.whl → 1.1.23__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.
- npcsh/_state.py +272 -120
- npcsh/benchmark/npcsh_agent.py +77 -240
- npcsh/benchmark/templates/install-npcsh.sh.j2 +12 -4
- npcsh/config.py +5 -2
- npcsh/npc_team/alicanto.npc +4 -8
- npcsh/npc_team/corca.npc +5 -11
- npcsh/npc_team/frederic.npc +4 -6
- npcsh/npc_team/guac.npc +4 -4
- npcsh/npc_team/jinxs/lib/core/delegate.jinx +1 -1
- npcsh/npc_team/jinxs/lib/core/edit_file.jinx +1 -1
- npcsh/npc_team/jinxs/lib/core/sh.jinx +1 -1
- npcsh/npc_team/jinxs/lib/core/skill.jinx +59 -0
- npcsh/npc_team/jinxs/lib/utils/help.jinx +194 -10
- npcsh/npc_team/jinxs/lib/utils/init.jinx +528 -37
- npcsh/npc_team/jinxs/lib/utils/jinxs.jinx +0 -1
- npcsh/npc_team/jinxs/lib/utils/serve.jinx +938 -21
- npcsh-1.1.22.data/data/npcsh/npc_team/config_tui.jinx → npcsh/npc_team/jinxs/modes/config.jinx +1 -1
- npcsh/npc_team/jinxs/modes/convene.jinx +76 -3
- npcsh/npc_team/jinxs/modes/crond.jinx +818 -0
- npcsh/npc_team/jinxs/modes/plonk.jinx +76 -14
- npcsh/npc_team/jinxs/modes/roll.jinx +368 -55
- npcsh/npc_team/jinxs/modes/skills.jinx +621 -0
- npcsh/npc_team/jinxs/modes/yap.jinx +504 -30
- npcsh/npc_team/jinxs/skills/code-review/SKILL.md +45 -0
- npcsh/npc_team/jinxs/skills/debugging/SKILL.md +44 -0
- npcsh/npc_team/jinxs/skills/git-workflow.jinx +44 -0
- npcsh/npc_team/kadiefa.npc +4 -5
- npcsh/npc_team/npcsh.ctx +16 -0
- npcsh/npc_team/plonk.npc +5 -9
- npcsh/npc_team/sibiji.npc +13 -5
- npcsh/npcsh.py +1 -0
- npcsh/routes.py +0 -4
- npcsh/yap.py +22 -4
- npcsh-1.1.23.data/data/npcsh/npc_team/SKILL.md +44 -0
- {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/alicanto.npc +4 -8
- npcsh/npc_team/jinxs/modes/config_tui.jinx → npcsh-1.1.23.data/data/npcsh/npc_team/config.jinx +1 -1
- npcsh-1.1.23.data/data/npcsh/npc_team/convene.jinx +670 -0
- {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/corca.npc +5 -11
- npcsh-1.1.23.data/data/npcsh/npc_team/crond.jinx +818 -0
- {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/delegate.jinx +1 -1
- {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/edit_file.jinx +1 -1
- {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/frederic.npc +4 -6
- npcsh-1.1.23.data/data/npcsh/npc_team/git-workflow.jinx +44 -0
- {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/guac.npc +4 -4
- npcsh-1.1.23.data/data/npcsh/npc_team/help.jinx +236 -0
- npcsh-1.1.23.data/data/npcsh/npc_team/init.jinx +532 -0
- {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/jinxs.jinx +0 -1
- {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/kadiefa.npc +4 -5
- npcsh-1.1.23.data/data/npcsh/npc_team/npcsh.ctx +34 -0
- {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/plonk.jinx +76 -14
- {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/plonk.npc +5 -9
- npcsh-1.1.23.data/data/npcsh/npc_team/roll.jinx +378 -0
- npcsh-1.1.23.data/data/npcsh/npc_team/serve.jinx +943 -0
- {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/sh.jinx +1 -1
- {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/sibiji.npc +13 -5
- npcsh-1.1.23.data/data/npcsh/npc_team/skill.jinx +59 -0
- npcsh-1.1.23.data/data/npcsh/npc_team/skills.jinx +621 -0
- {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/yap.jinx +504 -30
- {npcsh-1.1.22.dist-info → npcsh-1.1.23.dist-info}/METADATA +168 -7
- npcsh-1.1.23.dist-info/RECORD +216 -0
- npcsh/npc_team/jinxs/incognide/add_tab.jinx +0 -11
- npcsh/npc_team/jinxs/incognide/close_pane.jinx +0 -9
- npcsh/npc_team/jinxs/incognide/close_tab.jinx +0 -10
- npcsh/npc_team/jinxs/incognide/confirm.jinx +0 -10
- npcsh/npc_team/jinxs/incognide/focus_pane.jinx +0 -9
- npcsh/npc_team/jinxs/incognide/list_panes.jinx +0 -8
- npcsh/npc_team/jinxs/incognide/navigate.jinx +0 -10
- npcsh/npc_team/jinxs/incognide/notify.jinx +0 -10
- npcsh/npc_team/jinxs/incognide/open_pane.jinx +0 -13
- npcsh/npc_team/jinxs/incognide/read_pane.jinx +0 -9
- npcsh/npc_team/jinxs/incognide/run_terminal.jinx +0 -10
- npcsh/npc_team/jinxs/incognide/send_message.jinx +0 -10
- npcsh/npc_team/jinxs/incognide/split_pane.jinx +0 -12
- npcsh/npc_team/jinxs/incognide/switch_npc.jinx +0 -10
- npcsh/npc_team/jinxs/incognide/switch_tab.jinx +0 -10
- npcsh/npc_team/jinxs/incognide/write_file.jinx +0 -11
- npcsh/npc_team/jinxs/incognide/zen_mode.jinx +0 -9
- npcsh/npc_team/jinxs/lib/core/convene.jinx +0 -232
- npcsh-1.1.22.data/data/npcsh/npc_team/add_tab.jinx +0 -11
- npcsh-1.1.22.data/data/npcsh/npc_team/close_pane.jinx +0 -9
- npcsh-1.1.22.data/data/npcsh/npc_team/close_tab.jinx +0 -10
- npcsh-1.1.22.data/data/npcsh/npc_team/confirm.jinx +0 -10
- npcsh-1.1.22.data/data/npcsh/npc_team/convene.jinx +0 -232
- npcsh-1.1.22.data/data/npcsh/npc_team/focus_pane.jinx +0 -9
- npcsh-1.1.22.data/data/npcsh/npc_team/help.jinx +0 -52
- npcsh-1.1.22.data/data/npcsh/npc_team/init.jinx +0 -41
- npcsh-1.1.22.data/data/npcsh/npc_team/list_panes.jinx +0 -8
- npcsh-1.1.22.data/data/npcsh/npc_team/navigate.jinx +0 -10
- npcsh-1.1.22.data/data/npcsh/npc_team/notify.jinx +0 -10
- npcsh-1.1.22.data/data/npcsh/npc_team/npcsh.ctx +0 -18
- npcsh-1.1.22.data/data/npcsh/npc_team/open_pane.jinx +0 -13
- npcsh-1.1.22.data/data/npcsh/npc_team/read_pane.jinx +0 -9
- npcsh-1.1.22.data/data/npcsh/npc_team/roll.jinx +0 -65
- npcsh-1.1.22.data/data/npcsh/npc_team/run_terminal.jinx +0 -10
- npcsh-1.1.22.data/data/npcsh/npc_team/send_message.jinx +0 -10
- npcsh-1.1.22.data/data/npcsh/npc_team/serve.jinx +0 -26
- npcsh-1.1.22.data/data/npcsh/npc_team/split_pane.jinx +0 -12
- npcsh-1.1.22.data/data/npcsh/npc_team/switch_npc.jinx +0 -10
- npcsh-1.1.22.data/data/npcsh/npc_team/switch_tab.jinx +0 -10
- npcsh-1.1.22.data/data/npcsh/npc_team/write_file.jinx +0 -11
- npcsh-1.1.22.data/data/npcsh/npc_team/zen_mode.jinx +0 -9
- npcsh-1.1.22.dist-info/RECORD +0 -240
- /npcsh/npc_team/jinxs/{incognide → lib/utils}/incognide.jinx +0 -0
- {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/alicanto.jinx +0 -0
- {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/alicanto.png +0 -0
- {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/arxiv.jinx +0 -0
- {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/benchmark.jinx +0 -0
- {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/browser_action.jinx +0 -0
- {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/browser_screenshot.jinx +0 -0
- {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/build.jinx +0 -0
- {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/chat.jinx +0 -0
- {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/click.jinx +0 -0
- {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/close_browser.jinx +0 -0
- {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/cmd.jinx +0 -0
- {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/compile.jinx +0 -0
- {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/compress.jinx +0 -0
- {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/corca.jinx +0 -0
- {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/corca.png +0 -0
- {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/corca_example.png +0 -0
- {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/db_search.jinx +0 -0
- {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/file_search.jinx +0 -0
- {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/frederic4.png +0 -0
- {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/git.jinx +0 -0
- {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/guac.jinx +0 -0
- {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/guac.png +0 -0
- {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/incognide.jinx +0 -0
- {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/kadiefa.png +0 -0
- {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/key_press.jinx +0 -0
- {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/kg.jinx +0 -0
- {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/launch_app.jinx +0 -0
- {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/load_file.jinx +0 -0
- {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/memories.jinx +0 -0
- {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/models.jinx +0 -0
- {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/npcsh_sibiji.png +0 -0
- {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/nql.jinx +0 -0
- {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/open_browser.jinx +0 -0
- {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/ots.jinx +0 -0
- {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/papers.jinx +0 -0
- {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/paste.jinx +0 -0
- {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/plonk.png +0 -0
- {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/plonkjr.png +0 -0
- {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/pti.jinx +0 -0
- {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/python.jinx +0 -0
- {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/reattach.jinx +0 -0
- {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/sample.jinx +0 -0
- {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/screenshot.jinx +0 -0
- {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/set.jinx +0 -0
- {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/setup.jinx +0 -0
- {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/shh.jinx +0 -0
- {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/sibiji.png +0 -0
- {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/sleep.jinx +0 -0
- {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/spool.jinx +0 -0
- {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/spool.png +0 -0
- {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/sql.jinx +0 -0
- {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/switch.jinx +0 -0
- {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/switches.jinx +0 -0
- {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/sync.jinx +0 -0
- {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/team.jinx +0 -0
- {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/teamviz.jinx +0 -0
- {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/trigger.jinx +0 -0
- {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/type_text.jinx +0 -0
- {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/usage.jinx +0 -0
- {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/verbose.jinx +0 -0
- {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/vixynt.jinx +0 -0
- {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/wait.jinx +0 -0
- {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/wander.jinx +0 -0
- {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/web_search.jinx +0 -0
- {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/yap.png +0 -0
- {npcsh-1.1.22.dist-info → npcsh-1.1.23.dist-info}/WHEEL +0 -0
- {npcsh-1.1.22.dist-info → npcsh-1.1.23.dist-info}/entry_points.txt +0 -0
- {npcsh-1.1.22.dist-info → npcsh-1.1.23.dist-info}/licenses/LICENSE +0 -0
- {npcsh-1.1.22.dist-info → npcsh-1.1.23.dist-info}/top_level.txt +0 -0
|
@@ -24,6 +24,25 @@ steps:
|
|
|
24
24
|
|
|
25
25
|
from npcpy.llm_funcs import get_llm_response
|
|
26
26
|
|
|
27
|
+
# Helper to log jinx executions to DB
|
|
28
|
+
def _log_jinx(trigger_id, npc_name, inputs, output, status="success", error_msg=None):
|
|
29
|
+
try:
|
|
30
|
+
if state and hasattr(state, 'command_history') and state.command_history is not None and hasattr(state.command_history, 'save_jinx_execution'):
|
|
31
|
+
_conv_id = getattr(state, 'conversation_id', None) or ''
|
|
32
|
+
state.command_history.save_jinx_execution(
|
|
33
|
+
triggering_message_id=f"{_conv_id}-{trigger_id}",
|
|
34
|
+
conversation_id=_conv_id,
|
|
35
|
+
npc_name=npc_name,
|
|
36
|
+
jinx_name="plonk",
|
|
37
|
+
jinx_inputs=inputs,
|
|
38
|
+
jinx_output=str(output) if output else "",
|
|
39
|
+
status=status,
|
|
40
|
+
team_name=state.team.name if state and hasattr(state, 'team') and state.team else None,
|
|
41
|
+
error_message=error_msg,
|
|
42
|
+
)
|
|
43
|
+
except Exception:
|
|
44
|
+
pass
|
|
45
|
+
|
|
27
46
|
try:
|
|
28
47
|
from npcpy.data.image import capture_screenshot
|
|
29
48
|
from npcpy.work.desktop import perform_action
|
|
@@ -136,26 +155,43 @@ steps:
|
|
|
136
155
|
history_context = ""
|
|
137
156
|
if task['actions']:
|
|
138
157
|
history_context = "\nPrevious actions:\n"
|
|
139
|
-
for i, act in enumerate(task['actions'][-
|
|
158
|
+
for i, act in enumerate(task['actions'][-8:], 1):
|
|
140
159
|
history_context += " " + str(i) + ". " + act.get('action', '?')
|
|
141
160
|
if act.get('x'):
|
|
142
161
|
history_context += " at (" + str(act.get('x', '?')) + ", " + str(act.get('y', '?')) + ")"
|
|
162
|
+
if act.get('text'):
|
|
163
|
+
history_context += ' "' + act.get('text', '')[:50] + '"'
|
|
164
|
+
if act.get('command'):
|
|
165
|
+
history_context += ' [' + act.get('command', '')[:30] + ']'
|
|
143
166
|
history_context += " - " + act.get('reason', '') + "\n"
|
|
167
|
+
# Detect repetition: if last 3 actions are the same type, warn
|
|
168
|
+
recent = task['actions'][-3:]
|
|
169
|
+
if len(recent) == 3 and all(a.get('action') == recent[0].get('action') for a in recent):
|
|
170
|
+
history_context += "\n*** WARNING: You have repeated '" + recent[0].get('action', '?') + "' 3 times. "
|
|
171
|
+
history_context += "This is NOT working. You MUST try a completely different approach. ***\n"
|
|
144
172
|
|
|
145
173
|
prompt = "You are a GUI automation assistant. Analyze this screenshot and determine the next action.\n\n"
|
|
146
|
-
prompt += "
|
|
147
|
-
prompt += "
|
|
148
|
-
prompt += "
|
|
174
|
+
prompt += "CRITICAL RULES:\n"
|
|
175
|
+
prompt += "1. VERIFY: Check the screenshot BEFORE acting. Does it show the result of your last action? If not, wait.\n"
|
|
176
|
+
prompt += "2. NO REPEATS: If an action failed or had no effect, try a DIFFERENT approach. Never repeat the same action.\n"
|
|
177
|
+
prompt += "3. FULL TEXT: Always type the COMPLETE text in a single 'type' action. Never truncate or split text.\n"
|
|
178
|
+
prompt += "4. FOCUS FIRST: Before typing, make sure the target field is focused (click it or use keyboard shortcut).\n\n"
|
|
179
|
+
prompt += "KEYBOARD SHORTCUTS (use 'key' action with these):\n"
|
|
180
|
+
prompt += "- ctrl+l or F6: Focus browser address/search bar\n"
|
|
181
|
+
prompt += "- ctrl+a: Select all text in current field\n"
|
|
182
|
+
prompt += "- ctrl+t: New browser tab\n"
|
|
183
|
+
prompt += "- tab/shift+tab: Move between form fields\n"
|
|
184
|
+
prompt += "- escape: Close popups, cancel autocomplete\n\n"
|
|
149
185
|
prompt += "TASK: " + task['text'] + "\n"
|
|
150
186
|
prompt += history_context + "\n"
|
|
151
187
|
prompt += "Available actions:\n"
|
|
152
|
-
prompt += "- click: Click at x,y
|
|
153
|
-
prompt += "- type: Type text (
|
|
154
|
-
prompt += "- key: Press key
|
|
155
|
-
prompt += "- launch: Launch application
|
|
156
|
-
prompt += "- wait: Wait
|
|
157
|
-
prompt += "- done: Task completed
|
|
158
|
-
prompt += "- fail: Task
|
|
188
|
+
prompt += "- click: Click at x,y (0-100 % of screen). Fields: x, y\n"
|
|
189
|
+
prompt += "- type: Type text into focused field. Fields: text (MUST be complete text)\n"
|
|
190
|
+
prompt += "- key: Press key(s). Fields: text (e.g. 'enter', 'ctrl+l', 'tab', 'escape')\n"
|
|
191
|
+
prompt += "- launch: Launch application. Fields: command (e.g. " + app_examples + ")\n"
|
|
192
|
+
prompt += "- wait: Wait seconds. Fields: duration\n"
|
|
193
|
+
prompt += "- done: Task completed\n"
|
|
194
|
+
prompt += "- fail: Task impossible\n\n"
|
|
159
195
|
prompt += "Respond with JSON, e.g.: " + json_schema_example
|
|
160
196
|
|
|
161
197
|
ui.status = "Thinking... (iter " + str(ui.iteration) + "/" + str(ui.max_iter) + ")"
|
|
@@ -176,21 +212,38 @@ steps:
|
|
|
176
212
|
action_response = json.loads(action_response)
|
|
177
213
|
except:
|
|
178
214
|
task['actions'].append({"action": "error", "reason": "Invalid JSON from model"})
|
|
215
|
+
_log_jinx(f"plonk-task-{ui.current_task}-iter-{ui.iteration}",
|
|
216
|
+
npc.name if npc and hasattr(npc, 'name') else "plonk",
|
|
217
|
+
{"task": task['text'], "iteration": ui.iteration, "type": "analysis"},
|
|
218
|
+
str(resp.get('response', '')), status="error", error_msg="Invalid JSON from model")
|
|
179
219
|
ui.status = "Bad response, retrying..."
|
|
180
220
|
return
|
|
181
221
|
|
|
182
222
|
action = action_response.get('action', 'fail')
|
|
183
223
|
reason = action_response.get('reason', '')
|
|
184
224
|
|
|
225
|
+
_npc_name = npc.name if npc and hasattr(npc, 'name') else "plonk"
|
|
226
|
+
_log_jinx(f"plonk-task-{ui.current_task}-iter-{ui.iteration}",
|
|
227
|
+
_npc_name,
|
|
228
|
+
{"task": task['text'], "iteration": ui.iteration, "screenshot": screenshot_path,
|
|
229
|
+
"type": "analysis"},
|
|
230
|
+
json.dumps(action_response))
|
|
231
|
+
|
|
185
232
|
if action == 'done':
|
|
186
233
|
task['status'] = 'done'
|
|
187
234
|
task['actions'].append({"action": "done", "reason": reason})
|
|
235
|
+
_log_jinx(f"plonk-task-{ui.current_task}-done", _npc_name,
|
|
236
|
+
{"task": task['text'], "type": "task_complete", "total_actions": len(task['actions'])},
|
|
237
|
+
reason)
|
|
188
238
|
advance_to_next_task()
|
|
189
239
|
return
|
|
190
240
|
|
|
191
241
|
if action == 'fail':
|
|
192
242
|
task['status'] = 'failed'
|
|
193
243
|
task['actions'].append({"action": "fail", "reason": reason})
|
|
244
|
+
_log_jinx(f"plonk-task-{ui.current_task}-fail", _npc_name,
|
|
245
|
+
{"task": task['text'], "type": "task_failed", "total_actions": len(task['actions'])},
|
|
246
|
+
reason, status="error", error_msg=reason)
|
|
194
247
|
advance_to_next_task()
|
|
195
248
|
return
|
|
196
249
|
|
|
@@ -208,11 +261,16 @@ steps:
|
|
|
208
261
|
act_record['text'] = txt
|
|
209
262
|
elif action == 'key':
|
|
210
263
|
key = action_response.get('text', 'enter')
|
|
211
|
-
|
|
264
|
+
# Detect combo keys like ctrl+l, ctrl+a, shift+tab
|
|
265
|
+
if '+' in key:
|
|
266
|
+
parts = [k.strip() for k in key.split('+')]
|
|
267
|
+
perform_action({"type": "hotkey", "keys": parts})
|
|
268
|
+
else:
|
|
269
|
+
perform_action({"type": "key", "keys": key})
|
|
212
270
|
act_record['key'] = key
|
|
213
271
|
elif action == 'launch':
|
|
214
272
|
cmd = action_response.get('command', '')
|
|
215
|
-
perform_action({"type": "
|
|
273
|
+
perform_action({"type": "shell", "command": cmd})
|
|
216
274
|
act_record['command'] = cmd
|
|
217
275
|
time.sleep(2)
|
|
218
276
|
elif action == 'wait':
|
|
@@ -236,6 +294,10 @@ steps:
|
|
|
236
294
|
|
|
237
295
|
except Exception as e:
|
|
238
296
|
task['actions'].append({"action": "error", "reason": str(e)})
|
|
297
|
+
_log_jinx(f"plonk-task-{ui.current_task}-iter-{ui.iteration}-error",
|
|
298
|
+
npc.name if npc and hasattr(npc, 'name') else "plonk",
|
|
299
|
+
{"task": task['text'] if task else "unknown", "iteration": ui.iteration, "type": "error"},
|
|
300
|
+
str(e), status="error", error_msg=str(e))
|
|
239
301
|
ui.status = "Error: " + str(e)[:40]
|
|
240
302
|
|
|
241
303
|
def advance_to_next_task():
|
|
@@ -372,7 +434,7 @@ steps:
|
|
|
372
434
|
if a.get('x') is not None:
|
|
373
435
|
coords = "(" + str(a.get('x', '')) + "," + str(a.get('y', '')) + ") "
|
|
374
436
|
elif a.get('text'):
|
|
375
|
-
coords = '"' + str(a['text'])[:
|
|
437
|
+
coords = '"' + str(a['text'])[:40] + '" '
|
|
376
438
|
elif a.get('key'):
|
|
377
439
|
coords = '[' + str(a['key']) + '] '
|
|
378
440
|
elif a.get('command'):
|
|
@@ -11,15 +11,11 @@ colors:
|
|
|
11
11
|
top: "34,139,34"
|
|
12
12
|
bottom: "139,69,19"
|
|
13
13
|
primary_directive: |
|
|
14
|
-
You are plonk, the browser and
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
Use get_elements to discover selectors on the page. Use xpath:// prefix for XPath selectors.
|
|
21
|
-
|
|
22
|
-
Desktop tools: screenshot, click, type_text, key_press, launch_app, wait
|
|
14
|
+
You are plonk, the automation specialist for browser and desktop.
|
|
15
|
+
Browser: open_browser, browser_action, browser_screenshot, close_browser.
|
|
16
|
+
browser_action: click, type, type_and_enter, select, wait, scroll, get_text, get_page, get_elements, press_key.
|
|
17
|
+
Desktop: screenshot, click, type_text, key_press, launch_app, wait.
|
|
18
|
+
Use get_elements to find selectors. Use xpath:// for XPath. Screenshot after each step to verify.
|
|
23
19
|
jinxs:
|
|
24
20
|
- lib/browser/*
|
|
25
21
|
- lib/computer_use/*
|
|
@@ -0,0 +1,378 @@
|
|
|
1
|
+
jinx_name: "roll"
|
|
2
|
+
description: "Video creation studio TUI - generate and manage videos with parameter controls"
|
|
3
|
+
interactive: true
|
|
4
|
+
inputs:
|
|
5
|
+
- prompt: null
|
|
6
|
+
- model: null
|
|
7
|
+
- provider: null
|
|
8
|
+
- output_path: null
|
|
9
|
+
- num_frames: null
|
|
10
|
+
- width: null
|
|
11
|
+
- height: null
|
|
12
|
+
steps:
|
|
13
|
+
- name: "roll_tui"
|
|
14
|
+
engine: "python"
|
|
15
|
+
code: |
|
|
16
|
+
import os
|
|
17
|
+
import sys
|
|
18
|
+
import tty
|
|
19
|
+
import termios
|
|
20
|
+
import select
|
|
21
|
+
from datetime import datetime
|
|
22
|
+
|
|
23
|
+
from npcpy.llm_funcs import gen_video
|
|
24
|
+
|
|
25
|
+
npc = context.get('npc')
|
|
26
|
+
if isinstance(npc, str):
|
|
27
|
+
npc = None
|
|
28
|
+
|
|
29
|
+
# ========== State ==========
|
|
30
|
+
class RollState:
|
|
31
|
+
def __init__(self):
|
|
32
|
+
self.prompt = ""
|
|
33
|
+
self.model = ""
|
|
34
|
+
self.provider = ""
|
|
35
|
+
self.width_val = 256
|
|
36
|
+
self.height_val = 256
|
|
37
|
+
self.num_frames = 125
|
|
38
|
+
self.output_path = ""
|
|
39
|
+
# UI state
|
|
40
|
+
self.params = ['prompt', 'model', 'provider', 'width', 'height', 'num_frames', 'output']
|
|
41
|
+
self.sel = 0
|
|
42
|
+
self.scroll = 0
|
|
43
|
+
self.mode = 'params' # params, editing, gallery, generating
|
|
44
|
+
self.edit_buf = ""
|
|
45
|
+
self.edit_cursor = 0
|
|
46
|
+
self.status = "Ready"
|
|
47
|
+
self.gallery = [] # [{"path": str, "prompt": str, "timestamp": str}]
|
|
48
|
+
self.gallery_sel = 0
|
|
49
|
+
self.gallery_scroll = 0
|
|
50
|
+
|
|
51
|
+
ui = RollState()
|
|
52
|
+
|
|
53
|
+
# Load from context
|
|
54
|
+
ui.prompt = str(context.get('prompt') or '')
|
|
55
|
+
ui.model = str(context.get('model') or os.getenv('NPCSH_VIDEO_GEN_MODEL', ''))
|
|
56
|
+
ui.provider = str(context.get('provider') or os.getenv('NPCSH_VIDEO_GEN_PROVIDER', ''))
|
|
57
|
+
if not ui.model and npc and hasattr(npc, 'model') and npc.model:
|
|
58
|
+
ui.model = npc.model
|
|
59
|
+
if not ui.provider and npc and hasattr(npc, 'provider') and npc.provider:
|
|
60
|
+
ui.provider = npc.provider
|
|
61
|
+
if not ui.model:
|
|
62
|
+
ui.model = "stable-video-diffusion"
|
|
63
|
+
if not ui.provider:
|
|
64
|
+
ui.provider = "diffusers"
|
|
65
|
+
try:
|
|
66
|
+
ui.width_val = int(context.get('width') or 256)
|
|
67
|
+
except:
|
|
68
|
+
pass
|
|
69
|
+
try:
|
|
70
|
+
ui.height_val = int(context.get('height') or 256)
|
|
71
|
+
except:
|
|
72
|
+
pass
|
|
73
|
+
try:
|
|
74
|
+
ui.num_frames = int(context.get('num_frames') or 125)
|
|
75
|
+
except:
|
|
76
|
+
pass
|
|
77
|
+
ui.output_path = str(context.get('output_path') or '')
|
|
78
|
+
|
|
79
|
+
# Load existing gallery
|
|
80
|
+
vid_dir = os.path.expanduser("~/.npcsh/videos/")
|
|
81
|
+
if os.path.isdir(vid_dir):
|
|
82
|
+
for f in sorted(os.listdir(vid_dir), reverse=True)[:50]:
|
|
83
|
+
if f.lower().endswith(('.mp4', '.webm', '.avi', '.mov', '.mkv')):
|
|
84
|
+
ui.gallery.append({"path": os.path.join(vid_dir, f), "prompt": "", "timestamp": f})
|
|
85
|
+
|
|
86
|
+
def get_size():
|
|
87
|
+
try:
|
|
88
|
+
s = os.get_terminal_size()
|
|
89
|
+
return s.columns, s.lines
|
|
90
|
+
except:
|
|
91
|
+
return 80, 24
|
|
92
|
+
|
|
93
|
+
def get_param_value(idx):
|
|
94
|
+
p = ui.params[idx]
|
|
95
|
+
if p == 'prompt': return ui.prompt
|
|
96
|
+
if p == 'model': return ui.model or '(default)'
|
|
97
|
+
if p == 'provider': return ui.provider or '(default)'
|
|
98
|
+
if p == 'width': return str(ui.width_val)
|
|
99
|
+
if p == 'height': return str(ui.height_val)
|
|
100
|
+
if p == 'num_frames': return str(ui.num_frames)
|
|
101
|
+
if p == 'output': return ui.output_path or '(auto)'
|
|
102
|
+
return ''
|
|
103
|
+
|
|
104
|
+
def set_param_value(idx, val):
|
|
105
|
+
p = ui.params[idx]
|
|
106
|
+
if p == 'prompt': ui.prompt = val
|
|
107
|
+
elif p == 'model': ui.model = val
|
|
108
|
+
elif p == 'provider': ui.provider = val
|
|
109
|
+
elif p == 'width':
|
|
110
|
+
try: ui.width_val = int(val)
|
|
111
|
+
except: pass
|
|
112
|
+
elif p == 'height':
|
|
113
|
+
try: ui.height_val = int(val)
|
|
114
|
+
except: pass
|
|
115
|
+
elif p == 'num_frames':
|
|
116
|
+
try: ui.num_frames = max(1, int(val))
|
|
117
|
+
except: pass
|
|
118
|
+
elif p == 'output': ui.output_path = val
|
|
119
|
+
|
|
120
|
+
def generate_videos():
|
|
121
|
+
ui.mode = 'generating'
|
|
122
|
+
ui.status = "Generating..."
|
|
123
|
+
render_screen()
|
|
124
|
+
|
|
125
|
+
if not ui.prompt:
|
|
126
|
+
ui.status = "Error: No prompt provided"
|
|
127
|
+
ui.mode = 'params'
|
|
128
|
+
return
|
|
129
|
+
|
|
130
|
+
try:
|
|
131
|
+
if ui.output_path and ui.output_path.strip():
|
|
132
|
+
out_file = os.path.expanduser(ui.output_path)
|
|
133
|
+
else:
|
|
134
|
+
os.makedirs(vid_dir, exist_ok=True)
|
|
135
|
+
out_file = os.path.join(vid_dir, "roll_" + datetime.now().strftime('%Y%m%d_%H%M%S') + ".mp4")
|
|
136
|
+
|
|
137
|
+
result = gen_video(
|
|
138
|
+
prompt=ui.prompt,
|
|
139
|
+
model=ui.model or None,
|
|
140
|
+
provider=ui.provider or None,
|
|
141
|
+
npc=npc,
|
|
142
|
+
num_frames=ui.num_frames,
|
|
143
|
+
width=ui.width_val,
|
|
144
|
+
height=ui.height_val,
|
|
145
|
+
output_path=out_file,
|
|
146
|
+
**context.get('api_kwargs', {})
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
if isinstance(result, dict):
|
|
150
|
+
msg = result.get('output', 'Video generated.')
|
|
151
|
+
else:
|
|
152
|
+
msg = str(result)
|
|
153
|
+
|
|
154
|
+
ui.gallery.insert(0, {"path": out_file, "prompt": ui.prompt, "timestamp": os.path.basename(out_file)})
|
|
155
|
+
ui.status = "Generated: " + os.path.basename(out_file)
|
|
156
|
+
except Exception as e:
|
|
157
|
+
ui.status = "Error: " + str(e)[:60]
|
|
158
|
+
|
|
159
|
+
ui.mode = 'params'
|
|
160
|
+
|
|
161
|
+
# ========== Rendering ==========
|
|
162
|
+
def render_screen():
|
|
163
|
+
width, height = get_size()
|
|
164
|
+
out = []
|
|
165
|
+
out.append("\033[H")
|
|
166
|
+
|
|
167
|
+
header = " ROLL - Video Creation Studio "
|
|
168
|
+
out.append("\033[1;1H\033[7;1m" + header.ljust(width) + "\033[0m")
|
|
169
|
+
|
|
170
|
+
if ui.mode in ('params', 'editing', 'generating'):
|
|
171
|
+
# Parameter panel
|
|
172
|
+
out.append("\033[3;1H\033[36;1m Parameters \033[90m" + ("-" * (width - 13)) + "\033[0m")
|
|
173
|
+
|
|
174
|
+
for i, p in enumerate(ui.params):
|
|
175
|
+
row = 4 + i
|
|
176
|
+
out.append("\033[" + str(row) + ";1H\033[K")
|
|
177
|
+
label = p.capitalize() + ":"
|
|
178
|
+
val = get_param_value(i)
|
|
179
|
+
|
|
180
|
+
if ui.mode == 'editing' and i == ui.sel:
|
|
181
|
+
line = " " + label.ljust(14) + "\033[7m " + ui.edit_buf + " \033[0m"
|
|
182
|
+
else:
|
|
183
|
+
line = " " + label.ljust(14) + val[:width - 18]
|
|
184
|
+
|
|
185
|
+
if i == ui.sel and ui.mode != 'editing':
|
|
186
|
+
out.append("\033[7m>" + line + "\033[0m")
|
|
187
|
+
else:
|
|
188
|
+
out.append(" " + line)
|
|
189
|
+
|
|
190
|
+
# Gallery preview
|
|
191
|
+
gallery_row = 4 + len(ui.params) + 1
|
|
192
|
+
out.append("\033[" + str(gallery_row) + ";1H\033[33;1m Gallery (" + str(len(ui.gallery)) + ") \033[90m" + ("-" * (width - 20)) + "\033[0m")
|
|
193
|
+
|
|
194
|
+
gallery_h = height - gallery_row - 4
|
|
195
|
+
for i in range(max(0, gallery_h)):
|
|
196
|
+
idx = ui.gallery_scroll + i
|
|
197
|
+
row = gallery_row + 1 + i
|
|
198
|
+
out.append("\033[" + str(row) + ";1H\033[K")
|
|
199
|
+
if idx >= len(ui.gallery):
|
|
200
|
+
continue
|
|
201
|
+
g = ui.gallery[idx]
|
|
202
|
+
fname = os.path.basename(g['path'])[:width - 6]
|
|
203
|
+
out.append(" " + fname)
|
|
204
|
+
|
|
205
|
+
elif ui.mode == 'gallery':
|
|
206
|
+
out.append("\033[3;1H\033[33;1m Gallery (" + str(len(ui.gallery)) + ") \033[90m" + ("-" * (width - 20)) + "\033[0m")
|
|
207
|
+
|
|
208
|
+
gallery_h = height - 6
|
|
209
|
+
for i in range(gallery_h):
|
|
210
|
+
idx = ui.gallery_scroll + i
|
|
211
|
+
row = 4 + i
|
|
212
|
+
out.append("\033[" + str(row) + ";1H\033[K")
|
|
213
|
+
if idx >= len(ui.gallery):
|
|
214
|
+
continue
|
|
215
|
+
g = ui.gallery[idx]
|
|
216
|
+
fname = os.path.basename(g['path'])
|
|
217
|
+
if idx == ui.gallery_sel:
|
|
218
|
+
out.append("\033[7m> " + fname[:width-4] + "\033[0m")
|
|
219
|
+
else:
|
|
220
|
+
out.append(" " + fname[:width-4])
|
|
221
|
+
|
|
222
|
+
# Status + footer
|
|
223
|
+
out.append("\033[" + str(height-2) + ";1H\033[K\033[90m" + ("-" * width) + "\033[0m")
|
|
224
|
+
out.append("\033[" + str(height-1) + ";1H\033[K " + ui.status[:width-2])
|
|
225
|
+
|
|
226
|
+
if ui.mode == 'editing':
|
|
227
|
+
footer = " Type value, Enter:Confirm Esc:Cancel "
|
|
228
|
+
elif ui.mode == 'gallery':
|
|
229
|
+
footer = " j/k:Nav o:Open b:Back q:Quit "
|
|
230
|
+
elif ui.mode == 'generating':
|
|
231
|
+
footer = " Generating... "
|
|
232
|
+
else:
|
|
233
|
+
footer = " j/k:Nav e:Edit Enter:Generate g:Gallery q:Quit "
|
|
234
|
+
out.append("\033[" + str(height) + ";1H\033[K\033[7m" + footer.ljust(width) + "\033[0m")
|
|
235
|
+
|
|
236
|
+
sys.stdout.write(''.join(out))
|
|
237
|
+
sys.stdout.flush()
|
|
238
|
+
|
|
239
|
+
# ========== Input Handling ==========
|
|
240
|
+
def handle_input(c, fd):
|
|
241
|
+
if ui.mode == 'editing':
|
|
242
|
+
return handle_edit(c, fd)
|
|
243
|
+
if ui.mode == 'gallery':
|
|
244
|
+
return handle_gallery(c, fd)
|
|
245
|
+
|
|
246
|
+
if c == '\x1b':
|
|
247
|
+
if select.select([fd], [], [], 0.05)[0]:
|
|
248
|
+
c2 = os.read(fd, 1).decode('latin-1')
|
|
249
|
+
if c2 == '[':
|
|
250
|
+
c3 = os.read(fd, 1).decode('latin-1')
|
|
251
|
+
if c3 == 'A':
|
|
252
|
+
ui.sel = max(0, ui.sel - 1)
|
|
253
|
+
elif c3 == 'B':
|
|
254
|
+
ui.sel = min(len(ui.params) - 1, ui.sel + 1)
|
|
255
|
+
return True
|
|
256
|
+
|
|
257
|
+
if c == 'q':
|
|
258
|
+
return False
|
|
259
|
+
elif c == 'j':
|
|
260
|
+
ui.sel = min(len(ui.params) - 1, ui.sel + 1)
|
|
261
|
+
elif c == 'k':
|
|
262
|
+
ui.sel = max(0, ui.sel - 1)
|
|
263
|
+
elif c == 'e' or c in ('\r', '\n'):
|
|
264
|
+
if c in ('\r', '\n') and ui.sel == 0 and ui.prompt:
|
|
265
|
+
# Enter on prompt with existing prompt = generate
|
|
266
|
+
generate_videos()
|
|
267
|
+
else:
|
|
268
|
+
ui.mode = 'editing'
|
|
269
|
+
ui.edit_buf = get_param_value(ui.sel)
|
|
270
|
+
if ui.edit_buf in ('(default)', '(auto)'):
|
|
271
|
+
ui.edit_buf = ""
|
|
272
|
+
ui.edit_cursor = len(ui.edit_buf)
|
|
273
|
+
elif c == 'g':
|
|
274
|
+
ui.mode = 'gallery'
|
|
275
|
+
ui.gallery_sel = 0
|
|
276
|
+
ui.gallery_scroll = 0
|
|
277
|
+
elif c == 'G':
|
|
278
|
+
generate_videos()
|
|
279
|
+
|
|
280
|
+
return True
|
|
281
|
+
|
|
282
|
+
def handle_edit(c, fd):
|
|
283
|
+
if c == '\x1b':
|
|
284
|
+
if select.select([fd], [], [], 0.05)[0]:
|
|
285
|
+
os.read(fd, 2)
|
|
286
|
+
ui.mode = 'params'
|
|
287
|
+
return True
|
|
288
|
+
|
|
289
|
+
if c in ('\r', '\n'):
|
|
290
|
+
set_param_value(ui.sel, ui.edit_buf)
|
|
291
|
+
ui.mode = 'params'
|
|
292
|
+
return True
|
|
293
|
+
|
|
294
|
+
if c == '\x7f' or c == '\x08':
|
|
295
|
+
if ui.edit_cursor > 0:
|
|
296
|
+
ui.edit_buf = ui.edit_buf[:ui.edit_cursor-1] + ui.edit_buf[ui.edit_cursor:]
|
|
297
|
+
ui.edit_cursor -= 1
|
|
298
|
+
elif c >= ' ' and c <= '~':
|
|
299
|
+
ui.edit_buf = ui.edit_buf[:ui.edit_cursor] + c + ui.edit_buf[ui.edit_cursor:]
|
|
300
|
+
ui.edit_cursor += 1
|
|
301
|
+
|
|
302
|
+
return True
|
|
303
|
+
|
|
304
|
+
def handle_gallery(c, fd):
|
|
305
|
+
if c == '\x1b':
|
|
306
|
+
if select.select([fd], [], [], 0.05)[0]:
|
|
307
|
+
c2 = os.read(fd, 1).decode('latin-1')
|
|
308
|
+
if c2 == '[':
|
|
309
|
+
c3 = os.read(fd, 1).decode('latin-1')
|
|
310
|
+
if c3 == 'A':
|
|
311
|
+
ui.gallery_sel = max(0, ui.gallery_sel - 1)
|
|
312
|
+
elif c3 == 'B':
|
|
313
|
+
ui.gallery_sel = min(max(0, len(ui.gallery) - 1), ui.gallery_sel + 1)
|
|
314
|
+
else:
|
|
315
|
+
ui.mode = 'params'
|
|
316
|
+
return True
|
|
317
|
+
|
|
318
|
+
if c == 'q':
|
|
319
|
+
return False
|
|
320
|
+
elif c == 'b':
|
|
321
|
+
ui.mode = 'params'
|
|
322
|
+
elif c == 'j':
|
|
323
|
+
ui.gallery_sel = min(max(0, len(ui.gallery) - 1), ui.gallery_sel + 1)
|
|
324
|
+
elif c == 'k':
|
|
325
|
+
ui.gallery_sel = max(0, ui.gallery_sel - 1)
|
|
326
|
+
elif c == 'o' and ui.gallery:
|
|
327
|
+
import subprocess
|
|
328
|
+
path = ui.gallery[ui.gallery_sel]['path']
|
|
329
|
+
try:
|
|
330
|
+
subprocess.Popen(['xdg-open', path], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
|
331
|
+
ui.status = "Opened: " + os.path.basename(path)
|
|
332
|
+
except:
|
|
333
|
+
ui.status = "Could not open video"
|
|
334
|
+
|
|
335
|
+
# Keep gallery_scroll in range
|
|
336
|
+
_, height = get_size()
|
|
337
|
+
gallery_h = height - 6
|
|
338
|
+
if ui.gallery_sel < ui.gallery_scroll:
|
|
339
|
+
ui.gallery_scroll = ui.gallery_sel
|
|
340
|
+
elif ui.gallery_sel >= ui.gallery_scroll + gallery_h:
|
|
341
|
+
ui.gallery_scroll = ui.gallery_sel - gallery_h + 1
|
|
342
|
+
|
|
343
|
+
return True
|
|
344
|
+
|
|
345
|
+
# ========== One-shot mode ==========
|
|
346
|
+
if ui.prompt and context.get('prompt'):
|
|
347
|
+
# If prompt provided via CLI args, generate immediately
|
|
348
|
+
generate_videos()
|
|
349
|
+
context['output'] = ui.status
|
|
350
|
+
context['messages'] = context.get('messages', [])
|
|
351
|
+
|
|
352
|
+
# ========== Main TUI Loop ==========
|
|
353
|
+
elif not sys.stdin.isatty():
|
|
354
|
+
context['output'] = "Roll requires an interactive terminal."
|
|
355
|
+
else:
|
|
356
|
+
fd = sys.stdin.fileno()
|
|
357
|
+
old_settings = termios.tcgetattr(fd)
|
|
358
|
+
|
|
359
|
+
try:
|
|
360
|
+
tty.setcbreak(fd)
|
|
361
|
+
sys.stdout.write('\033[?25l')
|
|
362
|
+
sys.stdout.write('\033[2J')
|
|
363
|
+
render_screen()
|
|
364
|
+
|
|
365
|
+
running = True
|
|
366
|
+
while running:
|
|
367
|
+
c = os.read(fd, 1).decode('latin-1')
|
|
368
|
+
running = handle_input(c, fd)
|
|
369
|
+
render_screen()
|
|
370
|
+
|
|
371
|
+
finally:
|
|
372
|
+
termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
|
|
373
|
+
sys.stdout.write('\033[?25h')
|
|
374
|
+
sys.stdout.write('\033[2J\033[H')
|
|
375
|
+
sys.stdout.flush()
|
|
376
|
+
|
|
377
|
+
context['output'] = ui.status
|
|
378
|
+
context['messages'] = context.get('messages', [])
|