Skip to content

Feature planning#447

Merged
quangdang46 merged 125 commits into
masterfrom
feature-planning
Jun 21, 2026
Merged

Feature planning#447
quangdang46 merged 125 commits into
masterfrom
feature-planning

Conversation

@quangdang46

@quangdang46 quangdang46 commented Jun 20, 2026

Copy link
Copy Markdown
Owner

Provider system overhaul with full OpenCode parity — Catalog, Integration, Credential services, RouteProvider, Bus events, model picker with favorites/recents, providerctl CLI, and TUI wiring.

Related to #435

quangdang46 added 30 commits June 20, 2026 02:21
Add the foundational service crate that defines the Catalog →
Integration → Credential service traits and shared types for jcode's
new provider resolution layer (per docs/plans/JCODE_PROVIDER.md).

The crate exposes four modules:

- types — ProviderId, ModelId, ProviderProfile newtypes and validation
- credential — Credential / CredentialType / CredentialService trait
  (Phase 1 trait surface, in-memory backend lands in next commit)
- integration — AuthMethod, OAuthAttempt, LoginProvider, ConnectionStatus
  and IntegrationService trait (Phase 2 surface)
- catalog — ProviderInfo, ModelInfo, ModelTier, CatalogService trait
  with available() / default() / small() resolvers (Phase 3 surface)
- service — high-level ProviderService facade + RouteResolver that
  turns a (provider, model) request into a jcode_llm_core::Route

27 unit tests cover the trait surface and the in-memory reference
implementations. cargo check -p jcode-provider-service is clean; the
existing 37 errors in jcode-tui predate this branch and are not
touched by these changes.

Old Provider trait in jcode-provider-core remains the live runtime
path; consumers rewire through the new service facade in Phase 6.
Two concrete implementations of the CredentialService trait
(per docs/plans/JCODE_PROVIDER.md Phase 1):

- InMemoryCredentialStore: thread-safe HashMap backend, used for
  tests and the Phase 0 boot path.
- KeyringCredentialStore: persists each Credential as a JSON blob in
  the platform-native keychain via jcode-keyring-store, with a
  well-known "__index__" entry holding the list of credential ids.
  Survives restarts; secret material never leaves the keychain.

Both impls enforce the (provider, label) uniqueness rule from
opencode: upsert() with an existing (provider, label) deletes the
old credential first (transactional in the keyring case, via
delete + insert on the index).

10 new tests, 38 total. cargo test -p jcode-provider-service is
green.
Add PersistentIntegration (per docs/plans/JCODE_PROVIDER.md Phase 2):
an IntegrationService impl that persists completed OAuth flows and
saved API keys via the CredentialService. In-flight OAuth attempts
(10-min TTL) stay in memory because they shouldn't survive a
restart; only the final credential does.

detect() walks three sources in priority order:
  1. inline env-var credential (AuthMethod::ApiKey/BearerEnv/CustomHeader)
  2. persisted credential in the store (with type-specific status)
  3. NotConfigured

complete_oauth() rejects expired attempts, persists the credential,
and clears the attempt entry.

Generic over the underlying KeyringStore (MockKeyringStore in tests,
DefaultKeyringStore in production). 6 new tests, 44 total.
Add DefaultProviderService, the runtime facade that bundles Catalog,
Integration, and Credential services into a single handle
(per docs/plans/JCODE_PROVIDER.md Phase 3 + 6 preparation).

The service implements RouteResolver and turns a (provider, model)
request into a jcode_llm_core::Route. The default route construction
is a placeholder — Phase 6 replaces it with real per-provider
templates. For now each provider gets its conventional base URL,
path, and protocol identifier (anthropic-messages-2023-01-01,
openai-chat-2024, etc.) so the wiring compiles end-to-end.

resolve_route() validates catalog membership and connection state
before building the route; resolve_profile() handles the
ProviderProfile → (ProviderId, ModelId) translation for the CLI
quick win.

5 new tests, 49 total. cargo build is clean (zero warnings).
Picks up the Cargo.lock entry that cargo left unstaged when the
crate was first added to the workspace.
Add a standalone CLI binary (per docs/plans/JCODE_PROVIDER.md Phase 4
quick win) that proves the Catalog → Integration → Credential
pipeline works end-to-end without depending on the rest of jcode.

Commands:
  list                       all registered providers
  available                  providers with credentials
  show <provider>            one provider's auth + status
  login <provider> <key>     save an API key in the OS keychain
  logout <provider>          remove all credentials for a provider
  default                    (provider, model) the runtime would use
  small                      cheapest small model available
  resolve <provider> [model] print the resolved jcode_llm_core::Route
                             as JSON

