mycode-cli 0.17.0__py3-none-any.whl → 0.18.0__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.
mycode_cli/config.py CHANGED
@@ -99,8 +99,6 @@ class WebConfig:
99
99
  @dataclass(frozen=True)
100
100
  class Settings:
101
101
  providers: dict[str, ProviderConfig]
102
- default_provider: str | None
103
- default_model: str | None
104
102
  port: int
105
103
  cwd: str
106
104
  project: str
@@ -305,10 +303,14 @@ def _clean_config_layer(data: Any) -> dict[str, Any]:
305
303
 
306
304
  out: dict[str, Any] = {}
307
305
 
308
- if data.get("default") is not None:
309
- default = _validate_default_config(data["default"])
310
- if default:
311
- out["default"] = default
306
+ compact_threshold = data.get("compact_threshold")
307
+ if compact_threshold is False:
308
+ out["compact_threshold"] = False
309
+ elif compact_threshold is not None:
310
+ parsed = None if isinstance(compact_threshold, bool) else parse_compact_threshold(compact_threshold)
311
+ if parsed is None:
312
+ raise ValueError("compact_threshold must be a number in [0, 1] or false")
313
+ out["compact_threshold"] = parsed
312
314
 
313
315
  if data.get("permission") is not None:
314
316
  out["permission"] = _validate_permission_config(data["permission"])
@@ -345,30 +347,6 @@ def _optional_config_string(raw: dict[str, Any], key: str, label: str) -> str |
345
347
  return value.strip() or None
346
348
 
347
349
 
348
- def _validate_default_config(raw: Any) -> dict[str, Any]:
349
- if not isinstance(raw, dict):
350
- raise ValueError("default must be an object")
351
-
352
- out: dict[str, Any] = {}
353
- for key in ("provider", "model"):
354
- value = _optional_config_string(raw, key, f"default.{key}")
355
- if value:
356
- out[key] = value
357
-
358
- compact_threshold = raw.get("compact_threshold")
359
- if compact_threshold is False:
360
- out["compact_threshold"] = False
361
- elif compact_threshold is not None:
362
- if isinstance(compact_threshold, bool):
363
- raise ValueError("default.compact_threshold must be a number in [0, 1] or false")
364
- parsed = parse_compact_threshold(compact_threshold)
365
- if parsed is None:
366
- raise ValueError("default.compact_threshold must be a number in [0, 1] or false")
367
- out["compact_threshold"] = parsed
368
-
369
- return out
370
-
371
-
372
350
  def _validate_permission_config(raw: Any) -> PermissionLevel | dict[str, Any]:
373
351
  if isinstance(raw, str):
374
352
  return normalize_permission_level(raw)
@@ -498,8 +476,6 @@ def get_settings(cwd: str | None = None) -> Settings:
498
476
  resolved_project = str(resolve_project(resolved_cwd))
499
477
 
500
478
  raw_providers: dict[str, dict[str, Any]] = {}
501
- default_provider: str | None = None
502
- default_model: str | None = None
503
479
  compact_threshold: float | None = None
504
480
  permission = PermissionConfig()
505
481
  raw_web: dict[str, Any] = {}
@@ -534,13 +510,8 @@ def get_settings(cwd: str | None = None) -> Settings:
534
510
 
535
511
  raw_providers[name] = merged
536
512
 
537
- default = data.get("default", {})
538
- if "provider" in default:
539
- default_provider = default["provider"]
540
- if "model" in default:
541
- default_model = default["model"]
542
- if "compact_threshold" in default:
543
- compact_threshold = float(default["compact_threshold"])
513
+ if "compact_threshold" in data:
514
+ compact_threshold = float(data["compact_threshold"])
544
515
 
545
516
  if "permission" in data:
546
517
  permission = parse_permission(data.get("permission"), permission)
@@ -561,8 +532,6 @@ def get_settings(cwd: str | None = None) -> Settings:
561
532
 
