fix: update with fixes from new version

This commit is contained in:
Sylvia Crowe 2024-09-17 13:18:34 -07:00
commit 33f2532ae8
26 changed files with 1966 additions and 1703 deletions

View File

@ -16,6 +16,10 @@ jobs:
arch: "amd64"
runner: "ubuntu-latest"
scripthaus: "build-package-linux"
- platform: "linux"
arch: "arm64"
runner: ubuntu-24.04-arm64-16core
scripthaus: "build-package-linux"
runs-on: ${{ matrix.runner }}
steps:
- uses: actions/checkout@v4
@ -28,6 +32,8 @@ jobs:
run: |
sudo apt-get update
sudo apt-get install --no-install-recommends -y libarchive-tools libopenjp2-tools rpm
- name: Install FPM # The version of FPM that comes bundled with electron-builder doesn't include a Linux ARM target. Installing Gems onto the runner is super quick so we'll just do this for all targets.
run: sudo gem install fpm
- uses: actions/setup-go@v5
with:
go-version: ${{env.GO_VERSION}}
@ -58,6 +64,7 @@ jobs:
run: scripthaus run ${{ matrix.scripthaus }}
env:
GOARCH: ${{ matrix.arch }}
USE_SYSTEM_FPM: true # Ensure that the installed version of FPM is used rather than the bundled one.
CSC_LINK: ${{ matrix.platform == 'darwin' && secrets.PROD_MACOS_CERTIFICATE}}
CSC_KEY_PASSWORD: ${{ matrix.platform == 'darwin' && secrets.PROD_MACOS_CERTIFICATE_PWD }}
APPLE_ID: ${{ matrix.platform == 'darwin' && secrets.PROD_MACOS_NOTARIZATION_APPLE_ID }}

View File

@ -1,4 +1,4 @@
name: TestDriver.ai Regression Testing
name: TestDriver.ai Regression Testing - Waveterm
on:
push:
branches:
@ -14,53 +14,38 @@ permissions:
contents: read # To allow the action to read repository contents
pull-requests: write # To allow the action to create/update pull request comments
jobs:
test:
name: TestDriver
runs-on: ubuntu-latest
steps:
- uses: dashcamio/testdriver@main
id: testdriver
with:
version: v2.12.12
prerun: |
rm ~/Desktop/WITH-LOVE-FROM-AMERICA.txt
cd ~/actions-runner/_work/testdriver/testdriver/
brew install go
brew tap scripthaus-dev/scripthaus
brew install corepack
brew install scripthaus
corepack enable
yarn install
scripthaus run build-backend
echo "Yarn"
yarn
echo "Rebuild"
scripthaus run electron-rebuild
echo "Webpack"
scripthaus run webpack-build
echo "Starting Electron"
scripthaus run electron 1>/dev/null 2>&1 &
echo "Electron Done"
exit
prompt: |
1. wait 10 seconds
1. click "Get Started"
1. validate that overlapping text does not appear in the application
1. focus the Wave input with the keyboard shorcut Command + I
1. type 'ls' into the input
1. press return
1. validate Wave shows the result of 'ls'
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- uses: peter-evans/create-or-update-comment@v4
if: ${{always()}}
with:
issue-number: ${{ github.event.pull_request.number }}
body: |
## TestDriver Summary
${{ steps.testdriver.outputs.markdown }}
${{ steps.testdriver.outputs.summary }}
reactions: |
+1
-1
test:
name: "TestDriver"
runs-on: ubuntu-latest
steps:
- uses: dashcamio/testdriver@main
id: testdriver
with:
version: v3.9.0
key: ${{secrets.DASHCAM_API}}
os: mac
prerun: |
cd ~/actions-runner/_work/testdriver/testdriver/
brew install go
brew tap scripthaus-dev/scripthaus
brew install corepack
brew install scripthaus
corepack enable
yarn install
scripthaus run build-backend
echo "Yarn"
yarn
echo "Rebuild"
scripthaus run electron-rebuild
echo "Webpack"
scripthaus run webpack-build
echo "Starting Electron"
scripthaus run electron 1>/dev/null 2>&1 &
echo "Electron Done"
cd /Users/ec2-user/Downloads/td/
npm rebuild
exit
prompt: |
1. /run /Users/ec2-user/actions-runner/_work/testdriver/testdriver/.testdriver/wave1.yml

View File

@ -1,18 +0,0 @@
rm ~/Desktop/WITH-LOVE-FROM-AMERICA.txt
cd ~/actions-runner/_work/testdriver/testdriver/
brew install go
brew tap scripthaus-dev/scripthaus
brew install scripthaus
corepack enable
yarn install
scripthaus run build-backend
echo "Yarn"
yarn
echo "Rebuild"
scripthaus run electron-rebuild
echo "Webpack"
scripthaus run webpack-build
echo "Starting Electron"
scripthaus run electron 1>/dev/null 2>&1 &
echo "Electron Done"
exit

37
.testdriver/wave1.yml Normal file
View File

@ -0,0 +1,37 @@
version: 3.8.0
steps:
- prompt: "Focus electron"
commands:
- command: focus-application
name: Electron
- command: hover-text
description: Get started CTA
text: Get Started
action: click
- command: hover-text
description: Settings button
text: Settings
action: click
- command: hover-text
description: font size 13
text: 13px
action: click
- command: hover-text
description: font size 12
text: 12px
action: click
- command: hover-text
description: theme selector
text: Dark
action: click
- command: hover-text
description: theme color white
text: Light
action: click
- command: hover-text
description: workspace
text: workspace-1
action: click
- command: assert
expect: the terminal is white

View File

