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
seedcode/cli.py ADDED
@@ -0,0 +1,95 @@
1
+ """Seed Code command-line entry point (``seedcode``).
2
+
3
+ Thin wrapper: prepares the Windows console, builds the UI, hands off to the
4
+ application controller (:mod:`seedcode.app`), and provides the final safety
5
+ net so a stray exception is shown as a friendly message rather than a
6
+ traceback. Heavy imports are deferred into :func:`main` so ``--version``
7
+ (used by installers to verify the install) answers instantly.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import sys
13
+
14
+
15
+ def _prepare_console() -> None:
16
+ """Make stdout/stderr UTF-8-safe, primarily for Windows.
17
+
18
+ Interactive Windows consoles accept Unicode natively (PEP 528), but a
19
+ redirected stream falls back to the legacy ANSI code page (e.g. cp1252),
20
+ where the banner's box-drawing characters would raise UnicodeEncodeError.
21
+ Reconfiguring to UTF-8 with ``errors="replace"`` makes output safe in
22
+ Windows Terminal, PowerShell, CMD, and when piped to files.
23
+ """
24
+ for stream in (sys.stdout, sys.stderr):
25
+ try:
26
+ stream.reconfigure(encoding="utf-8", errors="replace") # type: ignore[union-attr]
27
+ except (AttributeError, OSError, ValueError):
28
+ pass # exotic streams (tests, embedders) — never fatal
29
+
30
+
31
+ _HELP = """\
32
+ Seed Code - a terminal-based AI coding assistant. Plant ideas. Grow code.
33
+
34
+ Usage:
35
+ seedcode Start the interactive app (chat + menu)
36
+ seedcode --version Print the version and exit
37
+ seedcode --help Show this help and exit
38
+
39
+ Inside the app, type /help for commands: /provider, /model, /agent,
40
+ /permission, /settings, /doctor, and more.
41
+
42
+ Docs: https://github.com/Alshahriar-07/seedcode-cli
43
+ """
44
+
45
+
46
+ def main() -> None:
47
+ """Console-script entry point (``seedcode``)."""
48
+ # --version/--help must work non-interactively and fast (installers and
49
+ # scripts verify with them), so handle both before any UI, config, or
50
+ # logging work happens.
51
+ if len(sys.argv) > 1 and sys.argv[1] in ("--version", "-V"):
52
+ from . import __version__
53
+
54
+ print(f"Seed Code v{__version__}")
55
+ return
56
+ if len(sys.argv) > 1 and sys.argv[1] in ("--help", "-h"):
57
+ print(_HELP)
58
+ return
59
+
60
+ _prepare_console()
61
+
62
+ from .utils.logger import get_logger, setup_logging
63
+
64
+ setup_logging()
65
+ log = get_logger("cli")
66
+
67
+ from . import __version__
68
+
69
+ log.info(
70
+ "Seed Code v%s starting (python %s on %s)",
71
+ __version__,
72
+ sys.version.split()[0],
73
+ sys.platform,
74
+ )
75
+
76
+ from .app import run
77
+ from .ui import UI
78
+
79
+ ui = UI()
80
+ try:
81
+ run(ui)
82
+ log.info("Clean exit.")
83
+ except KeyboardInterrupt:
84
+ ui.blank()
85
+ ui.dim("Interrupted.")
86
+ log.info("Exited via Ctrl+C.")
87
+ except Exception as exc: # final safety net — never show a traceback
88
+ log.exception("Fatal error")
89
+ ui.error(f"Fatal error: {exc}")
90
+ ui.dim("Details were logged to ~/.seedcode/logs/seedcode.log")
91
+ sys.exit(1)
92
+
93
+
94
+ if __name__ == "__main__":
95
+ main()
@@ -0,0 +1,81 @@
1
+ """Slash-command system for the Seed Code REPL.
2
+
3
+ Commands are registered in a table and dispatched by name. Each handler receives
4
+ the live :class:`CommandContext` and returns a :class:`CommandResult` telling the
5
+ REPL whether to keep looping or exit. Handlers live in the sibling modules
6
+ (:mod:`help`, :mod:`clear`, :mod:`history`, :mod:`about`) and are imported at the
7
+ bottom of this file so their ``@command`` decorators populate the registry.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from dataclasses import dataclass
13
+ from typing import Callable
14
+
15
+
16
+ @dataclass
17
+ class CommandContext:
18
+ """Everything a command handler might need to act on."""
19
+
20
+ ui: "object" # UI; typed loosely to avoid an import cycle.
21
+ config: "object" # AppConfig
22
+ engine: "object" # ChatEngine
23
+
24
+
25
+ @dataclass
26
+ class CommandResult:
27
+ """Signals control flow back to the REPL."""
28
+
29
+ should_exit: bool = False
30
+ handled: bool = True
31
+
32
+
33
+ Handler = Callable[[CommandContext, str], CommandResult]
34
+
35
+ # name -> (handler, help text). Populated via the @command decorator below.
36
+ _REGISTRY: dict[str, tuple[Handler, str]] = {}
37
+ _ALIASES: dict[str, str] = {}
38
+
39
+
40
+ def command(
41
+ name: str, help_text: str, aliases: tuple[str, ...] = ()
42
+ ) -> Callable[[Handler], Handler]:
43
+ def wrap(func: Handler) -> Handler:
44
+ _REGISTRY[name] = (func, help_text)
45
+ for alias in aliases:
46
+ _ALIASES[alias] = name
47
+ return func
48
+
49
+ return wrap
50
+
51
+
52
+ def is_command(text: str) -> bool:
53
+ return text.strip().startswith("/")
54
+
55
+
56
+ def dispatch(ctx: CommandContext, text: str) -> CommandResult:
57
+ """Route ``text`` (a '/...' string) to its handler."""
58
+ parts = text.strip().split(maxsplit=1)
59
+ name = parts[0].lstrip("/").lower()
60
+ arg = parts[1] if len(parts) > 1 else ""
61
+ name = _ALIASES.get(name, name)
62
+
63
+ entry = _REGISTRY.get(name)
64
+ if entry is None:
65
+ ctx.ui.warning(f"Unknown command: /{name}. Type /help for the list.")
66
+ return CommandResult(handled=True)
67
+ return entry[0](ctx, arg)
68
+
69
+
70
+ # Import handler modules for their registration side effects. Deferred to the
71
+ # bottom so ``command`` / ``_REGISTRY`` already exist when the handlers load.
72
+ from . import about, agent, assist, clear, desktop, doctor, help, history, palette, provider, theme # noqa: E402,F401
73
+
74
+ __all__ = [
75
+ "CommandContext",
76
+ "CommandResult",
77
+ "command",
78
+ "dispatch",
79
+ "is_command",
80
+ "_REGISTRY",
81
+ ]
@@ -0,0 +1,34 @@
1
+ """Meta commands: /about, /exit."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from rich.text import Text
6
+
7
+ from .. import TAGLINE, __author__, __version__
8
+ from ..core.providers import provider_label
9
+ from . import CommandContext, CommandResult, command
10
+
11
+
12
+ def show_about(ui, config) -> None:
13
+ """Render the About panel (shared by /about and the main menu)."""
14
+ body = Text()
15
+ body.append("Seed Code\n", style="seed.primary")
16
+ body.append(f"{TAGLINE}\n\n", style="seed.accent")
17
+ body.append(f"Version {__version__}\n", style="seed.text")
18
+ body.append(f"Author {__author__}\n", style="seed.text")
19
+ body.append(f"Provider {provider_label(config.provider)}\n", style="seed.text")
20
+ body.append(f"Model {config.model or '(none)'}\n\n", style="seed.text")
21
+ body.append("A premium terminal-based AI coding assistant.", style="seed.dim")
22
+ ui.panel(body, title="About")
23
+
24
+
25
+ @command("about", "About Seed Code")
26
+ def _about(ctx: CommandContext, arg: str) -> CommandResult:
27
+ show_about(ctx.ui, ctx.config)
28
+ return CommandResult()
29
+
30
+
31
+ @command("exit", "Leave the chat (back to the main menu)", aliases=("quit", "q", "menu"))
32
+ def _exit(ctx: CommandContext, arg: str) -> CommandResult:
33
+ ctx.ui.dim("Back to the menu.")
34
+ return CommandResult(should_exit=True)
@@ -0,0 +1,94 @@
1
+ """Legacy commands: /agent and /permission.
2
+
3
+ The old Agent Mode was merged into Assist Mode — /agent now routes there.
4
+ /permission keeps its dedicated interactive picker; /index and /tools stay
5
+ as inspection commands.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from ..config import save_config
11
+ from ..tools import TOOL_REGISTRY, PermissionManager, PermissionMode
12
+ from ..tools.filesystem import build_index
13
+ from ..ui.selector import Option, select
14
+ from . import CommandContext, CommandResult, command
15
+ from .assist import disable_assist, enable_assist
16
+
17
+
18
+ @command("agent", "Legacy alias for Assist Mode. Usage: /agent [on|off]")
19
+ def _agent(ctx: CommandContext, arg: str) -> CommandResult:
20
+ raw = arg.strip().lower()
21
+ if raw in ("on", "off"):
22
+ enable = raw == "on"
23
+ elif not raw:
24
+ enable = not ctx.config.agent_mode # bare /agent toggles
25
+ else:
26
+ ctx.ui.warning("Usage: /agent [on|off]")
27
+ return CommandResult()
28
+
29
+ # Agent Mode was merged into Assist Mode — route there transparently.
30
+ ctx.ui.dim("(/agent is now Assist Mode)")
31
+ if enable:
32
+ enable_assist(ctx.ui, ctx.config)
33
+ else:
34
+ disable_assist(ctx.ui, ctx.config)
35
+ return CommandResult()
36
+
37
+
38
+ @command("permission", "Show or set the Assist permission mode", aliases=("perm",))
39
+ def _permission(ctx: CommandContext, arg: str) -> CommandResult:
40
+ raw = arg.strip()
41
+ detail = {
42
+ PermissionMode.READ_ONLY: "inspect only — no writes, no commands",
43
+ PermissionMode.WORKSPACE: "edit and run inside this directory only",
44
+ PermissionMode.DESKTOP: "control this computer (mouse, keyboard, apps)",
45
+ PermissionMode.FULL_SYSTEM: "no path restriction + sensitive actions (use with care)",
46
+ }
47
+ if not raw:
48
+ current = PermissionMode.parse(ctx.config.permission_mode)
49
+ chosen = select(
50
+ [
51
+ Option(mode.label, mode.value_str, detail=detail[mode])
52
+ for mode in PermissionMode
53
+ ],
54
+ title="Permission Level",
55
+ initial=current.value_str,
56
+ searchable=False,
57
+ hint="↑↓ move Enter select Esc keep current",
58
+ )
59
+ if chosen is None:
60
+ ctx.ui.dim(f"Permission unchanged ({current.label}).")
61
+ return CommandResult()
62
+ raw = str(chosen)
63
+
64
+ try:
65
+ mode = PermissionMode.parse(raw)
66
+ except ValueError as exc:
67
+ ctx.ui.warning(str(exc))
68
+ return CommandResult()
69
+ ctx.config.permission_mode = mode.value_str
70
+ save_config(ctx.config)
71
+ ctx.ui.success(f"Permission mode set to {mode.label}.")
72
+ return CommandResult()
73
+
74
+
75
+ @command("index", "Show a compact tree of the current project")
76
+ def _index(ctx: CommandContext, arg: str) -> CommandResult:
77
+ perm = PermissionManager(mode=PermissionMode.READ_ONLY)
78
+ ctx.ui.panel(build_index(perm), title="Project Index")
79
+ return CommandResult()
80
+
81
+
82
+ @command("tools", "List the tools available in Assist Mode")
83
+ def _tools(ctx: CommandContext, arg: str) -> CommandResult:
84
+ from ..ui.layout import columns_grid
85
+
86
+ rows = []
87
+ for name in sorted(TOOL_REGISTRY):
88
+ tool = TOOL_REGISTRY[name]
89
+ kind = "changes files/system" if tool.mutates else "read-only"
90
+ rows.append((name, f"{tool.description} ({kind})"))
91
+ ctx.ui.panel(
92
+ columns_grid(rows, ("seed.primary", "seed.text")), title="Assist Tools"
93
+ )
94
+ return CommandResult()
@@ -0,0 +1,201 @@
1
+ """Assist Mode: unified AI + computer control.
2
+
3
+ /assist on — enables the full capability set (AI, filesystem, terminal,
4
+ git, browser, keyboard, mouse, windows, vision, OCR, desktop
5
+ automation).
6
+ /assist off — back to plain chat.
7
+
8
+ Assist Mode is the ONLY automation mode Seed Code exposes. The old Agent
9
+ and Desktop modes were merged into it; their commands now route here.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ from rich.table import Table
15
+
16
+ from ..computer import is_available
17
+ from ..config import save_config
18
+ from ..tools import TOOL_REGISTRY, PermissionMode
19
+ from . import CommandContext, CommandResult, command
20
+
21
+ # The Assist capability set, in display order. Desktop-engine rows are
22
+ # marked so they can be dimmed when the Computer Engine is unavailable.
23
+ _CAPABILITIES: tuple[tuple[str, str, bool], ...] = (
24
+ ("AI", "Chat, reasoning, and code generation", False),
25
+ ("Filesystem", "Read, write, edit, search, and organise files", False),
26
+ ("Terminal", "Run shell commands with live output", False),
27
+ ("Git", "Status, diff, log, commit, push, pull", False),
28
+ ("Browser", "Navigate, click, type, search", True),
29
+ ("Keyboard", "Type, hotkeys, shortcuts", True),
30
+ ("Mouse", "Move, click, drag, scroll", True),
31
+ ("Windows", "List, focus, open, close", True),
32
+ ("Vision", "See and understand the screen", True),
33
+ ("OCR", "Read text from the screen", True),
34
+ ("Desktop Automation", "Multi-step computer control", True),
35
+ )
36
+
37
+
38
+ @command("assist", "Enable/disable Assist Mode (unified AI + computer control)")
39
+ def _assist(ctx: CommandContext, arg: str) -> CommandResult:
40
+ raw = arg.strip().lower()
41
+ if raw in ("on", "off"):
42
+ enable = raw == "on"
43
+ elif not raw:
44
+ # Bare /assist: show status
45
+ _show_status(ctx)
46
+ return CommandResult()
47
+ else:
48
+ ctx.ui.warning("Usage: /assist [on|off]")
49
+ return CommandResult()
50
+
51
+ if enable:
52
+ enable_assist(ctx.ui, ctx.config)
53
+ else:
54
+ disable_assist(ctx.ui, ctx.config)
55
+ return CommandResult()
56
+
57
+
58
+ def capability_table(desktop_ok: bool) -> Table:
59
+ """The Assist capability list (✓ rows; unavailable rows dim).
60
+
61
+ OCR is probed independently of the rest of the desktop stack: it needs a
62
+ native engine the Python packages do not supply, so marking it available
63
+ purely because the Computer Engine imports would tell the user something
64
+ untrue.
65
+ """
66
+ table = Table.grid(padding=(0, 2))
67
+ table.add_column(justify="right", no_wrap=True)
68
+ table.add_column()
69
+ ocr_ok = _ocr_available() if desktop_ok else False
70
+ for name, detail, needs_desktop in _CAPABILITIES:
71
+ available = desktop_ok if needs_desktop else True
72
+ if name == "OCR":
73
+ available = ocr_ok
74
+ if available:
75
+ table.add_row(f"[seed.success]✓ {name}[/seed.success]", f"[seed.text]{detail}[/seed.text]")
76
+ else:
77
+ table.add_row(f"[seed.dim]○ {name}[/seed.dim]", f"[seed.dim]{detail}[/seed.dim]")
78
+ return table
79
+
80
+
81
+ def _ocr_available() -> bool:
82
+ """Whether the OCR tier can genuinely run (best-effort)."""
83
+ try:
84
+ from ..computer import ocr
85
+
86
+ return ocr.available()
87
+ except Exception:
88
+ return False
89
+
90
+
91
+ def enable_assist(ui, config) -> None:
92
+ """Enable Assist Mode: the full capability set in one switch."""
93
+ config.agent_mode = True
94
+
95
+ # Desktop capabilities require the Computer Engine. When available, Assist
96
+ # runs at the ``desktop`` level so the AI can drive the computer; otherwise
97
+ # it stays at ``workspace`` (AI + filesystem + terminal + git still work).
98
+ desktop_ok, desktop_reason = is_available()
99
+ config.permission_mode = (
100
+ PermissionMode.DESKTOP.value_str if desktop_ok
101
+ else PermissionMode.WORKSPACE.value_str
102
+ )
103
+ save_config(config)
104
+
105
+ ui.success("Assist Mode ON")
106
+ ui.blank()
107
+ ui.panel(capability_table(desktop_ok), title="Assist Mode")
108
+ if not desktop_ok:
109
+ ui.dim(f"Desktop capabilities unavailable: {desktop_reason}")
110
+ ui.blank()
111
+
112
+ level_label = "desktop" if desktop_ok else "workspace"
113
+ ui.dim(f"Permission level: {level_label} — change in Settings › Advanced or /permission.")
114
+
115
+ # Ask for routine desktop control ONCE, here, instead of interrupting every
116
+ # action later. Sensitive actions still confirm individually.
117
+ if desktop_ok:
118
+ request_session_permissions(ui)
119
+ ui.dim("The AI picks the right tools for each task automatically.")
120
+
121
+
122
+ def request_session_permissions(ui) -> bool:
123
+ """Request every routine desktop permission for the session, in one prompt.
124
+
125
+ Returns whether the session-wide grant was given. Declining is safe: the
126
+ engine simply falls back to confirming each action as it comes, which is
127
+ the old behaviour. Sensitive actions (registry writes, secrets, system
128
+ power, deletions, purchases) are never granted here — they always ask.
129
+ """
130
+ from ..computer.permissions import (
131
+ CATEGORY_LABELS,
132
+ SESSION_GRANT_CATEGORIES,
133
+ session_permissions,
134
+ )
135
+
136
+ session = session_permissions()
137
+ if session.requested:
138
+ return bool(session.granted)
139
+
140
+ wanted = "\n".join(
141
+ f" • {CATEGORY_LABELS.get(c, c)}" for c in SESSION_GRANT_CATEGORIES
142
+ )
143
+ description = (
144
+ "Grant these for this session so Assist doesn't interrupt every step:\n"
145
+ f"{wanted}\n"
146
+ "Sensitive actions (registry writes, passwords, system power, deletions, "
147
+ "purchases) will still ask each time."
148
+ )
149
+
150
+ try:
151
+ answer = ui.confirm_desktop("Assist Mode session permissions", description)
152
+ except Exception:
153
+ # No interactive prompt available (headless/non-TTY): leave the
154
+ # per-action flow in place rather than silently granting anything.
155
+ return False
156
+
157
+ allowed = answer in ("y", "a")
158
+ if allowed:
159
+ session.request(SESSION_GRANT_CATEGORIES, allow=True)
160
+ ui.dim("Desktop control granted for this session — you won't be asked again.")
161
+ else:
162
+ ui.dim("Declined: Assist will ask before each desktop action.")
163
+ return allowed
164
+
165
+
166
+ def disable_assist(ui, config) -> None:
167
+ """Disable Assist Mode: back to plain chat."""
168
+ from ..computer.permissions import session_permissions
169
+
170
+ config.agent_mode = False
171
+ # Drop back to the safe editing level (removes desktop capability).
172
+ config.permission_mode = PermissionMode.WORKSPACE.value_str
173
+ save_config(config)
174
+ # Forget the session-wide desktop grant: turning Assist back on asks again.
175
+ session_permissions().reset()
176
+ ui.success("Assist Mode OFF — back to plain chat")
177
+
178
+
179
+ def _show_status(ctx: CommandContext) -> None:
180
+ """Show current Assist Mode status."""
181
+ on = ctx.config.agent_mode
182
+ desktop_ok, desktop_reason = is_available()
183
+
184
+ table = Table.grid(padding=(0, 3))
185
+ table.add_column(style="seed.dim", justify="right", no_wrap=True)
186
+ table.add_column()
187
+
188
+ state = "[seed.accent]ON[/seed.accent]" if on else "[seed.dim]OFF[/seed.dim]"
189
+ table.add_row("Assist Mode", state)
190
+ table.add_row("Mode", "Assist" if on else "Chat")
191
+ if not desktop_ok:
192
+ table.add_row("Desktop", f"Unavailable: {desktop_reason}")
193
+ table.add_row("Permission", ctx.config.permission_mode.replace("_", " ").title())
194
+
195
+ core_tools = [n for n, t in TOOL_REGISTRY.items() if t.group == "core"]
196
+ desktop_tools = [n for n, t in TOOL_REGISTRY.items() if t.group == "desktop"]
197
+ table.add_row("Available tools", f"{len(core_tools)} core, {len(desktop_tools)} desktop")
198
+
199
+ ctx.ui.panel(table, title="Assist Mode")
200
+ ctx.ui.blank()
201
+ ctx.ui.dim("Toggle with: /assist on | /assist off")
@@ -0,0 +1,20 @@
1
+ """Screen and context commands: /clear, /reset."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from . import CommandContext, CommandResult, command
6
+
7
+
8
+ @command("clear", "Clear the screen")
9
+ def _clear(ctx: CommandContext, arg: str) -> CommandResult:
10
+ # The branded banner renders exactly once at startup, so /clear only
11
+ # wipes the screen.
12
+ ctx.ui.console.clear()
13
+ return CommandResult()
14
+
15
+
16
+ @command("reset", "Forget the current conversation context")
17
+ def _reset(ctx: CommandContext, arg: str) -> CommandResult:
18
+ ctx.engine.reset() # type: ignore[attr-defined]
19
+ ctx.ui.success("Conversation context cleared.")
20
+ return CommandResult()
@@ -0,0 +1,104 @@
1
+ """Desktop-related commands: /desktop (legacy → Assist), /computer,
2
+ /screenshot, /windows.
3
+
4
+ The old Desktop Mode was merged into Assist Mode — /desktop now routes
5
+ there. /computer shows engine status; /screenshot and /windows work
6
+ immediately (no Assist turn needed) so the user can sanity-check the
7
+ engine by hand.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from rich.table import Table
13
+
14
+ from ..computer import INSTALL_HINT, is_available, missing_packages
15
+ from . import CommandContext, CommandResult, command
16
+ from .assist import disable_assist, enable_assist
17
+
18
+
19
+ @command("desktop", "Legacy alias for Assist Mode. Usage: /desktop [on|off]")
20
+ def _desktop(ctx: CommandContext, arg: str) -> CommandResult:
21
+ raw = arg.strip().lower()
22
+ if raw in ("on", "off"):
23
+ enable = raw == "on"
24
+ elif not raw:
25
+ enable = not ctx.config.agent_mode # bare /desktop toggles Assist
26
+ else:
27
+ ctx.ui.warning("Usage: /desktop [on|off]")
28
+ return CommandResult()
29
+
30
+ # Desktop Mode was merged into Assist Mode — route there transparently.
31
+ ctx.ui.dim("(/desktop is now Assist Mode)")
32
+ if enable:
33
+ enable_assist(ctx.ui, ctx.config)
34
+ else:
35
+ disable_assist(ctx.ui, ctx.config)
36
+ return CommandResult()
37
+
38
+
39
+ @command("computer", "Show Computer Engine status and desktop permissions")
40
+ def _computer(ctx: CommandContext, arg: str) -> CommandResult:
41
+ ok, reason = is_available()
42
+
43
+ table = Table.grid(padding=(0, 3))
44
+ table.add_column(style="seed.dim", justify="right", no_wrap=True)
45
+ table.add_column(style="seed.text")
46
+ table.add_row("Mode", "Assist" if ctx.config.agent_mode else "Chat (/assist on)")
47
+ table.add_row("Engine", reason if not ok else "Available")
48
+ missing = missing_packages()
49
+ if missing:
50
+ table.add_row("Missing", f"{', '.join(missing)} → {INSTALL_HINT}")
51
+
52
+ if ok:
53
+ try:
54
+ from ..tools.desktop import get_controller
55
+
56
+ table.add_row("Screen", get_controller().screen_info().split("\n")[0])
57
+ except Exception:
58
+ pass
59
+ from ..tools import TOOL_REGISTRY
60
+
61
+ names = sorted(n for n, t in TOOL_REGISTRY.items() if t.group == "desktop")
62
+ table.add_row("Tools", ", ".join(names))
63
+
64
+ ctx.ui.panel(table, title="Computer Engine")
65
+ return CommandResult()
66
+
67
+
68
+ @command("screenshot", "Capture a screenshot now. Usage: /screenshot [path]")
69
+ def _screenshot(ctx: CommandContext, arg: str) -> CommandResult:
70
+ ok, reason = is_available()
71
+ if not ok:
72
+ ctx.ui.error(reason)
73
+ return CommandResult()
74
+ from pathlib import Path
75
+
76
+ from ..tools.desktop import get_controller
77
+ from ..computer.controller import ComputerError
78
+
79
+ save_to = Path(arg.strip()).expanduser() if arg.strip() else None
80
+ try:
81
+ path = get_controller().screenshot(save_to=save_to)
82
+ except ComputerError as exc:
83
+ ctx.ui.error(str(exc))
84
+ return CommandResult()
85
+ ctx.ui.success(f"Screenshot saved: {path}")
86
+ return CommandResult()
87
+
88
+
89
+ @command("windows", "List all open windows")
90
+ def _windows(ctx: CommandContext, arg: str) -> CommandResult:
91
+ ok, reason = is_available()
92
+ if not ok:
93
+ ctx.ui.error(reason)
94
+ return CommandResult()
95
+ from ..tools.desktop import get_controller
96
+ from ..computer.controller import ComputerError
97
+
98
+ try:
99
+ listing = get_controller().list_windows()
100
+ except ComputerError as exc:
101
+ ctx.ui.error(str(exc))
102
+ return CommandResult()
103
+ ctx.ui.panel(listing, title="Open Windows")
104
+ return CommandResult()