562
533
  return Settings(
563
534
  providers=_build_providers(raw_providers),
564
- default_provider=default_provider,
565
- default_model=default_model,
566
535
  compact_threshold=compact_threshold,
567
536
  permission=permission,
568
537
  web=_build_web_config(raw_web),
@@ -691,44 +660,35 @@ def resolve_provider(
691
660
  api_key: str | None = None,
692
661
  api_base: str | None = None,
693
662
  ) -> ResolvedProvider:
694
- """Resolve provider, model, api_key, and api_base from settings and overrides."""
695
-
696
- selected_name = (provider_name or settings.default_provider or "").strip()
697
- if selected_name:
698
- try:
699
- return _resolve_provider_runtime(
700
- settings,
701
- selected_name=selected_name,
702
- model=model,
703
- api_key=api_key,
704
- api_base=api_base,
663
+ """Resolve provider, model, api_key, and api_base from settings and overrides.
664
+
665
+ Without a provider name, the first available provider is used.
666
+ """
667
+
668
+ selected_name = (provider_name or "").strip()
669
+ if not selected_name:
670
+ refs = _available_provider_references(settings)
671
+ if not refs:
672
+ env_names = list(
673
+ dict.fromkeys(
674
+ env_name
675
+ for provider_id in list_env_discoverable_providers()
676
+ for env_name in provider_env_api_key_names(provider_id)
677
+ )
705
678
  )
706
- except ValueError:
707
- # A caller-supplied provider_name must fail loudly rather than silently fall back.
708
- if provider_name:
709
- raise
710
-
711
- refs = _available_provider_references(settings)
712
- if refs:
713
- return _resolve_provider_runtime(
714
- settings,
715
- selected_name=refs[0],
716
- model=model,
717
- api_key=api_key,
718
- api_base=api_base,
719
- )
679
+ checked = ", ".join(env_names) or "<api key env>"
680
+ raise ValueError(
681
+ "no available providers found; set one of the supported API key env vars "
682
+ + f"({checked}) or configure a provider in ~/.mycode/config.json or a project .mycode/config.json"
683
+ )
684
+ selected_name = refs[0]
720
685
 
721
- env_names = list(
722
- dict.fromkeys(
723
- env_name
724
- for provider_id in list_env_discoverable_providers()
725
- for env_name in provider_env_api_key_names(provider_id)
726
- )
727
- )
728
- checked = ", ".join(env_names) or "<api key env>"
729
- raise ValueError(
730
- "no available providers found; set one of the supported API key env vars "
731
- + f"({checked}) or configure a provider in ~/.mycode/config.json or a project .mycode/config.json"
686
+ return _resolve_provider_runtime(
687
+ settings,
688
+ selected_name=selected_name,
689
+ model=model,
690
+ api_key=api_key,
691
+ api_base=api_base,
732
692
  )
733
693
 
734
694
 
@@ -744,41 +704,32 @@ def resolve_provider_choices(settings: Settings) -> list[ResolvedProvider]:
744
704
  return choices
745
705
 
746
706
 
747
- def _available_provider_references(settings: Settings) -> list[str]:
748
- """Return usable provider names with the configured default first."""
749
-
750
- available: list[str] = []
751
- seen: set[str] = set()
752
- configured_types_with_credentials: set[str] = set()
707
+ def provider_models(settings: Settings, provider: ResolvedProvider) -> list[str]:
708
+ """Return a resolved provider's selectable models: configured ones, else the adapter defaults."""
753
709
 
754
- def add(name: str | None) -> None:
755
- cleaned = (name or "").strip()
756
- if not cleaned or cleaned in seen:
757
- return
710
+ provider_config = settings.providers.get(provider.provider_name or "")
711
+ if provider_config and provider_config.models:
712
+ return list(provider_config.models)
713
+ return list(provider_default_models(provider.provider)) or [provider.model]
758
714
 
759
- provider_config = settings.providers.get(cleaned)
760
- provider_type = provider_config.type if provider_config else cleaned
761
- if not is_supported_provider(provider_type):
762
- return
763
715
 
764
- if provider_config:
765
- if not provider_is_available(provider_config):
766
- return
767
- configured_types_with_credentials.add(provider_type)
768
- elif not provider_api_key_from_env(provider_type):
769
- return
770
-
771
- seen.add(cleaned)
772
- available.append(cleaned)
773
-
774
- add(settings.default_provider)
716
+ def _available_provider_references(settings: Settings) -> list[str]:
717
+ """Return usable provider names: configured providers in order, then env-discovered built-ins."""
775
718
 
776
- for name in settings.providers:
777
- add(name)
719
+ available: list[str] = []
720
+ configured_types: set[str] = set()
721
+ for name, config in settings.providers.items():
722
+ if is_supported_provider(config.type) and provider_is_available(config):
723
+ available.append(name)
724
+ configured_types.add(config.type)
778
725
 
779
726
  for provider_id in list_env_discoverable_providers():
780
- if provider_id not in configured_types_with_credentials:
781
- add(provider_id)
727
+ if (
728
+ provider_id not in configured_types
729
+ and provider_id not in settings.providers
730
+ and provider_api_key_from_env(provider_id)
731
+ ):
732
+ available.append(provider_id)
782
733
 
783
734
  return available
784
735
 
@@ -805,8 +756,6 @@ def _resolve_provider_runtime(
805
756
  resolved_model = requested_model
806
757
  elif provider_config and provider_config.models:
807
758
  resolved_model = next(iter(provider_config.models))
808
- elif selected_name == settings.default_provider and (settings.default_model or "").strip():
809
- resolved_model = str(settings.default_model).strip()
810
759
  else:
811
760
  defaults = provider_default_models(provider_type)
812
761
  if not defaults:
mycode_cli/main.py CHANGED
@@ -5,9 +5,9 @@ from __future__ import annotations
5
5
  import asyncio
6
6
  import os
7
7
  import sys
8
- from contextlib import aclosing, suppress
8
+ from contextlib import aclosing
9
9
  from dataclasses import dataclass, replace
10
- from typing import TYPE_CHECKING, Annotated, Any, Literal
10
+ from typing import Annotated, Any, Literal
11
11
  from uuid import uuid4
12
12
 
13
13
  import typer
@@ -21,17 +21,14 @@ from mycode_cli.config import (
21
21
  get_settings,
22
22
  normalize_reasoning_effort,
23
23
  parse_permission,
24
- resolve_provider,
25
24
  resolve_sessions_dir,
26
25
  )
27
26
  from mycode_cli.permissions import PERMISSION_DENIED_BY_USER_OUTPUT, PERMISSION_DENIED_OUTPUT
28
27
  from mycode_cli.sessions import SessionStore
28
+ from mycode_cli.state import load_state, resolve_remembered_provider
29
29
 
30
30
  from .runtime import build_agent
31
31
 
32
- if TYPE_CHECKING:
33
- from .tui.render import TerminalView
34
-
35
32
  app = typer.Typer(add_completion=False, pretty_exceptions_enable=False)
36
33
  session_app = typer.Typer(help="Session management")
37
34
  app.add_typer(session_app, name="session")
@@ -140,7 +137,6 @@ async def run_noninteractive(agent: Agent, message: str, *, store: SessionStore,
140
137
  @dataclass
141
138
  class _BootstrapContext:
142
139
  store: SessionStore
143
- view: TerminalView
144
140
  settings: Settings
145
141
  resolved_provider: ResolvedProvider
146
142
  resolved_session: ResolvedSession
@@ -159,21 +155,20 @@ def _bootstrap(
159
155
  ) -> _BootstrapContext:
160
156
  """Shared setup for the chat and run commands."""
161
157
 
162
- from .tui.render import TerminalView
163
-
164
158
  if session and continue_last:
165
159
  raise typer.BadParameter("--session and --continue are mutually exclusive")
166
160
 
167
161
  cwd = os.path.abspath(os.getcwd())
168
162
  store = SessionStore(data_dir=resolve_sessions_dir())
169
- view = TerminalView()
170
163
  try:
171
164
  settings = get_settings(cwd)
172
165
  if permission is not None:
173
166
  settings = replace(settings, permission=parse_permission(permission, settings.permission))
174
- resolved_provider = resolve_provider(settings, provider_name=provider, model=model)
175
- resolved_effort = normalize_reasoning_effort(reasoning_effort)
176
- if resolved_effort is not None:
167
+ state = load_state()
168
+ resolved_provider = resolve_remembered_provider(settings, state, provider_name=provider, model=model)
169
+ if reasoning_effort is None:
170
+ resolved_effort = state.effort_for(resolved_provider)
171
+ elif (resolved_effort := normalize_reasoning_effort(reasoning_effort)) is not None:
177
172
  if not resolved_provider.supports_reasoning_effort:
178
173
  raise ValueError(f"provider {resolved_provider.provider!r} does not support reasoning effort")
179
174
  if resolved_effort not in resolved_provider.reasoning_efforts:
@@ -182,6 +177,7 @@ def _bootstrap(
182
177
  f"reasoning effort {resolved_effort!r} is not supported by model {resolved_provider.model!r}; "
183
178
  + f"supported efforts: {supported}"
184
179
  )
180
+ resolved_provider = replace(resolved_provider, reasoning_effort=resolved_effort)
185
181
  resolved_session = asyncio.run(
186
182
  resolve_session(
187
183
  store=store,
@@ -191,7 +187,9 @@ def _bootstrap(
191
187
  )
192
188
  )
193
189
  except ValueError as exc:
194
- view.console.print(f"[red]{exc}[/red]")
190
+ from .tui.render import console, error_line
191
+
192
+ console.print(error_line(str(exc)))
195
193
  raise SystemExit(1) from exc
196
194
 
197
195
  agent = build_agent(
@@ -202,12 +200,9 @@ def _bootstrap(
202
200
  session_id=resolved_session.session_id,
203
201
  max_turns=max_turns,
204
202
  )
205
- if reasoning_effort is not None:
206
- agent.reasoning_effort = resolved_effort
207
203
 
208
204
  return _BootstrapContext(
209
205
  store=store,
210
- view=view,
211
206
  settings=settings,
212
207
  resolved_provider=resolved_provider,
213
208
  resolved_session=resolved_session,
@@ -266,22 +261,11 @@ def chat(
266
261
  session_id=setup.resolved_session.session_id,
267
262
  provider_name=setup.resolved_provider.provider_name,
268
263
  reasoning_efforts=setup.resolved_provider.reasoning_efforts,
269
- view=setup.view,
270
- )
271
-
272
- setup.view.print_header(
273
- provider=setup.resolved_provider.provider,
274
- model=setup.resolved_provider.model,
275
264
  session=setup.resolved_session.session,
276
265
  mode=setup.resolved_session.mode,
277
- message_count=len(setup.resolved_session.messages),
278
- reasoning_effort=setup.agent.reasoning_effort,
266
+ messages=setup.resolved_session.messages,
279
267
  )
280
- if setup.resolved_session.mode == "resumed":
281
- setup.view.print_history_preview(setup.resolved_session.messages)
282
-
283
- with suppress(KeyboardInterrupt):
284
- asyncio.run(terminal_chat.run())
268
+ asyncio.run(terminal_chat.run())
285
269
 
286
270
 
287
271
  @app.command()
@@ -355,15 +339,15 @@ def session_list(
355
339
  ) -> None:
356
340
  """List saved sessions."""
357
341
 
358
- from .tui.render import TerminalView
342
+ from .tui.render import console, session_list_table
359
343
 
360
344
  cwd = os.path.abspath(os.getcwd())
361
345
  store = SessionStore(data_dir=resolve_sessions_dir())
362
- view = TerminalView()
363
346
 
364
347
  sessions = asyncio.run(store.list_sessions(cwd=None if all_workspaces else cwd))
365
348
  heading = "all sessions" if all_workspaces else f"sessions for {cwd}"
366
- view.print_session_list(sessions, include_cwd=all_workspaces, heading=heading)
349
+ for renderable in session_list_table(sessions, include_cwd=all_workspaces, heading=heading):
350
+ console.print(renderable)
367
351
 
368
352
 
369
353
  def main() -> None:
@@ -30,11 +30,11 @@ from mycode.messages import (
30
30
  image_block,
31
31
  text_block,
32
32
  )
33
- from mycode.providers import provider_default_models
34
33
  from mycode_cli.config import (
35
34
  ResolvedProvider,
36
35
  get_settings,
37
36
  normalize_reasoning_effort,
37
+ provider_models,
38
38
  resolve_configured_model_metadata,
39
39
  resolve_provider,
40
40
  resolve_provider_choices,
@@ -402,13 +402,7 @@ async def get_config(cwd: Annotated[str | None, Query()] = None) -> dict[str, An
402
402
  providers_info: dict[str, Any] = {}
403
403
  for provider in resolve_provider_choices(settings):
404
404
  provider_config = settings.providers.get(provider.provider_name or "")
405
- models = (
406
- list(provider_config.models)
407
- if provider_config and provider_config.models
408
- else list(provider_default_models(provider.provider))
409
- )
410
- if not models:
411
- models = [provider.model]
405
+ models = provider_models(settings, provider)
412
406
 
413
407
  info: dict[str, Any] = {
414
408
  "name": provider.provider_name,