Skip to content

Commit e1f5245

Browse files
committed
Merge branch 'main' into evan/search-webview
2 parents 6c8edb0 + 93b9432 commit e1f5245

File tree

17 files changed

+382
-56
lines changed

17 files changed

+382
-56
lines changed

cmd/wsh/cmd/wshcmd-setbg.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -140,7 +140,8 @@ func setBgRun(cmd *cobra.Command, args []string) (rtnErr error) {
140140
}
141141

142142
// Create URL-safe path
143-
escapedPath := strings.ReplaceAll(absPath, "'", "\\'")
143+
escapedPath := strings.ReplaceAll(absPath, "\\", "\\\\")
144+
escapedPath = strings.ReplaceAll(escapedPath, "'", "\\'")
144145
bgStyle = fmt.Sprintf("url('%s')", escapedPath)
145146

146147
switch {

docs/docs/ai-presets.mdx

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,23 @@ To use Perplexity's models:
127127
}
128128
```
129129

130+
### Google (Gemini)
131+
132+
To use Google's Gemini models from [Google AI Studio](https://aistudio.google.com):
133+
134+
```json
135+
{
136+
137+
"display:name": "Gemini 2.0",
138+
"display:order": 5,
139+
"ai:*": true,
140+
"ai:apitype": "google",
141+
"ai:model": "gemini-2.0-flash-exp",
142+
"ai:apitoken": "<your Google AI API key>"
143+
}
144+
}
145+
```
146+
130147
## Multiple Presets Example
131148

132149
You can define multiple presets in your `ai.json` file:

frontend/app/app.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,11 @@ import { ContextMenuModel } from "@/store/contextmenu";
66
import {
77
atoms,
88
createBlock,
9+
getSettingsPrefixAtom,
910
globalStore,
1011
isDev,
1112
PLATFORM,
1213
removeFlashError,
13-
useSettingsPrefixAtom,
1414
} from "@/store/global";
1515
import { appHandleKeyDown } from "@/store/keymodel";
1616
import { getElemAsStr } from "@/util/focusutil";
@@ -123,7 +123,7 @@ async function handleContextMenu(e: React.MouseEvent<HTMLDivElement>) {
123123
}
124124

125125
function AppSettingsUpdater() {
126-
const windowSettingsAtom = useSettingsPrefixAtom("window");
126+
const windowSettingsAtom = getSettingsPrefixAtom("window");
127127
const windowSettings = useAtomValue(windowSettingsAtom);
128128
useEffect(() => {
129129
const isTransparentOrBlur =

frontend/app/element/progressbar.scss

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
// Copyright 2024, Command Line Inc.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
.progress-bar-container {
5+
position: relative;
6+
width: 100%;
7+
overflow: hidden;
8+
display: flex;
9+
align-items: center;
10+
justify-content: space-between;
11+
12+
.outer {
13+
border: 1px solid rgb(from var(--main-text-color) r g b / 15%);
14+
border-radius: 40px;
15+
padding: 4px;
16+
background-color: var(--main-bg-color);
17+
flex-grow: 1;
18+
19+
.progress-bar-fill {
20+
height: 100%;
21+
transition: width 0.3s ease-in-out;
22+
background-color: var(--success-color);
23+
border-radius: 9px;
24+
width: 100%;
25+
}
26+
}
27+
28+
.progress-bar-label {
29+
width: 40px;
30+
flex-shrink: 0;
31+
font-size: 0.9rem;
32+
color: var(--main-text-color);
33+
font-size: 12px;
34+
font-style: normal;
35+
font-weight: 400;
36+
line-height: normal;
37+
text-align: right;
38+
}
39+
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
// Copyright 2024, Command Line Inc.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
import type { Meta, StoryObj } from "@storybook/react";
5+
import { ProgressBar } from "./progressbar";
6+
7+
const meta: Meta<typeof ProgressBar> = {
8+
title: "Elements/ProgressBar",
9+
component: ProgressBar,
10+
args: {
11+
progress: 0, // Default value
12+
label: "Progress",
13+
},
14+
argTypes: {
15+
progress: {
16+
description: "Percentage of progress (0-100)",
17+
control: { type: "range", min: 0, max: 100 },
18+
},
19+
label: {
20+
description: "Accessible label for the progress bar",
21+
control: "text",
22+
},
23+
},
24+
};
25+
26+
export default meta;
27+
28+
type Story = StoryObj<typeof ProgressBar>;
29+
30+
export const EmptyProgress: Story = {
31+
render: (args) => (
32+
<div style={{ padding: "20px", background: "#111", color: "#fff" }}>
33+
<ProgressBar {...args} />
34+
</div>
35+
),
36+
args: {
37+
progress: 0, // No progress
38+
label: "Empty progress bar",
39+
},
40+
};
41+
42+
export const FilledProgress: Story = {
43+
render: (args) => (
44+
<div style={{ padding: "20px", background: "#111", color: "#fff" }}>
45+
<ProgressBar {...args} />
46+
</div>
47+
),
48+
args: {
49+
progress: 90, // Filled to 90%
50+
label: "Filled progress bar",
51+
},
52+
};

frontend/app/element/progressbar.tsx

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
// Copyright 2024, Command Line Inc.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
import { boundNumber } from "@/util/util";
5+
import "./progressbar.scss";
6+
7+
type ProgressBarProps = {
8+
progress: number;
9+
label?: string;
10+
};
11+
12+
const ProgressBar = ({ progress, label = "Progress" }: ProgressBarProps) => {
13+
const progressWidth = boundNumber(progress, 0, 100);
14+
15+
return (
16+
<div
17+
className="progress-bar-container"
18+
role="progressbar"
19+
aria-valuenow={progressWidth}
20+
aria-valuemin={0}
21+
aria-valuemax={100}
22+
aria-label={label}
23+
>
24+
<div className="outer">
25+
<div className="progress-bar-fill" style={{ width: `${progressWidth}%` }}></div>
26+
</div>
27+
<span className="progress-bar-label">{progressWidth}%</span>
28+
</div>
29+
);
30+
};
31+
32+
export { ProgressBar };

frontend/app/store/global.ts

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import {
1010
import { getLayoutModelForStaticTab } from "@/layout/lib/layoutModelHooks";
1111
import { getWebServerEndpoint } from "@/util/endpoints";
1212
import { fetch } from "@/util/fetchutil";
13-
import { getPrefixedSettings, isBlank } from "@/util/util";
13+
import { deepCompareReturnPrev, getPrefixedSettings, isBlank } from "@/util/util";
1414
import { atom, Atom, PrimitiveAtom, useAtomValue } from "jotai";
1515
import { globalStore } from "./jotaiStore";
1616
import { modalsModel } from "./modalmodel";
@@ -314,16 +314,15 @@ function useSettingsKeyAtom<T extends keyof SettingsType>(key: T): SettingsType[
314314
return useAtomValue(getSettingsKeyAtom(key));
315315
}
316316

317-
function useSettingsPrefixAtom(prefix: string): Atom<SettingsType> {
318-
// TODO: use a shallow equal here to make this more efficient
319-
let settingsPrefixAtom = settingsAtomCache.get(prefix + ":") as Atom<SettingsType>;
317+
function getSettingsPrefixAtom(prefix: string): Atom<SettingsType> {
318+
let settingsPrefixAtom = settingsAtomCache.get(prefix + ":");
320319
if (settingsPrefixAtom == null) {
320+
// create a stable, closured reference to use as the deepCompareReturnPrev key
321+
const cacheKey = {};
321322
settingsPrefixAtom = atom((get) => {
322323
const settings = get(atoms.settingsAtom);
323-
if (settings == null) {
324-
return {};
325-
}
326-
return getPrefixedSettings(settings, prefix);
324+
const newValue = getPrefixedSettings(settings, prefix);
325+
return deepCompareReturnPrev(cacheKey, newValue);
327326
});
328327
settingsAtomCache.set(prefix + ":", settingsPrefixAtom);
329328
}
@@ -674,6 +673,7 @@ export {
674673
getObjectId,
675674
getOverrideConfigAtom,
676675
getSettingsKeyAtom,
676+
getSettingsPrefixAtom,
677677
getUserName,
678678
globalStore,
679679
initGlobal,
@@ -700,6 +700,5 @@ export {
700700
useBlockMetaKeyAtom,
701701
useOverrideConfigAtom,
702702
useSettingsKeyAtom,
703-
useSettingsPrefixAtom,
704703
WOS,
705704
};

frontend/app/view/term/term.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,9 @@ import {
1717
getConnStatusAtom,
1818
getOverrideConfigAtom,
1919
getSettingsKeyAtom,
20+
getSettingsPrefixAtom,
2021
globalStore,
2122
useBlockAtom,
22-
useSettingsPrefixAtom,
2323
WOS,
2424
} from "@/store/global";
2525
import * as services from "@/store/services";
@@ -773,7 +773,7 @@ const TerminalView = ({ blockId, model }: TerminalViewProps) => {
773773
const viewRef = React.useRef<HTMLDivElement>(null);
774774
const connectElemRef = React.useRef<HTMLDivElement>(null);
775775
const [blockData] = WOS.useWaveObjectValue<Block>(WOS.makeORef("block", blockId));
776-
const termSettingsAtom = useSettingsPrefixAtom("term");
776+
const termSettingsAtom = getSettingsPrefixAtom("term");
777777
const termSettings = jotai.useAtomValue(termSettingsAtom);
778778
let termMode = blockData?.meta?.["term:mode"] ?? "term";
779779
if (termMode != "term" && termMode != "vdom") {

frontend/app/view/waveai/waveai.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -347,7 +347,7 @@ export class WaveAiModel implements ViewModel {
347347
// Add a typing indicator
348348
globalStore.set(this.addMessageAtom, typingMessage);
349349
const history = await this.fetchAiData();
350-
const beMsg: OpenAiStreamRequest = {
350+
const beMsg: WaveAIStreamRequest = {
351351
clientid: clientId,
352352
opts: opts,
353353
prompt: [...history, newPrompt],

frontend/util/util.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import base64 from "base64-js";
55
import clsx from "clsx";
66
import { Atom, atom, Getter, SetStateAction, Setter, useAtomValue } from "jotai";
77
import { debounce, throttle } from "throttle-debounce";
8+
const prevValueCache = new WeakMap<any, any>(); // stores a previous value for a deep equal comparison (used with the deepCompareReturnPrev function)
89

910
function isBlank(str: string): boolean {
1011
return str == null || str == "";
@@ -42,6 +43,20 @@ function boundNumber(num: number, min: number, max: number): number {
4243
return Math.min(Math.max(num, min), max);
4344
}
4445

46+
// key must be a suitable weakmap key. pass the new value
47+
// it will return the prevValue (for object equality) if the new value is deep equal to the prev value
48+
function deepCompareReturnPrev(key: any, newValue: any): any {
49+
if (key == null) {
50+
return newValue;
51+
}
52+
const previousValue = prevValueCache.get(key);
53+
if (previousValue !== undefined && JSON.stringify(newValue) === JSON.stringify(previousValue)) {
54+
return previousValue;
55+
}
56+
prevValueCache.set(key, newValue);
57+
return newValue;
58+
}
59+
4560
// works for json-like objects (arrays, objects, strings, numbers, booleans)
4661
function jsonDeepEqual(v1: any, v2: any): boolean {
4762
if (v1 === v2) {
@@ -294,6 +309,7 @@ export {
294309
base64ToString,
295310
boundNumber,
296311
countGraphemes,
312+
deepCompareReturnPrev,
297313
fireAndForget,
298314
getPrefixedSettings,
299315
getPromiseState,

go.mod

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ require (
88
github.com/fsnotify/fsnotify v1.8.0
99
github.com/golang-jwt/jwt/v5 v5.2.1
1010
github.com/golang-migrate/migrate/v4 v4.18.1
11+
github.com/google/generative-ai-go v0.19.0
1112
github.com/google/uuid v1.6.0
1213
github.com/gorilla/handlers v1.5.2
1314
github.com/gorilla/mux v1.8.1
@@ -16,7 +17,7 @@ require (
1617
github.com/kevinburke/ssh_config v1.2.0
1718
github.com/mattn/go-sqlite3 v1.14.24
1819
github.com/mitchellh/mapstructure v1.5.0
19-
github.com/sashabaranov/go-openai v1.36.0
20+
github.com/sashabaranov/go-openai v1.36.1
2021
github.com/sawka/txwrap v0.2.0
2122
github.com/shirou/gopsutil/v4 v4.24.11
2223
github.com/skeema/knownhosts v1.3.0
@@ -27,12 +28,24 @@ require (
2728
golang.org/x/crypto v0.31.0
2829
golang.org/x/sys v0.28.0
2930
golang.org/x/term v0.27.0
31+
google.golang.org/api v0.214.0
3032
)
3133

3234
require (
35+
cloud.google.com/go v0.115.0 // indirect
36+
cloud.google.com/go/ai v0.8.0 // indirect
37+
cloud.google.com/go/auth v0.13.0 // indirect
38+
cloud.google.com/go/auth/oauth2adapt v0.2.6 // indirect
39+
cloud.google.com/go/compute/metadata v0.6.0 // indirect
40+
cloud.google.com/go/longrunning v0.5.7 // indirect
3341
github.com/ebitengine/purego v0.8.1 // indirect
3442
github.com/felixge/httpsnoop v1.0.4 // indirect
43+
github.com/go-logr/logr v1.4.2 // indirect
44+
github.com/go-logr/stdr v1.2.2 // indirect
3545
github.com/go-ole/go-ole v1.2.6 // indirect
46+
github.com/google/s2a-go v0.1.8 // indirect
47+
github.com/googleapis/enterprise-certificate-proxy v0.3.4 // indirect
48+
github.com/googleapis/gax-go/v2 v2.14.0 // indirect
3649
github.com/hashicorp/errwrap v1.1.0 // indirect
3750
github.com/hashicorp/go-multierror v1.1.1 // indirect
3851
github.com/inconshreveable/mousetrap v1.1.0 // indirect
@@ -44,8 +57,21 @@ require (
4457
github.com/tklauser/numcpus v0.6.1 // indirect
4558
github.com/ubuntu/decorate v0.0.0-20230125165522-2d5b0a9bb117 // indirect
4659
github.com/yusufpapurcu/wmi v1.2.4 // indirect
60+
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.54.0 // indirect
61+
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0 // indirect
62+
go.opentelemetry.io/otel v1.29.0 // indirect
63+
go.opentelemetry.io/otel/metric v1.29.0 // indirect
64+
go.opentelemetry.io/otel/trace v1.29.0 // indirect
4765
go.uber.org/atomic v1.7.0 // indirect
4866
golang.org/x/net v0.33.0 // indirect
67+
golang.org/x/oauth2 v0.24.0 // indirect
68+
golang.org/x/sync v0.10.0 // indirect
69+
golang.org/x/text v0.21.0 // indirect
70+
golang.org/x/time v0.8.0 // indirect
71+
google.golang.org/genproto/googleapis/api v0.0.0-20241104194629-dd2ea8efbc28 // indirect
72+
google.golang.org/genproto/googleapis/rpc v0.0.0-20241209162323-e6fa225c2576 // indirect
73+
google.golang.org/grpc v1.67.1 // indirect
74+
google.golang.org/protobuf v1.35.2 // indirect
4975
)
5076

5177
replace github.com/kevinburke/ssh_config => github.com/wavetermdev/ssh_config v0.0.0-20241219203747-6409e4292f34

0 commit comments

Comments
 (0)