refactorai-cli 0.7.19__tar.gz → 0.7.22__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 (54) hide show
  1. {refactorai_cli-0.7.19 → refactorai_cli-0.7.22}/PKG-INFO +1 -1
  2. {refactorai_cli-0.7.19 → refactorai_cli-0.7.22}/pyproject.toml +1 -1
  3. {refactorai_cli-0.7.19 → refactorai_cli-0.7.22}/refactorai_cli/__init__.py +1 -1
  4. {refactorai_cli-0.7.19 → refactorai_cli-0.7.22}/refactorai_cli/commands/run_cmds.py +9 -21
  5. {refactorai_cli-0.7.19 → refactorai_cli-0.7.22}/refactorai_cli/commands/watch_cmds.py +70 -5
  6. {refactorai_cli-0.7.19 → refactorai_cli-0.7.22}/refactorai_cli/review_runner.py +62 -10
  7. {refactorai_cli-0.7.19 → refactorai_cli-0.7.22}/refactorai_cli/watch_state.py +57 -5
  8. {refactorai_cli-0.7.19 → refactorai_cli-0.7.22}/refactorai_cli.egg-info/PKG-INFO +1 -1
  9. {refactorai_cli-0.7.19 → refactorai_cli-0.7.22}/README.md +0 -0
  10. {refactorai_cli-0.7.19 → refactorai_cli-0.7.22}/refactorai_cli/auth.py +0 -0
  11. {refactorai_cli-0.7.19 → refactorai_cli-0.7.22}/refactorai_cli/client.py +0 -0
  12. {refactorai_cli-0.7.19 → refactorai_cli-0.7.22}/refactorai_cli/cloud_rr.py +0 -0
  13. {refactorai_cli-0.7.19 → refactorai_cli-0.7.22}/refactorai_cli/commands/__init__.py +0 -0
  14. {refactorai_cli-0.7.19 → refactorai_cli-0.7.22}/refactorai_cli/commands/account_cmds.py +0 -0
  15. {refactorai_cli-0.7.19 → refactorai_cli-0.7.22}/refactorai_cli/commands/auth_cmds.py +0 -0
  16. {refactorai_cli-0.7.19 → refactorai_cli-0.7.22}/refactorai_cli/commands/branch_cmds.py +0 -0
  17. {refactorai_cli-0.7.19 → refactorai_cli-0.7.22}/refactorai_cli/commands/cloud_cmds.py +0 -0
  18. {refactorai_cli-0.7.19 → refactorai_cli-0.7.22}/refactorai_cli/commands/engine_cmds.py +0 -0
  19. {refactorai_cli-0.7.19 → refactorai_cli-0.7.22}/refactorai_cli/commands/hook_cmds.py +0 -0
  20. {refactorai_cli-0.7.19 → refactorai_cli-0.7.22}/refactorai_cli/commands/model_cmds.py +0 -0
  21. {refactorai_cli-0.7.19 → refactorai_cli-0.7.22}/refactorai_cli/commands/policy_cmds.py +0 -0
  22. {refactorai_cli-0.7.19 → refactorai_cli-0.7.22}/refactorai_cli/commands/pre_push_cmds.py +0 -0
  23. {refactorai_cli-0.7.19 → refactorai_cli-0.7.22}/refactorai_cli/commands/request_cmds.py +0 -0
  24. {refactorai_cli-0.7.19 → refactorai_cli-0.7.22}/refactorai_cli/commands/rules_cmds.py +0 -0
  25. {refactorai_cli-0.7.19 → refactorai_cli-0.7.22}/refactorai_cli/commands/runtime_cmds.py +0 -0
  26. {refactorai_cli-0.7.19 → refactorai_cli-0.7.22}/refactorai_cli/commands/runtime_proxy_cmds.py +0 -0
  27. {refactorai_cli-0.7.19 → refactorai_cli-0.7.22}/refactorai_cli/commands/setup_cmds.py +0 -0
  28. {refactorai_cli-0.7.19 → refactorai_cli-0.7.22}/refactorai_cli/commands/toolchains_cmds.py +0 -0
  29. {refactorai_cli-0.7.19 → refactorai_cli-0.7.22}/refactorai_cli/commands/workspace_cmds.py +0 -0
  30. {refactorai_cli-0.7.19 → refactorai_cli-0.7.22}/refactorai_cli/commit_queue.py +0 -0
  31. {refactorai_cli-0.7.19 → refactorai_cli-0.7.22}/refactorai_cli/commit_telemetry.py +0 -0
  32. {refactorai_cli-0.7.19 → refactorai_cli-0.7.22}/refactorai_cli/control_plane.py +0 -0
  33. {refactorai_cli-0.7.19 → refactorai_cli-0.7.22}/refactorai_cli/credentials.py +0 -0
  34. {refactorai_cli-0.7.19 → refactorai_cli-0.7.22}/refactorai_cli/dotenv_loader.py +0 -0
  35. {refactorai_cli-0.7.19 → refactorai_cli-0.7.22}/refactorai_cli/git_scope.py +0 -0
  36. {refactorai_cli-0.7.19 → refactorai_cli-0.7.22}/refactorai_cli/local_constitution.py +0 -0
  37. {refactorai_cli-0.7.19 → refactorai_cli-0.7.22}/refactorai_cli/local_engine_runtime.py +0 -0
  38. {refactorai_cli-0.7.19 → refactorai_cli-0.7.22}/refactorai_cli/local_paths.py +0 -0
  39. {refactorai_cli-0.7.19 → refactorai_cli-0.7.22}/refactorai_cli/main.py +0 -0
  40. {refactorai_cli-0.7.19 → refactorai_cli-0.7.22}/refactorai_cli/mode_sync.py +0 -0
  41. {refactorai_cli-0.7.19 → refactorai_cli-0.7.22}/refactorai_cli/model_policy.py +0 -0
  42. {refactorai_cli-0.7.19 → refactorai_cli-0.7.22}/refactorai_cli/pre_push_gate.py +0 -0
  43. {refactorai_cli-0.7.19 → refactorai_cli-0.7.22}/refactorai_cli/refactor_branch_store.py +0 -0
  44. {refactorai_cli-0.7.19 → refactorai_cli-0.7.22}/refactorai_cli/runtime_manager.py +0 -0
  45. {refactorai_cli-0.7.19 → refactorai_cli-0.7.22}/refactorai_cli/settings.py +0 -0
  46. {refactorai_cli-0.7.19 → refactorai_cli-0.7.22}/refactorai_cli/setup_flow.py +0 -0
  47. {refactorai_cli-0.7.19 → refactorai_cli-0.7.22}/refactorai_cli/watch_ledger.py +0 -0
  48. {refactorai_cli-0.7.19 → refactorai_cli-0.7.22}/refactorai_cli/watch_supervisor.py +0 -0
  49. {refactorai_cli-0.7.19 → refactorai_cli-0.7.22}/refactorai_cli.egg-info/SOURCES.txt +0 -0
  50. {refactorai_cli-0.7.19 → refactorai_cli-0.7.22}/refactorai_cli.egg-info/dependency_links.txt +0 -0
  51. {refactorai_cli-0.7.19 → refactorai_cli-0.7.22}/refactorai_cli.egg-info/entry_points.txt +0 -0
  52. {refactorai_cli-0.7.19 → refactorai_cli-0.7.22}/refactorai_cli.egg-info/requires.txt +0 -0
  53. {refactorai_cli-0.7.19 → refactorai_cli-0.7.22}/refactorai_cli.egg-info/top_level.txt +0 -0
  54. {refactorai_cli-0.7.19 → refactorai_cli-0.7.22}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: refactorai-cli
