Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,37 @@ A **bext Cloud** view in Dev mode integrates the hosted control plane at [cloud.

Each SSH connection has a **bext** hover action that opens the Instance tab for that box. (v1 targets the local loopback `http://127.0.0.1` — managing a remote box over an SSH tunnel is the next step.) Palette: **bext Cloud : se connecter / ouvrir**.

## Use Codex or Claude Code with a subscription

In **Settings → AI**, choose **Codex CLI (ChatGPT subscription)** or
**Claude Code CLI (subscription)**. These backends reuse the provider CLI's
local login and do not require a ShellDeck API key.

Install the CLI and sign in on the computer where it will run:

```sh
# Codex: sign in with your eligible ChatGPT account
codex login
codex login status

# Claude Code: sign in with your eligible Claude subscription
claude auth login --claudeai
claude auth status
```

Settings shows the matching setup command, a copy button, the official sign-in
guide, and an explicit connection test. The **Agents** empty state shows the same
setup for its selected provider. For an SSH target, install and sign in on that
host; ShellDeck does not forward this computer's provider credentials.

The CLI determines authentication and billing. Existing API credentials in its
configuration or environment can select API usage instead of the subscription;
check the active login and remove that configuration when using your plan.
Subscription limits still apply. The separate **OpenAI API** and **Anthropic
API** backends continue to use OS-keychain API keys.
See [Codex authentication](https://developers.openai.com/codex/auth) and
[Claude Code authentication](https://code.claude.com/docs/en/authentication).

## Keyboard Shortcuts

Use `Ctrl` on Linux/Windows and `Cmd` on macOS unless noted otherwise.
Expand Down
12 changes: 12 additions & 0 deletions crates/shelldeck-core/locales/en.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2257,3 +2257,15 @@ bext.error.instance_not_found = "Resource or SDK route not found on the Bext ins
bext.error.instance_server = "The Bext instance hit an error. Check its logs, then try again."
bext.error.instance_bad_response = "Unexpected response from the Bext instance. Details are in the logs."
bext.error.instance_other = "The request to the Bext instance failed. Details are in the logs."

# Subscription-backed local CLI setup
settings.ai.backend.claude_cli = "Claude Code CLI (subscription)"
settings.ai.backend.codex_cli = "Codex CLI (ChatGPT subscription)"
ai.subscription.claude = "Use your eligible Claude subscription through Claude Code. No API key is required in ShellDeck. Your subscription limits apply."
ai.subscription.codex = "Use your eligible ChatGPT subscription through Codex. No API key is required in ShellDeck. Your subscription limits apply."
ai.subscription.local = "Install the CLI, then run this command in a terminal on this computer and complete sign-in in your browser."
ai.subscription.remote = "Install the CLI and sign in on the selected SSH host. This computer’s login is not forwarded to the remote host."
ai.subscription.copy = "Copy command"
ai.subscription.docs = "Sign-in guide"
ai.subscription.status = "Check the active account: %{command}"
ai.subscription.billing = "ShellDeck reuses the CLI login. An API key configured in the CLI or its environment can select API billing: check the active account and remove that configuration to use your subscription."
12 changes: 12 additions & 0 deletions crates/shelldeck-core/locales/fr.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2257,3 +2257,15 @@ bext.error.instance_not_found = "Ressource ou route SDK introuvable sur l’Inst
bext.error.instance_server = "L’Instance Bext a rencontré une erreur. Consultez ses journaux puis réessayez."
bext.error.instance_bad_response = "Réponse inattendue de l’Instance Bext. Le détail est dans les journaux."
bext.error.instance_other = "La requête vers l’Instance Bext a échoué. Le détail est dans les journaux."

# Subscription-backed local CLI setup
settings.ai.backend.claude_cli = "Claude Code CLI (abonnement)"
settings.ai.backend.codex_cli = "Codex CLI (abonnement ChatGPT)"
ai.subscription.claude = "Utilisez votre abonnement Claude éligible via Claude Code. Aucune clé API à saisir dans ShellDeck. Les limites de votre abonnement s’appliquent."
ai.subscription.codex = "Utilisez votre abonnement ChatGPT éligible via Codex. Aucune clé API à saisir dans ShellDeck. Les limites de votre abonnement s’appliquent."
ai.subscription.local = "Installez la CLI, puis exécutez cette commande dans un terminal sur cet ordinateur et terminez la connexion dans le navigateur."
ai.subscription.remote = "Installez la CLI et connectez-vous sur l’hôte SSH sélectionné. La connexion de cet ordinateur n’est pas transmise à l’hôte distant."
ai.subscription.copy = "Copier la commande"
ai.subscription.docs = "Guide de connexion"
ai.subscription.status = "Vérifier le compte utilisé : %{command}"
ai.subscription.billing = "ShellDeck réutilise la connexion de la CLI. Une clé API configurée dans la CLI ou son environnement peut choisir la facturation API : vérifiez le compte utilisé et retirez cette configuration pour utiliser votre abonnement."
73 changes: 73 additions & 0 deletions crates/shelldeck-core/src/ai.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2339,6 +2339,79 @@ mod tests {
std::fs::remove_file(path).unwrap();
}

// SDTEST-1935 — real provider-owned subscription credentials, no API keys.
#[cfg(unix)]
#[test]
#[ignore = "requires locally signed-in Codex/Claude subscriptions and sends two minimal completions"]
fn live_subscription_clis_complete_without_api_keys() {
const CHILD: &str = "SHELLDECK_SUBSCRIPTION_SMOKE_CHILD";
assert_eq!(
std::env::var("SHELLDECK_LIVE_SUBSCRIPTION").as_deref(),
Ok("1")
);
if std::env::var(CHILD).as_deref() != Ok("1") {
// Never mutate process-wide environment alongside other Rust tests.
// Retain HOME/CODEX_HOME/CLAUDE_CONFIG_DIR: the CLIs own OAuth storage.
let mut child = Command::new(std::env::current_exe().unwrap());
child
.args([
"--exact",
"ai::tests::live_subscription_clis_complete_without_api_keys",
"--ignored",
"--nocapture",
])
.env(CHILD, "1");
for key in [
"OPENAI_API_KEY",
"CODEX_API_KEY",
"ANTHROPIC_API_KEY",
"ANTHROPIC_AUTH_TOKEN",
] {
child.env_remove(key);
}
let result = child.output().unwrap();
assert!(String::from_utf8_lossy(&result.stdout).contains("running 1 test"));
assert!(
result.status.success(),
"subscription smoke failed: {}\n{}",
String::from_utf8_lossy(&result.stdout),
String::from_utf8_lossy(&result.stderr)
);
return;
}
let codex = Command::new("codex")
.args(["login", "status"])
.output()
.unwrap();
assert!(codex.status.success());
assert!(
format!(
"{}{}",
String::from_utf8_lossy(&codex.stdout),
String::from_utf8_lossy(&codex.stderr)
)
.contains("ChatGPT"),
"Codex must be signed in with ChatGPT before this smoke"
);
let claude = Command::new("claude")
.args(["auth", "status"])
.output()
.unwrap();
assert!(claude.status.success());
let status: Value = serde_json::from_slice(&claude.stdout).unwrap();
assert_eq!(status["loggedIn"], true);
assert_eq!(status["authMethod"], "claude.ai");
for backend in [AiBackend::CodexCli, AiBackend::ClaudeCli] {
let config = AiConfig {
enabled: true,
backend,
..AiConfig::default()
};
// create_client must not require ShellDeck's OS-keychain API key.
assert_eq!(test_connection(&config).unwrap().text, "SHELLDECK_AI_OK");
}
}

// SDTEST-1338
#[cfg(unix)]
#[test]
Expand Down
58 changes: 34 additions & 24 deletions crates/shelldeck-ui/src/agent_console_view.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2327,31 +2327,41 @@ impl Render for AgentConsoleView {
.px(if wide { px(24.0) } else { px(12.0) })
.py(px(16.0));
if !has_content {
output = output.child(
div()
.flex()
.flex_col()
.items_center()
.justify_center()
.flex_1()
.gap(px(8.0))
.text_color(ShellDeckColors::text_muted())
.child(lucide_icon("bot", 27.0, ShellDeckColors::primary()))
.child(
div()
.text_size(px(14.0))
.font_weight(FontWeight::SEMIBOLD)
.text_color(ShellDeckColors::text_primary())
.child(t!("agents.empty.title").to_string()),
)
.child(
div()
.max_w(px(520.0))
.text_size(px(11.0))
.text_align(TextAlign::Center)
.child(t!("agents.empty.description").to_string()),
),
let subscription_setup = crate::cli_subscription_setup::render_subscription_setup(
context.provider.binary(),
matches!(context.target, AgentTarget::Ssh { .. }),
);
let mut empty_state = div()
.flex()
.flex_col()
.items_center()
.justify_center()
.gap(px(8.0))
.text_color(ShellDeckColors::text_muted())
.child(lucide_icon("bot", 27.0, ShellDeckColors::primary()))
.child(
div()
.text_size(px(14.0))
.font_weight(FontWeight::SEMIBOLD)
.text_color(ShellDeckColors::text_primary())
.child(t!("agents.empty.title").to_string()),
)
.child(
div()
.max_w(px(520.0))
.text_size(px(11.0))
.text_align(TextAlign::Center)
.child(t!("agents.empty.description").to_string()),
);
if subscription_setup.is_some() {
empty_state = empty_state.flex_shrink_0().py(px(16.0));
} else {
empty_state = empty_state.flex_1();
}
output = output.child(empty_state);
if let Some(setup) = subscription_setup {
output = output.child(div().w_full().max_w(px(520.0)).mx_auto().child(setup));
}
} else {
let mut timeline_view = div()
.w_full()
Expand Down
83 changes: 83 additions & 0 deletions crates/shelldeck-ui/src/cli_subscription_setup.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
use crate::{scale::px, t, theme::ShellDeckColors};
use adabraka_ui::prelude::{Button, ButtonSize, ButtonVariant};
use gpui::{div, prelude::*, ClipboardItem, Div};

/// The provider CLI owns login and credentials, on the host where it runs.
/// Showing setup never starts a login or a model request.
pub(crate) fn render_subscription_setup(command: &str, remote: bool) -> Option<Div> {
let (description, login, status, docs) = match command {
"claude" => (
t!("ai.subscription.claude").to_string(),
"claude auth login --claudeai",
"claude auth status",
"https://code.claude.com/docs/en/authentication",
),
"codex" => (
t!("ai.subscription.codex").to_string(),
"codex login",
"codex login status",
"https://developers.openai.com/codex/auth",
),
_ => return None,
};
let location = if remote {
t!("ai.subscription.remote")
} else {
t!("ai.subscription.local")
};
Some(
div()
.flex()
.flex_col()
.flex_shrink_0()
.w_full()
.min_w(px(0.0))
.gap(px(8.0))
.p(px(12.0))
.rounded_md()
.border_1()
.border_color(ShellDeckColors::border())
.text_size(px(12.0))
.text_color(ShellDeckColors::text_primary())
.child(description)
.child(location.to_string())
.child(
div()
.flex()
.flex_wrap()
.items_center()
.gap(px(8.0))
.child(div().font_family("JetBrains Mono").child(login))
.child(
Button::new(
"subscription-copy-login",
t!("ai.subscription.copy").to_string(),
)
.size(ButtonSize::Sm)
.variant(ButtonVariant::Outline)
.on_click(move |_, _, cx| {
cx.write_to_clipboard(ClipboardItem::new_string(login.to_string()));
}),
)
.child(
Button::new(
"subscription-login-docs",
t!("ai.subscription.docs").to_string(),
)
.size(ButtonSize::Sm)
.variant(ButtonVariant::Ghost)
.on_click(move |_, _, cx| cx.open_url(docs)),
),
)
.child(
div()
.text_color(ShellDeckColors::text_muted())
.child(t!("ai.subscription.status", command = status).to_string()),
)
.child(
div()
.text_color(ShellDeckColors::text_muted())
.child(t!("ai.subscription.billing").to_string()),
),
)
}
1 change: 1 addition & 0 deletions crates/shelldeck-ui/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ pub mod ai_workflow;
pub mod attachment_annotator;
pub mod bext_cloud_view;
pub mod brand;
mod cli_subscription_setup;
pub mod command_palette;
pub mod command_palette_window;
pub mod connection_combobox;
Expand Down
18 changes: 15 additions & 3 deletions crates/shelldeck-ui/src/settings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1455,7 +1455,7 @@ impl SettingsView {
.child(Self::render_setting_row(
t!("settings.ai.backend.label").as_ref(),
t!("settings.ai.backend.description").as_ref(),
div().w(px(220.0)).child(self.ai_backend_select.clone()),
div().w(px(300.0)).child(self.ai_backend_select.clone()),
))
.child(Self::render_setting_row(
t!("settings.ai.status.label").as_ref(),
Expand All @@ -1481,6 +1481,12 @@ impl SettingsView {
div().w(px(220.0)).child(model_input),
));

if let Some(setup) = backend.cli_command().and_then(|command| {
crate::cli_subscription_setup::render_subscription_setup(command, false)
}) {
root = root.child(setup);
}

if let Some(provider) = backend.provider_key() {
let key_state = self.ai_api_key_state.clone();
let provider = provider.to_string();
Expand Down Expand Up @@ -3098,8 +3104,14 @@ fn build_ai_backend_select(
AiBackend::Disabled,
t!("settings.ai.backend.disabled").to_string(),
),
(AiBackend::ClaudeCli, "Claude Code CLI".to_string()),
(AiBackend::CodexCli, "Codex CLI".to_string()),
(
AiBackend::ClaudeCli,
t!("settings.ai.backend.claude_cli").to_string(),
),
(
AiBackend::CodexCli,
t!("settings.ai.backend.codex_cli").to_string(),
),
(AiBackend::AiderCli, "Aider CLI".to_string()),
(AiBackend::AutomoniqueAcp, "Automonique ACP".to_string()),
(AiBackend::OpenAi, "OpenAI API".to_string()),
Expand Down
8 changes: 8 additions & 0 deletions docs/design/reliability-test-tracker.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,14 @@ risques par frontière observable et impose une vérification avant correction.

## Journal

- **2026-09-30 — Abonnements Codex et Claude Code vérifiés.** Les clients CLI
réutilisaient déjà l’authentification du fournisseur, sans clé ShellDeck,
mais l’interface n’expliquait pas ce parcours. Réglages et Agents partagent
maintenant le guide connexion/copier/statut et la frontière hôte SSH.
SDTEST-1935 a effectué deux complétions réelles avec login ChatGPT et
Claude.ai/Max après retrait des variables de clés API. Ce test reste opt-in
hors CI ; SDTEST-1936 reste Rouge pour le harnais visuel automatisé.

- **2026-09-30 — Dernier passage, champs secrets.** Le formulaire de connexion
et la clé API démarraient sans masquage : le bouton œil ne configurait pas
le type de l’état. Les deux états sont maintenant initialisés en Password.
Expand Down
12 changes: 11 additions & 1 deletion docs/testing/USE_CASES.md
Original file line number Diff line number Diff line change
Expand Up @@ -880,6 +880,10 @@ The session search has a usable text viewport, and inherited navigator/provider
labels follow the active theme in both light and dark appearances
(SDTEST-1931/1932, manual native validation).

The empty Claude Code/Codex session shows the shared subscription sign-in
guide. SSH targets require their own installed CLI and provider login; local
provider credentials are never forwarded to them.

Dev mode exposes one provider-neutral agent console for Claude Code, Codex,
Automonique ACP, DeepSeek through Jcode, and Jcode's configured/default provider.
Every run names a target-valid working directory (a native absolute path locally,
Expand Down Expand Up @@ -1702,7 +1706,13 @@ the sidebar connection action. Unknown and malformed menu ids are ignored.

Settings → IA selects one local CLI (Claude Code, Codex, Aider) or API
provider (OpenAI, Anthropic), an optional model, and per-surface opt-outs. API
keys live only in the OS keychain. The assistant affordance remains hidden
keys live only in the OS keychain. Codex and Claude Code CLI backends also
reuse their provider-owned subscription login, without requiring a ShellDeck
API key. Settings labels those options and shows sign-in/copy/status guidance;
API billing remains a separate choice and CLI-configured credentials can
override subscription usage. A gated live smoke checks both subscription
methods before completing with API-key environment variables removed.
The assistant affordance remains hidden
when AI or the current surface is disabled, or when a selected local CLI is
not executable. The explicit connection test sends a real minimal completion
and reports the provider/model result, but does not become a per-launch lock.
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Loading