Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions docs/docs/config.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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 <VersionBadge version="v0.14" /> | bool | when enabled, plays the system beep sound when the terminal bell (BEL character) is received (default false) |
| term:bellindicator <VersionBadge version="v0.14" /> | bool | when enabled, shows a visual indicator in the tab when the terminal bell is received (default false) |
| term:exitindicator <VersionBadge version="v0.14" /> | bool | when enabled, shows a colored tab indicator when a process exits — green for success, red for error (default false) |
| term:durable <VersionBadge version="v0.14" /> | 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 |
Expand Down Expand Up @@ -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,
Expand Down
52 changes: 52 additions & 0 deletions frontend/app/tab/tab.scss
Original file line number Diff line number Diff line change
@@ -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: "<number>";
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;
Expand Down Expand Up @@ -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%;
Expand Down Expand Up @@ -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 {
Expand Down
9 changes: 8 additions & 1 deletion frontend/app/tab/tab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Mismatched amber fallback colors between background glow and icon.

Line 232 uses #f59e0b (amber-500) for the background glow fallback, while Line 255 uses #fbbf24 (amber-400) for the icon color fallback. These should likely be the same value for visual consistency when no explicit indicator.color is provided.

🐛 Proposed fix
-                                style={{ color: indicator.color || "#fbbf24" }}
+                                style={{ color: indicator.color || "#f59e0b" }}

Or extract a shared constant:

const DEFAULT_INDICATOR_COLOR = "#f59e0b";

Also applies to: 255-255

🤖 Prompt for AI Agents
In `@frontend/app/tab/tab.tsx` at line 232, The two fallback hex colors for the
tab indicator are inconsistent (one uses "#f59e0b" and the other "#fbbf24");
change both places to use a single shared constant (e.g.,
DEFAULT_INDICATOR_COLOR) and replace the inline fallbacks in the expressions
that read indicator.color || ... so both the background glow and icon color use
the same fallback value; update usages where the ternary/OR fallback appears
(the indicator color expressions) to reference that constant.

: undefined
}
onMouseDown={onDragStart}
onClick={handleTabClick}
onContextMenu={handleContextMenu}
Expand All @@ -242,7 +249,7 @@ const Tab = memo(
>
{tabData?.name}
</div>
{indicator && (
{indicator && indicator.icon !== "none" && (
<div
className="tab-indicator pointer-events-none"
style={{ color: indicator.color || "#fbbf24" }}
Expand Down
2 changes: 2 additions & 0 deletions frontend/types/gotypes.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1081,6 +1081,7 @@ declare global {
"term:conndebug"?: string;
"term:bellsound"?: boolean;
"term:bellindicator"?: boolean;
"term:exitindicator"?: boolean;
"term:durable"?: boolean;
"web:zoom"?: number;
"web:hidenav"?: boolean;
Expand Down Expand Up @@ -1267,6 +1268,7 @@ declare global {
"term:macoptionismeta"?: boolean;
"term:bellsound"?: boolean;
"term:bellindicator"?: boolean;
"term:exitindicator"?: boolean;
"term:durable"?: boolean;
"editor:minimapenabled"?: boolean;
"editor:stickyscrollenabled"?: boolean;
Expand Down
99 changes: 99 additions & 0 deletions pkg/blockcontroller/shellcontroller.go
Original file line number Diff line number Diff line change
Expand Up @@ -526,6 +526,34 @@ func (bc *ShellController) manageRunningShellProcess(shellProc *shellexec.ShellP
shellInputCh := make(chan *BlockInputUnion, 32)
bc.ShellInputCh = shellInputCh

// Fire amber "running" indicator for cmd blocks
if bc.ControllerType == BlockController_Cmd {
exitIndicatorEnabled := blockMeta.GetBool(waveobj.MetaKey_TermExitIndicator, false)
if !blockMeta.HasKey(waveobj.MetaKey_TermExitIndicator) {
if globalVal := wconfig.GetWatcher().GetFullConfig().Settings.TermExitIndicator; globalVal != nil {
exitIndicatorEnabled = *globalVal
}
}
if exitIndicatorEnabled {
indicator := wshrpc.TabIndicator{
Icon: "spinner+spin",
Color: "#f59e0b",
Priority: 1.5,
ClearOnFocus: false,
}
eventData := wshrpc.TabIndicatorEventData{
TabId: bc.TabId,
Indicator: &indicator,
}
event := wps.WaveEvent{
Event: wps.Event_TabIndicator,
Scopes: []string{waveobj.MakeORef(waveobj.OType_Tab, bc.TabId).String()},
Data: eventData,
}
wps.Broker.Publish(event)
}
}
Comment on lines +529 to +555
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Running indicator logic looks good. Global settings fallback is correctly implemented, and the indicator is appropriately scoped to BlockController_Cmd blocks only.

One edge case: if the user disables term:exitindicator while a command is still running, the exit goroutine (line 664) returns early without clearing this amber spinner—leaving it stuck on the tab. Consider unconditionally clearing the running indicator for Cmd blocks before the exitIndicatorEnabled early-return:

Proposed fix
+			// Always clear the running indicator for cmd blocks
+			if blockData.Meta.GetString(waveobj.MetaKey_Controller, "") == BlockController_Cmd {
+				clearEvent := wps.WaveEvent{
+					Event:  wps.Event_TabIndicator,
+					Scopes: []string{waveobj.MakeORef(waveobj.OType_Tab, bc.TabId).String()},
+					Data:   wshrpc.TabIndicatorEventData{TabId: bc.TabId, Indicator: nil},
+				}
+				wps.Broker.Publish(clearEvent)
+			}
 			if !exitIndicatorEnabled {
 				return
 			}


go func() {
// handles regular output from the pty (goes to the blockfile and xterm)
defer func() {
Expand Down Expand Up @@ -616,6 +644,77 @@ func (bc *ShellController) manageRunningShellProcess(shellProc *shellexec.ShellP
msg = fmt.Sprintf("%s (exit code %d)", baseMsg, exitCode)
}
bc.writeMutedMessageToTerminal("[" + msg + "]")
go func(exitCode int, exitSignal string) {
defer func() {
panichandler.PanicHandler("blockcontroller:exit-indicator", recover())
}()
ctx, cancelFn := context.WithTimeout(context.Background(), DefaultTimeout)
defer cancelFn()
blockData, err := wstore.DBMustGet[*waveobj.Block](ctx, bc.BlockId)
if err != nil {
log.Printf("error getting block data for exit indicator: %v\n", err)
return
}
exitIndicatorEnabled := blockData.Meta.GetBool(waveobj.MetaKey_TermExitIndicator, false)
if !blockData.Meta.HasKey(waveobj.MetaKey_TermExitIndicator) {
if globalVal := wconfig.GetWatcher().GetFullConfig().Settings.TermExitIndicator; globalVal != nil {
exitIndicatorEnabled = *globalVal
}
}
if !exitIndicatorEnabled {
return
}
closeOnExit := blockData.Meta.GetBool(waveobj.MetaKey_CmdCloseOnExit, false)
closeOnExitForce := blockData.Meta.GetBool(waveobj.MetaKey_CmdCloseOnExitForce, false)
if closeOnExitForce || (closeOnExit && exitCode == 0) {
// Clear running indicator before block gets deleted
if bc.ControllerType == BlockController_Cmd {
clearEvent := wps.WaveEvent{
Event: wps.Event_TabIndicator,
Scopes: []string{waveobj.MakeORef(waveobj.OType_Tab, bc.TabId).String()},
Data: wshrpc.TabIndicatorEventData{TabId: bc.TabId, Indicator: nil},
}
wps.Broker.Publish(clearEvent)
}
return
}
// Clear running indicator before exit indicator to prevent
// PersistentIndicator from resurrecting the amber glow
if bc.ControllerType == BlockController_Cmd {
clearEvent := wps.WaveEvent{
Event: wps.Event_TabIndicator,
Scopes: []string{waveobj.MakeORef(waveobj.OType_Tab, bc.TabId).String()},
Data: wshrpc.TabIndicatorEventData{TabId: bc.TabId, Indicator: nil},
}
wps.Broker.Publish(clearEvent)
}
var indicator wshrpc.TabIndicator
if exitCode == 0 && exitSignal == "" {
indicator = wshrpc.TabIndicator{
Icon: "check",
Color: "#4ade80",
Priority: 2,
ClearOnFocus: true,
}
} else {
indicator = wshrpc.TabIndicator{
Icon: "xmark-large",
Color: "#f87171",
Priority: 2,
ClearOnFocus: true,
}
}
eventData := wshrpc.TabIndicatorEventData{
TabId: bc.TabId,
Indicator: &indicator,
}
event := wps.WaveEvent{
Event: wps.Event_TabIndicator,
Scopes: []string{waveobj.MakeORef(waveobj.OType_Tab, bc.TabId).String()},
Data: eventData,
}
wps.Broker.Publish(event)
}(exitCode, exitSignal)
go checkCloseOnExit(bc.BlockId, exitCode)
}()
return nil
Expand Down
1 change: 1 addition & 0 deletions pkg/waveobj/metaconsts.go
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@ const (
MetaKey_TermConnDebug = "term:conndebug"
MetaKey_TermBellSound = "term:bellsound"
MetaKey_TermBellIndicator = "term:bellindicator"
MetaKey_TermExitIndicator = "term:exitindicator"
MetaKey_TermDurable = "term:durable"

MetaKey_WebZoom = "web:zoom"
Expand Down
1 change: 1 addition & 0 deletions pkg/waveobj/wtypemeta.go
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,7 @@ type MetaTSType struct {
TermConnDebug string `json:"term:conndebug,omitempty"` // null, info, debug
TermBellSound *bool `json:"term:bellsound,omitempty"`
TermBellIndicator *bool `json:"term:bellindicator,omitempty"`
TermExitIndicator *bool `json:"term:exitindicator,omitempty"`
TermDurable *bool `json:"term:durable,omitempty"`

WebZoom float64 `json:"web:zoom,omitempty"`
Expand Down
1 change: 1 addition & 0 deletions pkg/wconfig/defaultconfig/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
"telemetry:enabled": true,
"term:bellsound": false,
"term:bellindicator": false,
"term:exitindicator": false,
"term:copyonselect": true,
"term:durable": false,
"waveai:showcloudmodes": true,
Expand Down
1 change: 1 addition & 0 deletions pkg/wconfig/metaconsts.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ const (
ConfigKey_TermMacOptionIsMeta = "term:macoptionismeta"
ConfigKey_TermBellSound = "term:bellsound"
ConfigKey_TermBellIndicator = "term:bellindicator"
ConfigKey_TermExitIndicator = "term:exitindicator"
ConfigKey_TermDurable = "term:durable"

ConfigKey_EditorMinimapEnabled = "editor:minimapenabled"
Expand Down
1 change: 1 addition & 0 deletions pkg/wconfig/settingsconfig.go
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ type SettingsType struct {
TermMacOptionIsMeta *bool `json:"term:macoptionismeta,omitempty"`
TermBellSound *bool `json:"term:bellsound,omitempty"`
TermBellIndicator *bool `json:"term:bellindicator,omitempty"`
TermExitIndicator *bool `json:"term:exitindicator,omitempty"`
TermDurable *bool `json:"term:durable,omitempty"`

EditorMinimapEnabled bool `json:"editor:minimapenabled,omitempty"`
Expand Down
3 changes: 3 additions & 0 deletions schema/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,9 @@
"term:bellindicator": {
"type": "boolean"
},
"term:exitindicator": {
"type": "boolean"
},
"term:durable": {
"type": "boolean"
},
Expand Down