syncmill 0.2.0__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.
Files changed (56) hide show
  1. syncmill/__init__.py +11 -0
  2. syncmill/agents.py +2064 -0
  3. syncmill/artifact_io.py +201 -0
  4. syncmill/artifacts.py +1946 -0
  5. syncmill/board.py +2060 -0
  6. syncmill/breaker.py +327 -0
  7. syncmill/cli.py +4986 -0
  8. syncmill/command_errors.py +32 -0
  9. syncmill/config.py +1118 -0
  10. syncmill/dashboard.py +935 -0
  11. syncmill/gates.py +179 -0
  12. syncmill/gateway.py +234 -0
  13. syncmill/git_env.py +99 -0
  14. syncmill/isolated_session.py +431 -0
  15. syncmill/matcher.py +127 -0
  16. syncmill/memory.py +1067 -0
  17. syncmill/memory_backend.py +1907 -0
  18. syncmill/memory_migration.py +962 -0
  19. syncmill/model_routing.py +482 -0
  20. syncmill/models.py +244 -0
  21. syncmill/monitor.py +775 -0
  22. syncmill/phoenix_exporter.py +339 -0
  23. syncmill/planning.py +512 -0
  24. syncmill/platform_support.py +14 -0
  25. syncmill/preflight.py +1129 -0
  26. syncmill/project.py +1067 -0
  27. syncmill/report.py +502 -0
  28. syncmill/review_candidates.py +107 -0
  29. syncmill/room_compare.py +2380 -0
  30. syncmill/room_raw.py +484 -0
  31. syncmill/room_viewer.py +604 -0
  32. syncmill/rooms.py +4044 -0
  33. syncmill/session.py +444 -0
  34. syncmill/strategies/__init__.py +82 -0
  35. syncmill/strategies/base.py +910 -0
  36. syncmill/strategies/compete.py +233 -0
  37. syncmill/strategies/council.py +429 -0
  38. syncmill/strategies/decompose.py +570 -0
  39. syncmill/strategies/pipeline.py +221 -0
  40. syncmill/strategies/route.py +130 -0
  41. syncmill/supervisor.py +1423 -0
  42. syncmill/teamfile.py +571 -0
  43. syncmill/telemetry_hook.py +231 -0
  44. syncmill/terminal.py +245 -0
  45. syncmill/tracer.py +548 -0
  46. syncmill/tui.py +3537 -0
  47. syncmill/tui_collaboration.py +499 -0
  48. syncmill/tui_jobs.py +1407 -0
  49. syncmill/tui_render.py +156 -0
  50. syncmill/workflow.py +3746 -0
  51. syncmill/worktree.py +1045 -0
  52. syncmill-0.2.0.dist-info/METADATA +853 -0
  53. syncmill-0.2.0.dist-info/RECORD +56 -0
  54. syncmill-0.2.0.dist-info/WHEEL +4 -0
  55. syncmill-0.2.0.dist-info/entry_points.txt +2 -0
  56. syncmill-0.2.0.dist-info/licenses/LICENSE +191 -0
