diff --git a/docs/docs/config.mdx b/docs/docs/config.mdx
index aad9e7d8c6..ff1a87db80 100644
--- a/docs/docs/config.mdx
+++ b/docs/docs/config.mdx
@@ -67,6 +67,7 @@ wsh editconfig
| term:macoptionismeta | bool | on macOS, treat the Option key as Meta key for terminal keybindings (default false) |
| term:bellsound | bool | when enabled, plays the system beep sound when the terminal bell (BEL character) is received (default false) |
| term:bellindicator | bool | when enabled, shows a visual indicator in the tab when the terminal bell is received (default false) |
+| term:exitindicator | bool | when enabled, shows a colored tab indicator when a process exits — green for success, red for error (default false) |
| term:durable | bool | makes remote terminal sessions durable across network disconnects (defaults to true) |
| editor:minimapenabled | bool | set to false to disable editor minimap |
| editor:stickyscrollenabled | bool | enables monaco editor's stickyScroll feature (pinning headers of current context, e.g. class names, method names, etc.), defaults to false |
@@ -134,6 +135,7 @@ For reference, this is the current default configuration (v0.11.5):
"telemetry:enabled": true,
"term:bellsound": false,
"term:bellindicator": false,
+ "term:exitindicator": false,
"term:copyonselect": true,
"term:durable": true,
"waveai:showcloudmodes": true,
diff --git a/frontend/app/tab/tab.scss b/frontend/app/tab/tab.scss
index 3739752eee..211f996aa3 100644
--- a/frontend/app/tab/tab.scss
+++ b/frontend/app/tab/tab.scss
@@ -1,6 +1,28 @@
// Copyright 2024, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
+// Animate a shared breathing phase on :root so all indicator tabs pulse in sync.
+// Uses CSS @property for an animatable custom property (Chromium 78+).
+@property --breathe-phase {
+ syntax: "";
+ initial-value: 0;
+ inherits: true;
+}
+
+:root {
+ animation: indicatorBreathePhase 16s ease-in-out infinite;
+}
+
+@keyframes indicatorBreathePhase {
+ 0%,
+ 100% {
+ --breathe-phase: 0;
+ }
+ 50% {
+ --breathe-phase: 1;
+ }
+}
+
.tab {
position: absolute;
width: 130px;
@@ -96,6 +118,24 @@
transition: none !important;
}
+ &.has-indicator {
+ .tab-inner {
+ background: rgb(from var(--tab-indicator-color) r g b / 0.15);
+ }
+ &.active .tab-inner {
+ background: rgb(from var(--tab-indicator-color) r g b / 0.2);
+ }
+ }
+
+ &.indicator-breathing {
+ .tab-inner {
+ background: rgb(from var(--tab-indicator-color) r g b / calc(0.08 + var(--breathe-phase) * 0.14));
+ }
+ &.active .tab-inner {
+ background: rgb(from var(--tab-indicator-color) r g b / calc(0.12 + var(--breathe-phase) * 0.14));
+ }
+ }
+
.wave-button {
position: absolute;
top: 50%;
@@ -129,6 +169,18 @@ body:not(.nohover) .tab.dragging {
border-color: transparent;
background: rgb(from var(--main-text-color) r g b / 0.1);
}
+ &.has-indicator .tab-inner {
+ background: rgb(from var(--tab-indicator-color) r g b / 0.15);
+ }
+ &.has-indicator.active .tab-inner {
+ background: rgb(from var(--tab-indicator-color) r g b / 0.2);
+ }
+ &.indicator-breathing .tab-inner {
+ background: rgb(from var(--tab-indicator-color) r g b / calc(0.08 + var(--breathe-phase) * 0.14));
+ }
+ &.indicator-breathing.active .tab-inner {
+ background: rgb(from var(--tab-indicator-color) r g b / calc(0.12 + var(--breathe-phase) * 0.14));
+ }
.close {
visibility: visible;
&:hover {
diff --git a/frontend/app/tab/tab.tsx b/frontend/app/tab/tab.tsx
index 9a3d0f9925..814cae3d31 100644
--- a/frontend/app/tab/tab.tsx
+++ b/frontend/app/tab/tab.tsx
@@ -224,7 +224,14 @@ const Tab = memo(
dragging: isDragging,
"before-active": isBeforeActive,
"new-tab": isNew,
+ "has-indicator": indicator != null,
+ "indicator-breathing": indicator != null && indicator.icon === "none",
})}
+ style={
+ indicator != null
+ ? ({ "--tab-indicator-color": indicator.color || "#f59e0b" } as React.CSSProperties)
+ : undefined
+ }
onMouseDown={onDragStart}
onClick={handleTabClick}
onContextMenu={handleContextMenu}
@@ -242,7 +249,7 @@ const Tab = memo(
>
{tabData?.name}
- {indicator && (
+ {indicator && indicator.icon !== "none" && (