Skip to content
Open
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
25 changes: 25 additions & 0 deletions CodingAgentExplorer/Proxy/CaptureTransformProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,18 @@ private static async Task HandleSseStreamingAsync(
foreach (var header in proxyResponse.Headers
.Where(header => !clientResponse.Headers.ContainsKey(header.Key)))
{
if (IsHopByHopHeader(header.Key))
continue;

clientResponse.Headers[header.Key] = header.Value.ToArray();
}

foreach (var header in proxyResponse.Content.Headers
.Where(header => !clientResponse.Headers.ContainsKey(header.Key)))
{
if (IsHopByHopHeader(header.Key))
continue;

clientResponse.Headers[header.Key] = header.Value.ToArray();
}

Expand Down Expand Up @@ -248,6 +260,19 @@ private static async Task HandleSseStreamingAsync(
proxiedRequest.DurationMs = stopwatch.Elapsed.TotalMilliseconds;
}

private static bool IsHopByHopHeader(string headerName)
{
return headerName.Equals("Connection", StringComparison.OrdinalIgnoreCase)
|| headerName.Equals("Keep-Alive", StringComparison.OrdinalIgnoreCase)
|| headerName.Equals("Proxy-Authenticate", StringComparison.OrdinalIgnoreCase)
|| headerName.Equals("Proxy-Authorization", StringComparison.OrdinalIgnoreCase)
|| headerName.Equals("TE", StringComparison.OrdinalIgnoreCase)
|| headerName.Equals("Trailer", StringComparison.OrdinalIgnoreCase)
|| headerName.Equals("Transfer-Encoding", StringComparison.OrdinalIgnoreCase)
|| headerName.Equals("Upgrade", StringComparison.OrdinalIgnoreCase)
|| headerName.Equals("Content-Length", StringComparison.OrdinalIgnoreCase);
}

private static async Task HandleNonStreamingResponseAsync(
HttpResponseMessage proxyResponse, ProxiedRequest proxiedRequest, Stopwatch stopwatch,
bool isMcp = false)
Expand Down
37 changes: 35 additions & 2 deletions CodingAgentExplorer/Proxy/DynamicProxyConfigProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,10 @@

namespace CodingAgentExplorer.Proxy;

public class DynamicProxyConfigProvider(McpProxyConfig mcpConfig) : IProxyConfigProvider
public class DynamicProxyConfigProvider(
McpProxyConfig mcpConfig,
IConfiguration configuration,
ILogger<DynamicProxyConfigProvider> logger) : IProxyConfigProvider
{
private static readonly ForwarderRequestConfig LongTimeout = new()
{
Expand All @@ -15,6 +18,8 @@ public class DynamicProxyConfigProvider(McpProxyConfig mcpConfig) : IProxyConfig

public IProxyConfig GetConfig()
{
var anthropicDestination = ResolveAnthropicDestination(configuration, logger);

var routes = new List<RouteConfig>
{
new()
Expand All @@ -36,7 +41,7 @@ public IProxyConfig GetConfig()
ClusterId = "anthropic-cluster",
Destinations = new Dictionary<string, DestinationConfig>
{
["dest"] = new() { Address = "https://api.anthropic.com" }
["dest"] = new() { Address = anthropicDestination }
},
HttpRequest = LongTimeout
}
Expand Down Expand Up @@ -75,6 +80,34 @@ public IProxyConfig GetConfig()

return new InMemoryProxyConfig(routes, clusters, mcpConfig.GetChangeToken());
}

private static string ResolveAnthropicDestination(
IConfiguration configuration,
ILogger logger)
{
// Allow env var override for convenience in local setups.
var configured = Environment.GetEnvironmentVariable("CODING_AGENT_EXPLORER_UPSTREAM_URL");

if (string.IsNullOrWhiteSpace(configured))
{
configured = configuration
.GetSection("ReverseProxy:Clusters:anthropic-cluster:Destinations")
.GetChildren()
.Select(d => d["Address"])
.FirstOrDefault(a => !string.IsNullOrWhiteSpace(a));
}

if (!string.IsNullOrWhiteSpace(configured)
&& Uri.TryCreate(configured, UriKind.Absolute, out var uri)
&& (uri.Scheme == Uri.UriSchemeHttp || uri.Scheme == Uri.UriSchemeHttps))
{
return uri.GetLeftPart(UriPartial.Authority);
}

logger.LogWarning(
"No valid upstream destination configured. Falling back to https://api.anthropic.com");
return "https://api.anthropic.com";
}
}

internal sealed class InMemoryProxyConfig(
Expand Down
2 changes: 1 addition & 1 deletion CodingAgentExplorer/appsettings.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
"anthropic-cluster": {
"Destinations": {
"anthropic-api": {
"Address": "https://api.anthropic.com"
"Address": "http://localhost:1234"
}
},
"HttpRequest": {
Expand Down
4 changes: 4 additions & 0 deletions CodingAgentExplorer/wwwroot/conversation/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Coding Agent Explorer - Conversation View</title>
<link rel="stylesheet" href="../css/theme.css">
<link rel="stylesheet" href="../css/conversation.css">
<script src="../js/theme.js"></script>
</head>
<body>
<header>
Expand All @@ -23,6 +25,8 @@ <h1>Conversation View</h1>
<div class="status">
<button id="clearBtn" class="clear-btn">Clear</button>
<span class="separator">|</span>
<button id="themeToggle" class="theme-toggle-btn" title="Toggle light/dark mode" onclick="toggleTheme()"><span class="theme-icon">🌙</span></button>
<span class="separator">|</span>
<label class="hook-events-toggle">
<input type="checkbox" id="showHookEvents">
Hook Events
Expand Down
44 changes: 15 additions & 29 deletions CodingAgentExplorer/wwwroot/css/conversation.css
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,6 @@
padding: 0;
}

:root {
--bg: #0d1117;
--bg-secondary: #161b22;
--bg-tertiary: #21262d;
--border: #30363d;
--text: #e6edf3;
--text-secondary: #8b949e;
--accent: #58a6ff;
--green: #3fb950;
--red: #f85149;
--orange: #d29922;
--purple: #bc8cff;
}

body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif;
background: var(--bg);
Expand Down Expand Up @@ -204,7 +190,7 @@ main {
line-height: 1.8;
white-space: nowrap;
z-index: 100;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4);
box-shadow: 0 4px 12px var(--shadow);
color: var(--text-secondary);
}

Expand Down Expand Up @@ -521,15 +507,15 @@ main {
align-items: center;
gap: 6px;
padding: 6px 10px;
background: rgba(188, 140, 255, 0.1);
background: var(--purple-subtle);
font-size: 12px;
font-weight: 600;
color: var(--purple);
cursor: pointer;
}

.content-tool-use .tool-header:hover {
background: rgba(188, 140, 255, 0.15);
background: var(--purple-muted);
}

.content-tool-use .tool-header .arrow {
Expand Down Expand Up @@ -570,15 +556,15 @@ main {
align-items: center;
gap: 6px;
padding: 6px 10px;
background: rgba(63, 185, 80, 0.1);
background: var(--green-subtle);
font-size: 12px;
font-weight: 600;
color: var(--green);
cursor: pointer;
}

.content-tool-result .tool-result-header:hover {
background: rgba(63, 185, 80, 0.15);
background: var(--green-muted);
}

.content-tool-result .tool-result-header .arrow {
Expand Down Expand Up @@ -739,32 +725,32 @@ main {
text-transform: uppercase;
padding: 2px 7px;
border-radius: 10px;
background: rgba(210, 153, 34, 0.15);
background: var(--orange-subtle);
color: var(--orange);
border: 1px solid rgba(210, 153, 34, 0.3);
border: 1px solid var(--orange-border);
white-space: nowrap;
}

/* Per-event color overrides */
.hook-badge-sessionstart,
.hook-badge-sessionend {
background: rgba(88, 166, 255, 0.12);
background: var(--accent-subtle);
color: var(--accent);
border-color: rgba(88, 166, 255, 0.3);
border-color: var(--accent-border);
}

.hook-badge-stop {
background: rgba(248, 81, 73, 0.12);
background: var(--red-subtle);
color: var(--red);
border-color: rgba(248, 81, 73, 0.3);
border-color: var(--red-border);
}

.hook-badge-taskcompleted,
.hook-badge-subagentstart,
.hook-badge-subagentstopp {
background: rgba(63, 185, 80, 0.12);
background: var(--green-subtle);
color: var(--green);
border-color: rgba(63, 185, 80, 0.3);
border-color: var(--green-border);
}

.hook-meta {
Expand Down Expand Up @@ -850,8 +836,8 @@ main {
.hook-stdout-value {
margin: 0;
padding: 6px 10px;
background: rgba(63, 185, 80, 0.07);
border: 1px solid rgba(63, 185, 80, 0.25);
background: var(--green-faint);
border: 1px solid var(--green-border);
border-left: 3px solid var(--green);
border-radius: 4px;
font-family: 'Cascadia Code', 'Fira Code', Consolas, monospace;
Expand Down
20 changes: 3 additions & 17 deletions CodingAgentExplorer/wwwroot/css/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,6 @@
padding: 0;
}

:root {
--bg: #0d1117;
--bg-secondary: #161b22;
--bg-tertiary: #21262d;
--border: #30363d;
--text: #e6edf3;
--text-secondary: #8b949e;
--accent: #58a6ff;
--green: #3fb950;
--red: #f85149;
--orange: #d29922;
--purple: #bc8cff;
}

body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif;
background: var(--bg);
Expand Down Expand Up @@ -171,7 +157,7 @@ main {
}

.request-list tbody tr.selected {
background: rgba(88, 166, 255, 0.1);
background: var(--accent-subtle);
}

.col-time { width: 80px; }
Expand All @@ -194,12 +180,12 @@ main {
}

.badge-stream {
background: rgba(188, 140, 255, 0.2);
background: var(--purple-strong);
color: var(--purple);
}

.badge-sync {
background: rgba(88, 166, 255, 0.2);
background: var(--accent-muted);
color: var(--accent);
}

Expand Down
74 changes: 74 additions & 0 deletions CodingAgentExplorer/wwwroot/css/theme.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
:root {
--bg: #0d1117;
--bg-secondary: #161b22;
--bg-tertiary: #21262d;
--border: #30363d;
--text: #e6edf3;
--text-secondary: #8b949e;
--accent: #58a6ff;
--green: #3fb950;
--red: #f85149;
--orange: #d29922;
--purple: #bc8cff;
--accent-subtle: rgba(88,166,255,0.1);
--accent-muted: rgba(88,166,255,0.2);
--accent-border: rgba(88,166,255,0.3);
--purple-subtle: rgba(188,140,255,0.1);
--purple-muted: rgba(188,140,255,0.15);
--purple-strong: rgba(188,140,255,0.2);
--green-faint: rgba(63,185,80,0.07);
--green-subtle: rgba(63,185,80,0.1);
--green-muted: rgba(63,185,80,0.15);
--green-border: rgba(63,185,80,0.25);
--orange-subtle: rgba(210,153,34,0.15);
--orange-border: rgba(210,153,34,0.3);
--red-subtle: rgba(248,81,73,0.12);
--red-border: rgba(248,81,73,0.3);
--shadow: rgba(0,0,0,0.4);
--overlay-bg: rgba(0,0,0,0.6);
}

[data-theme="light"] {
--bg: #ffffff;
--bg-secondary: #f6f8fa;
--bg-tertiary: #eaeef2;
--border: #d0d7de;
--text: #24292f;
--text-secondary: #57606a;
--accent: #0969da;
--green: #1a7f37;
--red: #cf222e;
--orange: #9a6700;
--purple: #8250df;
--accent-subtle: rgba(9,105,218,0.15);
--accent-muted: rgba(9,105,218,0.25);
--accent-border: rgba(9,105,218,0.4);
--purple-subtle: rgba(130,80,223,0.15);
--purple-muted: rgba(130,80,223,0.22);
--purple-strong: rgba(130,80,223,0.28);
--green-faint: rgba(26,127,55,0.12);
--green-subtle: rgba(26,127,55,0.18);
--green-muted: rgba(26,127,55,0.24);
--green-border: rgba(26,127,55,0.4);
--orange-subtle: rgba(154,103,0,0.22);
--orange-border: rgba(154,103,0,0.4);
--red-subtle: rgba(207,34,46,0.18);
--red-border: rgba(207,34,46,0.4);
--shadow: rgba(0,0,0,0.15);
--overlay-bg: rgba(0,0,0,0.4);
}

.theme-toggle-btn {
background: none;
border: 1px solid var(--border);
border-radius: 4px;
color: var(--text);
cursor: pointer;
font-size: 14px;
padding: 2px 6px;
line-height: 1;
}

.theme-toggle-btn:hover {
background: var(--bg-tertiary);
}
Loading