seedcode-cli 6.1.5__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 (114) hide show
  1. seedcode/__init__.py +14 -0
  2. seedcode/__main__.py +12 -0
  3. seedcode/app.py +508 -0
  4. seedcode/apps/__init__.py +32 -0
  5. seedcode/apps/discovery.py +241 -0
  6. seedcode/apps/installer.py +164 -0
  7. seedcode/apps/launcher.py +156 -0
  8. seedcode/apps/verifier.py +119 -0
  9. seedcode/assets/logo.txt +15 -0
  10. seedcode/cli.py +95 -0
  11. seedcode/commands/__init__.py +81 -0
  12. seedcode/commands/about.py +34 -0
  13. seedcode/commands/agent.py +94 -0
  14. seedcode/commands/assist.py +201 -0
  15. seedcode/commands/clear.py +20 -0
  16. seedcode/commands/desktop.py +104 -0
  17. seedcode/commands/doctor.py +152 -0
  18. seedcode/commands/help.py +61 -0
  19. seedcode/commands/history.py +365 -0
  20. seedcode/commands/palette.py +100 -0
  21. seedcode/commands/provider.py +451 -0
  22. seedcode/commands/theme.py +76 -0
  23. seedcode/computer/__init__.py +98 -0
  24. seedcode/computer/browser.py +276 -0
  25. seedcode/computer/browser_cdp.py +567 -0
  26. seedcode/computer/browser_engine.py +546 -0
  27. seedcode/computer/browser_extract.py +301 -0
  28. seedcode/computer/browser_popups.py +329 -0
  29. seedcode/computer/browser_selenium.py +209 -0
  30. seedcode/computer/browser_skills.py +245 -0
  31. seedcode/computer/catalog.py +200 -0
  32. seedcode/computer/controller.py +324 -0
  33. seedcode/computer/dispatcher.py +272 -0
  34. seedcode/computer/dpi.py +185 -0
  35. seedcode/computer/engine.py +105 -0
  36. seedcode/computer/keyboard.py +101 -0
  37. seedcode/computer/logbook.py +104 -0
  38. seedcode/computer/mouse.py +48 -0
  39. seedcode/computer/ocr.py +213 -0
  40. seedcode/computer/operator_skills.py +577 -0
  41. seedcode/computer/permissions.py +203 -0
  42. seedcode/computer/recovery.py +115 -0
  43. seedcode/computer/registry.py +107 -0
  44. seedcode/computer/resolver.py +434 -0
  45. seedcode/computer/screen.py +130 -0
  46. seedcode/computer/screen_state.py +412 -0
  47. seedcode/computer/selfguard.py +197 -0
  48. seedcode/computer/semantic.py +100 -0
  49. seedcode/computer/skills.py +139 -0
  50. seedcode/computer/state.py +199 -0
  51. seedcode/computer/verifier.py +177 -0
  52. seedcode/computer/vision.py +327 -0
  53. seedcode/computer/windows.py +217 -0
  54. seedcode/config/__init__.py +8 -0
  55. seedcode/config/defaults.py +22 -0
  56. seedcode/config/manager.py +62 -0
  57. seedcode/core/__init__.py +31 -0
  58. seedcode/core/agent.py +534 -0
  59. seedcode/core/chat.py +128 -0
  60. seedcode/core/client.py +9 -0
  61. seedcode/core/errors.py +199 -0
  62. seedcode/core/identity.py +66 -0
  63. seedcode/core/identity_store.py +119 -0
  64. seedcode/core/lifecycle.py +240 -0
  65. seedcode/core/limits.py +35 -0
  66. seedcode/core/models.py +347 -0
  67. seedcode/core/project.py +96 -0
  68. seedcode/core/providers/__init__.py +58 -0
  69. seedcode/core/providers/aerolink.py +324 -0
  70. seedcode/core/providers/base.py +230 -0
  71. seedcode/core/providers/freemodel.py +931 -0
  72. seedcode/core/providers/ollama.py +262 -0
  73. seedcode/core/providers/openrouter.py +393 -0
  74. seedcode/core/streaming.py +21 -0
  75. seedcode/memory/__init__.py +8 -0
  76. seedcode/memory/manager.py +47 -0
  77. seedcode/memory/storage.py +38 -0
  78. seedcode/memory/store.py +257 -0
  79. seedcode/tools/__init__.py +35 -0
  80. seedcode/tools/base.py +179 -0
  81. seedcode/tools/desktop.py +371 -0
  82. seedcode/tools/filesystem.py +309 -0
  83. seedcode/tools/git.py +72 -0
  84. seedcode/tools/patch.py +170 -0
  85. seedcode/tools/permissions.py +288 -0
  86. seedcode/tools/search.py +137 -0
  87. seedcode/tools/terminal.py +200 -0
  88. seedcode/tools/textio.py +59 -0
  89. seedcode/ui/__init__.py +164 -0
  90. seedcode/ui/badges.py +64 -0
  91. seedcode/ui/banner.py +78 -0
  92. seedcode/ui/dashboard.py +197 -0
  93. seedcode/ui/dialog.py +62 -0
  94. seedcode/ui/fuzzy.py +128 -0
  95. seedcode/ui/layout.py +54 -0
  96. seedcode/ui/menu.py +61 -0
  97. seedcode/ui/palette.py +40 -0
  98. seedcode/ui/progress.py +41 -0
  99. seedcode/ui/prompts.py +16 -0
  100. seedcode/ui/renderer.py +36 -0
  101. seedcode/ui/searchbox.py +70 -0
  102. seedcode/ui/selector.py +514 -0
  103. seedcode/ui/statusbar.py +38 -0
  104. seedcode/ui/textbox.py +61 -0
  105. seedcode/ui/theme.py +204 -0
  106. seedcode/ui/tree.py +91 -0
  107. seedcode/utils/__init__.py +22 -0
  108. seedcode/utils/helpers.py +97 -0
  109. seedcode/utils/logger.py +65 -0
  110. seedcode_cli-6.1.5.dist-info/METADATA +368 -0
  111. seedcode_cli-6.1.5.dist-info/RECORD +114 -0
  112. seedcode_cli-6.1.5.dist-info/WHEEL +4 -0
  113. seedcode_cli-6.1.5.dist-info/entry_points.txt +2 -0
  114. seedcode_cli-6.1.5.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,197 @@
