chore: remove obsolete PR assets

Clean up old PR screenshot/preview files that are no longer needed.
This commit is contained in:
viettranx committed 2026-04-12 19:04:39 +07:00
1 parent ad893908a5
commit 7701058de6
49 files changed
-2495

No files matched your search

Binary file not shown.

Before

Width:  |  Height:  |  Size: 243 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 204 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 241 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 194 KiB

-467
View File
@@ -1,467 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>GoClaw Codex Pool UI Before / After</title>
<style>
:root {
--bg: #120905;
--panel: #1c100b;
--panel-2: #24150f;
--text: #f6eee8;
--muted: #c5afa0;
--line: rgba(255, 220, 196, 0.16);
--accent: #ff7b3d;
--bad: #ff5d5d;
--good: #49d18c;
--shadow: 0 24px 80px rgba(0, 0, 0, 0.35);
}
* {
box-sizing: border-box;
}
body {
margin: 0;
font-family:
Inter,
ui-sans-serif,
system-ui,
-apple-system,
BlinkMacSystemFont,
"Segoe UI",
sans-serif;
color: var(--text);
background:
radial-gradient(circle at top left, rgba(255, 123, 61, 0.16), transparent 28%),
radial-gradient(circle at top right, rgba(255, 183, 120, 0.08), transparent 22%),
linear-gradient(180deg, #100704 0%, var(--bg) 100%);
}
a {
color: inherit;
}
.wrap {
width: min(1500px, calc(100vw - 48px));
margin: 0 auto;
padding: 32px 0 56px;
}
.hero {
padding: 28px 28px 24px;
border: 1px solid var(--line);
border-radius: 24px;
background: linear-gradient(180deg, rgba(255, 255, 255, 0.03), rgba(255, 255, 255, 0.01));
box-shadow: var(--shadow);
}
.eyebrow {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 6px 10px;
border: 1px solid rgba(255, 123, 61, 0.28);
border-radius: 999px;
color: #ffd2ba;
background: rgba(255, 123, 61, 0.1);
font-size: 12px;
font-weight: 700;
letter-spacing: 0.08em;
text-transform: uppercase;
}
h1 {
margin: 14px 0 12px;
font-size: clamp(30px, 5vw, 52px);
line-height: 1.02;
letter-spacing: -0.04em;
}
.hero p {
max-width: 980px;
margin: 0;
color: var(--muted);
font-size: 17px;
line-height: 1.6;
}
.meta-grid {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 14px;
margin-top: 22px;
}
.meta-card {
padding: 14px 16px;
border: 1px solid var(--line);
border-radius: 18px;
background: rgba(255, 255, 255, 0.03);
}
.meta-label {
color: #d9c1b2;
font-size: 12px;
font-weight: 700;
letter-spacing: 0.08em;
text-transform: uppercase;
}
.meta-value {
margin-top: 8px;
font-size: 15px;
line-height: 1.45;
color: var(--text);
word-break: break-word;
}
.section {
margin-top: 28px;
padding: 24px;
border: 1px solid var(--line);
border-radius: 24px;
background: linear-gradient(180deg, rgba(255, 255, 255, 0.022), rgba(255, 255, 255, 0.015));
box-shadow: var(--shadow);
}
.section h2 {
margin: 0;
font-size: 26px;
letter-spacing: -0.03em;
}
.section-intro {
margin: 10px 0 0;
color: var(--muted);
font-size: 15px;
line-height: 1.6;
}
.findings {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 14px;
margin-top: 18px;
}
.finding {
padding: 16px;
border-radius: 18px;
border: 1px solid var(--line);
background: var(--panel-2);
}
.finding strong {
display: block;
margin-bottom: 8px;
font-size: 14px;
letter-spacing: 0.01em;
}
.finding p {
margin: 0;
color: var(--muted);
line-height: 1.6;
}
.comparison {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 18px;
margin-top: 22px;
}
.shot {
overflow: hidden;
border: 1px solid var(--line);
border-radius: 20px;
background: var(--panel);
}
.shot-head {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
padding: 14px 16px;
border-bottom: 1px solid var(--line);
}
.shot-title {
display: flex;
align-items: center;
gap: 10px;
font-size: 17px;
font-weight: 700;
}
.tag {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 6px 10px;
border-radius: 999px;
font-size: 12px;
font-weight: 700;
letter-spacing: 0.06em;
text-transform: uppercase;
}
.tag.before {
color: #ffd3d3;
background: rgba(255, 93, 93, 0.14);
border: 1px solid rgba(255, 93, 93, 0.22);
}
.tag.after {
color: #d9ffea;
background: rgba(73, 209, 140, 0.14);
border: 1px solid rgba(73, 209, 140, 0.22);
}
.shot img {
display: block;
width: 100%;
height: auto;
background: #0b0502;
}
.caption {
padding: 14px 16px 18px;
color: var(--muted);
font-size: 14px;
line-height: 1.65;
}
.caption ul {
margin: 10px 0 0 18px;
padding: 0;
}
.caption li + li {
margin-top: 8px;
}
.foot {
margin-top: 28px;
padding: 18px 20px;
border-radius: 18px;
border: 1px solid var(--line);
background: rgba(255, 255, 255, 0.03);
color: var(--muted);
font-size: 14px;
line-height: 1.7;
}
code {
padding: 2px 6px;
border-radius: 8px;
background: rgba(255, 255, 255, 0.06);
color: #ffe8d8;
font-family:
ui-monospace,
SFMono-Regular,
Menlo,
Consolas,
monospace;
font-size: 0.95em;
}
@media (max-width: 1100px) {
.meta-grid,
.comparison,
.findings {
grid-template-columns: 1fr;
}
}
@media (max-width: 720px) {
.wrap {
width: min(100vw - 24px, 100%);
padding-top: 18px;
}
.hero,
.section {
padding: 18px;
border-radius: 18px;
}
}
</style>
</head>
<body>
<main class="wrap">
<section class="hero">
<div class="eyebrow">UI Test Visual Report</div>
<h1>Provider-owned Codex pool UI before / after</h1>
<p>
This comparison uses two real local GoClaw dashboards seeded with the same fixture data.
The only difference is the code revision:
<code>main@b38a078e</code> versus <code>fix@faf98c73</code>.
The screenshots below isolate the two regressions fixed by the PR: hidden saved pool
membership on provider pages, and sticky custom routing state on agent pages.
</p>
<div class="meta-grid">
<div class="meta-card">
<div class="meta-label">Before Revision</div>
<div class="meta-value"><code>b38a078e</code><br />origin/main</div>
</div>
<div class="meta-card">
<div class="meta-label">After Revision</div>
<div class="meta-value"><code>faf98c73</code><br />PR branch</div>
</div>
<div class="meta-card">
<div class="meta-label">Fixture</div>
<div class="meta-value">
<code>codex-owner</code> owns pool membership<br />
<code>codex-disabled</code> is saved as a disabled member<br />
<code>solo-agent</code> uses <code>codex-solo</code> with no saved pool defaults
</div>
</div>
<div class="meta-card">
<div class="meta-label">Capture Method</div>
<div class="meta-value">
Real local UI via Playwright<br />
SQLite-backed gateways on separate ports
</div>
</div>
</div>
</section>
<section class="section">
<h2>Scenario 1: provider page hides saved non-ready members</h2>
<p class="section-intro">
The provider page should show saved pool membership even when an extra account is disabled
or not ready. Before the fix, the page derived membership only from quota-fetched ready
accounts, so the saved member disappeared entirely.
</p>
<div class="findings">
<div class="finding">
<strong>What was broken on main</strong>
<p>
The pool owner <code>codex-owner</code> had a saved extra member
<code>codex-disabled</code>, but the UI rendered <code>0 in pool</code>, an empty
account list, and no selectable extra account.
</p>
</div>
<div class="finding">
<strong>What the patch changes</strong>
<p>
The page now renders membership from the saved provider settings, while still limiting
quota lookups to ready accounts only. The disabled member remains visible and clearly
labeled as blocked.
</p>
</div>
</div>
<div class="comparison">
<article class="shot">
<div class="shot-head">
<div class="shot-title">Provider Detail</div>
<span class="tag before">Before</span>
</div>
<img src="./before-provider-overview.png" alt="Before fix: provider detail hides the saved disabled pool member." />
<div class="caption">
Main branch behavior:
<ul>
<li><code>Available Extra Accounts</code> is effectively empty.</li>
<li><code>Accounts in Pool</code> shows <code>0 in pool</code>.</li>
<li>The saved disabled member is invisible, so the provider-owned pool looks unsaved.</li>
</ul>
</div>
</article>
<article class="shot">
<div class="shot-head">
<div class="shot-title">Provider Detail</div>
<span class="tag after">After</span>
</div>
<img src="./after-provider-overview.png" alt="After fix: provider detail shows the saved disabled pool member and pool count." />
<div class="caption">
Fixed branch behavior:
<ul>
<li><code>Codex Disabled</code> is visible in <code>Available Extra Accounts</code>.</li>
<li><code>Accounts in Pool</code> correctly shows <code>2 in pool</code>.</li>
<li>The disabled member is still shown as saved, with blocked state clearly surfaced.</li>
</ul>
</div>
</article>
</div>
</section>
<section class="section">
<h2>Scenario 2: agent page manufactured a sticky custom override</h2>
<p class="section-intro">
When an agent had no explicit routing override and the provider had no saved pool yet,
the UI should have represented that state as <em>inherit / use provider defaults</em>.
Before the fix, the draft model incorrectly materialized it as a saved custom
<code>primary_first + []</code> state.
</p>
<div class="findings">
<div class="finding">
<strong>What was broken on main</strong>
<p>
The agent page defaulted to <code>Custom For This Agent</code> and framed the agent as
if it had its own routing policy, even though there was no provider-owned pool and no
explicit agent override saved.
</p>
</div>
<div class="finding">
<strong>What the patch changes</strong>
<p>
The draft state now preserves a true inherit mode. The UI shows
<code>Use Provider Defaults</code> and clarifies that the agent stays on its primary
account until provider-owned pool members are later added.
</p>
</div>
</div>
<div class="comparison">
<article class="shot">
<div class="shot-head">
<div class="shot-title">Agent Codex Pool</div>
<span class="tag before">Before</span>
</div>
<img src="./before-agent-codex-pool.png" alt="Before fix: agent page defaults to Custom For This Agent even with no provider-owned pool." />
<div class="caption">
Main branch behavior:
<ul>
<li><code>Custom For This Agent</code> is selected by default.</li>
<li>The copy tells the user to configure provider members first before enabling pooled routing.</li>
<li>This makes the implicit default state look like an explicit agent override.</li>
</ul>
</div>
</article>
<article class="shot">
<div class="shot-head">
<div class="shot-title">Agent Codex Pool</div>
<span class="tag after">After</span>
</div>
<img src="./after-agent-codex-pool.png" alt="After fix: agent page defaults to Use Provider Defaults and explains the temporary primary-only behavior." />
<div class="caption">
Fixed branch behavior:
<ul>
<li><code>Use Provider Defaults</code> is selected.</li>
<li>The helper copy explains the temporary primary-only behavior without inventing a custom override.</li>
<li>The agent can stay in inherit mode now and automatically pick up provider pool members later.</li>
</ul>
</div>
</article>
</div>
</section>
<section class="foot">
Generated from a real local browser session using two SQLite-backed GoClaw instances.
The frontends were served from separate Vite dev servers, authenticated with
<code>system</code> + a local owner token, and seeded through the HTTP API with the same
provider / agent records before capture.
</section>
</main>
</body>
</html>
-790
View File
@@ -1,790 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>PR #590 — OpenAI Tool Call Compatibility Before / After</title>
<style>
:root {
--font-body: "DM Sans", "Segoe UI", sans-serif;
--font-mono: "Fira Code", "SFMono-Regular", monospace;
--bg: #f0f4f8;
--surface: #ffffff;
--surface2: #e8eef4;
--surface-elevated: #f7fbff;
--border: rgba(30, 60, 100, 0.12);
--border-bright: rgba(30, 60, 100, 0.24);
--text: #16202d;
--text-dim: #5a7090;
--text-bright: #0c1622;
--accent: #1a5fa8;
--accent-dim: rgba(26, 95, 168, 0.1);
--green: #157347;
--green-dim: rgba(21, 115, 71, 0.1);
--red: #b42318;
--red-dim: rgba(180, 35, 24, 0.1);
--amber: #b7791f;
--amber-dim: rgba(183, 121, 31, 0.1);
--teal: #0f766e;
--teal-dim: rgba(15, 118, 110, 0.1);
--shadow: 0 18px 54px rgba(15, 23, 42, 0.1);
}
@media (prefers-color-scheme: dark) {
:root:not([data-theme="light"]) {
--bg: #0d1421;
--surface: #111d2e;
--surface2: #162438;
--surface-elevated: #1b2b42;
--border: rgba(100, 160, 220, 0.14);
--border-bright: rgba(100, 160, 220, 0.28);
--text: #d9e5f2;
--text-dim: #8ea3bb;
--text-bright: #f4f8fc;
--accent: #6eb4ff;
--accent-dim: rgba(110, 180, 255, 0.12);
--green: #4ade80;
--green-dim: rgba(74, 222, 128, 0.12);
--red: #ff8a8a;
--red-dim: rgba(255, 138, 138, 0.12);
--amber: #f6c453;
--amber-dim: rgba(246, 196, 83, 0.12);
--teal: #67e8f9;
--teal-dim: rgba(103, 232, 249, 0.12);
--shadow: 0 20px 64px rgba(2, 8, 23, 0.42);
}
}
[data-theme="dark"] {
--bg: #0d1421;
--surface: #111d2e;
--surface2: #162438;
--surface-elevated: #1b2b42;
--border: rgba(100, 160, 220, 0.14);
--border-bright: rgba(100, 160, 220, 0.28);
--text: #d9e5f2;
--text-dim: #8ea3bb;
--text-bright: #f4f8fc;
--accent: #6eb4ff;
--accent-dim: rgba(110, 180, 255, 0.12);
--green: #4ade80;
--green-dim: rgba(74, 222, 128, 0.12);
--red: #ff8a8a;
--red-dim: rgba(255, 138, 138, 0.12);
--amber: #f6c453;
--amber-dim: rgba(246, 196, 83, 0.12);
--teal: #67e8f9;
--teal-dim: rgba(103, 232, 249, 0.12);
--shadow: 0 20px 64px rgba(2, 8, 23, 0.42);
}
* {
box-sizing: border-box;
}
html {
scroll-behavior: smooth;
}
body {
margin: 0;
padding: 24px;
font-family: var(--font-body);
color: var(--text);
background:
radial-gradient(circle at top left, var(--accent-dim) 0%, transparent 26%),
linear-gradient(180deg, rgba(255, 255, 255, 0.12), rgba(255, 255, 255, 0) 24%),
var(--bg);
background-image:
radial-gradient(circle, var(--border) 1px, transparent 1px),
radial-gradient(circle at top left, var(--accent-dim) 0%, transparent 26%),
linear-gradient(180deg, rgba(255, 255, 255, 0.12), rgba(255, 255, 255, 0) 24%);
background-size: 24px 24px, auto, auto;
}
a {
color: inherit;
}
.theme-toggle {
position: fixed;
top: 16px;
right: 16px;
z-index: 300;
width: 36px;
height: 36px;
border-radius: 8px;
border: 1px solid var(--border);
background: var(--surface);
color: var(--text-dim);
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
font-size: 16px;
transition: background 0.15s, color 0.15s;
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.06);
}
.theme-toggle:hover {
background: var(--surface2);
color: var(--text);
}
.wrap {
max-width: 1400px;
margin: 0 auto;
display: grid;
grid-template-columns: 190px 1fr;
gap: 0 36px;
}
.toc {
position: sticky;
top: 24px;
align-self: start;
padding: 14px 0;
max-height: calc(100dvh - 48px);
overflow-y: auto;
}
.toc-title {
font-family: var(--font-mono);
font-size: 11px;
font-weight: 700;
letter-spacing: 0.16em;
text-transform: uppercase;
color: var(--text-dim);
padding-bottom: 10px;
margin-bottom: 8px;
border-bottom: 1px solid var(--border);
}
.toc a {
display: block;
padding: 6px 8px;
margin-bottom: 2px;
border-left: 2px solid transparent;
border-radius: 6px;
text-decoration: none;
color: var(--text-dim);
font-size: 12px;
}
.toc a:hover,
.toc a.active {
color: var(--text);
background: var(--surface);
border-left-color: var(--accent);
}
.main {
min-width: 0;
}
.hero,
.section,
.comparison-card {
border: 1px solid var(--border);
background: linear-gradient(180deg, var(--surface), var(--surface-elevated));
box-shadow: var(--shadow);
}
.hero {
padding: 28px;
border-radius: 28px;
}
.eyebrow {
display: inline-flex;
align-items: center;
gap: 10px;
padding: 7px 12px;
border-radius: 999px;
border: 1px solid var(--border-bright);
background: var(--accent-dim);
color: var(--accent);
font-family: var(--font-mono);
font-size: 12px;
font-weight: 700;
letter-spacing: 0.08em;
text-transform: uppercase;
}
.hero h1 {
margin: 16px 0 12px;
font-size: clamp(34px, 5vw, 58px);
line-height: 0.98;
letter-spacing: -0.05em;
color: var(--text-bright);
}
.hero p {
max-width: 980px;
margin: 0;
font-size: 17px;
line-height: 1.7;
color: var(--text-dim);
}
.hero-grid {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 14px;
margin-top: 22px;
}
.hero-stat {
padding: 15px 16px;
border-radius: 18px;
background: var(--surface2);
border: 1px solid var(--border);
}
.hero-stat__label {
font-family: var(--font-mono);
font-size: 11px;
font-weight: 700;
letter-spacing: 0.12em;
text-transform: uppercase;
color: var(--text-dim);
}
.hero-stat__value {
margin-top: 8px;
font-size: 15px;
line-height: 1.5;
}
.section {
margin-top: 24px;
border-radius: 24px;
padding: 24px;
}
.section-label {
display: inline-flex;
align-items: center;
gap: 9px;
margin-bottom: 10px;
color: var(--text-dim);
font-family: var(--font-mono);
font-size: 12px;
font-weight: 700;
letter-spacing: 0.12em;
text-transform: uppercase;
}
.section-label::before {
content: "";
width: 8px;
height: 8px;
border-radius: 999px;
background: var(--accent);
box-shadow: 0 0 0 6px var(--accent-dim);
}
.section h2 {
margin: 0;
font-size: 28px;
letter-spacing: -0.04em;
}
.section-intro {
margin: 10px 0 0;
font-size: 15px;
line-height: 1.7;
color: var(--text-dim);
}
.callout-grid,
.validation-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 14px;
margin-top: 18px;
}
.callout,
.validation-card {
border-radius: 18px;
padding: 16px;
border: 1px solid var(--border);
background: var(--surface2);
}
.callout strong,
.validation-card strong {
display: block;
margin-bottom: 8px;
font-size: 14px;
}
.callout p,
.validation-card p,
.validation-card ul {
margin: 0;
color: var(--text-dim);
font-size: 14px;
line-height: 1.65;
}
.comparison-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 18px;
margin-top: 22px;
}
.comparison-card {
border-radius: 22px;
overflow: hidden;
}
.comparison-head {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
padding: 16px 18px;
border-bottom: 1px solid var(--border);
background: rgba(255, 255, 255, 0.02);
}
.comparison-title {
display: flex;
align-items: center;
gap: 10px;
}
.comparison-title h3 {
margin: 0;
font-size: 19px;
letter-spacing: -0.03em;
}
.tag {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 6px 10px;
border-radius: 999px;
font-family: var(--font-mono);
font-size: 11px;
font-weight: 700;
letter-spacing: 0.08em;
text-transform: uppercase;
}
.tag.before {
color: var(--red);
background: var(--red-dim);
border: 1px solid rgba(180, 35, 24, 0.18);
}
.tag.after {
color: var(--green);
background: var(--green-dim);
border: 1px solid rgba(21, 115, 71, 0.18);
}
.comparison-body {
padding: 18px;
}
.comparison-body p {
margin: 0 0 12px;
color: var(--text-dim);
font-size: 14px;
line-height: 1.65;
}
.status-line {
display: flex;
align-items: center;
gap: 10px;
padding: 10px 12px;
margin-bottom: 14px;
border-radius: 14px;
font-family: var(--font-mono);
font-size: 12px;
font-weight: 700;
letter-spacing: 0.04em;
}
.status-line.bad {
background: var(--red-dim);
color: var(--red);
}
.status-line.good {
background: var(--green-dim);
color: var(--green);
}
pre {
margin: 0;
padding: 16px;
border-radius: 16px;
border: 1px solid var(--border);
background: #0b1320;
color: #d9e5f2;
overflow-x: auto;
font-family: var(--font-mono);
font-size: 12px;
line-height: 1.7;
}
[data-theme="light"] pre,
:root pre {
background: #102033;
}
.bullet-list {
margin: 14px 0 0;
padding-left: 18px;
color: var(--text-dim);
font-size: 14px;
line-height: 1.65;
}
.bullet-list li + li {
margin-top: 8px;
}
.validation-list {
margin: 0;
padding-left: 18px;
}
.footer-note {
margin-top: 20px;
font-size: 13px;
color: var(--text-dim);
}
@media (max-width: 1000px) {
body {
padding: 16px;
}
.wrap {
grid-template-columns: 1fr;
}
.toc {
position: sticky;
top: 0;
z-index: 200;
display: flex;
gap: 6px;
overflow-x: auto;
max-height: none;
padding: 10px 0 12px;
margin: 0 -16px 12px;
padding-left: 16px;
padding-right: 16px;
background: var(--bg);
border-bottom: 1px solid var(--border);
}
.toc-title {
display: none;
}
.toc a {
white-space: nowrap;
border-left: none;
border-bottom: 2px solid transparent;
}
.hero-grid,
.callout-grid,
.validation-grid,
.comparison-grid {
grid-template-columns: 1fr;
}
}
</style>
</head>
<body>
<button class="theme-toggle" id="themeToggle" title="Toggle theme" aria-label="Toggle light/dark theme"></button>
<div class="wrap">
<nav class="toc" id="toc">
<div class="toc-title">Contents</div>
<a href="#overview">Overview</a>
<a href="#tool-id-fix">Tool ID shortening</a>
<a href="#model-gating-fix">Model gating</a>
<a href="#validation">Validation</a>
</nav>
<main class="main">
<section class="hero" id="overview">
<div class="eyebrow">PR #590 • reviewer artifact</div>
<h1>OpenAI compatibility fix, shown as behavior not prose.</h1>
<p>
This PR has no product UI. The reviewer surface is wire behavior:
how long tool-call IDs are serialized and how prefixed OpenAI-compatible
model IDs are classified. The visuals below show the exact before/after
deltas that matter for issue #532.
</p>
<div class="hero-grid">
<div class="hero-stat">
<div class="hero-stat__label">Primary bug</div>
<div class="hero-stat__value">Legacy or replayed tool IDs could alias after a 40-char prefix cut.</div>
</div>
<div class="hero-stat">
<div class="hero-stat__label">Secondary bug</div>
<div class="hero-stat__value">Prefixed models like <code>openai/o3-mini</code> bypassed capability gates.</div>
</div>
<div class="hero-stat">
<div class="hero-stat__label">Fix shape</div>
<div class="hero-stat__value">Hash long IDs at the serializer boundary and normalize model family once.</div>
</div>
<div class="hero-stat">
<div class="hero-stat__label">Scope</div>
<div class="hero-stat__value">Covers main loop, resumed history, memory flush, and subagent replay through one path.</div>
</div>
</div>
</section>
<section class="section" id="tool-id-fix">
<div class="section-label">01 • Tool ID shortening</div>
<h2>Before: prefix cut preserved length, not uniqueness.</h2>
<p class="section-intro">
The original defensive fallback capped IDs by slicing the first 40 characters.
That stops the HTTP 400, but it still lets two distinct long IDs collapse into
the same wire value if they only diverge after character 40.
</p>
<div class="callout-grid">
<div class="callout">
<strong>Why this was still a blocker</strong>
<p>Fresh live tool calls were already hashed upstream, but resumed history and replay paths still depended on the serializer. That meant the bug could survive in exactly the sessions reviewers care about: legacy, resumed, or replayed state.</p>
</div>
<div class="callout">
<strong>Why the serializer fix is the right layer</strong>
<p>Every OpenAI-compatible request path uses the same request builder. Fixing the boundary closes the main loop and the side loops together instead of patching each caller independently.</p>
</div>
</div>
<div class="comparison-grid">
<article class="comparison-card" id="before-tool-id-card">
<div class="comparison-head">
<div class="comparison-title">
<span class="tag before">Before</span>
<h3>Shared-prefix IDs collide after truncation</h3>
</div>
</div>
<div class="comparison-body">
<div class="status-line bad">Collision risk remains on replay</div>
<p>
Two distinct IDs from a replayed or deduped session can become the same
outbound value. The request is shorter, but the assistant tool calls and
tool results are no longer uniquely identifiable.
</p>
<pre>{
"tool_calls": [
{ "id": "call_0123456789abcdef0123456789abcdef012" },
{ "id": "call_0123456789abcdef0123456789abcdef012" }
],
"messages": [
{ "role": "tool", "tool_call_id": "call_0123456789abcdef0123456789abcdef012" },
{ "role": "tool", "tool_call_id": "call_0123456789abcdef0123456789abcdef012" }
]
}</pre>
<ul class="bullet-list">
<li>Distinct legacy IDs differ only after byte 40.</li>
<li>Prefix slicing makes both values identical on the wire.</li>
<li>Parallel tool results can no longer be correlated safely.</li>
</ul>
</div>
</article>
<article class="comparison-card" id="after-tool-id-card">
<div class="comparison-head">
<div class="comparison-title">
<span class="tag after">After</span>
<h3>Long IDs shorten to stable hashed values</h3>
</div>
</div>
<div class="comparison-body">
<div class="status-line good">40-char cap without aliasing</div>
<p>
IDs longer than 40 characters are now re-encoded as deterministic
<code>call_</code>-prefixed hashes. The same original ID always maps
to the same shortened value, and different long IDs stay different.
</p>
<pre>{
"tool_calls": [
{ "id": "call_6625d2d3e5ec2c6c86f70fd9a7064cc77a1" },
{ "id": "call_60b8d9101dbdde451723a0888a84317d14f" }
],
"messages": [
{ "role": "tool", "tool_call_id": "call_6625d2d3e5ec2c6c86f70fd9a7064cc77a1" },
{ "role": "tool", "tool_call_id": "call_60b8d9101dbdde451723a0888a84317d14f" }
]
}</pre>
<ul class="bullet-list">
<li>Still meets the 40-character API limit.</li>
<li>Preserves assistant/tool-result correlation because the same helper is used for both fields.</li>
<li>Protects replay and legacy sessions, not just newly generated tool calls.</li>
</ul>
</div>
</article>
</div>
</section>
<section class="section" id="model-gating-fix">
<div class="section-label">02 • Model capability gating</div>
<h2>Before: prefixed model IDs skipped the intended reasoning-model rules.</h2>
<p class="section-intro">
The request builder already supports provider-prefixed IDs such as
<code>openai/o3-mini</code>. The bug was that capability checks ran on the
full string, so prefixed reasoning models could keep <code>temperature</code>
and use the wrong token field.
</p>
<div class="comparison-grid">
<article class="comparison-card" id="before-gating-card">
<div class="comparison-head">
<div class="comparison-title">
<span class="tag before">Before</span>
<h3>Prefixed reasoning models look like unknown families</h3>
</div>
</div>
<div class="comparison-body">
<div class="status-line bad">Wrong request shape for <code>openai/o3-mini</code></div>
<p>
Because the gate looked at the full model string, the builder missed
both the reasoning-model token routing and the temperature skip.
</p>
<pre>{
"model": "openai/o3-mini",
"max_tokens": 123,
"temperature": 0.7
}</pre>
<ul class="bullet-list">
<li>Contradicted the “model-level, not provider-specific” finding.</li>
<li>Left OpenRouter-style OpenAI IDs outside the intended guardrail.</li>
</ul>
</div>
</article>
<article class="comparison-card" id="after-gating-card">
<div class="comparison-head">
<div class="comparison-title">
<span class="tag after">After</span>
<h3>Capability checks use normalized model family</h3>
</div>
</div>
<div class="comparison-body">
<div class="status-line good">Same rules for bare and prefixed IDs</div>
<p>
The builder now strips transport prefixes once, then applies the same
capability logic to both bare and prefixed model IDs.
</p>
<pre>{
"model": "openai/o3-mini",
"max_completion_tokens": 123
}</pre>
<ul class="bullet-list">
<li><code>openai/o3-mini</code> now follows the same route as <code>o3-mini</code>.</li>
<li><code>openai/gpt-5.4</code> still keeps <code>temperature</code>, matching the flagship-model rule.</li>
<li>Azure and non-Azure paths are validated as model-based, not URL-based.</li>
</ul>
</div>
</article>
</div>
</section>
<section class="section" id="validation">
<div class="section-label">03 • Validation</div>
<h2>Tests and reviewer-facing outcomes</h2>
<p class="section-intro">
The changes above were verified with focused provider and agent tests, plus
a full-suite spot check to expose unrelated red tests explicitly rather than
hide them under a green subset.
</p>
<div class="validation-grid">
<div class="validation-card">
<strong>Passed</strong>
<ul class="validation-list">
<li><code>go test ./internal/providers ./internal/agent</code></li>
<li><code>go test ./internal/providers -run 'TestTruncateToolCallID|TestBuildRequestBody' -count=1</code></li>
<li><code>go test ./internal/agent -run '^TestUniquifyToolCallIDs$' -count=1</code></li>
</ul>
</div>
<div class="validation-card">
<strong>Known unrelated failure</strong>
<p>
<code>go test ./...</code> still fails in <code>pkg/browser</code> at
<code>TestResolveRemoteCDP_DefaultPort</code> with
<code>/json/version returned HTTP 404</code>. This pre-exists the provider
changes and is unrelated to the OpenAI compatibility patch.
</p>
</div>
</div>
<p class="footer-note">
Review note: these visuals intentionally show behavior and request-shape
diffs rather than product UI, because this PR does not alter application screens.
</p>
</section>
</main>
</div>
<script>
(function() {
const toggle = document.getElementById("themeToggle");
const saved = localStorage.getItem("theme");
const initial = saved || (window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light");
document.documentElement.setAttribute("data-theme", initial);
toggle.textContent = initial === "dark" ? "\u2600" : "\u263E";
toggle.addEventListener("click", function() {
const current = document.documentElement.getAttribute("data-theme") || "light";
const next = current === "light" ? "dark" : "light";
document.documentElement.setAttribute("data-theme", next);
localStorage.setItem("theme", next);
toggle.textContent = next === "dark" ? "\u2600" : "\u263E";
});
const toc = document.getElementById("toc");
const links = toc.querySelectorAll("a");
const sections = [];
links.forEach((link) => {
const id = link.getAttribute("href").slice(1);
const el = document.getElementById(id);
if (el) sections.push({ link, el });
});
const observer = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
if (!entry.isIntersecting) return;
links.forEach((link) => link.classList.remove("active"));
const match = sections.find((section) => section.el === entry.target);
if (match) match.link.classList.add("active");
});
},
{ rootMargin: "-10% 0px -70% 0px" },
);
sections.forEach((section) => observer.observe(section.el));
links.forEach((link) => {
link.addEventListener("click", (event) => {
event.preventDefault();
const id = link.getAttribute("href").slice(1);
const el = document.getElementById(id);
if (el) {
el.scrollIntoView({ behavior: "smooth", block: "start" });
history.replaceState(null, "", "#" + id);
}
});
});
})();
</script>
</body>
</html>
Binary file not shown.

