mirror of
https://github.com/wavetermdev/waveterm.git
synced 2025-04-16 20:26:12 +02:00
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:
parent
a7afefc340
commit
4ac5d93ed2
@ -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;
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
};
|
||||
|
@ -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 {
|
||||
|
@ -99,9 +99,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
.top {
|
||||
}
|
||||
|
||||
.separator {
|
||||
height: 1px;
|
||||
margin: 16px 0;
|
||||
|
@ -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)) {
|
||||
|
@ -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"],
|
||||
|
@ -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";
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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>
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
|
@ -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,
|
||||
};
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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()
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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"`
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user