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.
Files changed (71) hide show
  1. {drydock_cli-3.0.52/drydock_cli.egg-info → drydock_cli-3.0.53}/PKG-INFO +1 -1
  2. {drydock_cli-3.0.52 → drydock_cli-3.0.53}/drydock/tui/app.py +39 -0
  3. {drydock_cli-3.0.52 → drydock_cli-3.0.53}/drydock/tui/widgets.py +1 -1
  4. {drydock_cli-3.0.52 → drydock_cli-3.0.53/drydock_cli.egg-info}/PKG-INFO +1 -1
  5. {drydock_cli-3.0.52 → drydock_cli-3.0.53}/pyproject.toml +1 -1
  6. {drydock_cli-3.0.52 → drydock_cli-3.0.53}/tests/test_tui.py +37 -0
  7. {drydock_cli-3.0.52 → drydock_cli-3.0.53}/LICENSE +0 -0
  8. {drydock_cli-3.0.52 → drydock_cli-3.0.53}/NOTICE +0 -0
  9. {drydock_cli-3.0.52 → drydock_cli-3.0.53}/README.md +0 -0
  10. {drydock_cli-3.0.52 → drydock_cli-3.0.53}/drydock/__init__.py +0 -0
  11. {drydock_cli-3.0.52 → drydock_cli-3.0.53}/drydock/__main__.py +0 -0
  12. {drydock_cli-3.0.52 → drydock_cli-3.0.53}/drydock/agent.py +0 -0
  13. {drydock_cli-3.0.52 → drydock_cli-3.0.53}/drydock/bash_safety.py +0 -0
  14. {drydock_cli-3.0.52 → drydock_cli-3.0.53}/drydock/cli.py +0 -0
  15. {drydock_cli-3.0.52 → drydock_cli-3.0.53}/drydock/compaction.py +0 -0
  16. {drydock_cli-3.0.52 → drydock_cli-3.0.53}/drydock/config.py +0 -0
  17. {drydock_cli-3.0.52 → drydock_cli-3.0.53}/drydock/detect.py +0 -0
  18. {drydock_cli-3.0.52 → drydock_cli-3.0.53}/drydock/gittools.py +0 -0
  19. {drydock_cli-3.0.52 → drydock_cli-3.0.53}/drydock/graphrag.py +0 -0
  20. {drydock_cli-3.0.52 → drydock_cli-3.0.53}/drydock/guards.py +0 -0
  21. {drydock_cli-3.0.52 → drydock_cli-3.0.53}/drydock/loop_detect.py +0 -0
  22. {drydock_cli-3.0.52 → drydock_cli-3.0.53}/drydock/providers.py +0 -0
  23. {drydock_cli-3.0.52 → drydock_cli-3.0.53}/drydock/skills.py +0 -0
  24. {drydock_cli-3.0.52 → drydock_cli-3.0.53}/drydock/tool_registry.py +0 -0
  25. {drydock_cli-3.0.52 → drydock_cli-3.0.53}/drydock/tools/__init__.py +0 -0
  26. {drydock_cli-3.0.52 → drydock_cli-3.0.53}/drydock/tui/__init__.py +0 -0
  27. {drydock_cli-3.0.52 → drydock_cli-3.0.53}/drydock/tui/approval.py +0 -0
  28. {drydock_cli-3.0.52 → drydock_cli-3.0.53}/drydock/tui/messages.py +0 -0
  29. {drydock_cli-3.0.52 → drydock_cli-3.0.53}/drydock/tuning.py +0 -0
  30. {drydock_cli-3.0.52 → drydock_cli-3.0.53}/drydock/web.py +0 -0
  31. {drydock_cli-3.0.52 → drydock_cli-3.0.53}/drydock_cli.egg-info/SOURCES.txt +0 -0
  32. {drydock_cli-3.0.52 → drydock_cli-3.0.53}/drydock_cli.egg-info/dependency_links.txt +0 -0
  33. {drydock_cli-3.0.52 → drydock_cli-3.0.53}/drydock_cli.egg-info/entry_points.txt +0 -0
  34. {drydock_cli-3.0.52 → drydock_cli-3.0.53}/drydock_cli.egg-info/requires.txt +0 -0
  35. {drydock_cli-3.0.52 → drydock_cli-3.0.53}/drydock_cli.egg-info/top_level.txt +0 -0
  36. {drydock_cli-3.0.52 → drydock_cli-3.0.53}/setup.cfg +0 -0
  37. {drydock_cli-3.0.52 → drydock_cli-3.0.53}/tests/test_approval.py +0 -0
  38. {drydock_cli-3.0.52 → drydock_cli-3.0.53}/tests/test_back_command.py +0 -0
  39. {drydock_cli-3.0.52 → drydock_cli-3.0.53}/tests/test_bash_process_group.py +0 -0
  40. {drydock_cli-3.0.52 → drydock_cli-3.0.53}/tests/test_bash_safety.py +0 -0
  41. {drydock_cli-3.0.52 → drydock_cli-3.0.53}/tests/test_bash_timeout_network.py +0 -0
  42. {drydock_cli-3.0.52 → drydock_cli-3.0.53}/tests/test_cli_agents.py +0 -0
  43. {drydock_cli-3.0.52 → drydock_cli-3.0.53}/tests/test_compact_command.py +0 -0
  44. {drydock_cli-3.0.52 → drydock_cli-3.0.53}/tests/test_compaction.py +0 -0
  45. {drydock_cli-3.0.52 → drydock_cli-3.0.53}/tests/test_config.py +0 -0
  46. {drydock_cli-3.0.52 → drydock_cli-3.0.53}/tests/test_config_migration.py +0 -0
  47. {drydock_cli-3.0.52 → drydock_cli-3.0.53}/tests/test_context_limit_config.py +0 -0
  48. {drydock_cli-3.0.52 → drydock_cli-3.0.53}/tests/test_detect.py +0 -0
  49. {drydock_cli-3.0.52 → drydock_cli-3.0.53}/tests/test_empty_response.py +0 -0
  50. {drydock_cli-3.0.52 → drydock_cli-3.0.53}/tests/test_failure_loop.py +0 -0
  51. {drydock_cli-3.0.52 → drydock_cli-3.0.53}/tests/test_first_run_setup.py +0 -0
  52. {drydock_cli-3.0.52 → drydock_cli-3.0.53}/tests/test_gittools.py +0 -0
  53. {drydock_cli-3.0.52 → drydock_cli-3.0.53}/tests/test_graphrag.py +0 -0
  54. {drydock_cli-3.0.52 → drydock_cli-3.0.53}/tests/test_guards_and_tools.py +0 -0
  55. {drydock_cli-3.0.52 → drydock_cli-3.0.53}/tests/test_hallucinated_tools.py +0 -0
  56. {drydock_cli-3.0.52 → drydock_cli-3.0.53}/tests/test_leaked_tool_call.py +0 -0
  57. {drydock_cli-3.0.52 → drydock_cli-3.0.53}/tests/test_loop_detect.py +0 -0
  58. {drydock_cli-3.0.52 → drydock_cli-3.0.53}/tests/test_oneshot_unreachable.py +0 -0
  59. {drydock_cli-3.0.52 → drydock_cli-3.0.53}/tests/test_plan_autocontinue.py +0 -0
  60. {drydock_cli-3.0.52 → drydock_cli-3.0.53}/tests/test_providers_unreachable.py +0 -0
  61. {drydock_cli-3.0.52 → drydock_cli-3.0.53}/tests/test_runaway_repetition.py +0 -0
  62. {drydock_cli-3.0.52 → drydock_cli-3.0.53}/tests/test_skills.py +0 -0
  63. {drydock_cli-3.0.52 → drydock_cli-3.0.53}/tests/test_stop.py +0 -0
  64. {drydock_cli-3.0.52 → drydock_cli-3.0.53}/tests/test_streaming_newlines.py +0 -0
  65. {drydock_cli-3.0.52 → drydock_cli-3.0.53}/tests/test_subagent.py +0 -0
  66. {drydock_cli-3.0.52 → drydock_cli-3.0.53}/tests/test_todo.py +0 -0
  67. {drydock_cli-3.0.52 → drydock_cli-3.0.53}/tests/test_tool_arg_parsing.py +0 -0
  68. {drydock_cli-3.0.52 → drydock_cli-3.0.53}/tests/test_tools_undo.py +0 -0
  69. {drydock_cli-3.0.52 → drydock_cli-3.0.53}/tests/test_tuning.py +0 -0
  70. {drydock_cli-3.0.52 → drydock_cli-3.0.53}/tests/test_vision_input.py +0 -0
  71. {drydock_cli-3.0.52 → drydock_cli-3.0.53}/tests/test_web_tools.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: drydock-cli
3
- Version: 3.0.52
3
+ Version: 3.0.53
4
4
  Summary: Drydock — a local, provider-agnostic terminal coding agent for local LLMs
5
5
  Author: Frank Bobe III
6
6
  License-Expression: Apache-2.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
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: drydock-cli
3
- Version: 3.0.52
3
+ Version: 3.0.53
4
4
  Summary: Drydock — a local, provider-agnostic terminal coding agent for local LLMs
5
5
  Author: Frank Bobe III
6
6
  License-Expression: Apache-2.0
@@ -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.52"
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