Before

Width:  |  Height:  |  Size: 183 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 133 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 187 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 174 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 157 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 115 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 137 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 147 KiB

-668
View File
@@ -1,668 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>PR #634 — Channel Health Diagnostics UI Before / After</title>
<style>
:root {
--bg: #100705;
--panel: #1b110c;
--panel-2: #241610;
--text: #f7eee8;
--muted: #ceb6a8;
--line: rgba(255, 220, 196, 0.16);
--accent: #ff7a3d;
--accent-soft: rgba(255, 122, 61, 0.12);
--good: #4ad18c;
--warn: #ffd166;
--shadow: 0 26px 80px rgba(0, 0, 0, 0.34);
}
[data-theme="light"] {
--bg: #f7efe9;
--panel: #fffaf6;
--panel-2: #fff4ec;
--text: #23140d;
--muted: #71584b;
--line: rgba(122, 73, 44, 0.14);
--accent: #cf5c21;
--accent-soft: rgba(207, 92, 33, 0.1);
--good: #127a4b;
--warn: #9a6a07;
--shadow: 0 20px 60px rgba(72, 38, 14, 0.14);
}
* {
box-sizing: border-box;
}
html {
scroll-behavior: smooth;
}
body {
margin: 0;
font-family:
"Inter",
ui-sans-serif,
system-ui,
-apple-system,
BlinkMacSystemFont,
"Segoe UI",
sans-serif;
color: var(--text);
background:
radial-gradient(circle at top left, rgba(255, 122, 61, 0.16), transparent 26%),
radial-gradient(circle at top right, rgba(255, 210, 186, 0.08), transparent 20%),
linear-gradient(180deg, rgba(255, 255, 255, 0.02), rgba(255, 255, 255, 0)),
var(--bg);
}
a {
color: inherit;
}
.theme-toggle {
position: fixed;
top: 18px;
right: 18px;
z-index: 30;
border: 1px solid var(--line);
background: var(--panel);
color: var(--text);
border-radius: 999px;
padding: 10px 14px;
font: inherit;
cursor: pointer;
box-shadow: var(--shadow);
}
.wrap {
width: min(1480px, calc(100vw - 40px));
margin: 0 auto;
padding: 28px 0 56px;
}
.hero,
.section {
border: 1px solid var(--line);
border-radius: 28px;
background: linear-gradient(180deg, rgba(255, 255, 255, 0.03), rgba(255, 255, 255, 0.015));
box-shadow: var(--shadow);
}
.hero {
padding: 30px;
}
.eyebrow {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 7px 11px;
border-radius: 999px;
border: 1px solid rgba(255, 122, 61, 0.25);
background: var(--accent-soft);
color: var(--accent);
font-size: 12px;
font-weight: 800;
letter-spacing: 0.08em;
text-transform: uppercase;
}
h1 {
margin: 16px 0 12px;
font-size: clamp(34px, 5vw, 56px);
line-height: 0.98;
letter-spacing: -0.05em;
}
.hero p,
.section-intro,
.note,
.proof-list li,
.caption,
.bullets li {
color: var(--muted);
}
.hero p {
max-width: 980px;
margin: 0;
font-size: 17px;
line-height: 1.7;
}
.meta-grid,
.proof-grid,
.comparison,
.code-grid,
.assert-grid {
display: grid;
gap: 16px;
}
.meta-grid {
grid-template-columns: repeat(4, minmax(0, 1fr));
margin-top: 22px;
}
.meta-card,
.proof,
.code-card,
.assert-card {
border: 1px solid var(--line);
border-radius: 20px;
background: var(--panel-2);
}
.meta-card {
padding: 15px 16px;
}
.meta-label {
font-size: 11px;
font-weight: 800;
letter-spacing: 0.12em;
text-transform: uppercase;
color: var(--muted);
}
.meta-value {
margin-top: 8px;
font-size: 15px;
line-height: 1.5;
}
.section {
margin-top: 26px;
padding: 26px;
}
.section h2 {
margin: 0;
font-size: 28px;
letter-spacing: -0.04em;
}
.section-intro {
margin: 10px 0 0;
font-size: 15px;
line-height: 1.65;
}
.proof-grid {
grid-template-columns: repeat(2, minmax(0, 1fr));
margin-top: 18px;
}
.proof {
padding: 16px;
}
.proof strong,
.code-card strong,
.assert-card strong {
display: block;
margin-bottom: 8px;
font-size: 15px;
}
.comparison {
grid-template-columns: repeat(2, minmax(0, 1fr));
margin-top: 22px;
}
.shot {
overflow: hidden;
border: 1px solid var(--line);
border-radius: 22px;
background: var(--panel);
}
.shot-head {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
padding: 15px 16px;
border-bottom: 1px solid var(--line);
}
.shot-title {
font-size: 18px;
font-weight: 800;
}
.tag {
display: inline-flex;
align-items: center;
padding: 6px 10px;
border-radius: 999px;
font-size: 11px;
font-weight: 800;
letter-spacing: 0.08em;
text-transform: uppercase;
}
.before {
color: #ffd5d5;
background: rgba(255, 93, 93, 0.14);
border: 1px solid rgba(255, 93, 93, 0.2);
}
.after {
color: #dcffec;
background: rgba(74, 209, 140, 0.14);
border: 1px solid rgba(74, 209, 140, 0.2);
}
.shot img {
display: block;
width: 100%;
height: auto;
background: #0a0503;
}
.caption {
padding: 15px 16px 18px;
font-size: 14px;
line-height: 1.68;
}
.code-grid {
grid-template-columns: repeat(2, minmax(0, 1fr));
margin-top: 18px;
}
.code-card,
.assert-card {
padding: 18px;
}
.bullets,
.proof-list {
margin: 0;
padding-left: 18px;
}
.bullets li + li,
.proof-list li + li {
margin-top: 8px;
}
.assert-grid {
grid-template-columns: repeat(2, minmax(0, 1fr));
margin-top: 18px;
}
pre {
margin: 10px 0 0;
padding: 16px;
border-radius: 16px;
background: rgba(0, 0, 0, 0.2);
border: 1px solid var(--line);
color: var(--text);
font-size: 12px;
line-height: 1.55;
overflow: auto;
}
.note {
margin-top: 18px;
padding: 16px 18px;
border-radius: 18px;
border: 1px solid var(--line);
background: rgba(255, 255, 255, 0.03);
font-size: 14px;
line-height: 1.72;
}
code {
padding: 2px 6px;
border-radius: 8px;
background: rgba(255, 255, 255, 0.05);
font-family: "SFMono-Regular", ui-monospace, monospace;
font-size: 0.94em;
}
@media (max-width: 1100px) {
.meta-grid,
.proof-grid,
.comparison,
.code-grid,
.assert-grid {
grid-template-columns: 1fr;
}
}
</style>
</head>
<body>
<button class="theme-toggle" type="button" id="theme-toggle">Toggle theme</button>
<div class="wrap">
<section class="hero">
<div class="eyebrow">Draft PR #634 • Visual Review</div>
<h1>Channel Health Diagnostics UI Before / After</h1>
<p>
This evidence pack documents the user-facing impact of the channel-health diagnostics
work for issue <code>#628</code> and the follow-up UI hardening that landed during review.
The screenshots show the same seeded scenarios before the PR and after the current draft
head, so the deltas are attributable to this branch rather than a different test setup.
</p>
<div class="meta-grid">
<div class="meta-card">
<div class="meta-label">Issue Status</div>
<div class="meta-value">Partially solved. The product now explains the failure path, but bad credentials still need operator action.</div>
</div>
<div class="meta-card">
<div class="meta-label">Screens Compared</div>
<div class="meta-value">4 paired captures: overview, channels list, auth detail, missing-credentials detail.</div>
</div>
<div class="meta-card">
<div class="meta-label">Chosen Direction</div>
<div class="meta-value">Shared diagnostics + remediation, not a Telegram-only patch.</div>
</div>
<div class="meta-card">
<div class="meta-label">Review Hardening</div>
<div class="meta-value">Added neutral checking state, safer tab fallback, and no partial-data overcounting.</div>
</div>
</div>
</section>
<section class="section">
<h2>What This PR Proves</h2>
<p class="section-intro">
The screenshots validate the visible remediation experience. The code changes go a bit
further than the screenshots alone, so the report separates visual proof from the
additional hardening that prevents misleading UI states.
</p>
<div class="proof-grid">
<div class="proof">
<strong>Overview no longer hides the problem</strong>
<ul class="proof-list">
<li>Before: dashboard-level attention existed, but the missing-credentials path was easy to miss.</li>
<li>After: the overview clearly communicates that channel attention exists and aligns with the richer downstream diagnostics.</li>
</ul>
</div>
<div class="proof">
<strong>Channels list now points to action</strong>
<ul class="proof-list">
<li>Before: rows could degrade into vague summaries or omit the useful next step.</li>
<li>After: the row copy names the failure and exposes the remediation action more directly.</li>
</ul>
</div>
<div class="proof">
<strong>Auth failures are diagnosable</strong>
<ul class="proof-list">
<li>Before: the detail page lacked enough structure for a user to understand what failed.</li>
<li>After: the detail page shows what happened, recommended action, credentials entry point, and technical detail.</li>
</ul>
</div>
<div class="proof">
<strong>Config failures are diagnosable</strong>
<ul class="proof-list">
<li>Before: missing credentials did not surface a complete remediation path.</li>
<li>After: the detail page explains the config problem and exposes the credentials action clearly.</li>
</ul>
</div>
</div>
</section>
<section class="section" id="overview">
<h2>Overview</h2>
<p class="section-intro">
The dashboard summary is the first place a user looks. It now leads toward the richer diagnostics
instead of leaving the channel state effectively opaque.
</p>
<div class="comparison">
<article class="shot">
<div class="shot-head">
<div class="shot-title">Overview — Before</div>
<div class="tag before">Before</div>
</div>
<img src="before-overview.png" alt="Overview before channel diagnostics improvements" />
<div class="caption">
Before: the overview conveyed that attention existed, but it did not make the missing-credentials
path nearly as actionable as the PR now does downstream.
</div>
</article>
<article class="shot">
<div class="shot-head">
<div class="shot-title">Overview — After</div>
<div class="tag after">After</div>
</div>
<img src="after-overview.png" alt="Overview after channel diagnostics improvements" />
<div class="caption">
After: the overview aligns with the richer diagnostics flow and no longer relies on the old opaque
failure presentation.
</div>
</article>
</div>
</section>
<section class="section" id="channels-list">
<h2>Channels List</h2>
<p class="section-intro">
This is where the PR most clearly replaces vague channel status with named failures and next-step copy.
</p>
<div class="comparison">
<article class="shot">
<div class="shot-head">
<div class="shot-title">Channels List — Before</div>
<div class="tag before">Before</div>
</div>
<img src="before-channels-list.png" alt="Channels list before channel diagnostics improvements" />
<div class="caption">
Before: summaries and remediation copy were incomplete. The list did not consistently tell the user
what failed or which action to take next.
</div>
</article>
<article class="shot">
<div class="shot-head">
<div class="shot-title">Channels List — After</div>
<div class="tag after">After</div>
</div>
<img src="after-channels-list.png" alt="Channels list after channel diagnostics improvements" />
<div class="caption">
After: the row naming and next-step prompts are explicit. The UI now makes the channel-health state
actionable rather than just visible.
</div>
</article>
</div>
</section>
<section class="section" id="auth-detail">
<h2>Auth Failure Detail</h2>
<p class="section-intro">
This captures the original class of incident from <code>#628</code>: a channel is enabled but blocked by
auth/runtime reality. The page now explains that state instead of flattening it.
</p>
<div class="comparison">
<article class="shot">
<div class="shot-head">
<div class="shot-title">Auth Detail — Before</div>
<div class="tag before">Before</div>
</div>
<img src="before-auth-detail.png" alt="Auth failure detail before channel diagnostics improvements" />
<div class="caption">
Before: the user could land on the channel detail view without enough structure to understand
what happened, what to try next, or where the credentials/debug entry point was.
</div>
</article>
<article class="shot">
<div class="shot-head">
<div class="shot-title">Auth Detail — After</div>
<div class="tag after">After</div>
</div>
<img src="after-auth-detail.png" alt="Auth failure detail after channel diagnostics improvements" />
<div class="caption">
After: the page names the auth failure, provides recommended action, exposes the credentials entry
point, and shows technical detail useful for debugging.
</div>
</article>
</div>
</section>
<section class="section" id="missing-detail">
<h2>Missing Credentials Detail</h2>
<p class="section-intro">
Missing or incomplete channel configuration now gets the same structured treatment as auth failures.
</p>
<div class="comparison">
<article class="shot">
<div class="shot-head">
<div class="shot-title">Missing Credentials — Before</div>
<div class="tag before">Before</div>
</div>
<img src="before-missing-detail.png" alt="Missing credentials detail before channel diagnostics improvements" />
<div class="caption">
Before: the config failure path was visually thin and did not fully guide the user back to the
required credentials work.
</div>
</article>
<article class="shot">
<div class="shot-head">
<div class="shot-title">Missing Credentials — After</div>
<div class="tag after">After</div>
</div>
<img src="after-missing-detail.png" alt="Missing credentials detail after channel diagnostics improvements" />
<div class="caption">
After: the UI names the config problem, explains the credentials requirement, and surfaces the
action path directly on the page.
</div>
</article>
</div>
</section>
<section class="section">
<h2>Additional Hardening Beyond The Screenshots</h2>
<p class="section-intro">
Review surfaced a few cases where the visible diagnostics could still be misleading. Those are fixed in
code even when they do not have a dedicated screenshot in this pack.
</p>
<div class="code-grid">
<div class="code-card">
<strong>No more false “Stopped” state for unknown-but-enabled channels</strong>
<ul class="bullets">
<li>Enabled channels without backend status now stay neutral as <code>Checking</code>.</li>
<li>This prevents the UI from manufacturing attention where the runtime has not yet reported anything.</li>
</ul>
</div>
<div class="code-card">
<strong>No more partial-data overcounting in overview</strong>
<ul class="bullets">
<li>Overview attention synthesis only runs when the loaded instance slice is complete.</li>
<li>This avoids claiming more failing channels than the current dataset can justify.</li>
</ul>
</div>
<div class="code-card">
<strong>Fallback remediation is channel-specific</strong>
<ul class="bullets">
<li><code>zalo_personal</code> now points to re-auth instead of generic credentials repair.</li>
<li>Generic missing-credentials fallback stays on the credentials path with localized copy.</li>
</ul>
</div>
<div class="code-card">
<strong>Invalid channel detail tabs fail safely</strong>
<ul class="bullets">
<li>Unsupported <code>?tab=</code> values now fall back to <code>general</code>.</li>
<li>Non-Telegram channels can no longer land on the Telegram-only <code>groups</code> tab.</li>
</ul>
</div>
</div>
</section>
<section class="section">
<h2>Assertion Snapshot</h2>
<p class="section-intro">
These structured checks back up the screenshots by recording the UI markers that were present before and after.
</p>
<div class="assert-grid">
<div class="assert-card">
<strong>Before</strong>
<pre>{
"overview": {
"needsAttention": true,
"authChannelVisible": true,
"missingChannelVisible": false
},
"channelsList": {
"authSummary": false,
"missingSummary": true,
"nextStepVisible": true,
"credentialsActionVisible": false
},
"authDetail": {
"whatHappened": true,
"recommendedAction": true,
"reviewCredentials": false,
"technicalDetail": false
},
"missingCredentialsDetail": {
"missingCredentials": true,
"completeRequiredCredentials": false,
"openCredentials": false
}
}</pre>
</div>
<div class="assert-card">
<strong>After</strong>
<pre>{
"overview": {
"needsAttention": true,
"authVisible": false,
"missingVisible": false
},
"channelsList": {
"authSummary": true,
"missingSummary": false,
"reviewCredentials": true,
"completeCredentials": true
},
"authDetail": {
"whatHappened": true,
"authFailed": true,
"recommendedAction": true,
"openCredentials": true,
"technicalDetail": true
},
"missingDetail": {
"whatHappened": true,
"configInvalid": true,
"completeCredentials": true,
"openCredentials": true,
"technicalDetail": true
}
}</pre>
</div>
</div>
<div class="note">
Visual verdict: this PR fixes GoClaw’s product-side diagnosability gap for the seeded auth and config
channel failures. It does not auto-repair invalid credentials, so the incident class from <code>#628</code>
is better described as <strong>diagnosable and actionable</strong>, not automatically healed.
</div>
</section>
</div>
<script>
const key = "pr-634-proof-theme";
const root = document.documentElement;
const button = document.getElementById("theme-toggle");
const preferred = window.matchMedia("(prefers-color-scheme: light)").matches ? "light" : "dark";
const initial = localStorage.getItem(key) || preferred;
function applyTheme(theme) {
root.dataset.theme = theme;
}
applyTheme(initial);
button.addEventListener("click", () => {
const next = root.dataset.theme === "light" ? "dark" : "light";
localStorage.setItem(key, next);
applyTheme(next);
});
</script>
</body>
</html>
Binary file not shown.

