fix: worker credential setup
This commit is contained in:
@@ -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)
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user