seedcode-cli 6.2.0__py3-none-any.whl → 6.2.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.
- seedcode/__init__.py +1 -1
- seedcode/_default_key_template.py +5 -1
- seedcode/app.py +129 -17
- seedcode/branding.py +6 -1
- seedcode/cli.py +11 -3
- seedcode/commands/__init__.py +4 -2
- seedcode/commands/agent.py +7 -2
- seedcode/commands/assist.py +2 -1
- seedcode/commands/chat.py +46 -0
- seedcode/commands/codemode.py +3 -2
- seedcode/commands/mode.py +50 -0
- seedcode/commands/provider.py +52 -16
- seedcode/commands/status.py +95 -0
- seedcode/config/__init__.py +20 -2
- seedcode/config/defaults.py +29 -2
- seedcode/config/manager.py +177 -21
- seedcode/core/agent.py +30 -2
- seedcode/core/chat.py +33 -3
- seedcode/core/http.py +114 -5
- seedcode/core/models.py +46 -14
- seedcode/core/providers/__init__.py +34 -7
- seedcode/core/providers/base.py +24 -2
- seedcode/core/providers/default.py +157 -0
- seedcode/core/providers/ollama.py +9 -1
- seedcode/core/providers/openrouter.py +80 -29
- seedcode/default_api.py +54 -3
- seedcode/defaults.py +37 -0
- seedcode/tools/terminal.py +30 -5
- seedcode/ui/__init__.py +16 -3
- seedcode/ui/dashboard.py +98 -291
- seedcode/ui/menu.py +4 -0
- seedcode/ui/reference.py +46 -0
- seedcode/ui/selector.py +24 -0
- seedcode/utils/terminal_env.py +225 -0
- seedcode_cli-6.2.5.dist-info/METADATA +457 -0
- {seedcode_cli-6.2.0.dist-info → seedcode_cli-6.2.5.dist-info}/RECORD +39 -34
- {seedcode_cli-6.2.0.dist-info → seedcode_cli-6.2.5.dist-info}/WHEEL +1 -1
- seedcode/ui/banner.py +0 -78
- seedcode/ui/logo.py +0 -134
- seedcode_cli-6.2.0.dist-info/METADATA +0 -401
- {seedcode_cli-6.2.0.dist-info → seedcode_cli-6.2.5.dist-info}/entry_points.txt +0 -0
- {seedcode_cli-6.2.0.dist-info → seedcode_cli-6.2.5.dist-info}/licenses/LICENSE +0 -0
seedcode/__init__.py
CHANGED
|
@@ -6,7 +6,7 @@ Plant ideas. Grow code.
|
|
|
6
6
|
# Single source of truth for the application version. Every consumer reads it:
|
|
7
7
|
# the PyPI build (via [tool.hatch.version]), the CLI --version flag, the
|
|
8
8
|
# Windows installer metadata, the GitHub release tag, and the WinGet manifest.
|
|
9
|
-
__version__ = "6.2.
|
|
9
|
+
__version__ = "6.2.5"
|
|
10
10
|
__author__ = "Al shahriar sowan"
|
|
11
11
|
__publisher__ = "Eagox Studio"
|
|
12
12
|
|
|
@@ -1,10 +1,14 @@
|
|
|
1
|
-
"""Embedded
|
|
1
|
+
"""Embedded built-in credential for the Default provider (v6.2.5 artifact).
|
|
2
2
|
|
|
3
3
|
This file is NOT committed to source control. The release build generates
|
|
4
4
|
``seedcode/_default_key.py`` from the local ``.env`` (see
|
|
5
5
|
``scripts/windows/embed_default_key.py``). Source checkouts without that
|
|
6
6
|
generated file fall back to the empty default below, so behavior is
|
|
7
7
|
unchanged for developers and CI.
|
|
8
|
+
|
|
9
|
+
The credential belongs to the built-in ``Default`` provider and is resolved
|
|
10
|
+
per request by :mod:`seedcode.core.providers.default`; it is never copied
|
|
11
|
+
into another provider's stored configuration.
|
|
8
12
|
"""
|
|
9
13
|
|
|
10
14
|
DEFAULT_OPENROUTER_API_KEY = ""
|
seedcode/app.py
CHANGED
|
@@ -14,9 +14,12 @@ Ctrl+L clear screen.
|
|
|
14
14
|
|
|
15
15
|
from __future__ import annotations
|
|
16
16
|
|
|
17
|
+
import sys
|
|
18
|
+
|
|
17
19
|
from prompt_toolkit import PromptSession
|
|
18
20
|
from prompt_toolkit.key_binding import KeyBindings
|
|
19
21
|
|
|
22
|
+
from . import __version__
|
|
20
23
|
from .commands import CommandContext, dispatch, is_command
|
|
21
24
|
from .commands.about import show_about
|
|
22
25
|
from .commands.help import show_shortcuts
|
|
@@ -29,13 +32,14 @@ from .core.agent import AgentEngine, strip_tool_blocks
|
|
|
29
32
|
from .core.chat import ChatEngine, ChatError
|
|
30
33
|
from .core.lifecycle import lifecycle
|
|
31
34
|
from .core.models import AppConfig
|
|
32
|
-
from .core.providers import PROVIDERS, provider_label
|
|
35
|
+
from .core.providers import PROVIDERS, provider_label, provider_ready
|
|
33
36
|
from .core.providers.freemodel import AUTO_MODEL
|
|
34
37
|
from .memory import HistoryStore
|
|
35
38
|
from .tools import PermissionManager, PermissionMode
|
|
36
39
|
from .ui import UI
|
|
37
40
|
from .ui.badges import badge_for_status
|
|
38
41
|
from .ui.menu import MenuItem, run_menu
|
|
42
|
+
from .ui.reference import CONTROLS_HINT, INPUT_HINT
|
|
39
43
|
from .ui.textbox import prompt_label
|
|
40
44
|
from .ui.theme import pt_style, set_active_theme
|
|
41
45
|
from .utils.logger import get_logger
|
|
@@ -102,9 +106,14 @@ def _release_desktop_resources() -> None:
|
|
|
102
106
|
|
|
103
107
|
|
|
104
108
|
def _provider_status(config: AppConfig) -> str:
|
|
105
|
-
"""Menu status line: the active provider, or 'Not Configured'.
|
|
106
|
-
|
|
107
|
-
|
|
109
|
+
"""Menu status line: the active provider, or 'Not Configured'.
|
|
110
|
+
|
|
111
|
+
Uses the shared provider-ready rule, so the menu never disagrees with the
|
|
112
|
+
dashboard: Default and Ollama need no key, everyone else needs their own.
|
|
113
|
+
"""
|
|
114
|
+
if provider_ready(config.provider, config):
|
|
115
|
+
return provider_label(config.provider)
|
|
116
|
+
return "Not Configured"
|
|
108
117
|
|
|
109
118
|
|
|
110
119
|
def _model_status(config: AppConfig) -> str:
|
|
@@ -124,27 +133,75 @@ def _key_status(config: AppConfig) -> str:
|
|
|
124
133
|
return config.masked_key()
|
|
125
134
|
|
|
126
135
|
|
|
136
|
+
def _mode_status(config: AppConfig) -> str:
|
|
137
|
+
"""Menu status for the Code Mode item (ON only when really active)."""
|
|
138
|
+
try:
|
|
139
|
+
from .codemode_state import codemode_state
|
|
140
|
+
|
|
141
|
+
if codemode_state().enabled:
|
|
142
|
+
return "ON"
|
|
143
|
+
except Exception:
|
|
144
|
+
pass
|
|
145
|
+
return "ON" if config.agent_mode else "OFF"
|
|
146
|
+
|
|
147
|
+
|
|
127
148
|
def _main_menu(config: AppConfig):
|
|
128
|
-
"""The interactive main menu; returns an action id or None (exit).
|
|
149
|
+
"""The interactive main menu; returns an action id or None (exit).
|
|
150
|
+
|
|
151
|
+
v6.2.5 reference layout: the six mode/setup actions carry Ctrl+1..Ctrl+6
|
|
152
|
+
shortcuts that execute the real handlers (no decorative items), followed
|
|
153
|
+
by the chat/setup actions kept from earlier releases.
|
|
154
|
+
"""
|
|
129
155
|
provider = PROVIDERS.get(config.provider)
|
|
130
156
|
badge = badge_for_status(provider.status if provider is not None else "")
|
|
131
157
|
return run_menu(
|
|
132
158
|
[
|
|
159
|
+
MenuItem("Code Mode", "codemode", status=_mode_status(config), shortcut="1"),
|
|
160
|
+
MenuItem("Agent Mode", "agent", shortcut="2"),
|
|
161
|
+
MenuItem("Assist Mode", "assist", shortcut="3"),
|
|
162
|
+
MenuItem("Project Memory", "memory", shortcut="4"),
|
|
163
|
+
MenuItem("Settings", "settings", shortcut="5"),
|
|
164
|
+
MenuItem("Exit", "exit", shortcut="6"),
|
|
133
165
|
MenuItem("Start Chat", "chat", status=_model_status(config), badge=badge),
|
|
134
166
|
MenuItem("Provider", "provider", status=_provider_status(config)),
|
|
135
167
|
MenuItem("API Key", "apikey", status=_key_status(config)),
|
|
136
168
|
MenuItem("Model", "model", status=_model_status(config)),
|
|
137
|
-
MenuItem("Settings", "settings"),
|
|
138
169
|
MenuItem("Theme", "theme", status=config.theme),
|
|
139
170
|
MenuItem("About", "about"),
|
|
140
|
-
MenuItem("Exit", "exit"),
|
|
141
171
|
],
|
|
142
|
-
title="Seed Code",
|
|
143
|
-
hint=
|
|
172
|
+
title=f"Seed Code v{__version__}",
|
|
173
|
+
hint=CONTROLS_HINT,
|
|
144
174
|
initial="chat",
|
|
145
175
|
)
|
|
146
176
|
|
|
147
177
|
|
|
178
|
+
def _dispatch_command(ui: UI, config: AppConfig, engine, command: str) -> None:
|
|
179
|
+
"""Run a slash command from a menu action through the real router."""
|
|
180
|
+
dispatch(CommandContext(ui=ui, config=config, engine=engine), command)
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def _report_command_error(ui: UI, exc: Exception) -> None:
|
|
184
|
+
"""Render a failed command/action without leaking a raw traceback.
|
|
185
|
+
|
|
186
|
+
A transport failure becomes one actionable ``[Network Error]`` line, mapped
|
|
187
|
+
by :mod:`seedcode.core.http` (which never includes request headers, so an
|
|
188
|
+
API key can never surface here). Anything else keeps its own message; the
|
|
189
|
+
full traceback is already written to the log file.
|
|
190
|
+
"""
|
|
191
|
+
try:
|
|
192
|
+
import httpx
|
|
193
|
+
|
|
194
|
+
from .core.http import friendly_error
|
|
195
|
+
|
|
196
|
+
if isinstance(exc, httpx.HTTPError):
|
|
197
|
+
ui.error(f"[Network Error] {friendly_error(exc)}")
|
|
198
|
+
ui.dim("Action: check your network connection or provider configuration.")
|
|
199
|
+
return
|
|
200
|
+
except Exception:
|
|
201
|
+
pass # error reporting must never itself raise
|
|
202
|
+
ui.error(f"[Command Error] {exc}")
|
|
203
|
+
|
|
204
|
+
|
|
148
205
|
def _guided_setup(ui: UI, config: AppConfig) -> bool:
|
|
149
206
|
"""Provider -> API key -> validate -> fetch models -> select -> save.
|
|
150
207
|
|
|
@@ -343,14 +400,11 @@ def _chat_loop(
|
|
|
343
400
|
config: AppConfig,
|
|
344
401
|
engine: ChatEngine,
|
|
345
402
|
history: HistoryStore,
|
|
346
|
-
session: PromptSession,
|
|
403
|
+
session: PromptSession | _PlainSession,
|
|
347
404
|
) -> None:
|
|
348
405
|
"""Interactive chat until /exit (returns to the main menu)."""
|
|
349
406
|
ctx = CommandContext(ui=ui, config=config, engine=engine)
|
|
350
|
-
ui.dim(
|
|
351
|
-
"Type /help for commands, /exit for the menu — "
|
|
352
|
-
"Ctrl+K palette, Ctrl+P files, Ctrl+/ shortcuts."
|
|
353
|
-
)
|
|
407
|
+
ui.dim(INPUT_HINT)
|
|
354
408
|
|
|
355
409
|
# The Assist engine is built lazily on the first assist-mode turn and
|
|
356
410
|
# rebuilt when the permission or desktop mode changes (its system
|
|
@@ -398,7 +452,7 @@ def _chat_loop(
|
|
|
398
452
|
continue
|
|
399
453
|
except Exception as exc: # a broken command must not kill the REPL
|
|
400
454
|
_log.exception("command failed: %s", text.split()[0])
|
|
401
|
-
ui
|
|
455
|
+
_report_command_error(ui, exc)
|
|
402
456
|
continue
|
|
403
457
|
if result.should_exit:
|
|
404
458
|
return
|
|
@@ -435,6 +489,56 @@ def _chat_loop(
|
|
|
435
489
|
_handle_chat(ui, engine, history, text)
|
|
436
490
|
|
|
437
491
|
|
|
492
|
+
def _interactive() -> bool:
|
|
493
|
+
"""True when a real console is attached (mirrors ``ui.selector._interactive``).
|
|
494
|
+
|
|
495
|
+
prompt_toolkit needs a console/PTY it can drive. Piped, redirected, or
|
|
496
|
+
console-less hosts (CI, ``seedcode < file``, some Git Bash/MSYS sessions)
|
|
497
|
+
cannot provide one, and constructing a prompt there raises instead of
|
|
498
|
+
degrading — so every interactive entry point checks this first.
|
|
499
|
+
"""
|
|
500
|
+
try:
|
|
501
|
+
return sys.stdin.isatty() and sys.stdout.isatty()
|
|
502
|
+
except (AttributeError, ValueError):
|
|
503
|
+
return False
|
|
504
|
+
|
|
505
|
+
|
|
506
|
+
class _PlainSession:
|
|
507
|
+
"""Line-based stand-in for :class:`PromptSession` on non-console hosts.
|
|
508
|
+
|
|
509
|
+
The chat loop only needs ``prompt()``, so this shim keeps Seed Code usable
|
|
510
|
+
where prompt_toolkit cannot start. Reading raises ``EOFError`` /
|
|
511
|
+
``KeyboardInterrupt`` exactly like the real session, so the loop's existing
|
|
512
|
+
Ctrl+C / Ctrl+D handling is unchanged.
|
|
513
|
+
"""
|
|
514
|
+
|
|
515
|
+
def prompt(self, message=None, **_kwargs) -> str:
|
|
516
|
+
return input(_plain_text(message) or "> ")
|
|
517
|
+
|
|
518
|
+
|
|
519
|
+
def _plain_text(message) -> str:
|
|
520
|
+
"""Literal text of a prompt message (a prompt_toolkit ``FormattedText``)."""
|
|
521
|
+
if isinstance(message, str):
|
|
522
|
+
return message
|
|
523
|
+
try:
|
|
524
|
+
return "".join(fragment[1] for fragment in message)
|
|
525
|
+
except Exception:
|
|
526
|
+
return ""
|
|
527
|
+
|
|
528
|
+
|
|
529
|
+
def _make_chat_session(ui: UI):
|
|
530
|
+
"""A prompt_toolkit session when a console exists, else the plain shim."""
|
|
531
|
+
if not _interactive():
|
|
532
|
+
return _PlainSession()
|
|
533
|
+
try:
|
|
534
|
+
return _build_chat_session(ui)
|
|
535
|
+
except Exception:
|
|
536
|
+
# A console that exists but still cannot be driven (e.g. a MSYS/mintty
|
|
537
|
+
# TERM against a non-Windows console). Never fatal — degrade instead.
|
|
538
|
+
_log.warning("prompt_toolkit unavailable; using line-based input")
|
|
539
|
+
return _PlainSession()
|
|
540
|
+
|
|
541
|
+
|
|
438
542
|
def _build_chat_session(ui: UI) -> PromptSession:
|
|
439
543
|
"""The chat PromptSession with the global shortcut bindings attached."""
|
|
440
544
|
kb = KeyBindings()
|
|
@@ -480,7 +584,7 @@ def run(ui: UI) -> None:
|
|
|
480
584
|
active_backend = config.provider
|
|
481
585
|
engine = ChatEngine(config)
|
|
482
586
|
history = HistoryStore(provider_id=active_backend)
|
|
483
|
-
chat_session =
|
|
587
|
+
chat_session = _make_chat_session(ui)
|
|
484
588
|
|
|
485
589
|
# Best-effort teardown for the one legitimate shutdown path.
|
|
486
590
|
lifecycle().on_shutdown(_release_desktop_resources)
|
|
@@ -530,6 +634,14 @@ def run(ui: UI) -> None:
|
|
|
530
634
|
apikey_menu(ui, config)
|
|
531
635
|
elif choice == "model":
|
|
532
636
|
select_model(ui, config)
|
|
637
|
+
elif choice == "codemode":
|
|
638
|
+
_dispatch_command(ui, config, engine, "/codemode on")
|
|
639
|
+
elif choice == "agent":
|
|
640
|
+
_dispatch_command(ui, config, engine, "/agent on")
|
|
641
|
+
elif choice == "assist":
|
|
642
|
+
_dispatch_command(ui, config, engine, "/assist on")
|
|
643
|
+
elif choice == "memory":
|
|
644
|
+
_dispatch_command(ui, config, engine, "/codemode status")
|
|
533
645
|
elif choice == "settings":
|
|
534
646
|
settings_menu(ui, config)
|
|
535
647
|
elif choice == "theme":
|
|
@@ -543,4 +655,4 @@ def run(ui: UI) -> None:
|
|
|
543
655
|
ui.dim("Cancelled.")
|
|
544
656
|
except Exception as exc: # menu actions must never crash the app
|
|
545
657
|
_log.exception("menu action failed: %s", choice)
|
|
546
|
-
ui
|
|
658
|
+
_report_command_error(ui, exc)
|
seedcode/branding.py
CHANGED
|
@@ -4,9 +4,14 @@ The official Seed Code mark is the 16x16 pixel-art "sprouting seed" on a
|
|
|
4
4
|
dark rounded tile (the Seed Green identity). This module owns the grid and
|
|
5
5
|
the exact palette so every surface renders identical art:
|
|
6
6
|
|
|
7
|
-
* ``seedcode/ui/logo.py`` — terminal rendering (half-block pixels)
|
|
8
7
|
* ``scripts/windows/build_assets.py`` — ``seedcode.ico``, the Inno wizard
|
|
9
8
|
bitmaps, the canonical ``assets/logo.png`` and the preview render
|
|
9
|
+
* the installer wizard and the Windows taskbar/Explorer surfaces
|
|
10
|
+
|
|
11
|
+
v6.2.5 note: the interactive CLI no longer renders any ASCII art. The mark
|
|
12
|
+
is a packaging/desktop asset only (:mod:`seedcode.ui.dashboard` renders a
|
|
13
|
+
borderless text header with no logo), so this module is not imported by the
|
|
14
|
+
terminal UI at all.
|
|
10
15
|
|
|
11
16
|
Stdlib only (struct/zlib): safe to import from anywhere in the package,
|
|
12
17
|
costs nothing at startup, and keeps the brand byte-reproducible.
|
seedcode/cli.py
CHANGED
|
@@ -51,7 +51,8 @@ def main() -> None:
|
|
|
51
51
|
if len(sys.argv) > 1 and sys.argv[1] in ("--version", "-V"):
|
|
52
52
|
from . import __version__
|
|
53
53
|
|
|
54
|
-
|
|
54
|
+
# Installers verify the install by parsing this exact line.
|
|
55
|
+
print(f"Seed Code CLI {__version__}")
|
|
55
56
|
return
|
|
56
57
|
if len(sys.argv) > 1 and sys.argv[1] in ("--help", "-h"):
|
|
57
58
|
print(_HELP)
|
|
@@ -65,18 +66,25 @@ def main() -> None:
|
|
|
65
66
|
log = get_logger("cli")
|
|
66
67
|
|
|
67
68
|
from . import __version__
|
|
69
|
+
from .utils.terminal_env import detect_terminal, workspace_root
|
|
68
70
|
|
|
71
|
+
# Detect the terminal host/shell once (VS Code, Windows Terminal, Git Bash
|
|
72
|
+
# or unknown) so a support log says exactly where this session ran. The
|
|
73
|
+
# workspace root is the launch directory and is never changed below.
|
|
74
|
+
terminal = detect_terminal()
|
|
69
75
|
log.info(
|
|
70
|
-
"Seed Code v%s starting (python %s on %s)",
|
|
76
|
+
"Seed Code v%s starting (python %s on %s); terminal %s; cwd=%s",
|
|
71
77
|
__version__,
|
|
72
78
|
sys.version.split()[0],
|
|
73
79
|
sys.platform,
|
|
80
|
+
terminal.describe(),
|
|
81
|
+
workspace_root(),
|
|
74
82
|
)
|
|
75
83
|
|
|
76
84
|
from .app import run
|
|
77
85
|
from .ui import UI
|
|
78
86
|
|
|
79
|
-
ui = UI()
|
|
87
|
+
ui = UI(plain=terminal.plain)
|
|
80
88
|
try:
|
|
81
89
|
run(ui)
|
|
82
90
|
log.info("Clean exit.")
|
seedcode/commands/__init__.py
CHANGED
|
@@ -62,14 +62,16 @@ def dispatch(ctx: CommandContext, text: str) -> CommandResult:
|
|
|
62
62
|
|
|
63
63
|
entry = _REGISTRY.get(name)
|
|
64
64
|
if entry is None:
|
|
65
|
-
|
|
65
|
+
# Never silently ignore a command: name it, then point at /help.
|
|
66
|
+
ctx.ui.warning(f"[Command Error] Unknown command: /{name}")
|
|
67
|
+
ctx.ui.dim("Run /help for available commands.")
|
|
66
68
|
return CommandResult(handled=True)
|
|
67
69
|
return entry[0](ctx, arg)
|
|
68
70
|
|
|
69
71
|
|
|
70
72
|
# Import handler modules for their registration side effects. Deferred to the
|
|
71
73
|
# bottom so ``command`` / ``_REGISTRY`` already exist when the handlers load.
|
|
72
|
-
from . import about, agent, assist, clear, codemode, desktop, doctor, help, history, palette, provider, theme # noqa: E402,F401
|
|
74
|
+
from . import about, agent, assist, chat, clear, codemode, desktop, doctor, help, history, mode, palette, provider, status, theme # noqa: E402,F401
|
|
73
75
|
|
|
74
76
|
__all__ = [
|
|
75
77
|
"CommandContext",
|
seedcode/commands/agent.py
CHANGED
|
@@ -23,7 +23,8 @@ def _agent(ctx: CommandContext, arg: str) -> CommandResult:
|
|
|
23
23
|
elif not raw:
|
|
24
24
|
enable = not ctx.config.agent_mode # bare /agent toggles
|
|
25
25
|
else:
|
|
26
|
-
ctx.ui.warning("
|
|
26
|
+
ctx.ui.warning("[Command Error] Invalid syntax.")
|
|
27
|
+
ctx.ui.dim("Expected: /agent on|off")
|
|
27
28
|
return CommandResult()
|
|
28
29
|
|
|
29
30
|
# Agent Mode was merged into Assist Mode — route there transparently.
|
|
@@ -35,7 +36,11 @@ def _agent(ctx: CommandContext, arg: str) -> CommandResult:
|
|
|
35
36
|
return CommandResult()
|
|
36
37
|
|
|
37
38
|
|
|
38
|
-
@command(
|
|
39
|
+
@command(
|
|
40
|
+
"permission",
|
|
41
|
+
"Show or set the Assist permission mode",
|
|
42
|
+
aliases=("perm", "permissions"),
|
|
43
|
+
)
|
|
39
44
|
def _permission(ctx: CommandContext, arg: str) -> CommandResult:
|
|
40
45
|
raw = arg.strip()
|
|
41
46
|
detail = {
|
seedcode/commands/assist.py
CHANGED
|
@@ -45,7 +45,8 @@ def _assist(ctx: CommandContext, arg: str) -> CommandResult:
|
|
|
45
45
|
_show_status(ctx)
|
|
46
46
|
return CommandResult()
|
|
47
47
|
else:
|
|
48
|
-
ctx.ui.warning("
|
|
48
|
+
ctx.ui.warning("[Command Error] Invalid syntax.")
|
|
49
|
+
ctx.ui.dim("Expected: /assist on|off")
|
|
49
50
|
return CommandResult()
|
|
50
51
|
|
|
51
52
|
if enable:
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"""/chat — explicit Chat Mode (plain conversation, no tools).
|
|
2
|
+
|
|
3
|
+
/chat — show the current mode.
|
|
4
|
+
/chat on — leave Assist/Code Mode and return to plain Chat: the AI answers
|
|
5
|
+
with text only and never touches the filesystem or terminal.
|
|
6
|
+
|
|
7
|
+
Chat Mode is the default runtime mode; this command exists so a user can
|
|
8
|
+
always get back to it directly, without hunting through menus.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
from . import CommandContext, CommandResult, command
|
|
14
|
+
from .status import mode_label
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@command("chat", "Switch to plain Chat Mode. Usage: /chat [on]")
|
|
18
|
+
def _chat(ctx: CommandContext, arg: str) -> CommandResult:
|
|
19
|
+
raw = arg.strip().lower()
|
|
20
|
+
if raw in ("", "status"):
|
|
21
|
+
ctx.ui.info(f"Mode: {mode_label(ctx.config)}")
|
|
22
|
+
ctx.ui.dim("Switch with: /chat on · /assist on · /codemode on (or /mode chat|assist|code)")
|
|
23
|
+
return CommandResult()
|
|
24
|
+
|
|
25
|
+
if raw != "on":
|
|
26
|
+
ctx.ui.warning("[Command Error] Invalid syntax.")
|
|
27
|
+
ctx.ui.dim("Expected: /chat [on]")
|
|
28
|
+
return CommandResult()
|
|
29
|
+
|
|
30
|
+
_enter_chat(ctx)
|
|
31
|
+
return CommandResult()
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _enter_chat(ctx: CommandContext) -> None:
|
|
35
|
+
"""Return the session to plain Chat Mode (idempotent)."""
|
|
36
|
+
from .. import codemode_state as cms
|
|
37
|
+
from .assist import disable_assist
|
|
38
|
+
|
|
39
|
+
if cms.codemode_state().enabled:
|
|
40
|
+
cms.disable()
|
|
41
|
+
ctx.ui.dim("Code Mode OFF — .seedcode memory kept for next session")
|
|
42
|
+
|
|
43
|
+
was_assist = ctx.config.agent_mode
|
|
44
|
+
if was_assist:
|
|
45
|
+
disable_assist(ctx.ui, ctx.config)
|
|
46
|
+
ctx.ui.success("Chat Mode ON — plain conversation, no tools")
|
seedcode/commands/codemode.py
CHANGED
|
@@ -11,7 +11,7 @@ from __future__ import annotations
|
|
|
11
11
|
|
|
12
12
|
from rich.table import Table
|
|
13
13
|
|
|
14
|
-
from .. import codemode_state
|
|
14
|
+
from ..codemode_state import codemode_state
|
|
15
15
|
from ..config import save_config
|
|
16
16
|
from ..tools import PermissionMode
|
|
17
17
|
from . import CommandContext, CommandResult, command
|
|
@@ -45,7 +45,8 @@ def _codemode(ctx: CommandContext, arg: str) -> CommandResult:
|
|
|
45
45
|
_disable_codemode(ctx)
|
|
46
46
|
return CommandResult()
|
|
47
47
|
|
|
48
|
-
ctx.ui.warning("
|
|
48
|
+
ctx.ui.warning("[Command Error] Invalid syntax.")
|
|
49
|
+
ctx.ui.dim("Expected: /codemode on|off|status")
|
|
49
50
|
return CommandResult()
|
|
50
51
|
|
|
51
52
|
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"""/mode — one generic switcher for the runtime modes.
|
|
2
|
+
|
|
3
|
+
/mode — show the current mode.
|
|
4
|
+
/mode chat — plain conversation (no tools).
|
|
5
|
+
/mode assist — the unified AI + tools mode (filesystem, terminal, git, ...).
|
|
6
|
+
/mode code — Assist sharpened for the current workspace (Code Mode).
|
|
7
|
+
/mode agent — legacy alias of assist.
|
|
8
|
+
|
|
9
|
+
The individual commands (/chat, /assist, /codemode) remain the direct routes;
|
|
10
|
+
this command is the single entry point that understands all of them.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
from . import CommandContext, CommandResult, command
|
|
16
|
+
from .status import mode_label
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@command("mode", "Show or switch the runtime mode. Usage: /mode [chat|assist|code|agent]")
|
|
20
|
+
def _mode(ctx: CommandContext, arg: str) -> CommandResult:
|
|
21
|
+
raw = arg.strip().lower()
|
|
22
|
+
|
|
23
|
+
if not raw:
|
|
24
|
+
ctx.ui.info(f"Mode: {mode_label(ctx.config)}")
|
|
25
|
+
ctx.ui.dim("Switch with: /mode chat|assist|code|agent")
|
|
26
|
+
return CommandResult()
|
|
27
|
+
|
|
28
|
+
if raw == "chat":
|
|
29
|
+
from .chat import _enter_chat
|
|
30
|
+
|
|
31
|
+
_enter_chat(ctx)
|
|
32
|
+
elif raw == "assist":
|
|
33
|
+
from .assist import enable_assist
|
|
34
|
+
|
|
35
|
+
enable_assist(ctx.ui, ctx.config)
|
|
36
|
+
elif raw == "code":
|
|
37
|
+
from .codemode import _enable_codemode
|
|
38
|
+
|
|
39
|
+
_enable_codemode(ctx)
|
|
40
|
+
elif raw == "agent":
|
|
41
|
+
# Agent Mode merged into Assist Mode; keep the name working.
|
|
42
|
+
from .assist import enable_assist
|
|
43
|
+
|
|
44
|
+
enable_assist(ctx.ui, ctx.config)
|
|
45
|
+
ctx.ui.dim("(Agent Mode is Assist Mode now — same engine, same tools)")
|
|
46
|
+
else:
|
|
47
|
+
ctx.ui.warning("[Command Error] Invalid syntax.")
|
|
48
|
+
ctx.ui.dim("Expected: /mode chat|assist|code|agent")
|
|
49
|
+
|
|
50
|
+
return CommandResult()
|
seedcode/commands/provider.py
CHANGED
|
@@ -46,14 +46,45 @@ def _resolve_provider(text: str) -> Provider | None:
|
|
|
46
46
|
return matches[0] if len(matches) == 1 else None
|
|
47
47
|
|
|
48
48
|
|
|
49
|
+
# Selector groups, in display order. Providers are grouped by what they need
|
|
50
|
+
# from the user, so "Default" is never mistaken for "OpenRouter" (they can
|
|
51
|
+
# share an underlying service but are separate choices with separate config).
|
|
52
|
+
_GROUP_BUILTIN = "Built-in · no API key needed"
|
|
53
|
+
_GROUP_BYOK = "Your own API key"
|
|
54
|
+
_GROUP_LOCAL = "Local · runs on this machine"
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _provider_group(provider: Provider) -> str:
|
|
58
|
+
if provider.local:
|
|
59
|
+
return _GROUP_LOCAL
|
|
60
|
+
if provider.id == "default":
|
|
61
|
+
return _GROUP_BUILTIN
|
|
62
|
+
return _GROUP_BYOK
|
|
63
|
+
|
|
64
|
+
|
|
49
65
|
def _provider_backend(provider: Provider, config) -> str:
|
|
50
|
-
|
|
66
|
+
"""Human connection type: 'Local' for on-machine backends, else the API family."""
|
|
67
|
+
if provider.local:
|
|
51
68
|
return "Local"
|
|
52
69
|
return provider.backend_label or f"{provider.label} API"
|
|
53
70
|
|
|
54
71
|
|
|
72
|
+
def _provider_key_column(provider: Provider, config) -> str:
|
|
73
|
+
"""What this provider needs from the user: no key, a masked key, or a prompt.
|
|
74
|
+
|
|
75
|
+
This is the column that makes the API-key rules visible before switching:
|
|
76
|
+
Default and Ollama read "no API key", configured providers show their
|
|
77
|
+
masked key, and the rest ask for one. A key is never shown in full.
|
|
78
|
+
"""
|
|
79
|
+
if not provider.requires_key:
|
|
80
|
+
return "no API key"
|
|
81
|
+
if config.get_api_key(provider.id).strip():
|
|
82
|
+
return config.masked_key(provider.id)
|
|
83
|
+
return "API key needed"
|
|
84
|
+
|
|
85
|
+
|
|
55
86
|
def _provider_menu(ui, config) -> Provider | None:
|
|
56
|
-
"""Interactive provider selector: badge, backend, and
|
|
87
|
+
"""Interactive provider selector: badge, backend, model, and key state."""
|
|
57
88
|
options = []
|
|
58
89
|
for p in PROVIDERS.values():
|
|
59
90
|
entry = config.providers.get(p.id)
|
|
@@ -65,7 +96,12 @@ def _provider_menu(ui, config) -> Provider | None:
|
|
|
65
96
|
p.label,
|
|
66
97
|
value=p.id,
|
|
67
98
|
badge=badge_for_status(p.status),
|
|
68
|
-
columns=(
|
|
99
|
+
columns=(
|
|
100
|
+
_provider_backend(p, config),
|
|
101
|
+
model,
|
|
102
|
+
_provider_key_column(p, config),
|
|
103
|
+
),
|
|
104
|
+
group=_provider_group(p),
|
|
69
105
|
)
|
|
70
106
|
)
|
|
71
107
|
chosen = select(
|
|
@@ -113,22 +149,22 @@ def _collect_key(ui, config, provider: Provider, *, replacing: bool = False) ->
|
|
|
113
149
|
|
|
114
150
|
|
|
115
151
|
def _ensure_ready(ui, config, provider: Provider) -> bool:
|
|
116
|
-
"""Make ``provider`` usable: collect+validate a key, or
|
|
152
|
+
"""Make ``provider`` usable: collect+validate a key, or probe a key-less one.
|
|
117
153
|
|
|
118
|
-
Returns False only when the user cancels key entry
|
|
119
|
-
|
|
154
|
+
Returns False only when the user cancels key entry. An unreachable
|
|
155
|
+
key-less backend (local Ollama, or the built-in Default connection in a
|
|
156
|
+
build that carries no credential) is reported with its own actionable
|
|
157
|
+
message but is never fatal — the user may start Ollama or pick another
|
|
158
|
+
provider later.
|
|
120
159
|
"""
|
|
121
160
|
if not provider.requires_key:
|
|
122
|
-
with ui.thinking("Checking
|
|
123
|
-
|
|
124
|
-
provider.status = STATUS_CONNECTED if
|
|
125
|
-
if
|
|
126
|
-
ui.success("
|
|
161
|
+
with ui.thinking(f"Checking {provider.label}"):
|
|
162
|
+
reachable = provider.detect(config)
|
|
163
|
+
provider.status = STATUS_CONNECTED if reachable else STATUS_OFFLINE
|
|
164
|
+
if reachable:
|
|
165
|
+
ui.success(f"{provider.label} is ready.")
|
|
127
166
|
else:
|
|
128
|
-
ui.warning(
|
|
129
|
-
f"Ollama is not reachable at {config.ollama_host}. "
|
|
130
|
-
"Start it with 'ollama serve' — chatting will fail until it runs."
|
|
131
|
-
)
|
|
167
|
+
ui.warning(provider.unavailable_hint(config))
|
|
132
168
|
return True
|
|
133
169
|
|
|
134
170
|
if config.get_api_key(provider.id).strip():
|
|
@@ -352,7 +388,7 @@ def select_model(ui, config, target: str = "") -> None:
|
|
|
352
388
|
@command(
|
|
353
389
|
"provider",
|
|
354
390
|
"Select the active provider "
|
|
355
|
-
"(OpenRouter, FreeModel Claude, FreeModel Codex, AeroLink, Ollama)",
|
|
391
|
+
"(Default, OpenRouter, FreeModel Claude, FreeModel Codex, AeroLink, Ollama)",
|
|
356
392
|
)
|
|
357
393
|
def _provider_cmd(ctx: CommandContext, arg: str) -> CommandResult:
|
|
358
394
|
select_provider(ctx.ui, ctx.config, arg.strip())
|