fix: worker credential setup

This commit is contained in:
Timothy
2026-02-18 21:50:18 -08:00
parent af7a4ff4e8
commit f7268a44d9
4 changed files with 202 additions and 4 deletions
+53 -3
View File
@@ -369,9 +369,9 @@ class AdenTUI(App):
self._runner = runner
self.runtime = runner._agent_runtime
except CredentialError as e:
except CredentialError:
self.status_bar.set_graph_id("")
self.notify(f"Credential error: {e}", severity="error", timeout=10)
self._show_credential_setup(str(agent_path))
return
except Exception as e:
self.status_bar.set_graph_id("")
@@ -388,6 +388,47 @@ class AdenTUI(App):
self.notify(f"Agent loaded: {agent_name}", severity="information", timeout=3)
def _show_credential_setup(
self,
agent_path: str,
on_cancel: object | None = None,
) -> None:
"""Show the credential setup screen for an agent with missing credentials.
Args:
agent_path: Path to the agent that needs credentials.
on_cancel: Callable to invoke if the user skips/cancels setup.
"""
from framework.credentials.setup import CredentialSetupSession
from framework.tui.screens.credential_setup import CredentialSetupScreen
session = CredentialSetupSession.from_agent_path(agent_path)
if not session.missing:
self.notify(
"Credential error but no missing credentials detected",
severity="error",
timeout=10,
)
if callable(on_cancel):
on_cancel()
return
def _on_result(result: bool | None) -> None:
if result is True:
# Credentials saved — retry loading the agent
self._do_load_agent(agent_path)
else:
self.notify(
"Credential setup skipped. Agent not loaded.",
severity="warning",
timeout=5,
)
if callable(on_cancel):
on_cancel()
self.push_screen(CredentialSetupScreen(session), callback=_on_result)
# -- Agent picker --
def _show_agent_picker_initial(self) -> None:
@@ -503,7 +544,16 @@ class AdenTUI(App):
self._runner = runner
self.runtime = coder_runtime
except (CredentialError, Exception) as e:
except CredentialError:
self.status_bar.set_graph_id("")
coder_path = str(hive_coder_path)
self.call_from_thread(
self._show_credential_setup,
coder_path,
self._restore_from_escalation_stack,
)
return
except Exception as e:
self.status_bar.set_graph_id("")
self.notify(f"Failed to load coder: {e}", severity="error", timeout=10)
self._restore_from_escalation_stack()
@@ -0,0 +1,141 @@
"""Credential setup ModalScreen for configuring missing agent credentials."""
from __future__ import annotations
from textual.app import ComposeResult
from textual.binding import Binding
from textual.containers import Vertical, VerticalScroll
from textual.screen import ModalScreen
from textual.widgets import Button, Input, Label
from framework.credentials.setup import CredentialSetupSession, MissingCredential
class CredentialSetupScreen(ModalScreen[bool | None]):
"""Modal screen for configuring missing agent credentials.
Shows a form with one password Input per missing credential.
Returns True on successful save, or None on cancel/skip.
"""
BINDINGS = [
Binding("escape", "dismiss_setup", "Cancel"),
]
DEFAULT_CSS = """
CredentialSetupScreen {
align: center middle;
}
#cred-container {
width: 80%;
max-width: 100;
height: 80%;
background: $surface;
border: heavy $primary;
padding: 1 2;
}
#cred-title {
text-align: center;
text-style: bold;
width: 100%;
color: $text;
}
#cred-subtitle {
text-align: center;
width: 100%;
margin-bottom: 1;
}
#cred-scroll {
height: 1fr;
}
.cred-entry {
margin-bottom: 1;
padding: 1;
background: $panel;
height: auto;
}
.cred-entry Input {
margin-top: 1;
}
.cred-buttons {
height: auto;
margin-top: 1;
align: center middle;
}
.cred-buttons Button {
margin: 0 1;
}
#cred-footer {
text-align: center;
width: 100%;
margin-top: 1;
}
"""
def __init__(self, session: CredentialSetupSession) -> None:
super().__init__()
self._session = session
self._missing: list[MissingCredential] = session.missing
def compose(self) -> ComposeResult:
n = len(self._missing)
with Vertical(id="cred-container"):
yield Label("Credential Setup", id="cred-title")
yield Label(
f"[dim]{n} credential{'s' if n != 1 else ''} needed to run this agent[/dim]",
id="cred-subtitle",
)
with VerticalScroll(id="cred-scroll"):
for i, cred in enumerate(self._missing):
with Vertical(classes="cred-entry"):
yield Label(f"[bold]{cred.env_var}[/bold]")
affected = cred.tools or cred.node_types
if affected:
yield Label(f"[dim]Required by: {', '.join(affected)}[/dim]")
if cred.description:
yield Label(f"[dim]{cred.description}[/dim]")
if cred.help_url:
yield Label(f"[cyan]Get key:[/cyan] {cred.help_url}")
yield Input(
placeholder="Paste API key...",
password=True,
id=f"key-{i}",
)
with Vertical(classes="cred-buttons"):
yield Button("Save & Continue", variant="primary", id="btn-save")
yield Button("Skip", variant="default", id="btn-skip")
yield Label(
"[dim]Enter[/dim] Submit [dim]Esc[/dim] Cancel",
id="cred-footer",
)
def on_button_pressed(self, event: Button.Pressed) -> None:
if event.button.id == "btn-save":
self._save_credentials()
elif event.button.id == "btn-skip":
self.dismiss(None)
def _save_credentials(self) -> None:
"""Collect inputs, store credentials, and dismiss."""
# Init encryption key (generates one if missing)
self._session._ensure_credential_key()
configured = 0
for i, cred in enumerate(self._missing):
input_widget = self.query_one(f"#key-{i}", Input)
value = input_widget.value.strip()
if not value:
continue
try:
self._session._store_credential(cred, value)
configured += 1
except Exception as e:
self.notify(f"Error storing {cred.env_var}: {e}", severity="error")
if configured > 0:
self.dismiss(True)
else:
self.notify("No credentials entered", severity="warning", timeout=3)
def action_dismiss_setup(self) -> None:
self.dismiss(None)
+7
View File
@@ -966,6 +966,10 @@ class ChatRepl(Vertical):
stream_log = self.query_one("#streaming-output", RichLog)
stream_log.clear()
stream_log.display = False
# Hiding the streaming pane makes chat-history taller (1fr reclaims
# the space). Re-scroll so subsequent _write_history calls see
# is_vertical_scroll_end == True.
self.query_one("#chat-history", RichLog).scroll_end(animate=False)
def flush_streaming(self) -> None:
"""Flush any accumulated streaming text to history.
@@ -1236,6 +1240,9 @@ class ChatRepl(Vertical):
stream_log = self.query_one("#streaming-output", RichLog)
if not stream_log.display:
stream_log.display = True
# Showing the streaming pane shrinks chat-history (height: 1fr).
# Re-scroll so _write_history still sees is_vertical_scroll_end.
self.query_one("#chat-history", RichLog).scroll_end(animate=False)
# Rewrite the full snapshot as a single block so text wraps
# naturally instead of one token per line.
Generated
+1 -1
View File
@@ -766,7 +766,7 @@ wheels = [
[[package]]
name = "framework"
version = "0.4.2"
version = "0.5.1"
source = { editable = "core" }
dependencies = [
{ name = "anthropic" },