diff --git a/.changeset/calm-work-summaries.md b/.changeset/calm-work-summaries.md new file mode 100644 index 000000000..2016c593c --- /dev/null +++ b/.changeset/calm-work-summaries.md @@ -0,0 +1,5 @@ +--- +"@nanocollective/nanocoder": minor +--- + +Grouped each VS Code chat turn's thoughts, tool calls, edit cards, and task plan into one ordered, collapsible work summary while leaving the final answer visible. The summary reports completed, stopped, or failed duration and reopens for pending approvals. Thanks to @Gambit-Checkmate. Closes #858. diff --git a/plugins/vscode/media/chat-panel.css b/plugins/vscode/media/chat-panel.css index a7094b352..2f15d7b33 100644 --- a/plugins/vscode/media/chat-panel.css +++ b/plugins/vscode/media/chat-panel.css @@ -1 +1 @@ -*,:after,:before{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:rgba(59,130,246,.5);--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }::backdrop{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:rgba(59,130,246,.5);--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }/*! tailwindcss v3.4.19 | MIT License | https://tailwindcss.com*/*,:after,:before{box-sizing:border-box;border:0 solid #e5e7eb}:after,:before{--tw-content:""}:host,html{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:ui-sans-serif,system-ui,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji;font-feature-settings:normal;font-variation-settings:normal;-webkit-tap-highlight-color:transparent}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,pre,samp{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-feature-settings:normal;font-variation-settings:normal;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-feature-settings:inherit;font-variation-settings:inherit;font-size:100%;font-weight:inherit;line-height:inherit;letter-spacing:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,input:where([type=button]),input:where([type=reset]),input:where([type=submit]){-webkit-appearance:button;background-color:transparent;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dd,dl,figure,h1,h2,h3,h4,h5,h6,hr,p,pre{margin:0}fieldset{margin:0}fieldset,legend{padding:0}menu,ol,ul{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}[role=button],button{cursor:pointer}:disabled{cursor:default}audio,canvas,embed,iframe,img,object,svg,video{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]:where(:not([hidden=until-found])){display:none}.container{width:100%}@media (min-width:640px){.container{max-width:640px}}@media (min-width:768px){.container{max-width:768px}}@media (min-width:1024px){.container{max-width:1024px}}@media (min-width:1280px){.container{max-width:1280px}}@media (min-width:1536px){.container{max-width:1536px}}.pointer-events-none{pointer-events:none}.static{position:static}.fixed{position:fixed}.absolute{position:absolute}.relative{position:relative}.inset-0{inset:0}.-top-10{top:-2.5rem}.bottom-24{bottom:6rem}.bottom-\[calc\(100\%\+8px\)\]{bottom:calc(100% + 8px)}.left-0{left:0}.left-1\/2{left:50%}.right-0{right:0}.top-0{top:0}.z-0{z-index:0}.z-10{z-index:10}.z-50{z-index:50}.mx-1{margin-left:.25rem;margin-right:.25rem}.my-2{margin-top:.5rem;margin-bottom:.5rem}.my-3{margin-top:.75rem;margin-bottom:.75rem}.mb-1{margin-bottom:.25rem}.mb-2{margin-bottom:.5rem}.ml-1{margin-left:.25rem}.ml-auto{margin-left:auto}.mr-1\.5{margin-right:.375rem}.mr-\[2px\]{margin-right:2px}.mt-0\.5{margin-top:.125rem}.mt-1{margin-top:.25rem}.mt-1\.5{margin-top:.375rem}.mt-10{margin-top:2.5rem}.mt-2{margin-top:.5rem}.mt-\[1px\]{margin-top:1px}.\!block{display:block!important}.block{display:block}.inline{display:inline}.flex{display:flex}.table{display:table}.hidden{display:none}.h-12{height:3rem}.h-24{height:6rem}.h-5{height:1.25rem}.h-8{height:2rem}.h-full{height:100%}.max-h-64{max-height:16rem}.max-h-\[250px\]{max-height:250px}.max-h-\[calc\(100vh-100px\)\]{max-height:calc(100vh - 100px)}.max-h-full{max-height:100%}.min-h-\[44px\]{min-height:44px}.w-12{width:3rem}.w-24{width:6rem}.w-5{width:1.25rem}.w-8{width:2rem}.w-fit{width:-moz-fit-content;width:fit-content}.w-full{width:100%}.min-w-0{min-width:0}.max-w-\[15\%\]{max-width:15%}.max-w-\[30\%\]{max-width:30%}.max-w-\[40\%\]{max-width:40%}.max-w-\[85\%\]{max-width:85%}.max-w-full{max-width:100%}.flex-1{flex:1 1 0%}.shrink{flex-shrink:1}.shrink-0{flex-shrink:0}.-translate-x-1\/2{--tw-translate-x:-50%}.-translate-x-1\/2,.transform{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}@keyframes pulse{50%{opacity:.5}}.animate-pulse{animation:pulse 2s cubic-bezier(.4,0,.6,1) infinite}@keyframes spin{to{transform:rotate(1turn)}}.animate-spin{animation:spin 1s linear infinite}.cursor-not-allowed{cursor:not-allowed}.cursor-pointer{cursor:pointer}.select-none{-webkit-user-select:none;-moz-user-select:none;user-select:none}.resize-none{resize:none}.flex-row{flex-direction:row}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.items-start{align-items:flex-start}.items-end{align-items:flex-end}.items-center{align-items:center}.justify-end{justify-content:flex-end}.justify-center{justify-content:center}.justify-between{justify-content:space-between}.gap-0\.5{gap:.125rem}.gap-1{gap:.25rem}.gap-1\.5{gap:.375rem}.gap-2{gap:.5rem}.gap-3{gap:.75rem}.self-start{align-self:flex-start}.self-end{align-self:flex-end}.overflow-hidden{overflow:hidden}.overflow-y-auto{overflow-y:auto}.overflow-x-hidden{overflow-x:hidden}.truncate{overflow:hidden;white-space:nowrap}.text-ellipsis,.truncate{text-overflow:ellipsis}.whitespace-nowrap{white-space:nowrap}.break-words{overflow-wrap:break-word}.rounded{border-radius:.25rem}.rounded-2xl{border-radius:1rem}.rounded-full{border-radius:9999px}.rounded-lg{border-radius:.5rem}.rounded-md{border-radius:.375rem}.rounded-bl{border-bottom-left-radius:.25rem}.border{border-width:1px}.border-b{border-bottom-width:1px}.border-l-\[3px\]{border-left-width:3px}.border-t{border-top-width:1px}.border-none{border-style:none}.border-vscode-border{border-color:var(--vscode-panel-border,hsla(0,0%,50%,.2))}.border-vscode-button-secondary{border-color:var(--vscode-button-secondaryBackground)}.border-vscode-focusBorder{border-color:var(--vscode-focusBorder)}.border-vscode-input-border{border-color:var(--vscode-input-border,transparent)}.border-vscode-input-focus{border-color:var(--vscode-focusBorder)}.border-vscode-widget-border{border-color:var(--vscode-widget-border)}.bg-black\/50{background-color:rgba(0,0,0,.5)}.bg-transparent{background-color:transparent}.bg-vscode-bg{background-color:var(--vscode-editor-background)}.bg-vscode-button-bg{background-color:var(--vscode-button-background)}.bg-vscode-button-secondary{background-color:var(--vscode-button-secondaryBackground)}.bg-vscode-dropdown-bg{background-color:var(--vscode-dropdown-background)}.bg-vscode-input-bg{background-color:var(--vscode-input-background)}.bg-vscode-list-active{background-color:var(--vscode-list-activeSelectionBackground)}.bg-vscode-widget-bg{background-color:var(--vscode-editorWidget-background)}.bg-vscode-widget-header{background-color:var(--vscode-editorWidget-border)}.object-contain{-o-object-fit:contain;object-fit:contain}.object-cover{-o-object-fit:cover;object-fit:cover}.p-1{padding:.25rem}.p-3{padding:.75rem}.p-4{padding:1rem}.px-1{padding-left:.25rem;padding-right:.25rem}.px-2{padding-left:.5rem;padding-right:.5rem}.px-3{padding-left:.75rem;padding-right:.75rem}.px-4{padding-left:1rem;padding-right:1rem}.py-0\.5{padding-top:.125rem;padding-bottom:.125rem}.py-1{padding-top:.25rem;padding-bottom:.25rem}.py-1\.5{padding-top:.375rem;padding-bottom:.375rem}.py-2{padding-top:.5rem;padding-bottom:.5rem}.py-3{padding-top:.75rem;padding-bottom:.75rem}.py-5{padding-top:1.25rem;padding-bottom:1.25rem}.pb-1{padding-bottom:.25rem}.pb-1\.5{padding-bottom:.375rem}.pb-2{padding-bottom:.5rem}.pl-3{padding-left:.75rem}.pt-1{padding-top:.25rem}.pt-2{padding-top:.5rem}.pt-2\.5{padding-top:.625rem}.pt-3{padding-top:.75rem}.text-left{text-align:left}.text-center{text-align:center}.font-vscode{font-family:var(--vscode-font-family)}.text-\[0\.75em\]{font-size:.75em}.text-\[0\.78em\]{font-size:.78em}.text-\[0\.85em\]{font-size:.85em}.text-\[0\.8em\]{font-size:.8em}.text-\[0\.95em\]{font-size:.95em}.text-\[0\.9em\]{font-size:.9em}.text-xs{font-size:.75rem;line-height:1rem}.font-medium{font-weight:500}.font-semibold{font-weight:600}.uppercase{text-transform:uppercase}.leading-none{line-height:1}.leading-relaxed{line-height:1.625}.leading-snug{line-height:1.375}.tracking-\[0\.04em\]{letter-spacing:.04em}.tracking-\[0\.06em\]{letter-spacing:.06em}.text-\[\#3178C6\]{--tw-text-opacity:1;color:rgb(49 120 198/var(--tw-text-opacity,1))}.text-\[\#563D7C\]{--tw-text-opacity:1;color:rgb(86 61 124/var(--tw-text-opacity,1))}.text-\[\#89d185\]{--tw-text-opacity:1;color:rgb(137 209 133/var(--tw-text-opacity,1))}.text-\[\#CB3837\]{--tw-text-opacity:1;color:rgb(203 56 55/var(--tw-text-opacity,1))}.text-\[\#E34F26\]{--tw-text-opacity:1;color:rgb(227 79 38/var(--tw-text-opacity,1))}.text-\[\#F1E05A\]{--tw-text-opacity:1;color:rgb(241 224 90/var(--tw-text-opacity,1))}.text-\[\#cccccc\]{--tw-text-opacity:1;color:rgb(204 204 204/var(--tw-text-opacity,1))}.text-\[\#f14c4c\]{--tw-text-opacity:1;color:rgb(241 76 76/var(--tw-text-opacity,1))}.text-vscode-button-fg{color:var(--vscode-button-foreground)}.text-vscode-dropdown-fg{color:var(--vscode-dropdown-foreground)}.text-vscode-fg{color:var(--vscode-editor-foreground)}.text-vscode-input-fg{color:var(--vscode-input-foreground)}.text-vscode-list-activeFg{color:var(--vscode-list-activeSelectionForeground)}.text-white{--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity,1))}.line-through{text-decoration-line:line-through}.opacity-0{opacity:0}.opacity-50{opacity:.5}.opacity-60{opacity:.6}.opacity-70{opacity:.7}.opacity-80{opacity:.8}.shadow-2xl{--tw-shadow:0 25px 50px -12px rgba(0,0,0,.25);--tw-shadow-colored:0 25px 50px -12px var(--tw-shadow-color)}.shadow-2xl,.shadow-lg{box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.shadow-lg{--tw-shadow:0 10px 15px -3px rgba(0,0,0,.1),0 4px 6px -4px rgba(0,0,0,.1);--tw-shadow-colored:0 10px 15px -3px var(--tw-shadow-color),0 4px 6px -4px var(--tw-shadow-color)}.shadow-sm{--tw-shadow:0 1px 2px 0 rgba(0,0,0,.05);--tw-shadow-colored:0 1px 2px 0 var(--tw-shadow-color)}.shadow-sm,.shadow-xl{box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.shadow-xl{--tw-shadow:0 20px 25px -5px rgba(0,0,0,.1),0 8px 10px -6px rgba(0,0,0,.1);--tw-shadow-colored:0 20px 25px -5px var(--tw-shadow-color),0 8px 10px -6px var(--tw-shadow-color)}.outline-none{outline:2px solid transparent;outline-offset:2px}.blur{--tw-blur:blur(8px)}.blur,.filter{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.backdrop-blur-md{--tw-backdrop-blur:blur(12px);backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia)}.transition-all{transition-property:all;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-colors{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-opacity{transition-property:opacity;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-transform{transition-property:transform;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.duration-200{transition-duration:.2s}body,html{height:100%;width:100%;margin:0;padding:0;overflow:hidden;font-family:var(--vscode-font-family);font-size:var(--vscode-font-size)}::-webkit-scrollbar{width:10px;height:10px}::-webkit-scrollbar-track{background:transparent}::-webkit-scrollbar-thumb{background:var(--vscode-scrollbarSlider-background);border:3px solid transparent;background-clip:padding-box;border-radius:5px}::-webkit-scrollbar-thumb:hover{background:var(--vscode-scrollbarSlider-hoverBackground);border:3px solid transparent;background-clip:padding-box}::-webkit-scrollbar-thumb:active{background:var(--vscode-scrollbarSlider-activeBackground);border:3px solid transparent;background-clip:padding-box}select{border:1px solid var(--vscode-dropdown-border,transparent)}select,select option{background-color:var(--vscode-dropdown-background);color:var(--vscode-dropdown-foreground)}.markdown-body p{margin-bottom:.5em;margin-top:0}.markdown-body p:last-child{margin-bottom:0}.markdown-body strong{font-weight:600}.markdown-body ul{list-style-type:disc}.markdown-body ol,.markdown-body ul{padding-left:1.5em;margin-bottom:.75em}.markdown-body ol{list-style-type:decimal}.markdown-body li{margin-bottom:.25em}.markdown-body code{font-family:var(--vscode-editor-font-family,monospace);padding:.1em .3em;border-radius:3px;font-size:.9em}.markdown-body code,.markdown-body pre{background-color:var(--vscode-textCodeBlock-background,rgba(0,0,0,.1))}.markdown-body pre{padding:.75em;border-radius:4px;overflow-x:auto;max-width:100%;margin-bottom:.75em}.markdown-body pre code{background-color:transparent;padding:0;font-size:.85em}.markdown-body h1,.markdown-body h2,.markdown-body h3,.markdown-body h4{font-weight:600;margin-top:1em;margin-bottom:.5em}.markdown-body h1{font-size:1.5em}.markdown-body h2{font-size:1.3em}.markdown-body h3{font-size:1.1em}.context-chip{display:inline-flex;align-items:center;gap:6px;max-width:200px;padding:4px 8px;border-radius:12px;border:1px solid var(--vscode-dropdown-border,hsla(0,0%,59%,.2));background-color:var(--vscode-editorWidget-background);font-size:.85em;font-weight:500;cursor:pointer;-webkit-user-select:none;-moz-user-select:none;user-select:none;white-space:nowrap;transition:all .15s ease}.context-chip:hover{border-color:var(--vscode-focusBorder);opacity:.95}.context-chip .chip-name{overflow:hidden;text-overflow:ellipsis}.context-chip .chip-remove{margin-left:2px;opacity:.5;font-size:1.1em;line-height:1;cursor:pointer;transition:opacity .15s ease}.context-chip .chip-remove:hover{opacity:1}#composer-box.drag-over:after{content:"Drop files or folders";position:absolute;inset:0;display:flex;align-items:center;justify-content:center;border:2px dashed var(--vscode-focusBorder);border-radius:inherit;background:var(--vscode-input-background);opacity:.92;font-size:.85em;font-weight:600;pointer-events:none;z-index:10}.settings-tab{background:transparent;border:none;border-bottom:2px solid transparent;color:var(--vscode-editor-foreground);opacity:.6;padding:.5rem .75rem;font-family:var(--vscode-font-family);font-size:.85em;cursor:pointer;transition:opacity .15s,border-color .15s}.settings-tab:hover{opacity:.9}.settings-tab.active{opacity:1;border-bottom-color:var(--vscode-focusBorder);font-weight:600}.settings-section{background:var(--vscode-editorWidget-background);border:1px solid var(--vscode-widget-border);border-radius:6px;padding:.75rem}.settings-section-title{font-size:.8em;font-weight:600;text-transform:uppercase;letter-spacing:.04em;opacity:.6;margin-bottom:.5rem}.settings-list{display:flex;flex-direction:column;gap:0}.settings-list-item{display:flex;align-items:center;gap:.5rem;padding:.375rem 0;font-size:.9em;border-bottom:1px solid var(--vscode-widget-border)}.settings-list-item:last-child{border-bottom:none}.settings-list-item-name{font-weight:500;flex-shrink:0}.settings-list-item-detail{opacity:.6;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;flex:1;min-width:0}.settings-list-empty{font-size:.85em;opacity:.5;padding:.25rem 0;font-style:italic}.settings-row{display:flex;align-items:center;justify-content:space-between;gap:.75rem;padding:.375rem 0;border-bottom:1px solid var(--vscode-widget-border)}.settings-row:last-child{border-bottom:none}.settings-row-info{display:flex;flex-direction:column;flex:1;min-width:0}.settings-row-label{font-size:.9em;font-weight:500}.settings-row-desc{font-size:.78em;opacity:.55;margin-top:.1em}.settings-toggle{position:relative;display:inline-block;width:36px;height:20px;flex-shrink:0;cursor:pointer}.settings-toggle input{opacity:0;width:0;height:0}.settings-toggle-slider{position:absolute;top:0;left:0;right:0;bottom:0;background-color:var(--vscode-input-background);border:1px solid var(--vscode-input-border,transparent);border-radius:20px;transition:background-color .2s}.settings-toggle-slider:before{content:"";position:absolute;height:14px;width:14px;left:2px;bottom:2px;background-color:var(--vscode-editor-foreground);opacity:.6;border-radius:50%;transition:transform .2s,opacity .2s}.settings-toggle input:checked+.settings-toggle-slider{background-color:var(--vscode-button-background);border-color:var(--vscode-button-background)}.settings-toggle input:checked+.settings-toggle-slider:before{transform:translateX(16px);opacity:1;background-color:var(--vscode-button-foreground)}.settings-select{background-color:var(--vscode-dropdown-background);color:var(--vscode-dropdown-foreground);border:1px solid var(--vscode-dropdown-border,transparent);border-radius:4px;padding:.25rem .5rem;font-family:var(--vscode-font-family);font-size:.85em;cursor:pointer;outline:none;min-width:100px}.settings-select:focus{border-color:var(--vscode-focusBorder)}.settings-number-input{background-color:var(--vscode-input-background);color:var(--vscode-input-foreground);border:1px solid var(--vscode-input-border,transparent);border-radius:4px;padding:.25rem .5rem;font-family:var(--vscode-font-family);font-size:.85em;width:70px;outline:none;text-align:center}.settings-number-input:focus{border-color:var(--vscode-focusBorder)}.settings-action-btn{display:flex;align-items:center;gap:.5rem;background:transparent;border:1px solid var(--vscode-button-secondaryBackground);color:var(--vscode-editor-foreground);border-radius:4px;padding:.375rem .625rem;font-family:var(--vscode-font-family);font-size:.85em;cursor:pointer;transition:background-color .15s}.settings-action-btn:hover{background-color:var(--vscode-button-secondaryHoverBackground)}.settings-action-btn-danger{border-color:rgba(241,76,76,.4);color:#f14c4c}.settings-action-btn-danger:hover{background-color:rgba(241,76,76,.1)}.settings-badge{display:inline-flex;align-items:center;gap:.25rem;font-size:.78em;padding:.125rem .375rem;border-radius:3px;font-weight:500}.settings-badge-ok{background-color:rgba(137,209,133,.15);color:#89d185}.settings-badge-off{background-color:hsla(0,0%,80%,.1);color:#999}.first\:border-t-0:first-child{border-top-width:0}.empty\:hidden:empty{display:none}.focus-within\:border-vscode-input-focus:focus-within{border-color:var(--vscode-focusBorder)}.hover\:scale-105:hover{--tw-scale-x:1.05;--tw-scale-y:1.05;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.hover\:bg-black\/80:hover{background-color:rgba(0,0,0,.8)}.hover\:bg-vscode-button-hover:hover{background-color:var(--vscode-button-hoverBackground)}.hover\:bg-vscode-button-secondaryHover:hover{background-color:var(--vscode-button-secondaryHoverBackground)}.hover\:bg-vscode-list-hover:hover{background-color:var(--vscode-list-hoverBackground)}.hover\:bg-vscode-toolbarHover:hover{background-color:var(--vscode-toolbar-hoverBackground,hsla(0,0%,50%,.15))}.hover\:opacity-100:hover{opacity:1}.hover\:opacity-80:hover{opacity:.8}.hover\:opacity-90:hover{opacity:.9}.disabled\:pointer-events-none:disabled{pointer-events:none}.disabled\:opacity-50:disabled{opacity:.5}.group:hover .group-hover\:opacity-100{opacity:1}.group.is-processing .group-\[\.is-processing\]\:block{display:block}.group.is-processing .group-\[\.is-processing\]\:hidden{display:none}.\[\&\.is-processing\]\:bg-vscode-button-secondary.is-processing{background-color:var(--vscode-button-secondaryBackground)}.\[\&\.is-processing\]\:hover\:bg-vscode-button-secondaryHover:hover.is-processing{background-color:var(--vscode-button-secondaryHoverBackground)}.\[\&_svg\]\:mr-0 svg{margin-right:0}.\[\&_svg\]\:h-6 svg{height:1.5rem}.\[\&_svg\]\:w-6 svg{width:1.5rem} \ No newline at end of file +*,:after,:before{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:rgba(59,130,246,.5);--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }::backdrop{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:rgba(59,130,246,.5);--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }/*! tailwindcss v3.4.19 | MIT License | https://tailwindcss.com*/*,:after,:before{box-sizing:border-box;border:0 solid #e5e7eb}:after,:before{--tw-content:""}:host,html{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:ui-sans-serif,system-ui,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji;font-feature-settings:normal;font-variation-settings:normal;-webkit-tap-highlight-color:transparent}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,pre,samp{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-feature-settings:normal;font-variation-settings:normal;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-feature-settings:inherit;font-variation-settings:inherit;font-size:100%;font-weight:inherit;line-height:inherit;letter-spacing:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,input:where([type=button]),input:where([type=reset]),input:where([type=submit]){-webkit-appearance:button;background-color:transparent;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dd,dl,figure,h1,h2,h3,h4,h5,h6,hr,p,pre{margin:0}fieldset{margin:0}fieldset,legend{padding:0}menu,ol,ul{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}[role=button],button{cursor:pointer}:disabled{cursor:default}audio,canvas,embed,iframe,img,object,svg,video{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]:where(:not([hidden=until-found])){display:none}.container{width:100%}@media (min-width:640px){.container{max-width:640px}}@media (min-width:768px){.container{max-width:768px}}@media (min-width:1024px){.container{max-width:1024px}}@media (min-width:1280px){.container{max-width:1280px}}@media (min-width:1536px){.container{max-width:1536px}}.pointer-events-none{pointer-events:none}.static{position:static}.fixed{position:fixed}.absolute{position:absolute}.relative{position:relative}.inset-0{inset:0}.-top-10{top:-2.5rem}.bottom-24{bottom:6rem}.bottom-\[calc\(100\%\+8px\)\]{bottom:calc(100% + 8px)}.left-0{left:0}.left-1\/2{left:50%}.right-0{right:0}.top-0{top:0}.z-0{z-index:0}.z-10{z-index:10}.z-50{z-index:50}.mx-1{margin-left:.25rem;margin-right:.25rem}.my-2{margin-top:.5rem;margin-bottom:.5rem}.my-3{margin-top:.75rem;margin-bottom:.75rem}.mb-1{margin-bottom:.25rem}.mb-2{margin-bottom:.5rem}.ml-1{margin-left:.25rem}.ml-auto{margin-left:auto}.mr-1\.5{margin-right:.375rem}.mr-\[2px\]{margin-right:2px}.mt-0\.5{margin-top:.125rem}.mt-1{margin-top:.25rem}.mt-1\.5{margin-top:.375rem}.mt-10{margin-top:2.5rem}.mt-2{margin-top:.5rem}.mt-\[1px\]{margin-top:1px}.\!block{display:block!important}.block{display:block}.inline{display:inline}.flex{display:flex}.table{display:table}.hidden{display:none}.h-12{height:3rem}.h-24{height:6rem}.h-5{height:1.25rem}.h-8{height:2rem}.h-full{height:100%}.max-h-64{max-height:16rem}.max-h-\[250px\]{max-height:250px}.max-h-\[calc\(100vh-100px\)\]{max-height:calc(100vh - 100px)}.max-h-full{max-height:100%}.min-h-\[44px\]{min-height:44px}.w-12{width:3rem}.w-24{width:6rem}.w-5{width:1.25rem}.w-8{width:2rem}.w-fit{width:-moz-fit-content;width:fit-content}.w-full{width:100%}.min-w-0{min-width:0}.max-w-\[15\%\]{max-width:15%}.max-w-\[30\%\]{max-width:30%}.max-w-\[40\%\]{max-width:40%}.max-w-\[85\%\]{max-width:85%}.max-w-full{max-width:100%}.flex-1{flex:1 1 0%}.shrink{flex-shrink:1}.shrink-0{flex-shrink:0}.-translate-x-1\/2{--tw-translate-x:-50%}.-translate-x-1\/2,.transform{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}@keyframes pulse{50%{opacity:.5}}.animate-pulse{animation:pulse 2s cubic-bezier(.4,0,.6,1) infinite}@keyframes spin{to{transform:rotate(1turn)}}.animate-spin{animation:spin 1s linear infinite}.cursor-not-allowed{cursor:not-allowed}.cursor-pointer{cursor:pointer}.select-none{-webkit-user-select:none;-moz-user-select:none;user-select:none}.resize-none{resize:none}.flex-row{flex-direction:row}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.items-start{align-items:flex-start}.items-end{align-items:flex-end}.items-center{align-items:center}.justify-end{justify-content:flex-end}.justify-center{justify-content:center}.justify-between{justify-content:space-between}.gap-0\.5{gap:.125rem}.gap-1{gap:.25rem}.gap-1\.5{gap:.375rem}.gap-2{gap:.5rem}.gap-3{gap:.75rem}.self-start{align-self:flex-start}.self-end{align-self:flex-end}.overflow-hidden{overflow:hidden}.overflow-y-auto{overflow-y:auto}.overflow-x-hidden{overflow-x:hidden}.truncate{overflow:hidden;white-space:nowrap}.text-ellipsis,.truncate{text-overflow:ellipsis}.whitespace-nowrap{white-space:nowrap}.break-words{overflow-wrap:break-word}.rounded{border-radius:.25rem}.rounded-2xl{border-radius:1rem}.rounded-full{border-radius:9999px}.rounded-lg{border-radius:.5rem}.rounded-md{border-radius:.375rem}.rounded-bl{border-bottom-left-radius:.25rem}.border{border-width:1px}.border-b{border-bottom-width:1px}.border-l-\[3px\]{border-left-width:3px}.border-t{border-top-width:1px}.border-none{border-style:none}.border-vscode-border{border-color:var(--vscode-panel-border,hsla(0,0%,50%,.2))}.border-vscode-button-secondary{border-color:var(--vscode-button-secondaryBackground)}.border-vscode-focusBorder{border-color:var(--vscode-focusBorder)}.border-vscode-input-border{border-color:var(--vscode-input-border,transparent)}.border-vscode-input-focus{border-color:var(--vscode-focusBorder)}.border-vscode-widget-border{border-color:var(--vscode-widget-border)}.bg-black\/50{background-color:rgba(0,0,0,.5)}.bg-transparent{background-color:transparent}.bg-vscode-bg{background-color:var(--vscode-editor-background)}.bg-vscode-button-bg{background-color:var(--vscode-button-background)}.bg-vscode-button-secondary{background-color:var(--vscode-button-secondaryBackground)}.bg-vscode-dropdown-bg{background-color:var(--vscode-dropdown-background)}.bg-vscode-input-bg{background-color:var(--vscode-input-background)}.bg-vscode-list-active{background-color:var(--vscode-list-activeSelectionBackground)}.bg-vscode-widget-bg{background-color:var(--vscode-editorWidget-background)}.bg-vscode-widget-header{background-color:var(--vscode-editorWidget-border)}.object-contain{-o-object-fit:contain;object-fit:contain}.object-cover{-o-object-fit:cover;object-fit:cover}.p-0{padding:0}.p-1{padding:.25rem}.p-3{padding:.75rem}.p-4{padding:1rem}.px-1{padding-left:.25rem;padding-right:.25rem}.px-2{padding-left:.5rem;padding-right:.5rem}.px-3{padding-left:.75rem;padding-right:.75rem}.px-4{padding-left:1rem;padding-right:1rem}.py-0\.5{padding-top:.125rem;padding-bottom:.125rem}.py-1{padding-top:.25rem;padding-bottom:.25rem}.py-1\.5{padding-top:.375rem;padding-bottom:.375rem}.py-2{padding-top:.5rem;padding-bottom:.5rem}.py-3{padding-top:.75rem;padding-bottom:.75rem}.py-5{padding-top:1.25rem;padding-bottom:1.25rem}.pb-1{padding-bottom:.25rem}.pb-1\.5{padding-bottom:.375rem}.pb-2{padding-bottom:.5rem}.pl-3{padding-left:.75rem}.pt-1{padding-top:.25rem}.pt-2{padding-top:.5rem}.pt-2\.5{padding-top:.625rem}.pt-3{padding-top:.75rem}.text-left{text-align:left}.text-center{text-align:center}.font-vscode{font-family:var(--vscode-font-family)}.text-\[0\.75em\]{font-size:.75em}.text-\[0\.78em\]{font-size:.78em}.text-\[0\.85em\]{font-size:.85em}.text-\[0\.8em\]{font-size:.8em}.text-\[0\.95em\]{font-size:.95em}.text-\[0\.9em\]{font-size:.9em}.text-xs{font-size:.75rem;line-height:1rem}.font-medium{font-weight:500}.font-semibold{font-weight:600}.uppercase{text-transform:uppercase}.leading-none{line-height:1}.leading-relaxed{line-height:1.625}.leading-snug{line-height:1.375}.tracking-\[0\.04em\]{letter-spacing:.04em}.tracking-\[0\.06em\]{letter-spacing:.06em}.text-\[\#3178C6\]{--tw-text-opacity:1;color:rgb(49 120 198/var(--tw-text-opacity,1))}.text-\[\#563D7C\]{--tw-text-opacity:1;color:rgb(86 61 124/var(--tw-text-opacity,1))}.text-\[\#89d185\]{--tw-text-opacity:1;color:rgb(137 209 133/var(--tw-text-opacity,1))}.text-\[\#CB3837\]{--tw-text-opacity:1;color:rgb(203 56 55/var(--tw-text-opacity,1))}.text-\[\#E34F26\]{--tw-text-opacity:1;color:rgb(227 79 38/var(--tw-text-opacity,1))}.text-\[\#F1E05A\]{--tw-text-opacity:1;color:rgb(241 224 90/var(--tw-text-opacity,1))}.text-\[\#cccccc\]{--tw-text-opacity:1;color:rgb(204 204 204/var(--tw-text-opacity,1))}.text-\[\#f14c4c\]{--tw-text-opacity:1;color:rgb(241 76 76/var(--tw-text-opacity,1))}.text-vscode-button-fg{color:var(--vscode-button-foreground)}.text-vscode-dropdown-fg{color:var(--vscode-dropdown-foreground)}.text-vscode-fg{color:var(--vscode-editor-foreground)}.text-vscode-input-fg{color:var(--vscode-input-foreground)}.text-vscode-list-activeFg{color:var(--vscode-list-activeSelectionForeground)}.text-white{--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity,1))}.line-through{text-decoration-line:line-through}.opacity-0{opacity:0}.opacity-50{opacity:.5}.opacity-60{opacity:.6}.opacity-70{opacity:.7}.opacity-80{opacity:.8}.shadow-2xl{--tw-shadow:0 25px 50px -12px rgba(0,0,0,.25);--tw-shadow-colored:0 25px 50px -12px var(--tw-shadow-color)}.shadow-2xl,.shadow-lg{box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.shadow-lg{--tw-shadow:0 10px 15px -3px rgba(0,0,0,.1),0 4px 6px -4px rgba(0,0,0,.1);--tw-shadow-colored:0 10px 15px -3px var(--tw-shadow-color),0 4px 6px -4px var(--tw-shadow-color)}.shadow-sm{--tw-shadow:0 1px 2px 0 rgba(0,0,0,.05);--tw-shadow-colored:0 1px 2px 0 var(--tw-shadow-color)}.shadow-sm,.shadow-xl{box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.shadow-xl{--tw-shadow:0 20px 25px -5px rgba(0,0,0,.1),0 8px 10px -6px rgba(0,0,0,.1);--tw-shadow-colored:0 20px 25px -5px var(--tw-shadow-color),0 8px 10px -6px var(--tw-shadow-color)}.outline-none{outline:2px solid transparent;outline-offset:2px}.blur{--tw-blur:blur(8px)}.blur,.filter{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.backdrop-blur-md{--tw-backdrop-blur:blur(12px);backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia)}.transition-all{transition-property:all;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-colors{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-opacity{transition-property:opacity;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-transform{transition-property:transform;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.duration-200{transition-duration:.2s}body,html{height:100%;width:100%;margin:0;padding:0;overflow:hidden;font-family:var(--vscode-font-family);font-size:var(--vscode-font-size)}::-webkit-scrollbar{width:10px;height:10px}::-webkit-scrollbar-track{background:transparent}::-webkit-scrollbar-thumb{background:var(--vscode-scrollbarSlider-background);border:3px solid transparent;background-clip:padding-box;border-radius:5px}::-webkit-scrollbar-thumb:hover{background:var(--vscode-scrollbarSlider-hoverBackground);border:3px solid transparent;background-clip:padding-box}::-webkit-scrollbar-thumb:active{background:var(--vscode-scrollbarSlider-activeBackground);border:3px solid transparent;background-clip:padding-box}select{border:1px solid var(--vscode-dropdown-border,transparent)}select,select option{background-color:var(--vscode-dropdown-background);color:var(--vscode-dropdown-foreground)}.markdown-body p{margin-bottom:.5em;margin-top:0}.markdown-body p:last-child{margin-bottom:0}.markdown-body strong{font-weight:600}.markdown-body ul{list-style-type:disc}.markdown-body ol,.markdown-body ul{padding-left:1.5em;margin-bottom:.75em}.markdown-body ol{list-style-type:decimal}.markdown-body li{margin-bottom:.25em}.markdown-body code{font-family:var(--vscode-editor-font-family,monospace);padding:.1em .3em;border-radius:3px;font-size:.9em}.markdown-body code,.markdown-body pre{background-color:var(--vscode-textCodeBlock-background,rgba(0,0,0,.1))}.markdown-body pre{padding:.75em;border-radius:4px;overflow-x:auto;max-width:100%;margin-bottom:.75em}.markdown-body pre code{background-color:transparent;padding:0;font-size:.85em}.markdown-body h1,.markdown-body h2,.markdown-body h3,.markdown-body h4{font-weight:600;margin-top:1em;margin-bottom:.5em}.markdown-body h1{font-size:1.5em}.markdown-body h2{font-size:1.3em}.markdown-body h3{font-size:1.1em}.context-chip{display:inline-flex;align-items:center;gap:6px;max-width:200px;padding:4px 8px;border-radius:12px;border:1px solid var(--vscode-dropdown-border,hsla(0,0%,59%,.2));background-color:var(--vscode-editorWidget-background);font-size:.85em;font-weight:500;cursor:pointer;-webkit-user-select:none;-moz-user-select:none;user-select:none;white-space:nowrap;transition:all .15s ease}.context-chip:hover{border-color:var(--vscode-focusBorder);opacity:.95}.context-chip .chip-name{overflow:hidden;text-overflow:ellipsis}.context-chip .chip-remove{margin-left:2px;opacity:.5;font-size:1.1em;line-height:1;cursor:pointer;transition:opacity .15s ease}.context-chip .chip-remove:hover{opacity:1}#composer-box.drag-over:after{content:"Drop files or folders";position:absolute;inset:0;display:flex;align-items:center;justify-content:center;border:2px dashed var(--vscode-focusBorder);border-radius:inherit;background:var(--vscode-input-background);opacity:.92;font-size:.85em;font-weight:600;pointer-events:none;z-index:10}.settings-tab{background:transparent;border:none;border-bottom:2px solid transparent;color:var(--vscode-editor-foreground);opacity:.6;padding:.5rem .75rem;font-family:var(--vscode-font-family);font-size:.85em;cursor:pointer;transition:opacity .15s,border-color .15s}.settings-tab:hover{opacity:.9}.settings-tab.active{opacity:1;border-bottom-color:var(--vscode-focusBorder);font-weight:600}.settings-section{background:var(--vscode-editorWidget-background);border:1px solid var(--vscode-widget-border);border-radius:6px;padding:.75rem}.settings-section-title{font-size:.8em;font-weight:600;text-transform:uppercase;letter-spacing:.04em;opacity:.6;margin-bottom:.5rem}.settings-list{display:flex;flex-direction:column;gap:0}.settings-list-item{display:flex;align-items:center;gap:.5rem;padding:.375rem 0;font-size:.9em;border-bottom:1px solid var(--vscode-widget-border)}.settings-list-item:last-child{border-bottom:none}.settings-list-item-name{font-weight:500;flex-shrink:0}.settings-list-item-detail{opacity:.6;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;flex:1;min-width:0}.settings-list-empty{font-size:.85em;opacity:.5;padding:.25rem 0;font-style:italic}.settings-row{display:flex;align-items:center;justify-content:space-between;gap:.75rem;padding:.375rem 0;border-bottom:1px solid var(--vscode-widget-border)}.settings-row:last-child{border-bottom:none}.settings-row-info{display:flex;flex-direction:column;flex:1;min-width:0}.settings-row-label{font-size:.9em;font-weight:500}.settings-row-desc{font-size:.78em;opacity:.55;margin-top:.1em}.settings-toggle{position:relative;display:inline-block;width:36px;height:20px;flex-shrink:0;cursor:pointer}.settings-toggle input{opacity:0;width:0;height:0}.settings-toggle-slider{position:absolute;top:0;left:0;right:0;bottom:0;background-color:var(--vscode-input-background);border:1px solid var(--vscode-input-border,transparent);border-radius:20px;transition:background-color .2s}.settings-toggle-slider:before{content:"";position:absolute;height:14px;width:14px;left:2px;bottom:2px;background-color:var(--vscode-editor-foreground);opacity:.6;border-radius:50%;transition:transform .2s,opacity .2s}.settings-toggle input:checked+.settings-toggle-slider{background-color:var(--vscode-button-background);border-color:var(--vscode-button-background)}.settings-toggle input:checked+.settings-toggle-slider:before{transform:translateX(16px);opacity:1;background-color:var(--vscode-button-foreground)}.settings-select{background-color:var(--vscode-dropdown-background);color:var(--vscode-dropdown-foreground);border:1px solid var(--vscode-dropdown-border,transparent);border-radius:4px;padding:.25rem .5rem;font-family:var(--vscode-font-family);font-size:.85em;cursor:pointer;outline:none;min-width:100px}.settings-select:focus{border-color:var(--vscode-focusBorder)}.settings-number-input{background-color:var(--vscode-input-background);color:var(--vscode-input-foreground);border:1px solid var(--vscode-input-border,transparent);border-radius:4px;padding:.25rem .5rem;font-family:var(--vscode-font-family);font-size:.85em;width:70px;outline:none;text-align:center}.settings-number-input:focus{border-color:var(--vscode-focusBorder)}.settings-action-btn{display:flex;align-items:center;gap:.5rem;background:transparent;border:1px solid var(--vscode-button-secondaryBackground);color:var(--vscode-editor-foreground);border-radius:4px;padding:.375rem .625rem;font-family:var(--vscode-font-family);font-size:.85em;cursor:pointer;transition:background-color .15s}.settings-action-btn:hover{background-color:var(--vscode-button-secondaryHoverBackground)}.settings-action-btn-danger{border-color:rgba(241,76,76,.4);color:#f14c4c}.settings-action-btn-danger:hover{background-color:rgba(241,76,76,.1)}.settings-badge{display:inline-flex;align-items:center;gap:.25rem;font-size:.78em;padding:.125rem .375rem;border-radius:3px;font-weight:500}.settings-badge-ok{background-color:rgba(137,209,133,.15);color:#89d185}.settings-badge-off{background-color:hsla(0,0%,80%,.1);color:#999}.first\:border-t-0:first-child{border-top-width:0}.empty\:hidden:empty{display:none}.focus-within\:border-vscode-input-focus:focus-within{border-color:var(--vscode-focusBorder)}.hover\:scale-105:hover{--tw-scale-x:1.05;--tw-scale-y:1.05;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.hover\:bg-black\/80:hover{background-color:rgba(0,0,0,.8)}.hover\:bg-vscode-button-hover:hover{background-color:var(--vscode-button-hoverBackground)}.hover\:bg-vscode-button-secondaryHover:hover{background-color:var(--vscode-button-secondaryHoverBackground)}.hover\:bg-vscode-list-hover:hover{background-color:var(--vscode-list-hoverBackground)}.hover\:bg-vscode-toolbarHover:hover{background-color:var(--vscode-toolbar-hoverBackground,hsla(0,0%,50%,.15))}.hover\:opacity-100:hover{opacity:1}.hover\:opacity-80:hover{opacity:.8}.hover\:opacity-90:hover{opacity:.9}.disabled\:pointer-events-none:disabled{pointer-events:none}.disabled\:opacity-50:disabled{opacity:.5}.group:hover .group-hover\:opacity-100{opacity:1}.group.is-processing .group-\[\.is-processing\]\:block{display:block}.group.is-processing .group-\[\.is-processing\]\:hidden{display:none}.\[\&\.is-processing\]\:bg-vscode-button-secondary.is-processing{background-color:var(--vscode-button-secondaryBackground)}.\[\&\.is-processing\]\:hover\:bg-vscode-button-secondaryHover:hover.is-processing{background-color:var(--vscode-button-secondaryHoverBackground)}.\[\&_svg\]\:mr-0 svg{margin-right:0}.\[\&_svg\]\:h-6 svg{height:1.5rem}.\[\&_svg\]\:w-6 svg{width:1.5rem} \ No newline at end of file diff --git a/plugins/vscode/media/chat-panel.js b/plugins/vscode/media/chat-panel.js index 95acb1950..d2915201f 100644 --- a/plugins/vscode/media/chat-panel.js +++ b/plugins/vscode/media/chat-panel.js @@ -385,8 +385,9 @@ let sessionsData = []; let isHistoryView = false; let isProcessing = false; - let currentAggregator = null; - let currentThoughtBox = null; + let currentWorkSummary = null; + const workSummaryByToolCallId = new Map(); + const workSummaryByPlanId = new Map(); let visualLoader = null; const toolKinds = new Map(); let toastTimeout = null; @@ -501,24 +502,20 @@ } // --- Send / Stop toggle logic --- - function setProcessing(active) { + function setProcessing(active, outcome = 'completed') { isProcessing = active; if (!active) { - // Globally cancel any stuck spinners across all tool cards, - // in case multiple aggregators were created in the same session + // Globally settle any stuck spinners across all tool cards. const allSpinners = document.querySelectorAll('.tool-status'); allSpinners.forEach(statusEl => { const status = statusEl.dataset.status; if (status === 'pending' || status === 'in_progress') { statusEl.innerHTML = ICONS.cancelled; + statusEl.dataset.status = 'cancelled'; } }); stopVisualLoader(); - - if (currentAggregator) { - currentAggregator.close(); - } - currentAggregator = null; + finishCurrentWorkSummary(outcome); } if (sendStopBtn) { sendStopBtn.title = active ? 'Stop (cancel)' : 'Send (Enter)'; @@ -529,7 +526,7 @@ // Shared by the Stop button and Escape so the two can't drift apart. function requestCancel() { vscode.postMessage({ type: 'cancel' }); - setProcessing(false); + setProcessing(false, 'cancelled'); } if (sendStopBtn) { @@ -1059,7 +1056,10 @@ // A user message opens a new turn, so the agent segments that follow get // a fresh id. The raw-text accumulator is handed over lazily, once the // new response produces text. - if (role === 'user') agentTurnId++; + if (role === 'user') { + finishCurrentWorkSummary('completed'); + agentTurnId++; + } const wrapper = document.createElement('div'); wrapper.className = 'group flex flex-col min-w-0 shrink-0 ' + @@ -1178,6 +1178,9 @@ wrapper.appendChild(createMessageFooter(() => content, role, new Date())); messagesContainer.appendChild(wrapper); + if (role === 'user') { + currentWorkSummary = new WorkSummary(Date.now()); + } scrollToBottom(); if (role === 'agent') { @@ -1439,8 +1442,10 @@ case 'clear': // Session reset (new chat or resume) should return to the active // chat view, not leave the panel stuck on the history list. - if (isHistoryView) showChatView(); - if (isSettingsView) hideSettingsView(); + if (isHistoryView || isSettingsView) showChatView(); + discardCurrentWorkSummary(); + workSummaryByToolCallId.clear(); + workSummaryByPlanId.clear(); if (renderTimeout) { clearTimeout(renderTimeout); renderTimeout = null; } if (message.isLoading) { messagesContainer.innerHTML = `
${ICONS.pending}
Loading session...
`; @@ -1455,17 +1460,10 @@ lastAgentRawTurnId = -1; lastAgentSegments = ''; lastAgentRawText = ''; - if (currentThoughtBox) { - clearInterval(currentThoughtBox.timer); - currentThoughtBox = null; - } setProcessing(false); break; case 'sessionLoaded': - if (currentThoughtBox) { - currentThoughtBox.pause(); - currentThoughtBox = null; - } + finishCurrentWorkSummary('completed'); const loader = document.getElementById('session-loader'); if (loader) loader.remove(); scrollToBottom(); @@ -1679,8 +1677,8 @@ // Close the current streamed-text block: flush any pending throttled // render, then reset so the next agent_message_chunk starts a fresh - // markdown block. Called whenever another element (tool card, thought - // box, user message) is inserted - without this, text streamed after a + // markdown block. Called whenever another element (work activity or user + // message) is inserted - without this, text streamed after a // tool call is appended to the block ABOVE the card, fusing pre-tool // and post-tool output into one paragraph. function endCurrentTextBlock() { @@ -1696,10 +1694,6 @@ const update = payload.update ? payload.update : payload; if (update.sessionUpdate === 'user_message_chunk') { - if (currentThoughtBox) { - currentThoughtBox.pause(); - currentThoughtBox = null; - } if (update.content) { endCurrentTextBlock(); if (update.content.text) { @@ -1711,42 +1705,30 @@ } } } else if (update.sessionUpdate === 'agent_message_chunk') { - if (currentThoughtBox) { - currentThoughtBox.pause(); - } + currentWorkSummary?.endActivityGroup(); if (update.content && update.content.text) { stopVisualLoader(); appendChunk(update.content.text); } } else if (update.sessionUpdate === 'agent_thought_chunk') { - if (!currentThoughtBox) { - endCurrentTextBlock(); - currentThoughtBox = new ThoughtAggregator(); - } if (update.content && update.content.text) { - currentThoughtBox.append(update.content.text); + endCurrentTextBlock(); + ensureCurrentWorkSummary().appendThought(update.content.text); } } else if (update.sessionUpdate === 'tool_call' || update.sessionUpdate === 'tool_call_update') { - if (currentThoughtBox) { - currentThoughtBox.pause(); - } handleToolCallUpdate(update); } else if (update.sessionUpdate === 'plan') { handlePlanUpdate(update); } else if (update.sessionUpdate === 'prompt_response' || update.sessionUpdate === 'done') { - if (currentThoughtBox) { - currentThoughtBox.pause(); - currentThoughtBox = null; - } // Show token usage (and estimated cost) for the finished turn appendUsageIndicator(update.usage, update.cost); // Turn is complete — restore the send button - setProcessing(false); + setProcessing(false, update.outcome || 'completed'); } keepVisualLoaderAtBottom(); } - // ─── Settings Panel Logic ─────────────────────────────────────── + // --- Settings Panel Logic --- let isSettingsView = false; @@ -1800,9 +1782,8 @@ }); }); - // Behavior tab — interactive controls change handlers + // Behavior tab - interactive controls change handlers function initSettingsControls() { - // Default mode const modeSelect = document.getElementById('setting-defaultMode'); if (modeSelect) { modeSelect.addEventListener('change', () => { @@ -1810,7 +1791,6 @@ }); } - // Auto-compact enabled const acEnabled = document.getElementById('setting-autoCompact-enabled'); if (acEnabled) { acEnabled.addEventListener('change', () => { @@ -1818,7 +1798,6 @@ }); } - // Auto-compact threshold const acThreshold = document.getElementById('setting-autoCompact-threshold'); if (acThreshold) { acThreshold.addEventListener('change', () => { @@ -1829,7 +1808,6 @@ }); } - // Auto-compact mode const acMode = document.getElementById('setting-autoCompact-mode'); if (acMode) { acMode.addEventListener('change', () => { @@ -1837,7 +1815,6 @@ }); } - // Reasoning traces const rtToggle = document.getElementById('setting-reasoningTraces'); if (rtToggle) { rtToggle.addEventListener('change', () => { @@ -1845,7 +1822,6 @@ }); } - // Sessions auto-save const saToggle = document.getElementById('setting-sessions-autoSave'); if (saToggle) { saToggle.addEventListener('change', () => { @@ -1855,11 +1831,8 @@ } initSettingsControls(); - /** - * Populate the settings UI with data received from the extension host. - */ + /** Populate the settings UI with data received from the extension host. */ function renderSettingsData(settings) { - // ── Providers list ── const providersList = document.getElementById('settings-providers-list'); if (providersList) { if (settings.providers.length === 0) { @@ -1882,7 +1855,6 @@ } } - // ── MCP Servers list ── const mcpList = document.getElementById('settings-mcp-list'); if (mcpList) { if (settings.mcpServers.length === 0) { @@ -1898,21 +1870,17 @@ } } - // ── Tool auto-approval list ── const toolsList = document.getElementById('settings-tools-list'); if (toolsList) { if (settings.alwaysAllow.length === 0) { toolsList.innerHTML = '
No tools auto-approved
'; } else { toolsList.innerHTML = settings.alwaysAllow.map(t => - `
- ${escapeHtml(t)} -
` + `
${escapeHtml(t)}
` ).join(''); } } - // ── Web search status ── const wsStatus = document.getElementById('settings-websearch-status'); if (wsStatus) { wsStatus.innerHTML = settings.webSearch.configured @@ -1920,22 +1888,16 @@ : '
Not configured
'; } - // ── Behavior controls ── const modeSelect = document.getElementById('setting-defaultMode'); if (modeSelect) modeSelect.value = settings.defaultMode || 'normal'; - const acEnabled = document.getElementById('setting-autoCompact-enabled'); if (acEnabled) acEnabled.checked = settings.autoCompact.enabled; - const acThreshold = document.getElementById('setting-autoCompact-threshold'); if (acThreshold) acThreshold.value = settings.autoCompact.threshold; - const acMode = document.getElementById('setting-autoCompact-mode'); if (acMode) acMode.value = settings.autoCompact.mode; - const rtToggle = document.getElementById('setting-reasoningTraces'); if (rtToggle) rtToggle.checked = settings.reasoningTraces; - const saToggle = document.getElementById('setting-sessions-autoSave'); if (saToggle) saToggle.checked = settings.sessions.autoSave; } @@ -1946,62 +1908,159 @@ return div.innerHTML; } - // ─── End Settings Panel Logic ─────────────────────────────────── + // --- End Settings Panel Logic --- + + function formatWorkDuration(elapsedMs) { + const totalSeconds = Math.max(0, Math.floor(elapsedMs / 1000)); + if (totalSeconds < 60) return `${totalSeconds}s`; + + const totalMinutes = Math.floor(totalSeconds / 60); + const seconds = totalSeconds % 60; + if (totalMinutes < 60) { + return seconds ? `${totalMinutes}m ${seconds}s` : `${totalMinutes}m`; + } + + const hours = Math.floor(totalMinutes / 60); + const minutes = totalMinutes % 60; + return minutes ? `${hours}h ${minutes}m` : `${hours}h`; + } + + function ensureCurrentWorkSummary() { + if (!currentWorkSummary) { + currentWorkSummary = new WorkSummary(Date.now()); + } + return currentWorkSummary; + } + + function finishCurrentWorkSummary(outcome) { + if (!currentWorkSummary) return; + const summary = currentWorkSummary; + currentWorkSummary = null; + summary.finish(outcome); + } + + function discardCurrentWorkSummary() { + if (!currentWorkSummary) return; + currentWorkSummary.dispose(); + currentWorkSummary = null; + } + + class WorkSummary { + constructor(startedAt) { + this.startedAt = startedAt; + this.activityCount = 0; + this.isOpen = true; + this.isFinished = false; + this.currentThought = null; + this.currentToolGroup = null; + this.toolGroups = new Map(); - class ThoughtAggregator { - constructor() { this.el = document.createElement('div'); - this.el.className = 'my-2 flex flex-col shrink-0 thought-aggregator'; + this.el.className = 'my-2 flex flex-col shrink-0 work-summary'; + this.el.style.display = 'none'; - this.header = document.createElement('div'); - this.header.className = 'flex items-center gap-1.5 cursor-pointer opacity-70 text-vscode-fg hover:opacity-100 transition-opacity select-none w-fit'; + this.header = document.createElement('button'); + this.header.type = 'button'; + this.header.className = 'flex items-center gap-1.5 cursor-pointer opacity-70 text-vscode-fg hover:opacity-100 transition-opacity select-none w-fit bg-transparent border-none p-0'; + this.header.setAttribute('aria-expanded', 'true'); this.header.onclick = () => this.toggle(); this.title = document.createElement('span'); this.title.className = 'font-vscode text-[0.85em] font-medium'; - this.title.textContent = 'Thinking...'; + this.title.textContent = 'Working...'; this.chevron = document.createElement('span'); this.chevron.className = 'flex items-center justify-center opacity-70'; this.chevron.innerHTML = ICONS.chevron; - this.chevron.style.transform = 'rotate(0deg)'; // open by default this.header.appendChild(this.title); this.header.appendChild(this.chevron); this.el.appendChild(this.header); this.body = document.createElement('div'); - this.body.className = 'mt-2 pl-3 border-l-[3px] border-vscode-border opacity-70 text-vscode-fg markdown-body text-[0.95em]'; + this.body.className = 'mt-2 pl-3 border-l-[3px] border-vscode-border flex flex-col gap-2 min-w-0'; this.el.appendChild(this.body); - this.isOpen = true; - this.userToggled = false; - this.text = ''; - this.renderTimeout = null; - this.thinkingMs = 0; - this.segmentStart = Date.now(); - this.timer = setInterval(() => this.updateTimer(), 1000); - messagesContainer.appendChild(this.el); - scrollToBottom(); } - elapsedSeconds() { - const active = this.segmentStart ? Date.now() - this.segmentStart : 0; - return Math.floor((this.thinkingMs + active) / 1000); + elapsedMs() { + return Date.now() - this.startedAt; } updateTimer() { - this.title.textContent = `Thinking for ${this.elapsedSeconds()}s`; + if (this.isFinished || this.activityCount === 0) return; + this.title.textContent = `Working for ${formatWorkDuration(this.elapsedMs())}`; } - toggle(force) { - if (force === undefined) { - this.userToggled = true; + reveal() { + if (this.activityCount > 0) return; + this.el.style.display = ''; + this.updateTimer(); + } + + addActivity(element) { + this.reveal(); + this.activityCount++; + this.body.appendChild(element); + scrollToBottom(); + } + + removeActivity(element) { + if (element.parentElement !== this.body) return; + element.remove(); + this.activityCount = Math.max(0, this.activityCount - 1); + if (this.activityCount === 0) this.el.style.display = 'none'; + } + + appendThought(chunk) { + this.currentToolGroup = null; + if (!this.currentThought) { + this.currentThought = new WorkThought(); + this.addActivity(this.currentThought.el); + } + this.currentThought.append(chunk); + } + + endThought() { + if (!this.currentThought) return; + this.currentThought.finish(); + this.currentThought = null; + } + + endActivityGroup() { + this.endThought(); + this.currentToolGroup = null; + } + + addOrUpdateTool(toolCallId, update) { + this.endThought(); + let group = this.toolGroups.get(toolCallId); + if (!group) { + if (!this.currentToolGroup) { + this.currentToolGroup = new ToolAggregator(); + this.addActivity(this.currentToolGroup.el); + } + group = this.currentToolGroup; + this.toolGroups.set(toolCallId, group); } + group.addOrUpdateTool(toolCallId, update); + } + + addStandaloneActivity(element) { + this.endActivityGroup(); + this.addActivity(element); + } + + openForInteraction() { + if (this.activityCount > 0) this.toggle(true); + } + + toggle(force) { this.isOpen = force !== undefined ? force : !this.isOpen; - this.body.style.display = this.isOpen ? 'block' : 'none'; + this.body.style.display = this.isOpen ? '' : 'none'; + this.header.setAttribute('aria-expanded', String(this.isOpen)); const svg = this.chevron.querySelector('svg'); if (svg) { @@ -2009,6 +2068,60 @@ } } + finish(outcome) { + if (this.isFinished) return; + this.isFinished = true; + this.endActivityGroup(); + clearInterval(this.timer); + this.timer = null; + + if (this.activityCount === 0) { + this.el.remove(); + return; + } + + const terminalOutcome = outcome === 'cancelled' || outcome === 'failed' + ? outcome + : 'completed'; + this.el.dataset.outcome = terminalOutcome; + const duration = formatWorkDuration(this.elapsedMs()); + if (terminalOutcome === 'cancelled') { + this.title.textContent = `Stopped after ${duration}`; + } else if (terminalOutcome === 'failed') { + this.title.textContent = `Failed after ${duration}`; + } else { + this.title.textContent = `Worked for ${duration}`; + } + this.toggle(false); + } + + dispose() { + this.currentThought?.dispose(); + clearInterval(this.timer); + this.timer = null; + this.el.remove(); + } + } + + class WorkThought { + constructor() { + this.text = ''; + this.renderTimeout = null; + + this.el = document.createElement('div'); + this.el.className = 'work-summary-thought py-1 min-w-0'; + + this.label = document.createElement('div'); + this.label.className = 'font-vscode text-[0.78em] font-medium opacity-60 mb-1'; + this.label.textContent = 'Thought'; + + this.body = document.createElement('div'); + this.body.className = 'markdown-body text-[0.95em] opacity-70 text-vscode-fg min-w-0'; + + this.el.appendChild(this.label); + this.el.appendChild(this.body); + } + render() { if (typeof marked !== 'undefined') { this.body.innerHTML = marked.parse(this.text); @@ -2018,7 +2131,6 @@ } append(chunk) { - this.resume(); this.text += chunk; if (typeof marked !== 'undefined') { if (!this.renderTimeout) { @@ -2034,85 +2146,45 @@ } } - resume() { - if (this.segmentStart) return; - if (this.text && !this.text.endsWith('\n\n')) { - this.text += '\n\n'; - } - this.segmentStart = Date.now(); - this.timer = setInterval(() => this.updateTimer(), 1000); - this.updateTimer(); - if (!this.userToggled) { - this.toggle(true); - } - } - - pause() { - if (!this.segmentStart) return; - this.thinkingMs += Date.now() - this.segmentStart; - this.segmentStart = null; - clearInterval(this.timer); - this.timer = null; + finish() { if (this.renderTimeout) { clearTimeout(this.renderTimeout); this.renderTimeout = null; } this.render(); - this.title.textContent = `Thought for ${this.elapsedSeconds()}s`; - if (!this.userToggled) { - this.toggle(false); - } + } + + dispose() { + if (this.renderTimeout) clearTimeout(this.renderTimeout); + this.renderTimeout = null; } } class ToolAggregator { constructor() { this.el = document.createElement('div'); - this.el.className = 'my-3 border border-vscode-widget-border rounded bg-vscode-widget-bg overflow-hidden shrink-0 tool-aggregator'; + this.el.className = 'border border-vscode-widget-border rounded bg-vscode-widget-bg overflow-hidden shrink-0 tool-aggregator work-summary-tool-group'; this.header = document.createElement('div'); - this.header.className = 'px-3 py-2 flex items-center bg-vscode-widget-header border-b border-vscode-widget-border gap-2 cursor-pointer select-none'; - this.header.onclick = () => this.toggle(); + this.header.className = 'px-3 py-2 flex items-center bg-vscode-widget-header border-b border-vscode-widget-border gap-2'; this.title = document.createElement('span'); this.title.className = 'font-vscode text-[0.9em] opacity-80'; - this.title.textContent = 'Exploring...'; - - this.chevron = document.createElement('span'); - this.chevron.className = 'ml-auto flex items-center justify-center'; - this.chevron.innerHTML = ICONS.chevron; + this.title.textContent = 'Tools'; this.header.appendChild(this.title); - this.header.appendChild(this.chevron); this.el.appendChild(this.header); this.body = document.createElement('div'); this.body.className = 'flex flex-col'; this.el.appendChild(this.body); - this.isOpen = true; this.toolCount = 0; this.toolItems = new Map(); - - messagesContainer.appendChild(this.el); - } - - toggle() { - this.isOpen = !this.isOpen; - this.body.style.display = this.isOpen ? '' : 'none'; - - const svg = this.chevron.querySelector('svg'); - if (svg) { - svg.style.transform = this.isOpen ? 'rotate(0deg)' : 'rotate(-90deg)'; - } - } - - close() { - this.toggle(false); } updateTitle() { - this.title.textContent = `Exploring ${this.toolCount} tools...`; + this.title.textContent = `Tools (${this.toolCount})`; } addOrUpdateTool(toolCallId, update) { @@ -2181,17 +2253,24 @@ // carries the complete replacement list, so the card is rebuilt in place. function handlePlanUpdate(update) { const entries = Array.isArray(update.entries) ? update.entries : []; - let card = document.getElementById(`plan-card-${agentTurnId}`); + const planId = `plan-card-${agentTurnId}`; + let card = document.getElementById(planId); + const owner = workSummaryByPlanId.get(planId); if (entries.length === 0) { - if (card) card.remove(); + if (card) { + if (owner) owner.removeActivity(card); + else card.remove(); + } + workSummaryByPlanId.delete(planId); return; } if (!card) { endCurrentTextBlock(); + const summary = ensureCurrentWorkSummary(); card = document.createElement('div'); - card.id = `plan-card-${agentTurnId}`; + card.id = planId; card.className = 'my-3 border border-vscode-widget-border rounded bg-vscode-widget-bg overflow-hidden shrink-0'; const header = document.createElement('div'); @@ -2212,8 +2291,8 @@ body.className = 'plan-body flex flex-col'; card.appendChild(body); - messagesContainer.appendChild(card); - scrollToBottom(); + summary.addStandaloneActivity(card); + workSummaryByPlanId.set(planId, summary); } const body = card.querySelector('.plan-body'); @@ -2250,30 +2329,30 @@ function handleToolCallUpdate(update) { const toolCallId = update.toolCallId || (update.toolCall && update.toolCall.toolCallId); if (!toolCallId) return; + const existingCard = document.getElementById(`tool-card-${toolCallId}`); + const summary = workSummaryByToolCallId.get(toolCallId) || ensureCurrentWorkSummary(); + summary.endThought(); // A new card is about to be inserted below the current text block - // close the block so any text streamed after the tool starts fresh // below the card instead of appending to the paragraph above it. - if (!document.getElementById(`tool-card-${toolCallId}`)) { + if (!existingCard) { endCurrentTextBlock(); } if (update.kind) toolKinds.set(toolCallId, update.kind); if (toolKinds.get(toolCallId) === 'edit') { - let card = document.getElementById(`tool-card-${toolCallId}`); + let card = existingCard; if (!card) { card = createEditCard(toolCallId, update); - messagesContainer.appendChild(card); - scrollToBottom(); + summary.addStandaloneActivity(card); } updateEditCard(card, update); } else { - if (!currentAggregator) { - currentAggregator = new ToolAggregator(); - } - currentAggregator.addOrUpdateTool(toolCallId, update); + summary.addOrUpdateTool(toolCallId, update); } + workSummaryByToolCallId.set(toolCallId, summary); } // Verb per tool for the aggregated tool list. An entry only fires when the @@ -2468,6 +2547,7 @@ function handlePermissionRequested(toolCallId, toolCall, options) { const card = document.getElementById(`tool-card-${toolCallId}`); if (!card) return; + workSummaryByToolCallId.get(toolCallId)?.openForInteraction(); // Check if actions already exist if (card.querySelector('.tool-actions')) return; @@ -2531,9 +2611,11 @@ const actions = card.querySelector('.tool-actions'); if (actions) actions.remove(); - // Tool cards keep their status in .tool-status, edit cards in .ml-auto. - const statusEl = card.querySelector('.tool-status, .ml-auto'); - if (statusEl) statusEl.innerHTML = ICONS.cancelled; + const statusEl = card.querySelector('.tool-status'); + if (statusEl) { + statusEl.innerHTML = ICONS.cancelled; + statusEl.dataset.status = 'cancelled'; + } } } diff --git a/plugins/vscode/src/acp-client.spec.ts b/plugins/vscode/src/acp-client.spec.ts index 51ef880b3..302caa72d 100644 --- a/plugins/vscode/src/acp-client.spec.ts +++ b/plugins/vscode/src/acp-client.spec.ts @@ -100,9 +100,10 @@ test('NanocoderAcpClient - a cancelled prompt does not raise an error toast', as // Cancel mid-flight, then the agent tears the stream down and prompt rejects. await client.cancel(); rejectPrompt(new Error('Operation was cancelled')); - await promptPromise; + const cancelledResult = await promptPromise; t.is(shownError, undefined, 'Cancelling is not a failure the user needs to be told about'); + t.is(cancelledResult?.stopReason, 'cancelled', 'The webview needs the cancelled terminal state'); // The flag must not leak into the next turn, or a real failure goes unreported. rejectPrompt = () => {}; diff --git a/plugins/vscode/src/acp-client.ts b/plugins/vscode/src/acp-client.ts index 0354c5160..b74cd0837 100644 --- a/plugins/vscode/src/acp-client.ts +++ b/plugins/vscode/src/acp-client.ts @@ -328,7 +328,9 @@ export class NanocoderAcpClient { }); } catch (error) { this.outputChannel.appendLine(`Prompt failed: ${error}`); - if (!this.cancelRequested) { + if (this.cancelRequested) { + return {stopReason: 'cancelled'}; + } else { vscode.window.showErrorMessage(`Nanocoder prompt failed: ${error}`); } return undefined; diff --git a/plugins/vscode/src/chat-webview-provider.ts b/plugins/vscode/src/chat-webview-provider.ts index 0341304bf..667fa6c6c 100644 --- a/plugins/vscode/src/chat-webview-provider.ts +++ b/plugins/vscode/src/chat-webview-provider.ts @@ -599,6 +599,7 @@ export class ChatWebviewProvider const sessionId = await this._acpClient.getOrCreateSession(cwd); if (!sessionId) { vscode.window.showErrorMessage('Nanocoder: Failed to create ACP session.'); + this.postMessage({type: 'acpUpdate', update: {sessionUpdate: 'prompt_response', outcome: 'failed'}}); return; } @@ -612,6 +613,11 @@ export class ChatWebviewProvider }); const response = await this._acpClient.prompt(expandedText, images); + const outcome = response?.stopReason === 'cancelled' + ? 'cancelled' + : response + ? 'completed' + : 'failed'; // Signal turn completion so the Webview can flip back to the send // button. Forward the per-turn token usage and estimated cost so // the webview can render the usage indicator under the response. @@ -619,6 +625,7 @@ export class ChatWebviewProvider type: 'acpUpdate', update: { sessionUpdate: 'prompt_response', + outcome, usage: response?.usage, cost: (response?._meta as Record | undefined)?.['nanocoder/usage']?.cost, }, @@ -627,7 +634,7 @@ export class ChatWebviewProvider this._outputChannel.appendLine(`Prompt execution error: ${error}`); vscode.window.showErrorMessage(`Nanocoder Prompt error: ${error}`); // Always reset the button even on error - this.postMessage({type: 'acpUpdate', update: {sessionUpdate: 'prompt_response'}}); + this.postMessage({type: 'acpUpdate', update: {sessionUpdate: 'prompt_response', outcome: 'failed'}}); } } diff --git a/source/vscode/chat-panel-harness.ts b/source/vscode/chat-panel-harness.ts index dfd481b5f..1f12ab1b3 100644 --- a/source/vscode/chat-panel-harness.ts +++ b/source/vscode/chat-panel-harness.ts @@ -52,9 +52,9 @@ const SHELL_IDS = [ 'send-stop-btn', ]; -// biome-ignore lint/suspicious/noExplicitAny: the panel assigns arbitrary // properties (onclick, oninput, ...) to the nodes it builds, so the stub has to // stay open-ended. +// biome-ignore lint/suspicious/noExplicitAny: dynamic DOM fixture export type StubElement = any; /** @@ -108,8 +108,11 @@ export function createElement(tagName: string): StubElement { }, }, appendChild(child: StubElement) { + if (child.parentElement && child.parentElement !== element) { + child.parentElement.removeChild(child); + } child.parentElement = element; - element.children.push(child); + if (!element.children.includes(child)) element.children.push(child); return child; }, removeChild(child: StubElement) { @@ -149,6 +152,7 @@ export function createElement(tagName: string): StubElement { get: () => html, set: (value: string) => { html = String(value); + for (const child of element.children) child.parentElement = null; element.children = []; }, }); @@ -281,8 +285,8 @@ export function createPanel(options: {marked?: boolean} = {}) { status: 'pending', }); }, - finish() { - this.update({sessionUpdate: 'prompt_response'}); + finish(outcome = 'completed') { + this.update({sessionUpdate: 'prompt_response', outcome}); }, userMessage(value: string) { this.update({ @@ -299,10 +303,13 @@ export function createPanel(options: {marked?: boolean} = {}) { timer.fn(); } }, - boxes(): StubElement[] { + summaries(): StubElement[] { return container.children.filter((child: StubElement) => - child.className.includes('thought-aggregator'), + child.className.includes('work-summary'), ); }, + thoughts(): StubElement[] { + return container.querySelectorAll('.work-summary-thought'); + }, }; } diff --git a/source/vscode/chat-panel-thoughts.spec.ts b/source/vscode/chat-panel-thoughts.spec.ts index 3ea6ba8bb..73e02e5ad 100644 --- a/source/vscode/chat-panel-thoughts.spec.ts +++ b/source/vscode/chat-panel-thoughts.spec.ts @@ -1,212 +1,303 @@ import test from 'ava'; import {createPanel, type StubElement} from '@/vscode/chat-panel-harness'; -const titleOf = (box: StubElement) => box.children[0].children[0].textContent; -const bodyOf = (box: StubElement) => box.children[1].textContent; -const isOpen = (box: StubElement) => box.children[1].style.display !== 'none'; -const clickHeader = (box: StubElement) => box.children[0].onclick(); - -test('groups every thought of one response into a single section', t => { +const titleOf = (summary: StubElement) => + summary.children[0].children[0].textContent; +const bodyOf = (summary: StubElement) => summary.children[1]; +const thoughtText = (thought: StubElement) => thought.children[1].textContent; +const isOpen = (summary: StubElement) => + bodyOf(summary).style.display !== 'none'; +const clickHeader = (summary: StubElement) => summary.children[0].onclick(); + +const activityKind = (element: StubElement) => { + if (element.classList.contains('work-summary-thought')) return 'thought'; + if (element.classList.contains('tool-aggregator')) return 'tools'; + if (element.classList.contains('tool-card')) return 'edit'; + if (String(element.id).startsWith('plan-card-')) return 'plan'; + return 'unknown'; +}; + +const isInside = (element: StubElement, ancestor: StubElement) => { + let current = element.parentElement; + while (current) { + if (current === ancestor) return true; + current = current.parentElement; + } + return false; +}; + +test('groups one turn execution activity into a single ordered summary', t => { const panel = createPanel(); - panel.thought('first thought'); - panel.text('some answer'); - panel.tool('call-1'); - panel.thought('second thought'); - panel.text('more answer'); - panel.thought('third thought'); + panel.userMessage('please inspect and edit the project'); + panel.thought('inspect first'); + panel.update({ + sessionUpdate: 'tool_call', + toolCallId: 'read-1', + title: 'read_file: source/a.ts', + kind: 'read', + status: 'pending', + }); + panel.text('I found the relevant file.'); + panel.thought('now edit it'); + panel.update({ + sessionUpdate: 'tool_call', + toolCallId: 'edit-1', + title: 'write_file: source/a.ts', + kind: 'edit', + status: 'pending', + }); + panel.update({ + sessionUpdate: 'plan', + entries: [{content: 'Run tests', status: 'in_progress'}], + }); panel.finish(); - const boxes = panel.boxes(); - t.is(boxes.length, 1); - t.true(bodyOf(boxes[0]).includes('first thought')); - t.true(bodyOf(boxes[0]).includes('second thought')); - t.true(bodyOf(boxes[0]).includes('third thought')); + const summaries = panel.summaries(); + t.is(summaries.length, 1); + t.deepEqual(bodyOf(summaries[0]).children.map(activityKind), [ + 'thought', + 'tools', + 'thought', + 'edit', + 'plan', + ]); }); -test('counts only the stretches actually spent thinking', t => { +test('does not merge tool groups across intervening thoughts', t => { const panel = createPanel(); - - panel.thought('reasoning'); - panel.advance(3_000); - panel.text('answer'); - panel.advance(60_000); - panel.tool('call-1'); - panel.advance(30_000); - panel.thought('more reasoning'); - panel.advance(2_500); - panel.finish(); - - t.is(titleOf(panel.boxes()[0]), 'Thought for 5s'); + panel.userMessage('inspect two areas'); + + panel.update({ + sessionUpdate: 'tool_call', + toolCallId: 'first', + title: 'read_file: source/first.ts', + kind: 'read', + status: 'pending', + }); + panel.thought('the second area needs a separate check'); + panel.update({ + sessionUpdate: 'tool_call_update', + toolCallId: 'first', + status: 'completed', + }); + panel.update({ + sessionUpdate: 'tool_call', + toolCallId: 'second', + title: 'read_file: source/second.ts', + kind: 'read', + status: 'pending', + }); + + const groups = bodyOf(panel.summaries()[0]).children.filter( + (child: StubElement) => child.classList.contains('tool-aggregator'), + ); + t.is(groups.length, 2); + t.deepEqual( + groups[0] + .querySelectorAll('.tool-label') + .map((label: StubElement) => label.textContent), + ['Reading source/first.ts'], + ); + t.deepEqual( + groups[1] + .querySelectorAll('.tool-label') + .map((label: StubElement) => label.textContent), + ['Reading source/second.ts'], + ); }); -test('separates interrupted thought stretches with a blank line', t => { +test('keeps streaming chunks together and starts a new thought after work', t => { const panel = createPanel(); - panel.thought('first'); - panel.text('answer'); - panel.thought('second'); - panel.finish(); + panel.thought('let me '); + panel.thought('check that'); + panel.tool('call-1'); + panel.thought('one more detail'); - t.is(bodyOf(panel.boxes()[0]), 'first\n\nsecond'); + const thoughts = panel.thoughts(); + t.is(thoughts.length, 2); + t.is(thoughtText(thoughts[0]), 'let me check that'); + t.is(thoughtText(thoughts[1]), 'one more detail'); }); -test('keeps consecutive chunks of one stretch unseparated', t => { +test('measures the whole turn rather than only thought stretches', t => { const panel = createPanel(); - panel.thought('let me '); - panel.thought('check that'); - panel.finish(); + panel.userMessage('do the work'); + panel.advance(5_000); + panel.thought('reasoning'); + panel.runTimers(); - t.is(bodyOf(panel.boxes()[0]), 'let me check that'); -}); + const summary = panel.summaries()[0]; + t.is(titleOf(summary), 'Working for 5s'); -test('starts a new section for the next response', t => { - const panel = createPanel(); + panel.text('progress update'); + panel.advance(60_000); + panel.runTimers(); + t.is(titleOf(summary), 'Working for 1m 5s'); - panel.thought('first response thought'); - panel.text('first answer'); panel.finish(); - panel.thought('second response thought'); - panel.finish(); - - const boxes = panel.boxes(); - t.is(boxes.length, 2); - t.is(bodyOf(boxes[0]), 'first response thought'); - t.is(bodyOf(boxes[1]), 'second response thought'); + t.is(titleOf(summary), 'Worked for 1m 5s'); }); -test('collapses while paused and reopens when thoughts resume', t => { +test('stays open while streaming and collapses when the turn completes', t => { const panel = createPanel(); + panel.userMessage('run a task'); panel.thought('reasoning'); - const box = panel.boxes()[0]; - t.true(isOpen(box)); - t.is(titleOf(box), 'Thinking...'); + const summary = panel.summaries()[0]; + t.true(isOpen(summary)); - panel.advance(2_000); - panel.text('answer'); - t.false(isOpen(box)); - t.is(titleOf(box), 'Thought for 2s'); - - panel.thought('more reasoning'); - t.true(isOpen(box)); - t.is(titleOf(box), 'Thinking for 2s'); - - panel.advance(1_000); panel.finish(); - t.false(isOpen(box)); - t.is(titleOf(box), 'Thought for 3s'); + t.false(isOpen(summary)); + t.is(summary.children[0].getAttribute('aria-expanded'), 'false'); + + clickHeader(summary); + t.true(isOpen(summary)); + clickHeader(summary); + t.false(isOpen(summary)); }); -test('respects a manual toggle for the rest of the response', t => { +test('keeps the final answer outside the work summary', t => { const panel = createPanel(); - panel.thought('reasoning'); - const box = panel.boxes()[0]; - clickHeader(box); - t.false(isOpen(box)); + panel.userMessage('answer after checking'); + panel.thought('checking'); + panel.tool('call-1'); + panel.text('This is the final answer.'); + panel.finish(); - panel.thought('still reasoning'); - t.false(isOpen(box)); + const summary = panel.summaries()[0]; + const answer = panel.container.querySelector('.agent-markdown'); + t.false(isInside(answer, summary)); + t.is(bodyOf(summary).querySelectorAll('.agent-markdown').length, 0); + t.true( + panel.container.children.indexOf(summary) < + panel.container.children.indexOf(answer.parentElement.parentElement), + ); +}); - panel.text('answer'); - t.false(isOpen(box)); +test('does not leave an empty summary for a direct answer', t => { + const panel = createPanel(); - clickHeader(box); - panel.thought('more reasoning'); + panel.userMessage('say hello'); + panel.text('Hello.'); panel.finish(); - t.true(isOpen(box)); + + t.is(panel.summaries().length, 0); + t.is(panel.container.querySelectorAll('.agent-markdown').length, 1); }); -test('keeps the live timer running while thoughts stream', t => { - const panel = createPanel(); +test('labels cancelled and failed terminal states', t => { + const cancelled = createPanel(); + cancelled.userMessage('stop this'); + cancelled.thought('working'); + cancelled.advance(2_000); + cancelled.finish('cancelled'); + + const cancelledSummary = cancelled.summaries()[0]; + t.is(titleOf(cancelledSummary), 'Stopped after 2s'); + t.is(cancelledSummary.dataset.outcome, 'cancelled'); + + const failed = createPanel(); + failed.userMessage('this will fail'); + failed.tool('call-1'); + failed.advance(3_000); + failed.finish('failed'); + + const failedSummary = failed.summaries()[0]; + t.is(titleOf(failedSummary), 'Failed after 3s'); + t.is(failedSummary.dataset.outcome, 'failed'); +}); - panel.thought('reasoning'); - panel.advance(4_000); - panel.runTimers(); - t.is(titleOf(panel.boxes()[0]), 'Thinking for 4s'); +test('keeps late tool updates in their original cancelled turn', t => { + const panel = createPanel(); - panel.text('answer'); - panel.advance(10_000); - panel.runTimers(); - t.is(titleOf(panel.boxes()[0]), 'Thought for 4s'); + panel.userMessage('start the first task'); + panel.tool('first-tool'); + panel.finish('cancelled'); + + panel.userMessage('start another task'); + panel.thought('new '); + panel.update({ + sessionUpdate: 'tool_call_update', + toolCallId: 'first-tool', + status: 'failed', + rawOutput: 'Cancelled by user', + }); + panel.thought('thought'); + + const summaries = panel.summaries(); + t.is(summaries.length, 2); + t.is(titleOf(summaries[0]), 'Stopped after 0s'); + t.is(summaries[0].querySelectorAll('.tool-label').length, 1); + t.is(summaries[1].querySelectorAll('.work-summary-thought').length, 1); + t.is(thoughtText(summaries[1].querySelector('.work-summary-thought')), 'new thought'); }); -test('renders answer text and tool cards after the thought section', t => { +test('reopens the summary when a tool needs approval', t => { const panel = createPanel(); - panel.thought('reasoning'); - panel.text('answer'); + panel.userMessage('inspect a file'); panel.tool('call-1'); - panel.thought('more reasoning'); - panel.finish(); - - t.is(panel.boxes().length, 1); - t.true(panel.container.children[0].className.includes('thought-aggregator')); - t.true(panel.container.children.length > 2); + const summary = panel.summaries()[0]; + clickHeader(summary); + t.false(isOpen(summary)); + + panel.post({ + type: 'permissionRequested', + toolCallId: 'call-1', + toolCall: {}, + }); + + t.true(isOpen(summary)); + t.truthy(bodyOf(summary).querySelector('.tool-actions')); }); -test('renders the grouped thoughts as markdown when marked is loaded', t => { +test('renders thoughts as markdown when marked is loaded', t => { const panel = createPanel({marked: true}); - panel.thought('first'); - const box = panel.boxes()[0]; - t.is(box.children[1].innerHTML, ''); + panel.thought('**inspect** the code'); + const thought = panel.thoughts()[0]; + t.is(thought.children[1].innerHTML, ''); panel.runTimers(); - t.is(box.children[1].innerHTML, 'first'); - - panel.text('answer'); - panel.thought('second'); - panel.finish(); - t.is(box.children[1].innerHTML, 'first\n\nsecond'); + t.is(thought.children[1].innerHTML, '**inspect** the code'); }); -test('keeps one section per response when a session is replayed', t => { +test('keeps one summary per turn when a session is replayed', t => { const panel = createPanel(); panel.post({type: 'clear', isLoading: true}); panel.userMessage('first question'); - panel.thought('first response thought'); + panel.thought('first thought'); panel.text('first answer'); panel.userMessage('second question'); - panel.thought('second response thought'); + panel.tool('second-tool'); panel.text('second answer'); panel.post({type: 'sessionLoaded'}); - const boxes = panel.boxes(); - t.is(boxes.length, 2); - t.is(bodyOf(boxes[0]), 'first response thought'); - t.is(bodyOf(boxes[1]), 'second response thought'); + const summaries = panel.summaries(); + t.is(summaries.length, 2); + t.is(thoughtText(summaries[0].querySelector('.work-summary-thought')), 'first thought'); + t.is(summaries[1].querySelectorAll('.tool-label').length, 1); + t.false(isOpen(summaries[0])); + t.false(isOpen(summaries[1])); }); -test('closes a still-open section once the replayed session is loaded', t => { +test('drops the active summary when the session is cleared', t => { const panel = createPanel(); panel.userMessage('question'); - panel.thought('trailing thought'); - panel.advance(6_000); - panel.post({type: 'sessionLoaded'}); - - const box = panel.boxes()[0]; - t.is(titleOf(box), 'Thought for 6s'); - t.false(isOpen(box)); - - panel.advance(60_000); - panel.runTimers(); - t.is(titleOf(box), 'Thought for 6s'); -}); - -test('drops the section when the session is cleared', t => { - const panel = createPanel(); - panel.thought('reasoning'); + t.is(panel.summaries().length, 1); + panel.post({type: 'clear'}); - t.is(panel.boxes().length, 0); + t.is(panel.summaries().length, 0); + panel.userMessage('fresh question'); panel.thought('fresh reasoning'); - const boxes = panel.boxes(); - t.is(boxes.length, 1); - t.is(bodyOf(boxes[0]), 'fresh reasoning'); + t.is(panel.summaries().length, 1); + t.is(thoughtText(panel.thoughts()[0]), 'fresh reasoning'); });