3
- Version: 0.7.19
3
+ Version: 0.7.22
4
4
  Summary: Local-first CLI for the refactor platform
5
5
  Requires-Python: >=3.11
6
6
  Description-Content-Type: text/markdown
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "refactorai-cli"
3
- version = "0.7.19"
3
+ version = "0.7.22"
4
4
  description = "Local-first CLI for the refactor platform"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.11"
@@ -5,4 +5,4 @@ the shared `refactor_core` pipeline from a project folder while staying
5
5
  authenticated to the hosted platform via a developer key.
6
6
  """
7
7
 
8
- __version__ = "0.7.19"
8
+ __version__ = "0.7.22"
@@ -3329,28 +3329,16 @@ def review(
3329
3329
  sandbox_ctx = None
3330
3330
 
3331
3331
  try:
3332
- # Two orthogonal axes (docs/46): where the engine runs vs. where tests
3333
- # run. ``local_byok`` / ``local_managed`` use the remote (server) engine
3334
- # while verifying locally in the sandbox, so sandbox setup keys off
3335
- # verification location, and engine routing keys off engine location.
3332
+ # Engine routing keys off engine location (docs/46). Review is *read-only*
3333
+ # analysis: it produces findings / refactor requests and never executes
3334
+ # verification (tests) locally. The container sandbox exists for that
3335
+ # verification during ``code`` / ``refactor``, so provisioning + preflighting
3336
+ # it here only added container start + ``exec`` probe latency (and could hang
3337
+ # on the container runtime) for zero effect on the review result — the old
3338
+ # ``sandbox_ctx`` was consumed solely to print a preflight warning. Sandbox
3339
+ # readiness is therefore enforced lazily, when code is actually applied; use
3340
+ # ``refactor sandbox start`` to validate the toolchain ahead of time.
3336
3341
  engine_remote = _engine_is_remote(constitution)
3337
- verify_local = _verify_local(constitution)
3338
- if _sandbox_enabled(project_config):
3339
- if verify_local:
3340
- sandbox_ctx = _ensure_sandbox_ready(
3341
- project_root=project_root,
3342
- project_config=project_config,
3343
- stage="review",
3344
- auto_approve_install=False,
3345
- no_install=False,
3346
- continue_on_preflight_failure=True,
3347
- )
3348
- else:
3349
- variant = _execution_variant(constitution)
3350
- console.print(
3351
- f"[dim]review: sandbox.enabled is ignored in "
3352
- f"{variant} variant (verification runs in the cloud).[/dim]"
3353
- )
3354
3342
  if engine_remote:
3355
3343
  artifact = _cloud_inference(
3356
3344
  stage="review",
@@ -14,8 +14,10 @@ headless). The heavy lifting (state machine, rendering, review parsing) lives in
14
14
  from __future__ import annotations
15
15
 
16
16
  import contextlib
17
+ import inspect
17
18
  import json
18
19
  import os
20
+ import re
19
21
  import select
20
22
  import subprocess
21
23
  import sys
@@ -461,6 +463,53 @@ def _report_refactor_requests(project_root: Path) -> None:
461
463
  pass
462
464
 
463
465
 
466
+ _ANSI_RE = re.compile(r"\x1b\[[0-9;]*m")
467
+
468
+ # Map the review's stable stdout milestones to short, live sub-step labels shown
469
+ # on the reviewing node. Ordered: first match wins. Kept tolerant (substring /
470
+ # simple regex) so minor wording changes degrade to "no update" rather than
471
+ # crashing. Each entry is (compiled pattern, formatter(match) -> label).
472
+ _PROGRESS_PATTERNS: list[tuple[re.Pattern[str], Callable[[re.Match[str]], str]]] = [
473
+ (re.compile(r"batch\s+(\d+)\s*/\s*(\d+)", re.I), lambda m: f"reviewing batch {m.group(1)}/{m.group(2)}"),
474
+ (re.compile(r"split into\s+(\d+)\s+batch", re.I), lambda m: f"planning {m.group(1)} batch(es)"),
475
+ (re.compile(r"sandbox ready", re.I), lambda m: "sandbox ready"),
476
+ (re.compile(r"ensuring sandbox|sandbox:", re.I), lambda m: "preparing sandbox"),
477
+ (re.compile(r"review scope", re.I), lambda m: "resolving scope"),
478
+ (re.compile(r"security baseline", re.I), lambda m: "security scan"),
479
+ (re.compile(r"Created\s+\d+\s+refactor request", re.I), lambda m: "creating requests"),
480
+ (re.compile(r"\bgate:\s*", re.I), lambda m: "finalizing gate"),
481
+ (re.compile(r"\bindex", re.I), lambda m: "indexing files"),
482
+ ]
483
+
484
+
485
+ def _summarize_progress(line: str) -> str:
486
+ """Reduce one raw review stdout line to a short sub-step label, or ``""``.
487
+
488
+ Returns ``""`` for lines that carry no useful milestone so the caller can
489
+ skip a redundant repaint.
490
+ """
491
+ text = _ANSI_RE.sub("", line or "").strip()
492
+ if not text:
493
+ return ""
494
+ for pattern, fmt in _PROGRESS_PATTERNS:
495
+ match = pattern.search(text)
496
+ if match:
497
+ return fmt(match)
498
+ return ""
499
+
500
+
501
+ def _runner_supports_on_progress(runner: ReviewRunner) -> bool:
502
+ """True when ``runner.run`` accepts an ``on_progress`` keyword.
503
+
504
+ Lets streaming be opt-in per runner so simple/test runners with the older
505
+ signature keep working unchanged.
506
+ """
507
+ try:
508
+ return "on_progress" in inspect.signature(runner.run).parameters
509
+ except (TypeError, ValueError):
510
+ return False
511
+
512
+
464
513
  def process_pending(
465
514
  project_root: Path,
466
515
  state: WatchState,
@@ -500,11 +549,27 @@ def process_pending(
500
549
  on_update(state)
501
550
  _report_node(project_root, node)
502
551
  intent = intent_for(node.sha) if intent_for else None
503
- outcome = runner.run(
504
- sha=node.sha,
505
- intent=intent,
506
- yes_full_project=(node.scope_mode == "full"),
507
- )
552
+ run_kwargs: dict = {
553
+ "sha": node.sha,
554
+ "intent": intent,
555
+ "yes_full_project": (node.scope_mode == "full"),
556
+ }
557
+ # Stream live sub-steps into the reviewing node when the runner supports
558
+ # it, so a long review shows batch-by-batch progress instead of a static
559
+ # "analyzing..." spinner that looks hung.
560
+ if _runner_supports_on_progress(runner):
561
+ reviewing_sha = node.sha
562
+
563
+ def _on_progress(line: str, _sha: str = reviewing_sha) -> None:
564
+ label = _summarize_progress(line)
565
+ if not label:
566
+ return
567
+ state.set_substep(_sha, label)
568
+ if on_update:
569
+ on_update(state)
570
+
571
+ run_kwargs["on_progress"] = _on_progress
572
+ outcome = runner.run(**run_kwargs)
508
573
  if outcome.run_id:
509
574
  outcome.rr_preview = _preview_for_run(project_root, outcome.run_id)
510
575
  state.apply_outcome(node.sha, outcome)
@@ -14,12 +14,15 @@ machine without spawning a subprocess or needing the engine.
14
14
 
15
15
  from __future__ import annotations
16
16
 
17
+ import contextlib
17
18
  import os
18
19
  import re
20
+ import signal
19
21
  import subprocess
22
+ import threading
20
23
  from dataclasses import dataclass, field
21
24
  from pathlib import Path
22
- from typing import Protocol
25
+ from typing import Callable, Protocol
23
26
 
24
27
  _ANSI = re.compile(r"\x1b\[[0-9;]*m")
25
28
  _SCOPE_COUNTS = re.compile(r"(\d+)\s+changed\s*\+\s*(\d+)\s+associated")
@@ -58,6 +61,7 @@ class ReviewRunner(Protocol):
58
61
  sha: str,
59
62
  intent: str | None = None,
60
63
  yes_full_project: bool = False,
64
+ on_progress: Callable[[str], None] | None = None,
61
65
  ) -> ReviewOutcome: ...
62
66
 
63
67
 
@@ -123,6 +127,7 @@ class SubprocessReviewRunner:
123
127
  sha: str,
124
128
  intent: str | None = None,
125
129
  yes_full_project: bool = False,
130
+ on_progress: Callable[[str], None] | None = None,
126
131
  ) -> ReviewOutcome:
127
132
  cmd = [self.executable, "review", "--commit", sha, *self.extra_args]
128
133
  if intent and intent.strip():
@@ -132,23 +137,70 @@ class SubprocessReviewRunner:
132
137
  # non-interactive confirmation so the loop never blocks on a hidden prompt.
133
138
  cmd.append("--yes-full-project")
134
139
  # Wide, non-interactive output so rich does not wrap the status lines the
135
- # parser depends on.
136
- env = dict(os.environ, COLUMNS="200", NO_COLOR="1")
140
+ # parser depends on. ``PYTHONUNBUFFERED`` forces line-buffered child output
141
+ # so progress lines reach us (and the terminal UI) live instead of only at
142
+ # process exit -- the difference between a spinner that looks hung and one
143
+ # that shows real sub-steps.
144
+ env = dict(os.environ, COLUMNS="200", NO_COLOR="1", PYTHONUNBUFFERED="1")
137
145
  try:
138
- proc = subprocess.run(
146
+ proc = subprocess.Popen( # noqa: S603 - fixed argv, no shell
139
147
  cmd,
140
148
  cwd=str(self.project_root),
141
- capture_output=True,
149
+ stdout=subprocess.PIPE,
150
+ stderr=subprocess.STDOUT,
142
151
  text=True,
143
- timeout=self.timeout,
152
+ bufsize=1,
144
153
  env=env,
154
+ # New session/process group so the timeout can kill the whole tree
155
+ # (review spawns grandchildren, e.g. the sandbox runtime). Killing
156
+ # only the direct child would leave a grandchild holding the stdout
157
+ # pipe open, so the streaming read loop would block until it exits
158
+ # and the timeout would not take effect promptly.
159
+ start_new_session=True,
145
160
  )
146
161
  except FileNotFoundError:
147
162
  return ReviewOutcome(sha=sha, error=f"'{self.executable}' not found on PATH")
148
- except subprocess.TimeoutExpired:
149
- return ReviewOutcome(sha=sha, error=f"review timed out after {int(self.timeout)}s")
150
163
  except Exception as exc: # never let a review failure crash the terminal
151
164
  return ReviewOutcome(sha=sha, error=f"review invocation failed: {exc}")
152
165
 
153
- combined = (proc.stdout or "") + "\n" + (proc.stderr or "")
154
- return parse_review_output(sha, combined, proc.returncode)
166
+ def _kill_tree() -> None:
167
+ """Kill the child and all of its descendants (best-effort)."""
168
+ with contextlib.suppress(Exception):
169
+ os.killpg(os.getpgid(proc.pid), signal.SIGKILL)
170
+ with contextlib.suppress(Exception):
171
+ proc.kill()
172
+
173
+ # Enforce the overall timeout even while streaming: a watchdog kills the
174
+ # process group at the deadline (the streaming read loop then unblocks
175
+ # because every pipe write-end is closed).
176
+ done = threading.Event()
177
+ timed_out = threading.Event()
178
+
179
+ def _watchdog() -> None:
180
+ if not done.wait(self.timeout):
181
+ timed_out.set()
182
+ _kill_tree()
183
+
184
+ watchdog = threading.Thread(target=_watchdog, name="review-timeout", daemon=True)
185
+ watchdog.start()
186
+
187
+ lines: list[str] = []
188
+ try:
189
+ if proc.stdout is not None:
190
+ for raw in proc.stdout:
191
+ lines.append(raw)
192
+ if on_progress is not None:
193
+ # A progress-callback error must never abort the review.
194
+ with contextlib.suppress(Exception):
195
+ on_progress(raw.rstrip("\n"))
196
+ proc.wait()
197
+ except Exception as exc: # never let a streaming/read error crash the terminal
198
+ done.set()
199
+ _kill_tree()
200
+ return ReviewOutcome(sha=sha, error=f"review invocation failed: {exc}")
201
+ finally:
202
+ done.set()
203
+
204
+ if timed_out.is_set():
205
+ return ReviewOutcome(sha=sha, error=f"review timed out after {int(self.timeout)}s")
206
+ return parse_review_output(sha, "".join(lines), proc.returncode or 0)
@@ -43,6 +43,10 @@ PHASE_FINALIZE = "finalize"
43
43
  PHASE_DONE = "done"
44
44
  PHASE_NONE = "none"
45
45
 
46
+ # Cap the live sub-step trail so a long review never overflows the node block.
47
+ # Older steps scroll off the top (the checklist keeps the most recent progress).
48
+ _MAX_SUBSTEPS = 6
49
+
46
50
 
47
51
  @dataclass
48
52
  class RRCommit:
@@ -121,6 +125,13 @@ class CommitNode:
121
125
  intent_summary: str = ""
122
126
  run_id: str = ""
123
127
  error: str = ""
128
+ # Live sub-step trail while REVIEWING, oldest first (e.g. "resolving scope",
129
+ # "reviewing batch 2/5", "security scan"). Rendered as a Cursor-style
130
+ # checklist: completed steps ticked, the last one spinning. Volatile UI-only
131
+ # detail; not part of the persisted node contract. ``substep`` mirrors the
132
+ # current (last) step for the compact single-line status fallback.
133
+ substep: str = ""
134
+ substeps: list[str] = field(default_factory=list)
124
135
  finalize: "FinalizeInfo | None" = None
125
136
 
126
137
  @property
@@ -271,6 +282,25 @@ class WatchState:
271
282
  node = self._nodes.get(sha)
272
283
  if node:
273
284
  node.state = REVIEWING
285
+ node.substep = ""
286
+ node.substeps = []
287
+
288
+ def set_substep(self, sha: str, label: str) -> None:
289
+ """Append a new live sub-step to a reviewing node's trail (UI-only).
290
+
291
+ Consecutive duplicates are collapsed and the trail is capped at
292
+ :data:`_MAX_SUBSTEPS` (oldest scroll off). No-op unless the node is
293
+ currently ``REVIEWING`` so a stale progress line can never mutate a
294
+ finished node.
295
+ """
296
+ node = self._nodes.get(sha)
297
+ if not (node and node.state == REVIEWING):
298
+ return
299
+ if not node.substeps or node.substeps[-1] != label:
300
+ node.substeps.append(label)
301
+ if len(node.substeps) > _MAX_SUBSTEPS:
302
+ node.substeps = node.substeps[-_MAX_SUBSTEPS:]
303
+ node.substep = label
274
304
 
275
305
  def requeue(self, sha: str) -> None:
276
306
  """Reset a node to ``queued`` so the next pass re-reviews it (post-resolve)."""
@@ -282,6 +312,8 @@ class WatchState:
282
312
  node.rr_count = 0
283
313
  node.rr_preview = []
284
314
  node.error = ""
315
+ node.substep = ""
316
+ node.substeps = []
285
317
  node.finalize = None
286
318
  self.forced_review.discard(sha)
287
319
 
@@ -443,7 +475,8 @@ def _status_line(node: CommitNode) -> str:
443
475
  if node.state == REVIEWING:
444
476
  scope = f" ({node.changed} files)" if node.changed else ""
445
477
  spinner = _spinner_frame()
446
- return f"[cyan]{spinner}[/cyan] [dim]analyzing changed+associated{scope}...[/dim]"
478
+ detail = node.substep or "analyzing changed+associated"
479
+ return f"[cyan]{spinner}[/cyan] [dim]{detail}{scope}...[/dim]"
447
480
  if node.state == ERROR:
448
481
  return f"[red]review error:[/red] {node.error or 'unknown'}"
449
482
  # reviewed
@@ -538,10 +571,29 @@ def _render_review_view(state: WatchState, *, now: datetime | None = None, width
538
571
  row2 = Text.from_markup(f"[cyan]|[/cyan] [yellow]{node.short_sha}[/yellow] ")
539
572
  row2.append(node.subject or "(no subject)")
540
573
  node_lines.append(row2)
541
- # Row 3: pipe + status.
542
- row3 = Text.from_markup("[cyan]|[/cyan] ")
543
- row3.append_text(Text.from_markup(_status_line(node)))
544
- node_lines.append(row3)
574
+ # Row 3: while reviewing, render a Cursor-style checklist of sub-steps
575
+ # (completed steps ticked, the current one spinning) so progress reads as
576
+ # advancing work instead of a single overwritten line. Falls back to the
577
+ # compact status line before the first step arrives / for other states.
578
+ if node.state == REVIEWING and node.substeps:
579
+ spinner = _spinner_frame(now)
580
+ last = len(node.substeps) - 1
581
+ for idx, label in enumerate(node.substeps):
582
+ row = Text.from_markup("[cyan]|[/cyan] ")
583
+ if idx == last:
584
+ row.append_text(Text.from_markup(f"[cyan]{spinner}[/cyan] "))
585
+ row.append(label)
586
+ if node.changed:
587
+ row.append(f" ({node.changed} files)", style="dim")
588
+ row.append(" …", style="dim")
589
+ else:
590
+ row.append_text(Text.from_markup("[green]✓[/green] "))
591
+ row.append(label, style="dim")
592
+ node_lines.append(row)
593
+ else:
594
+ row3 = Text.from_markup("[cyan]|[/cyan] ")
595
+ row3.append_text(Text.from_markup(_status_line(node)))
596
+ node_lines.append(row3)
545
597
  if node.state == REVIEWED and node.rr_preview:
546
598
  for item in node.rr_preview:
547
599
  req = Text.from_markup("[cyan]|[/cyan] ")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: refactorai-cli
3
- Version: 0.7.19
3
+ Version: 0.7.22
4
4
  Summary: Local-first CLI for the refactor platform
5
5
  Requires-Python: >=3.11
6
6
  Description-Content-Type: text/markdown