1
+ """The Seed Code startup dashboard.
2
+
3
+ A single branded panel shown exactly once at application startup: the ASCII
4
+ logo, tagline and credits on the left, and a live "Current Session" summary
5
+ (provider, backend, model, status, context, history) on the right. Wide
6
+ terminals get the two-column layout with a vertical divider; narrow terminals
7
+ stack the session block below the logo. Every width decision is derived from
8
+ the live console size and measured content — nothing is padded by hand.
9
+
10
+ Rendering uses plain Rich primitives (Panel, Columns, Table.grid, Rule) so the
11
+ dashboard displays identically in Windows Terminal, PowerShell, CMD, Linux and
12
+ macOS terminals; legacy Windows consoles get pure-ASCII fallbacks.
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ from rich import box
18
+ from rich.align import Align
19
+ from rich.console import Console, Group, RenderableType
20
+ from rich.panel import Panel
21
+ from rich.rule import Rule
22
+ from rich.table import Table
23
+ from rich.text import Text
24
+
25
+ from .. import APP_NAME, TAGLINE, __version__
26
+ from ..core.models import AppConfig
27
+ from ..core.providers import PROVIDERS, provider_label
28
+ from ..core.providers.base import STATUS_CONNECTED, STATUS_UNKNOWN
29
+
30
+ _LOGO_LINES = [
31
+ r"███████╗███████╗███████╗██████╗ ██████╗ ██████╗ ██████╗ ███████╗",
32
+ r"██╔════╝██╔════╝██╔════╝██╔══██╗ ██╔════╝██╔═══██╗██╔══██╗██╔════╝",
33
+ r"███████╗█████╗ █████╗ ██║ ██║ ██║ ██║ ██║██║ ██║█████╗ ",
34
+ r"╚════██║██╔══╝ ██╔══╝ ██║ ██║ ██║ ██║ ██║██║ ██║██╔══╝ ",
35
+ r"███████║███████╗███████╗██████╔╝ ╚██████╗╚██████╔╝██████╔╝███████╗",
36
+ r"╚══════╝╚══════╝╚══════╝╚═════╝ ╚═════╝ ╚═════╝ ╚═════╝ ╚══════╝",
37
+ ]
38
+
39
+ # Pure-ASCII fallback for legacy Windows consoles (raster-font cmd.exe)
40
+ # where the block/box-drawing glyphs above render as garbage.
41
+ _LOGO_LINES_ASCII = [
42
+ r" ____ _____ _____ ____ ____ ___ ____ _____ ",
43
+ r"/ ___|| ____| ____| _ \ / ___/ _ \| _ \| ____|",
44
+ r"\___ \| _| | _| | | | | | | | | | | | | | _| ",
45
+ r" ___) | |___| |___| |_| | | |__| |_| | |_| | |___ ",
46
+ r"|____/|_____|_____|____/ \____\___/|____/|_____|",
47
+ ]
48
+
49
+ _CREDIT = "Created by Al Shahriar Sowan"
50
+
51
+
52
+ # --- session state ----------------------------------------------------------
53
+ def _provider_ready(config: AppConfig) -> bool:
54
+ """True when the active provider is selected and has what it needs."""
55
+ provider = PROVIDERS.get(config.provider)
56
+ if provider is None:
57
+ return False
58
+ return not provider.requires_key or bool(config.get_api_key().strip())
59
+
60
+
61
+ def _provider_value(config: AppConfig) -> str:
62
+ """Provider display label."""
63
+ if not _provider_ready(config):
64
+ return "Not selected"
65
+ return provider_label(config.provider)
66
+
67
+
68
+ def _backend_value(config: AppConfig) -> str:
69
+ """The API family the active provider talks to (its own backend_label)."""
70
+ if not _provider_ready(config):
71
+ return "--"
72
+ if config.provider == "ollama":
73
+ return config.ollama_host
74
+ provider = PROVIDERS.get(config.provider)
75
+ if provider is not None and provider.backend_label:
76
+ return provider.backend_label
77
+ return f"{provider_label(config.provider)} API"
78
+
79
+
80
+ def _model_value(config: AppConfig) -> str:
81
+ if not config.model:
82
+ return "Not selected"
83
+ if config.model == "auto":
84
+ return "Auto (best free model)"
85
+ return config.model
86
+
87
+
88
+ def _status_value(config: AppConfig, marker: str) -> Text:
89
+ """Connection status from the provider's session cache (no network I/O)."""
90
+ if not config.is_configured():
91
+ return Text("Not configured", style="seed.dim")
92
+ provider = PROVIDERS.get(config.provider)
93
+ status = provider.status if provider is not None else STATUS_UNKNOWN
94
+ if status == STATUS_CONNECTED:
95
+ return Text(f"{marker} Connected", style="seed.success")
96
+ if status == STATUS_UNKNOWN:
97
+ # Configured but not probed yet — ready to chat, not yet verified.
98
+ return Text(f"{marker} Ready", style="seed.success")
99
+ return Text(f"{marker} {status}", style="seed.dim")
100
+
101
+
102
+ def _context_value(config: AppConfig) -> str:
103
+ """Model context window — the live catalogue is not cached at startup."""
104
+ return "--"
105
+
106
+
107
+ def _mode_value(config: AppConfig) -> str:
108
+ """The user-facing mode: Chat or Assist (never Agent/Desktop)."""
109
+ return "Assist" if config.agent_mode else "Chat"
110
+
111
+
112
+ # --- blocks -----------------------------------------------------------------
113
+ def _brand_block(legacy: bool) -> RenderableType:
114
+ """Left side: the exact ASCII logo with tagline and author centered under it."""
115
+ lines = _LOGO_LINES_ASCII if legacy else _LOGO_LINES
116
+ logo = Text("\n".join(lines), style="seed.primary", no_wrap=True, overflow="crop")
117
+ tagline = Text(TAGLINE, style="seed.accent")
118
+ credit = Text(_CREDIT, style="seed.dim")
119
+ return Group(
120
+ Align.center(logo),
121
+ Text(),
122
+ Align.center(tagline),
123
+ Text(),
124
+ Align.center(credit),
125
+ )
126
+
127
+
128
+ def _session_block(config: AppConfig, legacy: bool) -> RenderableType:
129
+ """Right side: the dynamic Current Session summary."""
130
+ marker = "*" if legacy else "●"
131
+ grid = Table.grid(padding=(0, 1))
132
+ grid.add_column(style="seed.accent", no_wrap=True)
133
+ grid.add_column(style="seed.dim", no_wrap=True)
134
+ grid.add_column(style="seed.text", overflow="fold")
135
+ rows: list[tuple[str, RenderableType]] = [
136
+ ("Provider", Text(_provider_value(config))),
137
+ ("Backend", Text(_backend_value(config))),
138
+ ("Model", Text(_model_value(config))),
139
+ ("Mode", Text(_mode_value(config), style="seed.accent")),
140
+ ("Status", _status_value(config, marker)),
141
+ ("Context", Text(_context_value(config))),
142
+ ("History", Text("Enabled")),
143
+ ]
144
+ for label, value in rows:
145
+ grid.add_row(label, ":", value)
146
+ return Group(
147
+ Text("Current Session", style="seed.primary"),
148
+ Rule(style="seed.dim", characters="-" if legacy else "─"),
149
+ Text(),
150
+ grid,
151
+ )
152
+
153
+
154
+ def _logo_width(legacy: bool) -> int:
155
+ lines = _LOGO_LINES_ASCII if legacy else _LOGO_LINES
156
+ return max(len(line) for line in lines)
157
+
158
+
159
+ # --- dashboard --------------------------------------------------------------
160
+ def render_dashboard(console: Console, config: AppConfig) -> None:
161
+ """Render the startup dashboard panel, adapting to the terminal width."""
162
+ legacy = console.legacy_windows
163
+ brand = _brand_block(legacy)
164
+ session = _session_block(config, legacy)
165
+
166
+ session_width = console.measure(session).maximum
167
+ # Panel borders/padding (6) + column gutters and divider (5), all fixed
168
+ # rendering overhead — the terminal width itself is never assumed.
169
+ needed = _logo_width(legacy) + session_width + 11
170
+
171
+ if console.size.width >= needed:
172
+ # Two columns with a single vertical divider between them: a Table
173
+ # whose outer edge is hidden renders only the column separator.
174
+ body: RenderableType = Table(
175
+ box=box.SQUARE,
176
+ show_header=False,
177
+ show_edge=False,
178
+ border_style="seed.dim",
179
+ padding=(0, 2),
180
+ pad_edge=False,
181
+ )
182
+ body.add_column()
183
+ body.add_column()
184
+ body.add_row(brand, session)
185
+ else:
186
+ # Narrow terminals: stack the session block below the logo.
187
+ body = Group(brand, Text(), Align.center(session))
188
+
189
+ console.print(
190
+ Panel(
191
+ body,
192
+ title=f"{APP_NAME} v{__version__}",
193
+ title_align="left",
194
+ border_style="seed.primary",
195
+ padding=(1, 2),
196
+ )
197
+ )
seedcode/ui/dialog.py ADDED
@@ -0,0 +1,62 @@
1
+ """Interactive dialogs: confirmations and permission prompts.
2
+
3
+ Replaces every [Y]/[N] text prompt with an arrow-key choice list:
4
+
5
+ ❯ Allow Once
6
+ Always Allow
7
+ Deny
8
+
9
+ Esc/Ctrl+C always mean the safe answer (deny/cancel) — an accidental
10
+ cancel can never grant a permission.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ from .selector import Option, select
16
+
17
+ # Values returned by permission_dialog, aligned with the existing gates.
18
+ ALLOW_ONCE = "y"
19
+ ALLOW_ALWAYS = "a"
20
+ DENY = "n"
21
+
22
+
23
+ def permission_dialog(*, allow_always: bool = True) -> str:
24
+ """Ask Allow Once / Always Allow / Deny; returns 'y', 'a', or 'n'.
25
+
26
+ Cancelling (Esc/Ctrl+C) returns 'n' — never an approval.
27
+ """
28
+ options = [Option("Allow Once", ALLOW_ONCE, detail="approve this action only")]
29
+ if allow_always:
30
+ options.append(
31
+ Option("Always Allow", ALLOW_ALWAYS, detail="approve for this session")
32
+ )
33
+ options.append(Option("Deny", DENY, detail="block this action"))
34
+ result = select(
35
+ options,
36
+ searchable=False,
37
+ hint="↑↓ move Enter confirm Esc deny",
38
+ )
39
+ return result if result in (ALLOW_ONCE, ALLOW_ALWAYS, DENY) else DENY
40
+
41
+
42
+ def confirm_dialog(
43
+ question: str,
44
+ *,
45
+ yes_label: str = "Yes",
46
+ no_label: str = "No",
47
+ danger: bool = False,
48
+ ) -> bool:
49
+ """A two-option yes/no dialog; Esc/cancel counts as No.
50
+
51
+ ``danger=True`` puts No first so Enter-mashing never destroys anything.
52
+ """
53
+ yes = Option(yes_label, True)
54
+ no = Option(no_label, False)
55
+ options = [no, yes] if danger else [yes, no]
56
+ result = select(
57
+ options,
58
+ title=question,
59
+ searchable=False,
60
+ hint="↑↓ move Enter confirm Esc cancel",
61
+ )
62
+ return bool(result)
seedcode/ui/fuzzy.py ADDED
@@ -0,0 +1,128 @@
1
+ """Fuzzy matching for the interactive components.
2
+
3
+ Subsequence matching with a smallest-window score, tuned for picking model
4
+ ids and command names:
5
+
6
+ * ``cld`` matches **Cl**au**d**e
7
+ * ``gpt55`` matches **GPT-5.5** (separators are skipped freely)
8
+ * ``opus`` matches Claude **Opus**
9
+
10
+ Scoring prefers, in order: exact substring at a word start, exact substring
11
+ anywhere, then the tightest subsequence window with bonuses for matches at
12
+ word boundaries and consecutive runs. Pure Python, no dependencies, fast
13
+ enough for thousand-entry model lists (a single linear scan per candidate).
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ from dataclasses import dataclass
19
+
20
+ _WORD_SEPARATORS = set(" -_./:@")
21
+
22
+
23
+ @dataclass(slots=True)
24
+ class FuzzyResult:
25
+ """Outcome of matching one query against one candidate string."""
26
+
27
+ matched: bool
28
+ score: float
29
+ positions: tuple[int, ...] # candidate indices to highlight
30
+
31
+
32
+ _NO_MATCH = FuzzyResult(False, float("-inf"), ())
33
+
34
+
35
+ def _is_boundary(text: str, i: int) -> bool:
36
+ """True when ``text[i]`` starts a word (position 0, after a separator,
37
+ a digit after a letter, or an upper-case letter after a lower-case one)."""
38
+ if i == 0:
39
+ return True
40
+ prev, ch = text[i - 1], text[i]
41
+ if prev in _WORD_SEPARATORS:
42
+ return True
43
+ if ch.isdigit() and prev.isalpha():
44
+ return True
45
+ if ch.isupper() and prev.islower():
46
+ return True
47
+ return False
48
+
49
+
50
+ def _subsequence_from(low_text: str, low_query: str, start: int) -> list[int] | None:
51
+ """Greedy left-to-right subsequence match beginning at/after ``start``."""
52
+ positions: list[int] = []
53
+ i = start
54
+ for qc in low_query:
55
+ j = low_text.find(qc, i)
56
+ if j < 0:
57
+ return None
58
+ positions.append(j)
59
+ i = j + 1
60
+ return positions
61
+
62
+
63
+ def fuzzy_match(query: str, text: str) -> FuzzyResult:
64
+ """Match ``query`` against ``text``; empty queries match everything."""
65
+ if not query:
66
+ return FuzzyResult(True, 0.0, ())
67
+ if not text:
68
+ return _NO_MATCH
69
+
70
+ low_text = text.lower()
71
+ low_query = "".join(ch for ch in query.lower() if not ch.isspace())
72
+ if not low_query:
73
+ return FuzzyResult(True, 0.0, ())
74
+
75
+ # Fast paths: exact substring (best at a word boundary).
76
+ idx = low_text.find(low_query)
77
+ if idx >= 0:
78
+ span = tuple(range(idx, idx + len(low_query)))
79
+ bonus = 200.0 if _is_boundary(text, idx) else 100.0
80
+ return FuzzyResult(True, 1000.0 + bonus - idx * 0.5 - len(text) * 0.01, span)
81
+
82
+ # Subsequence: try anchoring at each occurrence of the first query char
83
+ # and keep the best-scoring window (bounded to a handful of anchors).
84
+ first = low_query[0]
85
+ best: FuzzyResult = _NO_MATCH
86
+ anchor = low_text.find(first)
87
+ tries = 0
88
+ while anchor >= 0 and tries < 8:
89
+ positions = _subsequence_from(low_text, low_query, anchor)
90
+ if positions is None:
91
+ break # later anchors can only fail too
92
+ score = _score(text, positions)
93
+ if score > best.score:
94
+ best = FuzzyResult(True, score, tuple(positions))
95
+ anchor = low_text.find(first, anchor + 1)
96
+ tries += 1
97
+ return best
98
+
99
+
100
+ def _score(text: str, positions: list[int]) -> float:
101
+ window = positions[-1] - positions[0] + 1
102
+ score = 500.0 - (window - len(positions)) * 10.0 # tighter window is better
103
+ for k, pos in enumerate(positions):
104
+ if _is_boundary(text, pos):
105
+ score += 15.0
106
+ if k > 0 and positions[k - 1] == pos - 1:
107
+ score += 5.0 # consecutive run
108
+ score -= positions[0] * 0.5 # earlier start is better
109
+ score -= len(text) * 0.01 # shorter candidates win ties
110
+ return score
111
+
112
+
113
+ def fuzzy_filter(
114
+ query: str, items: list, key=lambda item: str(item)
115
+ ) -> list[tuple[object, FuzzyResult]]:
116
+ """Return ``(item, result)`` for matching items, best score first.
117
+
118
+ Sorting is stable for equal scores, so the caller's original order is
119
+ preserved within ties (important for grouped model lists).
120
+ """
121
+ scored: list[tuple[object, FuzzyResult]] = []
122
+ for item in items:
123
+ result = fuzzy_match(query, key(item))
124
+ if result.matched:
125
+ scored.append((item, result))
126
+ if query:
127
+ scored.sort(key=lambda pair: -pair[1].score)
128
+ return scored
seedcode/ui/layout.py ADDED
@@ -0,0 +1,54 @@
1
+ """Shared layout conventions: panels, key-value grids, shortcut tables.
2
+
3
+ Every screen builds its content through these helpers so padding, borders
4
+ and column styles stay identical across the app.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from typing import Iterable, Sequence
10
+
11
+ from rich.console import RenderableType
12
+ from rich.panel import Panel
13
+ from rich.table import Table
14
+
15
+
16
+ def branded_panel(body: RenderableType, title: str | None = None) -> Panel:
17
+ """The standard Seed Code panel: primary border, left title, padding."""
18
+ return Panel(
19
+ body,
20
+ title=title,
21
+ border_style="seed.primary",
22
+ title_align="left",
23
+ padding=(1, 2),
24
+ )
25
+
26
+
27
+ def kv_grid(rows: Iterable[tuple[str, RenderableType]]) -> Table:
28
+ """A two-column label/value grid (labels dimmed right, values plain)."""
29
+ grid = Table.grid(padding=(0, 3))
30
+ grid.add_column(style="seed.dim", justify="right", no_wrap=True)
31
+ grid.add_column(style="seed.text")
32
+ for label, value in rows:
33
+ grid.add_row(label, value)
34
+ return grid
35
+
36
+
37
+ def columns_grid(rows: Sequence[Sequence[str]], styles: Sequence[str]) -> Table:
38
+ """An n-column grid with one style per column."""
39
+ grid = Table.grid(padding=(0, 2))
40
+ for style in styles:
41
+ grid.add_column(style=style)
42
+ for row in rows:
43
+ grid.add_row(*row)
44
+ return grid
45
+
46
+
47
+ def shortcuts_grid(pairs: Sequence[tuple[str, str]]) -> Table:
48
+ """Keyboard-shortcut table: accent keys, plain descriptions."""
49
+ grid = Table.grid(padding=(0, 3))
50
+ grid.add_column(style="seed.accent", no_wrap=True)
51
+ grid.add_column(style="seed.text")
52
+ for key, action in pairs:
53
+ grid.add_row(key, action)
54
+ return grid
seedcode/ui/menu.py ADDED
@@ -0,0 +1,61 @@
1
+ """Action menus built on the interactive Selector.
2
+
3
+ A menu is a selector over labelled actions: the main menu, the API-key
4
+ menu, and every "pick one of these things to do" screen use this instead
5
+ of numbered rows. Items may show a live status column and a badge.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from dataclasses import dataclass
11
+ from typing import Any, Sequence
12
+
13
+ from .selector import Option, select
14
+
15
+
16
+ @dataclass(slots=True)
17
+ class MenuItem:
18
+ """One menu action: a label, the value returned when chosen, and an
19
+ optional status column shown dimmed to the right."""
20
+
21
+ label: str
22
+ value: Any = None
23
+ status: str = ""
24
+ badge: str = ""
25
+ group: str = ""
26
+ disabled: bool = False
27
+
28
+ def __post_init__(self) -> None:
29
+ if self.value is None:
30
+ self.value = self.label
31
+
32
+
33
+ def run_menu(
34
+ items: Sequence[MenuItem],
35
+ *,
36
+ title: str = "",
37
+ breadcrumbs: Sequence[str] = (),
38
+ hint: str = "",
39
+ initial: Any = None,
40
+ searchable: bool = True,
41
+ ) -> Any | None:
42
+ """Show an interactive menu; returns the chosen item's value or None."""
43
+ options = [
44
+ Option(
45
+ label=item.label,
46
+ value=item.value,
47
+ columns=(item.status,) if item.status else (),
48
+ badge=item.badge,
49
+ group=item.group,
50
+ disabled=item.disabled,
51
+ )
52
+ for item in items
53
+ ]
54
+ return select(
55
+ options,
56
+ title=title,
57
+ breadcrumbs=breadcrumbs,
58
+ hint=hint,
59
+ initial=initial,
60
+ searchable=searchable,
61
+ )
seedcode/ui/palette.py ADDED
@@ -0,0 +1,40 @@
1
+ """Command palette (Ctrl+K) — searchable actions, VS Code style.
2
+
3
+ The palette is a fuzzy selector over registered actions, opened from the
4
+ chat prompt with Ctrl+K. Actions are plain (label, value) pairs supplied
5
+ by the app layer, so this module stays free of business logic.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from dataclasses import dataclass
11
+ from typing import Any, Sequence
12
+
13
+ from .selector import Option, select
14
+
15
+
16
+ @dataclass(slots=True)
17
+ class PaletteAction:
18
+ """One palette entry: display label, returned value, optional detail."""
19
+
20
+ label: str
21
+ value: Any
22
+ detail: str = ""
23
+ group: str = ""
24
+
25
+
26
+ def command_palette(
27
+ actions: Sequence[PaletteAction],
28
+ *,
29
+ title: str = "Command Palette",
30
+ ) -> Any | None:
31
+ """Open the palette; returns the chosen action's value or None."""
32
+ return select(
33
+ [
34
+ Option(a.label, a.value, detail=a.detail, group=a.group)
35
+ for a in actions
36
+ ],
37
+ title=title,
38
+ hint="type to search ↑↓ move Enter run Esc close",
39
+ max_rows=12,
40
+ )
@@ -0,0 +1,41 @@
1
+ """Progress primitives: themed spinners and step indicators.
2
+
3
+ Kept deliberately small — the heavy lifting is Rich's Live display, which
4
+ already does minimal-redraw updates. This module provides the branded
5
+ wrappers so every wait state looks the same.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from contextlib import contextmanager
11
+ from typing import Iterator
12
+
13
+ from rich.console import Console
14
+ from rich.live import Live
15
+ from rich.spinner import Spinner
16
+ from rich.text import Text
17
+
18
+
19
+ @contextmanager
20
+ def spinner(console: Console, label: str = "Working") -> Iterator[None]:
21
+ """A transient themed spinner around a blocking operation."""
22
+ view = Spinner("dots", text=Text(f" {label}...", style="seed.accent"))
23
+ with Live(view, console=console, refresh_per_second=12, transient=True):
24
+ yield
25
+
26
+
27
+ class StepProgress:
28
+ """Sequential step reporting: ⟳ while running, ✓/✗ when finished."""
29
+
30
+ def __init__(self, console: Console) -> None:
31
+ self._console = console
32
+
33
+ def start(self, label: str) -> None:
34
+ self._console.print(Text(f"⟳ {label}...", style="seed.warning"))
35
+
36
+ def done(self, label: str) -> None:
37
+ self._console.print(Text(f"✓ {label}", style="seed.success"))
38
+
39
+ def fail(self, label: str, reason: str = "") -> None:
40
+ suffix = f" — {reason}" if reason else ""
41
+ self._console.print(Text(f"✗ {label}{suffix}", style="seed.error"))
seedcode/ui/prompts.py ADDED
@@ -0,0 +1,16 @@
1
+ """Compatibility shim — the input component moved to :mod:`seedcode.ui.textbox`.
2
+
3
+ Kept so existing imports (``from ..ui.prompts import read_line``) continue
4
+ to work; new code should import from ``textbox`` directly.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from .textbox import prompt_label, read_line, read_text
10
+ from .theme import pt_style
11
+
12
+ # Legacy name: a static style object matching the current theme at import
13
+ # time. Prefer pt_style() for live-theme correctness.
14
+ PT_STYLE = pt_style()
15
+
16
+ __all__ = ["PT_STYLE", "prompt_label", "read_line", "read_text", "pt_style"]
@@ -0,0 +1,36 @@
1
+ """Live streaming markdown renderer.
2
+
3
+ Accumulates streamed tokens and re-renders them as live markdown so code blocks
4
+ and formatting appear as the assistant types.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from rich.console import Console
10
+ from rich.live import Live
11
+ from rich.markdown import Markdown
12
+
13
+
14
+ class StreamRenderer:
15
+ """Accumulates streamed tokens and re-renders them as live markdown."""
16
+
17
+ def __init__(self, console: Console) -> None:
18
+ self._console = console
19
+ self._buffer = ""
20
+ self._live: Live | None = None
21
+
22
+ def bind(self, live: Live) -> None:
23
+ self._live = live
24
+
25
+ def renderable(self):
26
+ # Assistant output rendered as markdown for code blocks and formatting.
27
+ return Markdown(self._buffer or "", code_theme="ansi_dark")
28
+
29
+ def feed(self, chunk: str) -> None:
30
+ self._buffer += chunk
31
+ if self._live is not None:
32
+ self._live.update(self.renderable())
33
+
34
+ @property
35
+ def text(self) -> str:
36
+ return self._buffer