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 <mike.sawka@gmail.com>
This commit is contained in:
Evan Simkowitz 2024-01-17 13:07:01 -05:00 committed by GitHub
parent a7afefc340
commit 4ac5d93ed2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
18 changed files with 347 additions and 171 deletions

View File

@ -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;

View File

@ -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;
}
}

View File

@ -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<ModalProps> {
}
}
interface StatusIndicatorProps {
level: StatusIndicatorLevel
className?: string
}
class StatusIndicator extends React.Component<StatusIndicatorProps> {
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 = <div className={`${this.props.className} fa-sharp fa-solid fa-circle-small status-indicator ${statusIndicatorClass}`}></div>;
}
return statusIndicator;
}
}
export {
CmdStrCode,
Toggle,
@ -1267,4 +1295,5 @@ export {
LinkButton,
Status,
Modal,
StatusIndicator,
};

View File

@ -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 {

View File

@ -99,9 +99,6 @@
}
}
.top {
}
.separator {
height: 1px;
margin: 16px 0;

View File

@ -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)) {

View File

@ -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"],

View File

@ -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";

View File

@ -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 (
<div className="icon svg-icon">
<SquareIcon className="left-icon" />
<SquareIcon className="svg-icon-inner" />
</div>
);
}
@ -84,9 +81,10 @@ class ScreenTab extends React.Component<
if (index + 1 <= 9) {
tabIndex = <div className="tab-index">{renderCmdText(String(index + 1))}</div>;
}
let settings = (
<div onClick={(e) => this.openScreenSettings(e, screen)} title="Actions" className="tab-gear">
<ActionsIcon className="icon hoverEffect " />
<div className="icon hoverEffect fa-sharp fa-solid fa-ellipsis-vertical"></div>
</div>
);
let archived = screen.archived.get() ? (
@ -97,6 +95,8 @@ class ScreenTab extends React.Component<
<i title="shared to web" className="fa-sharp fa-solid fa-share-nodes web-share-icon" />
) : null;
const statusIndicatorLevel = screen.statusIndicator.get();
return (
<Reorder.Item
ref={this.tabRef}
@ -115,14 +115,19 @@ class ScreenTab extends React.Component<
onContextMenu={(event) => this.openScreenSettings(event, screen)}
onDragEnd={this.handleDragEnd}
>
{this.renderTabIcon(screen)}
<div className="front-icon">
{this.renderTabIcon(screen)}
</div>
<div className="tab-name truncate">
{archived}
{webShared}
{screen.name.get()}
</div>
{tabIndex}
{settings}
<div className="end-icon">
<StatusIndicator level={statusIndicatorLevel} />
{tabIndex}
{settings}
</div>
</Reorder.Item>
);
}

View File

@ -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 {

View File

@ -191,26 +191,29 @@ class ScreenTabs extends React.Component<
return (
<div className="screen-tabs-container">
<Reorder.Group
className="screen-tabs"
ref={this.tabsRef}
as="ul"
axis="x"
onReorder={(tabs: Screen[]) => {
this.setState({ showingScreens: tabs });
}}
values={showingScreens}
>
<For each="screen" index="index" of={showingScreens}>
<ScreenTab
key={screen.screenId}
screen={screen}
activeScreenId={activeScreenId}
index={index}
onSwitchScreen={this.handleSwitchScreen}
/>
</For>
</Reorder.Group>
{/* 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. */}
<div className="screen-tabs-container-inner">
<Reorder.Group
className="screen-tabs"
ref={this.tabsRef}
as="ul"
axis="x"
onReorder={(tabs: Screen[]) => {
this.setState({ showingScreens: tabs });
}}
values={showingScreens}
>
<For each="screen" index="index" of={showingScreens}>
<ScreenTab
key={screen.screenId}
screen={screen}
activeScreenId={activeScreenId}
index={index}
onSwitchScreen={this.handleSwitchScreen}
/>
</For>
</Reorder.Group>
</div>
<div key="new-screen" className="new-screen" onClick={this.handleNewScreen}>
<AddIcon className="icon hoverEffect" />
</div>

View File

@ -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<string>;
webShareOpts: OV<WebShareOpts>;
filterRunning: OV<boolean>;
statusIndicator: OV<StatusIndicatorLevel>;
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);
}

View File

@ -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,
};

View File

@ -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)
}

View File

@ -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
}

View File

@ -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()

View File

@ -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)
}

View File

@ -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"`
}