diff --git a/.wave/pipelines/audit-dead-code.yaml b/.wave/pipelines/audit-dead-code.yaml index 73f07cca..83bc492e 100644 --- a/.wave/pipelines/audit-dead-code.yaml +++ b/.wave/pipelines/audit-dead-code.yaml @@ -1,7 +1,11 @@ kind: WavePipeline metadata: name: audit-dead-code - description: "Find dead or redundant code, remove it, and commit to a feature branch" + description: >- + Dead code detection pipeline that scans the codebase for unused functions, + unreachable branches, orphaned files, and redundant implementations. + Removes confirmed dead code, verifies tests still pass, and commits + the cleanup to a feature branch. release: true requires: diff --git a/.wave/pipelines/impl-issue.yaml b/.wave/pipelines/impl-issue.yaml index f1c05a02..3de0369f 100644 --- a/.wave/pipelines/impl-issue.yaml +++ b/.wave/pipelines/impl-issue.yaml @@ -1,7 +1,11 @@ kind: WavePipeline metadata: name: impl-issue - description: "Implement an issue end-to-end: fetch, assess, plan, implement, create PR" + description: >- + End-to-end issue implementation pipeline that fetches a GitHub issue, + assesses feasibility and complexity, generates an implementation plan, + writes the code in an isolated worktree, and opens a pull request + with the changes. release: true chat_context: diff --git a/.wave/pipelines/ops-hello-world.yaml b/.wave/pipelines/ops-hello-world.yaml index 53c4e896..c77a2b83 100644 --- a/.wave/pipelines/ops-hello-world.yaml +++ b/.wave/pipelines/ops-hello-world.yaml @@ -1,7 +1,10 @@ kind: WavePipeline metadata: name: ops-hello-world - description: "Simple test pipeline to verify Wave is working" + description: >- + Minimal smoke-test pipeline that validates Wave's core execution loop + is functional. Runs a single trivial step to confirm adapter connectivity, + workspace creation, and artifact handling all work end-to-end. release: true input: diff --git a/.wave/pipelines/ops-implement-epic.yaml b/.wave/pipelines/ops-implement-epic.yaml index 0058e4f6..44ead163 100644 --- a/.wave/pipelines/ops-implement-epic.yaml +++ b/.wave/pipelines/ops-implement-epic.yaml @@ -15,7 +15,11 @@ kind: WavePipeline metadata: name: ops-implement-epic - description: "Implement all child issues from a parent epic, gate on CI, then summarise" + description: >- + Epic implementation pipeline that iterates over all child issues of a + parent epic, implementing each one sequentially. Gates on CI passing + between issues, and produces a summary of all changes, PRs created, + and any issues that were skipped or failed. category: composition release: true diff --git a/.wave/pipelines/ops-parallel-audit.yaml b/.wave/pipelines/ops-parallel-audit.yaml index ae5b3012..5ab4571b 100644 --- a/.wave/pipelines/ops-parallel-audit.yaml +++ b/.wave/pipelines/ops-parallel-audit.yaml @@ -17,7 +17,11 @@ kind: WavePipeline metadata: name: ops-parallel-audit - description: "Fan out security, dead-code, and DX audits in parallel then merge findings" + description: >- + Parallel audit pipeline that fans out security vulnerability scanning, + dead-code detection, and developer experience analysis as concurrent + sub-pipelines. Aggregates all findings into a unified report sorted + by severity and actionability. category: composition release: true diff --git a/.wave/pipelines/ops-pr-fix-review.yaml b/.wave/pipelines/ops-pr-fix-review.yaml index 1e77f028..b6cec24f 100644 --- a/.wave/pipelines/ops-pr-fix-review.yaml +++ b/.wave/pipelines/ops-pr-fix-review.yaml @@ -1,7 +1,10 @@ kind: WavePipeline metadata: name: ops-pr-fix-review - description: "Review → triage → fix → push → reply to review comments on a PR" + description: >- + Automated PR review-fix pipeline that fetches review comments, triages + findings by severity, applies code fixes in priority order, pushes the + changes, and replies to each review thread with resolution details. category: composition release: true diff --git a/.wave/pipelines/ops-pr-review.yaml b/.wave/pipelines/ops-pr-review.yaml index 37ff5109..29d86a57 100644 --- a/.wave/pipelines/ops-pr-review.yaml +++ b/.wave/pipelines/ops-pr-review.yaml @@ -1,7 +1,11 @@ kind: WavePipeline metadata: name: ops-pr-review - description: "Pull request code review with automated security and quality analysis" + description: >- + Automated pull request review pipeline that performs security scanning, + code quality analysis, and architectural assessment. Reviews each changed + file for vulnerabilities, anti-patterns, and maintainability concerns, + then posts structured findings as review comments on the PR. release: true chat_context: diff --git a/.wave/pipelines/ops-rewrite.yaml b/.wave/pipelines/ops-rewrite.yaml index ac1d72c2..c3d2e08e 100644 --- a/.wave/pipelines/ops-rewrite.yaml +++ b/.wave/pipelines/ops-rewrite.yaml @@ -1,7 +1,11 @@ kind: WavePipeline metadata: name: ops-rewrite - description: "Analyze and rewrite poorly documented issues" + description: >- + Issue quality improvement pipeline that analyzes under-specified or + poorly documented issues, researches the codebase for context, and + rewrites the issue body with clear acceptance criteria, technical + details, and implementation guidance. release: true skills: diff --git a/internal/defaults/pipelines/audit-dead-code.yaml b/internal/defaults/pipelines/audit-dead-code.yaml index 73f07cca..83bc492e 100644 --- a/internal/defaults/pipelines/audit-dead-code.yaml +++ b/internal/defaults/pipelines/audit-dead-code.yaml @@ -1,7 +1,11 @@ kind: WavePipeline metadata: name: audit-dead-code - description: "Find dead or redundant code, remove it, and commit to a feature branch" + description: >- + Dead code detection pipeline that scans the codebase for unused functions, + unreachable branches, orphaned files, and redundant implementations. + Removes confirmed dead code, verifies tests still pass, and commits + the cleanup to a feature branch. release: true requires: diff --git a/internal/defaults/pipelines/impl-issue.yaml b/internal/defaults/pipelines/impl-issue.yaml index 7459afb1..1fbd3ed1 100644 --- a/internal/defaults/pipelines/impl-issue.yaml +++ b/internal/defaults/pipelines/impl-issue.yaml @@ -1,7 +1,11 @@ kind: WavePipeline metadata: name: impl-issue - description: "Implement an issue end-to-end: fetch, assess, plan, implement, create PR" + description: >- + End-to-end issue implementation pipeline that fetches a GitHub issue, + assesses feasibility and complexity, generates an implementation plan, + writes the code in an isolated worktree, and opens a pull request + with the changes. release: true chat_context: diff --git a/internal/defaults/pipelines/ops-hello-world.yaml b/internal/defaults/pipelines/ops-hello-world.yaml index 53c4e896..c77a2b83 100644 --- a/internal/defaults/pipelines/ops-hello-world.yaml +++ b/internal/defaults/pipelines/ops-hello-world.yaml @@ -1,7 +1,10 @@ kind: WavePipeline metadata: name: ops-hello-world - description: "Simple test pipeline to verify Wave is working" + description: >- + Minimal smoke-test pipeline that validates Wave's core execution loop + is functional. Runs a single trivial step to confirm adapter connectivity, + workspace creation, and artifact handling all work end-to-end. release: true input: diff --git a/internal/defaults/pipelines/ops-implement-epic.yaml b/internal/defaults/pipelines/ops-implement-epic.yaml index 0058e4f6..44ead163 100644 --- a/internal/defaults/pipelines/ops-implement-epic.yaml +++ b/internal/defaults/pipelines/ops-implement-epic.yaml @@ -15,7 +15,11 @@ kind: WavePipeline metadata: name: ops-implement-epic - description: "Implement all child issues from a parent epic, gate on CI, then summarise" + description: >- + Epic implementation pipeline that iterates over all child issues of a + parent epic, implementing each one sequentially. Gates on CI passing + between issues, and produces a summary of all changes, PRs created, + and any issues that were skipped or failed. category: composition release: true diff --git a/internal/defaults/pipelines/ops-parallel-audit.yaml b/internal/defaults/pipelines/ops-parallel-audit.yaml index ae5b3012..5ab4571b 100644 --- a/internal/defaults/pipelines/ops-parallel-audit.yaml +++ b/internal/defaults/pipelines/ops-parallel-audit.yaml @@ -17,7 +17,11 @@ kind: WavePipeline metadata: name: ops-parallel-audit - description: "Fan out security, dead-code, and DX audits in parallel then merge findings" + description: >- + Parallel audit pipeline that fans out security vulnerability scanning, + dead-code detection, and developer experience analysis as concurrent + sub-pipelines. Aggregates all findings into a unified report sorted + by severity and actionability. category: composition release: true diff --git a/internal/defaults/pipelines/ops-pr-review.yaml b/internal/defaults/pipelines/ops-pr-review.yaml index 7bafa1f2..413e5a6e 100644 --- a/internal/defaults/pipelines/ops-pr-review.yaml +++ b/internal/defaults/pipelines/ops-pr-review.yaml @@ -1,7 +1,11 @@ kind: WavePipeline metadata: name: ops-pr-review - description: "Pull request code review with automated security and quality analysis" + description: >- + Automated pull request review pipeline that performs security scanning, + code quality analysis, and architectural assessment. Reviews each changed + file for vulnerabilities, anti-patterns, and maintainability concerns, + then posts structured findings as review comments on the PR. release: true chat_context: diff --git a/internal/defaults/pipelines/ops-rewrite.yaml b/internal/defaults/pipelines/ops-rewrite.yaml index ac1d72c2..c3d2e08e 100644 --- a/internal/defaults/pipelines/ops-rewrite.yaml +++ b/internal/defaults/pipelines/ops-rewrite.yaml @@ -1,7 +1,11 @@ kind: WavePipeline metadata: name: ops-rewrite - description: "Analyze and rewrite poorly documented issues" + description: >- + Issue quality improvement pipeline that analyzes under-specified or + poorly documented issues, researches the codebase for context, and + rewrites the issue body with clear acceptance criteria, technical + details, and implementation guidance. release: true skills: diff --git a/internal/webui/embed.go b/internal/webui/embed.go index 664f1db3..17c4a144 100644 --- a/internal/webui/embed.go +++ b/internal/webui/embed.go @@ -3,6 +3,7 @@ package webui import ( "embed" "fmt" + "encoding/json" "html/template" "io/fs" "net/http" @@ -62,6 +63,8 @@ func parseTemplates(extraFuncs ...template.FuncMap) (map[string]*template.Templa "formatBytes": formatBytesFunc, "richInput": richInputFunc, "friendlyModel": friendlyModelFunc, + "toJSON": func(v interface{}) string { b, _ := json.Marshal(v); return string(b) }, + "formatTokensShort": formatTokensShort, "shortRunID": func(id string) string { if len(id) > 12 { return id[:12] }; return id }, "titleCase": titleCaseFunc, "contains": strings.Contains, diff --git a/internal/webui/handlers_compose.go b/internal/webui/handlers_compose.go index 153b240b..a13a4d4c 100644 --- a/internal/webui/handlers_compose.go +++ b/internal/webui/handlers_compose.go @@ -7,12 +7,27 @@ import ( "strings" "github.com/recinq/wave/internal/pipeline" + "github.com/recinq/wave/internal/state" ) // handleComposePage handles GET /compose - serves the HTML composition pipelines page. func (s *Server) handleComposePage(w http.ResponseWriter, r *http.Request) { pipelines := getCompositionPipelines() + // Enrich with run counts + if s.store != nil { + allRuns, err := s.store.ListRuns(state.ListRunsOptions{Limit: 10000}) + if err == nil { + counts := make(map[string]int) + for _, run := range allRuns { + counts[run.PipelineName]++ + } + for i := range pipelines { + pipelines[i].RunCount = counts[pipelines[i].Name] + } + } + } + data := struct { ActivePage string Pipelines []CompositionPipeline diff --git a/internal/webui/handlers_issues.go b/internal/webui/handlers_issues.go index 710fd4e9..3453ff6a 100644 --- a/internal/webui/handlers_issues.go +++ b/internal/webui/handlers_issues.go @@ -14,7 +14,10 @@ import ( // handleIssuesPage handles GET /issues - serves the HTML issues page. func (s *Server) handleIssuesPage(w http.ResponseWriter, r *http.Request) { - stateFilter := validateStateFilter(r.URL.Query().Get("state")) + stateFilter := r.URL.Query().Get("state") + if stateFilter == "" { + stateFilter = "open" + } page := parsePageNumber(r) issueData := s.getIssueListData(stateFilter, page) @@ -144,13 +147,27 @@ func (s *Server) handleIssueDetailPage(w http.ResponseWriter, r *http.Request) { } } + // Compute aggregate Wave stats + runCount := len(relatedRuns) + totalTokens := 0 + lastStatus := "" + for _, r := range relatedRuns { + totalTokens += r.TotalTokens + if lastStatus == "" { + lastStatus = r.Status + } + } + data := struct { - ActivePage string - Issue IssueDetail - Runs []RunSummary - Comments []CommentSummary + ActivePage string + Issue IssueDetail + Runs []RunSummary + Comments []CommentSummary + RunCount int + TotalTokens int + LastStatus string }{ - ActivePage: "issues", + ActivePage: "issues", Issue: IssueDetail{ Number: issue.Number, Title: issue.Title, @@ -164,8 +181,11 @@ func (s *Server) handleIssueDetailPage(w http.ResponseWriter, r *http.Request) { UpdatedAt: issue.UpdatedAt.Format("2006-01-02 15:04"), URL: issue.HTMLURL, }, - Runs: relatedRuns, - Comments: comments, + Runs: relatedRuns, + Comments: comments, + RunCount: runCount, + TotalTokens: totalTokens, + LastStatus: lastStatus, } w.Header().Set("Content-Type", "text/html; charset=utf-8") @@ -250,12 +270,32 @@ func (s *Server) getIssueListData(stateFilter string, page int) IssueListRespons summaries = []IssueSummary{} } + // Enrich with Wave run stats + if s.store != nil { + allRuns, err := s.store.ListRuns(state.ListRunsOptions{Limit: 10000}) + if err == nil { + enrichSummariesWithRuns(summaries, allRuns, "issue") + } + } + + // Count open/closed from current page + var openCount, closedCount int + for _, s := range summaries { + if s.State == "open" { + openCount++ + } else { + closedCount++ + } + } + return IssueListResponse{ Issues: summaries, RepoSlug: s.repoSlug, FilterState: stateFilter, Page: page, HasMore: hasMore, + TotalOpen: openCount, + TotalClosed: closedCount, } } diff --git a/internal/webui/handlers_pipelines.go b/internal/webui/handlers_pipelines.go index c6fb9b8a..cf9c390d 100644 --- a/internal/webui/handlers_pipelines.go +++ b/internal/webui/handlers_pipelines.go @@ -20,18 +20,56 @@ type PipelineSummary struct { IsComposition bool `json:"is_composition,omitempty"` Skills []string `json:"skills,omitempty"` Disabled bool `json:"disabled"` + RunCount int `json:"run_count,omitempty"` } // handlePipelinesPage handles GET /pipelines - serves the HTML pipelines page. func (s *Server) handlePipelinesPage(w http.ResponseWriter, r *http.Request) { pipelines := s.getPipelineSummaries() + // Enrich with run counts + if s.store != nil { + allRuns, err := s.store.ListRuns(state.ListRunsOptions{Limit: 10000}) + if err == nil { + counts := make(map[string]int) + for _, run := range allRuns { + counts[run.PipelineName]++ + } + for i := range pipelines { + pipelines[i].RunCount = counts[pipelines[i].Name] + } + } + } + + // Collect unique categories + categories := make(map[string]bool) + for _, p := range pipelines { + if p.Category != "" { + categories[p.Category] = true + } + } + var catList []string + for c := range categories { + catList = append(catList, c) + } + sort.Strings(catList) + + // Sort pipelines by run count (most used first), then alphabetically + sort.SliceStable(pipelines, func(i, j int) bool { + if pipelines[i].RunCount != pipelines[j].RunCount { + return pipelines[i].RunCount > pipelines[j].RunCount + } + return pipelines[i].Name < pipelines[j].Name + }) + data := struct { ActivePage string Pipelines []PipelineSummary + Categories []string }{ ActivePage: "pipelines", Pipelines: pipelines, + Categories: catList, } w.Header().Set("Content-Type", "text/html; charset=utf-8") @@ -332,10 +370,18 @@ func (s *Server) getPipelineSummaries() []PipelineSummary { hasComposition = true } } + // Infer category from name prefix if not explicitly set + cat := p.Metadata.Category + if cat == "" { + if idx := strings.Index(name, "-"); idx > 0 { + cat = name[:idx] + } + } + summaries = append(summaries, PipelineSummary{ Name: name, Description: p.Metadata.Description, - Category: p.Metadata.Category, + Category: cat, StepCount: len(p.Steps), Steps: stepIDs, IsComposition: hasComposition, diff --git a/internal/webui/handlers_prs.go b/internal/webui/handlers_prs.go index 18d1ec43..4c19f31e 100644 --- a/internal/webui/handlers_prs.go +++ b/internal/webui/handlers_prs.go @@ -16,7 +16,10 @@ import ( // handlePRsPage handles GET /prs - serves the HTML pull requests page. func (s *Server) handlePRsPage(w http.ResponseWriter, r *http.Request) { - stateFilter := validateStateFilter(r.URL.Query().Get("state")) + stateFilter := r.URL.Query().Get("state") + if stateFilter == "" { + stateFilter = "open" + } page := parsePageNumber(r) prData := s.getPRListData(stateFilter, page) @@ -154,12 +157,26 @@ func (s *Server) handlePRDetailPage(w http.ResponseWriter, r *http.Request) { } } + // Compute aggregate Wave stats + runCount := len(relatedRuns) + totalTokens := 0 + lastStatus := "" + for _, r := range relatedRuns { + totalTokens += r.TotalTokens + if lastStatus == "" { + lastStatus = r.Status + } + } + data := struct { ActivePage string PR PRDetail Runs []RunSummary Comments []CommentSummary CommitDetails []CommitSummary + RunCount int + TotalTokens int + LastStatus string }{ ActivePage: "prs", PR: PRDetail{ @@ -186,6 +203,9 @@ func (s *Server) handlePRDetailPage(w http.ResponseWriter, r *http.Request) { Runs: relatedRuns, Comments: comments, CommitDetails: commits, + RunCount: runCount, + TotalTokens: totalTokens, + LastStatus: lastStatus, } w.Header().Set("Content-Type", "text/html; charset=utf-8") @@ -296,12 +316,31 @@ func (s *Server) getPRListData(stateFilter string, page int) PRListResponse { summaries = []PRSummary{} } + // Enrich with Wave run stats + if s.store != nil { + allRuns, err := s.store.ListRuns(state.ListRunsOptions{Limit: 10000}) + if err == nil { + enrichPRSummariesWithRuns(summaries, allRuns) + } + } + + var openCount, closedCount int + for _, s := range summaries { + if s.State == "open" || s.Draft { + openCount++ + } else { + closedCount++ + } + } + return PRListResponse{ PullRequests: summaries, RepoSlug: s.repoSlug, FilterState: stateFilter, Page: page, HasMore: hasMore, + TotalOpen: openCount, + TotalClosed: closedCount, } } diff --git a/internal/webui/handlers_runs.go b/internal/webui/handlers_runs.go index 1dec5f67..bdb214ef 100644 --- a/internal/webui/handlers_runs.go +++ b/internal/webui/handlers_runs.go @@ -141,10 +141,19 @@ func (s *Server) handleRunsPage(w http.ResponseWriter, r *http.Request) { } limit := parsePageSize(r) status := r.URL.Query().Get("status") + if status == "" { + status = "running" + } pipelineFilter := r.URL.Query().Get("pipeline") + // "all" means no status filter + queryStatus := status + if queryStatus == "all" { + queryStatus = "" + } + opts := state.ListRunsOptions{ - Status: status, + Status: queryStatus, PipelineName: pipelineFilter, Limit: limit + 1, } @@ -367,6 +376,23 @@ func (s *Server) handleRunDetailPage(w http.ResponseWriter, r *http.Request) { } } + // Build template variable map for prompt resolution + templateVars := map[string]string{ + "input": run.Input, + } + // Add forge variables + forgeInfo, _ := forge.DetectFromGitRemotes() + templateVars["forge.cli_tool"] = forgeInfo.CLITool + templateVars["forge.type"] = string(forgeInfo.Type) + templateVars["forge.pr_term"] = forgeInfo.PRTerm + templateVars["forge.pr_command"] = forgeInfo.PRCommand + // Add project variables from manifest + if s.manifest != nil && s.manifest.Project != nil { + for k, v := range s.manifest.Project.ProjectVars() { + templateVars["project."+k] = v + } + } + // Collect child runs for sub-pipeline steps childRuns := make(map[string][]RunSummary) if children, err := s.store.GetChildRuns(runID); err == nil { @@ -392,6 +418,7 @@ func (s *Server) handleRunDetailPage(w http.ResponseWriter, r *http.Request) { LinkedNumber int LinkedType string ChildRuns map[string][]RunSummary + TemplateVars map[string]string }{ ActivePage: "runs", Run: runSummary, @@ -408,6 +435,7 @@ func (s *Server) handleRunDetailPage(w http.ResponseWriter, r *http.Request) { LinkedNumber: linkedNumber, LinkedType: linkedType, ChildRuns: childRuns, + TemplateVars: templateVars, } w.Header().Set("Content-Type", "text/html; charset=utf-8") @@ -641,19 +669,32 @@ func (s *Server) buildStepDetails(runID, pipelineName string) []StepDetail { edgeInfo = strings.Join(edgeParts, "; ") } + // Extract contract info for display + var contractType, contractSchemaName string + if contracts := step.Handover.EffectiveContracts(); len(contracts) > 0 { + contractType = contracts[0].Type + if contracts[0].Schema != "" { + contractSchemaName = contracts[0].Schema + } else if contracts[0].SchemaPath != "" { + contractSchemaName = contracts[0].SchemaPath + } + } + sd := StepDetail{ - RunID: runID, - StepID: step.ID, - Persona: resolveForgeVars(step.Persona), - State: "pending", - StepType: stepType, - Script: step.Script, - SubPipeline: step.SubPipeline, - GatePrompt: gatePrompt, - GateChoices: gateChoices, - EdgeInfo: edgeInfo, - Model: step.Model, - MaxVisits: step.MaxVisits, + RunID: runID, + StepID: step.ID, + Persona: resolveForgeVars(step.Persona), + State: "pending", + StepType: stepType, + Script: step.Script, + SubPipeline: step.SubPipeline, + GatePrompt: gatePrompt, + GateChoices: gateChoices, + EdgeInfo: edgeInfo, + Model: step.Model, + MaxVisits: step.MaxVisits, + Contract: contractType, + ContractSchemaName: contractSchemaName, } // Populate structured gate data for interactive UI diff --git a/internal/webui/run_stats.go b/internal/webui/run_stats.go new file mode 100644 index 00000000..bc8b3302 --- /dev/null +++ b/internal/webui/run_stats.go @@ -0,0 +1,95 @@ +package webui + +import ( + "fmt" + "strconv" + "strings" + + "github.com/recinq/wave/internal/state" +) + +// enrichSummariesWithRuns adds Wave pipeline run stats to issue summaries. +func enrichSummariesWithRuns(summaries []IssueSummary, runs []state.RunRecord, matchType string) { + // Build a map of issue/PR number -> runs + runMap := make(map[string][]state.RunRecord) + for _, run := range runs { + if run.Input == "" { + continue + } + // Match by /issues/ or /pull/ pattern + for _, sep := range []string{"/issues/", "/pull/"} { + idx := strings.Index(run.Input, sep) + if idx >= 0 { + rest := run.Input[idx+len(sep):] + end := strings.IndexByte(rest, '/') + if end > 0 { + rest = rest[:end] + } + if _, err := strconv.Atoi(rest); err == nil { + key := sep + rest + runMap[key] = append(runMap[key], run) + } + } + } + } + + for i := range summaries { + key := "/" + matchType + "/" + strconv.Itoa(summaries[i].Number) + matching := runMap[key] + if len(matching) == 0 { + continue + } + summaries[i].RunCount = len(matching) + summaries[i].LastStatus = matching[0].Status + for _, r := range matching { + summaries[i].TotalTokens += int64(r.TotalTokens) + } + } +} + +// enrichPRSummariesWithRuns adds Wave pipeline run stats to PR summaries. +func enrichPRSummariesWithRuns(summaries []PRSummary, runs []state.RunRecord) { + runMap := make(map[string][]state.RunRecord) + for _, run := range runs { + if run.Input == "" { + continue + } + idx := strings.Index(run.Input, "/pull/") + if idx >= 0 { + rest := run.Input[idx+len("/pull/"):] + end := strings.IndexByte(rest, '/') + if end > 0 { + rest = rest[:end] + } + if _, err := strconv.Atoi(rest); err == nil { + runMap[rest] = append(runMap[rest], run) + } + } + } + + for i := range summaries { + matching := runMap[strconv.Itoa(summaries[i].Number)] + if len(matching) == 0 { + continue + } + summaries[i].RunCount = len(matching) + summaries[i].LastStatus = matching[0].Status + for _, r := range matching { + summaries[i].TotalTokens += int64(r.TotalTokens) + } + } +} + +// formatTokensShort formats token counts compactly (e.g., "1.2k", "45k"). +func formatTokensShort(n int64) string { + if n == 0 { + return "" + } + if n < 1000 { + return strconv.FormatInt(n, 10) + } + if n < 1_000_000 { + return fmt.Sprintf("%.1fk", float64(n)/1000) + } + return fmt.Sprintf("%.1fM", float64(n)/1_000_000) +} diff --git a/internal/webui/static/style.css b/internal/webui/static/style.css index 7d08363a..c16f62fa 100644 --- a/internal/webui/static/style.css +++ b/internal/webui/static/style.css @@ -111,7 +111,7 @@ /* Reset */ *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; } -html { font-size: 14px; line-height: 1.5; } +html { font-size: 15px; line-height: 1.5; } body { font-family: var(--font-sans); background: var(--color-bg); @@ -368,22 +368,27 @@ a:focus-visible, button:focus-visible, select:focus-visible, input:focus-visible /* Forms */ /* Start Pipeline Dialog */ -.start-dialog { +.start-dialog, .w-dialog { background: var(--color-bg-secondary); color: var(--color-text); border: 1px solid var(--color-border); border-radius: var(--radius-lg); - padding: 1.5rem; max-width: 500px; width: 90%; + padding: 0; max-width: 500px; width: 90%; box-shadow: 0 8px 32px var(--color-shadow); margin: auto; position: fixed; inset: 0; height: fit-content; } -.start-dialog[open] { +.start-dialog[open], .w-dialog[open] { overscroll-behavior: contain; } -.start-dialog::backdrop { +.start-dialog::backdrop, .w-dialog::backdrop { background: rgba(0, 0, 0, 0.5); } +.w-dialog-head { display: flex; align-items: center; justify-content: space-between; padding: 0.75rem 1rem; border-bottom: 1px solid var(--color-border); } +.w-dialog-close { background: none; border: none; color: var(--color-text-muted); font-size: 1.2rem; cursor: pointer; padding: 0.2rem; line-height: 1; } +.w-dialog-close:hover { color: var(--color-text); } +.w-dialog-body { padding: 1rem; } +.w-dialog-actions { display: flex; justify-content: flex-end; gap: 0.4rem; padding: 0.75rem 1rem; border-top: 1px solid var(--color-border); } .start-dialog h3 { margin-bottom: 1rem; } /* Detail Dialog (contracts, personas) */ @@ -2643,3 +2648,475 @@ details[open] > .section-toggle::before { transform: rotate(90deg); } .ontology-context-summary::-webkit-details-marker { display: none; } .ontology-context-summary::marker { display: none; content: ''; } .ontology-context[open] > .ontology-context-summary { padding-bottom: 0; } + +/* ═══════════════════════════════════════════════════════════════ + Wave Run Detail & Pipeline Shapes + Shared component styles for run detail, pipeline detail, etc. + ═══════════════════════════════════════════════════════════════ */ +/* ═══════════════════════════════════════════════════════════════════ + Fat Gantt Shapes v2 – compact, differentiated, deep + All colors use var(--color-*) from style.css for theme support. + ═══════════════════════════════════════════════════════════════════ */ + +/* ── Header ── */ +.w-head { display: flex; align-items: flex-start; justify-content: space-between; margin-bottom: 0.25rem; } +.w-head h1 { font-size: 1.15rem; margin: 0; display: flex; align-items: center; gap: 0.4rem; flex-wrap: wrap; } +.w-head .back-link { font-size: 0.72rem; color: var(--color-text-muted); } +.w-head .back-link:hover { color: var(--color-link); } +.w-head .actions { display: flex; gap: 0.3rem; align-items: flex-start; padding-top: 0.15rem; } + +/* ── Stats bar ── */ +.w-stats { display: flex; gap: 0.5rem; align-items: center; font-size: 0.72rem; color: var(--color-text-secondary); margin-bottom: 0.4rem; flex-wrap: wrap; } +.w-stats b { color: var(--color-text); font-weight: 600; } +.w-stats .badge { font-size: 0.6rem; padding: 0 4px; } + +/* ── Progress ── */ +.w-prog { height: 2px; background: var(--color-bg-tertiary); border-radius: 1px; margin-bottom: 0.3rem; } +.w-prog-fill { height: 100%; border-radius: 1px; background: var(--color-completed); transition: width 0.3s; } + +/* ── I/O Hero Cards (pill-shaped -- visually distinct from step rectangles) ── */ +.w-io { border-radius: 6px; padding: 0.5rem 0.85rem; position: relative; } +.w-io-in { background: rgba(99,102,241,0.07); border: 1px solid rgba(99,102,241,0.3); border-left: 4px solid #6366f1; margin-bottom: 0.75rem; } +.w-io-out { background: rgba(34,197,94,0.07); border: 2px solid rgba(34,197,94,0.4); border-left: 4px solid #22c55e; margin-top: 1rem; padding: 0.5rem 0; overflow: hidden; } +.w-io-out.st-failed { background: rgba(239,68,68,0.07); border-color: rgba(239,68,68,0.35); border-left-color: #ef4444; } +.w-io-tag { display: inline-block; font-size: 0.52rem; font-weight: 800; letter-spacing: 0.1em; text-transform: uppercase; padding: 0 4px; border-radius: 2px; margin-right: 0.35rem; vertical-align: middle; font-family: var(--font-mono); } +.w-io-in .w-io-tag { background: rgba(99,102,241,0.2); color: #a5b4fc; } +.w-io-out .w-io-tag { background: rgba(34,197,94,0.2); color: #86efac; } +.w-io-out.st-failed .w-io-tag { background: rgba(239,68,68,0.2); color: #fca5a5; } +.w-io-text { font-size: 0.8rem; color: var(--color-text); display: inline; } +.w-io-text a { color: #818cf8; } +.w-io-out .w-io-text { font-size: 0.82rem; } +.w-io-out .w-io-result { font-size: 0.85rem; font-weight: 600; } + +/* ── Timeline Ruler ── */ +.w-ruler { display: flex; justify-content: space-between; align-items: flex-end; font-size: 0.6rem; color: var(--color-text-muted); padding: 0.1rem 0 0.08rem; border-bottom: 1px solid var(--color-border); margin-bottom: 0.2rem; font-variant-numeric: tabular-nums; } +.w-ruler span { min-width: 2rem; } + +/* ── Flow container (zero gap -- connectors provide spacing) ── */ +.w-flow { display: flex; flex-direction: column; gap: 0; } + +/* ── Connector arrows (CSS line + triangle, replaces unicode) ── */ +.w-conn { height: 10px; position: relative; } +.w-conn::before { content: ''; position: absolute; left: 50%; top: 0; bottom: 4px; width: 1px; background: var(--color-border); } +.w-conn::after { content: ''; position: absolute; left: 50%; bottom: 0; transform: translateX(-50%); border-left: 3px solid transparent; border-right: 3px solid transparent; border-top: 4px solid var(--color-border-light); } +.w-conn-dot { display: none; } + +/* ── Base shape: sharp rectangle, depth via shadow ── */ +.ws { + border-radius: 3px; + background: var(--color-bg-secondary); + position: relative; + overflow: hidden; + transition: border-color 0.12s, box-shadow 0.12s; + box-shadow: 0 1px 2px rgba(0,0,0,0.25); +} +.w-s:hover { box-shadow: 0 2px 5px rgba(0,0,0,0.35); } + +/* ── Gantt fill as shape background ── */ +.ws-bg { position: absolute; top: 0; left: 0; bottom: 0; border-radius: 3px; opacity: 0.06; pointer-events: none; z-index: 0; } +.ws-bg.st-completed { background: var(--color-completed); } +.ws-bg.st-failed { background: var(--color-failed); } +.ws-bg.st-running { background: var(--color-running); animation: v2pulse 2s ease-in-out infinite; } +@keyframes v2pulse { 0%,100%{opacity:0.03} 50%{opacity:0.1} } + +/* ── Status left accent (5px for strong signal) ── */ +.ws.st-completed { border-left: 5px solid var(--color-completed); } +.ws.st-failed { border-left: 5px solid var(--color-failed); } +.ws.st-running { border-left: 5px solid var(--color-running); } +.ws.st-pending { border-left: 3px solid var(--color-border); opacity: 0.5; } +.ws.st-skipped { border-left: 3px solid var(--color-border); opacity: 0.5; } +.ws.st-skipped:hover, .ws.st-skipped:focus-within { opacity: 0.9; background: var(--color-bg-secondary); } +.ws.st-pending:hover, .ws.st-pending:focus-within { opacity: 0.9; background: var(--color-bg-secondary); } + +/* ── Step type differentiation (geometric identity) ── */ +/* Sub-pipeline: purple accent + inset glow */ +.ws.tp-pipeline { + border: 1.5px solid rgba(139,92,246,0.5); border-left: 5px solid #8b5cf6; + box-shadow: inset 0 0 0 1px rgba(139,92,246,0.1), 0 1px 2px rgba(0,0,0,0.25); + background: rgba(124,58,237,0.03); +} +/* Gate: octagonal clip-path + dashed amber border */ +.ws.tp-gate { + border: 1.5px dashed rgba(245,158,11,0.45); border-radius: 0; + background: rgba(245,158,11,0.03); + +} +/* Conditional: chevron left edge */ +.ws.tp-conditional { + border: 1.5px solid rgba(6,182,212,0.35); border-radius: 0; + background: rgba(6,182,212,0.03); + +} +.ws.tp-conditional .ws-row1, +.ws.tp-conditional .ws-io, +.ws.tp-conditional .ws-log { padding-left: 0.75rem; } +/* Command: sharp corners, dashed bottom */ +.ws.tp-command { + border: 1px solid var(--color-border); border-radius: 0; + border-bottom: 2px dashed var(--color-border); + background: rgba(71,85,105,0.03); +} + +/* ── Shape content (z-index above gantt bg) ── */ +.ws-content { position: relative; z-index: 1; } + +/* ── Row 1: name + meta (primary scan target) ── */ +.ws-row1 { display: flex; align-items: center; gap: 0.3rem; padding: 0.25rem 0.55rem 0.1rem; flex-wrap: wrap; } +.ws-name { font-weight: 700; font-size: 0.9rem; white-space: nowrap; } +.ws-name .tp-ico { font-size: 0.7rem; opacity: 0.5; margin-right: 0.1rem; } +.ws-meta { display: flex; gap: 0.25rem; align-items: center; font-size: 0.68rem; color: var(--color-text-muted); margin-left: auto; } +.ws-meta .badge { font-size: 0.62rem; padding: 1px 4px; } +.ws-code-link { font-family: var(--font-mono); font-size: 0.68rem; padding: 0.05rem 0.35rem; border-radius: 3px; background: rgba(99,102,241,0.1); color: #a5b4fc; text-decoration: none; cursor: pointer; } +.ws-code-link:hover { background: rgba(99,102,241,0.2); } +.ws-dur { font-weight: 600; color: var(--color-text-secondary); font-variant-numeric: tabular-nums; font-size: 0.78rem; font-family: var(--font-mono); } +.ws-tok { color: var(--color-text-muted); font-size: 0.65rem; } + +/* ── Row 2: I/O ── */ +.ws-io { display: flex; gap: 0.75rem; padding: 0.12rem 0.55rem 0.2rem; font-size: 0.78rem; flex-wrap: wrap; } +.ws-io-in, .ws-io-out { display: flex; gap: 0.3rem; align-items: baseline; } +.ws-io-out { padding-left: 0.6rem; border-left: 1px solid rgba(100,116,139,0.25); } +.ws-io-lbl { color: var(--color-text-muted); font-weight: 700; font-size: 0.62rem; text-transform: uppercase; letter-spacing: 0.03em; font-family: var(--font-mono); flex-shrink: 0; } +.ws-io-val { color: var(--color-text-secondary); font-size: 0.75rem; } + +/* ── Step tab bar (artifacts + logs, unified) ── */ +.ws-tabs { display: flex; gap: 0; padding: 0; margin: 0.15rem 0 0; border-top: 1px solid var(--color-border); border-bottom: 1px solid var(--color-border); padding-left: 0.55rem; align-items: stretch; overflow-x: auto; } +.w-tab { + display: inline-flex; align-items: center; gap: 0.2rem; + padding: 0.4rem 0.75rem; + font-size: 0.75rem; font-family: var(--font-mono); + color: var(--color-text-muted); cursor: pointer; text-decoration: none; + border-bottom: 2px solid transparent; + transition: color 0.1s, border-color 0.1s; + white-space: nowrap; + background: none; border-top: none; border-left: none; border-right: none; +} +.w-tab:hover { color: var(--color-text-secondary); border-bottom-color: rgba(100,116,139,0.3); text-decoration: none; } +.w-tab.active { color: var(--color-text); border-bottom-color: var(--color-text); background: rgba(99,102,241,0.04); } +.w-tab .art-size { color: var(--color-text-muted); font-size: 0.6rem; } +.w-tab-action { + margin-left: auto; + color: #fca5a5; font-family: inherit; +} +.w-tab-action:hover { color: #f87171; border-bottom-color: rgba(239,68,68,0.4); } +/* Content area directly under tabs — no gap, no redundant header */ +.ws-tabcontent { margin: 0; padding: 0; overflow: hidden; } +.ws-tabcontent[hidden] { display: none !important; } +.ws-tabcontent .av-smart { padding: 0.5rem 0.55rem; } +.ws-tabcontent .markdown-body { font-size: 0.82rem; line-height: 1.6; } +.ws-tabcontent .markdown-body h1 { font-size: 1.2rem; margin: 0.5rem 0 0.3rem; border-bottom: 1px solid var(--color-border); padding-bottom: 0.2rem; } +.ws-tabcontent .markdown-body h2 { font-size: 1rem; margin: 0.4rem 0 0.2rem; } +.ws-tabcontent .markdown-body h3 { font-size: 0.9rem; margin: 0.3rem 0 0.15rem; } +.ws-tabcontent .markdown-body p { margin: 0.3rem 0; } +.ws-tabcontent .markdown-body ul, .ws-tabcontent .markdown-body ol { padding-left: 1.2rem; margin: 0.2rem 0; } +.ws-tabcontent .markdown-body code { background: rgba(99,102,241,0.1); padding: 0.1rem 0.3rem; border-radius: 3px; font-size: 0.78rem; } +.ws-tabcontent .markdown-body pre { background: var(--color-bg-tertiary); padding: 0.4rem; border-radius: 4px; overflow-x: auto; } +.ws-tabcontent .markdown-body pre code { background: none; padding: 0; } +.ws-tabcontent .markdown-body hr { border: none; border-top: 1px solid var(--color-border); margin: 0.5rem 0; } +.ws-tabcontent .markdown-body strong { color: var(--color-text); } +.ws-tabcontent pre { margin: 0; padding: 0.3rem 0.55rem; font-size: 0.68rem; white-space: pre-wrap; word-break: break-all; color: var(--color-text-secondary); } +.ws-tabcontent .av-toggle { display: flex; gap: 3px; padding: 0.25rem 0.4rem; border-bottom: 1px solid var(--color-border); background: rgba(15,23,42,0.5); align-items: center; } +.ws-tabcontent .av-toggle button { padding: 0.2rem 0.5rem; border-radius: 3px; font-size: 0.65rem; color: var(--color-text-muted); border: none; background: none; cursor: pointer; } +.ws-tabcontent .av-toggle button:hover { color: var(--color-text-secondary); } +.ws-tabcontent .av-toggle button.active { background: #334155; color: #cbd5e1; } +.ws-tabcontent .av-toggle a { font-size: 0.62rem; color: var(--color-text-muted); margin-left: auto; text-decoration: none; } +.ws-tabcontent .av-toggle a:hover { color: var(--color-text-secondary); } +/* Keep old class for OUT card chips */ +.w-art-chip { + display: inline-flex; align-items: center; gap: 0.15rem; + background: rgba(99,102,241,0.08); border: 1px solid rgba(99,102,241,0.2); + border-radius: 12px; padding: 2px 10px; + font-size: 0.72rem; color: #a5b4fc; cursor: pointer; text-decoration: none; + transition: background 0.1s; white-space: nowrap; line-height: 1.6; +} +.w-art-chip:hover { background: rgba(99,102,241,0.18); color: #c7d2fe; } +.w-art-chip .art-size { color: var(--color-text-muted); font-size: 0.6rem; } + +/* ── Contract verdict ── */ +.w-ctr { font-size: 0.58rem; padding: 0 3px; border-radius: 2px; font-weight: 700; vertical-align: middle; } +.w-ctr-pass { background: rgba(34,197,94,0.15); color: #4ade80; } +.w-ctr-fail { background: rgba(239,68,68,0.15); color: #f87171; } + +/* ── Bottom gantt bar (2px accent) ── */ +.ws-bar { height: 4px; background: var(--color-bg-tertiary); margin: 0; } +.ws-bar-fill { height: 100%; transition: width 0.3s; } +.ws-bar-fill.st-completed { background: var(--color-completed); } +.ws-bar-fill.st-failed { background: var(--color-failed); } +.ws-bar-fill.st-running { background: var(--color-running); } + +/* ── Logs ── */ +.ws-log-body { margin: 0.15rem 0.55rem 0.25rem; background: var(--color-bg-tertiary); border-radius: 4px; padding: 0.4rem; font-family: var(--font-mono); font-size: 0.72rem; line-height: 1.4; white-space: pre-wrap; word-break: break-all; color: var(--color-text-secondary); } + +/* ── Error strip ── */ +.ws-err { padding: 0.12rem 0.45rem; font-size: 0.68rem; color: #fca5a5; background: rgba(239,68,68,0.1); border-top: 1px solid rgba(239,68,68,0.25); } + +/* ── Gate ── */ +.ws-gate { padding: 0.08rem 0.45rem 0.15rem; } +.ws-gate-prompt { font-size: 0.7rem; color: var(--color-text); font-style: italic; margin-bottom: 0.1rem; } +.ws-gate-choices { display: flex; gap: 0.2rem; flex-wrap: wrap; } +.ws-gate-btn { padding: 0.1rem 0.35rem; border-radius: 3px; font-size: 0.65rem; border: 1px solid var(--color-border); background: var(--color-bg-tertiary); color: var(--color-text-secondary); cursor: pointer; transition: border-color 0.12s; } +.ws-gate-btn:hover { border-color: #f59e0b; color: #fbbf24; } +.ws-gate-result { font-size: 0.65rem; color: #4ade80; } + +/* ── Conditional edges ── */ +.ws-edges { padding: 0.06rem 0.45rem 0.12rem; font-size: 0.65rem; color: var(--color-text-muted); } + +/* ── Secondary bar ── */ +.w-sec { display: flex; gap: 0.25rem; margin-top: 0.5rem; padding: 0.3rem 0; border-top: 1px solid var(--color-border); flex-wrap: wrap; align-items: center; } +.w-sec-tab { font-size: 0.78rem; color: var(--color-text-secondary); padding: 0.35rem 0.6rem; border-radius: 4px; cursor: pointer; text-decoration: none; transition: background 0.1s; border: none; background: none; min-height: 28px; } +.w-sec-tab:hover { background: var(--color-bg-tertiary); color: var(--color-text-secondary); } +.w-sec-tab.active { background: var(--color-bg-tertiary); color: var(--color-text); border-bottom: 2px solid #6366f1; } + +/* ── Panels ── */ +.w-panel { background: var(--color-bg-secondary); border: 1px solid var(--color-border); border-radius: 4px; margin-top: 0.4rem; margin-bottom: 0.75rem; overflow: hidden; } +.w-panel[hidden] { display: none !important; } +/* Hide v1 events/cards that SSE creates outside our v2 panels */ +.events-timeline, .card > .events-timeline, .card:has(.events-timeline) { display: none !important; } +body > .main-content > .card:last-of-type:has(h2) { display: none !important; } +.w-panel-head { padding: 0.35rem 0.6rem; font-size: 0.8rem; font-weight: 600; color: var(--color-text); border-bottom: 1px solid var(--color-border); background: rgba(30,41,59,0.4); } + +/* Events */ +.w-events { } +.w-ev { display: flex; gap: 0.4rem; align-items: baseline; padding: 0.15rem 0.6rem; font-size: 0.72rem; border-bottom: 1px solid rgba(51,65,85,0.3); } +.w-ev:last-child { border-bottom: none; } +.w-ev-hook { opacity: 0.7; } +.w-ev-time { color: var(--color-text-muted); font-size: 0.65rem; font-family: var(--font-mono); min-width: 70px; flex-shrink: 0; } +.w-ev-step { color: var(--color-text-secondary); font-family: var(--font-mono); font-size: 0.68rem; } +.w-ev-msg { color: var(--color-text-secondary); flex: 1; word-break: break-word; } +.w-ev-dur { color: var(--color-text-muted); font-size: 0.62rem; } + +/* Metrics table */ +.w-metrics-table { width: 100%; border-collapse: collapse; font-size: 0.75rem; } +.w-metrics-table th { text-align: left; padding: 0.3rem 0.5rem; color: var(--color-text-muted); font-weight: 600; font-size: 0.68rem; text-transform: uppercase; letter-spacing: 0.03em; border-bottom: 1px solid var(--color-border); } +.w-metrics-table td { padding: 0.25rem 0.5rem; border-bottom: 1px solid rgba(51,65,85,0.3); color: var(--color-text-secondary); } +.w-metrics-table tr:last-child td { border-bottom: none; } +.w-mt-completed td:first-child { border-left: 3px solid var(--color-completed); } +.w-mt-failed td:first-child { border-left: 3px solid var(--color-failed); } +.w-mt-running td:first-child { border-left: 3px solid var(--color-running); } +.w-mt-pending td:first-child { border-left: 3px solid var(--color-border); } + +/* Diff: split-pane (file tree left, diff right) */ +.w-diff-layout { display: grid; grid-template-columns: 280px 1fr; min-height: 200px; } +.w-diff-tree { border-right: 1px solid var(--color-border); overflow-y: auto; } +.w-diff-content { overflow: auto; } +.w-diff-summary { display: flex; gap: 0.75rem; padding: 0.3rem 0.5rem; font-size: 0.75rem; color: var(--color-text-muted); border-bottom: 1px solid var(--color-border); grid-column: 1 / -1; } +.w-diff-summary b { color: var(--color-text); } +.w-diff-file { display: flex; gap: 0.3rem; align-items: center; padding: 0.2rem 0.4rem; font-size: 0.7rem; border-bottom: 1px solid rgba(51,65,85,0.2); cursor: pointer; } +.w-diff-file:hover { background: var(--color-bg-tertiary); } +.w-diff-file.active { background: rgba(99,102,241,0.08); border-left: 2px solid #6366f1; } +.w-diff-stat { font-family: var(--font-mono); font-size: 0.62rem; margin-left: auto; white-space: nowrap; } +.w-diff-add { color: #4ade80; } +.w-diff-del { color: #f87171; } +.w-diff-path { color: var(--color-text-secondary); font-family: var(--font-mono); font-size: 0.68rem; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } +.w-diff-status { font-size: 0.58rem; padding: 0 3px; border-radius: 2px; flex-shrink: 0; } +.w-diff-status-M { background: rgba(245,158,11,0.12); color: #fbbf24; } +.w-diff-status-A { background: rgba(34,197,94,0.12); color: #4ade80; } +.w-diff-status-D { background: rgba(239,68,68,0.12); color: #f87171; } +.w-diff-status-R { background: rgba(99,102,241,0.12); color: #a5b4fc; } +.w-diff-empty { padding: 1rem; color: var(--color-text-muted); font-size: 0.78rem; text-align: center; } +@media (max-width: 900px) { .w-diff-layout { grid-template-columns: 1fr; } .w-diff-tree { border-right: none; border-bottom: 1px solid var(--color-border); max-height: 200px; } } + +/* Minimal steps (no data from old runs) */ +.ws-minimal { opacity: 0.6; } +.ws-minimal .ws-io { display: none; } +.ws-minimal .ws-row1 { padding: 0.15rem 0.55rem; } + +/* Retro */ +.w-retro-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(120px, 1fr)); gap: 0.4rem; padding: 0.4rem; } +.w-retro-card { background: var(--color-bg-tertiary); border-radius: 4px; padding: 0.4rem 0.5rem; text-align: center; } +.w-retro-card-label { font-size: 0.62rem; color: var(--color-text-muted); text-transform: uppercase; letter-spacing: 0.03em; } +.w-retro-card-value { font-size: 1.1rem; font-weight: 700; color: var(--color-text); margin-top: 0.1rem; } + +/* ── Artifact viewer ── */ +.w-art-viewer { margin: 0.15rem 0.45rem; background: var(--color-bg-tertiary); border-radius: 3px; border: 1px solid var(--color-border); overflow: hidden; } +.w-art-viewer-head { padding: 0.2rem 0.4rem; font-size: 0.65rem; color: var(--color-text-muted); background: rgba(30,41,59,0.6); border-bottom: 1px solid var(--color-border); display: flex; justify-content: space-between; align-items: center; } +.w-art-viewer-head .av-tabs { display: flex; gap: 2px; } +.w-art-viewer-head .av-tab { padding: 1px 6px; border-radius: 3px; cursor: pointer; font-size: 0.6rem; color: var(--color-text-muted); border: none; background: none; } +.w-art-viewer-head .av-tab.active { background: #334155; color: #cbd5e1; } +.w-art-viewer-head .av-tab:hover { color: #94a3b8; } +.w-art-viewer pre { margin: 0; padding: 0.25rem 0.35rem; font-size: 0.65rem; white-space: pre-wrap; word-break: break-all; color: var(--color-text-secondary); } +.av-smart { } +/* Smart view for JSON artifacts */ +.av-smart { padding: 0.4rem 0.5rem; font-size: 0.75rem; } +.av-kv { display: flex; gap: 0.4rem; padding: 0.12rem 0; border-bottom: 1px solid rgba(51,65,85,0.5); align-items: baseline; } +.av-kv:last-child { border-bottom: none; } +.av-key { color: #64748b; min-width: 90px; flex-shrink: 0; font-size: 0.68rem; font-weight: 500; } +.av-val { color: #e2e8f0; word-break: break-word; } +.av-val a { color: #818cf8; text-decoration: underline; } +.av-section { margin-top: 0.35rem; } +.av-section-title { font-weight: 600; font-size: 0.72rem; color: #94a3b8; margin-bottom: 0.2rem; display: flex; align-items: center; gap: 0.3rem; } +.av-item { background: #1e293b; border-radius: 4px; padding: 0.2rem 0.4rem; margin-bottom: 0.15rem; display: flex; gap: 0.35rem; align-items: baseline; flex-wrap: wrap; } +.av-item-head { display: contents; } +.av-sev { padding: 0px 5px; border-radius: 3px; font-size: 0.6rem; font-weight: 600; line-height: 1.6; } +.av-sev-critical,.av-sev-major { background: rgba(239,68,68,0.15); color: #f87171; } +.av-sev-minor { background: rgba(245,158,11,0.12); color: #fbbf24; } +.av-sev-suggestion,.av-sev-info { background: rgba(99,102,241,0.12); color: #a5b4fc; } +.av-summary { color: #e2e8f0; font-size: 0.73rem; flex: 1; min-width: 200px; } +.av-file { color: #64748b; font-size: 0.62rem; font-family: var(--font-mono, monospace); margin-left: auto; white-space: nowrap; } +.av-detail { color: #94a3b8; font-size: 0.68rem; width: 100%; margin-top: 0.1rem; } +.av-status { font-size: 0.62rem; padding: 0 4px; border-radius: 2px; } +.av-status-fixed { background: rgba(34,197,94,0.12); color: #4ade80; } +.av-status-pending_fix { background: rgba(245,158,11,0.12); color: #fbbf24; } +.av-status-skip { background: rgba(100,116,139,0.2); color: #94a3b8; } +.av-stat { display: inline-flex; align-items: center; gap: 0.15rem; padding: 1px 6px; border-radius: 3px; font-size: 0.7rem; font-weight: 600; margin-right: 0.3rem; } +.av-stat-g { background: rgba(34,197,94,0.12); color: #4ade80; } +.av-stat-r { background: rgba(239,68,68,0.12); color: #f87171; } +.av-stat-y { background: rgba(245,158,11,0.12); color: #fbbf24; } +.av-stat-b { background: rgba(99,102,241,0.12); color: #a5b4fc; } + + +/* ── Compact mode (auto for 8+ steps, expand on hover/focus) ── */ +.w-compact .ws-io, +.w-compact .ws-log, +.w-compact .ws-gate, +.w-compact .ws-edges { display: none; } +.w-compact .ws-row1 { padding: 0.1rem 0.35rem 0.06rem; } +.w-compact .ws-name { font-size: 0.7rem; } +.w-compact .ws-dur { font-size: 0.6rem; } +.w-compact .ws-bar { height: 1px; } +.w-compact .w-conn { height: 5px; } +.w-compact .w-s:hover .ws-io { display: flex; } +.w-compact .w-s:hover .ws-log { display: block; } +.w-compact .w-s:hover .ws-gate, +.w-compact .w-s:hover .ws-edges { display: block; } +.w-compact .w-s:focus-within .ws-io { display: flex; } +.w-compact .w-s:focus-within .ws-log, +.w-compact .w-s:focus-within .ws-gate, +.w-compact .w-s:focus-within .ws-edges { display: block; } + +/* ── Parallel fork/join ── */ +.w-parallel { display: grid; gap: 2px; position: relative; padding: 3px 0; } +.w-parallel::before, .w-parallel::after { content: ''; position: absolute; left: 8%; right: 8%; height: 1px; background: var(--color-border); } +.w-parallel::before { top: 0; } +.w-parallel::after { bottom: 0; } +.w-parallel-2 { grid-template-columns: 1fr 1fr; } +.w-parallel-3 { grid-template-columns: 1fr 1fr 1fr; } +.w-parallel-4 { grid-template-columns: 1fr 1fr 1fr 1fr; } +.w-parallel-5 { grid-template-columns: 1fr 1fr 1fr 1fr 1fr; } + +/* ── Run/Pipeline/Issue List Cards ── */ +/* ── Runs v2 list ── */ +.wr-head { display: flex; align-items: center; justify-content: space-between; margin-bottom: 0.5rem; } +.wr-head h1 { font-size: 1.25rem; margin: 0; } + +/* Filter bar */ +.wr-toolbar { display: flex; align-items: center; justify-content: space-between; margin-bottom: 0.6rem; gap: 0.5rem; flex-wrap: wrap; } +.wr-filters { display: flex; gap: 0.3rem; align-items: center; flex-wrap: wrap; } +.wr-filter { padding: 0.2rem 0.5rem; border-radius: 4px; font-size: 0.72rem; color: var(--color-text-muted); border: 1px solid var(--color-border); background: none; cursor: pointer; text-decoration: none; } +.wr-filter:hover { background: var(--color-bg-tertiary); color: var(--color-text-secondary); } +.wr-filter.active { background: rgba(99,102,241,0.1); border-color: rgba(99,102,241,0.3); color: #a5b4fc; } +.wr-controls { display: flex; gap: 0.3rem; align-items: center; } +.wr-select { padding: 0.2rem 0.4rem; border-radius: 4px; font-size: 0.72rem; color: var(--color-text-secondary); border: 1px solid var(--color-border); background: var(--color-bg-tertiary); cursor: pointer; font-family: var(--font-sans); max-width: 220px; } +.wr-select:focus { border-color: var(--wave-primary); outline: none; } + +/* Run card */ +.wr-list { display: flex; flex-direction: column; gap: 0.35rem; } + +.wr-run { + display: grid; + grid-template-columns: 5px 1fr auto; + gap: 0; + background: var(--color-bg-secondary); + border-radius: 4px; + overflow: hidden; + text-decoration: none; + color: inherit; + transition: background 0.1s, box-shadow 0.1s; + box-shadow: 0 1px 2px rgba(0,0,0,0.2); +} +.wr-run:hover { background: var(--color-bg-tertiary); box-shadow: 0 2px 5px rgba(0,0,0,0.3); text-decoration: none; color: inherit; } + +/* Status accent bar (left edge) */ +.wr-accent { border-radius: 4px 0 0 4px; } +.wr-accent.st-completed { background: var(--color-completed); } +.wr-accent.st-failed { background: var(--color-failed); } +.wr-accent.st-running { background: var(--color-running); } +.wr-accent.st-pending { background: var(--color-border); } +.wr-accent.st-cancelled { background: var(--color-border); } + +/* Main content area */ +.wr-body { padding: 0.45rem 0.7rem; min-width: 0; } +.wr-row1 { display: flex; align-items: center; gap: 0.4rem; margin-bottom: 0.15rem; } +.wr-name { font-weight: 700; font-size: 0.85rem; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } +.wr-status { font-size: 0.65rem; } +.wr-input { font-size: 0.72rem; color: var(--color-text-muted); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 400px; } +.wr-row2 { display: flex; gap: 0.6rem; align-items: center; font-size: 0.7rem; color: var(--color-text-muted); flex-wrap: wrap; } +.wr-row2 b { color: var(--color-text-secondary); } +.wr-row2 .badge { font-size: 0.6rem; padding: 0 3px; } + +/* Right meta */ +.wr-meta { padding: 0.45rem 0.7rem; display: flex; flex-direction: column; align-items: flex-end; justify-content: center; gap: 0.15rem; white-space: nowrap; } +.wr-date { font-size: 0.68rem; color: var(--color-text-muted); font-family: var(--font-mono); } +.wr-dur { font-size: 0.78rem; font-weight: 600; color: var(--color-text-secondary); font-variant-numeric: tabular-nums; } +.wr-progress { font-size: 0.65rem; color: var(--color-text-muted); } + +/* Child run indent */ +.wr-child { margin-left: 1.5rem; opacity: 0.85; border-left: 2px solid var(--color-border); padding-left: 0.25rem; } +.wr-child .wr-run { background: var(--color-bg-tertiary); } +.wr-child .wr-accent { border-radius: 0 4px 4px 0; } + +/* Load more */ +.wr-more { text-align: center; padding: 0.5rem; } +.wr-more a { color: #818cf8; font-size: 0.78rem; text-decoration: none; } +.wr-more a:hover { text-decoration: underline; } + +/* Empty state */ +.wr-empty { text-align: center; padding: 2rem; color: var(--color-text-muted); font-size: 0.85rem; } + +/* ── Load More Button ── */ +.wr-load-more { text-align: center; padding: 0.6rem; } +.wr-load-more button { + padding: 0.3rem 1.2rem; border-radius: 4px; font-size: 0.78rem; + color: #818cf8; border: 1px solid rgba(99,102,241,0.3); + background: rgba(99,102,241,0.06); cursor: pointer; + transition: background 0.15s; +} +.wr-load-more button:hover { background: rgba(99,102,241,0.12); } +.wr-load-more button:disabled { opacity: 0.5; cursor: default; } +.wr-load-more .wr-spinner { display: inline-block; width: 14px; height: 14px; border: 2px solid rgba(99,102,241,0.2); border-top-color: #818cf8; border-radius: 50%; animation: wr-spin 0.6s linear infinite; vertical-align: middle; margin-right: 0.3rem; } +@keyframes wr-spin { to { transform: rotate(360deg); } } +.ws.st-defined { border-left: 3px solid var(--color-border); opacity: 1; } + +/* Remove markdown box when used as page body (not inside cards) */ +.w-head ~ .markdown-body, +.w-stats ~ .markdown-body { + border: none; + background: none; + padding: 0.5rem 0; + border-radius: 0; +} + +/* ── Infinite scroll loading indicator ── */ +.wr-loading { text-align: center; padding: 2rem 1rem; display: none; } + +/* ── Comment cards (issue/PR detail) ── */ +.w-comment-card { + margin-bottom: 0.5rem; + background: var(--color-bg-secondary); + border-radius: var(--radius-md); + border: 1px solid var(--color-border); + overflow: hidden; +} +.w-comment-card .markdown-body { + border: none; + background: transparent; + padding: 0.5rem 0.65rem; + font-size: 0.8rem; +} + +/* ── Section cards (checks, commits, review) ── */ +.w-section-card { + background: var(--color-bg-secondary); + border-radius: var(--radius-md); + border: 1px solid var(--color-border); + padding: 0.5rem 0.6rem; +} +.w-commit-row { display: flex; gap: 0.5rem; align-items: baseline; padding: 0.3rem 0.55rem; border-radius: 3px; text-decoration: none; color: inherit; transition: background 0.1s; } +.w-commit-row:nth-child(odd) { background: var(--color-bg-secondary); } +.w-commit-row:nth-child(even) { background: var(--color-bg-tertiary); } +.w-commit-row:hover { background: rgba(99,102,241,0.08); } +.w-commit-row .w-commit-sha { color: var(--color-link); font-family: var(--font-mono); font-size: 0.72rem; flex-shrink: 0; } +.w-commit-row .w-commit-msg { color: var(--color-text); font-size: 0.85rem; flex: 1; } +.w-commit-row .w-commit-meta { color: var(--color-text-muted); font-size: 0.68rem; white-space: nowrap; } +.wr-loading.active { display: block; } +.wr-loading-spinner { display: inline-block; width: 24px; height: 24px; border: 2px solid rgba(99,102,241,0.2); border-top-color: #818cf8; border-radius: 50%; animation: wr-spin 0.6s linear infinite; vertical-align: middle; margin-right: 0.5rem; } +.wr-loading-text { font-size: 0.82rem; color: var(--color-text-muted); vertical-align: middle; } diff --git a/internal/webui/templates/compose.html b/internal/webui/templates/compose.html index d52c8c93..28b1fab4 100644 --- a/internal/webui/templates/compose.html +++ b/internal/webui/templates/compose.html @@ -1,108 +1,127 @@ {{define "title"}}Compose · Wave{{end}} {{define "content"}} -