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,200 @@
1
+ """Terminal execution tool: run a shell command in the workspace, live.
2
+
3
+ Commands run through ``subprocess.Popen`` with a reader thread so output
4
+ streams line-by-line while the process is still running (Windows pipes have
5
+ no ``select``, hence the thread + queue). The agent layer can attach a
6
+ per-line callback via ``PermissionManager.on_output`` to echo progress to
7
+ the user as it happens.
8
+
9
+ Shell selection: ``cmd``, ``powershell`` and ``bash`` are supported
10
+ explicitly; the default ("") is the platform shell (cmd on Windows,
11
+ /bin/sh elsewhere) via ``shell=True``.
12
+
13
+ Cancellation: pressing Ctrl+C while a command is running kills that
14
+ command's whole process tree (``taskkill /T /F`` on Windows, process-group
15
+ SIGKILL elsewhere) and reports the cancellation as a failed tool result —
16
+ the agent turn itself continues, so the model learns the command was
17
+ cancelled instead of the loop dying.
18
+ """
19
+
20
+ from __future__ import annotations
21
+
22
+ import os
23
+ import queue
24
+ import signal
25
+ import subprocess
26
+ import sys
27
+ import threading
28
+ import time
29
+ from typing import IO, TYPE_CHECKING, Any, Callable
30
+
31
+ from .base import MAX_OUTPUT_CHARS, ToolResult, int_arg, register
32
+ from .permissions import CATEGORY_SHELL
33
+
34
+ if TYPE_CHECKING:
35
+ from .permissions import PermissionManager
36
+
37
+ _DEFAULT_TIMEOUT_S = 60
38
+ _MAX_TIMEOUT_S = 300
39
+
40
+ # Explicit shell -> argv builder. "" (default) uses shell=True instead.
41
+ _SHELLS: dict[str, Callable[[str], list[str]]] = {
42
+ "cmd": lambda c: ["cmd", "/d", "/c", c],
43
+ "powershell": lambda c: [
44
+ "powershell", "-NoProfile", "-NonInteractive", "-Command", c
45
+ ],
46
+ "bash": lambda c: ["bash", "-lc", c],
47
+ }
48
+
49
+ _EOF = object() # sentinel the reader thread puts when the pipe closes
50
+
51
+
52
+ def _kill_tree(proc: subprocess.Popen) -> None:
53
+ """Kill a process and all of its children; never raises."""
54
+ try:
55
+ if sys.platform == "win32":
56
+ subprocess.run(
57
+ ["taskkill", "/PID", str(proc.pid), "/T", "/F"],
58
+ capture_output=True,
59
+ )
60
+ else:
61
+ os.killpg(proc.pid, signal.SIGKILL)
62
+ except OSError:
63
+ pass
64
+ try:
65
+ proc.wait(timeout=5)
66
+ except (subprocess.TimeoutExpired, OSError):
67
+ pass
68
+
69
+
70
+ def _reader(pipe: IO[str], out: "queue.Queue[object]") -> None:
71
+ """Pump lines from the child's merged stdout into the queue."""
72
+ try:
73
+ for line in pipe:
74
+ out.put(line)
75
+ except (OSError, ValueError):
76
+ pass # pipe closed mid-read; treat as EOF
77
+ out.put(_EOF)
78
+
79
+
80
+ def run_command(
81
+ perm: "PermissionManager",
82
+ command: str,
83
+ timeout_s: int,
84
+ *,
85
+ shell: str = "",
86
+ on_line: Callable[[str], None] | None = None,
87
+ ) -> ToolResult:
88
+ """Shared runner (the git tool reuses it for real git invocations).
89
+
90
+ ``on_line`` receives each output line (rstripped) as it arrives; when
91
+ None, ``perm.on_output`` is used so the app layer's live echo applies
92
+ everywhere.
93
+ """
94
+ timeout_s = max(1, min(int(timeout_s), _MAX_TIMEOUT_S))
95
+ on_line = on_line or perm.on_output
96
+
97
+ if shell:
98
+ builder = _SHELLS.get(shell.strip().lower())
99
+ if builder is None:
100
+ choices = ", ".join(sorted(_SHELLS))
101
+ return ToolResult(False, f"Unknown shell '{shell}'. Choose one of: {choices}.")
102
+ popen_args: str | list[str] = builder(command)
103
+ use_shell = False
104
+ else:
105
+ popen_args = command
106
+ use_shell = True
107
+
108
+ kwargs: dict[str, Any] = {}
109
+ if sys.platform == "win32":
110
+ # Own process group so Ctrl+C in our console doesn't ambiguously
111
+ # signal the child; taskkill /T handles the whole tree regardless.
112
+ kwargs["creationflags"] = subprocess.CREATE_NEW_PROCESS_GROUP
113
+ else:
114
+ kwargs["start_new_session"] = True # killpg needs its own group
115
+
116
+ try:
117
+ proc = subprocess.Popen(
118
+ popen_args,
119
+ shell=use_shell,
120
+ cwd=perm.workspace,
121
+ stdout=subprocess.PIPE,
122
+ stderr=subprocess.STDOUT, # merged: live ordering stays sane
123
+ stdin=subprocess.DEVNULL,
124
+ text=True,
125
+ encoding="utf-8",
126
+ errors="replace",
127
+ **kwargs,
128
+ )
129
+ except OSError as exc:
130
+ return ToolResult(False, f"Could not run command: {exc}")
131
+
132
+ lines: "queue.Queue[object]" = queue.Queue()
133
+ thread = threading.Thread(target=_reader, args=(proc.stdout, lines), daemon=True)
134
+ thread.start()
135
+
136
+ chunks: list[str] = []
137
+ collected = 0
138
+ deadline = time.monotonic() + timeout_s
139
+ eof = False
140
+ try:
141
+ while not eof:
142
+ if time.monotonic() > deadline:
143
+ _kill_tree(proc)
144
+ partial = "".join(chunks).rstrip()
145
+ tail = f"\nOutput before timeout:\n{partial}" if partial else ""
146
+ return ToolResult(
147
+ False, f"Command timed out after {timeout_s}s: {command}{tail}"
148
+ )
149
+ try:
150
+ item = lines.get(timeout=0.2)
151
+ except queue.Empty:
152
+ continue
153
+ if item is _EOF:
154
+ eof = True
155
+ continue
156
+ line = str(item)
157
+ if collected < MAX_OUTPUT_CHARS:
158
+ chunks.append(line)
159
+ collected += len(line)
160
+ if on_line is not None:
161
+ try:
162
+ on_line(line.rstrip("\r\n"))
163
+ except Exception:
164
+ pass # a UI echo bug must never kill the command
165
+ except KeyboardInterrupt:
166
+ # Ctrl+C cancels THIS command, not the agent turn.
167
+ _kill_tree(proc)
168
+ partial = "".join(chunks).rstrip()
169
+ tail = f"\nOutput before cancel:\n{partial}" if partial else ""
170
+ return ToolResult(False, f"Command cancelled by user (Ctrl+C): {command}{tail}")
171
+
172
+ proc.wait()
173
+ body = "".join(chunks).rstrip() or "(no output)"
174
+
175
+ # Make exit code failure explicit
176
+ success = proc.returncode == 0
177
+ status = "✓ Success" if success else f"✗ FAILED (exit code {proc.returncode})"
178
+
179
+ return ToolResult(success, f"{status}\n{body}")
180
+
181
+
182
+ @register(
183
+ "run_command",
184
+ "Run a shell command in the workspace; output streams live to the user.",
185
+ {
186
+ "command": "the shell command to run",
187
+ "timeout": "(optional) seconds before the command is killed (default 60)",
188
+ "shell": "(optional) cmd, powershell, or bash (default: system shell)",
189
+ },
190
+ mutates=True,
191
+ types={"timeout": "integer"},
192
+ )
193
+ def _run_command(perm: "PermissionManager", args: dict[str, Any]) -> ToolResult:
194
+ command = str(args["command"]).strip()
195
+ if not command:
196
+ return ToolResult(False, "Command is empty.")
197
+ timeout_s = int_arg(args, "timeout", _DEFAULT_TIMEOUT_S, 1, _MAX_TIMEOUT_S)
198
+ perm.check_execute(command)
199
+ perm.confirm_action(CATEGORY_SHELL, command)
200
+ return run_command(perm, command, timeout_s, shell=str(args.get("shell", "")))
@@ -0,0 +1,59 @@
1
+ """Encoding- and newline-preserving text file I/O for edit-path tools.
2
+
3
+ Editing a latin-1 or CRLF file must not silently convert it to UTF-8/LF —
4
+ that corrupts files the user never asked to touch. Every tool that edits an
5
+ EXISTING file reads it through :func:`read_text_file` (which remembers the
6
+ encoding and dominant newline) and writes it back through
7
+ :func:`write_text_file` (which re-applies both). Matching and editing happen
8
+ on ``\\n``-normalized text, so tools never worry about CRLF.
9
+
10
+ Decoding order: UTF-8 with BOM ("utf-8-sig"), then strict UTF-8, then
11
+ latin-1 (which cannot fail — every byte is valid — so nothing is ever lossy
12
+ on the read side).
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ from dataclasses import dataclass
18
+ from pathlib import Path
19
+
20
+
21
+ @dataclass(slots=True)
22
+ class TextFile:
23
+ """A decoded text file: normalized text plus how to write it back."""
24
+
25
+ text: str # normalized to \n line endings
26
+ encoding: str # "utf-8-sig" | "utf-8" | "latin-1"
27
+ newline: str # "\r\n" | "\n"
28
+
29
+
30
+ def read_text_file(path: Path) -> TextFile:
31
+ """Read and decode, remembering encoding and dominant newline.
32
+
33
+ Raises OSError on filesystem problems (callers turn that into a
34
+ friendly ToolResult).
35
+ """
36
+ raw = path.read_bytes()
37
+
38
+ if raw.startswith(b"\xef\xbb\xbf"):
39
+ encoding = "utf-8-sig"
40
+ text = raw.decode("utf-8-sig")
41
+ else:
42
+ try:
43
+ text = raw.decode("utf-8")
44
+ encoding = "utf-8"
45
+ except UnicodeDecodeError:
46
+ text = raw.decode("latin-1")
47
+ encoding = "latin-1"
48
+
49
+ crlf = text.count("\r\n")
50
+ bare_lf = text.count("\n") - crlf
51
+ newline = "\r\n" if crlf > bare_lf else "\n"
52
+
53
+ return TextFile(text=text.replace("\r\n", "\n"), encoding=encoding, newline=newline)
54
+
55
+
56
+ def write_text_file(path: Path, tf: TextFile) -> None:
57
+ """Write normalized text back with the original newline and encoding."""
58
+ text = tf.text.replace("\n", tf.newline) if tf.newline != "\n" else tf.text
59
+ path.write_bytes(text.encode(tf.encoding))
@@ -0,0 +1,164 @@
1
+ """Rich-based presentation layer for Seed Code.
2
+
3
+ Everything the user sees on screen is produced here so the visual identity —
4
+ the Seed theme system, the startup dashboard, panels, spinners, and the
5
+ interactive component library (selector, menus, dialogs, palette) — stays in
6
+ one place. Business logic lives elsewhere and calls into these helpers.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from contextlib import contextmanager
12
+ from typing import Iterator
13
+
14
+ from rich.console import Console
15
+ from rich.live import Live
16
+ from rich.panel import Panel
17
+ from rich.spinner import Spinner
18
+ from rich.text import Text
19
+
20
+ from ..core.models import AppConfig
21
+ from .dashboard import render_dashboard
22
+ from .renderer import StreamRenderer
23
+ from .theme import SEED_THEME, rich_theme, set_active_theme
24
+
25
+ __all__ = ["UI", "StreamRenderer", "SEED_THEME"]
26
+
27
+
28
+ class UI:
29
+ """Thin wrapper around a Rich console with Seed Code styling helpers."""
30
+
31
+ def __init__(self) -> None:
32
+ self.console = Console(theme=rich_theme(), highlight=False)
33
+ # The active Live display (spinner/stream), if any — permission
34
+ # dialogs pause it so interactive input works cleanly.
35
+ self._live: Live | None = None
36
+ # Legacy Windows consoles (pre-Windows-Terminal cmd.exe with raster
37
+ # fonts) can't render ✔/✖ — fall back to pure-ASCII markers there.
38
+ if self.console.legacy_windows:
39
+ self._ok_mark, self._err_mark = "[OK]", "[X]"
40
+ else:
41
+ self._ok_mark, self._err_mark = "✔", "✖"
42
+ self._theme_pushed = False
43
+
44
+ def apply_theme(self, name: str) -> None:
45
+ """Switch the active theme everywhere (console + interactive styles).
46
+
47
+ Keeps exactly one theme overlay on the console so live-preview
48
+ arrowing through the picker never stacks themes.
49
+ """
50
+ set_active_theme(name)
51
+ if self._theme_pushed:
52
+ try:
53
+ self.console.pop_theme()
54
+ except Exception:
55
+ pass
56
+ self.console.push_theme(rich_theme(name))
57
+ self._theme_pushed = True
58
+
59
+ # --- primitives --------------------------------------------------------
60
+ def print(self, *args, **kwargs) -> None:
61
+ self.console.print(*args, **kwargs)
62
+
63
+ def blank(self) -> None:
64
+ self.console.print()
65
+
66
+ # --- startup -----------------------------------------------------------
67
+ def banner(self, config: AppConfig) -> None:
68
+ """Render the startup dashboard (shown exactly once at launch)."""
69
+ render_dashboard(self.console, config)
70
+
71
+ # --- chat rendering ----------------------------------------------------
72
+ @contextmanager
73
+ def thinking(self, label: str = "Thinking") -> Iterator[None]:
74
+ """Show a spinner while awaiting the first streamed token."""
75
+ spinner = Spinner("dots", text=Text(f" {label}...", style="seed.accent"))
76
+ with Live(
77
+ spinner, console=self.console, refresh_per_second=12, transient=True
78
+ ) as live:
79
+ self._live = live
80
+ try:
81
+ yield
82
+ finally:
83
+ self._live = None
84
+
85
+ @contextmanager
86
+ def streaming(self) -> Iterator["StreamRenderer"]:
87
+ """Provide a live, incrementally-updating markdown renderer."""
88
+ renderer = StreamRenderer(self.console)
89
+ with Live(
90
+ renderer.renderable(),
91
+ console=self.console,
92
+ refresh_per_second=15,
93
+ transient=False,
94
+ ) as live:
95
+ renderer.bind(live)
96
+ yield renderer
97
+ self.console.print()
98
+
99
+ # --- messaging ---------------------------------------------------------
100
+ def info(self, message: str) -> None:
101
+ self.console.print(Text(message, style="seed.text"))
102
+
103
+ def dim(self, message: str) -> None:
104
+ self.console.print(Text(message, style="seed.dim"))
105
+
106
+ def success(self, message: str) -> None:
107
+ self.console.print(Text(f"{self._ok_mark} {message}", style="seed.success"))
108
+
109
+ def warning(self, message: str) -> None:
110
+ self.console.print(Text(f"! {message}", style="seed.warning"))
111
+
112
+ def error(self, message: str) -> None:
113
+ self.console.print(Text(f"{self._err_mark} {message}", style="seed.error"))
114
+
115
+ def panel(self, body, title: str | None = None) -> None:
116
+ self.console.print(
117
+ Panel(
118
+ body,
119
+ title=title,
120
+ border_style="seed.primary",
121
+ title_align="left",
122
+ padding=(1, 2),
123
+ )
124
+ )
125
+
126
+ # --- action confirmation ------------------------------------------------
127
+ def _confirm(self, title: str, category_label: str, description: str) -> str:
128
+ """Ask the user to approve an action; returns 'y', 'a', or 'n'.
129
+
130
+ Shows the action details in a warning panel, then an interactive
131
+ Allow Once / Always Allow / Deny dialog. Pauses any live spinner so
132
+ the dialog renders cleanly, then resumes it. Cancelling (Esc or
133
+ Ctrl+C) counts as deny — never approve by accident.
134
+ """
135
+ from .dialog import permission_dialog
136
+
137
+ live = self._live
138
+ if live is not None:
139
+ live.stop()
140
+ try:
141
+ body = Text()
142
+ body.append(f"{category_label}\n", style="seed.warning")
143
+ body.append(description, style="seed.text")
144
+ self.console.print(
145
+ Panel(
146
+ body,
147
+ title=title,
148
+ border_style="seed.warning",
149
+ title_align="left",
150
+ padding=(1, 2),
151
+ )
152
+ )
153
+ return permission_dialog()
154
+ finally:
155
+ if live is not None:
156
+ live.start()
157
+
158
+ def confirm_desktop(self, category_label: str, description: str) -> str:
159
+ """Approve a desktop action; returns 'y', 'a', or 'n'."""
160
+ return self._confirm("Desktop Control", category_label, description)
161
+
162
+ def confirm_tool_action(self, category_label: str, description: str) -> str:
163
+ """Approve a dangerous agent tool action; returns 'y', 'a', or 'n'."""
164
+ return self._confirm("Assist Action", category_label, description)
seedcode/ui/badges.py ADDED
@@ -0,0 +1,64 @@
1
+ """Status badges — the one consistent indicator vocabulary for Seed Code.
2
+
3
+ Every screen that shows a status uses these markers so the language stays
4
+ uniform:
5
+
6
+ ● Connected ◐ Connecting ○ Offline ⚠ Error
7
+ ⟳ Loading ✓ Ready ✗ Failed
8
+
9
+ Badges exist in two renderings: prompt_toolkit fragments (interactive
10
+ selectors) and Rich markup (panels, dashboard).
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ from ..core.providers.base import (
16
+ STATUS_BAD_KEY,
17
+ STATUS_CONNECTED,
18
+ STATUS_NO_KEY,
19
+ STATUS_OFFLINE,
20
+ STATUS_UNKNOWN,
21
+ )
22
+
23
+ # badge key -> (marker, label, pt style class, rich style)
24
+ BADGES: dict[str, tuple[str, str, str, str]] = {
25
+ "connected": ("●", "Connected", "class:sel.ok", "seed.success"),
26
+ "connecting": ("◐", "Connecting", "class:sel.warn", "seed.warning"),
27
+ "offline": ("○", "Offline", "class:sel.off", "seed.dim"),
28
+ "error": ("⚠", "Error", "class:sel.err", "seed.error"),
29
+ "loading": ("⟳", "Loading", "class:sel.warn", "seed.warning"),
30
+ "ready": ("✓", "Ready", "class:sel.ok", "seed.success"),
31
+ "failed": ("✗", "Failed", "class:sel.err", "seed.error"),
32
+ }
33
+
34
+ # Provider session status -> badge key.
35
+ _STATUS_TO_BADGE = {
36
+ STATUS_CONNECTED: "connected",
37
+ STATUS_UNKNOWN: "ready",
38
+ STATUS_OFFLINE: "offline",
39
+ STATUS_NO_KEY: "offline",
40
+ STATUS_BAD_KEY: "error",
41
+ }
42
+
43
+
44
+ def badge_for_status(status: str) -> str:
45
+ """Map a provider connection status string to a badge key."""
46
+ return _STATUS_TO_BADGE.get(status, "offline")
47
+
48
+
49
+ def badge_fragment(key: str) -> tuple[str, str]:
50
+ """(pt style, text) for one badge, e.g. ('class:sel.ok', '● Connected')."""
51
+ marker, label, style, _ = BADGES.get(key, BADGES["offline"])
52
+ return style, f"{marker} {label}"
53
+
54
+
55
+ def badge_markup(key: str) -> str:
56
+ """Rich markup for one badge, e.g. '[seed.success]● Connected[/]'."""
57
+ marker, label, _, rich_style = BADGES.get(key, BADGES["offline"])
58
+ return f"[{rich_style}]{marker} {label}[/{rich_style}]"
59
+
60
+
61
+ def badge_text(key: str) -> str:
62
+ """Plain '● Connected' text for one badge."""
63
+ marker, label, _, _ = BADGES.get(key, BADGES["offline"])
64
+ return f"{marker} {label}"
seedcode/ui/banner.py ADDED
@@ -0,0 +1,78 @@
1
+ """The Seed Code startup banner.
2
+
3
+ A single branded screen: centered ASCII logo, wordmark, tagline and credits,
4
+ followed by a divider. Every line is centered dynamically against the live
5
+ terminal width (no hardcoded padding), rendered once at startup with plain
6
+ Text — no panels, boxes or animation — so it displays identically in Windows
7
+ Terminal, PowerShell, CMD and the VS Code terminal.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from rich.console import Console
13
+ from rich.text import Text
14
+
15
+ from ..core.models import AppConfig
16
+
17
+ _LOGO_LINES = [
18
+ r" ███████╗███████╗███████╗██████╗ ██████╗ ██████╗ ██████╗ ███████╗",
19
+ r" ██╔════╝██╔════╝██╔════╝██╔══██╗ ██╔════╝██╔═══██╗██╔══██╗██╔════╝",
20
+ r" ███████╗█████╗ █████╗ ██║ ██║ ██║ ██║ ██║██║ ██║█████╗ ",
21
+ r" ╚════██║██╔══╝ ██╔══╝ ██║ ██║ ██║ ██║ ██║██║ ██║██╔══╝ ",
22
+ r" ███████║███████╗███████╗██████╔╝ ╚██████╗╚██████╔╝██████╔╝███████╗",
23
+ r" ╚══════╝╚══════╝╚══════╝╚═════╝ ╚═════╝ ╚═════╝ ╚═════╝ ╚══════╝",
24
+ ]
25
+
26
+ # Pure-ASCII fallback for legacy Windows consoles (raster-font cmd.exe)
27
+ # where the block/box-drawing glyphs above render as garbage.
28
+ _LOGO_LINES_ASCII = [
29
+ r" ____ _____ _____ ____ ____ ___ ____ _____ ",
30
+ r" / ___|| ____| ____| _ \ / ___/ _ \| _ \| ____|",
31
+ r" \___ \| _| | _| | | | | | | | | | | | | | _| ",
32
+ r" ___) | |___| |___| |_| | | |__| |_| | |_| | |___ ",
33
+ r" |____/|_____|_____|____/ \____\___/|____/|_____|",
34
+ ]
35
+
36
+ # Monochrome green identity: bright bold logo, dimmer green for everything else.
37
+ _LOGO_STYLE = "bold #2ecc71"
38
+ _SOFT_STYLE = "#1cbf63"
39
+ _DIM_STYLE = "#159a4f"
40
+
41
+ _WORDMARK = "S E E D C O D E"
42
+ _TAGLINE = "Plant ideas. Grow code."
43
+ _CREDIT_HEADING = "Created by"
44
+ _CREDIT_NAME = "Al Shahriar Sowan"
45
+ _CREDIT_TOOLS = "Vibe coded with GPT-5.5 + Claude Opus 4.8"
46
+
47
+
48
+ def _centered(console: Console, text: str, style: str) -> None:
49
+ """Print one line centered against the current terminal width."""
50
+ pad = max((console.size.width - len(text)) // 2, 0)
51
+ line = Text(" " * pad + text, style=style)
52
+ line.no_wrap = True
53
+ console.print(line, overflow="crop")
54
+
55
+
56
+ def render_banner(console: Console, config: AppConfig) -> None:
57
+ """Render the branded startup banner to ``console``."""
58
+ logo = _LOGO_LINES_ASCII if console.legacy_windows else _LOGO_LINES
59
+ console.print()
60
+ for line in logo:
61
+ _centered(console, line, _LOGO_STYLE)
62
+
63
+ console.print()
64
+ _centered(console, _WORDMARK, _LOGO_STYLE)
65
+ console.print()
66
+ _centered(console, _TAGLINE, _SOFT_STYLE)
67
+ console.print()
68
+ _centered(console, _CREDIT_HEADING, _DIM_STYLE)
69
+ _centered(console, _CREDIT_NAME, _SOFT_STYLE)
70
+ console.print()
71
+ _centered(console, _CREDIT_TOOLS, _DIM_STYLE)
72
+
73
+ console.print()
74
+ divider_char = "-" if console.legacy_windows else "─"
75
+ divider = Text(divider_char * console.size.width, style=_DIM_STYLE)
76
+ divider.no_wrap = True
77
+ console.print(divider, overflow="crop")
78
+ console.print()