@agxnte/reidx 2.0.2

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 (135) hide show
  1. package/README.md +724 -0
  2. package/bin/reidx.js +33 -0
  3. package/package.json +40 -0
  4. package/pyproject.toml +45 -0
  5. package/scripts/find-python.js +24 -0
  6. package/scripts/postinstall.js +35 -0
  7. package/src/reidx/__init__.py +3 -0
  8. package/src/reidx/__main__.py +9 -0
  9. package/src/reidx/__pycache__/__init__.cpython-312.pyc +0 -0
  10. package/src/reidx/app/__init__.py +3 -0
  11. package/src/reidx/app/__pycache__/__init__.cpython-312.pyc +0 -0
  12. package/src/reidx/app/__pycache__/commands.cpython-312.pyc +0 -0
  13. package/src/reidx/app/commands.py +221 -0
  14. package/src/reidx/automation/__init__.py +3 -0
  15. package/src/reidx/automation/__pycache__/__init__.cpython-312.pyc +0 -0
  16. package/src/reidx/automation/__pycache__/exec.cpython-312.pyc +0 -0
  17. package/src/reidx/automation/exec.py +35 -0
  18. package/src/reidx/config/__init__.py +10 -0
  19. package/src/reidx/config/__pycache__/__init__.cpython-312.pyc +0 -0
  20. package/src/reidx/config/__pycache__/loader.cpython-312.pyc +0 -0
  21. package/src/reidx/config/__pycache__/models.cpython-312.pyc +0 -0
  22. package/src/reidx/config/__pycache__/settings.cpython-312.pyc +0 -0
  23. package/src/reidx/config/loader.py +104 -0
  24. package/src/reidx/config/models.py +49 -0
  25. package/src/reidx/config/settings.py +127 -0
  26. package/src/reidx/deepreid/__init__.py +8 -0
  27. package/src/reidx/deepreid/__pycache__/__init__.cpython-312.pyc +0 -0
  28. package/src/reidx/deepreid/__pycache__/pipeline.cpython-312.pyc +0 -0
  29. package/src/reidx/deepreid/pipeline.py +221 -0
  30. package/src/reidx/diagnostics/__init__.py +0 -0
  31. package/src/reidx/diagnostics/__pycache__/__init__.cpython-312.pyc +0 -0
  32. package/src/reidx/diagnostics/__pycache__/logger.cpython-312.pyc +0 -0
  33. package/src/reidx/diagnostics/logger.py +56 -0
  34. package/src/reidx/goals/__init__.py +21 -0
  35. package/src/reidx/goals/__pycache__/__init__.cpython-312.pyc +0 -0
  36. package/src/reidx/goals/__pycache__/models.cpython-312.pyc +0 -0
  37. package/src/reidx/goals/__pycache__/store.cpython-312.pyc +0 -0
  38. package/src/reidx/goals/models.py +101 -0
  39. package/src/reidx/goals/store.py +233 -0
  40. package/src/reidx/integrations/__init__.py +3 -0
  41. package/src/reidx/integrations/mcp.py +70 -0
  42. package/src/reidx/nyx/__init__.py +3 -0
  43. package/src/reidx/nyx/__pycache__/__init__.cpython-312.pyc +0 -0
  44. package/src/reidx/nyx/__pycache__/persona.cpython-312.pyc +0 -0
  45. package/src/reidx/nyx/persona.py +35 -0
  46. package/src/reidx/policy/__init__.py +15 -0
  47. package/src/reidx/policy/__pycache__/__init__.cpython-312.pyc +0 -0
  48. package/src/reidx/policy/__pycache__/engine.cpython-312.pyc +0 -0
  49. package/src/reidx/policy/__pycache__/models.cpython-312.pyc +0 -0
  50. package/src/reidx/policy/engine.py +101 -0
  51. package/src/reidx/policy/models.py +35 -0
  52. package/src/reidx/provider/__init__.py +16 -0
  53. package/src/reidx/provider/__pycache__/__init__.cpython-312.pyc +0 -0
  54. package/src/reidx/provider/__pycache__/_http.cpython-312.pyc +0 -0
  55. package/src/reidx/provider/__pycache__/anthropic.cpython-312.pyc +0 -0
  56. package/src/reidx/provider/__pycache__/base.cpython-312.pyc +0 -0
  57. package/src/reidx/provider/__pycache__/ollama.cpython-312.pyc +0 -0
  58. package/src/reidx/provider/__pycache__/openai.cpython-312.pyc +0 -0
  59. package/src/reidx/provider/__pycache__/registry.cpython-312.pyc +0 -0
  60. package/src/reidx/provider/__pycache__/store.cpython-312.pyc +0 -0
  61. package/src/reidx/provider/__pycache__/stub.cpython-312.pyc +0 -0
  62. package/src/reidx/provider/_http.py +29 -0
  63. package/src/reidx/provider/anthropic.py +176 -0
  64. package/src/reidx/provider/base.py +50 -0
  65. package/src/reidx/provider/ollama.py +109 -0
  66. package/src/reidx/provider/openai.py +144 -0
  67. package/src/reidx/provider/registry.py +88 -0
  68. package/src/reidx/provider/store.py +141 -0
  69. package/src/reidx/provider/stub.py +60 -0
  70. package/src/reidx/provider_manager/__init__.py +40 -0
  71. package/src/reidx/provider_manager/__pycache__/__init__.cpython-312.pyc +0 -0
  72. package/src/reidx/provider_manager/__pycache__/catalog.cpython-312.pyc +0 -0
  73. package/src/reidx/provider_manager/__pycache__/database.cpython-312.pyc +0 -0
  74. package/src/reidx/provider_manager/__pycache__/keychain.cpython-312.pyc +0 -0
  75. package/src/reidx/provider_manager/__pycache__/palette.cpython-312.pyc +0 -0
  76. package/src/reidx/provider_manager/catalog.py +330 -0
  77. package/src/reidx/provider_manager/database.py +234 -0
  78. package/src/reidx/provider_manager/keychain.py +102 -0
  79. package/src/reidx/provider_manager/palette.py +831 -0
  80. package/src/reidx/runtime/__init__.py +5 -0
  81. package/src/reidx/runtime/__pycache__/__init__.cpython-312.pyc +0 -0
  82. package/src/reidx/runtime/__pycache__/agent.cpython-312.pyc +0 -0
  83. package/src/reidx/runtime/__pycache__/orchestrator.cpython-312.pyc +0 -0
  84. package/src/reidx/runtime/__pycache__/reasoning.cpython-312.pyc +0 -0
  85. package/src/reidx/runtime/__pycache__/state.cpython-312.pyc +0 -0
  86. package/src/reidx/runtime/__pycache__/subagent.cpython-312.pyc +0 -0
  87. package/src/reidx/runtime/agent.py +198 -0
  88. package/src/reidx/runtime/orchestrator.py +244 -0
  89. package/src/reidx/runtime/reasoning.py +77 -0
  90. package/src/reidx/runtime/state.py +32 -0
  91. package/src/reidx/runtime/subagent.py +174 -0
  92. package/src/reidx/session/__init__.py +4 -0
  93. package/src/reidx/session/__pycache__/__init__.cpython-312.pyc +0 -0
  94. package/src/reidx/session/__pycache__/models.cpython-312.pyc +0 -0
  95. package/src/reidx/session/__pycache__/store.cpython-312.pyc +0 -0
  96. package/src/reidx/session/models.py +43 -0
  97. package/src/reidx/session/store.py +101 -0
  98. package/src/reidx/tasks/__init__.py +4 -0
  99. package/src/reidx/tasks/__pycache__/__init__.cpython-312.pyc +0 -0
  100. package/src/reidx/tasks/__pycache__/models.cpython-312.pyc +0 -0
  101. package/src/reidx/tasks/__pycache__/store.cpython-312.pyc +0 -0
  102. package/src/reidx/tasks/models.py +44 -0
  103. package/src/reidx/tasks/store.py +86 -0
  104. package/src/reidx/tools/__init__.py +44 -0
  105. package/src/reidx/tools/__pycache__/__init__.cpython-312.pyc +0 -0
  106. package/src/reidx/tools/__pycache__/base.cpython-312.pyc +0 -0
  107. package/src/reidx/tools/__pycache__/file_tools.cpython-312.pyc +0 -0
  108. package/src/reidx/tools/__pycache__/registry.cpython-312.pyc +0 -0
  109. package/src/reidx/tools/__pycache__/shell_tool.cpython-312.pyc +0 -0
  110. package/src/reidx/tools/__pycache__/spawn_agent.cpython-312.pyc +0 -0
  111. package/src/reidx/tools/__pycache__/web_tools.cpython-312.pyc +0 -0
  112. package/src/reidx/tools/base.py +84 -0
  113. package/src/reidx/tools/file_tools.py +222 -0
  114. package/src/reidx/tools/registry.py +45 -0
  115. package/src/reidx/tools/shell_tool.py +75 -0
  116. package/src/reidx/tools/spawn_agent.py +185 -0
  117. package/src/reidx/tools/web_tools.py +216 -0
  118. package/src/reidx/ui/__init__.py +41 -0
  119. package/src/reidx/ui/__pycache__/__init__.cpython-312.pyc +0 -0
  120. package/src/reidx/ui/__pycache__/app.cpython-312.pyc +0 -0
  121. package/src/reidx/ui/__pycache__/commands.cpython-312.pyc +0 -0
  122. package/src/reidx/ui/__pycache__/render.cpython-312.pyc +0 -0
  123. package/src/reidx/ui/__pycache__/repl.cpython-312.pyc +0 -0
  124. package/src/reidx/ui/__pycache__/theme.cpython-312.pyc +0 -0
  125. package/src/reidx/ui/app.py +1343 -0
  126. package/src/reidx/ui/commands.py +663 -0
  127. package/src/reidx/ui/render.py +517 -0
  128. package/src/reidx/ui/repl.py +14 -0
  129. package/src/reidx/ui/theme.py +126 -0
  130. package/src/reidx/workflows/__init__.py +4 -0
  131. package/src/reidx/workflows/__pycache__/__init__.cpython-312.pyc +0 -0
  132. package/src/reidx/workflows/__pycache__/models.cpython-312.pyc +0 -0
  133. package/src/reidx/workflows/__pycache__/store.cpython-312.pyc +0 -0
  134. package/src/reidx/workflows/models.py +19 -0
  135. package/src/reidx/workflows/store.py +56 -0
