@tea-agent/loop-agent 0.24.0 → 0.24.2-beta.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +45 -0
- package/dist/commands/init.js +2 -2
- package/dist/task/dag-source-paths.js +50 -0
- package/dist/task/frontend-project-capability.js +316 -0
- package/dist/task/task-demand-routing.js +432 -0
- package/dist/worker/console/chat/compaction-errors.js +64 -0
- package/dist/worker/console/chat/interview-adapter.js +17 -4
- package/dist/worker/console/chat/routes.js +6 -1
- package/dist/worker/console/interview/grill-me.js +28 -5
- package/dist/worker/console/static/assets/index-DePjoS_G.js +253 -0
- package/dist/worker/console/static/assets/index-Dp_2sKGk.css +1 -0
- package/dist/worker/console/static/index.html +2 -2
- package/dist/worker/observe/routes.js +8 -4
- package/dist/workflows/dag/backend-test-intake-context.js +10 -12
- package/dist/workflows/dag/backend-test-markdown-workflow.js +223 -42
- package/dist/workflows/dag/backend-test-result-contract.js +6 -2
- package/dist/workflows/dag/frontend-implementation-contract.js +23 -17
- package/dist/workflows/dag/frontend-project-capability.js +1 -316
- package/dist/workflows/dag/frontend-worktree-diff.js +4 -1
- package/dist/workflows/dag/init-hybrid.js +44 -42
- package/dist/workflows/dag/rerun-plan.js +6 -10
- package/dist/workflows/dag/task-demand-routing.js +1 -383
- package/docs/templates/frontend-implementation-contract.schema.json +1 -1
- package/package.json +1 -1
- package/skills/frontend-implementation/SKILL.md +7 -0
- package/skills/frontend-implementation/references/node-contracts.md +1 -1
- package/skills/frontend-verification/SKILL.md +4 -3
- package/skills/loop-agent/references/hybrid-dag.md +1 -1
- package/dist/worker/console/static/assets/index-BTbrEHnO.css +0 -1
- package/dist/worker/console/static/assets/index-D9qLevoP.js +0 -27
|
@@ -0,0 +1 @@
|
|
|
1
|
+
:root{--canvas: #f7f7f4;--canvas-soft: #fafaf7;--surface: #ffffff;--surface-muted: #f0efe9;--ink: #26251e;--body: #5a5852;--muted: #807d72;--muted-soft: #a09c92;--hairline: #e6e5e0;--hairline-strong: #cfcdc4;--orange: #f54e00;--orange-active: #d04200;--orange-soft: color-mix(in srgb, var(--orange) 12%, white);--blue: #3f6ea4;--green: #1f8a65;--amber: #a66a00;--red: #c72d53;--peach: #f1d8cb;--mint: #d8ead5;--space-1: 4px;--space-2: 8px;--space-3: 12px;--space-4: 16px;--space-5: 20px;--space-6: 24px;--space-8: 32px;--radius-sm: 6px;--radius-md: 8px;--radius-lg: 12px;--radius-pill: 999px;--transition-fast: .16s ease;--transition-base: .22s ease;--shadow-panel: 0 1px 2px rgb(38 37 30 / 4%), 0 8px 24px rgb(38 37 30 / 3%);--shadow-soft: 0 10px 30px rgb(38 37 30 / 6%);--shell-max: 1680px;color-scheme:light;font-family:Segoe UI,PingFang SC,Hiragino Sans GB,Noto Sans SC,system-ui,-apple-system,sans-serif;line-height:1.5}*{box-sizing:border-box}html{background:radial-gradient(1200px 480px at 8% -10%,rgb(241 216 203 / 45%),transparent 55%),radial-gradient(900px 420px at 100% 0%,rgb(216 234 213 / 35%),transparent 50%),var(--canvas);min-height:100%}body{margin:0;min-height:100vh;min-width:320px;background:transparent;color:var(--ink);font-size:14px}button,input,select,textarea{font:inherit}a{color:inherit}code,.cmd,.kv dd code{font-family:JetBrains Mono,SFMono-Regular,Consolas,monospace}.embedded-operator-nav{position:sticky;top:0;z-index:40;display:flex;gap:6px;align-items:center;margin:0;padding:8px max(24px,calc((100vw - var(--shell-max)) / 2));border-bottom:1px solid var(--hairline);background:color-mix(in srgb,var(--canvas) 92%,transparent);-webkit-backdrop-filter:blur(10px);backdrop-filter:blur(10px);font-size:13px;font-weight:700;flex-shrink:0}.console-app{display:flex;flex-direction:column;min-height:100dvh}html:has(.console-app.is-chat-focus),body:has(.console-app.is-chat-focus){height:100%;overflow:hidden}.console-app.is-chat-focus{height:100dvh;max-height:100dvh;overflow:hidden}.console-shell{flex:1 1 auto;display:flex;flex-direction:column;min-height:0;width:min(100% - 48px,var(--shell-max));margin:0 auto;padding:var(--space-5) 0 var(--space-12, 48px)}.embedded-operator-nav a,.embedded-operator-nav span{padding:6px 12px;border-radius:var(--radius-pill);text-decoration:none;color:var(--ink)}.embedded-operator-nav a:hover,.embedded-operator-nav a:focus-visible{background:var(--surface);color:var(--orange-active);outline:none}.embedded-operator-nav span[aria-current=page]{background:var(--ink);color:#fff}.top-nav{display:flex;flex-wrap:wrap;align-items:center;gap:var(--space-2);margin:0 0 var(--space-5);padding:6px;border:1px solid var(--hairline);border-radius:var(--radius-pill);background:color-mix(in srgb,var(--surface) 88%,transparent);-webkit-backdrop-filter:blur(8px);backdrop-filter:blur(8px);box-shadow:var(--shadow-panel)}.nav-link{appearance:none;display:inline-flex;align-items:center;min-height:34px;padding:0 14px;border:1px solid transparent;border-radius:var(--radius-pill);background:transparent;color:var(--body);font-size:13px;font-weight:600;cursor:pointer;transition:background-color var(--transition-fast),color var(--transition-fast),border-color var(--transition-fast),transform var(--transition-fast)}.nav-link:hover:not(:disabled),.nav-link:focus-visible:not(:disabled){background:var(--surface);color:var(--ink);outline:none}.nav-link-active{background:var(--ink);color:#fff}.nav-link-active:hover:not(:disabled),.nav-link-active:focus-visible:not(:disabled){background:var(--ink);color:#fff}.nav-link-external{margin-left:auto;border-color:var(--hairline);background:var(--surface);color:var(--orange-active)}.nav-link-external:hover:not(:disabled){border-color:var(--orange);background:var(--orange-soft);color:var(--orange-active)}.readiness{appearance:none;position:relative;display:inline-flex;align-items:center;gap:6px;margin-left:auto;min-height:34px;padding:0 28px 0 12px;border:1px solid var(--hairline);border-radius:var(--radius-pill);background:var(--surface);color:var(--ink);cursor:pointer;box-shadow:none;transition:background-color var(--transition-fast),border-color var(--transition-fast)}.readiness:hover{border-color:color-mix(in srgb,var(--ink) 18%,var(--hairline));background:color-mix(in srgb,var(--surface) 70%,var(--bg))}.readiness:focus-visible{outline:2px solid color-mix(in srgb,var(--orange) 45%,transparent);outline-offset:2px}.readiness:before{content:"";position:absolute;top:50%;right:10px;width:7px;height:7px;border-radius:50%;transform:translateY(-50%);background:var(--muted-soft)}.readiness[data-state=setup-required]{border-color:var(--hairline);background:var(--surface)}.readiness[data-state=setup-required]:before{background:var(--amber)}.readiness[data-state=ready]{border-color:var(--hairline);background:var(--surface)}.readiness[data-state=ready]:before{background:color-mix(in srgb,var(--green) 72%,white);box-shadow:0 0 0 3px color-mix(in srgb,var(--green) 12%,transparent)}.readiness[data-state=degraded]{border-color:var(--hairline);background:var(--surface)}.readiness[data-state=degraded]:before{background:var(--amber);box-shadow:0 0 0 3px color-mix(in srgb,var(--amber) 12%,transparent)}.readiness-label{margin:0;color:var(--muted);font-size:11px;font-weight:600;line-height:1;letter-spacing:.02em;text-transform:uppercase}.readiness strong{color:var(--ink);font-size:12px;font-weight:700;line-height:1}.readiness[data-state=ready] strong,.readiness[data-state=setup-required] strong,.readiness[data-state=degraded] strong{color:var(--ink)}.flash{display:flex;gap:var(--space-3);align-items:flex-start;margin:0 0 var(--space-4);padding:12px 14px;border:1px solid var(--hairline);border-radius:var(--radius-md);background:var(--surface);box-shadow:var(--shadow-panel);animation:flash-in .22s ease}.flash-error{border-color:color-mix(in srgb,var(--red) 35%,var(--hairline));background:color-mix(in srgb,var(--red) 6%,var(--surface));color:var(--red)}.flash-info{border-color:color-mix(in srgb,var(--blue) 30%,var(--hairline));background:color-mix(in srgb,var(--blue) 6%,var(--surface));color:var(--body)}.flash-label{flex:0 0 auto;min-width:3.5rem;font-size:12px;font-weight:700;letter-spacing:.02em}.flash-body{margin:0;overflow-wrap:anywhere}.muted{color:var(--muted)}.console-main{display:grid;gap:var(--space-4)}.console-main--fill{flex:1 1 auto;min-height:0;display:flex;flex-direction:column;gap:var(--space-3)}.console-shell.is-chat-focus{flex:1 1 auto;min-height:0;overflow:hidden;padding-bottom:var(--space-3)}.console-shell.is-chat-focus .top-nav{margin-bottom:var(--space-3);flex-shrink:0}.console-shell.is-chat-focus .console-main--fill{flex:1 1 auto;min-height:0;overflow:hidden}.panel--chat{flex:1 1 auto;min-height:0;display:flex;flex-direction:column;padding:var(--space-3);overflow:hidden}.panel--chat .oc-shell{flex:1 1 auto;min-height:0;height:auto;overflow:hidden}.console-shell.is-chat-focus .panel--chat{padding:var(--space-2)}.console-shell.is-chat-focus .panel--chat .oc-shell{min-height:0;gap:var(--space-2);overflow:hidden}.inspect-split-panel{position:fixed;z-index:50;top:16px;right:16px;bottom:16px;width:min(420px,calc(100vw - 32px));display:grid;align-content:start;gap:var(--space-4);padding:var(--space-5);overflow:auto;border:1px solid var(--hairline);border-radius:var(--radius-lg);background:var(--surface);box-shadow:var(--shadow-soft)}.inspect-split-nodes{display:grid;gap:6px;margin:0;padding:0;list-style:none;max-height:220px;overflow:auto}.inspect-split-nodes li{display:grid;grid-template-columns:1fr auto;gap:var(--space-2);align-items:center;padding:6px 8px;border:1px solid var(--hairline);border-radius:var(--radius-sm);background:var(--canvas-soft);font-size:12px}.inspect-split-nodes li.is-active{border-color:var(--orange);background:var(--orange-soft)}.inspect-split-nodes code{color:var(--muted);font-size:11px}.inspect-split-header{display:flex;align-items:start;justify-content:space-between;gap:var(--space-3)}.inspect-split-header h2,.inspect-split-panel h3{margin:0}.inspect-split-kicker,.inspect-split-id{margin:0;color:var(--muted);font-size:12px}.inspect-split-facts{display:grid;gap:var(--space-2);margin:0}.inspect-split-facts div{display:grid;grid-template-columns:7rem 1fr;gap:var(--space-2);padding-bottom:var(--space-2);border-bottom:1px solid var(--hairline)}.inspect-split-facts dt{color:var(--muted)}.inspect-split-facts dd{margin:0;overflow-wrap:anywhere}.inspect-split-events{display:grid;gap:8px;margin:0;padding-left:18px;color:var(--body);font-size:12px}.panel{min-width:0;border:1px solid var(--hairline);background:color-mix(in srgb,var(--surface) 96%,transparent);border-radius:var(--radius-lg);padding:var(--space-6);box-shadow:var(--shadow-panel)}.panel h2{margin:0 0 var(--space-2);color:var(--ink);font-size:18px;font-weight:700;letter-spacing:-.01em}.panel-lead{margin:0 0 var(--space-5);color:var(--muted);font-size:13px}.panel h3{margin:0 0 var(--space-3);color:var(--ink);font-size:15px;font-weight:700}.empty-state{border:1px dashed var(--hairline-strong);border-radius:var(--radius-md);padding:var(--space-6);background:var(--canvas-soft);color:var(--body)}.empty-title{margin:0 0 var(--space-2);color:var(--ink);font-size:16px;font-weight:700}.checklist{margin:0 0 var(--space-4);padding:var(--space-4);border:1px solid var(--hairline);border-radius:var(--radius-md);background:var(--canvas-soft);list-style:none;color:var(--body)}.checklist li{position:relative;padding:6px 0 6px 22px}.checklist li:before{content:"";position:absolute;left:0;top:12px;width:8px;height:8px;border-radius:50%;background:var(--orange);opacity:.75}.kv{display:grid;gap:var(--space-1);margin:0}.kv div{display:grid;grid-template-columns:9rem 1fr;gap:var(--space-3);padding:10px 0;border-bottom:1px solid var(--hairline)}.kv div:last-child{border-bottom:none}.kv dt{color:var(--muted);font-size:12px;font-weight:600}.kv dd{margin:0;color:var(--ink);overflow-wrap:anywhere}code{font-size:.85rem;color:var(--ink)}.cmd{display:block;width:100%;margin:0;padding:var(--space-4);border:1px solid var(--hairline);border-radius:var(--radius-md);background:#f3efe8;color:var(--ink);font-size:12px;line-height:1.55;overflow-x:auto;white-space:pre-wrap;word-break:break-word;max-height:280px}.external-link{color:var(--orange);text-decoration:none;font-weight:600}.external-link:hover{text-decoration:underline}.error{color:var(--red)}.wizard-steps{display:grid;grid-template-columns:repeat(auto-fit,minmax(108px,1fr));gap:var(--space-2);list-style:none;padding:0;margin:0 0 var(--space-5)}.wizard-steps li{position:relative;min-height:52px;display:grid;align-content:center;gap:2px;border:1px solid var(--hairline);border-radius:var(--radius-md);padding:8px 10px;background:var(--canvas-soft);color:var(--muted);font-size:12px;font-weight:650;transition:border-color var(--transition-fast),background-color var(--transition-fast),color var(--transition-fast),box-shadow var(--transition-fast)}.wizard-steps li[data-done=true]{border-color:color-mix(in srgb,var(--green) 35%,var(--hairline));background:color-mix(in srgb,var(--mint) 45%,var(--surface));color:var(--green)}.wizard-steps li[data-active=true]{border-color:var(--orange);background:var(--surface);color:var(--ink);box-shadow:0 0 0 3px var(--orange-soft)}.wizard-steps .step-index{font-size:10px;font-weight:700;letter-spacing:.04em;color:var(--muted-soft)}.wizard-steps li[data-active=true] .step-index,.wizard-steps li[data-done=true] .step-index{color:inherit;opacity:.8}.wizard-steps .step-label{line-height:1.25}.field{display:grid;gap:var(--space-1);margin:0 0 var(--space-4);width:min(100%,520px)}.field span{font-size:12px;font-weight:600;color:var(--muted)}.field input,.field textarea,.field select{width:100%;border:1px solid var(--hairline-strong);border-radius:var(--radius-md);background:var(--surface);color:var(--ink);transition:border-color var(--transition-fast),box-shadow var(--transition-fast)}.field input{min-height:40px;padding:0 var(--space-3)}.field select{min-height:40px;padding:0 var(--space-3);cursor:pointer}.field textarea{min-height:120px;padding:var(--space-3);resize:vertical}.field input:focus-visible,.field textarea:focus-visible,.field select:focus-visible{outline:none;border-color:var(--orange);box-shadow:0 0 0 3px var(--orange-soft)}.field input:disabled,.field textarea:disabled,.field select:disabled{background:var(--surface-muted);color:var(--muted);cursor:not-allowed}.workflow-hint{margin:calc(var(--space-1) * -1) 0 var(--space-3);color:var(--muted);font-size:12px}.meta-row{display:grid;gap:6px;margin:0 0 var(--space-4);padding:var(--space-3) var(--space-4);border:1px solid var(--hairline);border-radius:var(--radius-md);background:var(--canvas-soft);font-size:13px}.meta-row code{font-size:12px}.cta-row,.recovery-cta-row{display:flex;flex-wrap:wrap;gap:var(--space-2);align-items:center}.cta-row{margin-top:var(--space-3)}.cta-row .cmd{flex:1 1 100%}.wizard-body{display:grid;gap:var(--space-3);justify-items:stretch;animation:panel-in .24s ease}.wizard-body>div{display:grid;gap:var(--space-3);justify-items:stretch}.wizard-body .field{width:100%;max-width:640px}.wizard-body .cta-row{margin-top:var(--space-1)}.interview-card{width:100%;padding:var(--space-5);border:1px solid var(--hairline);border-radius:var(--radius-lg);background:linear-gradient(180deg,var(--surface),var(--canvas-soft));box-shadow:var(--shadow-panel)}.interview-q{margin:0 0 var(--space-2);color:var(--ink)}.btn,button{appearance:none;display:inline-flex;align-items:center;justify-content:center;gap:6px;min-height:40px;padding:0 16px;border:1px solid var(--hairline-strong);border-radius:var(--radius-md);background:var(--surface);color:var(--ink);font-size:13px;font-weight:650;cursor:pointer;transition:border-color var(--transition-fast),background-color var(--transition-fast),color var(--transition-fast),box-shadow var(--transition-fast),transform var(--transition-fast)}.btn:hover:not(:disabled),button:hover:not(:disabled){border-color:color-mix(in srgb,var(--ink) 18%,var(--hairline-strong));background:var(--canvas-soft);outline:none}.btn:focus-visible:not(:disabled),button:focus-visible:not(:disabled){outline:none;box-shadow:0 0 0 3px var(--orange-soft)}.btn:active:not(:disabled),button:active:not(:disabled){transform:translateY(1px)}.btn:disabled,button:disabled{opacity:.48;cursor:not-allowed;transform:none}.btn-primary,button.btn-primary{border-color:var(--orange);background:var(--orange);color:#fff;box-shadow:0 6px 16px #f54e002e}.btn-primary:hover:not(:disabled),button.btn-primary:hover:not(:disabled){border-color:var(--orange-active);background:var(--orange-active);color:#fff}.btn-secondary,button.btn-secondary{border-color:var(--hairline-strong);background:var(--surface);color:var(--ink)}.btn-ghost,button.btn-ghost{border-color:transparent;background:transparent;color:var(--body)}.btn-ghost:hover:not(:disabled),button.btn-ghost:hover:not(:disabled){background:var(--surface-muted);color:var(--ink)}.recovery-cta-row{margin-top:var(--space-3)}.recovery-confirm{margin-top:var(--space-3);padding:var(--space-4);border:1px solid color-mix(in srgb,var(--orange) 40%,var(--hairline));border-radius:var(--radius-md);background:color-mix(in srgb,var(--orange-soft) 55%,var(--surface))}.recovery-confirm h3{margin:0 0 var(--space-2);font-size:15px}.recovery-confirm .cta-row{margin-top:var(--space-3)}.recovery-report{margin-top:var(--space-3);padding:var(--space-4);border:1px solid var(--hairline);border-radius:var(--radius-md);background:var(--surface)}.recovery-report-header{display:flex;align-items:center;justify-content:space-between;gap:var(--space-3);margin-bottom:var(--space-3)}.recovery-report-header h3{margin:0;font-size:15px}.recovery-report-facts{display:grid;gap:var(--space-2);margin:0 0 var(--space-3)}.recovery-report-facts div{display:grid;grid-template-columns:5.5rem 1fr;gap:var(--space-2)}.recovery-report-facts dt{color:var(--muted);font-size:12px}.recovery-report-facts dd{margin:0;overflow-wrap:anywhere}.recovery-steps{margin:.5rem 0 .75rem 1.25rem;padding:0;line-height:1.5}.recovery-lane{margin-top:var(--space-4);padding-top:var(--space-3);border-top:1px solid var(--border, #ddd)}.recovery-lane h3{margin:0 0 .35rem;font-size:1rem}.recovery-cta-row button[disabled]{opacity:.4}@keyframes flash-in{0%{opacity:0;transform:translateY(-4px)}to{opacity:1;transform:none}}@keyframes panel-in{0%{opacity:0;transform:translateY(6px)}to{opacity:1;transform:none}}@media(max-width:800px){.console-shell{width:min(100% - 24px,var(--shell-max));padding-top:var(--space-5)}.readiness,.nav-link-external{margin-left:0}.inspect-split-panel{inset:auto 0 0;width:100%;max-height:min(76vh,680px);border-radius:var(--radius-lg) var(--radius-lg) 0 0}.kv div{grid-template-columns:1fr}.wizard-steps{grid-template-columns:repeat(2,minmax(0,1fr))}}.oc-shell{display:flex;flex-direction:column;height:100%;min-height:0;gap:var(--space-3);position:relative;overflow:hidden}.oc-workspace{display:flex;flex:1 1 auto;min-height:0;gap:0;position:relative;overflow:hidden}.oc-main{display:flex;flex-direction:column;flex:1 1 auto;min-width:0;min-height:0;overflow:hidden}.oc-sidebar{flex:0 0 280px;width:280px;min-width:0;display:flex;flex-direction:column;gap:var(--space-2);margin-right:var(--space-3);padding:var(--space-2);overflow:hidden;border:1px solid var(--hairline);border-radius:var(--radius-md);background:var(--surface)}.oc-sidebar.is-collapsed{flex:0 0 36px;width:36px;padding:6px;overflow:visible;align-items:center}.oc-sidebar-header-actions{display:inline-flex;align-items:center;gap:2px}.oc-sidebar-toggle{appearance:none;display:inline-grid;place-items:center;width:28px;height:28px;padding:0;border:1px solid transparent;border-radius:var(--radius-sm);background:transparent;color:var(--muted);cursor:pointer;transition:background-color var(--transition-fast),color var(--transition-fast),border-color var(--transition-fast)}.oc-sidebar-toggle:hover{color:var(--ink);background:color-mix(in srgb,var(--orange) 8%,var(--surface));border-color:var(--hairline)}.oc-sidebar-toggle:focus-visible{outline:2px solid color-mix(in srgb,var(--orange) 45%,transparent);outline-offset:1px}.oc-sidebar-toggle.is-collapsed{width:100%;height:28px}.oc-sidebar-section{display:flex;flex-direction:column;gap:6px;min-height:0}.oc-sidebar-section>header{display:flex;align-items:center;justify-content:space-between;gap:8px;font-size:13px;flex-shrink:0}.oc-sessions{flex:0 1 auto;max-height:min(34vh,320px)}.oc-session-scroll{min-height:0;max-height:min(28vh,260px);overflow:auto;padding:4px;border:1px solid var(--hairline);border-radius:var(--radius-md);background:linear-gradient(180deg,color-mix(in srgb,var(--canvas-soft) 70%,white),var(--canvas-soft))}.oc-session-group{display:flex;flex-direction:column;gap:2px}.oc-session-group>small{margin:4px 6px 2px;color:var(--muted-soft);font-size:10px;font-weight:700;letter-spacing:.04em;text-transform:uppercase}.oc-session-item{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:2px;align-items:center;border-radius:6px;transition:background-color var(--transition-fast)}.oc-session-item:hover,.oc-session-item:focus-within{background:color-mix(in srgb,var(--orange) 8%,var(--surface))}.oc-session-item.is-active{background:var(--orange-soft)}.oc-session-main{min-width:0;padding:5px 6px;border:0;background:transparent;color:inherit;text-align:left;cursor:pointer}.oc-session-main strong,.oc-session-main span{display:block;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.oc-session-main strong{font-size:12.5px;font-weight:650;line-height:1.25}.oc-session-main span{color:var(--muted-soft);font-size:10px;margin-top:1px;font-variant-numeric:tabular-nums}.oc-session-actions{display:none;align-items:center;gap:0;padding-right:2px}.oc-session-item:hover .oc-session-actions,.oc-session-item:focus-within .oc-session-actions,.oc-session-item.is-active .oc-session-actions{display:inline-flex}.oc-session-menu{padding:3px 4px;border:0;background:transparent;color:var(--muted);cursor:pointer;font:inherit;font-size:10px;white-space:nowrap}.oc-session-menu:hover{color:var(--ink)}.oc-session-delete{color:color-mix(in srgb,var(--red) 75%,var(--muted))}.oc-session-delete:hover{color:var(--red)}.oc-explorer{flex:1 1 auto;min-height:0;display:flex;flex-direction:column;gap:6px;border-top:1px solid var(--hairline);padding-top:var(--space-3);margin-top:2px}.oc-explorer-hint{margin:0 2px;color:var(--muted-soft);font-size:10.5px;letter-spacing:.01em}.oc-tree-scroll{flex:1 1 auto;min-height:120px;max-height:min(52vh,560px);overflow:auto;padding:4px;border:1px solid var(--hairline);border-radius:var(--radius-md);background:linear-gradient(180deg,color-mix(in srgb,var(--canvas-soft) 70%,white),var(--canvas-soft))}.oc-tree,.oc-tree ul{list-style:none;padding:0;margin:0}.oc-tree-node{position:relative}.oc-tree-row{appearance:none;display:grid;grid-template-columns:12px 14px minmax(0,1fr);align-items:center;gap:6px;width:100%;min-height:28px;margin:0;padding:3px 8px;border:0;border-radius:6px;background:transparent;color:var(--ink);text-align:left;cursor:pointer;transition:background-color var(--transition-fast),color var(--transition-fast)}.oc-tree-row:hover{background:color-mix(in srgb,var(--orange) 8%,var(--surface))}.oc-tree-row:focus-visible{outline:2px solid color-mix(in srgb,var(--orange) 45%,transparent);outline-offset:1px}.oc-tree-chevron{color:var(--muted-soft);transition:transform var(--transition-fast);transform:rotate(0)}.oc-tree-chevron.is-open{transform:rotate(90deg)}.oc-tree-chevron-spacer{width:12px;height:12px}.oc-tree-glyph{flex-shrink:0;color:var(--amber)}.oc-tree-glyph.is-folder-open{color:color-mix(in srgb,var(--orange) 70%,var(--amber))}.oc-tree-glyph.is-file{color:var(--muted)}.oc-tree-label{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:12.5px;line-height:1.25;font-variant-numeric:tabular-nums}.oc-tree-dir .oc-tree-label{font-weight:650;color:var(--ink)}.oc-tree-file .oc-tree-label{font-family:JetBrains Mono,SFMono-Regular,Consolas,Segoe UI Mono,monospace;font-size:11.5px;font-weight:500;color:var(--body)}.oc-tree-node.is-dot .oc-tree-label{color:var(--muted-soft);font-weight:500}.oc-tree-node.is-dot .oc-tree-glyph{opacity:.7}.oc-tree-children{position:relative;margin:0}.oc-tree-children:before{content:"";position:absolute;top:0;bottom:2px;left:var(--oc-tree-guide, 14px);width:1px;background:color-mix(in srgb,var(--hairline-strong) 55%,transparent);pointer-events:none}.oc-tree-empty,.oc-sidebar-error{margin:8px 6px;color:var(--muted);font-size:12px;line-height:1.45}.oc-sidebar-error{color:var(--red)}.oc-governance{margin-top:auto;border-top:1px solid var(--hairline);padding-top:8px}.oc-governance-links{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:4px}.oc-governance-links button{min-height:28px;padding:4px 6px;border:1px solid var(--hairline);border-radius:var(--radius-sm);background:var(--surface);color:var(--muted);cursor:pointer;font:inherit;font-size:11px;font-weight:600;text-align:center}.oc-governance-links button:hover{color:var(--ink);border-color:color-mix(in srgb,var(--ink) 18%,var(--hairline))}.oc-governance-panel button{border:0;background:transparent;color:var(--muted);cursor:pointer;font:inherit;font-size:11px}.oc-governance-panel{position:absolute;z-index:10;left:282px;bottom:0;width:min(440px,calc(100vw - 320px));max-height:75%;overflow:auto;padding:12px;border:1px solid var(--hairline);border-radius:var(--radius-md);background:var(--surface);box-shadow:var(--shadow-panel)}.oc-governance-panel header,.oc-governance-panel article,.oc-governance-panel label{display:flex;align-items:center;justify-content:space-between;gap:8px;padding:5px 0}.oc-governance-panel small,.oc-governance-panel span{color:var(--muted);font-size:11px}.oc-governance-panel button:disabled{cursor:wait;opacity:.5}.oc-governance-error{color:var(--danger);font-size:12px}.oc-model-provider{display:block!important;border-bottom:1px solid var(--hairline)}.oc-model-provider>div,.oc-model-row,.oc-model-row>div{display:flex;align-items:center;justify-content:space-between;gap:8px}.oc-model-row{padding:4px 0 4px 10px}.oc-model-form{display:grid;gap:6px;margin-top:10px}.oc-model-form input{min-width:0;border:1px solid var(--hairline);border-radius:var(--radius-sm);background:var(--surface);color:var(--ink);padding:6px 8px;font:inherit;font-size:12px}.oc-model-form button{justify-self:start}.oc-model-test-result{padding:6px 8px;border-radius:var(--radius-sm);background:var(--surface-muted);font-size:12px}.oc-file-preview{position:absolute;z-index:4;inset:0;display:flex;flex-direction:column;overflow:hidden;border:1px solid var(--hairline);border-radius:var(--radius-md);background:var(--surface);box-shadow:var(--shadow-panel)}.oc-file-preview header{display:flex;align-items:center;gap:var(--space-2);padding:10px 12px;border-bottom:1px solid var(--hairline)}.oc-file-tabs{display:flex;min-width:0;gap:2px;overflow-x:auto}.oc-file-tabs button{max-width:220px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;border:0;border-radius:var(--radius-sm);background:transparent;color:var(--muted);padding:4px 7px;cursor:pointer}.oc-file-tabs button.is-active{background:var(--surface-muted);color:var(--ink);font-weight:650}.oc-file-preview header span{flex:1;color:var(--muted);font-size:11px}.oc-file-preview pre{flex:1;min-height:0;margin:0;padding:var(--space-3);overflow:auto;font-size:12px;line-height:1.5;white-space:pre-wrap}.oc-file-preview-loading{position:absolute;z-index:5;right:12px;top:12px;padding:6px 8px;border-radius:var(--radius-sm);background:var(--surface-muted);color:var(--muted);font-size:12px}.oc-process{display:flex;flex-direction:column;flex:0 0 min(360px,42%);width:min(360px,42%);min-width:280px;min-height:0;border:1px solid var(--hairline);border-radius:var(--radius-md);background:var(--surface);box-shadow:var(--shadow-panel);margin-left:var(--space-3);overflow:hidden;animation:panel-in .18s ease}.oc-process-head{display:flex;align-items:center;justify-content:space-between;gap:var(--space-2);padding:10px 12px;border-bottom:1px solid var(--hairline);flex-shrink:0}.oc-process-title{display:inline-flex;align-items:center;gap:8px;font-weight:700;font-size:13px;color:var(--ink)}.oc-process-title-mark{width:8px;height:8px;border-radius:50%;background:var(--orange)}.oc-process-count{display:inline-flex;align-items:center;justify-content:center;min-width:20px;height:18px;padding:0 6px;border-radius:var(--radius-pill);background:var(--surface-muted);color:var(--muted);font-size:11px;font-weight:650}.oc-process-close{font-size:12px}.oc-process-timeline{flex:1 1 auto;min-height:0;overflow-y:auto;padding:var(--space-3);display:flex;flex-direction:column;gap:0}.oc-process-empty{padding:var(--space-4);color:var(--muted);font-size:12.5px;line-height:1.5;text-align:center}.oc-process-item{display:grid;grid-template-columns:20px 52px minmax(0,1fr);gap:8px;align-items:start;padding:4px 0}.oc-process-rail{position:relative;display:grid;place-items:center;min-height:34px}.oc-process-rail:before{position:absolute;top:0;bottom:0;width:1px;background:var(--hairline);content:""}.oc-process-item.is-first .oc-process-rail:before{top:50%}.oc-process-item.is-last .oc-process-rail:before{bottom:50%}.oc-process-dot{position:relative;z-index:1;width:8px;height:8px;border-radius:50%;background:var(--surface);border:2px solid color-mix(in srgb,var(--orange) 55%,var(--hairline))}.oc-process-item.is-running .oc-process-dot{border-color:var(--orange-active);background:var(--orange-soft)}.oc-process-item.is-error .oc-process-dot{border-color:var(--red);background:color-mix(in srgb,var(--red) 18%,var(--surface))}.oc-process-time{padding-top:10px;font-size:11px;color:var(--muted-soft);font-variant-numeric:tabular-nums;white-space:nowrap}.oc-process-body{min-width:0}.oc-process-toggle{cursor:pointer;font:inherit}.oc-process-toggle.is-active{border-color:color-mix(in srgb,var(--orange) 40%,var(--hairline));background:color-mix(in srgb,var(--orange) 10%,var(--surface));color:var(--orange-active)}.oc-process-toggle.is-running{border-color:color-mix(in srgb,var(--orange) 40%,var(--hairline));color:var(--orange-active)}.oc-process-toggle.is-error{border-color:color-mix(in srgb,var(--red) 35%,var(--hairline));background:color-mix(in srgb,var(--red) 8%,var(--surface));color:var(--red)}.oc-process-summary{display:inline-flex;align-items:center;gap:4px;padding:3px 10px;border:1px solid var(--hairline);border-radius:var(--radius-pill);background:var(--surface);color:var(--muted);font-size:11.5px;font-weight:650;cursor:pointer;font:inherit;max-width:min(420px,100%);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.oc-process-summary:hover,.oc-process-summary.is-open{border-color:color-mix(in srgb,var(--orange) 35%,var(--hairline));color:var(--orange-active)}.oc-process-summary.is-running{color:var(--orange-active);border-color:color-mix(in srgb,var(--orange) 40%,var(--hairline));background:var(--orange-soft)}.oc-process-summary.is-error{color:var(--red);border-color:color-mix(in srgb,var(--red) 35%,var(--hairline));background:color-mix(in srgb,var(--red) 8%,var(--surface))}.oc-process-summary.is-ok{color:var(--green);border-color:color-mix(in srgb,var(--green) 30%,var(--hairline))}@media(max-width:900px){.oc-sidebar{position:absolute;inset:0 auto 0 0;z-index:7;width:min(300px,calc(100% - 24px));margin-right:0;box-shadow:var(--shadow-soft)}.oc-sidebar.is-collapsed{position:static;width:auto}.oc-workspace.has-process .oc-process{position:absolute;top:0;right:0;bottom:0;z-index:8;width:min(360px,calc(100% - 24px));min-width:0;flex:none;margin-left:0;border-radius:var(--radius-md) 0 0 var(--radius-md);box-shadow:var(--shadow-soft)}}.oc-banner{display:flex;flex-wrap:wrap;align-items:center;gap:var(--space-2) var(--space-3);padding:10px 14px;border:1px solid var(--hairline);border-radius:var(--radius-md);background:color-mix(in srgb,var(--surface) 94%,transparent);box-shadow:var(--shadow-panel)}.oc-shell>.oc-alert:first-child{margin-top:0}.oc-banner-title{display:inline-flex;align-items:center;gap:6px;font-weight:700;color:var(--ink)}.oc-banner-dot{width:8px;height:8px;border-radius:50%;background:var(--orange)}.oc-banner-body{color:var(--body);font-size:12.5px;line-height:1.45}.oc-banner-body strong{color:var(--ink)}.oc-cap-chips{display:inline-flex;flex-wrap:wrap;gap:6px;margin-left:auto;align-items:center}.oc-cap-chip{display:inline-flex;align-items:center;gap:5px;padding:3px 10px;border:1px solid color-mix(in srgb,var(--green) 30%,var(--hairline));border-radius:var(--radius-pill);background:color-mix(in srgb,var(--mint) 50%,var(--surface));color:var(--green);font-size:11.5px;font-weight:650}button.oc-cap-chip{font:inherit;cursor:pointer}.oc-tools-menu{position:relative}.oc-tools-menu--composer .oc-tools-catalog{top:auto;bottom:calc(100% + 8px)}.oc-tools-toggle.is-active{border-color:color-mix(in srgb,var(--orange) 40%,var(--hairline));background:color-mix(in srgb,var(--orange) 10%,var(--surface));color:var(--orange-active)}.oc-tools-catalog{position:absolute;top:calc(100% + 8px);right:0;z-index:20;width:min(420px,calc(100vw - 48px));max-height:min(480px,60vh);display:flex;flex-direction:column;border:1px solid var(--hairline);border-radius:var(--radius-md);background:var(--surface);box-shadow:var(--shadow-soft);animation:panel-in .16s ease;overflow:hidden}.oc-tools-catalog-head{display:flex;align-items:center;gap:8px;padding:10px 12px;border-bottom:1px solid var(--hairline);flex-shrink:0}.oc-tools-catalog-head strong{font-size:13px;color:var(--ink)}.oc-tools-catalog-meta{font-size:11.5px;color:var(--muted-soft)}.oc-tools-catalog-close{margin-left:auto;font-size:12px}.oc-tools-catalog-body{overflow-y:auto;padding:var(--space-3);display:flex;flex-direction:column;gap:var(--space-3)}.oc-tools-group-title{margin:0 0 8px;font-size:11px;font-weight:700;letter-spacing:.04em;text-transform:uppercase;color:var(--muted-soft)}.oc-tools-list{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:8px}.oc-tools-item{padding:8px 10px;border:1px solid var(--hairline);border-radius:var(--radius-sm);background:color-mix(in srgb,var(--surface-muted) 55%,var(--surface))}.oc-tools-item-head{display:flex;align-items:center;gap:8px;margin-bottom:4px}.oc-tools-name{font-family:JetBrains Mono,SFMono-Regular,Consolas,monospace;font-size:12.5px;font-weight:650;color:var(--ink)}.oc-tools-kind{margin-left:auto;font-size:10.5px;font-weight:650;padding:1px 7px;border-radius:var(--radius-pill);color:var(--muted);background:var(--surface);border:1px solid var(--hairline)}.oc-tools-kind.is-explore,.oc-tools-kind.is-read{color:var(--green);border-color:color-mix(in srgb,var(--green) 28%,var(--hairline));background:color-mix(in srgb,var(--mint) 45%,transparent)}.oc-tools-kind.is-mutation{color:var(--orange-active);border-color:color-mix(in srgb,var(--orange) 35%,var(--hairline));background:var(--orange-soft)}.oc-tools-kind.is-long-running{color:var(--amber);border-color:color-mix(in srgb,var(--amber) 35%,var(--hairline));background:color-mix(in srgb,var(--amber) 10%,var(--surface))}.oc-tools-desc{margin:0;font-size:12px;line-height:1.45;color:var(--body)}.oc-tools-cli{margin:4px 0 0;font-size:11px;color:var(--muted-soft)}.oc-tools-cli code{font-size:11px;color:var(--muted)}.oc-alert{padding:8px 12px;border-radius:var(--radius-md);font-size:12.5px;line-height:1.45;animation:panel-in .18s ease}.oc-alert-warning{border:1px solid color-mix(in srgb,var(--amber) 35%,var(--hairline));background:color-mix(in srgb,var(--amber) 8%,var(--surface));color:var(--amber)}.oc-alert-error{border:1px solid color-mix(in srgb,var(--red) 35%,var(--hairline));background:color-mix(in srgb,var(--red) 7%,var(--surface));color:var(--red)}.oc-messages{flex:1 1 auto;min-height:0;overflow-y:auto;padding:var(--space-3) var(--space-3) var(--space-2);display:flex;flex-direction:column;gap:var(--space-3)}.oc-messages.has-thread:before{content:"";flex:1 1 auto;min-height:0}.oc-messages.is-empty{justify-content:center;align-items:center;padding:var(--space-5) var(--space-4) var(--space-4);background:radial-gradient(640px 300px at 50% 28%,rgb(241 216 203 / 34%),transparent 68%),radial-gradient(520px 240px at 78% 78%,rgb(216 234 213 / 26%),transparent 62%)}.oc-main.is-landing{position:relative}.oc-day-sep{display:flex;align-items:center;gap:var(--space-3);color:var(--muted-soft);font-size:11px;font-weight:600;letter-spacing:.04em;text-transform:uppercase;margin:var(--space-2) 0}.oc-day-sep:before,.oc-day-sep:after{content:"";flex:1;height:1px;background:var(--hairline)}.oc-empty{display:flex;flex-direction:column;align-items:center;justify-content:center;flex:0 1 auto;gap:var(--space-5);text-align:center;width:min(100%,680px);padding:var(--space-2) var(--space-2) var(--space-4);animation:oc-empty-in .28s ease both}@keyframes oc-empty-in{0%{opacity:0;transform:translateY(8px)}to{opacity:1;transform:translateY(0)}}@media(prefers-reduced-motion:reduce){.oc-empty{animation:none}}.oc-empty-hero{display:flex;flex-direction:column;align-items:center;gap:var(--space-3)}.oc-empty-icon{display:grid;place-items:center;width:56px;height:56px;border-radius:50%;background:linear-gradient(145deg,color-mix(in srgb,var(--orange) 18%,white),color-mix(in srgb,var(--mint) 35%,white));color:var(--orange-active);font-size:26px;font-weight:800;font-family:var(--font-mono, "JetBrains Mono", monospace);box-shadow:0 0 0 1px color-mix(in srgb,var(--orange) 18%,transparent)}.oc-empty-copy{display:flex;flex-direction:column;align-items:center;gap:var(--space-2)}.oc-empty h3{margin:0;font-size:20px;font-weight:700;letter-spacing:-.02em;color:var(--ink)}.oc-empty p{margin:0;max-width:48ch;color:var(--muted);font-size:13px;line-height:1.55}.oc-suggestions{display:grid;grid-template-columns:1fr 1fr;gap:var(--space-2);width:100%}@media(max-width:560px){.oc-suggestions{grid-template-columns:1fr}}.oc-suggestion{appearance:none;display:flex;flex-direction:column;gap:4px;text-align:left;padding:12px 14px;border:1px solid var(--hairline);border-radius:var(--radius-md);background:color-mix(in srgb,var(--surface) 88%,transparent);color:var(--ink);cursor:pointer;-webkit-backdrop-filter:blur(6px);backdrop-filter:blur(6px);transition:border-color var(--transition-fast),background var(--transition-fast),transform var(--transition-fast),box-shadow var(--transition-fast)}.oc-suggestion:hover:not(:disabled){border-color:var(--orange);background:var(--surface);box-shadow:0 0 0 3px var(--orange-soft);transform:translateY(-1px)}.oc-suggestion:disabled{opacity:.55;cursor:not-allowed}.oc-suggestion strong{font-size:13px;font-weight:700}.oc-suggestion span{color:var(--muted);font-size:12px;line-height:1.4}.oc-empty-tips{display:flex;flex-wrap:wrap;justify-content:center;gap:var(--space-2) var(--space-3);margin:0;padding:0;list-style:none;color:var(--muted-soft);font-size:12px}.oc-empty-tips li{display:inline-flex;align-items:center;gap:6px}.oc-empty-tips kbd{display:inline-grid;place-items:center;min-width:1.4em;padding:1px 6px;border:1px solid var(--hairline);border-radius:5px;background:var(--surface);color:var(--body);font-family:var(--font-mono, "JetBrains Mono", monospace);font-size:11px;font-weight:600;line-height:1.4}.oc-empty-cta{margin-top:calc(var(--space-1) * -1)}.oc-row{display:flex;flex-direction:column;gap:4px;max-width:100%}.oc-row.is-user{align-self:flex-end;align-items:flex-end;width:auto;max-width:min(100%,720px)}.oc-row.is-assistant,.oc-row.is-tool{align-self:stretch;align-items:stretch;width:100%;max-width:100%}.oc-meta{display:flex;align-items:center;gap:8px;font-size:11.5px;color:var(--muted-soft)}.oc-row.is-assistant .oc-meta,.oc-row.is-user .oc-meta{width:100%;max-width:100%}.oc-message-copy{appearance:none;display:inline-flex;align-items:center;justify-content:center;flex-shrink:0;width:26px;height:26px;margin-left:auto;padding:0;border:1px solid transparent;border-radius:var(--radius-sm);background:transparent;color:var(--muted-soft);cursor:pointer;opacity:0;transition:opacity var(--transition-fast),background var(--transition-fast),color var(--transition-fast),border-color var(--transition-fast)}.oc-row:hover .oc-message-copy,.oc-row:focus-within .oc-message-copy,.oc-message-copy:focus-visible{opacity:1}.oc-message-copy:hover{color:var(--body);background:color-mix(in srgb,var(--ink) 6%,transparent);border-color:var(--hairline)}.oc-message-copy.is-copied{opacity:1;color:var(--green, #1a7f37);border-color:color-mix(in srgb,var(--green, #1a7f37) 35%,transparent)}@media(hover:none){.oc-message-copy{opacity:.5}}.oc-avatar{display:grid;place-items:center;width:22px;height:22px;border-radius:50%;font-size:11px;font-weight:700;flex-shrink:0}.oc-avatar-user{background:var(--ink);color:#fff}.oc-avatar-assistant{background:var(--orange);color:#fff}.oc-role{font-weight:700;color:var(--body)}.oc-time{font-variant-numeric:tabular-nums}.oc-bubble{padding:10px 14px;border-radius:var(--radius-lg);font-size:14px;line-height:1.6;overflow-wrap:anywhere;word-break:break-word;animation:bubble-in .16s ease}@keyframes bubble-in{0%{opacity:0;transform:translateY(4px)}to{opacity:1;transform:none}}.oc-bubble-user{background:var(--ink);color:#fff;border-bottom-right-radius:4px}.oc-bubble-assistant{background:var(--surface);color:var(--ink);border:1px solid var(--hairline);border-bottom-left-radius:4px;box-shadow:var(--shadow-panel)}.oc-bubble-tool{background:var(--canvas-soft);color:var(--body);border:1px solid var(--hairline)}.oc-bubble-error{border-color:color-mix(in srgb,var(--red) 40%,var(--hairline));background:color-mix(in srgb,var(--red) 7%,var(--surface));color:var(--red)}.oc-md{display:flex;flex-direction:column;gap:var(--space-2)}.oc-md>*:first-child{margin-top:0}.oc-md>*:last-child{margin-bottom:0}.oc-md p{margin:0}.oc-md h1,.oc-md h2,.oc-md h3,.oc-md h4{margin:var(--space-3) 0 var(--space-2);color:inherit;font-weight:700;line-height:1.3}.oc-md h1{font-size:1.3em}.oc-md h2{font-size:1.18em}.oc-md h3{font-size:1.06em}.oc-md h4{font-size:1em}.oc-md ul,.oc-md ol{margin:0;padding-left:1.4em;display:flex;flex-direction:column;gap:2px}.oc-md li{margin:0}.oc-md blockquote{margin:0;padding:2px 0 2px 12px;border-left:3px solid var(--hairline-strong);color:var(--body)}.oc-bubble-user .oc-md blockquote{border-left-color:#ffffff59;color:#ffffffd1}.oc-md code{font-size:.86em;padding:1px 5px;border-radius:4px;background:color-mix(in srgb,var(--ink) 8%,transparent)}.oc-bubble-user .oc-md code{background:#ffffff29}.oc-md pre{margin:0;padding:0;background:transparent}.oc-codeblock{margin:0;border:1px solid var(--hairline);border-radius:var(--radius-md);background:var(--surface-muted);overflow:hidden}.oc-bubble-assistant .oc-codeblock{background:#f3efe8}.oc-codeblock-head{display:flex;align-items:center;justify-content:space-between;gap:var(--space-2);padding:5px 6px 5px 10px;border-bottom:1px solid var(--hairline);font-size:11px;color:var(--muted);background:color-mix(in srgb,var(--ink) 4%,transparent)}.oc-codeblock-lang{font-family:JetBrains Mono,SFMono-Regular,Consolas,monospace;font-weight:600;text-transform:lowercase}.oc-codeblock-copy{appearance:none;display:inline-flex;align-items:center;gap:5px;min-height:26px;padding:0 9px;border:1px solid transparent;border-radius:var(--radius-sm);background:transparent;color:var(--muted);font-size:11px;font-weight:600;cursor:pointer;transition:background var(--transition-fast),color var(--transition-fast),border-color var(--transition-fast)}.oc-codeblock-copy:hover{background:var(--surface);color:var(--ink);border-color:var(--hairline-strong)}.oc-codeblock pre{margin:0;padding:var(--space-3) var(--space-4);overflow-x:auto;font-family:JetBrains Mono,SFMono-Regular,Consolas,monospace;font-size:12.5px;line-height:1.55;white-space:pre;color:var(--ink)}.oc-md a{color:var(--orange);text-decoration:none;border-bottom:1px solid color-mix(in srgb,var(--orange) 40%,transparent)}.oc-md a:hover{border-bottom-color:var(--orange)}.oc-md hr{margin:var(--space-2) 0;border:none;border-top:1px solid var(--hairline-strong)}.oc-stream{white-space:pre-wrap;font-family:JetBrains Mono,SFMono-Regular,Consolas,monospace;font-size:13px;line-height:1.55}.oc-cursor{display:inline-block;width:7px;height:1.05em;margin-left:2px;vertical-align:text-bottom;background:var(--orange);animation:oc-blink 1s steps(2,start) infinite;border-radius:1px}@keyframes oc-blink{to{opacity:0}}.oc-tool{border:1px solid var(--hairline);border-radius:var(--radius-md);background:var(--surface);overflow:hidden;box-shadow:var(--shadow-panel);animation:panel-in .16s ease}.oc-tool.is-error{border-color:color-mix(in srgb,var(--red) 38%,var(--hairline))}.oc-tool.is-running{border-color:color-mix(in srgb,var(--orange) 40%,var(--hairline))}.oc-tool-head{display:flex;align-items:center;gap:var(--space-2);width:100%;padding:9px 12px;border:none;background:transparent;text-align:left;cursor:pointer;font:inherit;color:var(--ink)}.oc-tool-head:hover{background:var(--surface-muted)}.oc-tool-chevron{flex-shrink:0;width:12px;height:12px;transition:transform var(--transition-fast)}.oc-tool-head[aria-expanded=true] .oc-tool-chevron{transform:rotate(90deg)}.oc-tool-icon{display:grid;place-items:center;width:22px;height:22px;border-radius:6px;background:var(--surface-muted);color:var(--orange-active);flex-shrink:0}.oc-tool-name{font-family:JetBrains Mono,SFMono-Regular,Consolas,monospace;font-size:12.5px;font-weight:650;flex-shrink:0}.oc-tool-args{color:var(--muted);font-size:11.5px;font-family:JetBrains Mono,SFMono-Regular,Consolas,monospace;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;margin-right:8px;min-width:0}.oc-tool-status{margin-left:auto;flex-shrink:0;font-size:11px;font-weight:650;padding:2px 8px;border-radius:var(--radius-pill)}.oc-tool-waiting{color:var(--muted);font-size:12px;animation:pulse 1.5s infinite}.oc-tool-status.is-running{color:var(--orange-active);background:var(--orange-soft)}.oc-tool-status.is-ok{color:var(--green);background:color-mix(in srgb,var(--mint) 60%,transparent)}.oc-tool-status.is-error{color:var(--red);background:color-mix(in srgb,var(--red) 10%,transparent)}.oc-tool-body{padding:0 var(--space-4) var(--space-3);border-top:1px solid var(--hairline)}.oc-tool-section{display:flex;flex-direction:column;gap:4px;padding-top:var(--space-3)}.oc-tool-label{font-size:11px;font-weight:700;letter-spacing:.03em;text-transform:uppercase;color:var(--muted-soft)}.oc-tool-code{margin:0;padding:var(--space-3);border-radius:var(--radius-sm);background:var(--surface-muted);font-family:JetBrains Mono,SFMono-Regular,Consolas,monospace;font-size:12px;line-height:1.5;white-space:pre-wrap;word-break:break-word;max-height:260px;overflow-y:auto;color:var(--body)}.oc-composer{flex:0 0 auto;flex-shrink:0;border-top:1px solid var(--hairline);padding:var(--space-2) var(--space-3) var(--space-3);background:color-mix(in srgb,var(--canvas) 92%,transparent);-webkit-backdrop-filter:blur(8px);backdrop-filter:blur(8px);z-index:2}.oc-composer-inner{display:flex;flex-direction:column;gap:var(--space-2);max-width:1080px;margin:0 auto;position:relative}.console-shell.is-chat-focus .oc-composer-inner{max-width:none}.oc-composer-shell{position:relative;display:flex;align-items:flex-end;gap:var(--space-2);padding:8px 8px 8px 14px;border:1px solid var(--hairline-strong);border-radius:var(--radius-lg);background:var(--surface);box-shadow:var(--shadow-panel);transition:border-color var(--transition-fast),box-shadow var(--transition-fast)}.oc-composer-shell:focus-within{border-color:var(--orange);box-shadow:0 0 0 3px var(--orange-soft)}.oc-composer-shell.is-busy{opacity:.78}.oc-textarea{flex:1 1 auto;min-width:0;min-height:52px;max-height:280px;padding:8px 0;border:none;background:transparent;color:var(--ink);font:inherit;font-size:14px;line-height:1.5;resize:none;outline:none}.oc-textarea::placeholder{color:var(--muted-soft)}.oc-send{appearance:none;display:grid;place-items:center;width:38px;height:38px;flex-shrink:0;padding:0;border:1px solid var(--orange);border-radius:50%;background:var(--orange);color:#fff;cursor:pointer;transition:background var(--transition-fast),border-color var(--transition-fast),transform var(--transition-fast)}.oc-send:hover:not(:disabled){background:var(--orange-active);border-color:var(--orange-active)}.oc-send:active:not(:disabled){transform:scale(.94)}.oc-send:disabled{opacity:.4;cursor:not-allowed}.oc-stop{appearance:none;display:grid;place-items:center;width:38px;height:38px;flex-shrink:0;padding:0;border:1px solid var(--hairline-strong);border-radius:50%;background:var(--surface);color:var(--red);cursor:pointer;transition:background var(--transition-fast),border-color var(--transition-fast)}.oc-stop:hover{background:color-mix(in srgb,var(--red) 8%,var(--surface));border-color:var(--red)}.oc-composer-foot{display:flex;align-items:center;gap:var(--space-2);flex-wrap:wrap;font-size:11.5px;color:var(--muted)}.oc-composer-hint{display:inline-flex;align-items:center;gap:5px}.oc-kbd{display:inline-block;padding:1px 6px;border:1px solid var(--hairline-strong);border-bottom-width:2px;border-radius:4px;background:var(--surface);color:var(--body);font-family:JetBrains Mono,SFMono-Regular,Consolas,monospace;font-size:10.5px;line-height:1.4}.oc-operation-card{width:min(720px,calc(100% - 42px));margin:var(--space-3) auto;padding:var(--space-4);border:1px solid var(--hairline-strong);border-left:4px solid var(--blue);border-radius:var(--radius-lg);background:color-mix(in srgb,var(--surface) 96%,var(--blue) 4%);box-shadow:var(--shadow-sm)}.oc-operation-card.is-succeeded{border-left-color:var(--green)}.oc-operation-card.is-failed,.oc-operation-card.is-timed-out{border-left-color:var(--red)}.oc-operation-card.is-needs-reconcile{border-left-color:var(--orange)}.oc-operation-head,.oc-operation-actions{display:flex;align-items:center;justify-content:space-between;gap:var(--space-3)}.oc-operation-kicker{margin-bottom:2px;color:var(--muted);font-size:10px;font-weight:700;letter-spacing:.12em;text-transform:uppercase}.oc-operation-state{padding:3px 9px;border-radius:999px;background:color-mix(in srgb,var(--blue) 12%,transparent);color:var(--blue);font-size:11px;font-weight:700}.oc-operation-state.is-succeeded{background:color-mix(in srgb,var(--green) 12%,transparent);color:var(--green)}.oc-operation-state.is-failed,.oc-operation-state.is-timed-out{background:color-mix(in srgb,var(--red) 12%,transparent);color:var(--red)}.oc-operation-state.is-needs-reconcile{background:color-mix(in srgb,var(--orange) 14%,transparent);color:var(--orange-active)}.oc-operation-id,.oc-operation-controller{margin-top:var(--space-2);color:var(--muted);font-size:12px;overflow-wrap:anywhere}.oc-operation-identities{display:flex;flex-wrap:wrap;gap:6px;margin-top:var(--space-3)}.oc-operation-identities span{padding:3px 7px;border:1px solid var(--hairline);border-radius:999px;background:var(--surface-raised);color:var(--muted);font-size:11px}.oc-operation-preview{max-height:150px;margin:var(--space-3) 0 0;padding:var(--space-2) var(--space-3);overflow:auto;border:1px solid var(--hairline);border-radius:var(--radius-md);background:var(--surface-sunken);color:var(--body);font-size:11px;line-height:1.45;white-space:pre-wrap;overflow-wrap:anywhere}.oc-operation-preview.is-error{border-color:color-mix(in srgb,var(--red) 30%,var(--hairline));color:var(--red)}.oc-operation-actions{justify-content:flex-start;margin-top:var(--space-3)}.oc-operation-actions time{margin-left:auto;color:var(--muted-soft);font-size:11px}.oc-interview-start{margin:var(--space-3) auto}.oc-draft-card{margin:var(--space-3);padding:var(--space-4);border:1px solid var(--hairline);border-radius:var(--radius-lg);background:var(--surface-raised)}.oc-draft-card>header,.oc-task-kind-card{display:flex;align-items:center;gap:var(--space-3);justify-content:space-between}.oc-interview-bubble{margin:var(--space-3) 0;padding:var(--space-3);border-left:3px solid var(--accent);background:var(--surface-sunken)}.oc-interview-bubble textarea{width:100%;min-height:72px;margin:var(--space-2) 0;resize:vertical}.oc-interview-bubble .btn+.btn{margin-left:var(--space-2)}.oc-draft-card pre{max-height:300px;overflow:auto;white-space:pre-wrap;font-size:11px}.oc-assessment{margin:var(--space-3) 0;color:var(--orange)}.oc-assessment.is-fresh,.oc-task-kind-card.is-confirmed{color:var(--green)}.oc-task-kind-card{margin-top:var(--space-3);padding:var(--space-3);border:1px solid var(--hairline);border-radius:var(--radius-md)}.oc-task-kind-card small{color:var(--muted)}@media(prefers-reduced-motion:reduce){*,*:before,*:after{animation-duration:.01ms!important;animation-iteration-count:1!important;scroll-behavior:auto!important;transition-duration:.01ms!important}}.oc-human-gate-card{margin:12px 0;padding:14px;border:1px solid var(--border);border-left:4px solid var(--accent);border-radius:10px;background:var(--surface-raised)}.oc-human-gate-card header,.oc-human-gate-card footer{display:flex;justify-content:space-between;align-items:center;gap:12px}.oc-human-gate-card header div{display:grid;gap:3px}.oc-human-gate-card header span{font-size:12px;text-transform:uppercase}.oc-human-gate-challenges,.oc-human-gate-card ol{max-height:160px;overflow:auto}.oc-human-gate-card footer{margin-top:12px}.oc-human-gate-error{margin-top:8px;color:var(--danger)}.oc-human-gate-card.is-confirmed{border-left-color:var(--success)}.oc-task-context{margin:var(--space-3);padding:var(--space-3);border:1px solid var(--hairline);border-radius:var(--radius-lg);background:var(--surface-raised)}.oc-task-context>header{display:flex;justify-content:space-between;gap:var(--space-2);align-items:center}.oc-task-context>header div{display:grid;gap:2px}.oc-task-context>header small,.oc-task-context>header span,.oc-task-context-empty{color:var(--text-muted)}.oc-task-context-grid{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:var(--space-2);margin-top:var(--space-3)}.oc-task-context-grid>div{display:grid;gap:4px;min-width:0;padding:var(--space-2);border:1px solid var(--hairline);border-radius:var(--radius-md)}.oc-task-context-grid span,.oc-task-context-grid small{overflow-wrap:anywhere}.oc-shortcut-palette{position:absolute;z-index:12;left:0;right:0;bottom:calc(100% + 8px);display:grid;padding:6px;border:1px solid var(--hairline);border-radius:var(--radius-lg);background:var(--surface-raised);box-shadow:var(--shadow-lg)}.oc-shortcut-palette button{display:flex;gap:var(--space-3);align-items:center;border:0;border-radius:var(--radius-md);padding:9px 10px;color:inherit;background:transparent;text-align:left}.oc-shortcut-palette button:hover,.oc-shortcut-palette button.is-active{background:var(--surface-hover)}.oc-shortcut-palette button span{display:grid;gap:2px}.oc-shortcut-palette button small{color:var(--text-muted)}.oc-composer-inner{position:relative}@media(max-width:800px){.oc-task-context-grid{grid-template-columns:1fr}}.oc-file-palette{position:absolute;z-index:13;left:0;right:0;bottom:calc(100% + 8px);display:grid;max-height:240px;overflow:auto;padding:6px;border:1px solid var(--hairline);border-radius:var(--radius-lg);background:var(--surface-raised);box-shadow:var(--shadow-lg)}.oc-file-palette button{border:0;padding:8px 10px;color:inherit;background:transparent;text-align:left;border-radius:var(--radius-md)}.oc-file-palette button:hover{background:var(--surface-hover)}.oc-runtime-controls{display:flex;flex-wrap:wrap;align-items:center;gap:8px;margin-top:8px}.oc-runtime-controls select{max-width:220px;border:1px solid var(--hairline);border-radius:var(--radius-md);padding:5px 8px;color:inherit;background:var(--surface-raised)}.oc-runtime-controls select.oc-model-select{max-width:min(100%,360px);font-family:JetBrains Mono,SFMono-Regular,Consolas,monospace;font-size:12px}.oc-runtime-controls .oc-process-summary{margin-left:0}.oc-usage{color:var(--text-muted);font-size:12px;font-variant-numeric:tabular-nums}.oc-runtime-context-menu{position:relative;margin-left:auto;flex-shrink:0}.oc-runtime-context-toggle.is-active{color:var(--orange-active);background:color-mix(in srgb,var(--orange) 10%,var(--surface))}.oc-runtime-context-panel{position:absolute;right:0;bottom:calc(100% + 8px);z-index:16;width:min(400px,calc(100vw - 32px));max-height:min(360px,45vh);display:flex;flex-direction:column;border:1px solid var(--hairline);border-radius:var(--radius-md);background:var(--surface);box-shadow:var(--shadow-soft);animation:panel-in .16s ease;overflow:hidden}.oc-runtime-context-head{display:flex;align-items:center;gap:8px;flex-shrink:0;padding:10px 12px;border-bottom:1px solid var(--hairline);background:var(--surface-raised)}.oc-runtime-context-head strong{font-size:13px}.oc-runtime-context-meta{flex:1;font-size:11px;color:var(--muted)}.oc-runtime-context-close{flex-shrink:0;min-height:28px;padding:0 8px;font-size:12px}.oc-runtime-context-body{flex:1;min-height:0;overflow:auto;padding:10px 12px 12px}.oc-runtime-context-dl{margin:0;display:grid;gap:10px;font-size:12.5px}.oc-runtime-context-dl>div{display:grid;gap:3px}.oc-runtime-context-dl dt{margin:0;font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:.04em;color:var(--muted)}.oc-runtime-context-dl dd{margin:0;line-height:1.45;color:var(--body)}.oc-runtime-context-hash{display:inline-block;margin-left:6px;font-size:11px;color:var(--muted)}.oc-runtime-context-skills{margin:4px 0 0;padding:0;list-style:none;display:grid;gap:4px}.oc-runtime-context-skills li{display:flex;align-items:baseline;justify-content:space-between;gap:8px}.oc-runtime-context-skills span{font-size:11px;color:var(--muted);font-variant-numeric:tabular-nums}.oc-runtime-context-raw{margin-top:12px;padding-top:10px;border-top:1px solid var(--hairline);font-size:12px;color:var(--muted)}.oc-runtime-context-raw summary{cursor:pointer;-webkit-user-select:none;user-select:none}.oc-runtime-context-raw pre{margin:8px 0 0;max-height:140px;overflow:auto;padding:8px;border:1px solid var(--hairline);border-radius:var(--radius-sm);background:var(--canvas-soft);font-size:11px;line-height:1.45;white-space:pre-wrap;overflow-wrap:anywhere}@media(max-width:800px){.oc-runtime-context-menu{margin-left:0;width:100%}.oc-runtime-context-panel{left:0;right:0;width:auto}}.oc-artifact-card{max-width:100%;margin:12px 0;padding:14px;overflow:hidden;border:1px solid var(--hairline);border-radius:var(--radius-lg);background:var(--surface-raised)}.oc-artifact-card header{display:flex;justify-content:space-between;gap:12px}.oc-artifact-card pre{max-height:320px;overflow:auto;white-space:pre-wrap;overflow-wrap:anywhere}.oc-artifact-card iframe{width:100%;min-height:280px;border:1px solid var(--hairline);background:#fff}.oc-load-history{display:block;margin:8px auto}.oc-attach-button{display:inline-flex;min-height:24px;align-items:center;color:var(--muted);cursor:pointer;font-size:12px}.oc-composer-toolbar{display:flex;align-items:center;flex-wrap:wrap;gap:10px 14px;margin-bottom:6px;font-size:11.5px;color:var(--muted)}.oc-composer-toolbar .oc-attach-button{margin-bottom:0}.oc-attachment-list{display:flex;flex-wrap:wrap;gap:6px;margin-bottom:6px}.oc-attachment-list span{display:inline-flex;align-items:center;gap:5px;max-width:100%;padding:4px 8px;border:1px solid var(--hairline);border-radius:999px;overflow-wrap:anywhere}.oc-attachment-list button{border:0;color:inherit;background:transparent;cursor:pointer}@media(max-width:640px){.oc-runtime-controls,.oc-composer-foot{align-items:stretch;flex-direction:column}.oc-runtime-controls select,.oc-runtime-controls button{width:100%;max-width:100%;min-height:44px}.oc-artifact-card{width:100%}.oc-composer{padding-inline:8px}}.oc-branch-actions{display:inline-flex;gap:.4rem;align-items:center;flex-wrap:wrap}.oc-branch-badge{padding:.15rem .45rem;border-radius:999px;border:1px solid currentColor;font-size:.72rem}.oc-branch-badge.is-branch{color:#f6ad55}.oc-branch-badge.is-mainline{color:#68d391}
|
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
<meta charset="UTF-8" />
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
6
|
<title>Loop 操作台 · Operator Console</title>
|
|
7
|
-
<script type="module" crossorigin src="/assets/index-
|
|
8
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
7
|
+
<script type="module" crossorigin src="/assets/index-DePjoS_G.js"></script>
|
|
8
|
+
<link rel="stylesheet" crossorigin href="/assets/index-Dp_2sKGk.css">
|
|
9
9
|
</head>
|
|
10
10
|
<body>
|
|
11
11
|
<div id="root"></div>
|
|
@@ -8,12 +8,12 @@ import { parseWorkerEventLine } from "../observability/events.js";
|
|
|
8
8
|
import { isSafeObservabilityIdentifier } from "../observability/event-store.js";
|
|
9
9
|
import { clampEventHistoryLimit, listBatchEventHistory, listPoolEventHistory, } from "../observability/event-history.js";
|
|
10
10
|
import { buildGlobalSnapshot, clampTaskRunHistoryLimit, listTaskRunHistory, resolveLegacyTask, } from "../observability/read-model.js";
|
|
11
|
+
import { resolveDagTaskSourcePath } from "../../task/dag-source-paths.js";
|
|
11
12
|
import { dagSourceBindingSchema } from "../../workflows/dag/types.js";
|
|
12
13
|
import { getTaskPoolRoot } from "../pool/run-store.js";
|
|
13
14
|
import { isAllowedArtifactTextPath, resolveArtifactPath, resolveRepoFilePreview, toRepoRelativeArtifactPath, } from "./paths.js";
|
|
14
15
|
import { extractSpecEvidence, extractSpecReadContent, } from "./spec-evidence.js";
|
|
15
16
|
import { buildObserveHealthV1 } from "./health.js";
|
|
16
|
-
import { getTaskDir } from "../../task/runtime.js";
|
|
17
17
|
const ARTIFACT_PREVIEW_MAX_BYTES = 64 * 1024;
|
|
18
18
|
export function createObserveSnapshotCache() {
|
|
19
19
|
return { expiresAt: 0 };
|
|
@@ -805,15 +805,19 @@ async function handleDagNodeSpecEvidenceFile(_req, res, match, ctx) {
|
|
|
805
805
|
sendJson(res, 404, { error: "Path is not a bound source" });
|
|
806
806
|
return;
|
|
807
807
|
}
|
|
808
|
-
// sourceBinding.sources[].path is task-root-relative (.harness/tasks/<taskId>).
|
|
809
808
|
if (!isSafeObservabilityIdentifier(sourceBinding.taskId)) {
|
|
810
809
|
sendJson(res, 400, { error: "Invalid or unsafe path" });
|
|
811
810
|
return;
|
|
812
811
|
}
|
|
813
|
-
const taskRoot = getTaskDir(ctx.repoRoot, sourceBinding.taskId);
|
|
814
812
|
let resolved;
|
|
815
813
|
try {
|
|
816
|
-
|
|
814
|
+
const sourcePath = resolveDagTaskSourcePath({
|
|
815
|
+
workspaceRoot: ctx.repoRoot,
|
|
816
|
+
taskId: sourceBinding.taskId,
|
|
817
|
+
sourcePath: member.path,
|
|
818
|
+
});
|
|
819
|
+
const repoRelativePath = path.relative(ctx.repoRoot, sourcePath);
|
|
820
|
+
resolved = await resolveRepoFilePreview(ctx.repoRoot, repoRelativePath);
|
|
817
821
|
}
|
|
818
822
|
catch {
|
|
819
823
|
sendJson(res, 400, { error: "Invalid or unsafe path" });
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import { access } from "node:fs/promises";
|
|
3
3
|
import path from "node:path";
|
|
4
|
+
import { serializeDagTaskSourcePath } from "../../task/dag-source-paths.js";
|
|
4
5
|
const MAX_SOURCE_EXCERPT_CHARS = 2000;
|
|
5
6
|
export const BACKEND_TEST_DISCOVERY_EXCLUDED_DIRECTORIES = [
|
|
6
7
|
".git",
|
|
@@ -49,15 +50,12 @@ const DISCOVERY_CANDIDATES = {
|
|
|
49
50
|
function taskRelative(sources, filePath) {
|
|
50
51
|
return path.relative(sources.taskDir, filePath).replaceAll(path.sep, "/");
|
|
51
52
|
}
|
|
52
|
-
function
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
throw new Error(`backend-test reference escapes repository root: ${filePath}`);
|
|
59
|
-
}
|
|
60
|
-
return relative.replaceAll(path.sep, "/");
|
|
53
|
+
function sourceReadPath(sources, filePath) {
|
|
54
|
+
return serializeDagTaskSourcePath({
|
|
55
|
+
repoRoot: sources.repoRoot,
|
|
56
|
+
taskDir: sources.taskDir,
|
|
57
|
+
absolutePath: filePath,
|
|
58
|
+
});
|
|
61
59
|
}
|
|
62
60
|
function boundedExcerpt(markdown, sourcePath) {
|
|
63
61
|
const trimmed = markdown.trim();
|
|
@@ -120,8 +118,8 @@ async function discoverCandidatePaths(repoRoot) {
|
|
|
120
118
|
};
|
|
121
119
|
}
|
|
122
120
|
export async function buildBackendTestIntakeContext(sources) {
|
|
123
|
-
const requirementPath =
|
|
124
|
-
const constraintPath =
|
|
121
|
+
const requirementPath = sourceReadPath(sources, sources.requirementPath);
|
|
122
|
+
const constraintPath = sourceReadPath(sources, sources.constraintPath);
|
|
125
123
|
const parts = [
|
|
126
124
|
"## Derived task contract: 需求.md",
|
|
127
125
|
boundedExcerpt(sources.requirementMarkdown, requirementPath),
|
|
@@ -134,7 +132,7 @@ export async function buildBackendTestIntakeContext(sources) {
|
|
|
134
132
|
const relativePath = taskRelative(sources, reference.path);
|
|
135
133
|
return {
|
|
136
134
|
path: relativePath,
|
|
137
|
-
readPath:
|
|
135
|
+
readPath: sourceReadPath(sources, reference.path),
|
|
138
136
|
sha256: createHash("sha256").update(reference.markdown, "utf8").digest("hex"),
|
|
139
137
|
roleHints: inferRoleHints(relativePath),
|
|
140
138
|
};
|
|
@@ -727,11 +727,21 @@ function escapeHtml(value) {
|
|
|
727
727
|
function reportOutputLines(value, prefix) {
|
|
728
728
|
if (!value?.trim())
|
|
729
729
|
return [];
|
|
730
|
+
// Accept both the underscore form ("HTTP_REQUEST") and the space-separated
|
|
731
|
+
// form ("HTTP REQUEST") that some generated pytest helpers emit. Keep the
|
|
732
|
+
// original line text so downstream parsers can read the kv payload.
|
|
733
|
+
const underscorePrefix = prefix;
|
|
734
|
+
const spacePrefix = prefix.replaceAll("_", " ");
|
|
730
735
|
return value
|
|
731
736
|
.split(/\r?\n/)
|
|
732
737
|
.map((line) => line.trim())
|
|
733
|
-
.filter((line) => line.startsWith(
|
|
734
|
-
.map((line) =>
|
|
738
|
+
.filter((line) => line.startsWith(underscorePrefix) || line.startsWith(spacePrefix))
|
|
739
|
+
.map((line) => {
|
|
740
|
+
const trimmed = line.startsWith(underscorePrefix)
|
|
741
|
+
? line.slice(underscorePrefix.length)
|
|
742
|
+
: line.slice(spacePrefix.length);
|
|
743
|
+
return redactBackendTestOutput(trimmed.trim()).slice(0, 4000);
|
|
744
|
+
});
|
|
735
745
|
}
|
|
736
746
|
function requestResponseSummary(result) {
|
|
737
747
|
const combined = [result.stdout, result.stderr].filter(Boolean).join("\n");
|
|
@@ -740,6 +750,101 @@ function requestResponseSummary(result) {
|
|
|
740
750
|
responses: reportOutputLines(combined, "HTTP_RESPONSE"),
|
|
741
751
|
};
|
|
742
752
|
}
|
|
753
|
+
const HTTP_STATUS_TEXT = {
|
|
754
|
+
200: "OK",
|
|
755
|
+
201: "Created",
|
|
756
|
+
204: "No Content",
|
|
757
|
+
400: "Bad Request",
|
|
758
|
+
401: "Unauthorized",
|
|
759
|
+
403: "Forbidden",
|
|
760
|
+
404: "Not Found",
|
|
761
|
+
409: "Conflict",
|
|
762
|
+
500: "Internal Server Error",
|
|
763
|
+
};
|
|
764
|
+
/**
|
|
765
|
+
* Parse the kv payload of an HTTP log line into a structured entry.
|
|
766
|
+
*
|
|
767
|
+
* Accepts two shapes emitted by generated pytest helpers:
|
|
768
|
+
* - legacy: a bare JSON object ({"method":"POST",...})
|
|
769
|
+
* - current: kv pairs (method=POST url=... parameters={...} / status_code=201 result={...})
|
|
770
|
+
*/
|
|
771
|
+
function parseHttpLogPayload(line, kind) {
|
|
772
|
+
const trimmed = line.trim();
|
|
773
|
+
if (!trimmed)
|
|
774
|
+
return {};
|
|
775
|
+
let kv = {};
|
|
776
|
+
// Try JSON first (legacy HTTP_REQUEST {"method":...} shape).
|
|
777
|
+
if (trimmed.startsWith("{")) {
|
|
778
|
+
try {
|
|
779
|
+
const parsed = JSON.parse(trimmed);
|
|
780
|
+
for (const [key, value] of Object.entries(parsed)) {
|
|
781
|
+
kv[key.toLowerCase()] = typeof value === "string" ? value : safeStringify(value);
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
catch {
|
|
785
|
+
// Fall through to kv extraction below.
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
if (Object.keys(kv).length === 0) {
|
|
789
|
+
kv = extractKvPairs(trimmed);
|
|
790
|
+
}
|
|
791
|
+
if (kind === "request") {
|
|
792
|
+
return {
|
|
793
|
+
method: kv.method,
|
|
794
|
+
url: kv.url,
|
|
795
|
+
body: kv.parameters ?? kv.body ?? kv.json ?? kv.data ?? kv.query ?? kv.payload,
|
|
796
|
+
};
|
|
797
|
+
}
|
|
798
|
+
const status = kv.status_code ?? kv.status;
|
|
799
|
+
return {
|
|
800
|
+
method: kv.method,
|
|
801
|
+
url: kv.url,
|
|
802
|
+
status,
|
|
803
|
+
statusText: kv.status_text ?? (status ? HTTP_STATUS_TEXT[status] : undefined),
|
|
804
|
+
body: kv.result ?? kv.body ?? kv.text,
|
|
805
|
+
};
|
|
806
|
+
}
|
|
807
|
+
function extractKvPairs(value) {
|
|
808
|
+
const result = {};
|
|
809
|
+
// Split on top-level spaces but keep {...}/[...] payloads intact.
|
|
810
|
+
let depth = 0;
|
|
811
|
+
let buffer = "";
|
|
812
|
+
const flush = () => {
|
|
813
|
+
const segment = buffer.trim();
|
|
814
|
+
buffer = "";
|
|
815
|
+
if (!segment)
|
|
816
|
+
return;
|
|
817
|
+
const eq = segment.indexOf("=");
|
|
818
|
+
if (eq <= 0)
|
|
819
|
+
return;
|
|
820
|
+
const key = segment.slice(0, eq).trim();
|
|
821
|
+
const val = segment.slice(eq + 1).trim();
|
|
822
|
+
if (key)
|
|
823
|
+
result[key] = val;
|
|
824
|
+
};
|
|
825
|
+
for (const ch of value) {
|
|
826
|
+
if (ch === "{" || ch === "[")
|
|
827
|
+
depth += 1;
|
|
828
|
+
else if (ch === "}" || ch === "]")
|
|
829
|
+
depth = Math.max(0, depth - 1);
|
|
830
|
+
if (ch === " " && depth === 0) {
|
|
831
|
+
flush();
|
|
832
|
+
}
|
|
833
|
+
else {
|
|
834
|
+
buffer += ch;
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
flush();
|
|
838
|
+
return result;
|
|
839
|
+
}
|
|
840
|
+
function safeStringify(value) {
|
|
841
|
+
try {
|
|
842
|
+
return JSON.stringify(value, null, 2);
|
|
843
|
+
}
|
|
844
|
+
catch {
|
|
845
|
+
return String(value);
|
|
846
|
+
}
|
|
847
|
+
}
|
|
743
848
|
function reportDetails(title, values, emptyText) {
|
|
744
849
|
const body = values.length > 0
|
|
745
850
|
? values.map((value) => `<pre>${escapeHtml(value)}</pre>`).join("")
|
|
@@ -777,6 +882,96 @@ function conclusionIcon(failed) {
|
|
|
777
882
|
function reportDetailSection(title, content, open = false) {
|
|
778
883
|
return `<details class="report-detail"${open ? " open" : ""}><summary>${escapeHtml(title)}</summary><div class="report-detail-body">${content}</div></details>`;
|
|
779
884
|
}
|
|
885
|
+
/** Render a pretty "prettified" body block: if it looks like JSON/kv, reformat; otherwise show raw. */
|
|
886
|
+
function renderLogBody(raw, emptyText) {
|
|
887
|
+
const text = (raw ?? "").trim();
|
|
888
|
+
if (!text || text === "{}" || text === "null") {
|
|
889
|
+
return `<pre style="white-space:pre-wrap;background:#f6f8fa;color:#1f2937;padding:10px 12px;border-radius:8px;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:0.78rem;margin-top:6px;border:1px solid #eceef2;line-height:1.6">${escapeHtml(emptyText)}</pre>`;
|
|
890
|
+
}
|
|
891
|
+
let pretty = text;
|
|
892
|
+
// Best-effort: pretty-print JSON payloads.
|
|
893
|
+
if (text.startsWith("{") || text.startsWith("[")) {
|
|
894
|
+
try {
|
|
895
|
+
pretty = JSON.stringify(JSON.parse(text), null, 2);
|
|
896
|
+
}
|
|
897
|
+
catch {
|
|
898
|
+
// Keep original kv text.
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
else if (text.includes(":") || text.includes("=")) {
|
|
902
|
+
// kv-style "key: value" or "key=value" → one per line.
|
|
903
|
+
pretty = text
|
|
904
|
+
.replace(/\s+(?=[A-Za-z_]+[:=])/g, "\n")
|
|
905
|
+
.trim();
|
|
906
|
+
}
|
|
907
|
+
return `<pre style="white-space:pre-wrap;background:#f6f8fa;color:#1f2937;padding:10px 12px;border-radius:8px;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:0.78rem;margin-top:6px;border:1px solid #eceef2;line-height:1.6">${escapeHtml(pretty)}</pre>`;
|
|
908
|
+
}
|
|
909
|
+
function statusColor(status) {
|
|
910
|
+
if (status === "passed")
|
|
911
|
+
return { fg: "#067647", bg: "#ecfdf3" };
|
|
912
|
+
if (status === "skipped")
|
|
913
|
+
return { fg: "#9a6700", bg: "#fffaeb" };
|
|
914
|
+
return { fg: "#b42318", bg: "#fef3f2" };
|
|
915
|
+
}
|
|
916
|
+
function methodBadge(method) {
|
|
917
|
+
const m = (method ?? "?").toUpperCase();
|
|
918
|
+
const bg = m === "GET" ? "#2563eb" : m === "POST" ? "#16a34a" : m === "DELETE" ? "#b42318" : m === "PUT" || m === "PATCH" ? "#d97706" : "#475467";
|
|
919
|
+
return `<span style="display:inline-block;padding:2px 8px;border-radius:5px;background:${bg};color:#fff;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:0.72rem;font-weight:700">${escapeHtml(m)}</span>`;
|
|
920
|
+
}
|
|
921
|
+
function statusPill(status, fallback) {
|
|
922
|
+
const code = Number(status);
|
|
923
|
+
if (!Number.isFinite(code) || code === 0) {
|
|
924
|
+
return `<span style="color:#98a2b3;font-size:0.74rem">${escapeHtml(fallback)}</span>`;
|
|
925
|
+
}
|
|
926
|
+
const color = code >= 200 && code < 300 ? { fg: "#067647", bg: "#ecfdf3" } : code >= 400 ? { fg: "#b42318", bg: "#fef3f2" } : { fg: "#9a6700", bg: "#fffaeb" };
|
|
927
|
+
return `<span style="display:inline-block;padding:2px 9px;border-radius:999px;font-weight:700;font-size:0.72rem;color:${color.fg};background:${color.bg}">${code}</span>`;
|
|
928
|
+
}
|
|
929
|
+
/** Pair request lines with their matching response lines (by method+url or order). */
|
|
930
|
+
function pairHttpCalls(requests, responses) {
|
|
931
|
+
const parsedRequests = requests.map((line) => parseHttpLogPayload(line, "request"));
|
|
932
|
+
const parsedResponses = responses.map((line) => parseHttpLogPayload(line, "response"));
|
|
933
|
+
const used = new Set();
|
|
934
|
+
return parsedRequests.map((request) => {
|
|
935
|
+
let matchIndex = -1;
|
|
936
|
+
for (let i = 0; i < parsedResponses.length; i += 1) {
|
|
937
|
+
if (used.has(i))
|
|
938
|
+
continue;
|
|
939
|
+
const candidate = parsedResponses[i];
|
|
940
|
+
if (candidate.method && request.method && candidate.method.toUpperCase() === request.method.toUpperCase()
|
|
941
|
+
&& candidate.url && request.url && candidate.url === request.url) {
|
|
942
|
+
matchIndex = i;
|
|
943
|
+
break;
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
// Fallback: first unused response in order.
|
|
947
|
+
if (matchIndex < 0) {
|
|
948
|
+
for (let i = 0; i < parsedResponses.length; i += 1) {
|
|
949
|
+
if (!used.has(i)) {
|
|
950
|
+
matchIndex = i;
|
|
951
|
+
break;
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
if (matchIndex >= 0) {
|
|
956
|
+
used.add(matchIndex);
|
|
957
|
+
return { request, response: parsedResponses[matchIndex] };
|
|
958
|
+
}
|
|
959
|
+
return { request };
|
|
960
|
+
});
|
|
961
|
+
}
|
|
962
|
+
function renderHttpCallCard(call) {
|
|
963
|
+
// method/url come from the request line; status/result come from the response.
|
|
964
|
+
// Some log formats (HTTP RESPONSE status=... result=...) omit method/url on
|
|
965
|
+
// the response line, so prefer the request for those fields.
|
|
966
|
+
const method = call.request.method ?? call.response?.method;
|
|
967
|
+
const url = call.request.url ?? call.response?.url ?? "未知 URL";
|
|
968
|
+
const header = `<div style="display:flex;align-items:center;gap:10px;flex-wrap:wrap">${methodBadge(method)}<code style="font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:0.78rem;color:#344054">${escapeHtml(url)}</code><span style="flex:1"></span>${call.response ? statusPill(call.response.status, call.response.statusText ?? "") : "<span style=\"color:#98a2b3;font-size:0.74rem\">无响应</span>"}${call.response?.statusText ? `<span style="color:#98a2b3;font-size:0.74rem">${escapeHtml(call.response.statusText)}</span>` : ""}</div>`;
|
|
969
|
+
const requestBlock = `<div style="color:#667085;font-size:0.74rem;font-weight:600;margin-bottom:3px;margin-top:10px">请求参数 / 请求体</div>${renderLogBody(call.request.body, "无")}`;
|
|
970
|
+
const responseBlock = call.response
|
|
971
|
+
? `<div style="color:#667085;font-size:0.74rem;font-weight:600;margin-bottom:3px;margin-top:10px">响应结果</div>${renderLogBody(call.response.body, "无")}`
|
|
972
|
+
: "";
|
|
973
|
+
return `<div style="border:1px solid #eef0f3;border-radius:10px;padding:12px 14px;margin-top:10px;background:#fcfdff">${header}${requestBlock}${responseBlock}</div>`;
|
|
974
|
+
}
|
|
780
975
|
export function renderBackendTestHtml(input) {
|
|
781
976
|
const failed = input.parsed.failed + input.parsed.errors > 0;
|
|
782
977
|
const passRate = input.parsed.tests > 0 ? input.parsed.passed / input.parsed.tests : 0;
|
|
@@ -784,53 +979,39 @@ export function renderBackendTestHtml(input) {
|
|
|
784
979
|
const caseValidation = parseAdvisorySummary(input.caseValidationSummary);
|
|
785
980
|
const traceability = parseAdvisorySummary(input.traceabilitySummary);
|
|
786
981
|
const failures = input.parsed.cases.filter((result) => result.status !== "passed");
|
|
787
|
-
const
|
|
788
|
-
|
|
789
|
-
const item = catalog.get(caseId);
|
|
790
|
-
const io = requestResponseSummary(result);
|
|
791
|
-
const failureDetails = result.status === "passed"
|
|
792
|
-
? ""
|
|
793
|
-
: `<div class="failure-detail"><strong>${escapeHtml(result.message || humanStatus(result.status))}</strong>${result.details ? reportDetailSection("失败详情", `<pre>${escapeHtml(result.details)}</pre>`) : ""}</div>`;
|
|
794
|
-
return `<article class="case-card result-${result.status}"><div class="case-head"><div><span class="case-id">${escapeHtml(caseId)}</span><h3>${escapeHtml(item?.title ?? result.name)}</h3><p>${escapeHtml(item?.scenario ?? "未从 Markdown 用例提取场景说明。")}</p></div><div class="case-outcome"><span class="status ${result.status}">${humanStatus(result.status)}</span><span class="duration">${formatDuration(result.durationMs)}</span></div></div><div class="case-meta"><span>自动化用例</span><code>${escapeHtml(result.name)}</code></div><div class="io-grid">${reportDetails("接口请求参数", io.requests, "本次 JUnit 未捕获请求日志。")}${reportDetails("接口响应结果", io.responses, "本次 JUnit 未捕获响应日志。")}</div>${failureDetails}</article>`;
|
|
795
|
-
}).join("");
|
|
796
|
-
const failureOverview = failures.length > 0
|
|
797
|
-
? `<div class="failure-list">${failures.map((result) => {
|
|
798
|
-
const caseId = junitCaseId(result.name) ?? "未关联";
|
|
799
|
-
return `<article><div>${advisoryBadge(result.status === "skipped" ? "Unavailable" : "FAIL")}<strong>${escapeHtml(caseId)} · ${escapeHtml(catalog.get(caseId)?.title ?? result.name)}</strong></div><p>${escapeHtml(result.message || humanStatus(result.status))}</p></article>`;
|
|
800
|
-
}).join("")}</div>`
|
|
801
|
-
: '<div class="empty-state">本轮没有失败、错误或跳过用例。</div>';
|
|
802
|
-
const conclusion = failed
|
|
803
|
-
? `本轮测试未通过:${input.parsed.failed + input.parsed.errors} 条用例失败或错误。`
|
|
804
|
-
: `本轮测试通过:${input.parsed.passed} 条用例执行成功。`;
|
|
805
|
-
const executionOverview = `<div class="metric-grid"><div><span>用例总数</span><strong>${input.parsed.tests}</strong></div><div><span>通过</span><strong>${input.parsed.passed}</strong></div><div><span>失败 / 错误</span><strong>${input.parsed.failed} / ${input.parsed.errors}</strong></div><div><span>跳过</span><strong>${input.parsed.skipped}</strong></div><div><span>通过率</span><strong>${(passRate * 100).toFixed(2)}%</strong></div><div><span>总耗时</span><strong>${formatDuration(input.parsed.durationMs)}</strong></div></div>`;
|
|
806
|
-
const qualityDetails = `<div class="quality-grid"><div class="quality-card"><header><strong>Markdown 用例校验</strong>${advisoryBadge(caseValidation.status)}</header><p>Findings:${caseValidation.findings ?? "不可用"}</p><p>${escapeHtml(caseValidation.firstFinding)}</p></div><div class="quality-card"><header><strong>Markdown → pytest 追溯</strong>${advisoryBadge(traceability.status)}</header><p>Findings:${traceability.findings ?? "不可用"}</p><p>${escapeHtml(traceability.firstFinding)}</p></div></div><div class="evidence-line"><span>执行环境</span>${escapeHtml(input.environmentSummary || "不可用")}</div>`;
|
|
807
|
-
return `<!doctype html><html lang="zh-CN"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>${escapeHtml(input.title)}</title><style>:root{color-scheme:light;--ink:#182230;--muted:#667085;--line:#dce3ec;--panel:#f6f8fb;--brand:#163a63;--accent:#2e6da4;--pass:#067647;--pass-bg:#ecfdf3;--fail:#b42318;--fail-bg:#fef3f2;--skip:#9a6700;--skip-bg:#fffaeb;--shadow:0 18px 45px rgba(28,55,90,.09)}*{box-sizing:border-box}body{margin:0;background:radial-gradient(circle at top left,#dcecff 0,transparent 32rem),linear-gradient(180deg,#edf3fa 0,#f8fafc 34rem);color:var(--ink);font-family:Inter,system-ui,"Microsoft YaHei","PingFang SC",sans-serif;line-height:1.65}header,main{max-width:1280px;margin:auto}header{padding:3.5rem 2rem 1.75rem}main{padding:0 2rem 4rem}h1{margin:.3rem 0;color:var(--brand);font-size:clamp(2rem,4vw,3rem);letter-spacing:-.03em}h2{margin:0 0 1rem;color:var(--brand)}h3{margin:.35rem 0 .15rem;font-size:1.12rem}.eyebrow{color:var(--accent);font-weight:850;letter-spacing:.12em}.subtitle,.muted,.case-head p{color:var(--muted)}section,.report-detail{background:rgba(255,255,255,.94);border:1px solid rgba(220,227,236,.95);border-radius:1.15rem;padding:1.5rem;margin-top:1rem;box-shadow:var(--shadow)}.conclusion{display:flex;align-items:center;gap:1rem;padding:1rem 1.25rem;border-left:5px solid ${failed ? "var(--fail)" : "var(--pass)"};border-radius:.8rem;background:${failed ? "var(--fail-bg)" : "var(--pass-bg)"};color:${failed ? "var(--fail)" : "var(--pass)"}}.conclusion-icon{width:4rem;height:4rem;flex:0 0 4rem;display:block;overflow:visible}.conclusion-icon circle:first-child{fill:currentColor;fill-opacity:.12;stroke:currentColor;stroke-width:2}.conclusion-icon path{fill:none;stroke:currentColor;stroke-width:3.2;stroke-linecap:round;stroke-linejoin:round}.conclusion-icon .icon-dot{fill:currentColor;stroke:none}.conclusion-copy{min-width:0}.conclusion-copy strong{display:block;font-size:1.05rem}.conclusion-copy span{display:block;font-size:.9rem;font-weight:400}.metric-grid{display:grid;grid-template-columns:repeat(6,minmax(7rem,1fr));gap:.75rem;margin-top:1rem}.metric-grid>div{padding:1rem;border:1px solid var(--line);border-radius:.85rem;background:white}.metric-grid span,.metric-grid small{display:block;color:var(--muted);font-size:.88rem}.metric-grid strong{display:block;font-size:1.45rem;font-weight:700}.quality-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:1rem}.quality-card{padding:1rem;border:1px solid var(--line);border-radius:.85rem;background:white}.quality-card header{display:flex;align-items:center;justify-content:space-between;padding:0;margin:0 0 .5rem}.quality-card p{margin:.25rem 0;color:var(--muted)}.status{display:inline-flex;align-items:center;padding:.18rem .62rem;border-radius:999px;font-weight:800;font-size:.86rem}.status.passed{color:var(--pass);background:var(--pass-bg)}.status.failure,.status.error{color:var(--fail);background:var(--fail-bg)}.status.skipped{color:var(--skip);background:var(--skip-bg)}.failure-list{display:grid;gap:.75rem}.failure-list article{border-left:4px solid var(--fail);background:var(--fail-bg);padding:.85rem 1rem;border-radius:.65rem}.empty-state{padding:1rem;border:1px dashed var(--line);border-radius:.75rem;color:var(--muted);background:var(--panel)}.case-list{display:grid;gap:1rem}.case-card{border:1px solid var(--line);border-radius:1rem;padding:1.15rem;background:linear-gradient(145deg,#fff,#fbfcfe);box-shadow:0 8px 24px rgba(28,55,90,.06)}.case-card.result-failure,.case-card.result-error{border-left:5px solid var(--fail)}.case-card.result-passed{border-left:5px solid var(--pass)}.case-head{display:flex;justify-content:space-between;gap:1rem;align-items:flex-start}.case-head p{margin:.2rem 0}.case-id{display:inline-block;color:var(--accent);font-weight:850;letter-spacing:.04em}.case-outcome{display:flex;align-items:center;gap:.75rem;white-space:nowrap}.duration{color:var(--muted);font-variant-numeric:tabular-nums}.case-meta{display:flex;gap:.75rem;align-items:center;margin:.85rem 0;padding:.65rem .8rem;border-radius:.7rem;background:var(--panel);color:var(--muted)}code{color:var(--brand);font-weight:700}.io-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:.8rem}.io-details{border:1px solid var(--line);border-radius:.75rem;padding:.7rem;background:white}.io-details summary,.report-detail summary{cursor:pointer;color:var(--brand);font-weight:800}.io-details summary span{float:right;color:var(--muted)}pre{white-space:pre-wrap;overflow-wrap:anywhere;max-height:18rem;overflow:auto;background:#101828;color:#eef4ff;padding:.85rem;border-radius:.6rem;font-size:.82rem}.failure-detail{margin-top:.8rem;padding:.8rem;border-radius:.7rem;background:var(--fail-bg);color:var(--fail)}.report-detail{padding:0;overflow:hidden}.report-detail summary{list-style:none;padding:1rem 1.25rem}.report-detail summary::-webkit-details-marker{display:none}.report-detail summary::after{content:"+";float:right;font-size:1.2rem;font-weight:400;color:var(--muted)}.report-detail[open] summary::after{content:"−"}.report-detail-body{border-top:1px solid var(--line);padding:1.25rem}.evidence-line{display:flex;gap:.75rem;align-items:baseline;padding:.7rem .8rem;background:var(--panel);border-radius:.65rem;font-size:.9rem}.evidence-line span{color:var(--muted);min-width:8rem}.subtitle{margin:.2rem 0 0}@media(max-width:900px){.metric-grid{grid-template-columns:repeat(2,1fr)}.quality-grid,.io-grid{grid-template-columns:1fr}.case-head{display:block}.case-outcome{margin-top:.65rem}header,main{padding-left:1rem;padding-right:1rem}}@media(max-width:420px){.conclusion{align-items:flex-start}.conclusion-icon{width:3rem;height:3rem;flex-basis:3rem}.metric-grid{grid-template-columns:1fr 1fr}}</style></head><body><header><div class="eyebrow">后端自动化测试报告</div><h1>${escapeHtml(input.title)}</h1><p class="subtitle">测试结论、质量状态与每条接口用例的请求/响应事实均来自同一次 pytest 执行。</p></header><main><section><h2>当前结论</h2><div class="conclusion">${conclusionIcon(failed)}<div class="conclusion-copy"><strong>${escapeHtml(failed ? "测试未通过" : "测试通过")}</strong><span>${escapeHtml(conclusion)}</span></div></div>${executionOverview}</section>${reportDetailSection("质量校验", qualityDetails, true)}${reportDetailSection("失败概览", failureOverview, failures.length > 0)}${reportDetailSection("用例执行结果", `<div class="case-list">${rows || '<div class="empty-state">未发现可展示的 JUnit testcase。</div>'}</div>`, true)}</main></body></html>`;
|
|
808
|
-
}
|
|
809
|
-
function renderBackendTestHtmlLegacy(input) {
|
|
810
|
-
const passRate = input.parsed.tests > 0 ? input.parsed.passed / input.parsed.tests : 0;
|
|
811
|
-
const catalog = new Map((input.cases ?? []).map((item) => [item.id, item]));
|
|
812
|
-
const rows = input.parsed.cases.map((result) => {
|
|
982
|
+
const headColor = failed ? { fg: "#b42318", bg: "linear-gradient(135deg,#fef3f2 0,#fee4e2 100%)", border: "#fda29b", icon: "✗" } : { fg: "#067647", bg: "linear-gradient(135deg,#ecfdf3 0,#d1fadf 100%)", border: "#abefc6", icon: "✓" };
|
|
983
|
+
const caseCards = input.parsed.cases.map((result) => {
|
|
813
984
|
const caseId = junitCaseId(result.name) ?? "未关联";
|
|
814
985
|
const item = catalog.get(caseId);
|
|
815
986
|
const io = requestResponseSummary(result);
|
|
816
|
-
const
|
|
987
|
+
const calls = pairHttpCalls(io.requests, io.responses);
|
|
988
|
+
const statusColors = statusColor(result.status);
|
|
989
|
+
const logSection = calls.length > 0
|
|
990
|
+
? `<div style="margin-top:12px;display:flex;align-items:baseline;gap:8px"><span style="color:#17365d;font-size:0.9rem;font-weight:700">执行日志</span><span style="color:#98a2b3;font-size:0.76rem">共 ${calls.length} 次接口调用</span></div>${calls.map(renderHttpCallCard).join("")}`
|
|
991
|
+
: "";
|
|
992
|
+
const failureBlock = result.status === "passed"
|
|
817
993
|
? ""
|
|
818
|
-
: `<div
|
|
819
|
-
|
|
994
|
+
: `<div style="margin-top:12px;padding:10px 12px;border-radius:8px;background:#fef3f2;border:1px solid #fda29b;color:#b42318;font-size:0.84rem"><strong>${escapeHtml(result.message || humanStatus(result.status))}</strong>${result.details ? `<details style="margin-top:6px"><summary style="cursor:pointer;color:#b42318;font-size:0.78rem">失败详情</summary><pre style="white-space:pre-wrap;background:#f6f8fa;color:#1f2937;padding:10px 12px;border-radius:8px;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:0.78rem;margin-top:6px;border:1px solid #eceef2;line-height:1.6">${escapeHtml(result.details)}</pre></details>` : ""}</div>`;
|
|
995
|
+
const scriptPath = item?.scriptPath ?? inferredScriptPath(result.classname);
|
|
996
|
+
return `<details style="background:#fff;border:1px solid #e6eaf0;border-radius:12px;margin-top:10px"><summary style="display:flex;align-items:center;gap:12px;padding:12px 16px;cursor:pointer;list-style:none"><span style="background:#e8edf5;color:#2b3a55;padding:2px 9px;border-radius:6px;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:0.76rem;font-weight:600;white-space:nowrap">${escapeHtml(caseId)}</span><div style="flex:1;min-width:0"><div style="font-weight:600;color:#172033;font-size:0.92rem">${escapeHtml(item?.title ?? result.name)}</div><code style="font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:0.74rem;color:#98a2b3">${escapeHtml(result.name)}</code></div><span style="color:#98a2b3;font-size:0.8rem;white-space:nowrap">${formatDuration(result.durationMs)}</span><span style="display:inline-block;padding:2px 10px;border-radius:999px;font-weight:700;font-size:0.72rem;color:${statusColors.fg};background:${statusColors.bg}">${humanStatus(result.status)}</span><span style="color:#aab2bf;font-size:0.8rem">▾</span></summary><div style="padding:4px 16px 14px 16px;border-top:1px solid #f0f3f7"><div style="margin-top:10px"><div style="color:#667085;font-size:0.74rem;font-weight:600;margin-bottom:3px">自动化脚本</div><code style="background:#eef2f7;color:#344054;padding:1px 6px;border-radius:5px;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:0.8rem">${escapeHtml(scriptPath)}</code></div>${item?.scenario ? `<div style="margin-top:12px"><div style="color:#667085;font-size:0.74rem;font-weight:600;margin-bottom:3px">验收场景</div><div style="color:#475467;font-size:0.86rem;line-height:1.75">${escapeHtml(item.scenario)}</div></div>` : ""}${logSection}${failureBlock}</div></details>`;
|
|
820
997
|
}).join("");
|
|
821
|
-
const
|
|
822
|
-
const
|
|
823
|
-
const
|
|
998
|
+
const metricCard = (label, value, valueColor = "#172033") => `<div style="padding:14px 16px;border:1px solid #e3e8ef;border-radius:12px;background:#fbfcfe"><span style="color:#667085;font-size:0.78rem">${escapeHtml(label)}</span><div style="font-size:22px;font-weight:700;color:${valueColor}">${value}</div></div>`;
|
|
999
|
+
const metrics = `<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(130px,1fr));gap:12px;margin-top:14px">${metricCard("用例总数", input.parsed.tests)}${metricCard("通过", input.parsed.passed, "#067647")}${metricCard("失败", input.parsed.failed)}${metricCard("错误", input.parsed.errors)}${metricCard("跳过", input.parsed.skipped)}<div style="padding:14px 16px;border:1px solid #e3e8ef;border-radius:12px;background:#fbfcfe"><span style="color:#667085;font-size:0.78rem">通过率</span><div style="font-size:22px;font-weight:700;color:#067647">${(passRate * 100).toFixed(2)}%</div><span style="color:#667085;font-size:0.78rem">耗时 ${formatDuration(input.parsed.durationMs)}</span></div></div>`;
|
|
1000
|
+
const summaryBanner = `<div style="display:flex;align-items:center;gap:16px;padding:16px 18px;border-radius:14px;background:${headColor.bg};border:1px solid ${headColor.border}"><div style="flex:0 0 auto;width:44px;height:44px;border-radius:999px;background:${headColor.fg};display:flex;align-items:center;justify-content:center;color:#fff;font-size:24px;font-weight:900;box-shadow:0 4px 12px ${headColor.fg}4d">${headColor.icon}</div><div><div style="color:${headColor.fg};font-size:1.05rem;font-weight:800;line-height:1.3">${failed ? "本轮测试未通过" : "本轮测试通过"}</div><div style="color:${headColor.fg};font-size:0.88rem;margin-top:2px">${failed ? `${input.parsed.failed + input.parsed.errors} 条用例失败或错误` : `${input.parsed.passed} 条用例全部执行成功`}</div></div></div>`;
|
|
1001
|
+
const qualityBlock = `<div style="margin-top:18px"><div style="color:#17365d;font-size:16px;font-weight:700;margin:0 4px 6px">质量校验</div><div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(240px,1fr));gap:12px;margin-top:8px">${renderQualityCard("Markdown 用例校验", caseValidation)}${renderQualityCard("Markdown → pytest 追溯", traceability)}</div></div>`;
|
|
824
1002
|
const failureOverview = failures.length > 0
|
|
825
|
-
? `<div
|
|
1003
|
+
? `<div style="margin-top:18px"><div style="color:#17365d;font-size:16px;font-weight:700;margin:0 4px 6px">失败概览</div><div style="display:grid;gap:10px;margin-top:8px">${failures.map((result) => {
|
|
826
1004
|
const caseId = junitCaseId(result.name) ?? "未关联";
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
1005
|
+
const c = statusColor(result.status);
|
|
1006
|
+
return `<div style="border-left:4px solid ${c.fg};background:${c.bg};padding:10px 12px;border-radius:8px"><div style="font-weight:600;color:#172033;font-size:0.88rem">${escapeHtml(caseId)} · ${escapeHtml(catalog.get(caseId)?.title ?? result.name)}</div><div style="color:${c.fg};font-size:0.82rem;margin-top:2px">${escapeHtml(result.message || humanStatus(result.status))}</div></div>`;
|
|
1007
|
+
}).join("")}</div></div>`
|
|
1008
|
+
: "";
|
|
1009
|
+
return `<!doctype html><html lang="zh-CN"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>${escapeHtml(input.title)}</title></head><body style="margin:0;background:linear-gradient(180deg,#eef2f7 0,#f7f9fc 40rem);color:#172033;font-family:system-ui,'Microsoft YaHei','PingFang SC',sans-serif;line-height:1.6"><div style="max-width:1000px;margin:0 auto;padding:32px 22px 56px"><div style="padding:8px 0 4px"><div style="color:#315f93;font-weight:700;letter-spacing:0.06em;font-size:0.76rem">后端自动化测试报告</div><h1 style="margin:6px 0 0;color:#17365d;font-size:24px;line-height:1.35">${escapeHtml(input.title)}</h1><p style="color:#667085;margin:8px 0 0;font-size:0.9rem">执行事实来自同一次 pytest 执行;用例默认折叠,点击任意用例可展开执行日志(接口、请求参数、响应结果)与检查点。</p></div><div style="background:#fff;border:1px solid #e3e8ef;border-radius:16px;padding:18px 20px;margin-top:16px;box-shadow:0 1px 2px rgba(16,24,40,0.04)">${summaryBanner}${metrics}</div><div style="background:#fff;border:1px solid #e3e8ef;border-radius:16px;padding:18px 20px;margin-top:16px;box-shadow:0 1px 2px rgba(16,24,40,0.04)">${qualityBlock}<div style="margin-top:12px;padding:10px 12px;background:#f6f8fa;border-radius:8px;font-size:0.82rem;color:#475467"><strong style="color:#667085">执行环境</strong>:${escapeHtml((input.environmentSummary || "不可用").split("\n").slice(0, 3).join(" ").slice(0, 200))}</div></div>${failureOverview}<div style="margin-top:18px"><div style="display:flex;align-items:baseline;justify-content:space-between;margin:0 4px 6px"><span style="color:#17365d;font-size:16px;font-weight:700">用例执行明细</span><span style="color:#98a2b3;font-size:0.8rem">共 ${input.parsed.cases.length} 条 · 点击展开</span></div>${caseCards || '<div style="padding:16px;border:1px dashed #e3e8ef;border-radius:12px;color:#667085;background:#fbfcfe;text-align:center">未发现可展示的用例。</div>'}</div></div></body></html>`;
|
|
1010
|
+
}
|
|
1011
|
+
function renderQualityCard(title, summary) {
|
|
1012
|
+
const colors = summary.status === "PASS" ? { fg: "#067647", bg: "#ecfdf3" } : summary.status === "FAIL" ? { fg: "#b42318", bg: "#fef3f2" } : { fg: "#9a6700", bg: "#fffaeb" };
|
|
1013
|
+
const label = summary.status === "Unavailable" ? "不可用" : summary.status;
|
|
1014
|
+
return `<div style="padding:12px 14px;border:1px solid #e3e8ef;border-radius:10px;background:#fbfcfe"><div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:6px"><strong style="color:#17365d;font-size:0.88rem">${escapeHtml(title)}</strong><span style="display:inline-block;padding:2px 9px;border-radius:999px;font-weight:700;font-size:0.72rem;color:${colors.fg};background:${colors.bg}">${label}</span></div><div style="color:#667085;font-size:0.78rem">Findings:${summary.findings ?? "不可用"}</div><div style="color:#475467;font-size:0.8rem;margin-top:2px">${escapeHtml(summary.firstFinding)}</div></div>`;
|
|
834
1015
|
}
|
|
835
1016
|
export function renderBackendTestFacts(input) {
|
|
836
1017
|
const passRate = input.parsed.tests > 0 ? input.parsed.passed / input.parsed.tests : 0;
|
|
@@ -538,12 +538,16 @@ function splitPytestNodeId(testId) {
|
|
|
538
538
|
function splitPytestHtmlLogSections(log) {
|
|
539
539
|
if (!log)
|
|
540
540
|
return { stdout: "", stderr: "" };
|
|
541
|
+
// pytest-html HTML-entity-escapes the captured log content (e.g. `"`
|
|
542
|
+
// for `"`). Decode entities first so downstream HTTP log parsers see the
|
|
543
|
+
// real JSON/kv payload rather than escaped markup.
|
|
544
|
+
const decoded = decodeHtmlEntities(log);
|
|
541
545
|
// pytest-html interleaves captured stdout/stderr with section markers like
|
|
542
546
|
// "----------------------------- Captured stdout call -----------------------------".
|
|
543
547
|
// String.split includes capture-group matches in the result array, so the
|
|
544
548
|
// layout is [beforeText, label, body, label, body, ...]. Iterate label/body
|
|
545
549
|
// pairs starting at index 1.
|
|
546
|
-
const sections =
|
|
550
|
+
const sections = decoded.split(/^-+ Captured (stdout|stderr|log|call|setup|teardown)(?: call| setup| teardown)? -+$/m);
|
|
547
551
|
let stdout = "";
|
|
548
552
|
let stderr = "";
|
|
549
553
|
for (let index = 1; index < sections.length - 1; index += 2) {
|
|
@@ -559,7 +563,7 @@ function splitPytestHtmlLogSections(log) {
|
|
|
559
563
|
if (!stdout && !stderr) {
|
|
560
564
|
// No recognizable section markers: treat the whole log as stdout so HTTP
|
|
561
565
|
// request/response lines remain visible.
|
|
562
|
-
stdout =
|
|
566
|
+
stdout = decoded.trim();
|
|
563
567
|
}
|
|
564
568
|
return { stdout, stderr };
|
|
565
569
|
}
|