soothe-cli 0.7.2__tar.gz → 0.7.3__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 (131) hide show
  1. {soothe_cli-0.7.2 → soothe_cli-0.7.3}/PKG-INFO +1 -1
  2. {soothe_cli-0.7.2 → soothe_cli-0.7.3}/src/soothe_cli/cli/commands/autopilot_cmd.py +1 -31
  3. {soothe_cli-0.7.2 → soothe_cli-0.7.3}/src/soothe_cli/cli/commands/cron_cmd.py +13 -4
  4. {soothe_cli-0.7.2 → soothe_cli-0.7.3}/src/soothe_cli/cli/commands/status_cmd.py +26 -55
  5. {soothe_cli-0.7.2 → soothe_cli-0.7.3}/src/soothe_cli/cli/execution/daemon.py +2 -2
  6. {soothe_cli-0.7.2 → soothe_cli-0.7.3}/src/soothe_cli/runtime/state/session_stats.py +89 -44
  7. {soothe_cli-0.7.2 → soothe_cli-0.7.3}/src/soothe_cli/tui/_env_vars.py +28 -0
  8. {soothe_cli-0.7.2 → soothe_cli-0.7.3}/src/soothe_cli/tui/app/_app.py +12 -2
  9. {soothe_cli-0.7.2 → soothe_cli-0.7.3}/src/soothe_cli/tui/app/_execution.py +11 -31
  10. {soothe_cli-0.7.2 → soothe_cli-0.7.3}/src/soothe_cli/tui/app/_messages_mixin.py +120 -43
  11. {soothe_cli-0.7.2 → soothe_cli-0.7.3}/src/soothe_cli/tui/app/_model.py +15 -1
  12. {soothe_cli-0.7.2 → soothe_cli-0.7.3}/src/soothe_cli/tui/app/_startup.py +29 -23
  13. {soothe_cli-0.7.2 → soothe_cli-0.7.3}/src/soothe_cli/tui/app/_ui.py +28 -4
  14. {soothe_cli-0.7.2 → soothe_cli-0.7.3}/src/soothe_cli/tui/app/app.tcss +13 -1
  15. {soothe_cli-0.7.2 → soothe_cli-0.7.3}/src/soothe_cli/tui/command_registry.py +2 -8
  16. {soothe_cli-0.7.2 → soothe_cli-0.7.3}/src/soothe_cli/tui/commands/slash_commands.py +37 -10
  17. {soothe_cli-0.7.2 → soothe_cli-0.7.3}/src/soothe_cli/tui/commands/subagent_routing.py +3 -8
  18. {soothe_cli-0.7.2 → soothe_cli-0.7.3}/src/soothe_cli/tui/file_change_notify.py +23 -1
  19. {soothe_cli-0.7.2 → soothe_cli-0.7.3}/src/soothe_cli/tui/file_change_renderers.py +41 -0
  20. {soothe_cli-0.7.2 → soothe_cli-0.7.3}/src/soothe_cli/tui/preview_limits.py +4 -4
  21. soothe_cli-0.7.3/src/soothe_cli/tui/shell_color.py +48 -0
  22. soothe_cli-0.7.3/src/soothe_cli/tui/spinner_labels.py +55 -0
  23. {soothe_cli-0.7.2 → soothe_cli-0.7.3}/src/soothe_cli/tui/textual_adapter.py +397 -106
  24. soothe_cli-0.7.3/src/soothe_cli/tui/widgets/diff.py +228 -0
  25. {soothe_cli-0.7.2 → soothe_cli-0.7.3}/src/soothe_cli/tui/widgets/file_change_preview.py +111 -85
  26. soothe_cli-0.7.3/src/soothe_cli/tui/widgets/help_screen.py +210 -0
  27. {soothe_cli-0.7.2 → soothe_cli-0.7.3}/src/soothe_cli/tui/widgets/loading.py +35 -10
  28. {soothe_cli-0.7.2 → soothe_cli-0.7.3}/src/soothe_cli/tui/widgets/messages/assistant.py +8 -0
  29. {soothe_cli-0.7.2 → soothe_cli-0.7.3}/src/soothe_cli/tui/widgets/messages/clarification.py +6 -14
  30. {soothe_cli-0.7.2 → soothe_cli-0.7.3}/src/soothe_cli/tui/widgets/messages/cognition_goal_tree.py +126 -24
  31. {soothe_cli-0.7.2 → soothe_cli-0.7.3}/src/soothe_cli/tui/widgets/messages/cognition_step.py +5 -2
  32. {soothe_cli-0.7.2 → soothe_cli-0.7.3}/src/soothe_cli/tui/widgets/messages/cognition_step_activity.py +7 -0
  33. soothe_cli-0.7.3/src/soothe_cli/tui/widgets/plan_quick_view_overlay.py +150 -0
  34. soothe_cli-0.7.2/src/soothe_cli/tui/widgets/diff.py +0 -143
  35. {soothe_cli-0.7.2 → soothe_cli-0.7.3}/.gitignore +0 -0
  36. {soothe_cli-0.7.2 → soothe_cli-0.7.3}/README.md +0 -0
  37. {soothe_cli-0.7.2 → soothe_cli-0.7.3}/pyproject.toml +0 -0
  38. {soothe_cli-0.7.2 → soothe_cli-0.7.3}/src/soothe_cli/__init__.py +0 -0
  39. {soothe_cli-0.7.2 → soothe_cli-0.7.3}/src/soothe_cli/cli/__init__.py +0 -0
  40. {soothe_cli-0.7.2 → soothe_cli-0.7.3}/src/soothe_cli/cli/commands/__init__.py +0 -0
  41. {soothe_cli-0.7.2 → soothe_cli-0.7.3}/src/soothe_cli/cli/commands/loop_cmd.py +0 -0
  42. {soothe_cli-0.7.2 → soothe_cli-0.7.3}/src/soothe_cli/cli/commands/run_cmd.py +0 -0
  43. {soothe_cli-0.7.2 → soothe_cli-0.7.3}/src/soothe_cli/cli/execution/__init__.py +0 -0
  44. {soothe_cli-0.7.2 → soothe_cli-0.7.3}/src/soothe_cli/cli/execution/daemon_errors.py +0 -0
  45. {soothe_cli-0.7.2 → soothe_cli-0.7.3}/src/soothe_cli/cli/execution/headless.py +0 -0
  46. {soothe_cli-0.7.2 → soothe_cli-0.7.3}/src/soothe_cli/cli/execution/headless_renderer.py +0 -0
  47. {soothe_cli-0.7.2 → soothe_cli-0.7.3}/src/soothe_cli/cli/execution/launcher.py +0 -0
  48. {soothe_cli-0.7.2 → soothe_cli-0.7.3}/src/soothe_cli/cli/main.py +0 -0
  49. {soothe_cli-0.7.2 → soothe_cli-0.7.3}/src/soothe_cli/config/__init__.py +0 -0
  50. {soothe_cli-0.7.2 → soothe_cli-0.7.3}/src/soothe_cli/config/cli_config.py +0 -0
  51. {soothe_cli-0.7.2 → soothe_cli-0.7.3}/src/soothe_cli/config/loader.py +0 -0
  52. {soothe_cli-0.7.2 → soothe_cli-0.7.3}/src/soothe_cli/config/logging_setup.py +0 -0
  53. {soothe_cli-0.7.2 → soothe_cli-0.7.3}/src/soothe_cli/runtime/__init__.py +0 -0
  54. {soothe_cli-0.7.2 → soothe_cli-0.7.3}/src/soothe_cli/runtime/headless/processor.py +0 -0
  55. {soothe_cli-0.7.2 → soothe_cli-0.7.3}/src/soothe_cli/runtime/headless/processor_state.py +0 -0
  56. {soothe_cli-0.7.2 → soothe_cli-0.7.3}/src/soothe_cli/runtime/parse/message_processing.py +0 -0
  57. {soothe_cli-0.7.2 → soothe_cli-0.7.3}/src/soothe_cli/runtime/parse/tool_call_resolution.py +0 -0
  58. {soothe_cli-0.7.2 → soothe_cli-0.7.3}/src/soothe_cli/runtime/parse/tool_result.py +0 -0
  59. {soothe_cli-0.7.2 → soothe_cli-0.7.3}/src/soothe_cli/runtime/policy/display_policy.py +0 -0
  60. {soothe_cli-0.7.2 → soothe_cli-0.7.3}/src/soothe_cli/runtime/policy/essential_events.py +0 -0
  61. {soothe_cli-0.7.2 → soothe_cli-0.7.3}/src/soothe_cli/runtime/policy/tui_trace_log.py +0 -0
  62. {soothe_cli-0.7.2 → soothe_cli-0.7.3}/src/soothe_cli/runtime/presentation/duration_format.py +0 -0
  63. {soothe_cli-0.7.2 → soothe_cli-0.7.3}/src/soothe_cli/runtime/presentation/engine.py +0 -0
  64. {soothe_cli-0.7.2 → soothe_cli-0.7.3}/src/soothe_cli/runtime/presentation/explore_task_display.py +0 -0
  65. {soothe_cli-0.7.2 → soothe_cli-0.7.3}/src/soothe_cli/runtime/presentation/renderer_base.py +0 -0
  66. {soothe_cli-0.7.2 → soothe_cli-0.7.3}/src/soothe_cli/runtime/presentation/renderer_protocol.py +0 -0
  67. {soothe_cli-0.7.2 → soothe_cli-0.7.3}/src/soothe_cli/runtime/state/file_tracker.py +0 -0
  68. {soothe_cli-0.7.2 → soothe_cli-0.7.3}/src/soothe_cli/runtime/state/step_router.py +0 -0
  69. {soothe_cli-0.7.2 → soothe_cli-0.7.3}/src/soothe_cli/runtime/state/stream_accumulator.py +0 -0
  70. {soothe_cli-0.7.2 → soothe_cli-0.7.3}/src/soothe_cli/runtime/state/transcript.py +0 -0
  71. {soothe_cli-0.7.2 → soothe_cli-0.7.3}/src/soothe_cli/runtime/transport/session.py +0 -0
  72. {soothe_cli-0.7.2 → soothe_cli-0.7.3}/src/soothe_cli/runtime/turn/pipeline.py +0 -0
  73. {soothe_cli-0.7.2 → soothe_cli-0.7.3}/src/soothe_cli/runtime/turn/prepare.py +0 -0
  74. {soothe_cli-0.7.2 → soothe_cli-0.7.3}/src/soothe_cli/runtime/wire/chunk_filter.py +0 -0
  75. {soothe_cli-0.7.2 → soothe_cli-0.7.3}/src/soothe_cli/runtime/wire/display_text.py +0 -0
  76. {soothe_cli-0.7.2 → soothe_cli-0.7.3}/src/soothe_cli/runtime/wire/message_text.py +0 -0
  77. {soothe_cli-0.7.2 → soothe_cli-0.7.3}/src/soothe_cli/runtime/wire/messages.py +0 -0
  78. {soothe_cli-0.7.2 → soothe_cli-0.7.3}/src/soothe_cli/tui/__init__.py +0 -0
  79. {soothe_cli-0.7.2 → soothe_cli-0.7.3}/src/soothe_cli/tui/_cli_context.py +0 -0
  80. {soothe_cli-0.7.2 → soothe_cli-0.7.3}/src/soothe_cli/tui/_version.py +0 -0
  81. {soothe_cli-0.7.2 → soothe_cli-0.7.3}/src/soothe_cli/tui/app/__init__.py +0 -0
  82. {soothe_cli-0.7.2 → soothe_cli-0.7.3}/src/soothe_cli/tui/app/_commands.py +0 -0
  83. {soothe_cli-0.7.2 → soothe_cli-0.7.3}/src/soothe_cli/tui/app/_history.py +0 -0
  84. {soothe_cli-0.7.2 → soothe_cli-0.7.3}/src/soothe_cli/tui/app/_module_init.py +0 -0
  85. {soothe_cli-0.7.2 → soothe_cli-0.7.3}/src/soothe_cli/tui/binding.py +0 -0
  86. {soothe_cli-0.7.2 → soothe_cli-0.7.3}/src/soothe_cli/tui/commands/__init__.py +0 -0
  87. {soothe_cli-0.7.2 → soothe_cli-0.7.3}/src/soothe_cli/tui/commands/command_router.py +0 -0
  88. {soothe_cli-0.7.2 → soothe_cli-0.7.3}/src/soothe_cli/tui/config.py +0 -0
  89. {soothe_cli-0.7.2 → soothe_cli-0.7.3}/src/soothe_cli/tui/hooks.py +0 -0
  90. {soothe_cli-0.7.2 → soothe_cli-0.7.3}/src/soothe_cli/tui/input.py +0 -0
  91. {soothe_cli-0.7.2 → soothe_cli-0.7.3}/src/soothe_cli/tui/markdown_theme.py +0 -0
  92. {soothe_cli-0.7.2 → soothe_cli-0.7.3}/src/soothe_cli/tui/media_utils.py +0 -0
  93. {soothe_cli-0.7.2 → soothe_cli-0.7.3}/src/soothe_cli/tui/model_config.py +0 -0
  94. {soothe_cli-0.7.2 → soothe_cli-0.7.3}/src/soothe_cli/tui/path_utils.py +0 -0
  95. {soothe_cli-0.7.2 → soothe_cli-0.7.3}/src/soothe_cli/tui/project_utils.py +0 -0
  96. {soothe_cli-0.7.2 → soothe_cli-0.7.3}/src/soothe_cli/tui/sessions.py +0 -0
  97. {soothe_cli-0.7.2 → soothe_cli-0.7.3}/src/soothe_cli/tui/skills/__init__.py +0 -0
  98. {soothe_cli-0.7.2 → soothe_cli-0.7.3}/src/soothe_cli/tui/skills/invocation.py +0 -0
  99. {soothe_cli-0.7.2 → soothe_cli-0.7.3}/src/soothe_cli/tui/skills/load.py +0 -0
  100. {soothe_cli-0.7.2 → soothe_cli-0.7.3}/src/soothe_cli/tui/theme.py +0 -0
  101. {soothe_cli-0.7.2 → soothe_cli-0.7.3}/src/soothe_cli/tui/tips.py +0 -0
  102. {soothe_cli-0.7.2 → soothe_cli-0.7.3}/src/soothe_cli/tui/tool_display.py +0 -0
  103. {soothe_cli-0.7.2 → soothe_cli-0.7.3}/src/soothe_cli/tui/unicode_security.py +0 -0
  104. {soothe_cli-0.7.2 → soothe_cli-0.7.3}/src/soothe_cli/tui/update_check.py +0 -0
  105. {soothe_cli-0.7.2 → soothe_cli-0.7.3}/src/soothe_cli/tui/widgets/__init__.py +0 -0
  106. {soothe_cli-0.7.2 → soothe_cli-0.7.3}/src/soothe_cli/tui/widgets/_links.py +0 -0
  107. {soothe_cli-0.7.2 → soothe_cli-0.7.3}/src/soothe_cli/tui/widgets/autocomplete.py +0 -0
  108. {soothe_cli-0.7.2 → soothe_cli-0.7.3}/src/soothe_cli/tui/widgets/autopilot_dashboard.py +0 -0
  109. {soothe_cli-0.7.2 → soothe_cli-0.7.3}/src/soothe_cli/tui/widgets/autopilot_screen.py +0 -0
  110. {soothe_cli-0.7.2 → soothe_cli-0.7.3}/src/soothe_cli/tui/widgets/chat_input.py +0 -0
  111. {soothe_cli-0.7.2 → soothe_cli-0.7.3}/src/soothe_cli/tui/widgets/clipboard.py +0 -0
  112. {soothe_cli-0.7.2 → soothe_cli-0.7.3}/src/soothe_cli/tui/widgets/context_viewer.py +0 -0
  113. {soothe_cli-0.7.2 → soothe_cli-0.7.3}/src/soothe_cli/tui/widgets/editor.py +0 -0
  114. {soothe_cli-0.7.2 → soothe_cli-0.7.3}/src/soothe_cli/tui/widgets/history.py +0 -0
  115. {soothe_cli-0.7.2 → soothe_cli-0.7.3}/src/soothe_cli/tui/widgets/loop_selector.py +0 -0
  116. {soothe_cli-0.7.2 → soothe_cli-0.7.3}/src/soothe_cli/tui/widgets/mcp_viewer.py +0 -0
  117. {soothe_cli-0.7.2 → soothe_cli-0.7.3}/src/soothe_cli/tui/widgets/message_store.py +0 -0
  118. {soothe_cli-0.7.2 → soothe_cli-0.7.3}/src/soothe_cli/tui/widgets/messages/__init__.py +0 -0
  119. {soothe_cli-0.7.2 → soothe_cli-0.7.3}/src/soothe_cli/tui/widgets/messages/_helpers.py +0 -0
  120. {soothe_cli-0.7.2 → soothe_cli-0.7.3}/src/soothe_cli/tui/widgets/messages/app.py +0 -0
  121. {soothe_cli-0.7.2 → soothe_cli-0.7.3}/src/soothe_cli/tui/widgets/messages/cognition_reason.py +0 -0
  122. {soothe_cli-0.7.2 → soothe_cli-0.7.3}/src/soothe_cli/tui/widgets/messages/cognition_subagent.py +0 -0
  123. {soothe_cli-0.7.2 → soothe_cli-0.7.3}/src/soothe_cli/tui/widgets/messages/diff_message.py +0 -0
  124. {soothe_cli-0.7.2 → soothe_cli-0.7.3}/src/soothe_cli/tui/widgets/messages/error.py +0 -0
  125. {soothe_cli-0.7.2 → soothe_cli-0.7.3}/src/soothe_cli/tui/widgets/messages/skill.py +0 -0
  126. {soothe_cli-0.7.2 → soothe_cli-0.7.3}/src/soothe_cli/tui/widgets/messages/user.py +0 -0
  127. {soothe_cli-0.7.2 → soothe_cli-0.7.3}/src/soothe_cli/tui/widgets/model_selector.py +0 -0
  128. {soothe_cli-0.7.2 → soothe_cli-0.7.3}/src/soothe_cli/tui/widgets/notification_settings.py +0 -0
  129. {soothe_cli-0.7.2 → soothe_cli-0.7.3}/src/soothe_cli/tui/widgets/status.py +0 -0
  130. {soothe_cli-0.7.2 → soothe_cli-0.7.3}/src/soothe_cli/tui/widgets/theme_selector.py +0 -0
  131. {soothe_cli-0.7.2 → soothe_cli-0.7.3}/src/soothe_cli/tui/widgets/welcome.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: soothe-cli
