drydock-cli 3.0.52__tar.gz → 3.0.53__tar.gz
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.
- {drydock_cli-3.0.52/drydock_cli.egg-info → drydock_cli-3.0.53}/PKG-INFO +1 -1
- {drydock_cli-3.0.52 → drydock_cli-3.0.53}/drydock/tui/app.py +39 -0
- {drydock_cli-3.0.52 → drydock_cli-3.0.53}/drydock/tui/widgets.py +1 -1
- {drydock_cli-3.0.52 → drydock_cli-3.0.53/drydock_cli.egg-info}/PKG-INFO +1 -1
- {drydock_cli-3.0.52 → drydock_cli-3.0.53}/pyproject.toml +1 -1
- {drydock_cli-3.0.52 → drydock_cli-3.0.53}/tests/test_tui.py +37 -0
- {drydock_cli-3.0.52 → drydock_cli-3.0.53}/LICENSE +0 -0
- {drydock_cli-3.0.52 → drydock_cli-3.0.53}/NOTICE +0 -0
- {drydock_cli-3.0.52 → drydock_cli-3.0.53}/README.md +0 -0
- {drydock_cli-3.0.52 → drydock_cli-3.0.53}/drydock/__init__.py +0 -0
- {drydock_cli-3.0.52 → drydock_cli-3.0.53}/drydock/__main__.py +0 -0
- {drydock_cli-3.0.52 → drydock_cli-3.0.53}/drydock/agent.py +0 -0
- {drydock_cli-3.0.52 → drydock_cli-3.0.53}/drydock/bash_safety.py +0 -0
- {drydock_cli-3.0.52 → drydock_cli-3.0.53}/drydock/cli.py +0 -0
- {drydock_cli-3.0.52 → drydock_cli-3.0.53}/drydock/compaction.py +0 -0
- {drydock_cli-3.0.52 → drydock_cli-3.0.53}/drydock/config.py +0 -0
- {drydock_cli-3.0.52 → drydock_cli-3.0.53}/drydock/detect.py +0 -0
- {drydock_cli-3.0.52 → drydock_cli-3.0.53}/drydock/gittools.py +0 -0
- {drydock_cli-3.0.52 → drydock_cli-3.0.53}/drydock/graphrag.py +0 -0
- {drydock_cli-3.0.52 → drydock_cli-3.0.53}/drydock/guards.py +0 -0
- {drydock_cli-3.0.52 → drydock_cli-3.0.53}/drydock/loop_detect.py +0 -0
- {drydock_cli-3.0.52 → drydock_cli-3.0.53}/drydock/providers.py +0 -0
- {drydock_cli-3.0.52 → drydock_cli-3.0.53}/drydock/skills.py +0 -0
- {drydock_cli-3.0.52 → drydock_cli-3.0.53}/drydock/tool_registry.py +0 -0
- {drydock_cli-3.0.52 → drydock_cli-3.0.53}/drydock/tools/__init__.py +0 -0
- {drydock_cli-3.0.52 → drydock_cli-3.0.53}/drydock/tui/__init__.py +0 -0
- {drydock_cli-3.0.52 → drydock_cli-3.0.53}/drydock/tui/approval.py +0 -0
- {drydock_cli-3.0.52 → drydock_cli-3.0.53}/drydock/tui/messages.py +0 -0
- {drydock_cli-3.0.52 → drydock_cli-3.0.53}/drydock/tuning.py +0 -0
- {drydock_cli-3.0.52 → drydock_cli-3.0.53}/drydock/web.py +0 -0
- {drydock_cli-3.0.52 → drydock_cli-3.0.53}/drydock_cli.egg-info/SOURCES.txt +0 -0
- {drydock_cli-3.0.52 → drydock_cli-3.0.53}/drydock_cli.egg-info/dependency_links.txt +0 -0
- {drydock_cli-3.0.52 → drydock_cli-3.0.53}/drydock_cli.egg-info/entry_points.txt +0 -0
- {drydock_cli-3.0.52 → drydock_cli-3.0.53}/drydock_cli.egg-info/requires.txt +0 -0
- {drydock_cli-3.0.52 → drydock_cli-3.0.53}/drydock_cli.egg-info/top_level.txt +0 -0
- {drydock_cli-3.0.52 → drydock_cli-3.0.53}/setup.cfg +0 -0
- {drydock_cli-3.0.52 → drydock_cli-3.0.53}/tests/test_approval.py +0 -0
- {drydock_cli-3.0.52 → drydock_cli-3.0.53}/tests/test_back_command.py +0 -0
- {drydock_cli-3.0.52 → drydock_cli-3.0.53}/tests/test_bash_process_group.py +0 -0
- {drydock_cli-3.0.52 → drydock_cli-3.0.53}/tests/test_bash_safety.py +0 -0
- {drydock_cli-3.0.52 → drydock_cli-3.0.53}/tests/test_bash_timeout_network.py +0 -0
- {drydock_cli-3.0.52 → drydock_cli-3.0.53}/tests/test_cli_agents.py +0 -0
- {drydock_cli-3.0.52 → drydock_cli-3.0.53}/tests/test_compact_command.py +0 -0
- {drydock_cli-3.0.52 → drydock_cli-3.0.53}/tests/test_compaction.py +0 -0
- {drydock_cli-3.0.52 → drydock_cli-3.0.53}/tests/test_config.py +0 -0
- {drydock_cli-3.0.52 → drydock_cli-3.0.53}/tests/test_config_migration.py +0 -0
- {drydock_cli-3.0.52 → drydock_cli-3.0.53}/tests/test_context_limit_config.py +0 -0
- {drydock_cli-3.0.52 → drydock_cli-3.0.53}/tests/test_detect.py +0 -0
- {drydock_cli-3.0.52 → drydock_cli-3.0.53}/tests/test_empty_response.py +0 -0
- {drydock_cli-3.0.52 → drydock_cli-3.0.53}/tests/test_failure_loop.py +0 -0
- {drydock_cli-3.0.52 → drydock_cli-3.0.53}/tests/test_first_run_setup.py +0 -0
- {drydock_cli-3.0.52 → drydock_cli-3.0.53}/tests/test_gittools.py +0 -0
- {drydock_cli-3.0.52 → drydock_cli-3.0.53}/tests/test_graphrag.py +0 -0
- {drydock_cli-3.0.52 → drydock_cli-3.0.53}/tests/test_guards_and_tools.py +0 -0
- {drydock_cli-3.0.52 → drydock_cli-3.0.53}/tests/test_hallucinated_tools.py +0 -0
- {drydock_cli-3.0.52 → drydock_cli-3.0.53}/tests/test_leaked_tool_call.py +0 -0
- {drydock_cli-3.0.52 → drydock_cli-3.0.53}/tests/test_loop_detect.py +0 -0
- {drydock_cli-3.0.52 → drydock_cli-3.0.53}/tests/test_oneshot_unreachable.py +0 -0
- {drydock_cli-3.0.52 → drydock_cli-3.0.53}/tests/test_plan_autocontinue.py +0 -0
- {drydock_cli-3.0.52 → drydock_cli-3.0.53}/tests/test_providers_unreachable.py +0 -0
- {drydock_cli-3.0.52 → drydock_cli-3.0.53}/tests/test_runaway_repetition.py +0 -0
- {drydock_cli-3.0.52 → drydock_cli-3.0.53}/tests/test_skills.py +0 -0
- {drydock_cli-3.0.52 → drydock_cli-3.0.53}/tests/test_stop.py +0 -0
- {drydock_cli-3.0.52 → drydock_cli-3.0.53}/tests/test_streaming_newlines.py +0 -0
- {drydock_cli-3.0.52 → drydock_cli-3.0.53}/tests/test_subagent.py +0 -0
- {drydock_cli-3.0.52 → drydock_cli-3.0.53}/tests/test_todo.py +0 -0
- {drydock_cli-3.0.52 → drydock_cli-3.0.53}/tests/test_tool_arg_parsing.py +0 -0
- {drydock_cli-3.0.52 → drydock_cli-3.0.53}/tests/test_tools_undo.py +0 -0
- {drydock_cli-3.0.52 → drydock_cli-3.0.53}/tests/test_tuning.py +0 -0
- {drydock_cli-3.0.52 → drydock_cli-3.0.53}/tests/test_vision_input.py +0 -0
- {drydock_cli-3.0.52 → drydock_cli-3.0.53}/tests/test_web_tools.py +0 -0
|
@@ -153,7 +153,10 @@ class DrydockApp(App):
|
|
|
153
153
|
self._abort_inflight()
|
|
154
154
|
dropped = len(self._queue)
|
|
155
155
|
self._queue.clear()
|
|
156
|
+
looping = self._repeat is not None
|
|
157
|
+
self._repeat = None # Esc/stop also ends an active /loop
|
|
156
158
|
note = "⏹ stopped." + (f" (discarded {dropped} queued)" if dropped else "")
|
|
159
|
+
note += " loop ended." if looping else ""
|
|
157
160
|
self._info(note)
|
|
158
161
|
self._refresh_status()
|
|
159
162
|
|
|
@@ -251,6 +254,7 @@ class DrydockApp(App):
|
|
|
251
254
|
# A plain dict survives run()'s dict(config) shallow copy by reference.
|
|
252
255
|
self.config["_abort"] = {}
|
|
253
256
|
self._queue: list[str] = [] # prompts submitted while a turn is running
|
|
257
|
+
self._repeat: dict | None = None # active /loop: {prompt, remaining, total}
|
|
254
258
|
self._ctx_tokens = 0 # current context size (last turn's prompt tokens)
|
|
255
259
|
self._ctrl_c_armed = False # first Ctrl+C arms; second within ~2s exits
|
|
256
260
|
# Live "working" line state.
|
|
@@ -489,10 +493,13 @@ class DrydockApp(App):
|
|
|
489
493
|
" /graphrag build/query a knowledge base from your docs\n"
|
|
490
494
|
" /graphrag build <path> · /graphrag status · /graphrag clear\n"
|
|
491
495
|
" /skills list your reusable /<name> skills\n"
|
|
496
|
+
" /loop /loop <count> <prompt> — repeat a prompt (Esc stops)\n"
|
|
492
497
|
" /clear reset the conversation\n"
|
|
493
498
|
" /quit exit\n"
|
|
494
499
|
"Type a task and press Enter. ↑/↓ recall history · Esc stops · Ctrl+O expands tools."
|
|
495
500
|
)
|
|
501
|
+
elif cmd == "/loop":
|
|
502
|
+
self._cmd_loop(arg)
|
|
496
503
|
elif cmd == "/skills":
|
|
497
504
|
self._cmd_skills()
|
|
498
505
|
elif cmd[1:] in self._skills:
|
|
@@ -500,6 +507,29 @@ class DrydockApp(App):
|
|
|
500
507
|
else:
|
|
501
508
|
self._mount(ErrorMessage(f"unknown command: {cmd} (try /help)"))
|
|
502
509
|
|
|
510
|
+
def _cmd_loop(self, arg: str) -> None:
|
|
511
|
+
"""/loop <count> <prompt> — run <prompt> up to <count> times (1–50),
|
|
512
|
+
re-submitting after each turn finishes. Esc (or /stop) ends the loop."""
|
|
513
|
+
parts = arg.split(maxsplit=1)
|
|
514
|
+
if len(parts) < 2 or not parts[0].isdigit():
|
|
515
|
+
self._info(
|
|
516
|
+
"usage: /loop <count> <prompt> e.g. /loop 5 fix the next "
|
|
517
|
+
"failing test and run pytest\n(count 1–50; Esc stops the loop)"
|
|
518
|
+
)
|
|
519
|
+
return
|
|
520
|
+
count = max(1, min(int(parts[0]), 50))
|
|
521
|
+
prompt = parts[1].strip()
|
|
522
|
+
if not prompt:
|
|
523
|
+
self._info("usage: /loop <count> <prompt>")
|
|
524
|
+
return
|
|
525
|
+
if self._busy:
|
|
526
|
+
self._info("A turn is already running — stop it (Esc) before starting a loop.")
|
|
527
|
+
return
|
|
528
|
+
self._repeat = {"prompt": prompt, "remaining": count, "total": count}
|
|
529
|
+
self._info(f"↻ looping {count}× (Esc to stop): {prompt}")
|
|
530
|
+
self._mount(UserMessage(prompt))
|
|
531
|
+
self._begin(prompt)
|
|
532
|
+
|
|
503
533
|
def _cmd_skills(self) -> None:
|
|
504
534
|
if not self._skills:
|
|
505
535
|
self._info(
|
|
@@ -780,6 +810,15 @@ class DrydockApp(App):
|
|
|
780
810
|
if self._queue:
|
|
781
811
|
self._begin(self._queue.pop(0))
|
|
782
812
|
return
|
|
813
|
+
# /loop: re-run the prompt until the iteration count is exhausted (Esc/
|
|
814
|
+
# stop clears self._repeat). Queued user prompts take priority (above).
|
|
815
|
+
if self._repeat and self._repeat["remaining"] > 1 and not self._cancel.is_set():
|
|
816
|
+
self._repeat["remaining"] -= 1
|
|
817
|
+
done = self._repeat["total"] - self._repeat["remaining"] + 1
|
|
818
|
+
self._info(f"↻ loop iteration {done}/{self._repeat['total']}")
|
|
819
|
+
self._begin(self._repeat["prompt"])
|
|
820
|
+
return
|
|
821
|
+
self._repeat = None
|
|
783
822
|
self._refresh_status()
|
|
784
823
|
self.query_one("#prompt", PromptArea).focus()
|
|
785
824
|
|
|
@@ -153,7 +153,7 @@ class PromptHistory:
|
|
|
153
153
|
# completes the prefix.
|
|
154
154
|
SLASH_COMMANDS = [
|
|
155
155
|
"/help", "/model", "/cwd", "/undo", "/back", "/stop", "/status",
|
|
156
|
-
"/compact", "/graphrag", "/clear", "/quit",
|
|
156
|
+
"/compact", "/graphrag", "/skills", "/loop", "/clear", "/quit",
|
|
157
157
|
]
|
|
158
158
|
|
|
159
159
|
|
|
@@ -7,7 +7,7 @@ build-backend = "setuptools.build_meta"
|
|
|
7
7
|
# PyPI distribution name is drydock-cli (the established install name, continued
|
|
8
8
|
# from the retired v2 fork); the import package + CLI command stay `drydock`.
|
|
9
9
|
name = "drydock-cli"
|
|
10
|
-
version = "3.0.
|
|
10
|
+
version = "3.0.53"
|
|
11
11
|
description = "Drydock — a local, provider-agnostic terminal coding agent for local LLMs"
|
|
12
12
|
readme = "README.md"
|
|
13
13
|
requires-python = ">=3.11"
|
|
@@ -348,3 +348,40 @@ def test_new_user_turn_clears_stale_plan():
|
|
|
348
348
|
assert str(panel.render()) == "" # panel cleared
|
|
349
349
|
|
|
350
350
|
asyncio.run(main())
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
def test_loop_command_iterates_and_stops():
|
|
354
|
+
"""/loop <n> <prompt> re-runs the prompt n times, then clears; Esc ends it."""
|
|
355
|
+
async def main():
|
|
356
|
+
app = DrydockApp({"model": "gemma4", "provider": "vllm", "cwd": "/tmp"})
|
|
357
|
+
async with app.run_test() as pilot:
|
|
358
|
+
await pilot.pause()
|
|
359
|
+
begins = []
|
|
360
|
+
app._begin = lambda text: begins.append(text) # stub out the real agent
|
|
361
|
+
|
|
362
|
+
# bad usage → no loop
|
|
363
|
+
app._handle_slash("/loop notanumber")
|
|
364
|
+
assert app._repeat is None and not begins
|
|
365
|
+
|
|
366
|
+
# /loop 3 <prompt> → starts, first iteration begun
|
|
367
|
+
app._handle_slash("/loop 3 do the thing")
|
|
368
|
+
assert app._repeat and app._repeat["total"] == 3
|
|
369
|
+
assert begins == ["do the thing"]
|
|
370
|
+
|
|
371
|
+
# each finished turn re-begins until the count is exhausted
|
|
372
|
+
from drydock.tui.messages import AgentFinished
|
|
373
|
+
app.on_agent_finished(AgentFinished()) # iter 2
|
|
374
|
+
app.on_agent_finished(AgentFinished()) # iter 3
|
|
375
|
+
assert len(begins) == 3
|
|
376
|
+
app.on_agent_finished(AgentFinished()) # done → loop cleared
|
|
377
|
+
assert app._repeat is None
|
|
378
|
+
assert len(begins) == 3 # no 4th run
|
|
379
|
+
|
|
380
|
+
# Esc/stop ends an active loop
|
|
381
|
+
app._begin = lambda text: begins.append(text)
|
|
382
|
+
app._handle_slash("/loop 5 again")
|
|
383
|
+
app._busy = True
|
|
384
|
+
app.action_stop()
|
|
385
|
+
assert app._repeat is None
|
|
386
|
+
|
|
387
|
+
asyncio.run(main())
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|