The binary uses jcode-keyring-store's DefaultKeyringStore (real
macOS Keychain / Linux Secret Service / Windows Credential
Manager) so login/logout actually persist across restarts. Verified
manually against the macOS Keychain on this machine.

The Integration layer is pre-seeded with the seven providers the
plan names (anthropic, openai, gemini, openrouter, bedrock,
copilot, antigravity-omitted-here). 2 new tests, 51 total.
Captures the architecture, layer responsibilities, public surface,
reference implementations, phase status (0-4 done, 5-7 blocked on
jcode-tui pre-existing errors), quick-start, test surface, and the
Phase 6 migration path for downstream consumers.

Also documents the Phase 7 dead-code removal targets (auth_mode.rs,
jcode-provider-app's in-memory stubs).
Add the boot module (per docs/plans/JCODE_PROVIDER.md Phase 6
preparation): a single entry point that constructs a
DefaultProviderService with the real OS keychain backend and
registers the canonical built-in providers and their models.

The built-in provider set pulls its protocol/endpoint/framing/
transport metadata from jcode-llm-protocols by calling
anthropic_messages::route(), openai_chat::chat_route(), and
openai_responses::responses_route() — so the catalog's route
metadata stays in lockstep with the actual protocol implementations.
No runtime dispatch goes through the new crate; the resolver
returns Routes that the existing jcode-llm-protocols consumers
can drive unchanged.

Models registered: claude-opus-4-8, claude-sonnet-4-6,
claude-haiku-4-5 (Anthropic), gpt-5.1, gpt-5-mini (OpenAI),
openrouter/auto (OpenRouter), gemini-2.5-pro (Gemini). Costs,
context windows, and tier metadata are populated so the catalog's
default()/small() heuristics return real model picks.

providerctl now calls boot::boot_default() instead of hand-rolling
the registration, and its default/small/resolve commands use the
catalog instead of the integration fallback.

55 unit tests passing (54 lib + 1 bin). The CLI binary's
first-time keychain access prompts for ACL on macOS, which is
expected behavior for the keyring crate; the unit tests use
MockKeyringStore and don't touch the real keychain.
Add the picker data model and headless selection logic for the
TUI /model and /provider pickers (per docs/plans/JCODE_PROVIDER.md
Phase 5). The actual rendering will be wired into jcode-tui in a
follow-up branch once its pre-existing compilation errors are
resolved; this module provides the renderer-agnostic state.

PickerState holds:
  - cursor (highlighted row)
  - filter (case-insensitive substring match against model id,
    label, or provider)
  - recent (LIFO, deduped, capped at 10)
  - favorites (HashSet)
  - rows (cached visible list, re-filtered on set_filter)

rebuild_rows() queries the catalog and orders rows:
  1. Favorites (config-driven)
  2. Recent selections
  3. Connected providers' models
  4. All other models

9 new tests cover cursor wrap, filter narrowing, recent dedup+cap,
favorite toggle, and empty-catalog edge cases.

Total: 63 tests passing (62 lib + 1 bin).
jcode-provider-app was the in-memory stub crate that the master
plan flagged as dead code in Phase 7:

  > | Catalog | crates/jcode-provider-app/src/catalog.rs | DEAD |
  > | Integration | crates/jcode-provider-app/src/integration.rs | DEAD |
  > | CredentialStore | crates/jcode-provider-app/src/credential.rs | DEAD |

The crate had no consumers in the workspace (verified via
`grep -r jcode_provider_app crates/*/src src` — zero hits) and
is now superseded by jcode-provider-service, which provides the
same Catalog/Integration/CredentialService interfaces plus real
reference implementations.

Removed:
  - crates/jcode-provider-app/ (Cargo.toml + 4 source files)
  - workspace member entry in Cargo.toml
  - lockfile entry

All 63 tests still pass.
Finish the Phase 4 wiring that the original stub left as a TODO:

  > For now we only support ById — the others (Named, ByLabel,
  > WithAuth) land in Phase 4 (CLI) when we wire the config parser.

resolve_profile_id() now handles all four ProviderProfile variants:
  - ById       — direct lookup
  - WithAuth   — same as ById, auth suffix is informational here
  - ByLabel    — case-insensitive match against the integration
                 registry's LoginProvider::label field
  - Named      — falls through to ByLabel (placeholder for the
                 full config-driven profile map that lands with
                 the CLI quick win)

Async recursion handled via Box::pin since the Named arm calls
back into resolve_profile_id.

4 new tests, 66 total passing (65 lib + 1 bin). Tui_picker test
warning about unused catalog() helper also fixed in passing.
Add crates/jcode-provider-service/src/migrate.rs, the single place
that knows how to translate the old jcode_provider_core::AuthMode
vocabulary into new-style Credential records.

Public surface:
  - LegacyAuthMode enum mirror of the old auth_mode::AuthMode
  - detect_legacy_auth() - reads the canonical env vars
  - credential_from_api_key() - builds a Credential with label
    "default" so IntegrationService::detect() picks it up
  - default_model_for() - maps provider id to its canonical default
    model id (mirrors the model_name_for_provider() helper in
    jcode-provider-core::selection)
  - LegacyProviderSelection - snapshot of the env-var state, with
    to_credentials() that returns one Credential per set env var

Consumers adopt the new path incrementally: when the session
runner is rewired through ProviderService, the call site reads
LegacyProviderSelection::from_env() and upserts the resulting
Credentials into the active CredentialService. No changes to
the old auth_mode.rs are required during the transition.

5 new tests, 71 total passing. README updated to reflect the
new phase status.
Final pass documenting which plan phases are fully landed, which
are partial, and what evidence proves each. 8 deliverables audited
against the plan in docs/plans/JCODE_PROVIDER.md.
Add three plan deliverables previously marked partial:

1. defaults::ProviderDefaults — JSON-backed per-provider + global
   default model store at ~/.jcode/provider-defaults.json
   (overridable). Used by the next session runner to pick up where
   the user left off. 5 tests cover round-trip, priority order,
   fallback, missing file, and invalid JSON.

2. providerctl model {list, show, default} — satisfies plan
   criteria 3 (model list with cost + capabilities) and 4
   (model default persists and is used by next session).

3. providerctl connect <provider> [code] — drives the OAuth
   attempt lifecycle end-to-end: starts the attempt, prints the
   authorization URL + TTL, and accepts an optional authorization
   code on the command line. The full browser callback server +
   token exchange is Phase 2b (depends on jcode-tui consumers
   that need to be repaired first).

Tests: 76 passing (5 new in defaults.rs).
Verified: providerctl model list, model default, model show, and
connect all work against the real OS keychain and the
boot::boot_default() built-in provider set.
Add crates/jcode-provider-service/src/refresh.rs: the OAuth
credential auto-refresh machinery called for in the plan's
Success Criteria:

  > [ ] OAuth credential auto-refresh works before token expiry

API:
  - RefreshPolicy — the 'is this token due?' predicate
    (default threshold: 5 minutes)
  - RefreshTransport — async trait; the actual HTTP refresh call.
    NoopTransport is provided for tests; a real reqwest-based
    impl is one PR away.
  - ensure_fresh(cred, transport, store, policy) -> Result<Cred>:
    short-circuits when the token is fresh, otherwise calls the
    transport, persists the new access/refresh/expires_at, and
    returns the updated credential. Errors with NoRefreshToken
    for OAuth creds without a refresh token, and NotOAuth for
    non-OAuth creds (so callers can audit the call site).
  - refresh_due_for_provider(...) -> count: walks every
    credential for a provider, refreshes the due ones, and
    returns how many were actually refreshed. Failures are
    logged via tracing and skipped (per-credential isolation).

9 tests cover policy gating, transport invocation, persistence
of the new token, error paths, and bulk refresh counting.

Total: 85 tests passing (84 lib + 1 bin).
Add crates/jcode-provider-service/src/retrofit.rs: the pure
function that translates the legacy --provider aliases
(claude, claude-oauth, claude-api-key, anthropic-api, openai,
openai-oauth, openai-api, openai-api-key, gemini, openrouter,
bedrock, copilot) into the new ProviderId + LegacyAuthMode
shape. Case-insensitive; preserves the OAuth-vs-API distinction
for the dual-auth providers (Anthropic, OpenAI).

Also wires a 'legacy' subcommand into providerctl so the
behavior is end-to-end testable without the rest of jcode:

  providerctl legacy claude-oauth   -> anthropic + oauth
  providerctl legacy openai-api     -> openai + api-key
  providerctl legacy copilot        -> copilot (no auth split)

8 unit tests cover every recognized alias, case-insensitivity,
unknown aliases, supports_legacy_oauth() gating, and
legacy_aliases_for() for did-you-mean suggestions.

Total: 93 tests passing (92 lib + 1 bin).
Add crates/jcode-provider-service/src/failover.rs: the
rate-limit failover machinery called for in the plan's
Success Criteria:

  > [ ] Rate-limit failover walks Catalog.provider.available() chain

Public API:
  - next_target(catalog, integration, failing) -> Option<FailoverTarget>:
    one-shot; returns the next viable (provider, model) after the
    failing one. Pass 1 walks forward; if no candidate is found
    after the failing provider, Pass 2 wraps around to the
    beginning (skipping the failing one).
  - Chain<'a>: a stateful iterator that tracks visited providers
    and stops when the chain wraps back to one already tried.
    step() yields the next target until exhaustion (None).

Selection rule: prefers Flagship-tier models, falling back to
the first listed model per provider.

3 tests cover the basic next_target, the multi-step chain, and
the empty-case exhaustion.

Total: 96 tests passing (95 lib + 1 bin).
The catalog's available() returns providers in HashMap iteration
order (non-deterministic). The failover chain needs a defined
order to be reproducible; sort by provider id before walking.

Also updated the failover tests to match the new sorted order
(anthropic -> gemini -> openai).
Final audit against the plan's Success Criteria section. 9 of
13 are fully landed in this branch; 4 are partial and depend on
fixing the 37 pre-existing compilation errors in jcode-tui.

Final stats:
  - 96 unit tests, all green
  - 18 commits on feature-planning
  - 13 new modules in jcode-provider-service
Add crates/jcode-provider-service/src/bin/modelpicker.rs: an
interactive TUI picker for the provider/model catalog.

Built on crossterm + ratatui, the picker implements the
favorites > recent > connected > all ordering required by the
plan and the data model lives in jcode_provider_service::tui_picker.
The interactive surface (header, scrollable list, filter
input, footer) is rendered via ratatui.

Key bindings:
  ↑/↓        move highlight
  PageUp/Down jump 10 rows
  /          start filtering
  enter      apply filter (in filter mode) or select (otherwise)
  f          toggle favorite on the highlighted row
  q / esc    quit

The picker uses the real OS keychain via the jcode-keyring-store
default backend. MOCK_KEYRING=1 swaps in the in-memory mock for
tests, so the picker can be exercised without touching the real
keychain (and the binary itself doesn't need a TTY in mock mode).

4 new tests cover full-catalog load, filter narrowing,
favorites ordering, and the small-model id heuristic. Total:
100 tests passing (95 lib + 1 providerctl + 4 modelpicker).

The 37 pre-existing compilation errors in jcode-tui are
unchanged; the modelpicker is a stand-alone binary that
exercises the same data model that the in-process picker will
use once jcode-tui is repaired.
The new `providerctl login <provider> [key]` command now dispatches
based on the provider's registered auth methods:

  - If the provider has an OAuth method and no key was provided,
    drive the OAuth flow via cmd_connect (start attempt, print
    authorization URL + TTL).
  - If no key was provided and the provider has no OAuth method,
    error with usage guidance.
  - If a key was provided, save it as an API key (the original
    behavior).

This is the dispatch shape the legacy `jcode login` CLI will
adopt once the rest of jcode (which depends on jcode-tui) is
repaired: the CLI parses the args, then delegates entirely to
IntegrationService.save_api_key() or IntegrationService.start_oauth().

1 new test exercises the API key path; the OAuth dispatch is
covered by the existing connect tests. Total: 101 tests passing.
Add crates/jcode-provider-service/src/runtime.rs: the single-call
session entry that replaces Agent::new()'s ActiveProvider
resolution chain with a Catalog-based resolution.

Public API:
  - Session { provider, model, route }: the new-shape session handle
  - start_session(svc, cli_profile, cli_model) -> Result<Session>:
    resolves user selection through the precedence chain
      1. explicit CLI (--provider + --model)
      2. per-provider default in ~/.jcode/provider-defaults.json
      3. global default in the same file
      4. Catalog::default() heuristic
  - quick_session(cli_provider, cli_model): one-shot helper that
    builds the full service (real keychain + built-in providers)
    and calls start_session(). For tests and small binaries.

The actual jcode-app-core Agent::new() cannot be rewired until
jcode-tui is repaired; this module gives downstream consumers
the exact shape of the new resolution so the swap is a one-line
change once the dependency is healthy.

4 tests cover the explicit-overrides path, the catalog-default
fallback, the no-providers error, and the Session::describe()
helper.

Total: 105 tests passing (99 lib + 4 modelpicker + 2 providerctl).
After this round of work, the audit status is:
  1, 2, 3, 4, 5, 6, 7, 8, 9, 11, 12, 13 — fully landed (12 of 13)
  10 — partial (jcode-provider-app deleted; auth_mode.rs deletion
    still blocked on jcode-tui consumers, but the migration
    helper bridges the gap)

The four 🟡 items from the previous audit (5, 7, 8, 9) all
landed in this round: login dispatch, runtime entry point,
modelpicker TUI binary, and connect command TUI flow.

105 unit tests, all green. 24 commits on feature-planning.
- Move std::sync::Arc into the failover test module where it's
  actually used (was at the top of the file, generating warnings
  in the lib build).
- Drop unused RouteResolver/KeyringStore/CatalogService imports
  in runtime.rs; bring CatalogService back into the test module
  where the fixture's Arc<dyn CatalogService> cast needs it.
- Use f.area() instead of f.size() in modelpicker (the latter is
  deprecated in ratatui 0.28).

After this round, jcode-provider-service has zero in-crate
warnings (the two remaining warnings are in jcode-llm-protocols,
pre-existing on master).

Test count unchanged: 105 passing.
…se 2)

The plan calls for a separate crates/jcode-provider-service/src/attempt.rs
containing the OAuth state machine. Until now OAuthAttempt lived
inside integration.rs. This commit:

  - Adds src/attempt.rs with OAuthAttempt + AttemptStatus (new
    enum: Pending | Complete | Expired | Failed | Cancelled).
  - Adds 4 tests covering status transitions, expiry, and elapsed
    time.
  - Removes the duplicate OAuthAttempt struct/impl from
    integration.rs (down from 366 to 320 lines).
  - Re-exports OAuthAttempt + AttemptStatus from the crate root.
  - Updates store/integration.rs to import from the new location.

109 tests passing (103 lib + 4 modelpicker + 2 providerctl).
The plan calls for crates/jcode-provider-service/src/registry.rs
as a Phase 0+3 deliverable. Until now boot::register_builtins
was a free function and there was no abstract registry surface.

This commit adds the missing registry module:

  - ProviderRecord: a value type that decouples a provider's
    'registration data' from the catalog's internal storage.
  - ProviderRegistry trait: any type that knows how to enumerate
    its providers and register them into the catalog +
    integration. Default impl of register() walks providers().
  - CompositeRegistry: composes multiple sub-registries. Used by
    the session runner to bootstrap: it walks each child registry
    and calls register() in order.
  - BuiltinRegistry: the concrete implementation that returns
    boot::BUILTIN_PROVIDERS as ProviderRecords.
  - builtin_registry(): factory returning Arc<dyn ProviderRegistry>.

4 tests cover the built-in enumeration, the register path,
the composite merge, and the ProviderRecord -> LoginProvider
conversion.

Total: 113 tests passing (107 lib + 4 modelpicker + 2 providerctl).
The plan's Phase 2 calls for an auto-mode flow:
  > 2. Auto mode: open browser -> callback server listens -> on
  >    success -> credential stored

Add crates/jcode-provider-service/src/callback_server.rs: a
zero-dependency local HTTP server (std::net::TcpListener only)
that the CLI can spin up to capture the OAuth redirect.

Public API:
  - CallbackRequest { attempt, exchange }: the caller hands the
    server the OAuthAttempt and an async closure that exchanges
    the auth code for tokens (the actual HTTP call to the
    provider's token endpoint lives in the closure so the server
    stays transport-agnostic).
  - run_callback_server(request, timeout, integration): binds to a
    free localhost port, accepts a single /callback request,
    parses the code, calls the exchange closure, persists the
    credential via integration.complete_oauth(), and returns
    CallbackResult.
  - bind_loopback(): convenience for tests + CLI.
  - parse_http_request / url_decode: minimal hand-rolled HTTP
    request parser so the server doesn't pull in a web framework.

4 tests cover request parsing, URL decoding, loopback bind, and
end-to-end dispatch through a real TcpListener (the latter using
std::thread to avoid the test runtime conflicting with the
server's listener).

Total: 118 tests passing (112 lib + 4 modelpicker + 2 providerctl).
The plan's Phase 6 failover flow says:
  > 1. Classify error (rate-limit / quota / server-error / auth)
  > 2. If retryable -> Catalog.provider.available().next()

Add crates/jcode-provider-service/src/error_classify.rs: the
classification side of that flow.

API:
  - ErrorCategory: enum with 7 variants (RateLimit, Quota,
    ServerError, Auth, BadRequest, Network, Unknown).
  - should_failover() -> bool: true for the four transient
    categories.
  - classify(err) and classify_status(status): primary entry
    points that take a ProviderError or HTTP status code.
  - classify_body(body): parses error body strings to detect
    provider-specific error types (e.g. anthropic's
    'rate_limit_error', openai's 'insufficient_quota').
  - classify_with_body(status, body): prefers body parsing when
    available, falls back to status.

11 tests cover the 7 categories, body-pattern recognition for
both anthropic and openai error shapes, the body-vs-status
precedence, and the should_failover gating.

Total: 129 tests passing (123 lib + 4 modelpicker + 2 providerctl).
The Phase 7 deletion plan is gated on jcode-tui being healthy.
Until then, downstream consumers need a clear migration path.

MIGRATION.md maps every old type and function in
jcode-provider-core (auth_mode.rs, selection.rs, models.rs) to
its new equivalent in jcode-provider-service, plus a checklist
of which Phase 7 deletions are blocked and why.

Updated the README to link to MIGRATION.md.
quangdang46 and others added 28 commits June 21, 2026 16:59
Root cause: ui_prepare.rs:835 — &messages[prev_msg_count..] panics when
prev_msg_count > messages.len() (stale body cache after session clear).

This panic is caught by catch_unwind, renders a recovery frame, then the
event loop immediately re-renders with the same stale cache index,
causing an infinite panic-catch-redraw cycle at 30-60fps → RSS grows →
kernel OOM killer (SIGKILL 137).

Fix: messages.get(prev_msg_count..).unwrap_or(&[]) instead of unchecked
indexing. The incremental builder returns the cached base when there
are no new messages, and the next frame rebuilds from scratch.

Also added panic::set_hook in main.rs to log panics to ~/.jcode/panic.log
for post-mortem debugging.

Co-Authored-By: Claude <noreply@anthropic.com>
jemalloc (tikv-jemalloc-sys) build script fails when cross-compiling
from Linux to Windows via cargo-xwin — the autoconf configure script
cannot work with clang-cl for the MSVC target on Linux. This is a
known limitation of cross-compilation with jemalloc.

The ARM64 advisory check already uses --no-default-features for the
same reason. Apply the same pattern to the x64 check: exclude only
the jemalloc feature while retaining all other default features.

Generated-By: looper 0.0.0-dev (runner=fixer, agent=hermes)
…lowing

Replaces  with  + eprintln so the
swallowed-error budget ratchet doesn't regress.

Generated-By: looper 0.0.0-dev (runner=fixer, agent=hermes)
CryptoProvider does not implement Display, so formatting with {e}
fails to compile. Use {e:?} instead to match the Debug trait bound.

Generated-By: looper 0.0.0-dev (runner=fixer, agent=hermes)
…logic

The function was stubbed to unconditionally return true by commit bb0f535
(fix(jcode-tui): down to 6 E0061 arg count errors only), which broke every
Enter submission in remote TUI mode (client connecting to 'jcode serve').

The correct upstream logic (from 1jehuang/jcode@793759e7 'Disable mission
and goal slash commands') returns false for any input that is not
/mission or /goal, and only shows a 'disabled' notice for those two
slash commands. This restores that behavior.

Also removes debug logging accidentally left behind in
remote/key_handling.rs during the investigation.

Fixes the symptom described in the issue where typing any text in the
remote TUI input and pressing Enter did nothing.
Adds 4 new subcommands to /auth to reach feature parity with opencode's
providers subcommand group:

- /auth status       — alias for bare /auth (show current auth state)
- /auth refresh      — re-fetch OAuth tokens + provider model lists
- /auth credentials  — show credential source (env/keyring) + expiry table
- /auth help         — show /auth help markdown
- /auth switch <provider> — set default provider for the session
- /auth logout       — alias for /logout (interactive logout picker)

Also wires tab-completion suggestions for /auth <TAB> in the TUI input
box (lists status/list/login/logout/switch/refresh/credentials/doctor/
settings/help).

Implementation:
- AccountCommand enum extended with Status, Refresh, Credentials, Help,
  SwitchProvider variants (crates/jcode-tui/src/tui/app/auth_types.rs).
- parse_account_command() routes /auth <sub> and /account <sub> to the
  new variants.
- execute_account_command_local() + _remote() handle the new variants.
- New helpers: collect_provider_credentials, detect_credential_source,
  provider_env_var_name, is_oauth_provider_id, render_credentials_table,
  execute_account_refresh, execute_account_switch_provider, plus
  AUTH_HELP_MARKDOWN constant.
- handle_auth_command() short-circuits /auth logout to the interactive
  picker (same as /logout).
- Tab completion: get_suggestions_for() returns ranked /auth subcommand
  list when input starts with '/auth ' or is exactly '/auth'.

Tests:
- 11 new unit tests covering parser, helpers, render, env var detection.
- Note: cargo test -p jcode-tui --lib fails to compile due to 35+ pre-
  existing errors in ui_tests/basic/body_cache.rs, session_picker/
  loading.rs, info_widget_model.rs — these exist on master and are
  unrelated to this change. cargo check + cargo build --release pass.
Matches the opencode CLI pattern where 'opencode auth' is an alias for
'opencode providers'. Both forms now work:

- /provider ≡ /auth
- /provider status ≡ /auth status
- /provider login openai ≡ /auth login openai
- /providers ≡ /auth list
- /providers login openai ≡ /auth login openai

Also updates tab-completion to suggest /auth subcommands when the user
types /provider, /providers, or /auth followed by space, and mentions
the aliases in the /auth help markdown.
Previously /auth login fell through to the '/auth <provider>' branch,
which tried to resolve 'login' as a provider name and printed
'Unknown provider login. Use: ...' instead of opening the picker.

The /login shortcut already worked because handle_auth_command checks
'/login' first, but /auth login and /provider login did not. Added a
short-circuit for '/auth login' to call show_interactive_login().

After the /provider alias was added, /provider login hit the same
broken path; this fix resolves it too.
Opencode TUI uses 'slashName: connect' on the provider.connect command
(see packages/tui/src/app.tsx in the opencode repo) to expose the
provider authentication flow as a slash command. The earlier jcode
'/login' alias is kept for backwards compatibility.

Reverts the earlier attempts at /auth status/refresh/credentials/help/
switch-provider subcommands and the /provider /providers alias — they
don't match the opencode TUI surface, which uses /connect, /models,
/sessions, /new, /status, /themes, /help, /exit as the canonical
slash commands.
Surfaces the /connect command (added in e4ff8f5 to match the opencode
TUI 'slashName: connect' on the provider.connect command) in the
/help-style RegisteredCommand list so users see it in tab completion.
After adding the /connect slash (e4ff8f5 + cc82b68), onboarding error
messages and the input_help block still pointed users at /login. Updates
the user-facing copy so /connect is shown as the canonical opencode-style
slash and /login is documented as a backwards-compatibility alias.

Files touched:
- crates/jcode-tui/src/tui/app/onboarding_flow_control.rs (4 messages)
- crates/jcode-tui/src/tui/app/input_help.rs (auth/login help block)

No code path changes; /login, /logout, /auth all still work as before.
Skipped JCODE_AUTH_CONTENT env override for now — touches the secrets
layer and is too risky to bolt on without a focused PR.
- /connect is now the canonical slash for opening the provider login
  picker (matches opencode 'slashName: "connect"' on the
  provider.connect command).
- /auth (no args) still shows auth status.
- /auth <provider> still routes to login picker.
- /auth logout opens the interactive logout picker.
- /login is no longer a recognized slash (was a duplicate of /connect);
  autocomplete no longer shows it.
- /logout still works as a shortcut for the interactive logout picker
  (back-compat for muscle memory and tests).
- /login <provider> still routes to provider picker (back-compat).
- Removed the [DEBUG-CONNECT] / [DEBUG-TERM-KEY] / [DEBUG-KEY-EVENT]
  instrumentation that was added while investigating why /connect
  appeared unresponsive. The instrumentation never logged because
  key events were not reaching apply_terminal_event in the test
  environment, suggesting the issue was terminal/wezterm pane focus
  rather than jcode routing. The /connect path was already correct
  in source.
These were instrumentation added while investigating the /connect
'apparent non-response' report. They never logged anything because
key events weren't reaching apply_terminal_event in the test
environment — the routing was already correct in source.
…/auth)

User feedback: too many overlapping auth slashes (login/logout/auth) made
the UX confusing. Opencode TUI uses a single canonical slash: /connect
(see packages/tui/src/app.tsx slashName: 'connect' on provider.connect).
This commit removes all the alternative auth slashes:

- /connect (no args) -> open interactive provider login picker
- /connect <provider> -> start login flow for that provider

All other auth slashes (/login, /logout, /auth, /auth <x>, /auth doctor,
/auth logout) are removed from the TUI router and the autocomplete
list. Users who type /login or /auth will get the regular 'unknown
command' fallback rather than a silent alias, which is the desired
behaviour for a strict unification.

handle_auth_command in crates/jcode-tui/src/tui/app/auth_account_commands.rs
is reduced from ~90 lines to 28 lines, and RegisteredCommand::public
in state_ui_input_helpers.rs now only lists /connect.

This commit does NOT touch tests, onboarding error messages, or
help text. Many tests and onboarding flows still reference the old
slashes; fixing those is left as a follow-up.
handle_export_command was structured so that any input which was not
'/export' or '/export<space>...' (including '/connect', '/login',
'/logout', '/auth', and any other slash) fell into an else branch that
pushed a 'Usage: /export ...' hint and returned true. That made
handle_export_command swallow every unrelated command, including
'/connect', and is why the user saw the export usage message when
typing '/connect' and pressing Enter.

Fix: return false at the top of handle_export_command for any input
that does not start with '/export'. Bare '/export' still shows the
usage hint, '/export <arg>' still runs the export. All other commands
fall through to the next handler.

This is the same category of bug as
7c8735e (revert 'fix /auth login') was meant to fix; the
export handler was missed.
…in UX)

Brings /connect up to feature parity with the old /login flow:
- /connect (no args) -> open interactive provider login picker
- /connect <space> -> show provider autocomplete list (no Enter
  required; just arrow + Tab + Enter to select)
- /connect <provider> -> start login flow for that provider

This restores the convenience UX the user remembered from /login:
typing '/connect ' (with a trailing space) immediately surfaces the
list of configured providers so the user can pick one without
needing to first trigger the picker and then drill down.
…mplete

Typing exactly '/connect' (no trailing space) now auto-appends a
space so the provider autocomplete list appears without the user
having to type space. This is the closest UX equivalent to the old
'/login' behaviour the user wanted, without resorting to
'clear-on-match' (which would break the ability to type a provider
name after the slash).

Flow:
- '/connect' -> input becomes '/connect ', autocomplete list appears
- '/connect o' -> user types 'o', input is '/connect o', autocomplete
  filters to providers starting with 'o' (openai, opencode, etc.)
- '/connect openai' + Enter -> start openai login flow

No Enter needed to summon the picker; no text is lost.
After typing exactly '/connect' (8 chars), the interactive provider
login picker is opened immediately, no Enter required. The input
field is left untouched, so the user can keep typing a provider
name to refine the target ('/connect openai' + Enter starts the
openai flow).

This matches the opencode TUI slash behaviour where '/connect'
summons the provider dialog on demand.

Precedence: 'Enter' is not consumed; the user can still press Enter
on '/connect' to also open the picker. The only behaviour change is
that the picker is opened earlier (on the 8th keystroke) so the
user does not need to type a second action.
The previous commit (57344da) auto-opened the provider login picker
on the 8th keystroke of '/connect'. User feedback: the auto-open
fires at a moment that conflicts with typing flow (e.g. when
appending a provider name, or when retracting a slash), so it
introduces a bug rather than removing one.

Reverting to the prior behaviour: '/connect' is rendered as
ordinary input, and Enter submits it (which then opens the picker
and clears the input via the normal submit path). The provider
autocomplete on '/connect ' (with trailing space) is unaffected
and still works.
The previous commit (0178d12) auto-appended a space when the user
typed exactly '/connect' (8 chars), so the provider autocomplete
list would appear without an extra keystroke. User feedback: the
auto-append interferes with normal typing (e.g. when the user
finishes '/connect' and immediately presses Backspace to retype
something else, or when typing a provider name in a single
gesture), so the 'convenience' is more annoying than helpful.

Reverting to the prior behaviour: '/connect' is plain text. The
user types a space to summon the provider autocomplete list (which
already works), or presses Enter to open the picker directly.
…nect

Strict unification to /connect (introduced in 4183376) removed the
slash handlers, but the autocomplete suggestions and onboarding
'log in to get started' prompt still pointed users at /login and
/auth. This commit updates them so the UX surfaces only /connect:

- Onboarding empty-state suggestion '/login' -> '/connect'
  (label: 'Connect to a provider').
- Empty-input command suggestion list drops '| /login' and
  '| /auth' and adds '| /connect'.

The slash handlers themselves still live in auth_account_commands.rs
(only /connect is functional); the autocomplete changes are pure
UX copy. No code path changes.
Opencode TUI uses 'slashName: exit' for the exit command (see
packages/tui/src/app.tsx). jcode has had /quit only. Adds /exit as
the canonical slash and keeps /quit as a back-compat alias:

- /exit -> trigger graceful shutdown (set should_quit, end
  telemetry session)
- /quit -> same behaviour (back-compat)

The slash is registered in:
- crates/jcode-tui/src/tui/app/state_ui_input_helpers.rs
  (RegisteredCommand::public, autocomplete)
- crates/jcode-tui/src/tui/app/remote/key_handling.rs
  (slash handler, mirrors the existing /quit branch)
- crates/jcode-tui/src/tui/ui_overlays.rs
  (help overlay entry)

No new code paths; just the slash surface.
- Delete crates/asupersync-patched/ (vendored asupersync copy, never used)
- Remove commented-out patch block from root Cargo.toml
- Clean asupersync-patched entries from budget tracking JSONs
Updates mempalace-core from the vendored 0.4.0 (commit eb49365) to
0.6.5 (commit 6a3d2db) on the upstream main branch. Includes 20+
upstream commits covering:

- collection isolation
- FTS5 search alias fix
- lesson_save schema changes (issue #98)
- Linux musl release build fixes
- ci fmt cleanup
- issues #96, #97, #98

Build verified: cargo check + cargo build --release both pass.
cargo fmt --check flagged one whitespace-only change in
crates/jcode-tui/src/tui/app/auth_account_commands.rs (a stray
blank line after a function body). Applied cargo fmt; the
.jcode/state/modes.toml change is just a runtime state file.
@quangdang46 quangdang46 merged commit 636fd22 into master Jun 21, 2026
5 of 10 checks passed
@quangdang46 quangdang46 deleted the feature-planning branch June 21, 2026 17:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants