diff --git a/agent/chat_completion_helpers.py b/agent/chat_completion_helpers.py
index 6c6ba9e12b4..8bab29cae47 100644
--- a/agent/chat_completion_helpers.py
+++ b/agent/chat_completion_helpers.py
@@ -2319,7 +2319,15 @@ def interruptible_streaming_api_call(agent, api_kwargs: dict, *, on_first_delta=
_fire_first_delta()
agent._fire_reasoning_delta(thinking_text)
- # Return the native Anthropic Message for downstream processing
+ # Return the native Anthropic Message for downstream processing.
+ # If the stream was interrupted (the event loop broke out above on
+ # agent._interrupt_requested), do NOT call get_final_message() — on
+ # a partially-consumed stream the SDK may hang draining remaining
+ # events or return a Message with incomplete tool_use blocks (partial
+ # JSON in `input`). The outer poll loop raises InterruptedError, so
+ # this return value is discarded anyway.
+ if agent._interrupt_requested:
+ return None
return stream.get_final_message()
def _call():
diff --git a/agent/redact.py b/agent/redact.py
index 43fe046b4de..c69003fcf66 100644
--- a/agent/redact.py
+++ b/agent/redact.py
@@ -222,6 +222,28 @@ _DB_CONNSTR_RE = re.compile(
re.IGNORECASE,
)
+# Bare-token credential in a web/transport URL: ``scheme://TOKEN@host``.
+# This is the ``git remote set-url origin https://PASSWORD@github.com/...``
+# shape from issue #6396 — a single opaque credential in the userinfo position
+# with NO ``user:pass`` colon. It is unambiguously a secret: legitimate
+# round-trip URLs (OAuth callbacks, magic links, pre-signed shares — see the
+# "Web-URL redaction is intentionally OFF" note in redact_sensitive_text) carry
+# their tokens in the QUERY STRING, never in bare userinfo. The colon form
+# ``user:pass@`` is deliberately left to pass through (commit "pass web URLs
+# through unchanged", #34029) and is NOT matched here — the token class forbids
+# ``:``. DB schemes are handled by _DB_CONNSTR_RE above and excluded here.
+#
+# Guards against false positives:
+# - 8+ char floor skips short usernames (git, admin, root, deploy, ubuntu).
+# - The token class ``[^\s:@/]`` cannot cross ``/``, so an ``@`` sitting in a
+# path or query (e.g. ``?q=user@example.com``) is never treated as userinfo.
+_URL_BARE_TOKEN_RE = re.compile(
+ r"((?:https?|wss?|git|ssh|ftp|ftps|sftp)://)" # scheme
+ r"([^\s:@/]{8,})" # bare token (no colon/slash/@), 8+ chars
+ r"(@[^\s]+)", # @host...
+ re.IGNORECASE,
+)
+
# JWT tokens: header.payload[.signature] — always start with "eyJ" (base64 for "{")
# Matches 1-part (header only), 2-part (header.payload), and full 3-part JWTs.
_JWT_RE = re.compile(
@@ -564,6 +586,16 @@ def redact_sensitive_text(
else:
text = _DB_CONNSTR_RE.sub(lambda m: f"{m.group(1)}***{m.group(3)}", text)
+ # Bare-token userinfo in web/transport URLs: ``scheme://TOKEN@host``.
+ # The git-remote-with-embedded-password shape from #6396. Only the
+ # colon-less bare-token form is redacted — ``user:pass@`` and
+ # query-string tokens are left to pass through (see the web-URL note
+ # below). See _URL_BARE_TOKEN_RE for the false-positive guards.
+ text = _URL_BARE_TOKEN_RE.sub(
+ lambda m: f"{m.group(1)}{_mask_token(m.group(2))}{m.group(3)}",
+ text,
+ )
+
# JWT tokens (eyJ... — base64-encoded JSON headers)
if "eyJ" in text:
text = _JWT_RE.sub(lambda m: _mask_token(m.group(0)), text)
@@ -575,7 +607,12 @@ def redact_sensitive_text(
# blanket-redacting param values by name breaks those skills mid-flow.
# Known credential shapes (sk-, ghp_, JWTs, etc.) inside URLs are still
# caught by _PREFIX_RE and _JWT_RE above. DB connection-string passwords
- # are still caught by _DB_CONNSTR_RE.
+ # are still caught by _DB_CONNSTR_RE. The ONE userinfo case still redacted
+ # is the colon-less bare-token form ``scheme://TOKEN@host`` (#6396, handled
+ # by _URL_BARE_TOKEN_RE in the ``://`` block above): a bare credential in
+ # userinfo is never a round-trip workflow token (those live in the query
+ # string), so masking it can't break a skill. The ``user:pass@`` form is
+ # left to pass through per #34029.
# Form-urlencoded bodies (only triggers on clean k=v&k=v inputs).
if "&" in text and "=" in text:
diff --git a/apps/desktop/src/app/agents/index.tsx b/apps/desktop/src/app/agents/index.tsx
index 8f6c2349f83..fd13758599b 100644
--- a/apps/desktop/src/app/agents/index.tsx
+++ b/apps/desktop/src/app/agents/index.tsx
@@ -19,7 +19,7 @@ import {
type SubagentStreamEntry
} from '@/store/subagents'
-import { OverlayView } from '../overlays/overlay-view'
+import { Panel, PanelEmpty, PanelHeader } from '../overlays/panel'
// Mirrors statusGlyph() in tool-fallback.tsx so subagent rows speak the
// same visual vocabulary as the chat tool blocks.
@@ -86,18 +86,16 @@ export function AgentsView({ onClose }: AgentsViewProps) {
const tree = useMemo(() => buildSubagentTree(allSubagents(subagentsBySession)), [subagentsBySession])
return (
-
-
-