syncmill/tui_render.py ADDED
@@ -0,0 +1,156 @@
1
+ """What the Textual apps (``tui``, ``dashboard``) put on the terminal (#210).
2
+
3
+ Textual removes only BEL, BS, VT, FF and CR from text it displays; ESC, C1
4
+ controls (U+009B is CSI) and bidi overrides reach the terminal, so stored or
5
+ agent-authored text could write the clipboard (OSC 52), clear the screen or
6
+ reorder what a line appears to say. Three pieces close that:
7
+
8
+ - ``install_render_guard`` replaces ``textual.strip.Strip.render``, through which
9
+ every compositor update turns widgets into terminal bytes. Each dangerous
10
+ character in a segment becomes a character of the same cell width that a
11
+ terminal does not interpret: U+200B, or U+FFFD where an old rich Unicode table
12
+ (``UNICODE_VERSION``) gives the character a width. Layout was computed before
13
+ this point, so a replacement must not change any width; deleting would (a
14
+ ZWJ next to a removed ESC joins a different grapheme). A link whose URL has
15
+ such a character is dropped.
16
+ - ``error_console`` replaces ``App.error_console``, which prints fatal errors
17
+ outside ``Strip.render``; its output is escaped and uncoloured.
18
+ - ``PlainStatic``/``PlainLabel`` and ``plain`` keep markup off for text syncmill
19
+ did not write: Textual parses ``[link=...]`` and ``[/b]`` in a string, and a
20
+ malformed tag raises while the widget renders.
21
+
22
+ The guard drops characters silently; showing them as escapes, as the CLI does,
23
+ is the job of the renderer that builds the text.
24
+ """
25
+
26
+ from __future__ import annotations
27
+
28
+ from rich.cells import cell_len
29
+ from rich.console import Console
30
+ from rich.text import Text
31
+ from textual.widgets import Label, Select, Static
32
+ from textual.widgets.option_list import Option
33
+
34
+ from syncmill.artifacts import printable_name, printable_text
35
+ from syncmill.terminal import EscapedStderr
36
+
37
+ _BIDI = "\u061c\u200e\u200f\u202a\u202b\u202c\u202d\u202e\u2066\u2067\u2068\u2069"
38
+ DANGEROUS = frozenset(
39
+ [chr(c) for c in range(0x20)] + [chr(c) for c in range(0x7F, 0xA0)]
40
+ + list(_BIDI) + ["\u2028", "\u2029"]
41
+ )
42
+
43
+
44
+ def _substitutions() -> dict[int, str]:
45
+ """Each dangerous character's same-width replacement, under the rich
46
+ Unicode table this process uses (read once, like rich itself)."""
47
+ table = {}
48
+ for ch in DANGEROUS:
49
+ width = cell_len(ch)
50
+ if width == cell_len("\u200b"):
51
+ table[ord(ch)] = "\u200b"
52
+ elif width == cell_len("\ufffd"):
53
+ table[ord(ch)] = "\ufffd"
54
+ else:
55
+ raise RuntimeError(f"no same-width replacement for U+{ord(ch):04X}")
56
+ return table
57
+
58
+
59
+ def _safe_link(url: str) -> bool:
60
+ return printable_name(url) == url
61
+
62
+
63
+ def install_render_guard() -> None:
64
+ """Replace ``Strip.render`` with the guarded version, once per process.
65
+
66
+ Same output as Textual's for text without dangerous characters, same
67
+ caching; called by the apps' entry points before ``App.run``, never at
68
+ import."""
69
+ from rich.color import ColorSystem
70
+ from textual.strip import Strip
71
+
72
+ if getattr(Strip.render, "_syncmill_guard", False):
73
+ return
74
+ table = _substitutions()
75
+
76
+ def render(self: Strip, console: Console) -> str:
77
+ if self._render_cache is None:
78
+ color_system = console._color_system or ColorSystem.TRUECOLOR
79
+ render_style = self.render_style
80
+ parts = []
81
+ for text, style, _ in self._segments:
82
+ text = text.translate(table)
83
+ if style is None:
84
+ parts.append(text)
85
+ continue
86
+ if style.link is not None and not _safe_link(style.link):
87
+ style = style.update_link(None)
88
+ parts.append(render_style(style, text, color_system=color_system))
89
+ self._render_cache = "".join(parts)
90
+ return self._render_cache
91
+
92
+ render._syncmill_guard = True
93
+ Strip.render = render
94
+
95
+
96
+ def error_console() -> Console:
97
+ """A console for ``App.error_console``: Textual prints fatal tracebacks and
98
+ exit messages through it, not through ``Strip.render``. No colour, so rich
99
+ writes no escape sequence of its own, and everything is escaped."""
100
+ return Console(
101
+ file=EscapedStderr(), markup=False, highlight=False, color_system=None
102
+ )
103
+
104
+
105
+ def plain(value: object) -> Text:
106
+ """``value`` as plain, one-line text for a widget that parses markup in a
107
+ ``str`` (DataTable cells and column labels, Select labels), escaped as the
108
+ CLI shows a one-line value."""
109
+ return Text(printable_name(str(value)))
110
+
111
+
112
+ def _shown(content: object) -> object:
113
+ """A string as the CLI shows a body (``printable_text``, idempotent on text
114
+ a renderer already escaped); anything else unchanged."""
115
+ return printable_text(content) if isinstance(content, str) else content
116
+
117
+
118
+ class PlainStatic(Static):
119
+ """A ``Static`` that never parses markup and shows a string escaped: what
120
+ it shows is often stored text, errors and statuses."""
121
+
122
+ def __init__(self, content: object = "", *args, **kwargs) -> None:
123
+ kwargs["markup"] = False
124
+ super().__init__(_shown(content), *args, **kwargs)
125
+
126
+ def update(self, content: object = "", *args, **kwargs) -> None:
127
+ super().update(_shown(content), *args, **kwargs)
128
+
129
+
130
+ def _one_line(content: object) -> object:
131
+ """A string as the CLI shows a one-line value (``printable_name``)."""
132
+ return printable_name(content) if isinstance(content, str) else content
133
+
134
+
135
+ class PlainLabel(Label):
136
+ """A ``Label`` that never parses markup and shows a string as one escaped
137
+ line: labels are titles and field names."""
138
+
139
+ def __init__(self, content: object = "", *args, **kwargs) -> None:
140
+ kwargs["markup"] = False
141
+ super().__init__(_one_line(content), *args, **kwargs)
142
+
143
+ def update(self, content: object = "", *args, **kwargs) -> None:
144
+ super().update(_one_line(content), *args, **kwargs)
145
+
146
+
147
+ def plain_select(options, **kwargs) -> Select:
148
+ """A ``Select`` whose option labels are plain text."""
149
+ return Select([(plain(label), value) for label, value in options], **kwargs)
150
+
151
+
152
+ class PlainOption(Option):
153
+ """An ``OptionList`` entry whose string prompt is shown as one escaped line."""
154
+
155
+ def __init__(self, prompt: object, id: str | None = None, disabled: bool = False):
156
+ super().__init__(_one_line(prompt), id=id, disabled=disabled)