3
- Version: 0.7.2
3
+ Version: 0.7.3
4
4
  Summary: Soothe CLI client - communicates with daemon via WebSocket
5
5
  Project-URL: Homepage, https://github.com/mirasoth/soothe
6
6
  Project-URL: Documentation, https://soothe.readthedocs.io
@@ -55,7 +55,7 @@ def run(
55
55
  max_iterations: int | None = typer.Option(
56
56
  None,
57
57
  "--max-iterations",
58
- help="Ignored — use daemon config agent.autonomous.max_iterations.",
58
+ help="Ignored — use daemon config agent.autopilot.max_iterations.",
59
59
  ),
60
60
  workspace: str | None = typer.Option(
61
61
  None,
@@ -333,36 +333,6 @@ def cancel_goal(
333
333
  typer.echo(f"Cancel result: {result.get('status', result)}")
334
334
 
335
335
 
336
- @app.command("approve")
337
- def approve_goal(
338
- goal_id: str = typer.Argument(
339
- ..., help="Confirmation ID to approve (use 'inbox' to list pending)."
340
- ),
341
- ) -> None:
342
- """Approve a MUST-confirmation goal."""
343
- client = _require_daemon_ws()
344
- try:
345
- result = client.autopilot_approve(goal_id)
346
- except RuntimeError as exc:
347
- typer.echo(str(exc), err=True)
348
- raise typer.Exit(1) from exc
349
- typer.echo(f"Confirmation approved: {result.get('goal_id', goal_id)}")
350
-
351
-
352
- @app.command("reject")
353
- def reject_goal(
354
- goal_id: str = typer.Argument(..., help="Confirmation ID to reject."),
355
- ) -> None:
356
- """Reject a proposed goal."""
357
- client = _require_daemon_ws()
358
- try:
359
- result = client.autopilot_reject(goal_id)
360
- except RuntimeError as exc:
361
- typer.echo(str(exc), err=True)
362
- raise typer.Exit(1) from exc
363
- typer.echo(f"Confirmation rejected: {result.get('goal_id', goal_id)}")
364
-
365
-
366
336
  @app.command("resume")
367
337
  def resume_goal(
368
338
  goal_id: str = typer.Argument(..., help="Goal ID to resume."),
@@ -85,11 +85,16 @@ def add_job(
85
85
  try:
86
86
  result = client.cron_add(text, priority=priority)
87
87
  except RuntimeError as exc:
88
- typer.echo(f"Error: {exc}", err=True)
88
+ message = str(exc)
89
+ if "Autopilot is disabled" in message:
90
+ console.print(f"[yellow]{message}[/yellow]")
91
+ else:
92
+ typer.echo(f"Error: {message}", err=True)
89
93
  sys.exit(1)
90
94
 
91
95
  job = result.get("job") or {}
92
96
  job_id = job.get("id", "?")
97
+ is_duplicate = bool(result.get("duplicate"))
93
98
  console.print(
94
99
  Panel(
95
100
  "\n".join(
@@ -101,10 +106,14 @@ def add_job(
101
106
  f"[bold]Status:[/bold] [green]{job.get('status', 'pending')}[/green]",
102
107
  ]
103
108
  ),
104
- title="Scheduled Job Created",
105
- border_style="green",
109
+ title="Scheduled Job Already Exists" if is_duplicate else "Scheduled Job Created",
110
+ border_style="yellow" if is_duplicate else "green",
106
111
  )
107
112
  )
113
+ if is_duplicate:
114
+ console.print(
115
+ "[yellow]An identical active job is already scheduled; no new job was created.[/yellow]"
116
+ )
108
117
 
109
118
 
110
119
  @app.command("list")
@@ -128,7 +137,7 @@ def list_jobs(
128
137
  """
129
138
  client = _require_cron_client()
130
139
  try:
131
- result = client.cron_list_jobs(status=status)
140
+ result = client.cron_list(status=status)
132
141
  except RuntimeError as exc:
133
142
  typer.echo(f"Error: {exc}", err=True)
134
143
  sys.exit(1)
@@ -50,47 +50,6 @@ async def _fetch_status(ws_url: str, timeout: float = 5.0) -> dict[str, Any]:
50
50
  await client.close()
51
51
 
52
52
 
53
- async def _fetch_ready_state(ws_url: str, timeout: float = 5.0) -> dict[str, Any] | None:
54
- """Fetch daemon readiness state via WebSocket connection_init/ack handshake.
55
-
56
- Sends ``connection_init`` and waits for ``connection_ack`` which carries
57
- the daemon's ``readiness_state``.
58
-
59
- Args:
60
- ws_url: WebSocket URL.
61
- timeout: Timeout for handshake.
62
-
63
- Returns:
64
- ``connection_ack`` result dict (containing ``readiness_state``) or None.
65
- """
66
- import websockets
67
-
68
- init_msg = json.dumps(
69
- {
70
- "proto": "1",
71
- "type": "connection_init",
72
- "params": {
73
- "client_version": "0.5.0",
74
- "accept_proto": ["1"],
75
- "capabilities": ["streaming"],
76
- },
77
- }
78
- )
79
-
80
- try:
81
- async with asyncio.timeout(timeout):
82
- async with websockets.connect(ws_url) as ws:
83
- await ws.send(init_msg)
84
- for _ in range(3):
85
- msg = await ws.recv()
86
- data = json.loads(msg)
87
- if data.get("type") == "connection_ack":
88
- return data.get("result", data)
89
- except Exception:
90
- pass
91
- return None
92
-
93
-
94
53
  def _render_unified_status_table(
95
54
  config: Any,
96
55
  ws_url: str,
@@ -156,10 +115,6 @@ def daemon_status(
156
115
  bool,
157
116
  typer.Option("--json", help="Output as JSON."),
158
117
  ] = False,
159
- verbose: Annotated[
160
- bool,
161
- typer.Option("--verbose", "-v", help="Show detailed status."),
162
- ] = False,
163
118
  ) -> None:
164
119
  """Check daemon status from client side.
165
120
 
@@ -168,7 +123,6 @@ def daemon_status(
168
123
  Examples:
169
124
  soothe status daemon
170
125
  soothe status daemon --json
171
- soothe status daemon -v
172
126
  """
173
127
  config = load_config()
174
128
  ws_url = websocket_url_from_config(config)
@@ -217,11 +171,6 @@ def daemon_status(
217
171
  )
218
172
  sys.exit(1)
219
173
 
220
- # Get readiness state for verbose mode
221
- ready_state = None
222
- if verbose:
223
- ready_state = asyncio.run(_fetch_ready_state(ws_url, timeout=5.0))
224
-
225
174
  if json_output:
226
175
  output = {
227
176
  "status": "running",
@@ -232,10 +181,10 @@ def daemon_status(
232
181
  "daemon_pid": status.get("daemon_pid"),
233
182
  "daemon_version": status.get("daemon_version"),
234
183
  "core_version": status.get("core_version"),
184
+ "readiness_state": status.get("readiness_state", "unknown"),
235
185
  }
236
- if ready_state:
237
- output["readiness_state"] = ready_state.get("state", "unknown")
238
- output["readiness_message"] = ready_state.get("message")
186
+ if status.get("readiness_message"):
187
+ output["readiness_message"] = status.get("readiness_message")
239
188
  console.print_json(json.dumps(output))
240
189
  return
241
190
 
@@ -246,6 +195,15 @@ def daemon_status(
246
195
  daemon_pid = status.get("daemon_pid")
247
196
  daemon_version = status.get("daemon_version")
248
197
  core_version = status.get("core_version")
198
+ # Use readiness_state from daemon_status RPC (already includes state + message)
199
+ readiness_state_from_status = (
200
+ {
201
+ "state": status.get("readiness_state", "unknown"),
202
+ "message": status.get("readiness_message"),
203
+ }
204
+ if status.get("readiness_state")
205
+ else None
206
+ )
249
207
 
250
208
  table = _render_unified_status_table(
251
209
  config,
@@ -254,7 +212,7 @@ def daemon_status(
254
212
  port_live,
255
213
  active_threads,
256
214
  daemon_pid,
257
- ready_state,
215
+ readiness_state_from_status,
258
216
  daemon_live=True,
259
217
  daemon_version=daemon_version,
260
218
  core_version=core_version,
@@ -349,6 +307,9 @@ def status_main(
349
307
  output["daemon"]["daemon_pid"] = status.get("daemon_pid")
350
308
  output["daemon"]["daemon_version"] = status.get("daemon_version")
351
309
  output["daemon"]["core_version"] = status.get("core_version")
310
+ output["daemon"]["readiness_state"] = status.get("readiness_state", "unknown")
311
+ if status.get("readiness_message"):
312
+ output["daemon"]["readiness_message"] = status.get("readiness_message")
352
313
  console.print_json(json.dumps(output))
353
314
  return
354
315
 
@@ -378,6 +339,15 @@ def status_main(
378
339
  daemon_pid = status.get("daemon_pid")
379
340
  daemon_version = status.get("daemon_version")
380
341
  core_version = status.get("core_version")
342
+ # Use readiness_state from daemon_status RPC (already includes state + message)
343
+ readiness_state_from_status = (
344
+ {
345
+ "state": status.get("readiness_state", "unknown"),
346
+ "message": status.get("readiness_message"),
347
+ }
348
+ if status.get("readiness_state")
349
+ else None
350
+ )
381
351
 
382
352
  table = _render_unified_status_table(
383
353
  config,
@@ -386,6 +356,7 @@ def status_main(
386
356
  port_live,
387
357
  active_threads,
388
358
  daemon_pid,
359
+ readiness_state_from_status,
389
360
  daemon_live=True,
390
361
  daemon_version=daemon_version,
391
362
  core_version=core_version,
@@ -8,7 +8,6 @@ from __future__ import annotations
8
8
 
9
9
  import asyncio
10
10
  import logging
11
- import os
12
11
  import signal
13
12
  from typing import Any
14
13
 
@@ -28,6 +27,7 @@ from soothe_cli.cli.execution.daemon_errors import (
28
27
  from soothe_cli.cli.execution.headless_renderer import HeadlessCliRenderer
29
28
  from soothe_cli.runtime import EventProcessor
30
29
  from soothe_cli.runtime.presentation.engine import PresentationEngine
30
+ from soothe_cli.tui._env_vars import resolve_cli_loop_workspace
31
31
  from soothe_cli.tui.commands.subagent_routing import parse_subagent_from_input
32
32
 
33
33
  logger = logging.getLogger(__name__)
@@ -151,7 +151,7 @@ async def _run_headless_session_once(
151
151
 
152
152
  try:
153
153
  await connect_websocket_with_retries(client)
154
- cli_ws = os.environ.get("SOOTHE_CLI_WORKSPACE", "").strip() or os.getcwd()
154
+ cli_ws = resolve_cli_loop_workspace()
155
155
  # IG-441: three first-class modes (batch / adaptive / streaming).
156
156
  # Default is ``adaptive`` for headless runs as well — it gives smooth
157
157
  # progress on long synthesis while keeping wire traffic bounded.
@@ -8,14 +8,16 @@ config, no widget imports) so that `app.py` can import `SessionStats` and
8
8
 
9
9
  from __future__ import annotations
10
10
 
11
+ import json
11
12
  import time
12
13
  from dataclasses import dataclass, field
14
+ from typing import Any
13
15
 
14
16
  SpinnerStatus = str | None
15
17
  """Spinner line label, or `None` to hide.
16
18
 
17
- Common values include ``Thinking``, ``Offloading``, ``Writing`` (assistant streaming),
18
- ``Tools`` (tool execution), and ``Synthesizing`` (goal-completion stream).
19
+ Common single-word values include ``Thinking``, ``Planning``, ``Executing``,
20
+ ``Tools``, ``Offloading``, ``Writing``, and ``Synthesizing``.
19
21
  """
20
22
 
21
23
 
@@ -242,48 +244,91 @@ class TurnEventStats:
242
244
  self.latency = TurnLatencyStats()
243
245
  self.latency.merge(other.latency)
244
246
 
245
- def summary_line(self) -> str:
246
- """Return a one-line summary suitable for structured logging.
247
-
248
- Returns:
249
- Compact string like ``847 total (612 msg, 89 upd, 146 custom; …)``.
250
- """
251
- parts = [f"{self.total} total"]
252
- mode_parts = []
253
- if self.messages:
254
- mode_parts.append(f"{self.messages} msg")
255
- if self.updates:
256
- mode_parts.append(f"{self.updates} upd")
257
- if self.custom:
258
- mode_parts.append(f"{self.custom} custom")
259
- if mode_parts:
260
- parts.append(f"({', '.join(mode_parts)})")
261
-
262
- detail_parts = []
263
- if self.tool_calls:
264
- detail_parts.append(f"{self.tool_calls} tools")
265
- if self.tool_results:
266
- detail_parts.append(f"{self.tool_results} results")
267
- if self.text_chunks:
268
- detail_parts.append(f"{self.text_chunks} text")
269
- if self.skipped:
270
- detail_parts.append(f"{self.skipped} skipped")
271
- if self.filtered_early:
272
- detail_parts.append(f"{self.filtered_early} filtered-early")
273
- if self.heartbeats_dropped:
274
- detail_parts.append(f"{self.heartbeats_dropped} hb-drop")
275
- if self.post_idle_drained:
276
- detail_parts.append(f"{self.post_idle_drained} post-idle")
277
- if self.inbound_dropped:
278
- detail_parts.append(f"{self.inbound_dropped} inbound-drop")
279
- if self.latency is not None and self.latency.time_to_first_chunk_ms is not None:
280
- detail_parts.append(f"ttfc={self.latency.time_to_first_chunk_ms:.0f}ms")
281
- if self.latency is not None and self.latency.synthesis_visible_ms is not None:
282
- detail_parts.append(f"synth={self.latency.synthesis_visible_ms:.0f}ms")
283
- if detail_parts:
284
- parts.append("; ".join(detail_parts))
285
-
286
- return " ".join(parts)
247
+ @staticmethod
248
+ def _nonzero_fields(**fields: int) -> dict[str, int]:
249
+ return {key: value for key, value in fields.items() if value}
250
+
251
+ def to_log_dict(self) -> dict[str, Any]:
252
+ """Return structured event counters for ``cli.log`` JSON lines."""
253
+ payload: dict[str, Any] = {"total": self.total}
254
+
255
+ modes = self._nonzero_fields(
256
+ messages=self.messages,
257
+ updates=self.updates,
258
+ custom=self.custom,
259
+ )
260
+ if modes:
261
+ payload["modes"] = modes
262
+
263
+ activity = self._nonzero_fields(
264
+ tool_calls=self.tool_calls,
265
+ tool_results=self.tool_results,
266
+ text_chunks=self.text_chunks,
267
+ )
268
+ if activity:
269
+ payload["activity"] = activity
270
+
271
+ degradation = self._nonzero_fields(
272
+ skipped=self.skipped,
273
+ filtered_early=self.filtered_early,
274
+ heartbeats_dropped=self.heartbeats_dropped,
275
+ post_idle_drained=self.post_idle_drained,
276
+ inbound_dropped=self.inbound_dropped,
277
+ )
278
+ if degradation:
279
+ payload["degradation"] = degradation
280
+
281
+ if self.latency is not None:
282
+ latency: dict[str, int] = {}
283
+ if self.latency.time_to_first_chunk_ms is not None:
284
+ latency["ttfc_ms"] = round(self.latency.time_to_first_chunk_ms)
285
+ if self.latency.synthesis_visible_ms is not None:
286
+ latency["synthesis_ms"] = round(self.latency.synthesis_visible_ms)
287
+ if latency:
288
+ payload["latency"] = latency
289
+
290
+ return payload
291
+
292
+
293
+ def build_goal_completed_log_event(
294
+ ev_stats: TurnEventStats,
295
+ *,
296
+ status: str,
297
+ goal_progress: str,
298
+ total_steps: int,
299
+ elapsed_seconds: float,
300
+ ) -> dict[str, Any]:
301
+ """Build a structured goal-completion record for ``cli.log``."""
302
+ event: dict[str, Any] = {
303
+ "event": "goal_completed",
304
+ "elapsed_s": round(elapsed_seconds, 1),
305
+ "events": ev_stats.to_log_dict(),
306
+ }
307
+ if status.strip():
308
+ event["status"] = status.strip()
309
+ if goal_progress.strip():
310
+ event["progress"] = goal_progress.strip()
311
+ if total_steps > 0:
312
+ event["steps"] = total_steps
313
+ return event
314
+
315
+
316
+ def build_turn_finished_log_event(
317
+ ev_stats: TurnEventStats,
318
+ *,
319
+ wall_seconds: float,
320
+ ) -> dict[str, Any]:
321
+ """Build a structured turn-finished record for ``cli.log``."""
322
+ return {
323
+ "event": "turn_finished",
324
+ "wall_s": round(wall_seconds, 1),
325
+ "events": ev_stats.to_log_dict(),
326
+ }
327
+
328
+
329
+ def format_cli_log_event(payload: dict[str, Any]) -> str:
330
+ """Serialize a structured CLI log event as compact JSON."""
331
+ return json.dumps(payload, separators=(",", ":"), sort_keys=True)
287
332
 
288
333
 
289
334
  def format_token_count(count: int) -> str:
@@ -61,3 +61,31 @@ TUI_REFRESH_INTERVAL_MS = "SOOTHE_CLI_TUI_REFRESH_INTERVAL_MS"
61
61
  Set to throttle frequent refreshes during streaming, reducing UI lag.
62
62
  Lower values = more responsive but more CPU load; higher values = smoother.
63
63
  """
64
+
65
+ WORKSPACE = "SOOTHE_CLI_WORKSPACE"
66
+ """Explicit project directory sent to the daemon on ``loop_new`` (defaults to cwd)."""
67
+
68
+ OMIT_WORKSPACE = "SOOTHE_CLI_OMIT_WORKSPACE"
69
+ """Optional: when ``1``/``true``/``yes``, omit ``workspace`` on ``loop_new`` (default: send cwd)."""
70
+
71
+
72
+ def resolve_cli_loop_workspace() -> str | None:
73
+ """Return the workspace path to send on ``loop_new``, or ``None`` to omit it.
74
+
75
+ By default sends ``cwd``. The daemon ignores host paths that are not present
76
+ on the daemon filesystem (falls back to persisted layout) unless
77
+ ``workspace_mount`` is configured (RFC-621). ``SOOTHE_CLI_OMIT_WORKSPACE`` is
78
+ optional — use it only to skip sending ``workspace`` on the wire.
79
+ """
80
+ import os
81
+
82
+ omit = os.environ.get(OMIT_WORKSPACE, "").strip().lower()
83
+ if omit in ("1", "true", "yes", "on"):
84
+ return None
85
+
86
+ explicit = os.environ.get(WORKSPACE, "").strip()
87
+ if explicit.lower() in ("none", "-", "omit"):
88
+ return None
89
+ if explicit:
90
+ return explicit
91
+ return os.getcwd()
@@ -43,6 +43,7 @@ from soothe_cli.tui.widgets.messages import (
43
43
  AssistantMessage,
44
44
  QueuedUserMessage,
45
45
  )
46
+ from soothe_cli.tui.widgets.plan_quick_view_overlay import PlanQuickViewOverlay
46
47
  from soothe_cli.tui.widgets.status import StatusBar
47
48
  from soothe_cli.tui.widgets.welcome import WelcomeBanner
48
49
 
@@ -79,7 +80,7 @@ class SootheApp(
79
80
  """Vertical scroll speed (reduced from Textual default for finer control)."""
80
81
 
81
82
  BINDINGS: ClassVar[list[BindingType]] = [
82
- Binding("escape", "interrupt", "Interrupt", show=False, priority=True),
83
+ Binding("escape", "dismiss_ui", "Dismiss", show=False, priority=True),
83
84
  Binding(
84
85
  "ctrl+c",
85
86
  "quit_or_interrupt",
@@ -115,8 +116,15 @@ class SootheApp(
115
116
  "Copy Selection",
116
117
  show=False,
117
118
  ),
119
+ Binding(
120
+ "ctrl+t",
121
+ "toggle_plan_quick_view",
122
+ "Plan View",
123
+ show=False,
124
+ priority=True,
125
+ ),
118
126
  ]
119
- """App-level keybindings for interrupt, quit, and navigation."""
127
+ """App-level keybindings for dismiss, quit, and navigation."""
120
128
 
121
129
  class ServerStartFailed(Message):
122
130
  """Posted when daemon bootstrap or background connection fails."""
@@ -216,6 +224,7 @@ class SootheApp(
216
224
  self._status_bar: StatusBar | None = None
217
225
 
218
226
  self._chat_input: ChatInput | None = None
227
+ self._plan_quick_view_overlay: PlanQuickViewOverlay | None = None
219
228
 
220
229
  self._quit_pending = False
221
230
 
@@ -374,6 +383,7 @@ class SootheApp(
374
383
  )
375
384
  yield Container(id="messages")
376
385
  with Container(id="bottom-app-container"):
386
+ yield PlanQuickViewOverlay(id="plan-quick-view-overlay")
377
387
  yield Container(id="thinking-status")
378
388
  yield ChatInput(
379
389
  cwd=self._cwd,
@@ -28,7 +28,6 @@ from soothe_cli.cli.execution.daemon_errors import (
28
28
  from soothe_cli.runtime.state.session_stats import SessionStats, format_token_count
29
29
  from soothe_cli.tui import theme
30
30
  from soothe_cli.tui._cli_context import CLIContext
31
- from soothe_cli.tui._version import DOCS_URL
32
31
  from soothe_cli.tui.app._module_init import (
33
32
  _COMMAND_URLS,
34
33
  DeferredAction,
@@ -36,6 +35,7 @@ from soothe_cli.tui.app._module_init import (
36
35
  _extract_model_params_flag,
37
36
  )
38
37
  from soothe_cli.tui.hooks import dispatch_hook
38
+ from soothe_cli.tui.shell_color import shell_subprocess_env, wrap_shell_command_for_color
39
39
  from soothe_cli.tui.widgets.chat_input import ChatInput
40
40
  from soothe_cli.tui.widgets.messages import (
41
41
  AppMessage,
@@ -288,10 +288,11 @@ class _ExecutionMixin:
288
288
  """
289
289
  try:
290
290
  proc = await asyncio.create_subprocess_shell(
291
- command,
291
+ wrap_shell_command_for_color(command),
292
292
  stdout=asyncio.subprocess.PIPE,
293
293
  stderr=asyncio.subprocess.PIPE,
294
294
  cwd=self._cwd,
295
+ env=shell_subprocess_env(),
295
296
  start_new_session=(sys.platform != "win32"),
296
297
  )
297
298
  self._shell_process = proc
@@ -312,7 +313,7 @@ class _ExecutionMixin:
312
313
  output += f"\n[stderr]\n{stderr_text}"
313
314
 
314
315
  if output:
315
- msg = AssistantMessage(f"```\n{output}\n```")
316
+ msg = AssistantMessage(output, render_markdown=False, render_ansi=True)
316
317
  await self._mount_message(msg)
317
318
  await msg.write_initial_content()
318
319
  else:
@@ -461,7 +462,7 @@ class _ExecutionMixin:
461
462
  )
462
463
  from soothe_cli.tui.commands.slash_commands import COMMANDS as _RFC404_COMMANDS
463
464
 
464
- # RFC-454 daemon *routing* commands (/research, /explore, /plan, optional /«id»):
465
+ # RFC-454 daemon *routing* commands (/research, /plan, optional /«id»):
465
466
  # send the full line as a normal user turn so ``parse_subagent_from_input``
466
467
  # runs in the daemon adapter (same as headless CLI). Without this branch,
467
468
  # ``cmd == "/research …"`` never matches the bare single-token handlers below.
@@ -484,37 +485,14 @@ class _ExecutionMixin:
484
485
  self.query_one("#chat", VerticalScroll).anchor()
485
486
  return
486
487
 
487
- from soothe_cli.tui.config import newline_shortcut, settings
488
+ from soothe_cli.tui.config import settings
488
489
 
489
490
  cmd = command.lower().strip()
490
491
 
491
492
  if cmd in {"/quit", "/q"}:
492
493
  self._detach_or_exit()
493
494
  elif cmd == "/help":
494
- await self._mount_message(UserMessage(command))
495
- help_body = (
496
- "Commands: /quit, /clear, /editor, /autopilot <task>, /cron <schedule>, "
497
- "/context, /mcp, "
498
- "/model [--model-params JSON] [--default], /notifications, "
499
- "/reload, /skill:<name>, /theme, "
500
- "/tokens, /resume, "
501
- "/research, /explore, /plan, /«subagent» (when configured), "
502
- "/update, /auto-update, /changelog, /docs, /feedback, /help\n\n"
503
- "Interactive Features:\n"
504
- " Enter Submit your message\n"
505
- f" {newline_shortcut():<15} Insert newline\n"
506
- " Ctrl+X Open prompt in external editor\n"
507
- " Shift+Tab Cycle loop selector\n"
508
- " @filename Auto-complete files and inject content\n"
509
- " /command Slash commands (/help, /clear, /quit)\n"
510
- " !command Run shell commands directly\n\n"
511
- "Docs: "
512
- )
513
- help_text = Content.assemble(
514
- (help_body, "dim italic"),
515
- (DOCS_URL, TStyle(dim=True, italic=True, link=DOCS_URL)),
516
- )
517
- await self._mount_message(AppMessage(help_text))
495
+ await self._show_help_screen()
518
496
 
519
497
  elif cmd in {"/changelog", "/docs", "/feedback"}:
520
498
  await self._open_url_command(command, cmd)
@@ -1031,9 +1009,11 @@ class _ExecutionMixin:
1031
1009
  self._agent_running = False
1032
1010
  self._agent_worker = None
1033
1011
 
1034
- # Restore input focus FIRST so the user can start typing immediately
1012
+ # Restore input focus first so the user can type immediately
1035
1013
  # while remaining cleanup (spinner, tokens, deferred) runs.
1036
- if self._chat_input:
1014
+ if self._primary_text_input() is not None:
1015
+ self.focus_primary_input()
1016
+ elif self._chat_input:
1037
1017
  self._chat_input.set_cursor_active(active=True)
1038
1018
 
1039
1019
  # Remove spinner if present