Compare commits

..

9 Commits

Author SHA1 Message Date
sawka c06d846efe working on a clean server restart, fix some typescript errors in emain 2024-02-11 15:16:26 -03:00
Red J Adaya 2a4d85430a Fix term width in view connection detail modal (#274)
* fix term width

* add horizontal scroll to terminal when it overflows

* revert width of modal
2024-02-10 19:18:50 -03:00
Evan Simkowitz d319e72609 Refactor ModelUpdate to set up for decoupling sstore (#280)
This PR changes ModelUpdate mechanism from a statically-typed struct to an interface, allowing us to define the update mechanism and the update types separately. This sets us up to move app logic and update mechanisms into separate packages. Ultimately, sstore will only define low-level persistence logic.
2024-02-09 17:19:44 -08:00
sawka 7a5afccab3 do not unset ZDOTDIR otherwise interactive configs will be unnecessarily re-read (causing big performance problem for zsh command initialization) 2024-02-09 20:05:00 -03:00
sawka efe567398a fix ts warning 2024-02-09 18:57:07 -03:00
Sylvia Crowe 187509504d fix: type mismatch in userinput modal
A null was used in a place where a string should have been. It has been
updated to a string to remove the error associated with it.
2024-02-08 21:04:38 -08:00
Sylvie Crowe 903b26bfca Use ssh library: add user input (#281)
* feat: create backend for user input requests

This is the first part of a change that allows the backend to request
user input from the frontend. Essentially, the backend will send a
request for the user to answer some query, and the frontend will send
that answer back. It is blocking, so it needs to be used within a
goroutine.

There is some placeholder code in the frontend that will be updated in
future commits. Similarly, there is some debug code in the backend
remote.go file.

* feat: create frontend for user input requests

This is part of a change to allow the backend to request user input from
the frontend. This adds a component specifically for handling this
logic. It is only a starting point, and does not work perfectly yet.

* refactor: update user input backend/interface

This updates the user input backend to fix a few potential bugs. It also
refactors the user input request and response types to better handle
markdown and errors while making it more convenient to work with.

A couple frontend changes were made to keep everything compatible.

* fix: add props to user input request modal

There was a second place that the modals were created that I previously
missed. This fixes that second casel

* feat: complete user input modal

This rounds out the most immediate concerns for the new user input
modal. The frontend now includes a timer to show how much time is left
and will close itself once it reaches zero. Css
formatting has been cleaned up to be more reasonable.

There is still some test code present on the back end. This will be
removed once actuall examples of the new modal are in place.

* feat: create first pass known_hosts detection

Manually integrating with golang's ssh library means that the code must
authenticate known_hosts on its own. This is a first pass at creating a
system that parses the known hosts files and denys a connection if there
is a mismatch. This needs to be updated with a means to add keys to the
known-hosts file if the user requests it.

* feat: allow writing to known_hosts first pass

As a follow-up to the previous change, we now allow the user to respond
to interactive queries in order to determine if an unknown known hosts
key can be added to a known_hosts file if it is missing. This needs to
be refined further, but it gets the basic functionality there.

* feat: add user input for kbd-interactive auth

This adds a modal so the user can respond to prompts provided using the
keyboard interactive authentication method.

* feat: add interactive password authentication

This makes the ssh password authentication interactive with its own user
input modal. Unfortunately, this method does not allow trying a default
first. This will need to be expanded in the future to accomodate that.

* fix: allow automatic and interactive auth together

Previously, it was impossible to use to separate methods of the same
type to try ssh authentication. This made it impossible to make an auto
attempt before a manual one. This change restricts that by combining
them into one method where the auto attempt is tried once first and
cannot be tried again. Following that, interactive authentication can be
tried separately.

It also lowers the time limit on kbd interactive authentication to 15
seconds due to limitations on the library we are using.

* fix: set number of retries to one in ssh

Number of retries means number of attempts after the fact, not number of
total attempts. It has been adjusted from 2 to 1 to reflect this.

* refactor: change argument order in GetUserInput

This is a simple change to move the context to the first argument of
GetUserInput to match the convention used elsewhere in the code.

* fix: set number of retries to two again

I was wrong in my previous analysis. The number given is the total
number of tries. This is confusing when keyboard authentication and
password authentication are both available which usually doesn't happen.

* feat: create naive ui for ssh key passphrases

This isn't quite as reactive as the other methods, but it does attempt
to use publickey without a passphrase, then attempt to use the password
as the passphrase, and finally prompting the user for a passphrase. The
problem with this approach is that if multiple keys are used and they
all have passphrases, they need to all be checked up front. In practice,
this will not happen often, but it is something to be aware of.

* fix: add the userinput.tsx changes

These were missed in the previous commit. Adding them now.
2024-02-09 00:16:56 -03:00
Red J Adaya e692c7c180 Split model into separate files (#279)
* split settings modals

* init

* init

* remove styles not related to checkbox

* remove styles not related to CmdStrCode

* renderCmdText doesn't need styles

* remove styles not related to dropdown

* IconButton doesn't need styles because it extends Button

* remove old connections

* InfoMessage conmponent no longer needed

* fix import error

* remove styles not related to InlineSettingsTextEdit

* remove styles not related to InputDecoration

* LinkButton doesn't need styles cos it's extends Button component

* remove styles not related to markdown

* remove styles not related to modal

* NumberField doesn't need styles cos it extends TextField

* remove styles not related to PasswordField

* RemoteStatusLight no longer used. It's replaced by Status component.

* remove styles not related to ResizableSidebar

* SettingsError doesn't need styles cos it uses classnames in app.less

* remove styles not related to Status

* remove styles not related to TextField

* remove styles not related to Toggle

* remove styles not related to Tooltip

* init

* cleanup bookmarks model

* cleanup clientsettingsview model

* cleanup cmd model

* remove dayjs import from bookmarks model

* cleanup commandrunner model

* cleanup historyview model

* cleanup input model

* cleanup linecontainer model

* cleanup mainsidebar model

* cleanup main model

* cleanup plugins model

* cleanup remote model

* cleanup screen model

* cleanup screenlines model

* cleanup session model

* cleanup screenlinecontainer model

* cleanup connectionsview model

* add index.ts inside model dir

* replace /model_old imports with /model

* rename model to models

* move model_old to models

* fix ptyDataSource errors

* split models/model
2024-02-08 17:52:42 -03:00
Cole Lashley b37f7f722e Command to copy file from remote to local (#231)
* first pass of copy file

* first pass fixing up function

* fleshed out copy function, still working on display and parameters

* implemented scp like syntax

* finished implemententation of copy file - there are still issues

* more bug fixes, still running into error

* pushing waveshell concurrency and channel fixes - still need to do some qol fixes before merge

* aesthetic fixes and removed logs

* fixed bug in GetRemoteRuntimeState

* formatting small fix

* fixed pretty print bytes

* added local to local command

* small fix removing workaround

* added workaround back

* added some logs for debug

* added some more logs

* quick bug fix for update cmd race condition

* added fix for race condition

* added some more logs for debugging

* fixed up logs

* added proper fe state for dest parameter

* implemented setting status indicator output

* first pass at updating status indicators

* removed logs and small fix ups

* removed whitespace

* addressed review comments
2024-02-08 17:37:23 -03:00
33 changed files with 2417 additions and 1003 deletions
+1
View File
@@ -8,6 +8,7 @@ export const SESSION_SETTINGS = "sessionSettings";
export const LINE_SETTINGS = "lineSettings"; export const LINE_SETTINGS = "lineSettings";
export const CLIENT_SETTINGS = "clientSettings"; export const CLIENT_SETTINGS = "clientSettings";
export const TAB_SWITCHER = "tabSwitcher"; export const TAB_SWITCHER = "tabSwitcher";
export const USER_INPUT = "userInput";
export const LineContainer_Main = "main"; export const LineContainer_Main = "main";
export const LineContainer_History = "history"; export const LineContainer_History = "history";
+1
View File
@@ -5,6 +5,7 @@
margin-bottom: 10px; margin-bottom: 10px;
font-family: @markdown-font; font-family: @markdown-font;
font-size: 14px; font-size: 14px;
overflow-wrap: break-word;
code { code {
background-color: @markdown-highlight; background-color: @markdown-highlight;
+7 -1
View File
@@ -60,7 +60,13 @@ class PasswordField extends TextField {
}; };
return ( return (
<div className={cn(`wave-textfield wave-password ${className || ""}`, { focused: focused, error: error })}> <div
className={cn(`wave-textfield wave-password ${className || ""}`, {
focused: focused,
error: error,
"no-label": !label,
})}
>
{decoration?.startDecoration && <>{decoration.startDecoration}</>} {decoration?.startDecoration && <>{decoration.startDecoration}</>}
<div className="wave-textfield-inner"> <div className="wave-textfield-inner">
<label <label
+1
View File
@@ -9,3 +9,4 @@ export { TabSwitcherModal } from "./tabswitcher";
export { SessionSettingsModal } from "./sessionsettings"; export { SessionSettingsModal } from "./sessionsettings";
export { ScreenSettingsModal } from "./screensettings"; export { ScreenSettingsModal } from "./screensettings";
export { LineSettingsModal } from "./linesettings"; export { LineSettingsModal } from "./linesettings";
export { UserInputModal } from "./userinput";
+1 -1
View File
@@ -17,7 +17,7 @@ class ModalsProvider extends React.Component {
for (let i = 0; i < store.length; i++) { for (let i = 0; i < store.length; i++) {
let entry = store[i]; let entry = store[i];
let Comp = entry.component; let Comp = entry.component;
rtn.push(<Comp key={entry.uniqueKey} />); rtn.push(<Comp key={entry.uniqueKey} {...entry.props} />);
} }
return <>{rtn}</>; return <>{rtn}</>;
} }
+12 -10
View File
@@ -12,19 +12,21 @@ import {
SessionSettingsModal, SessionSettingsModal,
ScreenSettingsModal, ScreenSettingsModal,
LineSettingsModal, LineSettingsModal,
UserInputModal,
} from "../modals"; } from "../modals";
import * as constants from "../../appconst"; import * as constants from "../../appconst";
const modalsRegistry: { [key: string]: () => React.ReactElement } = { const modalsRegistry: { [key: string]: React.ComponentType } = {
[constants.ABOUT]: () => <AboutModal />, [constants.ABOUT]: AboutModal,
[constants.CREATE_REMOTE]: () => <CreateRemoteConnModal />, [constants.CREATE_REMOTE]: CreateRemoteConnModal,
[constants.VIEW_REMOTE]: () => <ViewRemoteConnDetailModal />, [constants.VIEW_REMOTE]: ViewRemoteConnDetailModal,
[constants.EDIT_REMOTE]: () => <EditRemoteConnModal />, [constants.EDIT_REMOTE]: EditRemoteConnModal,
[constants.ALERT]: () => <AlertModal />, [constants.ALERT]: AlertModal,
[constants.SCREEN_SETTINGS]: () => <ScreenSettingsModal />, [constants.SCREEN_SETTINGS]: ScreenSettingsModal,
[constants.SESSION_SETTINGS]: () => <SessionSettingsModal />, [constants.SESSION_SETTINGS]: SessionSettingsModal,
[constants.LINE_SETTINGS]: () => <LineSettingsModal />, [constants.LINE_SETTINGS]: LineSettingsModal,
[constants.TAB_SWITCHER]: () => <TabSwitcherModal />, [constants.TAB_SWITCHER]: TabSwitcherModal,
[constants.USER_INPUT]: UserInputModal,
}; };
export { modalsRegistry }; export { modalsRegistry };
+15
View File
@@ -0,0 +1,15 @@
@import "../../../app/common/themes/themes.less";
.userinput-modal {
width: 500px;
.wave-modal-content {
.wave-modal-body {
padding: 20px 20px;
.userinput-query {
margin-bottom: 10px;
}
}
}
}
+88
View File
@@ -0,0 +1,88 @@
import * as React from "react";
import { GlobalModel } from "../../../models";
import { Choose, When, If } from "tsx-control-statements/components";
import { Modal, PasswordField, Markdown } from "../elements";
import { UserInputRequest } from "../../../types/types";
import "./userinput.less";
export const UserInputModal = (userInputRequest: UserInputRequest) => {
const [responseText, setResponseText] = React.useState("");
const [countdown, setCountdown] = React.useState(Math.floor(userInputRequest.timeoutms / 1000));
const closeModal = React.useCallback(() => {
GlobalModel.sendUserInput({
type: "userinputresp",
requestid: userInputRequest.requestid,
errormsg: "Canceled by the user",
});
GlobalModel.remotesModel.closeModal();
}, [responseText, userInputRequest]);
const handleSendText = React.useCallback(() => {
GlobalModel.sendUserInput({
type: "userinputresp",
requestid: userInputRequest.requestid,
text: responseText,
});
GlobalModel.remotesModel.closeModal();
}, [responseText, userInputRequest]);
const handleSendConfirm = React.useCallback(
(response: boolean) => {
GlobalModel.sendUserInput({
type: "userinputresp",
requestid: userInputRequest.requestid,
confirm: response,
});
GlobalModel.remotesModel.closeModal();
},
[userInputRequest]
);
React.useEffect(() => {
let timeout: ReturnType<typeof setTimeout>;
if (countdown == 0) {
timeout = setTimeout(() => {
GlobalModel.remotesModel.closeModal();
}, 300);
} else {
timeout = setTimeout(() => {
setCountdown(countdown - 1);
}, 1000);
}
return () => clearTimeout(timeout);
}, [countdown]);
return (
<Modal className="userinput-modal">
<Modal.Header onClose={closeModal} title={userInputRequest.title + ` (${countdown})`} />
<div className="wave-modal-body">
<div className="userinput-query">
<If condition={userInputRequest.markdown}>
<Markdown text={userInputRequest.querytext} />
</If>
<If condition={!userInputRequest.markdown}>{userInputRequest.querytext}</If>
</div>
<Choose>
<When condition={userInputRequest.responsetype == "text"}>
<PasswordField onChange={setResponseText} value={responseText} maxLength={400} />
</When>
</Choose>
</div>
<Choose>
<When condition={userInputRequest.responsetype == "text"}>
<Modal.Footer onCancel={closeModal} onOk={handleSendText} okLabel="Continue" />
</When>
<When condition={userInputRequest.responsetype == "confirm"}>
<Modal.Footer
onCancel={() => handleSendConfirm(false)}
onOk={() => handleSendConfirm(true)}
okLabel="Yes"
cancelLabel="No"
/>
</When>
</Choose>
</Modal>
);
};
@@ -54,6 +54,8 @@
} }
.remote-detail { .remote-detail {
width: 100%;
.settings-field { .settings-field {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
@@ -91,8 +93,9 @@
} }
.terminal-wrapper { .terminal-wrapper {
width: 100%;
margin-top: 5px; margin-top: 5px;
overflow-x: auto;
overflow-y: hidden;
.terminal-connectelem { .terminal-connectelem {
height: 163px !important; // Needed to override plugin height height: 163px !important; // Needed to override plugin height
@@ -111,7 +114,6 @@
.xterm-screen { .xterm-screen {
padding: 10px; padding: 10px;
width: 541px !important; // Needed to override plugin width
} }
} }
} }
@@ -12,12 +12,10 @@ import * as T from "../../../types/types";
import { Modal, Tooltip, Button, Status } from "../elements"; import { Modal, Tooltip, Button, Status } from "../elements";
import * as util from "../../../util/util"; import * as util from "../../../util/util";
import * as textmeasure from "../../../util/textmeasure"; import * as textmeasure from "../../../util/textmeasure";
import * as appconst from "../../appconst";
import "./viewremoteconndetail.less"; import "./viewremoteconndetail.less";
const RemotePtyRows = 9;
const RemotePtyCols = 80;
@mobxReact.observer @mobxReact.observer
class ViewRemoteConnDetailModal extends React.Component<{}, {}> { class ViewRemoteConnDetailModal extends React.Component<{}, {}> {
termRef: React.RefObject<any> = React.createRef(); termRef: React.RefObject<any> = React.createRef();
@@ -293,7 +291,7 @@ class ViewRemoteConnDetailModal extends React.Component<{}, {}> {
let model = this.model; let model = this.model;
let isTermFocused = this.model.remoteTermWrapFocus.get(); let isTermFocused = this.model.remoteTermWrapFocus.get();
let termFontSize = GlobalModel.termFontSize.get(); let termFontSize = GlobalModel.termFontSize.get();
let termWidth = textmeasure.termWidthFromCols(RemotePtyCols, termFontSize); let termWidth = textmeasure.termWidthFromCols(appconst.RemotePtyCols, termFontSize);
let remoteAliasText = util.isBlank(remote.remotealias) ? "(none)" : remote.remotealias; let remoteAliasText = util.isBlank(remote.remotealias) ? "(none)" : remote.remotealias;
let selectedRemoteStatus = this.getSelectedRemote().status; let selectedRemoteStatus = this.getSelectedRemote().status;
@@ -371,7 +369,7 @@ class ViewRemoteConnDetailModal extends React.Component<{}, {}> {
ref={this.termRef} ref={this.termRef}
data-remoteid={remote.remoteid} data-remoteid={remote.remoteid}
style={{ style={{
height: textmeasure.termHeightFromRows(RemotePtyRows, termFontSize), height: textmeasure.termHeightFromRows(appconst.RemotePtyRows, termFontSize),
width: termWidth, width: termWidth,
}} }}
></div> ></div>
+21 -14
View File
@@ -14,6 +14,7 @@ import { sprintf } from "sprintf-js";
import { v4 as uuidv4 } from "uuid"; import { v4 as uuidv4 } from "uuid";
import { checkKeyPressed, adaptFromElectronKeyEvent, setKeyUtilPlatform } from "../util/keyutil"; import { checkKeyPressed, adaptFromElectronKeyEvent, setKeyUtilPlatform } from "../util/keyutil";
import { platform } from "os"; import { platform } from "os";
import * as T from "../types/types";
const WaveAppPathVarName = "WAVETERM_APP_PATH"; const WaveAppPathVarName = "WAVETERM_APP_PATH";
const WaveDevVarName = "WAVETERM_DEV"; const WaveDevVarName = "WAVETERM_DEV";
@@ -37,21 +38,25 @@ ensureDir(waveHome);
// normalize darwin/x64 to darwin/amd64 for GOARCH compatibility // normalize darwin/x64 to darwin/amd64 for GOARCH compatibility
let unamePlatform = process.platform; let unamePlatform = process.platform;
let unameArch: string = process.arch; let unameArch: string = process.arch;
``;
if (unameArch == "x64") { if (unameArch == "x64") {
unameArch = "amd64"; unameArch = "amd64";
} }
let logger; let logger: winston.Logger;
let loggerConfig = { let transports: winston.transport[] = [
new winston.transports.File({ filename: path.join(waveHome, "waveterm-app.log"), level: "info" }),
];
if (isDev) {
transports.push(new winston.transports.Console());
}
let loggerConfig: winston.LoggerOptions = {
level: "info", level: "info",
format: winston.format.combine( format: winston.format.combine(
winston.format.timestamp({ format: "YYYY-MM-DD HH:mm:ss" }), winston.format.timestamp({ format: "YYYY-MM-DD HH:mm:ss" }),
winston.format.printf((info) => `${info.timestamp} ${info.message}`) winston.format.printf((info) => `${info.timestamp} ${info.message}`)
), ),
transports: [new winston.transports.File({ filename: path.join(waveHome, "waveterm-app.log"), level: "info" })], transports: transports,
}; };
if (isDev) {
loggerConfig.transports.push(new winston.transports.Console());
}
logger = winston.createLogger(loggerConfig); logger = winston.createLogger(loggerConfig);
function log(...msg) { function log(...msg) {
try { try {
@@ -75,7 +80,7 @@ if (isDev) {
} }
let app = electron.app; let app = electron.app;
app.setName(isDev ? "Wave (Dev)" : "Wave"); app.setName(isDev ? "Wave (Dev)" : "Wave");
let waveSrvProc = null; let waveSrvProc: child_process.ChildProcessWithoutNullStreams | null = null;
let waveSrvShouldRestart = false; let waveSrvShouldRestart = false;
electron.dialog.showErrorBox = (title, content) => { electron.dialog.showErrorBox = (title, content) => {
@@ -101,8 +106,8 @@ function checkPromptMigrate() {
// don't migrate if we're running dev version or if wave home directory already exists // don't migrate if we're running dev version or if wave home directory already exists
return; return;
} }
let homeDir = process.env.HOME; let homeDir: string = process.env.HOME as string;
let promptHome = path.join(homeDir, "prompt"); let promptHome: string = path.join(homeDir, "prompt");
if (!fs.existsSync(promptHome) || !fs.existsSync(path.join(promptHome, "prompt.db"))) { if (!fs.existsSync(promptHome) || !fs.existsSync(path.join(promptHome, "prompt.db"))) {
// make sure we have a valid prompt home directory (prompt.db must exist inside) // make sure we have a valid prompt home directory (prompt.db must exist inside)
return; return;
@@ -170,7 +175,7 @@ function readAuthKey() {
return authKeyStr.trim(); return authKeyStr.trim();
} }
let menuTemplate = [ let menuTemplate: (Electron.MenuItem | Electron.MenuItemConstructorOptions)[] = [
{ {
role: "appMenu", role: "appMenu",
submenu: [ submenu: [
@@ -250,7 +255,7 @@ function shFrameNavHandler(event: any, url: any) {
return; return;
} }
function createMainWindow(clientData) { function createMainWindow(clientData: T.ClientDataType | null) {
let bounds = calcBounds(clientData); let bounds = calcBounds(clientData);
setKeyUtilPlatform(platform()); setKeyUtilPlatform(platform());
let win = new electron.BrowserWindow({ let win = new electron.BrowserWindow({
@@ -609,7 +614,9 @@ function runWaveSrv() {
pReject(new Error(sprintf("failed to start local server (%s)", waveSrvCmd))); pReject(new Error(sprintf("failed to start local server (%s)", waveSrvCmd)));
if (waveSrvShouldRestart) { if (waveSrvShouldRestart) {
waveSrvShouldRestart = false; waveSrvShouldRestart = false;
this.runWaveSrv(); // small timeout here so we can reuse the listen socket
// this is normally only used in dev mode for debugging
setTimeout(runWaveSrv, 500);
} }
}); });
proc.on("spawn", (e) => { proc.on("spawn", (e) => {
@@ -644,7 +651,7 @@ electron.ipcMain.on("context-editmenu", (event, { x, y }, opts) => {
} }
console.log("context-editmenu"); console.log("context-editmenu");
let menu = new electron.Menu(); let menu = new electron.Menu();
let menuItem = null; let menuItem: Electron.MenuItem;
if (opts.showCut) { if (opts.showCut) {
menuItem = new electron.MenuItem({ label: "Cut", role: "cut" }); menuItem = new electron.MenuItem({ label: "Cut", role: "cut" });
menu.append(menuItem); menu.append(menuItem);
@@ -657,7 +664,7 @@ electron.ipcMain.on("context-editmenu", (event, { x, y }, opts) => {
}); });
async function createMainWindowWrap() { async function createMainWindowWrap() {
let clientData = null; let clientData: T.ClientDataType | null = null;
try { try {
clientData = await getClientDataPoll(1); clientData = await getClientDataPoll(1);
} catch (e) { } catch (e) {
+10 -15
View File
@@ -13,10 +13,10 @@ import {
HistoryQueryOpts, HistoryQueryOpts,
HistoryTypeStrs, HistoryTypeStrs,
OpenAICmdInfoChatMessageType, OpenAICmdInfoChatMessageType,
OV,
StrWithPos,
} from "../types/types"; } from "../types/types";
import { StrWithPos } from "../types/types";
import * as appconst from "../app/appconst"; import * as appconst from "../app/appconst";
import { OV } from "../types/types";
import { Model } from "./model"; import { Model } from "./model";
import { GlobalCommandRunner } from "./global"; import { GlobalCommandRunner } from "./global";
@@ -207,7 +207,6 @@ class InputModel {
this.historyQueryOpts.set(opts); this.historyQueryOpts.set(opts);
let bestIndex = this.findBestNewIndex(oldItem); let bestIndex = this.findBestNewIndex(oldItem);
setTimeout(() => this.setHistoryIndex(bestIndex, true), 10); setTimeout(() => this.setHistoryIndex(bestIndex, true), 10);
return;
})(); })();
} }
@@ -624,13 +623,17 @@ class InputModel {
} }
openAIAssistantChat(): void { openAIAssistantChat(): void {
this.aIChatShow.set(true); mobx.action(() => {
this.setAIChatFocus(); this.aIChatShow.set(true);
this.setAIChatFocus();
})();
} }
closeAIAssistantChat(): void { closeAIAssistantChat(): void {
this.aIChatShow.set(false); mobx.action(() => {
this.giveFocus(); this.aIChatShow.set(false);
this.giveFocus();
})();
} }
clearAIAssistantChat(): void { clearAIAssistantChat(): void {
@@ -721,14 +724,6 @@ class InputModel {
setCurLine(val: string): void { setCurLine(val: string): void {
let hidx = this.historyIndex.get(); let hidx = this.historyIndex.get();
mobx.action(() => { mobx.action(() => {
// if (val == "\" ") {
// this.setInputMode("comment");
// val = "";
// }
// if (val == "//") {
// this.setInputMode("global");
// val = "";
// }
if (this.modHistory.length <= hidx) { if (this.modHistory.length <= hidx) {
this.modHistory.length = hidx + 1; this.modHistory.length = hidx + 1;
} }
+3 -3
View File
@@ -8,14 +8,14 @@ import { modalsRegistry } from "../app/common/modals/registry";
import { OArr } from "../types/types"; import { OArr } from "../types/types";
class ModalsModel { class ModalsModel {
store: OArr<ModalStoreEntry> = mobx.observable.array([], { name: "ModalsModel-store" }); store: OArr<ModalStoreEntry> = mobx.observable.array([], { name: "ModalsModel-store", deep: false });
pushModal(modalId: string) { pushModal(modalId: string, props?: any) {
const modalFactory = modalsRegistry[modalId]; const modalFactory = modalsRegistry[modalId];
if (modalFactory && !this.store.some((modal) => modal.id === modalId)) { if (modalFactory && !this.store.some((modal) => modal.id === modalId)) {
mobx.action(() => { mobx.action(() => {
this.store.push({ id: modalId, component: modalFactory, uniqueKey: uuidv4() }); this.store.push({ id: modalId, component: modalFactory, uniqueKey: uuidv4(), props });
})(); })();
} }
} }
+299 -242
View File
File diff suppressed because it is too large Load Diff
@@ -51,6 +51,8 @@ import {
HistoryViewDataType, HistoryViewDataType,
AlertMessageType, AlertMessageType,
HistorySearchParams, HistorySearchParams,
UserInputRequest,
UserInputResponsePacket,
FocusTypeStrs, FocusTypeStrs,
ScreenLinesType, ScreenLinesType,
HistoryTypeStrs, HistoryTypeStrs,
@@ -3376,14 +3378,14 @@ class RemotesModel {
} }
class ModalsModel { class ModalsModel {
store: OArr<T.ModalStoreEntry> = mobx.observable.array([], { name: "ModalsModel-store" }); store: OArr<T.ModalStoreEntry> = mobx.observable.array([], { name: "ModalsModel-store", deep: false });
pushModal(modalId: string) { pushModal(modalId: string, props?: any) {
const modalFactory = modalsRegistry[modalId]; const modalFactory = modalsRegistry[modalId];
if (modalFactory && !this.store.some((modal) => modal.id === modalId)) { if (modalFactory && !this.store.some((modal) => modal.id === modalId)) {
mobx.action(() => { mobx.action(() => {
this.store.push({ id: modalId, component: modalFactory, uniqueKey: uuidv4() }); this.store.push({ id: modalId, component: modalFactory, uniqueKey: uuidv4(), props: props });
})(); })();
} }
} }
@@ -4184,6 +4186,10 @@ class Model {
this.getScreenById_single(snc.screenid)?.setNumRunningCmds(snc.num); this.getScreenById_single(snc.screenid)?.setNumRunningCmds(snc.num);
} }
} }
if ("userinputrequest" in update) {
let userInputRequest: UserInputRequest = update.userinputrequest;
this.modalsModel.pushModal(appconst.USER_INPUT, userInputRequest);
}
} }
updateRemotes(remotes: RemoteType[]): void { updateRemotes(remotes: RemoteType[]): void {
@@ -4591,6 +4597,10 @@ class Model {
this.ws.pushMessage(inputPacket); this.ws.pushMessage(inputPacket);
} }
sendUserInput(userInputResponsePacket: UserInputResponsePacket) {
this.ws.pushMessage(userInputResponsePacket);
}
sendCmdInputText(screenId: string, sp: T.StrWithPos) { sendCmdInputText(screenId: string, sp: T.StrWithPos) {
let pk: T.CmdInputTextPacketType = { let pk: T.CmdInputTextPacketType = {
type: "cmdinputtext", type: "cmdinputtext",
+3 -6
View File
@@ -12,9 +12,6 @@ import { GlobalCommandRunner } from "./global";
import { Model } from "./model"; import { Model } from "./model";
import { getTermPtyData } from "../util/modelutil"; import { getTermPtyData } from "../util/modelutil";
const RemotePtyRows = 8; // also in main.tsx
const RemotePtyCols = 80;
class RemotesModel { class RemotesModel {
globalModel: Model; globalModel: Model;
selectedRemoteId: OV<string> = mobx.observable.box(null, { selectedRemoteId: OV<string> = mobx.observable.box(null, {
@@ -180,14 +177,14 @@ class RemotesModel {
return; return;
} }
let termOpts = { let termOpts = {
rows: RemotePtyRows, rows: appconst.RemotePtyRows,
cols: RemotePtyCols, cols: appconst.RemotePtyCols,
flexrows: false, flexrows: false,
maxptysize: 64 * 1024, maxptysize: 64 * 1024,
}; };
let termWrap = new TermWrap(elem, { let termWrap = new TermWrap(elem, {
termContext: { remoteId: remoteId }, termContext: { remoteId: remoteId },
usedRows: RemotePtyRows, usedRows: appconst.RemotePtyRows,
termOpts: termOpts, termOpts: termOpts,
winSize: null, winSize: null,
keyHandler: (e, termWrap) => { keyHandler: (e, termWrap) => {
+1 -1
View File
@@ -139,7 +139,7 @@ class ScreenLines {
return; return;
} }
let lineIdx = 0; let lineIdx = 0;
for (lineIdx = 0; lineIdx < lines.length; lineIdx++) { for (lineIdx; lineIdx < lines.length; lineIdx++) {
let lineId = lines[lineIdx].lineid; let lineId = lines[lineIdx].lineid;
let curTs = lines[lineIdx].ts; let curTs = lines[lineIdx].ts;
if (lineId == line.lineid) { if (lineId == line.lineid) {
+2 -2
View File
@@ -3,10 +3,10 @@
.terminal-wrapper { .terminal-wrapper {
.xterm-viewport { .xterm-viewport {
overflow-y: hidden; overflow: hidden;
} }
&:hover .xterm-viewport, &:hover .xterm-viewport,
&:focus-within .xterm-viewport { &:focus-within .xterm-viewport {
overflow-y: auto; overflow: auto;
} }
} }
+66 -15
View File
@@ -29,7 +29,6 @@ type SessionDataType = {
// for updates // for updates
remove?: boolean; remove?: boolean;
full?: boolean;
}; };
type LineStateType = { [k: string]: any }; type LineStateType = { [k: string]: any };
@@ -92,7 +91,6 @@ type ScreenDataType = {
anchor: { anchorline: number; anchoroffset: number }; anchor: { anchorline: number; anchoroffset: number };
// for updates // for updates
full?: boolean;
remove?: boolean; remove?: boolean;
}; };
@@ -260,6 +258,11 @@ type CmdDataType = {
restarted?: boolean; restarted?: boolean;
}; };
type LineUpdateType = {
line: LineType;
cmd: CmdDataType;
};
type PtyDataUpdateType = { type PtyDataUpdateType = {
screenid: string; screenid: string;
lineid: string; lineid: string;
@@ -309,30 +312,48 @@ type ScreenNumRunningCommandsUpdateType = {
num: number; num: number;
}; };
type ConnectUpdateType = {
sessions: SessionDataType[];
screens: ScreenDataType[];
remotes: RemoteType[];
screenstatusindicators: ScreenStatusIndicatorUpdateType[];
screennumrunningcommands: ScreenNumRunningCommandsUpdateType[];
activesessionid: string;
};
type BookmarksUpdateType = {
bookmarks: BookmarkType[];
selectedbookmark: string;
};
type MainViewUpdateType = {
mainview: string;
historyview?: HistoryViewDataType;
bookmarksview?: BookmarksUpdateType;
};
type ModelUpdateType = { type ModelUpdateType = {
interactive: boolean; interactive: boolean;
sessions?: SessionDataType[]; session?: SessionDataType;
activesessionid?: string; activesessionid?: string;
screens?: ScreenDataType[]; screen?: ScreenDataType;
screenlines?: ScreenLinesType; screenlines?: ScreenLinesType;
line?: LineType; line?: LineUpdateType;
lines?: LineType[];
cmd?: CmdDataType; cmd?: CmdDataType;
info?: InfoType; info?: InfoType;
cmdline?: StrWithPos; cmdline?: StrWithPos;
remotes?: RemoteType[]; remote?: RemoteType;
history?: HistoryInfoType; history?: HistoryInfoType;
connect?: boolean; connect?: ConnectUpdateType;
mainview?: string; mainview?: MainViewUpdateType;
bookmarks?: BookmarkType[]; bookmarks?: BookmarksUpdateType;
selectedbookmark?: string;
clientdata?: ClientDataType; clientdata?: ClientDataType;
historyviewdata?: HistoryViewDataType;
remoteview?: RemoteViewType; remoteview?: RemoteViewType;
openaicmdinfochat?: OpenAICmdInfoChatMessageType[]; openaicmdinfochat?: OpenAICmdInfoChatMessageType[];
alertmessage?: AlertMessageType; alertmessage?: AlertMessageType;
screenstatusindicators?: ScreenStatusIndicatorUpdateType[]; screenstatusindicator?: ScreenStatusIndicatorUpdateType;
screennumrunningcommands?: ScreenNumRunningCommandsUpdateType[]; screennumrunningcommands?: ScreenNumRunningCommandsUpdateType;
userinputrequest?: UserInputRequest;
}; };
type HistoryViewDataType = { type HistoryViewDataType = {
@@ -414,7 +435,7 @@ type ContextMenuOpts = {
showCut?: boolean; showCut?: boolean;
}; };
type UpdateMessage = PtyDataUpdateType | ModelUpdateType; type UpdateMessage = PtyDataUpdateType | ModelUpdateType[];
type RendererContext = { type RendererContext = {
screenId: string; screenId: string;
@@ -539,6 +560,14 @@ type ReleaseInfoType = {
latestversion: string; latestversion: string;
}; };
type ClientWinSizeType = {
width: number;
height: number;
top: number;
left: number;
fullscreen: boolean;
};
type ClientDataType = { type ClientDataType = {
clientid: string; clientid: string;
userid: string; userid: string;
@@ -548,6 +577,7 @@ type ClientDataType = {
dbversion: number; dbversion: number;
openaiopts?: OpenAIOptsType; openaiopts?: OpenAIOptsType;
releaseinfo?: ReleaseInfoType; releaseinfo?: ReleaseInfoType;
winsize: ClientWinSizeType;
}; };
type OpenAIOptsType = { type OpenAIOptsType = {
@@ -595,6 +625,23 @@ type HistorySearchParams = {
filterCmds?: boolean; filterCmds?: boolean;
}; };
type UserInputRequest = {
requestid: string;
querytext: string;
responsetype: string;
title: string;
markdown: boolean;
timeoutms: number;
};
type UserInputResponsePacket = {
type: string;
requestid: string;
text?: string;
confirm?: boolean;
errormsg?: string;
};
type RenderModeType = "normal" | "collapsed" | "expanded"; type RenderModeType = "normal" | "collapsed" | "expanded";
type WebScreen = { type WebScreen = {
@@ -730,6 +777,7 @@ type ModalStoreEntry = {
id: string; id: string;
component: React.ComponentType; component: React.ComponentType;
uniqueKey: string; uniqueKey: string;
props?: any;
}; };
type StrWithPos = { type StrWithPos = {
@@ -809,6 +857,8 @@ export type {
RenderModeType, RenderModeType,
AlertMessageType, AlertMessageType,
HistorySearchParams, HistorySearchParams,
UserInputRequest,
UserInputResponsePacket,
ScreenLinesType, ScreenLinesType,
FocusTypeStrs, FocusTypeStrs,
HistoryTypeStrs, HistoryTypeStrs,
@@ -845,6 +895,7 @@ export type {
CmdInputTextPacketType, CmdInputTextPacketType,
OpenAICmdInfoChatMessageType, OpenAICmdInfoChatMessageType,
ScreenStatusIndicatorUpdateType, ScreenStatusIndicatorUpdateType,
ScreenNumRunningCommandsUpdateType,
OV, OV,
OArr, OArr,
OMap, OMap,
+53 -69
View File
@@ -1,4 +1,4 @@
// Copyright 2023, Command Line Inc. // Copyright 2024, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: Apache-2.0
import * as mobx from "mobx"; import * as mobx from "mobx";
@@ -17,7 +17,7 @@ function isBlank(s: string): boolean {
} }
function handleNotOkResp(resp: any, url: URL): Promise<any> { function handleNotOkResp(resp: any, url: URL): Promise<any> {
let errMsg = sprintf( const errMsg = sprintf(
"Bad status code response from fetch '%s': code=%d %s", "Bad status code response from fetch '%s': code=%d %s",
url.toString(), url.toString(),
resp.status, resp.status,
@@ -41,18 +41,18 @@ function handleNotOkResp(resp: any, url: URL): Promise<any> {
} }
function fetchJsonData(resp: any, ctErr: boolean): Promise<any> { function fetchJsonData(resp: any, ctErr: boolean): Promise<any> {
let contentType = resp.headers.get("Content-Type"); const contentType = resp.headers.get("Content-Type");
if (contentType != null && contentType.startsWith("application/json")) { if (contentType?.startsWith("application/json")) {
return resp.text().then((textData) => { return resp.text().then((textData) => {
let rtnData: any = null; let rtnData: any = null;
try { try {
rtnData = JSON.parse(textData); rtnData = JSON.parse(textData);
} catch (err) { } catch (err) {
let errMsg = sprintf("Unparseable JSON: " + err.message); const errMsg = sprintf("Unparseable JSON: " + err.message);
let rtnErr = new Error(errMsg); const rtnErr = new Error(errMsg);
throw rtnErr; throw rtnErr;
} }
if (rtnData != null && rtnData.error) { if (rtnData?.error) {
throw new Error(rtnData.error); throw new Error(rtnData.error);
} }
return rtnData; return rtnData;
@@ -67,23 +67,23 @@ function handleJsonFetchResponse(url: URL, resp: any): Promise<any> {
if (!resp.ok) { if (!resp.ok) {
return handleNotOkResp(resp, url); return handleNotOkResp(resp, url);
} }
let rtnData = fetchJsonData(resp, true); const rtnData = fetchJsonData(resp, true);
return rtnData; return rtnData;
} }
function base64ToString(b64: string): string { function base64ToString(b64: string): string {
let stringBytes = base64.toByteArray(b64); const stringBytes = base64.toByteArray(b64);
return new TextDecoder().decode(stringBytes); return new TextDecoder().decode(stringBytes);
} }
function stringToBase64(input: string): string { function stringToBase64(input: string): string {
let stringBytes = new TextEncoder().encode(input); const stringBytes = new TextEncoder().encode(input);
return base64.fromByteArray(stringBytes); return base64.fromByteArray(stringBytes);
} }
function base64ToArray(b64: string): Uint8Array { function base64ToArray(b64: string): Uint8Array {
let rawStr = atob(b64); const rawStr = atob(b64);
let rtnArr = new Uint8Array(new ArrayBuffer(rawStr.length)); const rtnArr = new Uint8Array(new ArrayBuffer(rawStr.length));
for (let i = 0; i < rawStr.length; i++) { for (let i = 0; i < rawStr.length; i++) {
rtnArr[i] = rawStr.charCodeAt(i); rtnArr[i] = rawStr.charCodeAt(i);
} }
@@ -92,7 +92,6 @@ function base64ToArray(b64: string): Uint8Array {
interface IDataType { interface IDataType {
remove?: boolean; remove?: boolean;
full?: boolean;
} }
interface IObjType<DataType> { interface IObjType<DataType> {
@@ -113,31 +112,28 @@ function genMergeSimpleData<T extends ISimpleDataType>(
if (dataArr == null || dataArr.length == 0) { if (dataArr == null || dataArr.length == 0) {
return; return;
} }
let objMap: Record<string, T> = {}; const objMap: Record<string, T> = {};
for (let i = 0; i < objs.length; i++) { for (const obj of objs) {
let obj = objs[i]; const id = idFn(obj);
let id = idFn(obj);
objMap[id] = obj; objMap[id] = obj;
} }
for (let i = 0; i < dataArr.length; i++) { for (const dataItem of dataArr) {
let dataItem = dataArr[i];
if (dataItem == null) { if (dataItem == null) {
console.log("genMergeSimpleData, null item"); console.log("genMergeSimpleData, null item");
console.trace(); console.trace();
} }
let id = idFn(dataItem); const id = idFn(dataItem);
if (dataItem.remove) { if (dataItem.remove) {
delete objMap[id]; delete objMap[id];
continue;
} else { } else {
objMap[id] = dataItem; objMap[id] = dataItem;
} }
} }
let newObjs = Object.values(objMap); const newObjs = Object.values(objMap);
if (sortIdxFn) { if (sortIdxFn) {
newObjs.sort((a, b) => { newObjs.sort((a, b) => {
let astr = sortIdxFn(a); const astr = sortIdxFn(a);
let bstr = sortIdxFn(b); const bstr = sortIdxFn(b);
return astr.localeCompare(bstr); return astr.localeCompare(bstr);
}); });
} }
@@ -155,20 +151,18 @@ function genMergeData<ObjType extends IObjType<DataType>, DataType extends IData
if (dataArr == null || dataArr.length == 0) { if (dataArr == null || dataArr.length == 0) {
return; return;
} }
let objMap: Record<string, ObjType> = {}; const objMap: Record<string, ObjType> = {};
for (let i = 0; i < objs.length; i++) { for (const obj of objs) {
let obj = objs[i]; const id = objIdFn(obj);
let id = objIdFn(obj);
objMap[id] = obj; objMap[id] = obj;
} }
for (let i = 0; i < dataArr.length; i++) { for (const dataItem of dataArr) {
let dataItem = dataArr[i];
if (dataItem == null) { if (dataItem == null) {
console.log("genMergeData, null item"); console.log("genMergeData, null item");
console.trace(); console.trace();
continue; continue;
} }
let id = dataIdFn(dataItem); const id = dataIdFn(dataItem);
let obj = objMap[id]; let obj = objMap[id];
if (dataItem.remove) { if (dataItem.remove) {
if (obj != null) { if (obj != null) {
@@ -178,17 +172,13 @@ function genMergeData<ObjType extends IObjType<DataType>, DataType extends IData
continue; continue;
} }
if (obj == null) { if (obj == null) {
if (!dataItem.full) {
console.log("cannot create object, dataitem is not full", objs, dataItem);
continue;
}
obj = ctorFn(dataItem); obj = ctorFn(dataItem);
objMap[id] = obj; objMap[id] = obj;
continue; continue;
} }
obj.mergeData(dataItem); obj.mergeData(dataItem);
} }
let newObjs = Object.values(objMap); const newObjs = Object.values(objMap);
if (sortIdxFn) { if (sortIdxFn) {
newObjs.sort((a, b) => { newObjs.sort((a, b) => {
return sortIdxFn(a) - sortIdxFn(b); return sortIdxFn(a) - sortIdxFn(b);
@@ -204,18 +194,17 @@ function genMergeDataMap<ObjType extends IObjType<DataType>, DataType extends ID
dataIdFn: (data: DataType) => string, dataIdFn: (data: DataType) => string,
ctorFn: (data: DataType) => ObjType ctorFn: (data: DataType) => ObjType
): { added: string[]; removed: string[] } { ): { added: string[]; removed: string[] } {
let rtn: { added: string[]; removed: string[] } = { added: [], removed: [] }; const rtn: { added: string[]; removed: string[] } = { added: [], removed: [] };
if (dataArr == null || dataArr.length == 0) { if (dataArr == null || dataArr.length == 0) {
return rtn; return rtn;
} }
for (let i = 0; i < dataArr.length; i++) { for (const dataItem of dataArr) {
let dataItem = dataArr[i];
if (dataItem == null) { if (dataItem == null) {
console.log("genMergeDataMap, null item"); console.log("genMergeDataMap, null item");
console.trace(); console.trace();
continue; continue;
} }
let id = dataIdFn(dataItem); const id = dataIdFn(dataItem);
let obj = objMap.get(id); let obj = objMap.get(id);
if (dataItem.remove) { if (dataItem.remove) {
if (obj != null) { if (obj != null) {
@@ -226,10 +215,6 @@ function genMergeDataMap<ObjType extends IObjType<DataType>, DataType extends ID
continue; continue;
} }
if (obj == null) { if (obj == null) {
if (!dataItem.full) {
console.log("cannot create object, dataitem is not full", dataItem);
continue;
}
obj = ctorFn(dataItem); obj = ctorFn(dataItem);
objMap.set(id, obj); objMap.set(id, obj);
rtn.added.push(id); rtn.added.push(id);
@@ -262,23 +247,23 @@ function incObs(inum: mobx.IObservableValue<number>) {
// @check:font // @check:font
function loadFonts() { function loadFonts() {
let jbmFontNormal = new FontFace("JetBrains Mono", "url('public/fonts/jetbrains-mono-v13-latin-regular.woff2')", { const jbmFontNormal = new FontFace("JetBrains Mono", "url('public/fonts/jetbrains-mono-v13-latin-regular.woff2')", {
style: "normal", style: "normal",
weight: "400", weight: "400",
}); });
let jbmFont200 = new FontFace("JetBrains Mono", "url('public/fonts/jetbrains-mono-v13-latin-200.woff2')", { const jbmFont200 = new FontFace("JetBrains Mono", "url('public/fonts/jetbrains-mono-v13-latin-200.woff2')", {
style: "normal", style: "normal",
weight: "200", weight: "200",
}); });
let jbmFont700 = new FontFace("JetBrains Mono", "url('public/fonts/jetbrains-mono-v13-latin-700.woff2')", { const jbmFont700 = new FontFace("JetBrains Mono", "url('public/fonts/jetbrains-mono-v13-latin-700.woff2')", {
style: "normal", style: "normal",
weight: "700", weight: "700",
}); });
let faFont = new FontFace("FontAwesome", "url(public/fonts/fontawesome-webfont-4.7.woff2)", { const faFont = new FontFace("FontAwesome", "url(public/fonts/fontawesome-webfont-4.7.woff2)", {
style: "normal", style: "normal",
weight: "normal", weight: "normal",
}); });
let docFonts: any = document.fonts; // work around ts typing issue const docFonts: any = document.fonts; // work around ts typing issue
docFonts.add(jbmFontNormal); docFonts.add(jbmFontNormal);
docFonts.add(jbmFont200); docFonts.add(jbmFont200);
docFonts.add(jbmFont700); docFonts.add(jbmFont700);
@@ -296,13 +281,13 @@ function getTodayStr(): string {
} }
function getYesterdayStr(): string { function getYesterdayStr(): string {
let d = new Date(); const d = new Date();
d.setDate(d.getDate() - 1); d.setDate(d.getDate() - 1);
return getDateStr(d); return getDateStr(d);
} }
function getDateStr(d: Date): string { function getDateStr(d: Date): string {
let yearStr = String(d.getFullYear()); const yearStr = String(d.getFullYear());
let monthStr = String(d.getMonth() + 1); let monthStr = String(d.getMonth() + 1);
if (monthStr.length == 1) { if (monthStr.length == 1) {
monthStr = "0" + monthStr; monthStr = "0" + monthStr;
@@ -311,31 +296,30 @@ function getDateStr(d: Date): string {
if (dayStr.length == 1) { if (dayStr.length == 1) {
dayStr = "0" + dayStr; dayStr = "0" + dayStr;
} }
let dowStr = DOW_STRS[d.getDay()]; const dowStr = DOW_STRS[d.getDay()];
return dowStr + " " + yearStr + "-" + monthStr + "-" + dayStr; return dowStr + " " + yearStr + "-" + monthStr + "-" + dayStr;
} }
function getRemoteConnVal(r: RemoteType): number { function getRemoteConnVal(r: RemoteType): number {
if (r.status == "connected") { switch (r.status) {
return 1; case "connected":
return 1;
case "connecting":
return 2;
case "disconnected":
return 3;
case "error":
return 4;
default:
return 5;
} }
if (r.status == "connecting") {
return 2;
}
if (r.status == "disconnected") {
return 3;
}
if (r.status == "error") {
return 4;
}
return 5;
} }
function sortAndFilterRemotes(origRemotes: RemoteType[]): RemoteType[] { function sortAndFilterRemotes(origRemotes: RemoteType[]): RemoteType[] {
let remotes = origRemotes.filter((r) => !r.archived); const remotes = origRemotes.filter((r) => !r.archived);
remotes.sort((a, b) => { remotes.sort((a, b) => {
let connValA = getRemoteConnVal(a); const connValA = getRemoteConnVal(a);
let connValB = getRemoteConnVal(b); const connValB = getRemoteConnVal(b);
if (connValA != connValB) { if (connValA != connValB) {
return connValA - connValB; return connValA - connValB;
} }
@@ -372,7 +356,7 @@ function openLink(url: string): void {
window.open(url, "_blank"); window.open(url, "_blank");
} }
function getColorRGB(colorInput) { function getColorRGB(colorInput: string) {
const tempElement = document.createElement("div"); const tempElement = document.createElement("div");
tempElement.style.color = colorInput; tempElement.style.color = colorInput;
document.body.appendChild(tempElement); document.body.appendChild(tempElement);
@@ -399,7 +383,7 @@ function getRemoteName(remote: RemoteType): string {
if (remote == null) { if (remote == null) {
return ""; return "";
} }
let { remotealias, remotecanonicalname } = remote; const { remotealias, remotecanonicalname } = remote;
return remotealias ? `${remotealias} [${remotecanonicalname}]` : remotecanonicalname; return remotealias ? `${remotealias} [${remotecanonicalname}]` : remotecanonicalname;
} }

Some files were not shown because too many files have changed in this diff Show More