@@ -0,0 +1,517 @@
1
+ """UI rendering helpers (Rich-based).
2
+
3
+ A Claude-Code-style skin: a rounded welcome box, ⏺ bullets for assistant turns
4
+ and tool calls, ⎿ connectors for tool results, and a low-noise status line.
5
+ Rendered in ReidX's red palette. Assistant output stays markdown with
6
+ syntax highlighting; tool calls hang under a bullet with their result.
7
+ """
8
+ from __future__ import annotations
9
+
10
+ import sys
11
+ from datetime import UTC, datetime
12
+ from pathlib import Path
13
+
14
+ from rich.console import Console, Group
15
+ from rich.markdown import Markdown
16
+ from rich.panel import Panel
17
+ from rich.table import Table
18
+ from rich.text import Text
19
+ from rich.tree import Tree
20
+
21
+ from reidx.goals.models import Goal, GoalNode
22
+ from reidx.policy.engine import PolicyEngine
23
+ from reidx.policy.models import PermissionMode
24
+ from reidx.provider.base import Message
25
+ from reidx.session.models import Session
26
+ from reidx.tasks.models import Task
27
+ from reidx.ui.theme import (
28
+ APP_NAME,
29
+ BOX,
30
+ BULLET,
31
+ DANGER,
32
+ DIM,
33
+ MAX_WIDTH,
34
+ MODE_STYLE,
35
+ PRIMARY,
36
+ PROMPT,
37
+ RISK_STYLE,
38
+ ROLE_ICON,
39
+ ROLE_STYLE,
40
+ SPARKLE,
41
+ STATUS_STYLE,
42
+ SUCCESS,
43
+ TREE,
44
+ WARN,
45
+ context_window_for,
46
+ fmt_tokens,
47
+ short_path,
48
+ )
49
+
50
+ # The Claude-Code-style glyphs (✻ ⏺ ⎿ › ·) are non-ASCII; a legacy Windows
51
+ # codepage (cp1252) would raise UnicodeEncodeError mid-render. Force UTF-8 so
52
+ # the branding survives regardless of the host console's default encoding.
53
+ for _stream in (sys.stdout, sys.stderr):
54
+ try:
55
+ _stream.reconfigure(encoding="utf-8", errors="replace") # type: ignore[union-attr]
56
+ except (AttributeError, ValueError):
57
+ pass
58
+
59
+ console = Console()
60
+
61
+
62
+ # --- Thinking spinner constants (Claude-Code-style: "✻ Gerund… (12s · ↑ 2.1k tokens)") ---
63
+ # Rendered natively by ui.app's persistent footer (prompt_toolkit), not Rich —
64
+ # the spinner ticks too often (~8Hz) to round-trip through the console-capture
65
+ # bridge each frame. These constants are shared so both look the same.
66
+
67
+ _GERUNDS = [
68
+ "Flibbertigibbeting", "Cogitating", "Percolating", "Ruminating", "Noodling",
69
+ "Conjuring", "Finagling", "Marinating", "Puttering", "Simmering",
70
+ "Frolicking", "Galavanting", "Bamboozling", "Whirring", "Pondering",
71
+ "Scheming", "Effervescing", "Transmuting", "Kerfuffling", "Wrangling",
72
+ "Vibing", "Tinkering", "Synthesizing", "Meandering", "Incubating",
73
+ "Hornswoggling", "Discombobulating", "Moseying", "Percolatin'", "Brewing",
74
+ ]
75
+
76
+
77
+ _STAR_FRAMES = "✶✸✹✺✹✷"
78
+
79
+
80
+ def _bullet_grid(marker: Text, body) -> Table: # type: ignore[no-untyped-def]
81
+ """A two-column grid: a bullet marker + hanging-indented body.
82
+
83
+ Wrapped lines in the body align under the body column, mirroring the
84
+ Claude Code '⏺ text' layout.
85
+ """
86
+ grid = Table.grid(padding=(0, 1))
87
+ grid.add_column(width=1, no_wrap=True)
88
+ grid.add_column(overflow="fold")
89
+ grid.add_row(marker, body)
90
+ return grid
91
+
92
+
93
+ # Mascot ASCII art, printed to the left of the welcome panel in the empty
94
+ # space there. Purely decorative — kept as its own constant so it's easy to
95
+ # swap out.
96
+ _MASCOT = r""" "...'...
97
+ ^l'+??!_????:^'...:`"
98
+ ,`????????????????"'......
99
+ r^???????i-+??????:?-;Il"...
100
+ .^`_???+,.i`..'<,???????~..." ^
101
+ .-??<.....?l-""?????i::i???l>~..
102
+ .`?_~>>'`";;.^'i???????????~!-?>.."
103
+ ^.:???~^~??>!ll_??-???__??+.!~-.''. :
104
+ ^..i....!;Iii!!;!????????<l?+,`-<...^
105
+ `...""..",^";'....;!l!<>??^??>!<.i.`.
106
+ `.". ..^."`;-~~?,?.I?.I?.i>.. ?
107
+ ... `.'....+?.I.+?._'.'.".x
108
+ x :.....<.;..>? `.`..' -
109
+ :.....~...:'.`. '.
110
+ .....I...".... .
111
+ ..........'.'. `
112
+ .......... .i.
113
+ .^l..^.. .
114
+ l `.. .
115
+ ~ """
116
+
117
+
118
+ def banner() -> None:
119
+ """Claude-Code-style welcome box, with a mascot to its left."""
120
+ from reidx import __version__
121
+
122
+ body = Text.assemble(
123
+ (f"{SPARKLE} ", PRIMARY),
124
+ ("Welcome to ", "bold"),
125
+ (APP_NAME, f"bold {PRIMARY}"),
126
+ ("!", "bold"),
127
+ (f" v{__version__}\n\n", DIM),
128
+ (" /help for help, /status for your current setup\n\n", DIM),
129
+ (" cwd: ", DIM),
130
+ (str(Path.cwd()), DIM),
131
+ )
132
+ panel = Panel(body, box=BOX, border_style=PRIMARY, padding=(0, 1), width=MAX_WIDTH)
133
+
134
+ grid = Table.grid(padding=(0, 2))
135
+ grid.add_column()
136
+ grid.add_column()
137
+ grid.add_row(Text(_MASCOT, style=PRIMARY), panel)
138
+ console.print(grid)
139
+
140
+
141
+ def status_line_text(status: dict) -> Text:
142
+ """Build the full status line: app · mode · model · effort · tokens · cwd · tasks."""
143
+ mode = status.get("mode", "—")
144
+ model = status.get("model", "—")
145
+ effort = status.get("effort", "—")
146
+ used = status.get("tokens_used", 0)
147
+ window = status.get("context_window", 0)
148
+ workspace = status.get("workspace", "—")
149
+ tasks = status.get("tasks", 0)
150
+
151
+ mode_color = MODE_STYLE.get(mode, DIM)
152
+ sep = (" · ", DIM)
153
+ pct = f"{(used / window * 100):.0f}%" if window else "—"
154
+ usage = f"{fmt_tokens(used)}/{fmt_tokens(window)} ({pct})" if window else fmt_tokens(used)
155
+
156
+ return Text.assemble(
157
+ (f" {APP_NAME}", f"bold {PRIMARY}"), sep,
158
+ (mode, f"bold {mode_color}"), sep,
159
+ (model, DIM), sep,
160
+ (f"effort:{effort}", DIM), sep,
161
+ (usage, DIM), sep,
162
+ (short_path(workspace), DIM), sep,
163
+ (f"{tasks} tasks", WARN if tasks else DIM),
164
+ )
165
+
166
+
167
+ def status_bar(session: Session | None, mode: PermissionMode, task_count: int = 0, tokens_used: int = 0) -> None:
168
+ """Low-noise status line, Claude-Code-style — dim, dotted, no box."""
169
+ if session is None:
170
+ console.print(Text(" no active session", style=DIM))
171
+ return
172
+ console.print(
173
+ status_line_text(
174
+ {
175
+ "mode": mode.value,
176
+ "model": session.model,
177
+ "effort": session.reasoning_effort,
178
+ "tokens_used": tokens_used,
179
+ "context_window": context_window_for(session.model),
180
+ "workspace": str(session.workspace),
181
+ "tasks": task_count,
182
+ }
183
+ )
184
+ )
185
+
186
+
187
+ def status_prompt(session: Session | None, mode: PermissionMode | None) -> Text:
188
+ """Input caret, Claude-Code-style. Session context lives in the status line."""
189
+ return Text(f"{PROMPT} ", style=f"bold {PRIMARY}")
190
+
191
+
192
+ def print_user(text: str) -> None:
193
+ """Echo the submitted prompt after the input box collapses, under a small
194
+ "User" label, with extra blank lines before/after to clearly separate it
195
+ from whatever came before (the prior reply) and after (this turn's
196
+ reply)."""
197
+ console.print()
198
+ console.print()
199
+ console.print(Text(" User", style=DIM))
200
+ console.print(Text.assemble((f"{PROMPT} ", f"bold {PRIMARY}"), (text, "bold")))
201
+ console.print()
202
+ console.print()
203
+
204
+
205
+ def print_thinking(text: str) -> None:
206
+ """Chain-of-thought shown above the answer: dim italic under a ✻ marker."""
207
+ if not text or not text.strip():
208
+ return
209
+ console.print(_bullet_grid(Text(SPARKLE, style=DIM), Text(text.strip(), style="dim italic")))
210
+
211
+
212
+ def print_assistant(text: str) -> None:
213
+ """Markdown assistant output hanging under a ⏺ bullet."""
214
+ console.print(_bullet_grid(Text(BULLET, style=PRIMARY), Markdown(text)))
215
+
216
+
217
+ def print_tool_calls(tool_log: list[dict]) -> None:
218
+ """Tool calls as ⏺ Name(args) with a ⎿ result line beneath each."""
219
+ if not tool_log:
220
+ return
221
+ for entry in tool_log:
222
+ name = entry["name"]
223
+ ok = entry["ok"]
224
+ error = entry.get("error", "")
225
+ args = entry.get("args", {})
226
+ args_str = ", ".join(f"{k}={v!r}" for k, v in args.items()) if args else ""
227
+ header = Text.assemble(
228
+ (name, "bold"), ("(", DIM), (args_str, DIM), (")", DIM),
229
+ )
230
+ console.print(_bullet_grid(Text(BULLET, style=PRIMARY), header))
231
+ if ok:
232
+ result = Text("ok", style=SUCCESS)
233
+ else:
234
+ result = Text(f"Error: {error}", style=DANGER)
235
+ console.print(Text.assemble((" ", ""), (TREE, DIM), (" ", ""), result))
236
+
237
+
238
+ def print_tasks(tasks: list[Task]) -> None:
239
+ if not tasks:
240
+ console.print(Text("no tasks", style=DIM))
241
+ return
242
+ table = Table(title="tasks", box=BOX, show_header=True, header_style=f"bold {PRIMARY}", border_style=PRIMARY, width=MAX_WIDTH)
243
+ table.add_column("id", style=DIM, width=12)
244
+ table.add_column("status", width=12)
245
+ table.add_column("title")
246
+ for t in tasks:
247
+ color = STATUS_STYLE.get(t.status.value, "white")
248
+ table.add_row(t.id, Text(t.status.value, style=f"bold {color}"), t.title)
249
+ console.print(table)
250
+ # Summary line.
251
+ counts: dict[str, int] = {}
252
+ for t in tasks:
253
+ counts[t.status.value] = counts.get(t.status.value, 0) + 1
254
+ parts = []
255
+ for k, v in counts.items():
256
+ color = STATUS_STYLE.get(k, DIM)
257
+ parts.append(Text(f"{v} {k}", style=color))
258
+ summary = Text(" ").join(parts)
259
+ console.print(summary)
260
+
261
+
262
+ def _status_text(value: str) -> Text:
263
+ color = STATUS_STYLE.get(value, "white")
264
+ return Text(value, style=f"bold {color}")
265
+
266
+
267
+ def _goal_evidence_progress(goal: Goal) -> str:
268
+ total = len(goal.evidence)
269
+ done = sum(1 for evidence in goal.evidence if evidence.satisfied)
270
+ return f"{done}/{total}" if total else "-"
271
+
272
+
273
+ def _goal_node_progress(goal: Goal) -> str:
274
+ total = len(goal.nodes)
275
+ done = sum(1 for node in goal.nodes if node.status.value == "completed")
276
+ return f"{done}/{total}" if total else "-"
277
+
278
+
279
+ def print_goals(goals: list[Goal], active_goal_id: str | None = None) -> None:
280
+ if not goals:
281
+ console.print(Text("no goals", style=DIM))
282
+ return
283
+ table = Table(title="goals", box=BOX, show_header=True, header_style=f"bold {PRIMARY}", border_style=PRIMARY, width=MAX_WIDTH)
284
+ table.add_column("", width=2)
285
+ table.add_column("id", style=DIM, width=12)
286
+ table.add_column("status", width=12)
287
+ table.add_column("evidence", width=9)
288
+ table.add_column("nodes", width=7)
289
+ table.add_column("title")
290
+ for goal in goals:
291
+ marker = "●" if goal.id == active_goal_id else ""
292
+ table.add_row(
293
+ marker,
294
+ goal.id,
295
+ _status_text(goal.status.value),
296
+ _goal_evidence_progress(goal),
297
+ _goal_node_progress(goal),
298
+ goal.title,
299
+ )
300
+ console.print(table)
301
+ console.print(Text(" ● = active goal", style=DIM))
302
+
303
+
304
+ def print_goal(goal: Goal) -> None:
305
+ evidence_done = sum(1 for evidence in goal.evidence if evidence.satisfied)
306
+ header = Text.assemble(
307
+ (goal.title, f"bold {PRIMARY}"),
308
+ (" ", ""),
309
+ (goal.id, DIM),
310
+ (" ", ""),
311
+ (goal.status.value, f"bold {STATUS_STYLE.get(goal.status.value, DIM)}"),
312
+ )
313
+ body = Table.grid(padding=(0, 1))
314
+ body.add_column(width=12, style=DIM)
315
+ body.add_column()
316
+ body.add_row("outcome", goal.outcome or "(none)")
317
+ body.add_row("evidence", f"{evidence_done}/{len(goal.evidence)} satisfied" if goal.evidence else "(none)")
318
+ body.add_row("nodes", f"{len(goal.nodes)}")
319
+ body.add_row("tasks", f"{len(goal.task_ids)} linked")
320
+ console.print(Panel(Group(header, body), box=BOX, border_style=PRIMARY, width=MAX_WIDTH))
321
+
322
+ if goal.evidence:
323
+ table = Table(title="evidence", box=BOX, show_header=True, header_style=f"bold {PRIMARY}", border_style=PRIMARY, width=MAX_WIDTH)
324
+ table.add_column("#", style=DIM, width=4)
325
+ table.add_column("done", width=6)
326
+ table.add_column("description")
327
+ table.add_column("note", style=DIM)
328
+ for index, evidence in enumerate(goal.evidence, 1):
329
+ table.add_row(
330
+ str(index),
331
+ "yes" if evidence.satisfied else "no",
332
+ evidence.description,
333
+ evidence.note or "",
334
+ )
335
+ console.print(table)
336
+
337
+ if goal.constraints:
338
+ table = Table(title="constraints", box=BOX, show_header=True, header_style=f"bold {PRIMARY}", border_style=PRIMARY, width=MAX_WIDTH)
339
+ table.add_column("kind", style=DIM, width=12)
340
+ table.add_column("description")
341
+ for constraint in goal.constraints:
342
+ table.add_row(constraint.kind, constraint.description)
343
+ console.print(table)
344
+
345
+ if goal.nodes:
346
+ print_goal_tree(goal)
347
+
348
+ if goal.revisions:
349
+ console.print(Text("revisions", style=f"bold {PRIMARY}"))
350
+ for revision in goal.revisions[-5:]:
351
+ console.print(Text.assemble((" - ", DIM), (revision.note, "white")))
352
+
353
+
354
+ def print_goal_tree(goal: Goal) -> None:
355
+ by_parent: dict[str | None, list[GoalNode]] = {}
356
+ for node in goal.nodes:
357
+ by_parent.setdefault(node.parent_id, []).append(node)
358
+
359
+ root = Tree(Text("nodes", style=f"bold {PRIMARY}"))
360
+
361
+ def add_nodes(parent_tree: Tree, parent_id: str | None) -> None:
362
+ for node in by_parent.get(parent_id, []):
363
+ deps = f" deps:{','.join(node.depends_on)}" if node.depends_on else ""
364
+ label = Text.assemble(
365
+ (node.id, DIM),
366
+ (" ", ""),
367
+ (node.kind.value, DIM),
368
+ (" ", ""),
369
+ (node.status.value, f"bold {STATUS_STYLE.get(node.status.value, DIM)}"),
370
+ (" ", ""),
371
+ (node.title, "white"),
372
+ (deps, DIM),
373
+ )
374
+ branch = parent_tree.add(label)
375
+ add_nodes(branch, node.id)
376
+
377
+ add_nodes(root, None)
378
+ console.print(root)
379
+
380
+
381
+ def print_sessions(sessions: list[Session]) -> None:
382
+ if not sessions:
383
+ console.print(Text("no sessions", style=DIM))
384
+ return
385
+ table = Table(title="sessions", box=BOX, show_header=True, header_style=f"bold {PRIMARY}", border_style=PRIMARY, width=MAX_WIDTH)
386
+ table.add_column("id", style=DIM, width=14)
387
+ table.add_column("status", width=10)
388
+ table.add_column("title")
389
+ table.add_column("updated", width=12)
390
+ table.add_column("workspace", style=DIM)
391
+ now = datetime.now(UTC)
392
+ for s in sessions:
393
+ color = STATUS_STYLE.get(s.status.value, "white")
394
+ age = now - s.updated_at
395
+ mins = int(age.total_seconds() // 60)
396
+ when = f"{mins}m ago" if mins < 60 else f"{mins // 60}h ago"
397
+ table.add_row(
398
+ s.id,
399
+ Text(s.status.value, style=f"bold {color}"),
400
+ s.title,
401
+ when,
402
+ str(s.workspace),
403
+ )
404
+ console.print(table)
405
+
406
+
407
+ def print_workflows(workflows: list) -> None: # type: ignore[no-untyped-def]
408
+ if not workflows:
409
+ console.print(Text("no workflows", style=DIM))
410
+ return
411
+ table = Table(title="workflows", box=BOX, show_header=True, header_style=f"bold {PRIMARY}", border_style=PRIMARY, width=MAX_WIDTH)
412
+ table.add_column("name", style="bold", width=18)
413
+ table.add_column("steps", width=6)
414
+ table.add_column("description")
415
+ for wf in workflows:
416
+ table.add_row(wf.name, str(len(wf.steps)), wf.description or "(none)")
417
+ console.print(table)
418
+
419
+
420
+ def print_workflow_steps(workflow) -> None: # type: ignore[no-untyped-def]
421
+ console.print(Text.assemble((workflow.name, f"bold {PRIMARY}"), (" ", ""), (workflow.description, DIM)))
422
+ for i, step in enumerate(workflow.steps, 1):
423
+ console.print(Text.assemble((f" {i}. ", DIM), (step, "white")))
424
+
425
+
426
+ def print_providers(records, active_name: str, extra_names: list[str]) -> None: # type: ignore[no-untyped-def]
427
+ """Show connected providers.
428
+
429
+ `records` is the persisted list (name, kind, base_url, default_model);
430
+ `extra_names` is any provider registered but not persisted (e.g. `stub`,
431
+ or `anthropic` picked up from env vars) — those show as kind=built-in.
432
+ `active_name` is the current session provider (highlighted).
433
+ """
434
+ table = Table(title="providers", box=BOX, show_header=True, header_style=f"bold {PRIMARY}", border_style=PRIMARY, width=MAX_WIDTH)
435
+ table.add_column("name", style="bold", width=18)
436
+ table.add_column("kind", width=20)
437
+ table.add_column("model", width=22)
438
+ table.add_column("base_url")
439
+ for name in extra_names:
440
+ marker = "● " if name == active_name else " "
441
+ table.add_row(f"{marker}{name}", "built-in", "-", "-")
442
+ for r in records:
443
+ marker = "● " if r.name == active_name else " "
444
+ table.add_row(f"{marker}{r.name}", r.kind, r.default_model or "-", r.base_url or "-")
445
+ console.print(table)
446
+ console.print(Text(" ● = active provider (use /use <name> to switch)", style=DIM))
447
+
448
+
449
+ def print_permissions(policy: PolicyEngine) -> None:
450
+ """Structured permissions view using a table for readability."""
451
+ cfg = policy.config.policy
452
+ table = Table(title="permissions", box=BOX, show_header=False, border_style=PRIMARY, padding=(0, 1), width=MAX_WIDTH)
453
+ table.add_column("key", style=DIM, width=18)
454
+ table.add_column("value")
455
+ mode_color = MODE_STYLE.get(policy.mode.value, DIM)
456
+ table.add_row("mode", Text(policy.mode.value, style=f"bold {mode_color}"))
457
+ table.add_row("blocked commands", ", ".join(sorted(policy.blocked_commands)) or "(none)")
458
+ table.add_row("allowed commands", ", ".join(sorted(policy.allowed_commands)) or "(none)")
459
+ table.add_row(
460
+ "writable roots",
461
+ ", ".join(str(r) for r in cfg.additional_writable_roots) or "(workspace only)",
462
+ )
463
+ table.add_row("read-only paths", ", ".join(str(r) for r in cfg.read_only_paths) or "(none)")
464
+ table.add_row("shell timeout", f"{cfg.shell_timeout_seconds}s")
465
+ console.print(table)
466
+
467
+
468
+ def print_transcript(messages: list[Message], n: int = 20) -> None:
469
+ if not messages:
470
+ console.print(Text("no transcript", style=DIM))
471
+ return
472
+ for m in messages[-n:]:
473
+ icon = ROLE_ICON.get(m.role, "·")
474
+ style = ROLE_STYLE.get(m.role, "white")
475
+ if m.tool_calls:
476
+ calls = ", ".join(c.name for c in m.tool_calls)
477
+ console.print(Text.assemble((f"{icon} ", style), (f"{m.role} ", f"bold {style}"), (f"tools: {calls}", DIM)))
478
+ else:
479
+ text = m.content[:300] + ("…" if len(m.content) > 300 else "")
480
+ console.print(Text.assemble((f"{icon} ", style), (f"{m.role} ", f"bold {style}"), (text, "white")))
481
+
482
+
483
+ def print_tools(definitions: list) -> None: # type: ignore[no-untyped-def]
484
+ """Grouped tool listing with risk badges."""
485
+ table = Table(title="tools", box=BOX, show_header=True, header_style=f"bold {PRIMARY}", border_style=PRIMARY, width=MAX_WIDTH)
486
+ table.add_column("name", style="bold", width=18)
487
+ table.add_column("risk", width=8)
488
+ table.add_column("description")
489
+ for d in definitions:
490
+ risk_color = RISK_STYLE.get(d.risk.value, DIM)
491
+ table.add_row(d.name, Text(d.risk.value, style=f"bold {risk_color}"), d.description)
492
+ console.print(table)
493
+
494
+
495
+ def print_error(text: str) -> None:
496
+ """Inline red error under a ⏺ bullet, Claude-Code-style."""
497
+ console.print(_bullet_grid(Text(BULLET, style=DANGER), Text(f"Error: {text}", style=DANGER)))
498
+
499
+
500
+ def print_info(text: str) -> None:
501
+ console.print(Text(text, style=DIM))
502
+
503
+
504
+ def print_warn(text: str) -> None:
505
+ console.print(Text(text, style=WARN))
506
+
507
+
508
+ def rule(title: str = "") -> None:
509
+ """Horizontal separator between sections/turns."""
510
+ if title:
511
+ console.rule(Text(title, style=DIM), style=DIM, align="left")
512
+ else:
513
+ console.rule(style=DIM, align="left")
514
+
515
+
516
+ # Backward-compat aliases for any callers expecting the old names.
517
+ status_line = status_bar
@@ -0,0 +1,14 @@
1
+ """Interactive REPL entry point.
2
+
3
+ Thin wrapper around the full-screen chat TUI in ui.app — kept as a separate
4
+ module so app/commands.py's `from reidx.ui.repl import repl` doesn't need
5
+ to change.
6
+ """
7
+ from __future__ import annotations
8
+
9
+ from reidx.runtime.orchestrator import Orchestrator
10
+ from reidx.ui import app
11
+
12
+
13
+ def repl(orchestrator: Orchestrator, initial_prompt: str | None = None) -> int:
14
+ return app.run(orchestrator, initial_prompt=initial_prompt)
@@ -0,0 +1,126 @@
1
+ """Centralized theme: colors, styles, and visual constants.
2
+
3
+ ReidX wears a Claude-Code-style skin in a red palette:
4
+ - dark technical feel, high contrast
5
+ - rounded borders, ⏺ bullets, ⎿ tree connectors
6
+ - clean density over decorative excess
7
+
8
+ All UI modules should pull colors + glyphs from here so the look stays consistent.
9
+ """
10
+ from __future__ import annotations
11
+
12
+ from pathlib import Path
13
+
14
+ from rich import box
15
+ from rich.text import Text
16
+
17
+ # Brand.
18
+ APP_NAME = "ReidX"
19
+
20
+ # Core palette — red-forward, high-contrast.
21
+ PRIMARY = "#ff5f5f" # brand, assistant, headers (brand red)
22
+ SECONDARY = "#d75f5f" # secondary accents
23
+ DIM = "dim" # labels, metadata
24
+ SUCCESS = "green" # ok, completed
25
+ WARN = "yellow" # active, prompts, warnings
26
+ DANGER = "red" # errors, failed, blocked
27
+ MUTED = "magenta" # special states
28
+
29
+ # Claude-Code-style glyphs. Use text-presentation code points (not emoji) so the
30
+ # terminal renders them flat and honors the Rich style color. U+23FA (⏺) is an
31
+ # emoji and renders as a fixed-color double-width blob — avoid it.
32
+ SPARKLE = "✻" # welcome banner + thinking mark
33
+ BULLET = "●" # assistant / tool-call bullet (U+25CF, text circle)
34
+ TREE = "⎿" # tool-result connector
35
+ PROMPT = "›" # input prompt caret
36
+
37
+ # Default border box for panels/tables.
38
+ BOX = box.ROUNDED
39
+
40
+ # Role icons for transcript display.
41
+ ROLE_ICON = {
42
+ "system": "§",
43
+ "user": "›",
44
+ "assistant": BULLET,
45
+ "tool": TREE,
46
+ }
47
+
48
+ ROLE_STYLE = {
49
+ "system": DIM,
50
+ "user": "bold",
51
+ "assistant": PRIMARY,
52
+ "tool": SUCCESS,
53
+ }
54
+
55
+ # Status badge styles.
56
+ STATUS_STYLE = {
57
+ "draft": DIM,
58
+ "completed": SUCCESS,
59
+ "active": WARN,
60
+ "failed": DANGER,
61
+ "blocked": MUTED,
62
+ "pending": DIM,
63
+ "skipped": DIM,
64
+ "archived": DIM,
65
+ "abandoned": DIM,
66
+ }
67
+
68
+ # Risk colors.
69
+ RISK_STYLE = {
70
+ "low": SUCCESS,
71
+ "medium": WARN,
72
+ "high": DANGER,
73
+ }
74
+
75
+ # Permission mode colors.
76
+ MODE_STYLE = {
77
+ "strict": DANGER,
78
+ "balanced": WARN,
79
+ "autonomous": SUCCESS,
80
+ "custom": MUTED,
81
+ }
82
+
83
+
84
+ # Visual constants.
85
+ MAX_WIDTH = 80 # constrain panels/tables so they don't span ultra-wide terminals
86
+
87
+ # Known context-window sizes (tokens) by model prefix, for the status bar's
88
+ # usage readout. Falls back to DEFAULT_CONTEXT_WINDOW for unrecognized models.
89
+ DEFAULT_CONTEXT_WINDOW = 128_000
90
+ CONTEXT_WINDOWS = {
91
+ "stub": 32_000,
92
+ "gpt-5": 400_000,
93
+ "gpt-4": 128_000,
94
+ "claude": 200_000,
95
+ "o1": 200_000,
96
+ "o3": 200_000,
97
+ }
98
+
99
+
100
+ def context_window_for(model: str) -> int:
101
+ for prefix, size in CONTEXT_WINDOWS.items():
102
+ if model.startswith(prefix):
103
+ return size
104
+ return DEFAULT_CONTEXT_WINDOW
105
+
106
+
107
+ def fmt_tokens(n: int) -> str:
108
+ return f"{n / 1000:.1f}k" if n >= 1000 else str(n)
109
+
110
+
111
+ def short_path(path: str, keep: int = 2) -> str:
112
+ """Collapse a long workspace path to its last `keep` components."""
113
+ parts = Path(path).parts
114
+ if len(parts) <= keep:
115
+ return path
116
+ return str(Path(*parts[-keep:]))
117
+
118
+
119
+ def badge(text: str, color: str) -> Text:
120
+ """A compact colored badge like [ok] or [strict]."""
121
+ return Text(f"[{text}]", style=color)
122
+
123
+
124
+ def label_value(label: str, value: str, label_color: str = DIM) -> Text:
125
+ """A dim-label followed by a value: session abc123."""
126
+ return Text.assemble((f"{label} ", label_color), value)
@@ -0,0 +1,4 @@
1
+ from reidx.workflows.models import Workflow
2
+ from reidx.workflows.store import WorkflowStore
3
+
4
+ __all__ = ["Workflow", "WorkflowStore"]
@@ -0,0 +1,19 @@
1
+ """Workflow model: a named, reusable sequence of steps.
2
+
3
+ Each step is either a slash command (e.g. "/model gpt-5-5") or a plain prompt
4
+ submitted as a user turn — exactly what you'd type into the box, run in
5
+ order. Workflows are global (not per-session or per-workspace): saved once,
6
+ runnable from any session.
7
+ """
8
+ from __future__ import annotations
9
+
10
+ from datetime import UTC, datetime
11
+
12
+ from pydantic import BaseModel, Field
13
+
14
+
15
+ class Workflow(BaseModel):
16
+ name: str
17
+ description: str = ""
18
+ steps: list[str] = Field(default_factory=list)
19
+ created_at: datetime = Field(default_factory=lambda: datetime.now(UTC))