Before

Width:  |  Height:  |  Size: 144 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 161 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 160 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 149 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 149 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 196 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 148 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 116 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 145 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 162 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 161 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 199 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 198 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 134 KiB

-152
View File
@@ -1,152 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>PR #671 — Pool Management UX Evidence</title>
<style>
:root { --bg: #0d0d0d; --fg: #e8e4df; --muted: #8a8278; --accent: #e8733a; --card: #1a1714; --border: #2d2923; --badge-fix-bg: #1a2e1a; --badge-fix-fg: #4ade80; --badge-fix-bd: #2d5a2d; --badge-be-bg: #1a1a2e; --badge-be-fg: #818cf8; --badge-be-bd: #2d2d5a; }
.light { --bg: #f8f6f3; --fg: #1a1714; --muted: #6b6560; --accent: #d4612a; --card: #ffffff; --border: #e5e0da; --badge-fix-bg: #dcfce7; --badge-fix-fg: #166534; --badge-fix-bd: #bbf7d0; --badge-be-bg: #e0e7ff; --badge-be-fg: #3730a3; --badge-be-bd: #c7d2fe; }
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: system-ui, -apple-system, sans-serif; background: var(--bg); color: var(--fg); line-height: 1.6; transition: background 0.2s, color 0.2s; }
.container { max-width: 1200px; margin: 0 auto; padding: 2rem 1.5rem; }
.top-bar { display: flex; align-items: center; justify-content: space-between; margin-bottom: 0.25rem; }
h1 { font-size: 1.5rem; font-weight: 600; }
.theme-toggle { background: var(--card); border: 1px solid var(--border); border-radius: 6px; padding: 0.35rem 0.75rem; font-size: 0.8rem; color: var(--muted); cursor: pointer; transition: all 0.2s; }
.theme-toggle:hover { color: var(--fg); border-color: var(--accent); }
.subtitle { color: var(--muted); font-size: 0.875rem; margin-bottom: 2.5rem; }
.subtitle a { color: var(--accent); text-decoration: none; }
.comparison { margin-bottom: 3rem; }
.comparison h2 { font-size: 1.125rem; font-weight: 600; margin-bottom: 0.25rem; border-left: 3px solid var(--accent); padding-left: 0.75rem; }
.comparison .desc { color: var(--muted); font-size: 0.875rem; margin-bottom: 1rem; padding-left: calc(0.75rem + 3px); }
.grid { display: grid; grid-template-columns: 1fr 1fr; gap: 1rem; }
@media (max-width: 768px) { .grid { grid-template-columns: 1fr; } }
.col { background: var(--card); border: 1px solid var(--border); border-radius: 8px; overflow: hidden; transition: background 0.2s, border-color 0.2s; }
.col-header { padding: 0.5rem 0.75rem; font-size: 0.75rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.05em; border-bottom: 1px solid var(--border); }
.col-header.before { color: var(--muted); }
.col-header.after { color: var(--accent); }
.col img { width: 100%; display: block; }
.callout { background: var(--card); border: 1px solid var(--border); border-radius: 8px; padding: 1rem 1.25rem; margin-bottom: 2rem; font-size: 0.875rem; color: var(--muted); transition: background 0.2s, border-color 0.2s; }
.callout strong { color: var(--fg); }
.badge { display: inline-block; padding: 0.125rem 0.5rem; border-radius: 4px; font-size: 0.75rem; font-weight: 500; }
.badge-fix { background: var(--badge-fix-bg); color: var(--badge-fix-fg); border: 1px solid var(--badge-fix-bd); }
.badge-backend { background: var(--badge-be-bg); color: var(--badge-be-fg); border: 1px solid var(--badge-be-bd); }
.fixes { display: flex; gap: 0.5rem; flex-wrap: wrap; margin-bottom: 2rem; }
.fix-item { background: var(--card); border: 1px solid var(--border); border-radius: 8px; padding: 0.75rem 1rem; flex: 1; min-width: 200px; transition: background 0.2s, border-color 0.2s; }
.fix-item h3 { font-size: 0.8rem; font-weight: 600; margin-bottom: 0.25rem; }
.fix-item p { font-size: 0.75rem; color: var(--muted); }
</style>
</head>
<body>
<div class="container">
<div class="top-bar">
<h1>PR #671 — Pool Management UX</h1>
<button class="theme-toggle" onclick="document.documentElement.classList.toggle('light'); this.textContent = document.documentElement.classList.contains('light') ? '● Dark' : '○ Light'">○ Light</button>
</div>
<p class="subtitle">
<a href="https://github.com/nextlevelbuilder/goclaw/pull/671">nextlevelbuilder/goclaw#671</a> &middot;
Closes <a href="https://github.com/nextlevelbuilder/goclaw/issues/670">#670</a> &middot;
Relates <a href="https://github.com/nextlevelbuilder/goclaw/issues/583">#583</a>
</p>
<div class="fixes">
<div class="fix-item">
<h3><span class="badge badge-fix">Fix 1</span> Selector affordance</h3>
<p>Dashed primary border + icon badge + "Click to add" hint. Unmistakably interactive in both themes.</p>
</div>
<div class="fix-item">
<h3><span class="badge badge-backend">Fix 2</span> Stale pool refs</h3>
<p>Backend skips unknown members instead of blocking saves. Deleted/disabled providers no longer cause errors.</p>
</div>
<div class="fix-item">
<h3><span class="badge badge-fix">Fix 3</span> Managed-by banner</h3>
<p>Pool members now show which pool they belong to, with a direct link to the owner provider.</p>
</div>
<div class="fix-item">
<h3><span class="badge badge-fix">Fix 4</span> Pool discovery</h3>
<p>Per-card "Pool available" badge replaces verbose info banner. One-click wizard to create pools from unpooled accounts.</p>
</div>
<div class="fix-item">
<h3><span class="badge badge-fix">Fix 5</span> Provider selector</h3>
<p>Pool members hidden from agent provider dropdown. Only pool owners listed — routing handles members automatically.</p>
</div>
</div>
<div class="callout">
<strong>Environment:</strong> Same GoClaw v2.65.0 instance, dark theme, user kai / tenant Master.
Before = production dashboard (claw:3000). After = local dev server (localhost:5173) with this PR.
</div>
<div class="comparison">
<h2>1. Pool Owner — Available Accounts Selector</h2>
<p class="desc">Before: bare text, no icon, invisible border in dark mode. After: dashed primary border, + icon badge, "Click to add" label.</p>
<div class="grid">
<div class="col">
<div class="col-header before">Before</div>
<img src="before-pool-owner-annotated.png" alt="Before: pool owner selector">
</div>
<div class="col">
<div class="col-header after">After</div>
<img src="after-pool-owner-unselected-annotated.png" alt="After: pool owner selector with dashed border and plus icon">
</div>
</div>
</div>
<div class="comparison">
<h2>2. Pool Member — Managed-By Banner</h2>
<p class="desc">Before: pool section completely hidden, no explanation. After: "Pool Defaults" section shows owner name + clickable link.</p>
<div class="grid">
<div class="col">
<div class="col-header before">Before</div>
<img src="before-pool-member-annotated.png" alt="Before: pool member page, no pool section visible">
</div>
<div class="col">
<div class="col-header after">After</div>
<img src="after-pool-member-annotated.png" alt="After: pool member page with managed-by banner and link">
</div>
</div>
</div>
<div class="comparison">
<h2>3. Provider List — Pool Discovery</h2>
<p class="desc">Before: verbose info banner with pool stats took up space. After: clean per-card "Pool available" badge on each unpooled account.</p>
<div class="grid">
<div class="col">
<div class="col-header before">Before</div>
<img src="before-provider-list-annotated.png" alt="Before: provider list with verbose info banner">
</div>
<div class="col">
<div class="col-header after">After</div>
<img src="after-provider-list-annotated.png" alt="After: provider list with per-card Pool available badges">
</div>
</div>
</div>
<div class="comparison">
<h2>4. Agent Provider Selector — Pool Members Hidden</h2>
<p class="desc">Before: pool member (codex-secondary) shown as standalone option, no pool indicator. After: member hidden, pool owner shows "Pool" badge so users know it routes to multiple accounts.</p>
<div class="grid">
<div class="col">
<div class="col-header before">Before</div>
<img src="before-agent-provider-annotated.png" alt="Before: provider dropdown showing pool member as standalone option">
</div>
<div class="col">
<div class="col-header after">After</div>
<img src="after-agent-provider-annotated.png" alt="After: provider dropdown with pool member hidden">
</div>
</div>
</div>
<div class="comparison">
<h2>5. Pool Setup Wizard</h2>
<p class="desc">New one-click wizard dialog opened from any "Pool available" badge. Select owner, members, strategy — all in one step.</p>
<div class="grid" style="grid-template-columns: 1fr;">
<div class="col" style="max-width: 720px; margin: 0 auto;">
<div class="col-header after">New</div>
<img src="after-pool-wizard.png" alt="Pool setup wizard dialog with owner/member selection and strategy toggle">
</div>
</div>
</div>
</div>
</body>
</html>
Binary file not shown.

Before

Width:  |  Height:  |  Size: 233 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 234 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 100 KiB

-71
View File
@@ -1,71 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>PR #730 — fix(chat): first conversation click</title>
<style>
:root { --bg: #0d0d0d; --fg: #e8e4df; --card: #1a1714; --border: #2d2923; --accent: #ef4444; --muted: #6b6560; }
.light { --bg: #f8f6f3; --fg: #1a1714; --card: #ffffff; --border: #e5e0da; }
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; background: var(--bg); color: var(--fg); padding: 2rem; }
.theme-toggle { position: fixed; top: 1rem; right: 1rem; background: var(--card); border: 1px solid var(--border); color: var(--fg); padding: 0.5rem 1rem; border-radius: 6px; cursor: pointer; z-index: 100; }
h1 { font-size: 1.5rem; margin-bottom: 0.5rem; }
.subtitle { color: var(--muted); margin-bottom: 2rem; }
.summary { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 1rem; margin-bottom: 2rem; }
.summary-card { background: var(--card); border: 1px solid var(--border); border-radius: 8px; padding: 1rem; }
.summary-card h3 { font-size: 0.75rem; text-transform: uppercase; color: var(--muted); margin-bottom: 0.5rem; }
.env { background: var(--card); border: 1px solid var(--border); border-radius: 8px; padding: 1rem; margin-bottom: 2rem; font-size: 0.85rem; color: var(--muted); }
.comparison { margin-bottom: 3rem; }
.comparison h2 { font-size: 1.1rem; margin-bottom: 1rem; border-bottom: 1px solid var(--border); padding-bottom: 0.5rem; }
.grid { display: grid; grid-template-columns: 1fr 1fr; gap: 1rem; }
@media (max-width: 900px) { .grid { grid-template-columns: 1fr; } }
.col-header { font-size: 0.75rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.05em; margin-bottom: 0.5rem; }
.col-header.before { color: var(--muted); }
.col-header.after { color: var(--accent); }
.grid img { width: 100%; border: 1px solid var(--border); border-radius: 6px; }
.desc { font-size: 0.85rem; color: var(--muted); margin-top: 0.5rem; }
</style>
</head>
<body>
<button class="theme-toggle" onclick="document.documentElement.classList.toggle('light'); this.textContent = document.documentElement.classList.contains('light') ? '● Dark' : '○ Light'">○ Light</button>
<h1>fix(chat): load message history on first conversation click</h1>
<p class="subtitle">PR #730 — Closes #729</p>
<div class="summary">
<div class="summary-card">
<h3>Bug</h3>
<p>First conversation click from clean state shows empty placeholder instead of messages</p>
</div>
<div class="summary-card">
<h3>Root Cause</h3>
<p><code>skipNextHistoryRef</code> unconditionally set on empty→non-empty transition, skipping <code>loadHistory()</code></p>
</div>
<div class="summary-card">
<h3>Fix</h3>
<p>Guard skip with <code>expectingRunRef.current</code> — only active during new-chat send flow</p>
</div>
</div>
<div class="env">
Same instance (claw:3000 backend), same user (kai/Master), same agent (glm thục bô), same conversation (Chat a95c8ffa), light theme. Callout borders mark the review area.
</div>
<div class="comparison">
<h2>Chat — First Click from Clean State</h2>
<div class="grid">
<div>
<div class="col-header before">BEFORE</div>
<img src="before-chat-first-click.png" alt="Before: empty placeholder on first click">
<p class="desc">Before: Clicking "Chat a95c8ffa" from initial state — conversation highlighted but main area shows "Start a conversation" placeholder. Messages never load.</p>
</div>
<div>
<div class="col-header after">AFTER</div>
<img src="after-chat-first-click.png" alt="After: messages load on first click">
<p class="desc">After: Same click — conversation messages load immediately. The 5-message history is displayed correctly on first click.</p>
</div>
</div>
</div>
</body>
</html>
-25
View File
@@ -1,25 +0,0 @@
=== RUN TestSetupToolRegistryExecWorkspacePaths
2026/04/07 21:24:13 INFO memory + knowledge graph tools registered (PG-backed)
2026/04/07 21:24:13 INFO web_search tool enabled
2026/04/07 21:24:13 INFO web_fetch tool enabled policy="" blocked=0
2026/04/07 21:24:13 INFO tts enabled provider=edge auto=off
2026/04/07 21:24:13 INFO tool rate limiting enabled per_hour=150
2026/04/07 21:24:13 INFO exec approval enabled security=full ask=off
=== RUN TestSetupToolRegistryExecWorkspacePaths/personal_workspace_prefixed_uploads_allowed
2026/04/07 21:24:13 WARN secure_cli.lookup: store is nil, skipping credentialed exec command="printf '%s' file=@\"/var/folders/t9/0cyfylls7d1gdy_6qp8kn2qc0000gn/T/TestSetupToolRegistryExecWorkspacePaths3385685647/001/personal/.uploads/Quarterly Report.png\""
=== RUN TestSetupToolRegistryExecWorkspacePaths/team_workspace_quoted_input_allowed
2026/04/07 21:24:13 WARN secure_cli.lookup: store is nil, skipping credentialed exec command="printf '%s' --input=\"/var/folders/t9/0cyfylls7d1gdy_6qp8kn2qc0000gn/T/TestSetupToolRegistryExecWorkspacePaths3385685647/001/teams/team-123/Quarterly Report.png\""
=== RUN TestSetupToolRegistryExecWorkspacePaths/legacy_dotgoclaw_uploads_layout_allowed
2026/04/07 21:24:13 WARN secure_cli.lookup: store is nil, skipping credentialed exec command="cp \"/var/folders/t9/0cyfylls7d1gdy_6qp8kn2qc0000gn/T/TestSetupToolRegistryExecWorkspacePaths3385685647/001/.goclaw/goclaw-workspace/ws/system/uploads/Quarterly Report.png\" \"/var/folders/t9/0cyfylls7d1gdy_6qp8kn2qc0000gn/T/TestSetupToolRegistryExecWorkspacePaths3385685647/002/partner.png\""
=== RUN TestSetupToolRegistryExecWorkspacePaths/team_workspace_symlink_escape_denied
=== RUN TestSetupToolRegistryExecWorkspacePaths/unrelated_data_dir_path_denied
=== RUN TestSetupToolRegistryExecWorkspacePaths/workspace_local_dotgoclaw_denied
--- PASS: TestSetupToolRegistryExecWorkspacePaths (0.02s)
--- PASS: TestSetupToolRegistryExecWorkspacePaths/personal_workspace_prefixed_uploads_allowed (0.01s)
--- PASS: TestSetupToolRegistryExecWorkspacePaths/team_workspace_quoted_input_allowed (0.00s)
--- PASS: TestSetupToolRegistryExecWorkspacePaths/legacy_dotgoclaw_uploads_layout_allowed (0.01s)
--- PASS: TestSetupToolRegistryExecWorkspacePaths/team_workspace_symlink_escape_denied (0.00s)
--- PASS: TestSetupToolRegistryExecWorkspacePaths/unrelated_data_dir_path_denied (0.00s)
--- PASS: TestSetupToolRegistryExecWorkspacePaths/workspace_local_dotgoclaw_denied (0.00s)
PASS
ok github.com/nextlevelbuilder/goclaw/cmd 0.041s
-94
View File
@@ -1,94 +0,0 @@
go: downloading github.com/golang-migrate/migrate/v4 v4.19.1
go: downloading github.com/jackc/pgx/v5 v5.6.0
go: downloading github.com/google/uuid v1.6.0
go: downloading github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674
go: downloading github.com/charmbracelet/huh v0.8.0
go: downloading github.com/spf13/cobra v1.10.2
go: downloading github.com/bwmarrin/discordgo v0.29.0
go: downloading golang.org/x/sync v0.19.0
go: downloading github.com/slack-go/slack v0.19.0
go: downloading github.com/mattn/go-runewidth v0.0.16
go: downloading github.com/mymmrac/telego v1.6.0
go: downloading github.com/fsnotify/fsnotify v1.9.0
go: downloading github.com/titanous/json5 v1.0.0
go: downloading github.com/disintegration/imaging v1.6.2
go: downloading golang.org/x/image v0.27.0
go: downloading golang.org/x/time v0.14.0
go: downloading github.com/mark3labs/mcp-go v0.44.0
go: downloading github.com/adhocore/gronx v1.19.6
go: downloading github.com/go-rod/rod v0.116.2
go: downloading github.com/lib/pq v1.10.9
go: downloading github.com/mattn/go-shellwords v1.0.12
go: downloading golang.org/x/net v0.49.0
go: downloading golang.org/x/text v0.33.0
go: downloading github.com/rivo/uniseg v0.4.7
go: downloading github.com/catppuccin/go v0.3.0
go: downloading github.com/charmbracelet/bubbles v0.21.1-0.20250623103423-23b8fd6302d7
go: downloading github.com/charmbracelet/bubbletea v1.3.6
go: downloading github.com/charmbracelet/lipgloss v1.1.0
go: downloading github.com/charmbracelet/x/cellbuf v0.0.13
go: downloading github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0
go: downloading github.com/mitchellh/hashstructure/v2 v2.0.2
go: downloading github.com/spf13/pflag v1.0.9
go: downloading golang.org/x/crypto v0.47.0
go: downloading golang.org/x/sys v0.42.0
go: downloading github.com/valyala/fasthttp v1.69.0
go: downloading github.com/invopop/jsonschema v0.13.0
go: downloading github.com/spf13/cast v1.7.1
go: downloading github.com/yosida95/uritemplate/v3 v3.0.2
go: downloading github.com/dustin/go-humanize v1.0.1
go: downloading github.com/charmbracelet/x/ansi v0.9.3
go: downloading github.com/charmbracelet/x/term v0.2.1
go: downloading github.com/atotto/clipboard v0.1.4
go: downloading github.com/grbit/go-json v0.11.0
go: downloading github.com/valyala/fastjson v1.6.7
go: downloading github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6
go: downloading github.com/muesli/cancelreader v0.2.2
go: downloading github.com/muesli/termenv v0.16.0
go: downloading github.com/wk8/go-ordered-map/v2 v2.1.8
go: downloading github.com/ysmood/goob v0.4.0
go: downloading github.com/ysmood/got v0.40.0
go: downloading github.com/ysmood/gson v0.7.3
go: downloading github.com/ysmood/fetchup v0.2.3
go: downloading github.com/ysmood/leakless v0.9.0
go: downloading github.com/andybalholm/brotli v1.2.0
go: downloading github.com/klauspost/compress v1.18.2
go: downloading github.com/valyala/bytebufferpool v1.0.0
go: downloading github.com/jackc/pgpassfile v1.0.0
go: downloading github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761
go: downloading github.com/jackc/puddle/v2 v2.2.2
go: downloading github.com/lucasb-eyer/go-colorful v1.2.0
go: downloading github.com/bahlo/generic-list-go v0.2.0
go: downloading github.com/buger/jsonparser v1.1.1
go: downloading github.com/mailru/easyjson v0.7.7
go: downloading gopkg.in/yaml.v3 v3.0.1
go: downloading github.com/aymanbagabas/go-osc52/v2 v2.0.1
go: downloading github.com/mattn/go-isatty v0.0.20
go: downloading github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc
go: downloading github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e
=== RUN TestSetupToolRegistryExecWorkspacePaths
2026/04/08 01:24:40 INFO memory + knowledge graph tools registered (PG-backed)
2026/04/08 01:24:40 INFO web_search tool enabled
2026/04/08 01:24:40 INFO web_fetch tool enabled policy="" blocked=0
2026/04/08 01:24:40 INFO tts enabled provider=edge auto=off
2026/04/08 01:24:40 INFO tool rate limiting enabled per_hour=150
2026/04/08 01:24:40 INFO exec approval enabled security=full ask=off
=== RUN TestSetupToolRegistryExecWorkspacePaths/personal_workspace_prefixed_uploads_allowed
gateway_setup_exec_workspace_test.go:133: denied = true, want false; output = command denied by safety policy: matches pattern /tmp/TestSetupToolRegistryExecWorkspacePaths1657441769/001
=== RUN TestSetupToolRegistryExecWorkspacePaths/team_workspace_quoted_input_allowed
gateway_setup_exec_workspace_test.go:133: denied = true, want false; output = command denied by safety policy: matches pattern /tmp/TestSetupToolRegistryExecWorkspacePaths1657441769/001
=== RUN TestSetupToolRegistryExecWorkspacePaths/legacy_dotgoclaw_uploads_layout_allowed
gateway_setup_exec_workspace_test.go:133: denied = true, want false; output = command denied by safety policy: matches pattern /tmp/TestSetupToolRegistryExecWorkspacePaths1657441769/001
=== RUN TestSetupToolRegistryExecWorkspacePaths/team_workspace_symlink_escape_denied
=== RUN TestSetupToolRegistryExecWorkspacePaths/unrelated_data_dir_path_denied
=== RUN TestSetupToolRegistryExecWorkspacePaths/workspace_local_dotgoclaw_denied
--- FAIL: TestSetupToolRegistryExecWorkspacePaths (0.00s)
--- FAIL: TestSetupToolRegistryExecWorkspacePaths/personal_workspace_prefixed_uploads_allowed (0.00s)
--- FAIL: TestSetupToolRegistryExecWorkspacePaths/team_workspace_quoted_input_allowed (0.00s)
--- FAIL: TestSetupToolRegistryExecWorkspacePaths/legacy_dotgoclaw_uploads_layout_allowed (0.00s)
--- PASS: TestSetupToolRegistryExecWorkspacePaths/team_workspace_symlink_escape_denied (0.00s)
--- PASS: TestSetupToolRegistryExecWorkspacePaths/unrelated_data_dir_path_denied (0.00s)
--- PASS: TestSetupToolRegistryExecWorkspacePaths/workspace_local_dotgoclaw_denied (0.00s)
FAIL
FAIL github.com/nextlevelbuilder/goclaw/cmd 0.019s
FAIL
Binary file not shown.

Before

Width:  |  Height:  |  Size: 183 KiB

-228
View File
@@ -1,228 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>PR #748 — fix(exec): allow uploaded files in active workspaces</title>
<style>
:root {
--bg: #0d0d0d;
--fg: #e8e4df;
--card: #1a1714;
--border: #2d2923;
--accent: #ef4444;
--good: #22c55e;
--muted: #8a837d;
--code: #151210;
}
.light {
--bg: #f8f6f3;
--fg: #1a1714;
--card: #ffffff;
--border: #e5e0da;
--accent: #dc2626;
--good: #15803d;
--muted: #6b6560;
--code: #f3f0eb;
}
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
background: var(--bg);
color: var(--fg);
padding: 2rem;
line-height: 1.5;
}
.theme-toggle {
position: fixed;
top: 1rem;
right: 1rem;
background: var(--card);
border: 1px solid var(--border);
color: var(--fg);
padding: 0.5rem 1rem;
border-radius: 6px;
cursor: pointer;
z-index: 100;
}
h1 { font-size: 1.5rem; margin-bottom: 0.5rem; }
.subtitle { color: var(--muted); margin-bottom: 2rem; }
.summary { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 1rem; margin-bottom: 2rem; }
.summary-card, .env, .comparison, .supporting {
background: var(--card);
border: 1px solid var(--border);
border-radius: 10px;
}
.summary-card { padding: 1rem; }
.summary-card h3 {
font-size: 0.75rem;
text-transform: uppercase;
color: var(--muted);
margin-bottom: 0.5rem;
letter-spacing: 0.04em;
}
.env, .supporting, .comparison { padding: 1.25rem; margin-bottom: 1.5rem; }
.env { color: var(--muted); font-size: 0.92rem; }
.comparison h2, .supporting h2 {
font-size: 1.05rem;
margin-bottom: 1rem;
}
.grid { display: grid; grid-template-columns: 1fr 1fr; gap: 1rem; }
@media (max-width: 980px) { .grid { grid-template-columns: 1fr; } }
.col-header {
font-size: 0.75rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.05em;
margin-bottom: 0.5rem;
}
.before { color: var(--muted); }
.after { color: var(--accent); }
.panel {
border: 1px solid var(--border);
border-radius: 8px;
overflow: hidden;
background: var(--code);
}
.panel img {
display: block;
width: 100%;
height: auto;
}
.terminal {
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
font-size: 0.8rem;
line-height: 1.45;
white-space: pre-wrap;
overflow-wrap: anywhere;
padding: 1rem;
min-height: 320px;
}
.hl-bad {
display: inline-block;
background: rgba(239, 68, 68, 0.14);
border-left: 3px solid var(--accent);
padding: 0.05rem 0.35rem;
}
.hl-good {
display: inline-block;
background: rgba(34, 197, 94, 0.14);
border-left: 3px solid var(--good);
padding: 0.05rem 0.35rem;
}
.desc {
color: var(--muted);
font-size: 0.88rem;
margin-top: 0.65rem;
}
code {
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
background: var(--code);
padding: 0.12rem 0.32rem;
border-radius: 4px;
}
ul { margin-left: 1.1rem; color: var(--muted); }
</style>
</head>
<body>
<button class="theme-toggle" onclick="document.documentElement.classList.toggle('light'); this.textContent = document.documentElement.classList.contains('light') ? '● Dark' : '○ Light'">○ Light</button>
<h1>fix(exec): allow uploaded files in active workspaces</h1>
<p class="subtitle">PR #748 — visual proof pack for issue #739</p>
<div class="summary">
<div class="summary-card">
<h3>Bug</h3>
<p>Active workspace files under deny-roots were rejected by <code>exec</code> even when they belonged to the current upload/team scope.</p>
</div>
<div class="summary-card">
<h3>Root Cause</h3>
<p>Path exemptions were static and non-shell-aware, so quoted, prefixed, and legacy screenshot-era upload paths still matched the deny root.</p>
</div>
<div class="summary-card">
<h3>Fix</h3>
<p>Shell-aware parsing + canonical path checks now exempt the active personal <code>.uploads</code> subtree, the legacy <code>uploads</code> subtree, and the active team workspace.</p>
</div>
</div>
<div class="env">
Before source: stable <code>claw:3000</code> backed by the unfixed <code>/opt/goclaw</code> deployment (<code>v2.7.1</code>). After source: temporary PR deployment on <code>claw:3001</code>, backed by <code>goclaw-pr748:local</code> built from the fixed code commit <code>83fa1342</code>. Both UI captures use fresh chats with the same user and <code>GLM thục bô</code>. Callout border marks the reviewed <code>exec</code> row.
</div>
<div class="comparison">
<h2>UI Before / After</h2>
<div class="grid">
<div>
<div class="col-header before">Before</div>
<div class="panel">
<img src="pr748-before-absolute-goclaw-blocked.png" alt="Before: stable claw blocks absolute .goclaw upload path">
</div>
<p class="desc">Stable <code>claw</code> release, fresh chat with <code>GLM thục bô</code>: the agent runs the exact absolute <code>/app/.goclaw/.../.uploads/...jpg</code> copy command and the <code>exec</code> row fails with the safety-policy <code>.goclaw/</code> denial.</p>
</div>
<div>
<div class="col-header after">After</div>
<div class="panel">
<img src="pr748-after-absolute-goclaw-allowed.png" alt="After: fixed temporary claw deployment allows absolute .goclaw upload path">
</div>
<p class="desc">Temporary fixed deployment on <code>claw:3001</code>, fresh chat with the same agent: the same absolute <code>/app/.goclaw/.../.uploads/...jpg</code> copy command now completes successfully and returns <code>/tmp/partner-abs.png</code>.</p>
</div>
</div>
</div>
<div class="comparison">
<h2>Gateway Reproduction</h2>
<div class="grid">
<div>
<div class="col-header before">Before</div>
<div class="panel">
<pre class="terminal">$ ssh claw 'docker run --rm -v /tmp/goclaw-739-before:/src -w /src golang:1.26 sh -lc "/usr/local/go/bin/go test ./cmd -run TestSetupToolRegistryExecWorkspacePaths -count=1 -v"'
=== RUN TestSetupToolRegistryExecWorkspacePaths
=== RUN TestSetupToolRegistryExecWorkspacePaths/personal_workspace_prefixed_uploads_allowed
<span class="hl-bad">gateway_setup_exec_workspace_test.go:133: denied = true, want false; output = command denied by safety policy: matches pattern /tmp/TestSetupToolRegistryExecWorkspacePaths.../001</span>
=== RUN TestSetupToolRegistryExecWorkspacePaths/team_workspace_quoted_input_allowed
<span class="hl-bad">gateway_setup_exec_workspace_test.go:133: denied = true, want false; output = command denied by safety policy: matches pattern /tmp/TestSetupToolRegistryExecWorkspacePaths.../001</span>
=== RUN TestSetupToolRegistryExecWorkspacePaths/legacy_dotgoclaw_uploads_layout_allowed
<span class="hl-bad">gateway_setup_exec_workspace_test.go:133: denied = true, want false; output = command denied by safety policy: matches pattern /tmp/TestSetupToolRegistryExecWorkspacePaths.../001</span>
FAIL</pre>
</div>
<p class="desc">Baseline source on <code>claw</code>: the exact gateway wiring still rejects all three representative workspace-path forms.</p>
</div>
<div>
<div class="col-header after">After</div>
<div class="panel">
<pre class="terminal">$ go test ./cmd -run TestSetupToolRegistryExecWorkspacePaths -count=1 -v
=== RUN TestSetupToolRegistryExecWorkspacePaths
=== RUN TestSetupToolRegistryExecWorkspacePaths/personal_workspace_prefixed_uploads_allowed
=== RUN TestSetupToolRegistryExecWorkspacePaths/team_workspace_quoted_input_allowed
=== RUN TestSetupToolRegistryExecWorkspacePaths/legacy_dotgoclaw_uploads_layout_allowed
<span class="hl-good">--- PASS: TestSetupToolRegistryExecWorkspacePaths (0.01s)</span>
<span class="hl-good"> --- PASS: personal_workspace_prefixed_uploads_allowed</span>
<span class="hl-good"> --- PASS: team_workspace_quoted_input_allowed</span>
<span class="hl-good"> --- PASS: legacy_dotgoclaw_uploads_layout_allowed</span>
PASS</pre>
</div>
<p class="desc">Current worktree: the same gateway path handling now passes for personal uploads, team workspace paths, and the screenshot-era legacy <code>.goclaw/.../uploads</code> layout.</p>
</div>
</div>
</div>
<div class="supporting">
<h2>Supporting Live Recreation on <code>claw</code></h2>
<div class="panel">
<img src="claw-live-glm-thuc-bo-before.png" alt="Live claw recreation with GLM thục bô">
</div>
<p class="desc">
This earlier live run documents the same production agent and workspace context on stable <code>claw</code>. The report’s primary Before/After pair above is the clean isolated proof you asked for; this screenshot is kept as supporting context for the reproduction journey.
</p>
</div>
<div class="comparison">
<h2>What This Proves</h2>
<ul>
<li>Quoted/prefixed upload arguments now survive the deny matcher when they point into the active personal <code>.uploads</code> subtree, the legacy <code>.goclaw/.../uploads</code> subtree, or the active team workspace.</li>
<li>Symlink escapes, unrelated <code>dataDir</code> files, and workspace-local <code>.goclaw/*</code> remain denied.</li>
<li>The visual behavior difference is now shown on two real frontends: stable <code>claw:3000</code> Before and temporary fixed <code>claw:3001</code> After, both using fresh conversations.</li>
</ul>
</div>
</body>
</html>
Binary file not shown.

Before

Width:  |  Height:  |  Size: 142 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 230 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 112 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 141 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 145 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 119 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 138 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 97 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 141 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 161 KiB