From 4ac5d93ed2fb8727e2868dc40f190f507c0a971a Mon Sep 17 00:00:00 2001 From: Evan Simkowitz Date: Wed, 17 Jan 2024 13:07:01 -0500 Subject: [PATCH] Add job status indicators to tabs within a workspace (#232) Adds job status indicators that will show any updates to running commands while you are focused away from a tab. These will show up as status icons in the tab view. These indicators will reset for a given tab when you focus back to it. I've updated the inner formatting of the tab to use flexboxes, allowing the title to display more text when there are no icons to display. Also includes some miscellaneous for-loop pattern improvements in model.ts and removing of unused variables, etc. --------- Co-authored-by: sawka --- src/app/app.less | 13 ++++ src/app/common/common.less | 16 +++++ src/app/common/common.tsx | 31 ++++++++- src/app/line/lines.less | 13 ---- src/app/sidebar/sidebar.less | 3 - src/app/sidebar/sidebar.tsx | 4 -- src/app/workspace/cmdinput/cmdinput.less | 19 ------ src/app/workspace/screen/screenview.tsx | 4 +- src/app/workspace/screen/tab.tsx | 25 ++++--- src/app/workspace/screen/tabs.less | 86 +++++++++++++++--------- src/app/workspace/screen/tabs.tsx | 43 ++++++------ src/model/model.ts | 45 ++++++++----- src/types/types.ts | 21 ++++++ wavesrv/pkg/remote/remote.go | 10 ++- wavesrv/pkg/sstore/dbops.go | 17 ++++- wavesrv/pkg/sstore/memops.go | 47 +++++++------ wavesrv/pkg/sstore/sstore.go | 67 +++++++++++++++++- wavesrv/pkg/sstore/updatebus.go | 54 ++++++++------- 18 files changed, 347 insertions(+), 171 deletions(-) diff --git a/src/app/app.less b/src/app/app.less index 3e764e43..2035273f 100644 --- a/src/app/app.less +++ b/src/app/app.less @@ -623,6 +623,19 @@ a.a-block { user-select: text; } +.spin { + animation: infiniteRotate 2s linear infinite; + + @keyframes infiniteRotate { + from { + transform:rotate(0deg); + } + to { + transform:rotate(360deg); + } + } +} + .view { background-color: @background-session; flex-grow: 1; diff --git a/src/app/common/common.less b/src/app/common/common.less index ddfd827c..3a9f65dc 100644 --- a/src/app/common/common.less +++ b/src/app/common/common.less @@ -1158,3 +1158,19 @@ } } } + +.status-indicator { + position: relative; + top: 1px; + margin-right: 3px; + + &.error { + color: @term-red; + } + &.success { + color: @term-green; + } + &.output { + color: @text-primary; + } +} diff --git a/src/app/common/common.tsx b/src/app/common/common.tsx index 90183571..b329b4ee 100644 --- a/src/app/common/common.tsx +++ b/src/app/common/common.tsx @@ -9,7 +9,7 @@ import ReactMarkdown from "react-markdown"; import remarkGfm from "remark-gfm"; import cn from "classnames"; import { If } from "tsx-control-statements/components"; -import type { RemoteType } from "../../types/types"; +import { RemoteType, StatusIndicatorLevel } from "../../types/types"; import ReactDOM from "react-dom"; import { GlobalModel } from "../../model/model"; @@ -1246,6 +1246,34 @@ class Modal extends React.Component { } } +interface StatusIndicatorProps { + level: StatusIndicatorLevel + className?: string +} + +class StatusIndicator extends React.Component { + render() { + const statusIndicatorLevel = this.props.level; + let statusIndicator = null; + if (statusIndicatorLevel != StatusIndicatorLevel.None) { + let statusIndicatorClass = null; + switch (statusIndicatorLevel) { + case StatusIndicatorLevel.Output: + statusIndicatorClass = "output"; + break; + case StatusIndicatorLevel.Success: + statusIndicatorClass = "success"; + break; + case StatusIndicatorLevel.Error: + statusIndicatorClass = "error"; + break; + } + statusIndicator =
; + } + return statusIndicator; + } +} + export { CmdStrCode, Toggle, @@ -1267,4 +1295,5 @@ export { LinkButton, Status, Modal, + StatusIndicator, }; diff --git a/src/app/line/lines.less b/src/app/line/lines.less index 7fd96e48..8fc4a747 100644 --- a/src/app/line/lines.less +++ b/src/app/line/lines.less @@ -248,19 +248,6 @@ .warning { fill: @warning-yellow; } - - @keyframes infiniteRotate { - 0% { - transform: rotate(0deg); - } - 100% { - transform: rotate(360deg); - } - } - - .spin { - animation: infiniteRotate 2s linear infinite; - } } &.top-border { diff --git a/src/app/sidebar/sidebar.less b/src/app/sidebar/sidebar.less index 1173bd86..bec57c83 100644 --- a/src/app/sidebar/sidebar.less +++ b/src/app/sidebar/sidebar.less @@ -99,9 +99,6 @@ } } - .top { - } - .separator { height: 1px; margin: 16px 0; diff --git a/src/app/sidebar/sidebar.tsx b/src/app/sidebar/sidebar.tsx index e12f2eb5..884ecda6 100644 --- a/src/app/sidebar/sidebar.tsx +++ b/src/app/sidebar/sidebar.tsx @@ -187,10 +187,7 @@ class MainSideBar extends React.Component<{}, {}> { activeRemoteId = rptr.remoteid; } } - let session: Session = null; let remotes = model.remotes ?? []; - let remote: RemoteType = null; - let idx: number = 0; remotes = sortAndFilterRemotes(remotes); let sessionList = []; for (let session of model.sessionList) { @@ -199,7 +196,6 @@ class MainSideBar extends React.Component<{}, {}> { } } let isCollapsed = this.collapsed.get(); - let mainView = GlobalModel.activeMainView.get(); let clientData = GlobalModel.clientData.get(); let needsUpdate = false; if (!clientData?.clientopts.noreleasecheck && !isBlank(clientData?.releaseinfo?.latestversion)) { diff --git a/src/app/workspace/cmdinput/cmdinput.less b/src/app/workspace/cmdinput/cmdinput.less index 21a34236..07134cb6 100644 --- a/src/app/workspace/cmdinput/cmdinput.less +++ b/src/app/workspace/cmdinput/cmdinput.less @@ -55,9 +55,6 @@ .button { margin-left: 10px; } - - .remote-name { - } } .input-minmax-control { @@ -100,19 +97,6 @@ .warning { fill: @warning-yellow; } - - @keyframes infiniteRotate { - 0% { - transform: rotate(0deg); - } - 100% { - transform: rotate(360deg); - } - } - - .spin { - animation: infiniteRotate 2s linear infinite; - } } .cmd-input-field { @@ -482,9 +466,6 @@ } .remote-field-control { - &.text-control { - } - &.text-input { input[type="text"], input[type="number"], diff --git a/src/app/workspace/screen/screenview.tsx b/src/app/workspace/screen/screenview.tsx index d608e954..15251f3e 100644 --- a/src/app/workspace/screen/screenview.tsx +++ b/src/app/workspace/screen/screenview.tsx @@ -10,13 +10,12 @@ import { If, For } from "tsx-control-statements/components"; import cn from "classnames"; import { debounce } from "throttle-debounce"; import dayjs from "dayjs"; -import { GlobalCommandRunner, TabColors, TabIcons, ForwardLineContainer } from "../../../model/model"; +import { GlobalCommandRunner, TabColors, TabIcons, ForwardLineContainer, GlobalModel, ScreenLines, Screen, Session } from "../../../model/model"; import type { LineType, RenderModeType, LineFactoryProps } from "../../../types/types"; import * as T from "../../../types/types"; import localizedFormat from "dayjs/plugin/localizedFormat"; import { Button } from "../../common/common"; import { getRemoteStr } from "../../common/prompt/prompt"; -import { GlobalModel, ScreenLines, Screen, Session } from "../../../model/model"; import { Line } from "../../line/linecomps"; import { LinesView } from "../../line/linesview"; import * as util from "../../../util/util"; @@ -26,7 +25,6 @@ import { ReactComponent as Check12Icon } from "../../assets/icons/check12.svg"; import { ReactComponent as SquareIcon } from "../../assets/icons/tab/square.svg"; import { ReactComponent as GlobeIcon } from "../../assets/icons/globe.svg"; import { ReactComponent as StatusCircleIcon } from "../../assets/icons/statuscircle.svg"; -import { termWidthFromCols, termHeightFromRows } from "../../../util/textmeasure"; import * as appconst from "../../appconst"; import "./screenview.less"; diff --git a/src/app/workspace/screen/tab.tsx b/src/app/workspace/screen/tab.tsx index 09f0c92d..abbd2c6d 100644 --- a/src/app/workspace/screen/tab.tsx +++ b/src/app/workspace/screen/tab.tsx @@ -4,14 +4,11 @@ import * as React from "react"; import * as mobxReact from "mobx-react"; import * as mobx from "mobx"; -import { sprintf } from "sprintf-js"; import { boundMethod } from "autobind-decorator"; -import { For } from "tsx-control-statements/components"; import cn from "classnames"; -import { GlobalModel, GlobalCommandRunner, Session, Screen } from "../../../model/model"; -import { renderCmdText } from "../../common/common"; +import { GlobalModel, GlobalCommandRunner, Screen } from "../../../model/model"; +import { StatusIndicator, renderCmdText } from "../../common/common"; import { ReactComponent as SquareIcon } from "../../assets/icons/tab/square.svg"; -import { ReactComponent as ActionsIcon } from "../../assets/icons/tab/actions.svg"; import * as constants from "../../appconst"; import { Reorder } from "framer-motion"; import { MagicLayout } from "../../magiclayout"; @@ -66,7 +63,7 @@ class ScreenTab extends React.Component< if (tabIcon === "default" || tabIcon === "square") { return (
- +
); } @@ -84,9 +81,10 @@ class ScreenTab extends React.Component< if (index + 1 <= 9) { tabIndex =
{renderCmdText(String(index + 1))}
; } + let settings = (
this.openScreenSettings(e, screen)} title="Actions" className="tab-gear"> - +
); let archived = screen.archived.get() ? ( @@ -97,6 +95,8 @@ class ScreenTab extends React.Component< ) : null; + const statusIndicatorLevel = screen.statusIndicator.get(); + return ( this.openScreenSettings(event, screen)} onDragEnd={this.handleDragEnd} > - {this.renderTabIcon(screen)} +
+ {this.renderTabIcon(screen)} +
{archived} {webShared} {screen.name.get()}
- {tabIndex} - {settings} +
+ + {tabIndex} + {settings} +
); } diff --git a/src/app/workspace/screen/tabs.less b/src/app/workspace/screen/tabs.less index c22bad16..103fb207 100644 --- a/src/app/workspace/screen/tabs.less +++ b/src/app/workspace/screen/tabs.less @@ -10,7 +10,7 @@ &.color-green, &.color-default { - svg.left-icon path { + svg.svg-icon-inner path { fill: @tab-green; } @@ -34,7 +34,7 @@ } &.color-orange { - svg.left-icon path { + svg.svg-icon-inner path { fill: @tab-orange; } @@ -54,7 +54,7 @@ } &.color-red { - svg.left-icon path { + svg.svg-icon-inner path { fill: @tab-red; } @@ -74,7 +74,7 @@ } &.color-yellow { - svg.left-icon path { + svg.svg-icon-inner path { fill: @tab-yellow; } @@ -94,7 +94,7 @@ } &.color-blue { - svg.left-icon path { + svg.svg-icon-inner path { fill: @tab-blue; } @@ -114,7 +114,7 @@ } &.color-mint { - svg.left-icon path { + svg.svg-icon-inner path { fill: @tab-mint; } @@ -134,7 +134,7 @@ } &.color-cyan { - svg.left-icon path { + svg.svg-icon-inner path { fill: @tab-cyan; } @@ -154,7 +154,7 @@ } &.color-white { - svg.left-icon path { + svg.svg-icon-inner path { fill: @tab-white; } @@ -174,7 +174,7 @@ } &.color-violet { - svg.left-icon path { + svg.svg-icon-inner path { fill: @tab-violet; } @@ -194,7 +194,7 @@ } &.color-pink { - svg.left-icon path { + svg.svg-icon-inner path { fill: @tab-pink; } @@ -240,12 +240,8 @@ display: flex; } - .screen-tabs { - display: flex; - flex-direction: row; - align-items: center; + .screen-tabs-container-inner { overflow-x: scroll; - &::-webkit-scrollbar-thumb, &::-webkit-scrollbar-track { display: none; @@ -254,32 +250,36 @@ &:hover::-webkit-scrollbar-thumb { display: block; } + } + + .screen-tabs { + display: flex; + flex-direction: row; + align-items: center; .screen-tab { display: flex; + flex-direction: row; height: 3em; min-width: 14em; max-width: 14em; align-items: center; cursor: pointer; - position: relative; - .icon.svg-icon { + .front-icon { margin: 0 12px; - - svg { - width: 14px; - height: 14px; + .svg-icon svg { + width: 14px; + height: 14px; } - } - - .icon.fa-icon { - font-size: 16px; - margin: 0 12px; + .fa-icon { + font-size: 16px; + } + } .tab-name { - width: 8rem; + flex-grow: 1; } &.is-active { @@ -293,12 +293,27 @@ } } - .tab-index, - .tab-gear { - display: none; + .end-icon { + margin: 0 6px; .icon { + margin: 0; + padding: 0 6px; + width: 0; border-radius: 50%; } + .status-indicator { + display: block; + } + .tab-gear { + display: none; + // Account for the fact that the ellipsis icon is a little taller than the other icons + margin-bottom: -2px; + } + + .tab-index { + display: none; + font-size: 0.9em; + } } &:hover { @@ -308,9 +323,14 @@ } } - &:hover .screen-tab .tab-index { - display: block; - font-size: 0.8em; + &:hover .screen-tab { + .tab-index { + display: block; + } + + .status-indicator { + display: none; + } } &:hover .screen-tab:hover .tab-index { diff --git a/src/app/workspace/screen/tabs.tsx b/src/app/workspace/screen/tabs.tsx index 201226a7..c2211a5c 100644 --- a/src/app/workspace/screen/tabs.tsx +++ b/src/app/workspace/screen/tabs.tsx @@ -191,26 +191,29 @@ class ScreenTabs extends React.Component< return (
- { - this.setState({ showingScreens: tabs }); - }} - values={showingScreens} - > - - - - + {/* Inner container ensures that hovering over the scrollbar doesn't trigger the hover effect on the tabs. This prevents weird flickering of the icons when the mouse is moved over the scrollbar. */} +
+ { + this.setState({ showingScreens: tabs }); + }} + values={showingScreens} + > + + + + +
diff --git a/src/model/model.ts b/src/model/model.ts index af419eec..7d937114 100644 --- a/src/model/model.ts +++ b/src/model/model.ts @@ -20,7 +20,7 @@ import { } from "../util/util"; import { TermWrap } from "../plugins/terminal/term"; import { PluginModel } from "../plugins/plugins"; -import type { +import { SessionDataType, LineType, RemoteType, @@ -30,19 +30,16 @@ import type { CmdDataType, FeCmdPacketType, TermOptsType, - RemoteStateType, ScreenDataType, ScreenOptsType, PtyDataUpdateType, ModelUpdateType, UpdateMessage, InfoType, - CmdLineUpdateType, UIContextType, HistoryInfoType, HistoryQueryOpts, FeInputPacketType, - TermWinSize, RemoteInputPacketType, ContextMenuOpts, RendererContext, @@ -66,6 +63,7 @@ import type { WebCmd, WebRemote, OpenAICmdInfoChatMessageType, + StatusIndicatorLevel, } from "../types/types"; import * as T from "../types/types"; import { WSControl } from "./ws"; @@ -370,6 +368,7 @@ class Screen { shareMode: OV; webShareOpts: OV; filterRunning: OV; + statusIndicator: OV; constructor(sdata: ScreenDataType) { this.sessionId = sdata.sessionid; @@ -410,6 +409,9 @@ class Screen { this.filterRunning = mobx.observable.box(false, { name: "screen-filter-running", }); + this.statusIndicator = mobx.observable.box(StatusIndicatorLevel.None, { + name: "screen-status-indicator", + }); } dispose() {} @@ -623,9 +625,9 @@ class Screen { if (lines == null || lines.length == 0) { return null; } - for (let i = 0; i < lines.length; i++) { - if (lines[i].linenum == lineNum) { - return lines[i]; + for (const line of lines) { + if (line.linenum == lineNum) { + return line; } } return null; @@ -643,9 +645,9 @@ class Screen { if (lines == null || lines.length == 0) { return null; } - for (let i = 0; i < lines.length; i++) { - if (lines[i].lineid == lineId) { - return lines[i]; + for (const line of lines) { + if (line.lineid == lineId) { + return line; } } return null; @@ -663,8 +665,7 @@ class Screen { if (lineNum == 0) { return null; } - for (let i = 0; i < lines.length; i++) { - let line = lines[i]; + for (const line of lines) { if (line.linenum == lineNum) { return lineNum; } @@ -799,6 +800,16 @@ class Screen { } } + /** + * Set the status indicator for the screen. + * @param indicator The value of the status indicator. One of "none", "error", "success", "output". + */ + setStatusIndicator(indicator: StatusIndicatorLevel): void { + mobx.action(() => { + this.statusIndicator.set(indicator); + })(); + } + termCustomKeyHandlerInternal(e: any, termWrap: TermWrap): void { if (e.code == "ArrowUp") { termWrap.terminal.scrollLines(-1); @@ -1192,8 +1203,7 @@ class Session { if (rptr.name.startsWith("*")) { screenId = ""; } - for (let i = 0; i < this.remoteInstances.length; i++) { - let rdata = this.remoteInstances[i]; + for (const rdata of this.remoteInstances) { if ( rdata.screenid == screenId && rdata.remoteid == rptr.remoteid && @@ -3946,8 +3956,8 @@ class Model { (sdata: ScreenDataType) => sdata.screenid, (sdata: ScreenDataType) => new Screen(sdata) ); - for (let i = 0; i < mods.removed.length; i++) { - this.removeScreenLinesByScreenId(mods.removed[i]); + for (const screenId of mods.removed) { + this.removeScreenLinesByScreenId(screenId); } } if ("sessions" in update || "activesessionid" in update) { @@ -4046,6 +4056,9 @@ class Model { if ("openaicmdinfochat" in update) { this.inputModel.setOpenAICmdInfoChat(update.openaicmdinfochat); } + if ("screenstatusindicator" in update) { + this.getScreenById_single(update.screenstatusindicator.screenid).setStatusIndicator(update.screenstatusindicator.status); + } // console.log("run-update>", Date.now(), interactive, update); } diff --git a/src/types/types.ts b/src/types/types.ts index c5e417b2..d80f8a93 100644 --- a/src/types/types.ts +++ b/src/types/types.ts @@ -282,6 +282,21 @@ type OpenAICmdInfoChatMessageType = { userquery?: string; }; +/** + * Levels for the screen status indicator + */ +enum StatusIndicatorLevel { + None = 0, + Output = 1, + Success = 2, + Error = 3, +} + +type ScreenStatusIndicatorUpdateType = { + screenid: string; + status: StatusIndicatorLevel; +} + type ModelUpdateType = { interactive: boolean; sessions?: SessionDataType[]; @@ -304,6 +319,7 @@ type ModelUpdateType = { remoteview?: RemoteViewType; openaicmdinfochat?: OpenAICmdInfoChatMessageType[]; alertmessage?: AlertMessageType; + screenstatusindicator?: ScreenStatusIndicatorUpdateType; }; type HistoryViewDataType = { @@ -782,4 +798,9 @@ export type { StrWithPos, CmdInputTextPacketType, OpenAICmdInfoChatMessageType, + ScreenStatusIndicatorUpdateType, +}; + +export { + StatusIndicatorLevel, }; diff --git a/wavesrv/pkg/remote/remote.go b/wavesrv/pkg/remote/remote.go index 553fd733..9a4be791 100644 --- a/wavesrv/pkg/remote/remote.go +++ b/wavesrv/pkg/remote/remote.go @@ -1910,6 +1910,7 @@ func (msh *MShellProc) ProcessPackets() { if pk.GetType() == packet.DataPacketStr { dataPk := pk.(*packet.DataPacketType) runCmdUpdateFn(dataPk.CK, msh.makeHandleDataPacketClosure(dataPk, dataPosMap)) + go pushStatusIndicatorUpdate(&dataPk.CK, sstore.StatusIndicatorLevel_Output) continue } if pk.GetType() == packet.DataAckPacketStr { @@ -1919,7 +1920,8 @@ func (msh *MShellProc) ProcessPackets() { } if pk.GetType() == packet.CmdDataPacketStr { dataPacket := pk.(*packet.CmdDataPacketType) - msh.WriteToPtyBuffer("cmd-data> [remote %s] [%s] pty=%d run=%d\n", msh.GetRemoteName(), dataPacket.CK, dataPacket.PtyDataLen, dataPacket.RunDataLen) + go msh.WriteToPtyBuffer("cmd-data> [remote %s] [%s] pty=%d run=%d\n", msh.GetRemoteName(), dataPacket.CK, dataPacket.PtyDataLen, dataPacket.RunDataLen) + go pushStatusIndicatorUpdate(&dataPacket.CK, sstore.StatusIndicatorLevel_Output) continue } if pk.GetType() == packet.CmdDonePacketStr { @@ -2185,3 +2187,9 @@ func (msh *MShellProc) GetDisplayName() string { rcopy := msh.GetRemoteCopy() return rcopy.GetName() } + +// Identify the screen for a given CommandKey and push the given status indicator update for that screen +func pushStatusIndicatorUpdate(ck *base.CommandKey, level sstore.StatusIndicatorLevel) { + screenId := ck.GetGroupId() + sstore.SetStatusIndicatorLevel(context.Background(), screenId, level, false) +} diff --git a/wavesrv/pkg/sstore/dbops.go b/wavesrv/pkg/sstore/dbops.go index fd9a2323..4186edcf 100644 --- a/wavesrv/pkg/sstore/dbops.go +++ b/wavesrv/pkg/sstore/dbops.go @@ -912,7 +912,19 @@ func UpdateCmdDoneInfo(ctx context.Context, ck base.CommandKey, donePk *packet.C if rtnCmd == nil { return nil, fmt.Errorf("cmd data not found for ck[%s]", ck) } - return &ModelUpdate{Cmd: rtnCmd}, nil + + update := &ModelUpdate{Cmd: rtnCmd} + + // Update in-memory screen indicator status + var indicator StatusIndicatorLevel + if rtnCmd.ExitCode == 0 { + indicator = StatusIndicatorLevel_Success + } else { + indicator = StatusIndicatorLevel_Error + } + SetStatusIndicatorLevel_Update(ctx, update, screenId, indicator, false) + + return update, nil } func UpdateCmdRtnState(ctx context.Context, ck base.CommandKey, statePtr ShellStatePtr) error { @@ -1065,6 +1077,9 @@ func SwitchScreenById(ctx context.Context, sessionId string, screenId string) (* if memState != nil { update.CmdLine = &memState.CmdInputText update.OpenAICmdInfoChat = ScreenMemGetCmdInfoChat(screenId).Messages + + // Clear any previous status indicator for this screen + ResetStatusIndicator_Update(update, screenId) } return update, nil } diff --git a/wavesrv/pkg/sstore/memops.go b/wavesrv/pkg/sstore/memops.go index 5f7ddd85..31e1bf87 100644 --- a/wavesrv/pkg/sstore/memops.go +++ b/wavesrv/pkg/sstore/memops.go @@ -18,19 +18,14 @@ import ( var MemLock *sync.Mutex = &sync.Mutex{} var ScreenMemStore map[string]*ScreenMemState = make(map[string]*ScreenMemState) // map of screenid -> ScreenMemState -const ( - ScreenIndicator_None = "" - ScreenIndicator_Error = "error" - ScreenIndicator_Success = "success" - ScreenIndicator_Output = "output" -) +type StatusIndicatorLevel int -var screenIndicatorLevels map[string]int = map[string]int{ - ScreenIndicator_None: 0, - ScreenIndicator_Output: 1, - ScreenIndicator_Success: 2, - ScreenIndicator_Error: 3, -} +const ( + StatusIndicatorLevel_None StatusIndicatorLevel = iota + StatusIndicatorLevel_Output + StatusIndicatorLevel_Success + StatusIndicatorLevel_Error +) func dumpScreenMemStore() { MemLock.Lock() @@ -40,11 +35,6 @@ func dumpScreenMemStore() { } } -// returns true if i1 > i2 -func isIndicatorGreater(i1 string, i2 string) bool { - return screenIndicatorLevels[i1] > screenIndicatorLevels[i2] -} - type OpenAICmdInfoChatStore struct { MessageCount int `json:"messagecount"` Messages []*packet.OpenAICmdInfoChatMessage `json:"messages"` @@ -52,7 +42,7 @@ type OpenAICmdInfoChatStore struct { type ScreenMemState struct { NumRunningCommands int `json:"numrunningcommands,omitempty"` - IndicatorType string `json:"indicatortype,omitempty"` + StatusIndicator StatusIndicatorLevel `json:"statusindicator,omitempty"` CmdInputText utilfn.StrWithPos `json:"cmdinputtext,omitempty"` CmdInputSeqNum int `json:"cmdinputseqnum,omitempty"` AICmdInfoChat *OpenAICmdInfoChatStore `json:"aicmdinfochat,omitempty"` @@ -172,17 +162,32 @@ func ScreenMemSetNumRunningCommands(screenId string, num int) { ScreenMemStore[screenId].NumRunningCommands = num } -func ScreenMemCombineIndicator(screenId string, indicator string) { +// If the new indicator level is higher than the current indicator, update the current indicator. Returns the new indicator level. +func ScreenMemCombineIndicatorLevels(screenId string, level StatusIndicatorLevel) StatusIndicatorLevel { MemLock.Lock() defer MemLock.Unlock() if ScreenMemStore[screenId] == nil { ScreenMemStore[screenId] = &ScreenMemState{} } - if isIndicatorGreater(indicator, ScreenMemStore[screenId].IndicatorType) { - ScreenMemStore[screenId].IndicatorType = indicator + curLevel := ScreenMemStore[screenId].StatusIndicator + if level > curLevel { + ScreenMemStore[screenId].StatusIndicator = level + return level + } else { + return curLevel } } +// Set the indicator to the given level, regardless of the current indicator level. +func ScreenMemSetIndicatorLevel(screenId string, level StatusIndicatorLevel) { + MemLock.Lock() + defer MemLock.Unlock() + if ScreenMemStore[screenId] == nil { + ScreenMemStore[screenId] = &ScreenMemState{} + } + ScreenMemStore[screenId].StatusIndicator = StatusIndicatorLevel_None +} + // safe because we return a copy func GetScreenMemState(screenId string) *ScreenMemState { MemLock.Lock() diff --git a/wavesrv/pkg/sstore/sstore.go b/wavesrv/pkg/sstore/sstore.go index c33f09b4..cfa60036 100644 --- a/wavesrv/pkg/sstore/sstore.go +++ b/wavesrv/pkg/sstore/sstore.go @@ -525,8 +525,9 @@ type ScreenType struct { ArchivedTs int64 `json:"archivedts,omitempty"` // only for updates - Full bool `json:"full,omitempty"` - Remove bool `json:"remove,omitempty"` + Full bool `json:"full,omitempty"` + Remove bool `json:"remove,omitempty"` + StatusIndicator string `json:"statusindicator,omitempty"` } func (s *ScreenType) ToMap() map[string]interface{} { @@ -1464,3 +1465,65 @@ func SetReleaseInfo(ctx context.Context, releaseInfo ReleaseInfoType) error { }) return txErr } + +// Sets the in-memory status indicator for the given screenId to the given value and adds it to the ModelUpdate. By default, the active screen will be ignored when updating status. To force a status update for the active screen, set force=true. +func SetStatusIndicatorLevel_Update(ctx context.Context, update *ModelUpdate, screenId string, level StatusIndicatorLevel, force bool) error { + var newStatus StatusIndicatorLevel + + if force { + // Force the update and set the new status to the given level, regardless of the current status or the active screen + ScreenMemSetIndicatorLevel(screenId, level) + newStatus = level + } else { + // Only update the status if the given screen is not the active screen and if the given level is higher than the current level + activeSessionId, err := GetActiveSessionId(ctx) + if err != nil { + return fmt.Errorf("error getting active session id: %w", err) + } + bareSession, err := GetBareSessionById(ctx, activeSessionId) + if err != nil { + return fmt.Errorf("error getting bare session: %w", err) + } + activeScreenId := bareSession.ActiveScreenId + if activeScreenId == screenId { + return nil + } + + // If we are not forcing the update, follow the rules for combining status indicators + newLevel := ScreenMemCombineIndicatorLevels(screenId, level) + if newLevel == level { + newStatus = level + } else { + return nil + } + } + + update.ScreenStatusIndicator = &ScreenStatusIndicatorType{ + ScreenId: screenId, + Status: newStatus, + } + return nil +} + +// Sets the in-memory status indicator for the given screenId to the given value and pushes the new value to the FE +func SetStatusIndicatorLevel(ctx context.Context, screenId string, level StatusIndicatorLevel, force bool) { + update := &ModelUpdate{} + err := SetStatusIndicatorLevel_Update(ctx, update, screenId, level, false) + if err != nil { + log.Printf("error setting status indicator level: %v\n", err) + return + } + MainBus.SendUpdate(update) +} + +// Resets the in-memory status indicator for the given screenId to StatusIndicatorLevel_None and adds it to the ModelUpdate +func ResetStatusIndicator_Update(update *ModelUpdate, screenId string) error { + // We do not need to set context when resetting the status indicator because we will not need to call the DB + return SetStatusIndicatorLevel_Update(context.TODO(), update, screenId, StatusIndicatorLevel_None, true) +} + +// Resets the in-memory status indicator for the given screenId to StatusIndicatorLevel_None and pushes the new value to the FE +func ResetStatusIndicator(screenId string) { + // We do not need to set context when resetting the status indicator because we will not need to call the DB + SetStatusIndicatorLevel(context.TODO(), screenId, StatusIndicatorLevel_None, true) +} diff --git a/wavesrv/pkg/sstore/updatebus.go b/wavesrv/pkg/sstore/updatebus.go index 12a74f0c..d7e2c71e 100644 --- a/wavesrv/pkg/sstore/updatebus.go +++ b/wavesrv/pkg/sstore/updatebus.go @@ -39,30 +39,31 @@ func (*PtyDataUpdate) UpdateType() string { func (pdu *PtyDataUpdate) Clean() {} type ModelUpdate struct { - Sessions []*SessionType `json:"sessions,omitempty"` - ActiveSessionId string `json:"activesessionid,omitempty"` - Screens []*ScreenType `json:"screens,omitempty"` - ScreenLines *ScreenLinesType `json:"screenlines,omitempty"` - Line *LineType `json:"line,omitempty"` - Lines []*LineType `json:"lines,omitempty"` - Cmd *CmdType `json:"cmd,omitempty"` - CmdLine *utilfn.StrWithPos `json:"cmdline,omitempty"` - Info *InfoMsgType `json:"info,omitempty"` - ClearInfo bool `json:"clearinfo,omitempty"` - Remotes []RemoteRuntimeState `json:"remotes,omitempty"` - History *HistoryInfoType `json:"history,omitempty"` - Interactive bool `json:"interactive"` - Connect bool `json:"connect,omitempty"` - MainView string `json:"mainview,omitempty"` - Bookmarks []*BookmarkType `json:"bookmarks,omitempty"` - SelectedBookmark string `json:"selectedbookmark,omitempty"` - HistoryViewData *HistoryViewData `json:"historyviewdata,omitempty"` - ClientData *ClientData `json:"clientdata,omitempty"` - RemoteView *RemoteViewType `json:"remoteview,omitempty"` - ScreenTombstones []*ScreenTombstoneType `json:"screentombstones,omitempty"` - SessionTombstones []*SessionTombstoneType `json:"sessiontombstones,omitempty"` - OpenAICmdInfoChat []*packet.OpenAICmdInfoChatMessage `json:"openaicmdinfochat,omitempty"` - AlertMessage *AlertMessageType `json:"alertmessage,omitempty"` + Sessions []*SessionType `json:"sessions,omitempty"` + ActiveSessionId string `json:"activesessionid,omitempty"` + Screens []*ScreenType `json:"screens,omitempty"` + ScreenLines *ScreenLinesType `json:"screenlines,omitempty"` + Line *LineType `json:"line,omitempty"` + Lines []*LineType `json:"lines,omitempty"` + Cmd *CmdType `json:"cmd,omitempty"` + CmdLine *utilfn.StrWithPos `json:"cmdline,omitempty"` + Info *InfoMsgType `json:"info,omitempty"` + ClearInfo bool `json:"clearinfo,omitempty"` + Remotes []RemoteRuntimeState `json:"remotes,omitempty"` + History *HistoryInfoType `json:"history,omitempty"` + Interactive bool `json:"interactive"` + Connect bool `json:"connect,omitempty"` + MainView string `json:"mainview,omitempty"` + Bookmarks []*BookmarkType `json:"bookmarks,omitempty"` + SelectedBookmark string `json:"selectedbookmark,omitempty"` + HistoryViewData *HistoryViewData `json:"historyviewdata,omitempty"` + ClientData *ClientData `json:"clientdata,omitempty"` + RemoteView *RemoteViewType `json:"remoteview,omitempty"` + ScreenTombstones []*ScreenTombstoneType `json:"screentombstones,omitempty"` + SessionTombstones []*SessionTombstoneType `json:"sessiontombstones,omitempty"` + OpenAICmdInfoChat []*packet.OpenAICmdInfoChatMessage `json:"openaicmdinfochat,omitempty"` + AlertMessage *AlertMessageType `json:"alertmessage,omitempty"` + ScreenStatusIndicator *ScreenStatusIndicatorType `json:"screenstatusindicator,omitempty"` } func (*ModelUpdate) UpdateType() string { @@ -261,3 +262,8 @@ func MakeSessionsUpdateForRemote(sessionId string, ri *RemoteInstance) []*Sessio type BookmarksViewType struct { Bookmarks []*BookmarkType `json:"bookmarks"` } + +type ScreenStatusIndicatorType struct { + ScreenId string `json:"screenid"` + Status StatusIndicatorLevel `json:"status"` +}