feat(viewer): restyle session detail sections (#1358)

* feat(viewer): restyle session detail sections

* fix(viewer): remove duplicate tool call icon

* fix(viewer): keep tool call icon on label

* test(viewer): cover tool call icon placement
This commit is contained in:
DiegoCluv7
2026-09-17 20:59:40 +08:00
committed by GitHub
parent 8d57bc9e83
commit 189be5b024
4 changed files with 139 additions and 76 deletions
+6
View File
@@ -331,6 +331,12 @@ func parseTemplate(name string) (*template.Template, error) {
"formatNumber": formatNumber,
"icon": inlineIcon,
"add": func(a, b int) int { return a + b },
"countLabel": func(n int, singular, plural string) string {
if n == 1 {
return strconv.Itoa(n) + " " + singular
}
return strconv.Itoa(n) + " " + plural
},
"cardCount": func(tasks map[TaskType][]*TaskCard) int {
n := 0
for _, cards := range tasks {
+50 -2
View File
@@ -310,6 +310,51 @@ func TestRenderTemplate_HidesEmptyConversationsSection(t *testing.T) {
}
}
func TestRenderTemplate_RendersConversationToolCallDetails(t *testing.T) {
rr := httptest.NewRecorder()
renderTemplate(rr, "session.html", sessionPageData{
EncodedRepo: "repo",
RepoName: "MyRepo",
Session: &ViewSession{
Summary: SessionSummary{SessionID: "abc", CWD: "/test"},
Files: []*FileGroup{{
FilePath: "internal/viewer/server.go",
Tasks: map[TaskType][]*TaskCard{
MainTask: {{
RequestNo: 1,
Model: "model-a",
PromptTokens: 10,
CompletionTokens: 20,
DurationMs: 30,
ToolCalls: []ToolCallInfo{{
Name: "code_search",
Arguments: `{"query":"viewer"}`,
Result: "matched server.go",
Ok: true,
}},
}},
},
}},
},
})
body := rr.Body.String()
if strings.Contains(body, "1 files") || strings.Contains(body, "1 requests") {
t.Error("single-item counts should use singular labels")
}
if strings.Contains(body, "⚙") {
t.Error("tool call controls should use the shared SVG icon, not a Unicode glyph")
}
for _, want := range []string{
"Conversations", "1 file", "1 request", "internal/viewer/server.go", "Request #1", "model-a",
"Tool Calls (1)", "code_search", "Arguments", "matched server.go", "<svg",
} {
if !strings.Contains(body, want) {
t.Errorf("rendered session page missing %q", want)
}
}
}
func TestRenderTemplate_ExecutionError(t *testing.T) {
rr := httptest.NewRecorder()
// Pass wrong data type to trigger template execution error
@@ -457,8 +502,11 @@ func TestRenderTemplate_ToolCallIconIsInlineSVG(t *testing.T) {
if strings.Contains(body, "&#9881;") || strings.Contains(body, "⚙") {
t.Error("tool-call icon should no longer use the unicode gear glyph")
}
if !strings.Contains(body, `<span class="tool-icon" aria-hidden="true"><svg`) {
t.Error("tool-call header should render the inline settings icon")
if !strings.Contains(body, `<span class="tool-calls-icon" aria-hidden="true"><svg`) {
t.Error("tool-calls label should render the inline settings icon")
}
if strings.Contains(body, `class="tool-icon"`) {
t.Error("individual tool-call rows should not render a duplicate settings icon")
}
}
+67 -57
View File
@@ -560,7 +560,7 @@ h3 {
.token-breakdown-toggle::-webkit-details-marker { display: none; }
.token-breakdown-toggle::marker { content: none; }
.token-breakdown-toggle .file-count-badge { margin-left: auto; }
.token-breakdown[open] > .token-breakdown-toggle .chevron-sm { transform: rotate(45deg); }
.token-breakdown[open] > .token-breakdown-toggle .chevron-sm svg { transform: rotate(90deg); }
.token-breakdown-body {
overflow-x: auto;
}
@@ -575,7 +575,7 @@ h3 {
overflow: hidden;
}
.file-list li {
padding: 0.6rem 1.25rem;
padding: 0.65rem 0.9rem;
color: var(--text-secondary);
border-bottom: 1px solid var(--border-subtle);
font-family: var(--mono);
@@ -606,19 +606,19 @@ h3 {
background: var(--surface);
border-radius: var(--radius);
margin-bottom: 0.75rem;
box-shadow: var(--shadow-sm);
box-shadow: none;
border: 1px solid var(--border);
overflow: hidden;
transition: box-shadow var(--transition);
}
.file-accordion:hover {
box-shadow: var(--shadow-md);
box-shadow: var(--shadow-sm);
}
.file-accordion-header {
cursor: pointer;
padding: 1rem 1.25rem;
font-size: 0.95rem;
padding: 0.8rem 0.9rem;
font-size: 0.84rem;
color: var(--text-strong);
user-select: none;
display: flex;
@@ -642,21 +642,17 @@ h3 {
background: var(--surface-alt);
border: 1px solid var(--border-subtle);
flex-shrink: 0;
transition: transform var(--transition), background var(--transition);
color: var(--text-muted);
transition: background var(--transition);
}
.chevron::after {
content: "";
width: 6px;
height: 6px;
border-right: 1.5px solid var(--text-muted);
border-bottom: 1.5px solid var(--text-muted);
transform: rotate(-45deg);
margin-top: -1px;
.chevron svg {
width: 0.7rem;
height: 0.7rem;
display: block;
transition: transform var(--transition);
}
.file-accordion[open] > .file-accordion-header .chevron::after {
transform: rotate(45deg);
margin-top: -2px;
.file-accordion[open] > .file-accordion-header .chevron svg {
transform: rotate(90deg);
}
.file-path {
@@ -687,12 +683,24 @@ h3 {
}
.file-accordion-body {
padding: 1rem 1.25rem 1.25rem;
padding: 0.75rem 0.9rem 0.9rem;
}
.section-accordion {
margin-top: 1.25rem;
border-radius: var(--radius);
background: var(--surface);
box-shadow: var(--shadow-sm);
}
.section-accordion > .file-accordion-header {
cursor: pointer;
padding: 0.9rem 1rem 0.35rem;
font-size: 0.82rem;
}
.section-accordion > .file-accordion-header:hover { background: transparent; }
.section-accordion:not([open]) > .file-accordion-header { padding-bottom: 0.9rem; }
.section-accordion[open] > .file-accordion-header { border-bottom: none; }
.section-accordion > .file-accordion-body { padding: 0.6rem 1rem 1rem; }
.section-title {
flex: 1;
font-weight: 600;
@@ -703,14 +711,14 @@ h3 {
}
/* Task Group */
.task-group { margin-bottom: 1.25rem; }
.task-group { margin-bottom: 1rem; }
.task-group:last-child { margin-bottom: 0; }
.task-type-label {
font-size: 0.7rem;
font-size: 0.64rem;
text-transform: uppercase;
color: var(--text-muted);
margin-bottom: 0.75rem;
margin: 0.1rem 0 0.45rem;
letter-spacing: 0.06em;
display: flex;
align-items: center;
@@ -737,7 +745,7 @@ h3 {
.card {
border: 1px solid var(--border);
border-radius: var(--radius);
margin-bottom: 0.85rem;
margin-bottom: 0.6rem;
overflow: hidden;
transition: box-shadow var(--transition), border-color var(--transition);
}
@@ -754,8 +762,8 @@ h3 {
.card-header {
background: var(--surface-alt);
padding: 0.7rem 1rem;
font-size: 0.85rem;
padding: 0.45rem 0.65rem;
font-size: 0.76rem;
display: flex;
align-items: center;
gap: 0.6rem;
@@ -766,12 +774,12 @@ h3 {
.request-label {
font-weight: 600;
color: var(--text-strong);
font-size: 0.82rem;
font-size: 0.72rem;
}
/* Badges */
.badge {
padding: 0.2em 0.6em;
padding: 0.18em 0.5em;
border-radius: 20px;
font-size: 0.7em;
font-weight: 500;
@@ -876,7 +884,7 @@ h3 {
.tool-calls-section {
background: var(--surface-inset);
border-top: 1px solid var(--border);
padding: 0.85rem 1rem;
padding: 0.65rem;
}
.tool-calls-label {
@@ -885,7 +893,19 @@ h3 {
text-transform: uppercase;
color: var(--text-muted);
letter-spacing: 0.05em;
margin-bottom: 0.6rem;
margin-bottom: 0.45rem;
display: flex;
align-items: center;
gap: 0.35rem;
}
.tool-calls-icon {
color: var(--text-faint);
display: inline-flex;
}
.tool-calls-icon svg {
width: 0.8rem;
height: 0.8rem;
display: block;
}
.tool-call-item {
@@ -907,7 +927,7 @@ h3 {
}
.tool-call-header {
padding: 0.55rem 0.85rem;
padding: 0.45rem 0.65rem;
display: flex;
align-items: center;
gap: 0.5rem;
@@ -915,17 +935,6 @@ h3 {
flex-wrap: wrap;
}
.tool-icon {
color: var(--text-faint);
display: inline-flex;
align-items: center;
}
.tool-icon svg {
width: 0.85em;
height: 0.85em;
display: block;
}
.tool-name {
color: var(--tool-name);
font-family: var(--mono);
@@ -957,17 +966,20 @@ h3 {
/* Small chevron for tool details */
.chevron-sm {
display: inline-block;
width: 5px;
height: 5px;
border-right: 1.5px solid var(--link);
border-bottom: 1.5px solid var(--link);
transform: rotate(-45deg);
transition: transform var(--transition);
display: inline-flex;
width: 0.7rem;
height: 0.7rem;
color: var(--link);
flex-shrink: 0;
}
.tool-detail[open] > .tool-detail-toggle .chevron-sm {
transform: rotate(45deg);
.chevron-sm svg {
width: 100%;
height: 100%;
display: block;
transition: transform var(--transition);
}
.tool-detail[open] > .tool-detail-toggle .chevron-sm svg {
transform: rotate(90deg);
}
.tool-detail-body {
@@ -1001,12 +1013,11 @@ h3 {
font-weight: 500;
}
.error-detail-toggle:hover { background: color-mix(in srgb, var(--danger) 10%, transparent); }
.error-detail[open] > .error-detail-toggle .chevron-sm {
transform: rotate(45deg);
.error-detail[open] > .error-detail-toggle .chevron-sm svg {
transform: rotate(90deg);
}
.error-detail-toggle .chevron-sm {
border-right-color: var(--danger);
border-bottom-color: var(--danger);
color: var(--danger);
}
.error-detail-body {
padding: 0 0.85rem 0.75rem;
@@ -1165,9 +1176,8 @@ p {
.comment-file-group[open] > .comment-file-header {
border-bottom: 1px solid var(--border-subtle);
}
.comment-file-group[open] > .comment-file-header .chevron::after {
transform: rotate(45deg);
margin-top: -2px;
.comment-file-group[open] > .comment-file-header .chevron svg {
transform: rotate(90deg);
}
.comment-file-body {
+16 -17
View File
@@ -85,9 +85,9 @@
{{with .Session.TokenUsage.FileTokenBreakdown}}
<details class="token-breakdown">
<summary class="token-breakdown-toggle">
<span class="chevron-sm"></span>
<span class="chevron-sm" aria-hidden="true">{{icon "chevron-right"}}</span>
<span>File breakdown</span>
<span class="file-count-badge">{{len .}} files</span>
<span class="file-count-badge">{{countLabel (len .) "file" "files"}}</span>
</summary>
<div class="token-breakdown-body">
<table class="token-table">
@@ -145,7 +145,7 @@
{{range groupCommentsByFile .Session.Comments}}
<details class="comment-file-group" open>
<summary class="comment-file-header">
<span class="chevron"></span>
<span class="chevron" aria-hidden="true">{{icon "chevron-right"}}</span>
<span class="file-path">{{.FilePath}}</span>
<span class="file-count-badge" data-comment-count>{{len .Comments}} comments</span>
</summary>
@@ -196,9 +196,9 @@
{{if .Session.Summary.FilesReviewed}}
<details class="file-accordion section-accordion">
<summary class="file-accordion-header">
<span class="chevron"></span>
<span class="chevron" aria-hidden="true">{{icon "chevron-right"}}</span>
<span class="section-title">Files Reviewed</span>
<span class="file-count-badge">{{len .Session.Summary.FilesReviewed}} files</span>
<span class="file-count-badge">{{countLabel (len .Session.Summary.FilesReviewed) "file" "files"}}</span>
</summary>
<div class="file-accordion-body">
<ul class="file-list">
@@ -213,9 +213,9 @@
{{if .Session.SessionTasks}}
<details class="file-accordion section-accordion" open>
<summary class="file-accordion-header">
<span class="chevron"></span>
<span class="chevron" aria-hidden="true">{{icon "chevron-right"}}</span>
<span class="section-title">Session Tasks</span>
<span class="file-count-badge">{{len .Session.SessionTasks}} tasks</span>
<span class="file-count-badge">{{countLabel (len .Session.SessionTasks) "task" "tasks"}}</span>
</summary>
<div class="file-accordion-body">
<div class="conversations">
@@ -230,9 +230,9 @@
{{if .Session.Files}}
<details class="file-accordion section-accordion">
<summary class="file-accordion-header">
<span class="chevron"></span>
<span class="chevron" aria-hidden="true">{{icon "chevron-right"}}</span>
<span class="section-title">Conversations</span>
<span class="file-count-badge">{{len .Session.Files}} files</span>
<span class="file-count-badge">{{countLabel (len .Session.Files) "file" "files"}}</span>
</summary>
<div class="file-accordion-body">
<div class="conversations">
@@ -254,9 +254,9 @@
{{define "task-cards"}}
<details class="file-accordion">
<summary class="file-accordion-header">
<span class="chevron"></span>
<span class="chevron" aria-hidden="true">{{icon "chevron-right"}}</span>
<span class="file-path">{{sessionTaskLabel .FilePath}}</span>
<span class="file-count-badge">{{cardCount .Tasks}} requests</span>
<span class="file-count-badge">{{countLabel (cardCount .Tasks) "request" "requests"}}</span>
</summary>
<div class="file-accordion-body">
{{range $tg := orderedTasks .Tasks}}
@@ -278,7 +278,7 @@
{{if .Error}}
<details class="error-detail">
<summary class="error-detail-toggle">
<span class="chevron-sm"></span>
<span class="chevron-sm" aria-hidden="true">{{icon "chevron-right"}}</span>
<span>Error Detail</span>
</summary>
<div class="error-detail-body">
@@ -289,7 +289,7 @@
{{with .ReasoningContent}}
<details class="tool-detail">
<summary class="tool-detail-toggle">
<span class="chevron-sm"></span>
<span class="chevron-sm" aria-hidden="true">{{icon "chevron-right"}}</span>
<span>Reasoning</span>
</summary>
<div class="tool-detail-body">
@@ -314,7 +314,7 @@
{{with $card.ResponseContent}}
<details class="tool-detail">
<summary class="tool-detail-toggle">
<span class="chevron-sm"></span>
<span class="chevron-sm" aria-hidden="true">{{icon "chevron-right"}}</span>
<span>Raw LLM response</span>
</summary>
<div class="tool-detail-body">
@@ -330,18 +330,17 @@
{{end}}
{{if .ToolCalls}}
<div class="tool-calls-section">
<div class="tool-calls-label">Tool Calls ({{len .ToolCalls}})</div>
<div class="tool-calls-label"><span class="tool-calls-icon" aria-hidden="true">{{icon "settings"}}</span>Tool Calls ({{len .ToolCalls}})</div>
{{range .ToolCalls}}
<div class="tool-call-item {{if not .Ok}}tool-call-error{{end}}">
<div class="tool-call-header">
<span class="tool-icon" aria-hidden="true">{{icon "settings"}}</span>
<strong class="tool-name">{{.Name}}</strong>
{{if .DurationMs}}<span class="badge badge-duration">{{.DurationMs}}ms</span>{{end}}
{{if not .Ok}}<span class="badge badge-error">Failed</span>{{end}}
</div>
<details class="tool-detail">
<summary class="tool-detail-toggle">
<span class="chevron-sm"></span>
<span class="chevron-sm" aria-hidden="true">{{icon "chevron-right"}}</span>
<span>Show Details</span>
</summary>
<div class="tool-detail-body">