@ -309,10 +309,6 @@
"command": "aichat:clearHistory",
"keys": ["Ctrl:l"]
},
{
"command": "aichat:setCmdInputValue",
"keys": ["Ctrl:Shift:e"]
},
{
"command": "terminal:copy",
"keys": ["Ctrl:Shift:c"]

View File

@ -34,20 +34,23 @@ You'll now have to move the built `scripthaus` binary to a directory in your pat
sudo cp scripthaus /usr/local/bin
```
## Install nodejs, npm, and yarn
## Install nodejs and yarn
We use [nvm](https://github.com/nvm-sh/nvm) to install nodejs on Linux (you can use an alternate installer if you wish). You must have a relatively recent version of node in order to build the terminal. Different distributions and shells will require different setup instructions. These instructions work for Ubuntu 22 using bash (will install node v20.8.1):
You also need a relatively modern nodejs with npm and yarn installed.
```
wget -qO- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.5/install.sh | bash
source ~/.bashrc
nvm install v20.8.1
```
Node can be installed from [https://nodejs.org](https://nodejs.org).
Now we can install yarn:
We use Yarn Modern to manage our packages. The recommended way to install Yarn Modern is using Corepack, a new utility shipped by NodeJS that lets you manage your package manager versioning as you would any packages.
```
npm install -g yarn
If you installed NodeJS from the official feed (via the website or using NVM), this should come preinstalled. If you use Homebrew or some other feed, you may need to manually install Corepack using `npm install -g corepack`.
For more information on Corepack, check out [this link](https://yarnpkg.com/corepack).
Once you've verified that you have Corepack installed, run the following script to set up Yarn for the repository:
```sh
corepack enable
yarn install
```
## Clone the Wave Repo

View File

@ -18,95 +18,95 @@
"appId": "dev.commandline.waveterm"
},
"dependencies": {
"@lexical/react": "^0.14.3",
"@monaco-editor/react": "^4.5.1",
"@lexical/react": "^0.17.0",
"@monaco-editor/react": "^4.6.0",
"@table-nav/core": "^0.0.7",
"@table-nav/react": "^0.0.7",
"@tanstack/match-sorter-utils": "^8.8.4",
"@tanstack/react-table": "^8.10.3",
"@withfig/autocomplete": "^2.652.3",
"@tanstack/match-sorter-utils": "^8.19.4",
"@tanstack/react-table": "^8.20.1",
"@withfig/autocomplete": "^2.672.0",
"autobind-decorator": "^2.4.0",
"base64-js": "^1.5.1",
"clsx": "^2.1.1",
"dayjs": "^1.11.3",
"dompurify": "^3.0.2",
"electron-squirrel-startup": "^1.0.0",
"electron-updater": "^6.1.8",
"framer-motion": "^10.16.16",
"dayjs": "^1.11.12",
"dompurify": "^3.1.6",
"electron-squirrel-startup": "^1.0.1",
"electron-updater": "^6.3.2",
"framer-motion": "^10.18.0",
"lexical": "0.14.5",
"mobx": "6.12",
"mobx-react": "^7.5.0",
"mobx": "6.12.5",
"mobx-react": "^7.6.0",
"monaco-editor": "0.48.0",
"mustache": "^4.2.0",
"node-fetch": "^3.2.10",
"overlayscrollbars": "^2.6.1",
"overlayscrollbars-react": "^0.5.5",
"node-fetch": "^3.3.2",
"overlayscrollbars": "^2.10.0",
"overlayscrollbars-react": "^0.5.6",
"papaparse": "^5.4.1",
"react": "^18.1.0",
"react-dom": "^18.1.0",
"react-markdown": "^9.0.0",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-markdown": "^9.0.1",
"remark": "^15.0.1",
"remark-gfm": "^4.0.0",
"sprintf-js": "^1.1.2",
"throttle-debounce": "^5.0.0",
"sprintf-js": "^1.1.3",
"throttle-debounce": "^5.0.2",
"tinycolor2": "^1.6.0",
"tsx-control-statements": "^5.1.1",
"uuid": "^9.0.0",
"winston": "^3.8.2",
"uuid": "^9.0.1",
"winston": "^3.13.1",
"xterm": "^5.3.0",
"xterm-addon-serialize": "^0.11.0",
"xterm-addon-web-links": "^0.9.0",
"xterm-addon-webgl": "^0.16.0"
},
"devDependencies": {
"@babel/cli": "^7.17.10",
"@babel/core": "^7.18.2",
"@babel/cli": "^7.24.8",
"@babel/core": "^7.25.2",
"@babel/plugin-proposal-class-properties": "^7.18.6",
"@babel/plugin-proposal-decorators": "^7.18.2",
"@babel/plugin-proposal-decorators": "^7.24.7",
"@babel/plugin-proposal-private-methods": "^7.18.6",
"@babel/plugin-proposal-private-property-in-object": "^7.21.11",
"@babel/plugin-transform-react-jsx": "^7.17.12",
"@babel/plugin-transform-runtime": "^7.23.4",
"@babel/preset-env": "^7.18.2",
"@babel/preset-react": "^7.23.3",
"@babel/preset-typescript": "^7.17.12",
"@babel/plugin-transform-react-jsx": "^7.25.2",
"@babel/plugin-transform-runtime": "^7.24.7",
"@babel/preset-env": "^7.25.3",
"@babel/preset-react": "^7.24.7",
"@babel/preset-typescript": "^7.24.7",
"@electron/rebuild": "^3.6.0",
"@svgr/webpack": "^8.1.0",
"@types/electron": "^1.6.10",
"@types/node": "^20.11.0",
"@types/papaparse": "^5.3.10",
"@types/react": "^18.0.12",
"@types/semver": "^7.5.6",
"@types/sprintf-js": "^1.1.3",
"@types/throttle-debounce": "^5.0.1",
"@types/tinycolor2": "^1",
"@types/uuid": "^9.0.7",
"@types/webpack-env": "^1.18.3",
"@withfig/autocomplete-types": "^1.30.0",
"@types/node": "^22.1.0",
"@types/papaparse": "^5.3.14",
"@types/react": "^18.3.3",
"@types/semver": "^7.5.8",
"@types/sprintf-js": "^1.1.4",
"@types/throttle-debounce": "^5.0.2",
"@types/tinycolor2": "^1.4.6",
"@types/uuid": "^9.0.8",
"@types/webpack-env": "^1.18.5",
"@withfig/autocomplete-types": "^1.31.0",
"babel-loader": "^9.1.3",
"babel-plugin-jsx-control-statements": "^4.1.2",
"copy-webpack-plugin": "^12.0.0",
"css-loader": "^7.1.0",
"electron": "^30.0.8",
"copy-webpack-plugin": "^12.0.2",
"css-loader": "^7.1.2",
"electron": "^31.3.1",
"electron-builder": "^24.13.3",
"electron-builder-squirrel-windows": "^24.13.3",
"electron-builder-squirrel-windows": "25.0.0-alpha.10",
"file-loader": "^6.2.0",
"http-server": "^14.1.1",
"less": "^4.1.2",
"less-loader": "^12.0.0",
"less": "^4.2.0",
"less-loader": "^12.2.0",
"lodash-webpack-plugin": "^0.11.6",
"mini-css-extract-plugin": "^2.6.0",
"mini-css-extract-plugin": "^2.9.0",
"prettier": "^2.8.8",
"raw-loader": "^4.0.2",
"react-split-it": "^2.0.0",
"style-loader": "4.0.0",
"typescript": "^5.0.0",
"webpack": "^5.73.0",
"webpack-bundle-analyzer": "^4.10.1",
"typescript": "^5.5.4",
"webpack": "^5.94.0",
"webpack-bundle-analyzer": "^4.10.2",
"webpack-cli": "^5.1.4",
"webpack-dev-server": "^5.0.4",
"webpack-merge": "^5.8.0",
"yaml": "^2.4.0"
"webpack-merge": "^5.10.0",
"yaml": "^2.5.0"
},
"scripts": {
"postinstall": "electron-builder install-app-deps"

View File

@ -170,6 +170,7 @@
--cmdinput-button-bg-color: var(--tab-green);
--cmdinput-disabled-bg-color: var(--app-text-bg-disabled-color);
--cmdinput-history-bg-color: var(--app-bg-color);
--cmdinput-ghost-text-color: rgb(145, 150, 144);
/* screen view color */
--screen-view-text-caption-color: rgb(139, 145, 138);

View File

@ -50,6 +50,7 @@
--modal-header-bottom-border-color: rgba(0, 0, 0, 0.3);
/* cmd input */
--cmdinput-ghost-text-color: rgb(116, 116, 116);
/* scroll colors */
--scrollbar-background-color: var(--app-bg-color);

View File

@ -7,7 +7,6 @@ export { InlineSettingsTextEdit } from "./inlinesettingstextedit";
export { InputDecoration } from "./inputdecoration";
export { LinkButton } from "./linkbutton";
export { Markdown } from "./markdown";
export { Markdown2 } from "./markdown2";
export { Modal } from "./modal";
export { PasswordField } from "./passwordfield";
export { ResizableSidebar } from "./resizablesidebar";

View File

@ -46,7 +46,7 @@
padding: 2px 4px 2px 6px;
}
pre.codeblock {
pre {
background-color: var(--markdown-bg-color);
margin: 4px 10px;
padding: 0.4em 0.7em;

View File

@ -5,14 +5,14 @@ import * as React from "react";
import * as mobxReact from "mobx-react";
import ReactMarkdown from "react-markdown";
import remarkGfm from "remark-gfm";
import { CopyButton } from "@/elements";
import { clsx } from "clsx";
import { GlobalModel } from "@/models";
import { v4 as uuidv4 } from "uuid";
import * as mobx from "mobx";
import { If } from "tsx-control-statements/components";
import "./markdown.less";
import { boundMethod } from "autobind-decorator";
function LinkRenderer(props: any): any {
function Link(props: any): JSX.Element {
let newUrl = "https://extern?" + encodeURIComponent(props.href);
return (
<a href={newUrl} target="_blank" rel={"noopener"}>
@ -21,99 +21,86 @@ function LinkRenderer(props: any): any {
);
}
function HeaderRenderer(props: any, hnum: number): any {
function Header(props: any, hnum: number): JSX.Element {
return <div className={clsx("title", "is-" + hnum)}>{props.children}</div>;
}
function CodeRenderer(props: any): any {
function Code(props: any): JSX.Element {
return <code>{props.children}</code>;
}
@mobxReact.observer
class CodeBlockMarkdown extends React.Component<
{ children: React.ReactNode; codeSelectSelectedIndex?: number; uuid: string },
{}
> {
blockIndex: number;
blockRef: React.RefObject<HTMLPreElement>;
const CodeBlock = mobxReact.observer(
(props: { children: React.ReactNode; onClickExecute?: (cmd: string) => void }): JSX.Element => {
const copied: OV<boolean> = mobx.observable.box(false, { name: "copied" });
constructor(props) {
super(props);
this.blockRef = React.createRef();
this.blockIndex = GlobalModel.inputModel.addCodeBlockToCodeSelect(this.blockRef, this.props.uuid);
}
const getTextContent = (children: any) => {
if (typeof children === "string") {
return children;
} else if (Array.isArray(children)) {
return children.map(getTextContent).join("");
} else if (children.props && children.props.children) {
return getTextContent(children.props.children);
}
return "";
};
render() {
let clickHandler: (e: React.MouseEvent<HTMLElement>, blockIndex: number) => void;
let inputModel = GlobalModel.inputModel;
clickHandler = (e: React.MouseEvent<HTMLElement>, blockIndex: number) => {
const sel = window.getSelection();
if (sel?.toString().length == 0) {
inputModel.setCodeSelectSelectedCodeBlock(blockIndex);
const handleCopy = async (e: React.MouseEvent) => {
let textToCopy = getTextContent(props.children);
textToCopy = textToCopy.replace(/\n$/, ""); // remove trailing newline
await navigator.clipboard.writeText(textToCopy);
copied.set(true);
setTimeout(() => copied.set(false), 2000); // Reset copied state after 2 seconds
};
const handleExecute = (e: React.MouseEvent) => {
let textToCopy = getTextContent(props.children);
textToCopy = textToCopy.replace(/\n$/, ""); // remove trailing newline
if (props.onClickExecute) {
props.onClickExecute(textToCopy);
return;
}
};
let selected = this.blockIndex == this.props.codeSelectSelectedIndex;
return (
<pre
ref={this.blockRef}
className={clsx({ selected: selected })}
onClick={(event) => clickHandler(event, this.blockIndex)}
>
{this.props.children}
<pre className="codeblock">
{props.children}
<div className="codeblock-actions">
<CopyButton className="copy-button" onClick={handleCopy} title="Copy" />
<If condition={props.onClickExecute}>
<i className="fa-regular fa-square-terminal" onClick={handleExecute}></i>
</If>
</div>
</pre>
);
}
}
);
@mobxReact.observer
class Markdown extends React.Component<
{ text: string; style?: any; extraClassName?: string; codeSelect?: boolean },
{
text: string;
style?: any;
className?: string;
onClickExecute?: (cmd: string) => void;
},
{}
> {
curUuid: string;
constructor(props) {
super(props);
this.curUuid = uuidv4();
}
@boundMethod
CodeBlockRenderer(props: any, codeSelect: boolean, codeSelectIndex: number, curUuid: string): any {
if (codeSelect) {
return (
<CodeBlockMarkdown codeSelectSelectedIndex={codeSelectIndex} uuid={curUuid}>
{props.children}
</CodeBlockMarkdown>
);
} else {
const clickHandler = (e: React.MouseEvent<HTMLElement>) => {
let blockText = (e.target as HTMLElement).innerText;
if (blockText) {
blockText = blockText.replace(/\n$/, ""); // remove trailing newline
navigator.clipboard.writeText(blockText);
}
};
return <pre onClick={(event) => clickHandler(event)}>{props.children}</pre>;
}
}
render() {
let text = this.props.text;
let codeSelect = this.props.codeSelect;
let curCodeSelectIndex = GlobalModel.inputModel.getCodeSelectSelectedIndex();
let { text, className, onClickExecute } = this.props;
let markdownComponents = {
a: LinkRenderer,
h1: (props) => HeaderRenderer(props, 1),
h2: (props) => HeaderRenderer(props, 2),
h3: (props) => HeaderRenderer(props, 3),
h4: (props) => HeaderRenderer(props, 4),
h5: (props) => HeaderRenderer(props, 5),
h6: (props) => HeaderRenderer(props, 6),
code: (props) => CodeRenderer(props),
pre: (props) => this.CodeBlockRenderer(props, codeSelect, curCodeSelectIndex, this.curUuid),
a: Link,
h1: (props) => <Header {...props} hnum={1} />,
h2: (props) => <Header {...props} hnum={2} />,
h3: (props) => <Header {...props} hnum={3} />,
h4: (props) => <Header {...props} hnum={4} />,
h5: (props) => <Header {...props} hnum={5} />,
h6: (props) => <Header {...props} hnum={6} />,
code: Code,
pre: (props) => <CodeBlock {...props} onClickExecute={onClickExecute} />,
};
return (
<div className={clsx("markdown content", this.props.extraClassName)} style={this.props.style}>
<div className={clsx("markdown content", className)} style={this.props.style}>
<ReactMarkdown remarkPlugins={[remarkGfm]} components={markdownComponents}>
{text}
</ReactMarkdown>

View File

@ -1,112 +0,0 @@
// Copyright 2023, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
import * as React from "react";
import * as mobxReact from "mobx-react";
import ReactMarkdown from "react-markdown";
import remarkGfm from "remark-gfm";
import { CopyButton } from "@/elements";
import { clsx } from "clsx";
import * as mobx from "mobx";
import { If } from "tsx-control-statements/components";
import "./markdown.less";
function Link(props: any): JSX.Element {
let newUrl = "https://extern?" + encodeURIComponent(props.href);
return (
<a href={newUrl} target="_blank" rel={"noopener"}>
{props.children}
</a>
);
}
function Header(props: any, hnum: number): JSX.Element {
return <div className={clsx("title", "is-" + hnum)}>{props.children}</div>;
}
function Code(props: any): JSX.Element {
return <code>{props.children}</code>;
}
const CodeBlock = mobxReact.observer(
(props: { children: React.ReactNode; onClickExecute?: (cmd: string) => void }): JSX.Element => {
const copied: OV<boolean> = mobx.observable.box(false, { name: "copied" });
const getTextContent = (children: any) => {
if (typeof children === "string") {
return children;
} else if (Array.isArray(children)) {
return children.map(getTextContent).join("");
} else if (children.props && children.props.children) {
return getTextContent(children.props.children);
}
return "";
};
const handleCopy = async (e: React.MouseEvent) => {
let textToCopy = getTextContent(props.children);
textToCopy = textToCopy.replace(/\n$/, ""); // remove trailing newline
await navigator.clipboard.writeText(textToCopy);
copied.set(true);
setTimeout(() => copied.set(false), 2000); // Reset copied state after 2 seconds
};
const handleExecute = (e: React.MouseEvent) => {
let textToCopy = getTextContent(props.children);
textToCopy = textToCopy.replace(/\n$/, ""); // remove trailing newline
if (props.onClickExecute) {
props.onClickExecute(textToCopy);
return;
}
};
return (
<pre className="codeblock">
{props.children}
<div className="codeblock-actions">
<CopyButton className="copy-button" onClick={handleCopy} title="Copy" />
<If condition={props.onClickExecute}>
<i className="fa-regular fa-square-terminal" onClick={handleExecute}></i>
</If>
</div>
</pre>
);
}
);
@mobxReact.observer
class Markdown2 extends React.Component<
{
text: string;
style?: any;
className?: string;
onClickExecute?: (cmd: string) => void;
},
{}
> {
render() {
let { text, className, onClickExecute } = this.props;
let markdownComponents = {
a: Link,
h1: (props) => <Header {...props} hnum={1} />,
h2: (props) => <Header {...props} hnum={2} />,
h3: (props) => <Header {...props} hnum={3} />,
h4: (props) => <Header {...props} hnum={4} />,
h5: (props) => <Header {...props} hnum={5} />,
h6: (props) => <Header {...props} hnum={6} />,
code: Code,
pre: (props) => <CodeBlock {...props} onClickExecute={onClickExecute} />,
};
return (
<div className={clsx("markdown content", className)} style={this.props.style}>
<ReactMarkdown remarkPlugins={[remarkGfm]} components={markdownComponents}>
{text}
</ReactMarkdown>
</div>
);
}
}
export { Markdown2 };

View File

@ -178,7 +178,7 @@ class LineActions extends React.Component<{ screen: LineContainerType; line: Lin
<div className="line-actions">
<Choose>
<When condition={containerType == appconst.LineContainer_Main}>
<div key="chat" title="Restart Command" className="line-icon" onClick={this.clickChat}>
<div key="chat" title="Ask Wave AI" className="line-icon" onClick={this.clickChat}>
<i className="fa-sharp fa-regular fa-sparkles fa-fw" />
</div>
<div key="restart" title="Restart Command" className="line-icon" onClick={this.clickRestart}>

View File

@ -7,7 +7,7 @@ import * as mobx from "mobx";
import { GlobalModel } from "@/models";
import { boundMethod } from "autobind-decorator";
import { For, If } from "tsx-control-statements/components";
import { Markdown2, TypingIndicator } from "@/elements";
import { Markdown, TypingIndicator } from "@/elements";
import type { OverlayScrollbars } from "overlayscrollbars";
import { OverlayScrollbarsComponent, OverlayScrollbarsComponentRef } from "overlayscrollbars-react";
import tinycolor from "tinycolor2";
@ -40,9 +40,6 @@ class ChatKeyBindings extends React.Component<{ component: ChatSidebar }, {}> {
keybindManager.registerKeybinding("pane", "aichat", "generic:selectBelow", (waveEvent) => {
return component.onArrowDownPressed();
});
keybindManager.registerKeybinding("pane", "aichat", "aichat:setCmdInputValue", (waveEvent) => {
return component.onSetCmdInputValue();
});
}
componentWillUnmount(): void {
@ -75,7 +72,7 @@ class ChatItem extends React.Component<
<div className="chat-msg-header">
<i className="fa-sharp fa-solid fa-user"></i>
</div>
<Markdown2 className="msg-text" text={chatItem.userquery} />
<Markdown className="msg-text" text={chatItem.userquery} />
</>
);
if (isassistantresponse) {
@ -97,7 +94,7 @@ class ChatItem extends React.Component<
<div className="chat-msg-header">
<i className="fa-sharp fa-solid fa-sparkles"></i>
</div>
<Markdown2 text={assistantresponse.message} onClickExecute={onSetCmdInputValue} />
<Markdown text={assistantresponse.message} onClickExecute={onSetCmdInputValue} />
</>
);
}
@ -195,7 +192,6 @@ class ChatSidebar extends React.Component<{}, {}> {
value: OV<string> = mobx.observable.box("", { deep: false, name: "value" });
osInstance: OverlayScrollbars;
termFontSize: number = 14;
blockIndex: number;
disposeReaction: () => void;
constructor(props) {
@ -227,32 +223,21 @@ class ChatSidebar extends React.Component<{}, {}> {
}
);
if (this.sidebarRef.current) {
this.sidebarRef.current.addEventListener("click", this.handleSidebarClick);
this.sidebarRef.current.addEventListener("click", this.onSidebarClick);
}
document.addEventListener("click", this.handleClickOutside);
this.requestChatUpdate();
}
componentWillUnmount() {
if (this.sidebarRef.current) {
this.sidebarRef.current.removeEventListener("click", this.handleSidebarClick);
this.sidebarRef.current.removeEventListener("click", this.onSidebarClick);
}
document.removeEventListener("click", this.handleClickOutside);
GlobalModel.sidebarchatModel.resetFocus();
if (this.disposeReaction) {
this.disposeReaction();
}
}
@mobx.action.bound
handleClickOutside(e: MouseEvent) {
const sidebar = this.sidebarRef.current;
if (sidebar && !sidebar.contains(e.target as Node)) {
GlobalModel.sidebarchatModel.resetFocus();
GlobalModel.inputModel.giveFocus();
}
}
requestChatUpdate() {
const chatMessageItems = GlobalModel.inputModel.AICmdInfoChatItems.slice();
if (chatMessageItems == null || chatMessageItems.length === 0) {
@ -309,10 +294,16 @@ class ChatSidebar extends React.Component<{}, {}> {
@mobx.action.bound
onEnterKeyPressed() {
const blockIndex = GlobalModel.sidebarchatModel.getSelectedCodeBlockIndex();
if (blockIndex != null) {
this.onSetCmdInputValue();
return true;
}
const messageStr = this.value.get();
this.submitChatMessage(messageStr);
this.value.set("");
GlobalModel.sidebarchatModel.resetCmdAndOutput();
return true;
}
@mobx.action.bound
@ -324,6 +315,11 @@ class ChatSidebar extends React.Component<{}, {}> {
currentRef.setRangeText("\n", currentRef.selectionStart, currentRef.selectionEnd, "end");
}
@mobx.action.bound
onBlur() {
GlobalModel.sidebarchatModel.resetFocus();
}
updatePreTagOutline(clickedPre?) {
const pres = this.chatWindowRef.current?.querySelectorAll("pre");
if (pres == null) {
@ -340,7 +336,7 @@ class ChatSidebar extends React.Component<{}, {}> {
}
@mobx.action.bound
handleSidebarClick(event) {
onSidebarClick(event) {
const target = event.target as HTMLElement;
if (
target.closest(".copy-button") ||
@ -526,7 +522,6 @@ class ChatSidebar extends React.Component<{}, {}> {
const chatMessageItems = GlobalModel.inputModel.AICmdInfoChatItems.slice();
const renderAIChatKeybindings = GlobalModel.sidebarchatModel.hasFocus();
const textAreaValue = this.value.get();
return (
<div ref={this.sidebarRef} className="sidebarchat">
<If condition={renderAIChatKeybindings}>
@ -546,6 +541,7 @@ class ChatSidebar extends React.Component<{}, {}> {
autoComplete="off"
autoCorrect="off"
className="sidebarchat-input chat-textarea"
onBlur={this.onBlur}
onFocus={this.onTextAreaFocus}
onMouseDown={this.onTextAreaMouseDown} // When the user clicks on the textarea
onChange={this.onTextAreaChange}

View File

@ -1,72 +0,0 @@
.cmd-aichat {
padding-bottom: 0 !important;
.chat-window {
display: flex;
overflow-y: auto;
margin-bottom: 5px;
flex-direction: column;
height: 100%;
// This is the filler that will push the chat messages to the bottom until the chat window is full
.filler {
flex: 1 1 auto;
}
}
.chat-input {
padding: 0.5em 0.5em 0.5em 0.5em;
flex: 0 0 auto;
.chat-textarea {
color: var(--app-text-primary-color);
background-color: var(--cmdinput-textarea-bg);
resize: none;
width: 100%;
border: transparent;
outline: none;
overflow: auto;
overflow-wrap: anywhere;
font-family: var(--termfontfamily);
font-weight: normal;
line-height: var(--termlineheight);
}
}
.chat-msg {
margin-top: calc(var(--termpad) * 2);
margin-bottom: calc(var(--termpad) * 2);
.chat-msg-header {
display: flex;
margin-bottom: 2px;
i {
margin-right: 0.5em;
}
.chat-username {
font-weight: bold;
margin-right: 5px;
}
}
}
.chat-msg-assistant {
color: var(--app-text-color);
}
.chat-msg-user {
.msg-text {
font-family: var(--markdown-font);
font-size: 14px;
white-space: pre-wrap;
}
}
.chat-msg-error {
color: var(--cmdinput-text-error);
font-family: var(--markdown-font);
font-size: 14px;
}
}

View File

@ -1,282 +0,0 @@
// Copyright 2023, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
import * as React from "react";
import * as mobxReact from "mobx-react";
import * as mobx from "mobx";
import { GlobalModel } from "@/models";
import { boundMethod } from "autobind-decorator";
import { If, For } from "tsx-control-statements/components";
import { Markdown } from "@/elements";
import { AuxiliaryCmdView } from "./auxview";
import * as appconst from "@/app/appconst";
import "./aichat.less";
class AIChatKeybindings extends React.Component<{ AIChatObject: AIChat }, {}> {
componentDidMount(): void {
const AIChatObject = this.props.AIChatObject;
const keybindManager = GlobalModel.keybindManager;
const inputModel = GlobalModel.inputModel;
keybindManager.registerKeybinding("pane", "aichat", "generic:confirm", (waveEvent) => {
AIChatObject.onEnterKeyPressed();
return true;
});
keybindManager.registerKeybinding("pane", "aichat", "generic:expandTextInput", (waveEvent) => {
AIChatObject.onExpandInputPressed();
return true;
});
keybindManager.registerKeybinding("pane", "aichat", "generic:cancel", (waveEvent) => {
inputModel.closeAuxView();
return true;
});
keybindManager.registerKeybinding("pane", "aichat", "aichat:clearHistory", (waveEvent) => {
inputModel.clearAIAssistantChat();
return true;
});
keybindManager.registerKeybinding("pane", "aichat", "generic:selectAbove", (waveEvent) => {
return AIChatObject.onArrowUpPressed();
});
keybindManager.registerKeybinding("pane", "aichat", "generic:selectBelow", (waveEvent) => {
return AIChatObject.onArrowDownPressed();
});
}
componentWillUnmount(): void {
GlobalModel.keybindManager.unregisterDomain("aichat");
}
render() {
return null;
}
}
@mobxReact.observer
class AIChat extends React.Component<{}, {}> {
chatListKeyCount: number = 0;
chatWindowScrollRef: React.RefObject<HTMLDivElement>;
textAreaRef: React.RefObject<HTMLTextAreaElement>;
termFontSize: number = 14;
constructor(props: any) {
super(props);
mobx.makeObservable(this);
this.chatWindowScrollRef = React.createRef();
this.textAreaRef = React.createRef();
}
componentDidMount() {
const inputModel = GlobalModel.inputModel;
if (this.chatWindowScrollRef?.current != null) {
this.chatWindowScrollRef.current.scrollTop = this.chatWindowScrollRef.current.scrollHeight;
}
if (this.textAreaRef.current != null) {
this.textAreaRef.current.focus();
inputModel.setCmdInfoChatRefs(this.textAreaRef, this.chatWindowScrollRef);
}
this.requestChatUpdate();
this.onTextAreaChange(null);
}
componentDidUpdate() {
if (this.chatWindowScrollRef?.current != null) {
this.chatWindowScrollRef.current.scrollTop = this.chatWindowScrollRef.current.scrollHeight;
}
}
requestChatUpdate() {
this.submitChatMessage("");
}
submitChatMessage(messageStr: string) {
const curLine = GlobalModel.inputModel.curLine;
const prtn = GlobalModel.submitChatInfoCommand(messageStr, curLine, false);
prtn.then((rtn) => {
if (!rtn.success) {
console.log("submit chat command error: " + rtn.error);
}
}).catch((_) => {});
}
getLinePos(elem: any): { numLines: number; linePos: number } {
const numLines = elem.value.split("\n").length;
const linePos = elem.value.substr(0, elem.selectionStart).split("\n").length;
return { numLines, linePos };
}
@mobx.action.bound
onTextAreaFocused(e: any) {
GlobalModel.inputModel.setAuxViewFocus(true);
this.onTextAreaChange(e);
}
@mobx.action.bound
onTextAreaBlur(e: any) {
//GlobalModel.inputModel.setAuxViewFocus(false);
}
// Adjust the height of the textarea to fit the text
@boundMethod
onTextAreaChange(e: any) {
// Calculate the bounding height of the text area
const textAreaMaxLines = 4;
const textAreaLineHeight = this.termFontSize * 1.5;
const textAreaMinHeight = textAreaLineHeight;
const textAreaMaxHeight = textAreaLineHeight * textAreaMaxLines;
// Get the height of the wrapped text area content. Courtesy of https://stackoverflow.com/questions/995168/textarea-to-resize-based-on-content-length
this.textAreaRef.current.style.height = "1px";
const scrollHeight: number = this.textAreaRef.current.scrollHeight;
// Set the new height of the text area, bounded by the min and max height.
const newHeight = Math.min(Math.max(scrollHeight, textAreaMinHeight), textAreaMaxHeight);
this.textAreaRef.current.style.height = newHeight + "px";
}
onTextAreaInput(e: any) {
GlobalModel.inputModel.codeSelectDeselectAll();
}
onEnterKeyPressed() {
const inputModel = GlobalModel.inputModel;
const currentRef = this.textAreaRef.current;
if (currentRef == null) {
return;
}
if (inputModel.getCodeSelectSelectedIndex() == -1) {
const messageStr = currentRef.value;
this.submitChatMessage(messageStr);
currentRef.value = "";
} else {
mobx.action(() => {
inputModel.grabCodeSelectSelection();
inputModel.setAuxViewFocus(false);
})();
}
}
onExpandInputPressed() {
const currentRef = this.textAreaRef.current;
if (currentRef == null) {
return;
}
currentRef.setRangeText("\n", currentRef.selectionStart, currentRef.selectionEnd, "end");
GlobalModel.inputModel.codeSelectDeselectAll();
}
onArrowUpPressed(): boolean {
const currentRef = this.textAreaRef.current;
if (currentRef == null) {
return false;
}
if (this.getLinePos(currentRef).linePos > 1) {
// normal up arrow
GlobalModel.inputModel.codeSelectDeselectAll();
return false;
}
GlobalModel.inputModel.codeSelectSelectNextOldestCodeBlock();
return true;
}
onArrowDownPressed(): boolean {
const currentRef = this.textAreaRef.current;
const inputModel = GlobalModel.inputModel;
if (currentRef == null) {
return false;
}
if (inputModel.getCodeSelectSelectedIndex() == inputModel.codeSelectBottom) {
GlobalModel.inputModel.codeSelectDeselectAll();
return false;
}
inputModel.codeSelectSelectNextNewestCodeBlock();
return true;
}
@boundMethod
onKeyDown(e: any) {}
renderError(err: string): any {
return <div className="chat-msg-error">{err}</div>;
}
renderChatMessage(chatItem: OpenAICmdInfoChatMessageType): any {
const curKey = "chatmsg-" + this.chatListKeyCount;
this.chatListKeyCount++;
const senderClassName = chatItem.isassistantresponse ? "chat-msg-assistant" : "chat-msg-user";
const msgClassName = "chat-msg " + senderClassName;
let innerHTML: React.JSX.Element = (
<span>
<div className="chat-msg-header">
<i className="fa-sharp fa-solid fa-user"></i>
<div className="chat-username">You</div>
</div>
<p className="msg-text">{chatItem.userquery}</p>
</span>
);
if (chatItem.isassistantresponse) {
if (chatItem.assistantresponse.error != null && chatItem.assistantresponse.error != "") {
innerHTML = this.renderError(chatItem.assistantresponse.error);
} else {
innerHTML = (
<span>
<div className="chat-msg-header">
<i className="fa-sharp fa-solid fa-sparkles"></i>
<div className="chat-username">AI Assistant</div>
</div>
<Markdown text={chatItem.assistantresponse.message} codeSelect />
</span>
);
}
}
return (
<div className={msgClassName} key={curKey}>
{innerHTML}
</div>
);
}
render() {
const chatMessageItems = GlobalModel.inputModel.AICmdInfoChatItems.slice();
const chitem: OpenAICmdInfoChatMessageType = null;
const renderKeybindings = GlobalModel.inputModel.shouldRenderAuxViewKeybindings(appconst.InputAuxView_AIChat);
return (
<AuxiliaryCmdView
title="Wave AI"
className="cmd-aichat"
onClose={() => GlobalModel.inputModel.closeAuxView()}
iconClass="fa-sharp fa-solid fa-sparkles"
>
<If condition={renderKeybindings}>
<AIChatKeybindings AIChatObject={this}></AIChatKeybindings>
</If>
<div className="chat-window" ref={this.chatWindowScrollRef}>
<div className="filler"></div>
<For each="chitem" index="idx" of={chatMessageItems}>
{this.renderChatMessage(chitem)}
</For>
</div>
<div className="chat-input">
<textarea
key="main"
ref={this.textAreaRef}
autoComplete="off"
autoCorrect="off"
id="chat-cmd-input"
onFocus={this.onTextAreaFocused}
onBlur={this.onTextAreaBlur}
onChange={this.onTextAreaChange}
onInput={this.onTextAreaInput}
onKeyDown={this.onKeyDown}
style={{ fontSize: this.termFontSize }}
className="chat-textarea"
placeholder="Send a Message..."
></textarea>
</div>
</AuxiliaryCmdView>
);
}
}
export { AIChat };

View File

@ -131,7 +131,7 @@
}
.textarea-ghost {
color: var(--app-text-secondary-color);
color: var(--cmdinput-ghost-text-color);
z-index: 1;
}

View File

@ -16,7 +16,6 @@ import { InfoMsg } from "./infomsg";
import { HistoryInfo } from "./historyinfo";
import { Prompt } from "@/common/prompt/prompt";
import { CenteredIcon, RotateIcon } from "@/common/icons/icons";
import { AIChat } from "./aichat";
import * as util from "@/util/util";
import * as appconst from "@/app/appconst";
import { AutocompleteSuggestionView } from "./suggestionview";
@ -29,6 +28,7 @@ dayjs.extend(localizedFormat);
class CmdInput extends React.Component<{}, {}> {
cmdInputRef: React.RefObject<any> = React.createRef();
promptRef: React.RefObject<any> = React.createRef();
sbcTimeoutId: NodeJS.Timeout = null;
constructor(props) {
super(props);
@ -57,6 +57,13 @@ class CmdInput extends React.Component<{}, {}> {
this.updateCmdInputHeight();
}
componentWillUnmount() {
if (this.sbcTimeoutId) {
clearTimeout(this.sbcTimeoutId);
this.sbcTimeoutId = null;
}
}
@boundMethod
handleInnerHeightUpdate(): void {
this.updateCmdInputHeight();
@ -96,9 +103,15 @@ class CmdInput extends React.Component<{}, {}> {
@mobx.action.bound
clickAIChatAction(e: any): void {
const rightSidebarModel = GlobalModel.rightSidebarModel;
const width = rightSidebarModel.getWidth(true);
rightSidebarModel.saveState(width, false);
const isCollapsed = GlobalModel.rightSidebarModel.getCollapsed();
GlobalModel.rightSidebarModel.setCollapsed(!isCollapsed);
if (isCollapsed) {
this.sbcTimeoutId = setTimeout(() => {
GlobalModel.inputModel.setChatSidebarFocus();
}, 100);
} else {
GlobalModel.inputModel.setChatSidebarFocus(false);
}
}
@boundMethod
@ -187,10 +200,6 @@ class CmdInput extends React.Component<{}, {}> {
<div className="cmd-input-grow-spacer"></div>
<HistoryInfo />
</When>
<When condition={openView === appconst.InputAuxView_AIChat}>
<div className="cmd-input-grow-spacer"></div>
<AIChat />
</When>
<When condition={openView === appconst.InputAuxView_Info}>
<InfoMsg key="infomsg" />
</When>

View File

@ -187,9 +187,7 @@ export class AutocompleteModel {
* @see getPrimarySuggestionIndex
*/
getPrimarySuggestionCompletion(): string {
if (!this.isEnabled) {
return null;
}
if (!this.isEnabled || !this.globalModel.inputModel.curLine) return null;
const suggestionIndex = this.getPrimarySuggestionIndex();
const retVal = this.getSuggestionCompletion(suggestionIndex);
if (retVal) {

View File

@ -234,10 +234,21 @@ class Screen {
}
refocusLine(sdata: ScreenDataType, oldFocusType: string, oldSelectedLine: number): void {
if (this.globalModel.activeMainView.get() != "session") {
return;
}
let isCmdFocus = sdata.focustype == "cmd";
if (!isCmdFocus) {
return;
}
if (document.activeElement != null) {
if (document.activeElement.nodeName == "INPUT" || document.activeElement.nodeName == "TEXTAREA") {
return;
}
}
if (this.globalModel.modalsModel.hasOpenModals()) {
return;
}
let curLineFocus = this.globalModel.getFocusedLine();
let sline: LineType = null;
if (sdata.selectedline != 0) {

View File

@ -6,7 +6,6 @@ import * as mobx from "mobx";
import * as mobxReact from "mobx-react";
import { sprintf } from "sprintf-js";
import { Markdown } from "@/elements";
import { GlobalModel } from "@/models/global";
import "./markdown.less";

View File

@ -18,6 +18,7 @@ import (
"os/exec"
"regexp"
"sort"
"strconv"
"strings"
"syscall"
"unicode/utf8"
@ -673,3 +674,26 @@ func GetFirstLine(s string) string {
}
return s[0:idx]
}
func TrimQuotes(s string) (string, bool) {
if len(s) > 2 && s[0] == '"' {
trimmed, err := strconv.Unquote(s)
if err != nil {
return s, false
}
return trimmed, true
}
return s, false
}
func TryTrimQuotes(s string) string {
trimmed, _ := TrimQuotes(s)
return trimmed
}
func ReplaceQuotes(s string, shouldReplace bool) string {
if shouldReplace {
return strconv.Quote(s)
}
return s
}

View File

@ -132,7 +132,7 @@ var SetVarScopes = []SetVarScope{
{ScopeName: "remote", VarNames: []string{}},
}
var userHostRe = regexp.MustCompile(`^(sudo@)?([a-zA-Z0-9][a-zA-Z0-9._@\\-]*@)?([a-z0-9][a-z0-9.-]*)(?::([0-9]+))?$`)
var userHostRe = regexp.MustCompile(`^(sudo@)?([a-zA-Z0-9][a-zA-Z0-9._@:\\-]*@)?([a-z0-9][a-z0-9.-]*)(?::([0-9]+))?$`)
var remoteAliasRe = regexp.MustCompile("^[a-zA-Z0-9][a-zA-Z0-9._-]*$")
var genericNameRe = regexp.MustCompile("^[a-zA-Z][a-zA-Z0-9_ .()<>,/\"'\\[\\]{}=+$@!*-]*$")
var rendererRe = regexp.MustCompile("^[a-zA-Z][a-zA-Z0-9_.:-]*$")
@ -769,7 +769,7 @@ func EvalCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (scbus.U
} else {
return nil, fmt.Errorf("error in Eval Meta Command: %w", rtnErr)
}
if !resolveBool(pk.Kwargs[KwArgNoHist], false) {
if !resolveBool(pk.Kwargs[KwArgNoHist], false) && pk.EphemeralOpts == nil {
// TODO should this be "pk" or "newPk" (2nd arg)
err := addToHistory(ctx, pk, historyContext, (newPk.MetaCmd != "run"), (rtnErr != nil))
if err != nil {

View File

@ -23,6 +23,7 @@ import (
"github.com/kevinburke/ssh_config"
"github.com/wavetermdev/waveterm/waveshell/pkg/base"
"github.com/wavetermdev/waveterm/waveshell/pkg/utilfn"
"github.com/wavetermdev/waveterm/wavesrv/pkg/scbus"
"github.com/wavetermdev/waveterm/wavesrv/pkg/sstore"
"github.com/wavetermdev/waveterm/wavesrv/pkg/userinput"
@ -110,10 +111,6 @@ func createPublicKeyCallback(connCtx context.Context, sshKeywords *SshKeywords,
}
unencryptedPrivateKey, err := ssh.ParseRawPrivateKey(privateKey)
if _, ok := err.(*ssh.PassphraseMissingError); !ok {
// skip this key and try with the next
return createDummySigner()
}
if err == nil {
signer, err := ssh.NewSignerFromKey(unencryptedPrivateKey)
if err == nil {
@ -124,7 +121,10 @@ func createPublicKeyCallback(connCtx context.Context, sshKeywords *SshKeywords,
}
return []ssh.Signer{signer}, err
}
}
if _, ok := err.(*ssh.PassphraseMissingError); !ok {
// skip this key and try with the next
return createDummySigner()
}
signer, err := ssh.ParsePrivateKey(privateKey)
@ -137,10 +137,6 @@ func createPublicKeyCallback(connCtx context.Context, sshKeywords *SshKeywords,
}
unencryptedPrivateKey, err = ssh.ParseRawPrivateKeyWithPassphrase(privateKey, []byte(passphrase))
if err != x509.IncorrectPasswordError && err.Error() != "bcrypt_pbkdf: empty password" {
// skip this key and try with the next
return createDummySigner()
}
if err == nil {
signer, err := ssh.NewSignerFromKey(unencryptedPrivateKey)
if err == nil {
@ -152,18 +148,10 @@ func createPublicKeyCallback(connCtx context.Context, sshKeywords *SshKeywords,
return []ssh.Signer{signer}, err
}
}
/*
signer, err = ssh.ParsePrivateKeyWithPassphrase(privateKey, []byte(passphrase))
if err == nil {
log.Printf("with passphrase %v\n", signer.PublicKey().Marshal())
return []ssh.Signer{signer}, err
}
if err != x509.IncorrectPasswordError && err.Error() != "bcrypt_pbkdf: empty password" {
// skip this key and try with the next
return createDummySigner()
}
*/
if err != x509.IncorrectPasswordError && err.Error() != "bcrypt_pbkdf: empty password" {
// skip this key and try with the next
return createDummySigner()
}
// batch mode deactivates user input
if sshKeywords.BatchMode {
@ -201,16 +189,6 @@ func createPublicKeyCallback(connCtx context.Context, sshKeywords *SshKeywords,
})
}
return []ssh.Signer{signer}, err
/*
signer, err = ssh.ParsePrivateKeyWithPassphrase(privateKey, []byte(response.Text))
if err != nil {
// skip this key and try with the next
return createDummySigner()
}
log.Printf("with passphrase %v\n", signer.PublicKey().Marshal())
return []ssh.Signer{signer}, err
*/
}
}
@ -809,9 +787,9 @@ func findSshConfigKeywords(hostPattern string, sshAuthSock string) (*SshKeywords
return nil, err
}
if identityAgentRaw == "" {
sshKeywords.IdentityAgent = sshAuthSock
sshKeywords.IdentityAgent = base.ExpandHomeDir(utilfn.TryTrimQuotes(strings.TrimSpace(string(sshAuthSock))))
} else {
sshKeywords.IdentityAgent = identityAgentRaw
sshKeywords.IdentityAgent = base.ExpandHomeDir(utilfn.TryTrimQuotes(identityAgentRaw))
}
return sshKeywords, nil

2628
yarn.lock

File diff suppressed because it is too large Load Diff