Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 52 additions & 1 deletion docs/docs/waveai-modes.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ Wave AI now supports provider-based configuration which automatically applies se
- **`google`** - Google AI (Gemini) [[see example](#google-ai-gemini)]
- **`azure`** - Azure OpenAI Service (modern API) [[see example](#azure-openai-modern-api)]
- **`azure-legacy`** - Azure OpenAI Service (legacy deployment API) [[see example](#azure-openai-legacy-deployment-api)]
- **`github-copilot`** - GitHub Copilot (authenticates via device flow, auto-creates modes) [[see example](#github-copilot)]
- **`custom`** - Custom API endpoint (fully manual configuration) [[see examples](#local-model-examples)]

### Supported API Types
Expand All @@ -46,6 +47,7 @@ Wave AI supports the following API types:
- **`openai-chat`**: Uses the `/v1/chat/completions` endpoint (most common)
- **`openai-responses`**: Uses the `/v1/responses` endpoint (modern API for GPT-5+ models)
- **`google-gemini`**: Google's Gemini API format (automatically set when using `ai:provider: "google"`, not typically used directly)
- **`github-copilot`**: GitHub Copilot API (automatically set when using `ai:provider: "github-copilot"`, not typically used directly)

## Global Wave AI Settings

Expand Down Expand Up @@ -336,6 +338,55 @@ The provider automatically constructs the full endpoint URL and sets the API ver
For Azure Legacy provider, you must manually specify `ai:capabilities` based on your model's features.
:::

### GitHub Copilot

[GitHub Copilot](https://github.com/features/copilot) is available to users with a GitHub Copilot subscription. Unlike other providers, Copilot uses **GitHub's device authorization flow** instead of an API key — no manual key configuration is needed.

#### Quick Setup

1. Open the AI mode dropdown in Wave
2. Click **"Login to GitHub Copilot"**
3. A device code is copied to your clipboard and GitHub opens in your browser
4. Paste the code on GitHub and authorize Wave Terminal
5. Copilot modes are automatically created for all models your subscription supports

After login, copilot modes (e.g., `copilot/gpt-4o`, `copilot/gpt-4.1`, `copilot/claude-3.7-sonnet`) appear in the dropdown and in your `waveai.json`.

#### How It Works

Authentication is handled automatically:
- Your GitHub token is stored securely in Wave's [secret store](./secrets.mdx) as `GITHUB_COPILOT_TOKEN`
- Wave exchanges this for a short-lived Copilot API token on each request
- Available models are discovered automatically from the Copilot API based on your subscription

#### Auto-Generated Configuration

After login, Wave creates mode entries like this in `waveai.json`:

```json
{
"copilot/gpt-4o": {
"display:name": "Copilot gpt-4o",
"display:order": 10,
"display:icon": "github",
"display:description": "GitHub Copilot (gpt-4o)",
"ai:provider": "github-copilot",
"ai:model": "gpt-4o",
"ai:capabilities": ["tools", "images"]
}
}
```

You can customize these entries (change display names, order, etc.) just like any other mode.

#### Logging Out

To disconnect your GitHub Copilot account, click the **"Logout"** button next to "Copilot Connected" in the AI mode dropdown. This deletes the stored token. You can log in again at any time.

:::note
The available models depend on your Copilot subscription tier. Common models include `gpt-4o`, `gpt-4.1`, `gpt-4.1-mini`, `gpt-4.1-nano`, `o3-mini`, `claude-3.5-sonnet`, `claude-3.7-sonnet`, and `gemini-2.0-flash-001`.
:::

## Using Secrets for API Keys

Instead of storing API keys directly in the configuration, you should use Wave's secret store to keep your credentials secure. Secrets are stored encrypted using your system's native keychain.
Expand Down Expand Up @@ -473,7 +524,7 @@ If you get "model not found" errors:
| `display:order` | No | Sort order in the selector (lower numbers first) |
| `display:icon` | No | Icon identifier for the mode (can use any [FontAwesome icon](https://fontawesome.com/search), use the name without the "fa-" prefix). Default is "sparkles" |
| `display:description` | No | Full description of the mode |
| `ai:provider` | No | Provider preset: `openai`, `openrouter`, `google`, `azure`, `azure-legacy`, `custom` |
| `ai:provider` | No | Provider preset: `openai`, `openrouter`, `google`, `azure`, `azure-legacy`, `github-copilot`, `custom` |
| `ai:apitype` | No | API type: `openai-chat`, `openai-responses`, or `google-gemini` (defaults to `openai-chat` if not specified) |
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

ai:apitype field reference is missing github-copilot.

Line 528 lists valid API types as openai-chat, openai-responses, or google-gemini, but github-copilot was added to the supported API types list at line 50 and to the schema. Update the field reference to include it for consistency.

🤖 Prompt for AI Agents
In `@docs/docs/waveai-modes.mdx` at line 528, The table row for the ai:apitype
field is missing the `github-copilot` option; update the `ai:apitype` field
reference in docs/waveai-modes.mdx (the table entry containing "`ai:apitype` |
No | API type: ...") to include `github-copilot` alongside `openai-chat`,
`openai-responses`, and `google-gemini` so it matches the schema and the earlier
mention of supported API types.

| `ai:model` | No | Model identifier (required for most providers) |
| `ai:thinkinglevel` | No | Thinking level: `low`, `medium`, or `high` |
Expand Down
155 changes: 153 additions & 2 deletions frontend/app/aipanel/aimode.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,12 @@
// SPDX-License-Identifier: Apache-2.0

import { Tooltip } from "@/app/element/tooltip";
import { atoms, getSettingsKeyAtom } from "@/app/store/global";
import { atoms, getApi, getSettingsKeyAtom } from "@/app/store/global";
import { RpcApi } from "@/app/store/wshclientapi";
import { TabRpcClient } from "@/app/store/wshrpcutil";
import { cn, fireAndForget, makeIconClass } from "@/util/util";
import { useAtomValue } from "jotai";
import { memo, useRef, useState } from "react";
import { memo, useCallback, useEffect, useRef, useState } from "react";
import { getFilteredAIModeConfigs, getModeDisplayName } from "./ai-utils";
import { WaveAIModel } from "./waveai-model";

Expand Down Expand Up @@ -147,6 +147,91 @@ export const AIModeDropdown = memo(({ compatibilityMode = false }: AIModeDropdow
const telemetryEnabled = useAtomValue(getSettingsKeyAtom("telemetry:enabled")) ?? false;
const [isOpen, setIsOpen] = useState(false);
const dropdownRef = useRef<HTMLDivElement>(null);
const copilotLoginAbortRef = useRef<AbortController | null>(null);
const [copilotLoggedIn, setCopilotLoggedIn] = useState(false);
const [copilotLoginState, setCopilotLoginState] = useState<
| { status: "idle" }
| { status: "waiting"; userCode: string; verificationUri: string }
| { status: "polling" }
| { status: "complete" }
| { status: "error"; message: string }
>({ status: "idle" });

// Check Copilot login status on mount and when dropdown opens
useEffect(() => {
fireAndForget(async () => {
try {
const status = await RpcApi.CopilotDeviceLoginStatusCommand(TabRpcClient);
setCopilotLoggedIn(status.loggedin);
} catch {
// ignore errors
}
});
}, [isOpen]);
Comment on lines +161 to +170
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Missing cleanup on unmount — leaked RPC poll and timers.

If the component unmounts (e.g., tab switch) while a Copilot login poll is in progress, the fireAndForget async chain continues running, and setTimeout callbacks (lines 197, 201, 207, 212) will attempt to set state on an unmounted component. The abort controller should be triggered on unmount.

🛡️ Proposed fix — add cleanup effect
     // Check Copilot login status on mount and when dropdown opens
     useEffect(() => {
         fireAndForget(async () => {
             try {
                 const status = await RpcApi.CopilotDeviceLoginStatusCommand(TabRpcClient);
                 setCopilotLoggedIn(status.loggedin);
             } catch {
                 // ignore errors
             }
         });
+        return () => {
+            if (copilotLoginAbortRef.current) {
+                copilotLoginAbortRef.current.abort();
+                copilotLoginAbortRef.current = null;
+            }
+        };
     }, [isOpen]);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
useEffect(() => {
fireAndForget(async () => {
try {
const status = await RpcApi.CopilotDeviceLoginStatusCommand(TabRpcClient);
setCopilotLoggedIn(status.loggedin);
} catch {
// ignore errors
}
});
}, [isOpen]);
useEffect(() => {
fireAndForget(async () => {
try {
const status = await RpcApi.CopilotDeviceLoginStatusCommand(TabRpcClient);
setCopilotLoggedIn(status.loggedin);
} catch {
// ignore errors
}
});
return () => {
if (copilotLoginAbortRef.current) {
copilotLoginAbortRef.current.abort();
copilotLoginAbortRef.current = null;
}
};
}, [isOpen]);
🤖 Prompt for AI Agents
In `@frontend/app/aipanel/aimode.tsx` around lines 161 - 170, The useEffect
starting with fireAndForget that calls
RpcApi.CopilotDeviceLoginStatusCommand/TabRpcClient and then calls
setCopilotLoggedIn leaks work on unmount; modify it to create an AbortController
(or an isCancelled flag) inside the effect, pass its signal to any cancellable
RPC call (RpcApi.CopilotDeviceLoginStatusCommand) if supported, store any
setTimeout IDs used later (the timers referenced around lines 197/201/207/212)
and clear them in the effect cleanup, and in async callbacks check
abortController.signal.aborted (or isCancelled) before calling
setCopilotLoggedIn so state is never set after unmount. Ensure the cleanup
function calls abortController.abort() and clears all pending timeouts.


const handleCopilotLogin = useCallback(() => {
const abortController = new AbortController();
copilotLoginAbortRef.current = abortController;
fireAndForget(async () => {
setCopilotLoginState({ status: "polling" });
try {
const startData = await RpcApi.CopilotDeviceLoginStartCommand(TabRpcClient);
if (abortController.signal.aborted) return;
setCopilotLoginState({
status: "waiting",
userCode: startData.usercode,
verificationUri: startData.verificationuri,
});
// Copy code to clipboard and open browser
await navigator.clipboard.writeText(startData.usercode);
getApi().openExternal(startData.verificationuri);
// Now poll for completion
const pollResult = await RpcApi.CopilotDeviceLoginPollCommand(TabRpcClient, {}, { timeout: 15 * 60 * 1000 });
if (abortController.signal.aborted) return;
if (pollResult.status === "complete") {
setCopilotLoginState({ status: "complete" });
setCopilotLoggedIn(true);
// Auto-switch to the copilot mode if one was created
if (pollResult.modename) {
// Wait briefly for config watcher to pick up the new mode
setTimeout(() => {
model.setAIMode(pollResult.modename);
}, 500);
Comment on lines +196 to +199
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Race condition: 500ms setTimeout to auto-switch mode is fragile.

The delay assumes the config watcher will have processed the new Copilot modes within 500ms, but this depends on file I/O, watcher debounce, and event propagation timing. If the mode hasn't been registered yet, model.setAIMode will silently fail or set an invalid mode.

Consider subscribing to config updates and switching when the mode actually becomes available, rather than relying on a fixed delay.

🤖 Prompt for AI Agents
In `@frontend/app/aipanel/aimode.tsx` around lines 196 - 199, The 500ms setTimeout
that calls model.setAIMode(pollResult.modename) is brittle and can race with the
config watcher; remove the fixed delay and instead wait until the new mode is
actually registered before switching — e.g., subscribe to the config/modes
update event (or poll model.hasMode / availableModes) and only call
model.setAIMode(pollResult.modename) once the registry reports the mode exists,
with a reasonable fallback timeout and error/logging path if it never appears;
replace the setTimeout block with this event-driven or existence-checked flow so
model.setAIMode is called reliably.

}
setTimeout(() => setCopilotLoginState({ status: "idle" }), 3000);
} else {
setCopilotLoginState({
status: "error",
message: pollResult.error || "Login failed",
});
setTimeout(() => setCopilotLoginState({ status: "idle" }), 5000);
}
} catch (e: any) {
if (abortController.signal.aborted) return;
setCopilotLoginState({ status: "error", message: e.message || "Login failed" });
setTimeout(() => setCopilotLoginState({ status: "idle" }), 5000);
}
});
}, []);
Comment on lines +172 to +215
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# Search for CopilotDeviceLoginPollCommand definition
rg -n "CopilotDeviceLoginPollCommand" --type ts --type=go -B 2 -A 5

Repository: wavetermdev/waveterm

Length of output: 4072


🏁 Script executed:

# Search for RpcOpts definition and how it's used
rg -n "RpcOpts" -A 10 --type ts | head -80

Repository: wavetermdev/waveterm

Length of output: 6446


🏁 Script executed:

# Search for RpcOpts type/interface definition
rg -n "interface RpcOpts|type RpcOpts" --type ts -A 10

Repository: wavetermdev/waveterm

Length of output: 650


🏁 Script executed:

# Get the full implementation of CopilotDeviceLoginPollCommand in Go
sed -n '1549,1600p' pkg/wshrpc/wshserver/wshserver.go

Repository: wavetermdev/waveterm

Length of output: 1698


🏁 Script executed:

# Search for how timeout is handled on the server side in the RPC processing
rg -n "timeout" pkg/wshrpc/wshutil/ --type go -B 2 -A 2 | head -100

Repository: wavetermdev/waveterm

Length of output: 123


🏁 Script executed:

# Find the correct directory structure
fd . pkg/wshrpc --type d | head -20

Repository: wavetermdev/waveterm

Length of output: 129


🏁 Script executed:

# Search for timeout handling in the wshrpc package
rg -n "timeout" pkg/wshrpc/ --type go -B 2 -A 2 | head -150

Repository: wavetermdev/waveterm

Length of output: 3058


🏁 Script executed:

# Search for RPC message dispatch and handler invocation
rg -n "timeout" pkg/wshrpc/wshserver/ --type go -B 3 -A 3 | head -150

Repository: wavetermdev/waveterm

Length of output: 46


🏁 Script executed:

# Search for how RPC commands are dispatched/handled
rg -n "handleRequest|dispatch|invokeCommand" pkg/wshrpc/ --type go -B 2 -A 5 | head -150

Repository: wavetermdev/waveterm

Length of output: 46


🏁 Script executed:

# Search for message handling and command routing
rg -n "command.*RpcMessage|func.*command" pkg/wshrpc/wshserver/ --type go -B 1 -A 3 | head -150

Repository: wavetermdev/waveterm

Length of output: 46


🏁 Script executed:

# List files in wshserver
fd . pkg/wshrpc/wshserver/ --type f

# Also search for where commands are actually invoked
rg -n "case.*:" pkg/wshrpc/wshserver/ --type go -B 1 -A 3 | head -100

Repository: wavetermdev/waveterm

Length of output: 3537


🏁 Script executed:

# Look for how RPC commands are dispatched from client messages to server handlers
rg -n "sendRpcRequest|wshRpcCall" pkg/wshrpc/wshclient/ --type go -B 2 -A 10 | head -150

Repository: wavetermdev/waveterm

Length of output: 11983


🏁 Script executed:

# Search for WshRpc implementation and how it processes opts/timeout
rg -n "SendCommand|SendComplexRequest" --type go -B 2 -A 10 | head -200

Repository: wavetermdev/waveterm

Length of output: 6716


🏁 Script executed:

# Search for RPC message dispatch on server side
rg -n "invokeHandler|processRpc|dispatchCommand|handleMessage" pkg/wshrpc/ --type go -B 2 -A 10 | head -200

Repository: wavetermdev/waveterm

Length of output: 46


🏁 Script executed:

# Look at wshserver.go structure - search for message handling
rg -n "func.*message|processMsg|handleMsg" pkg/wshrpc/wshserver/wshserver.go --type go -B 1 -A 5

Repository: wavetermdev/waveterm

Length of output: 46


🏁 Script executed:

# Look at wshserver.go line counts and search for dispatch/invoke patterns more broadly
wc -l pkg/wshrpc/wshserver/wshserver.go

# Search for where commands are actually invoked with the message
rg -n "reflect|invokeMethod" pkg/wshrpc/wshserver/ --type go -B 2 -A 5 | head -100

Repository: wavetermdev/waveterm

Length of output: 102


AbortController doesn't cancel the backend RPC poll.

abortController.signal.aborted is only checked between async steps (lines 179, 190, 210), but the long-running CopilotDeviceLoginPollCommand (line 189, 15-minute timeout) isn't passed the abort signal and will continue executing server-side even after "Cancel" is clicked. The cancel is cosmetic — the backend poll continues consuming resources for up to 15 minutes.

The RpcOpts interface only supports timeout, noresponse, and route fields — there is no mechanism to pass an abort/cancellation signal through the RPC layer. Consider either:

  1. Implementing a cancellation endpoint the client can call to interrupt in-flight polls
  2. Or at minimum, documenting that cancel is UI-only
🤖 Prompt for AI Agents
In `@frontend/app/aipanel/aimode.tsx` around lines 172 - 215, The long-running
poll started in handleCopilotLogin calls RpcApi.CopilotDeviceLoginPollCommand
with a 15-minute timeout but does not actually cancel server-side when the
AbortController is triggered; implement cancellation by adding a client cancel
call and backend handler (e.g., introduce RpcApi.CopilotDeviceLoginCancelCommand
/ backend CopilotDeviceLoginCancel endpoint) that signals the server poll to
stop, wire the UI cancel path to call that cancel RPC when
copilotLoginAbortRef.current.abort() is invoked, and update
CopilotDeviceLoginPollCommand server logic to listen for that cancel signal (or
a shared cancellation token) so the server stops the in-flight poll immediately
instead of continuing for the full timeout; alternatively, if you prefer
RPC-layer cancellation, extend the RpcOpts shape (used by
CopilotDeviceLoginPollCommand) to carry an abort/cancellation token and thread
that through the RPC transport and server poll loop so the server can abort on
demand.


const handleCopilotLoginCancel = useCallback(() => {
if (copilotLoginAbortRef.current) {
copilotLoginAbortRef.current.abort();
copilotLoginAbortRef.current = null;
}
setCopilotLoginState({ status: "idle" });
}, []);

const handleCopilotLogout = useCallback(() => {
fireAndForget(async () => {
try {
await RpcApi.CopilotDeviceLogoutCommand(TabRpcClient);
setCopilotLoggedIn(false);
} catch (e: any) {
console.error("Failed to logout from Copilot:", e);
}
});
}, []);

const { waveProviderConfigs, otherProviderConfigs } = getFilteredAIModeConfigs(
aiModeConfigs,
Expand Down Expand Up @@ -312,6 +397,72 @@ export const AIModeDropdown = memo(({ compatibilityMode = false }: AIModeDropdow
<i className={makeIconClass("plus", false)}></i>
<span className="text-sm">New Chat</span>
</button>
<div className="border-t border-gray-600 my-1" />
{copilotLoginState.status === "idle" && !copilotLoggedIn && (
<button
onClick={handleCopilotLogin}
className="w-full flex items-center gap-2 px-3 pt-1 pb-1 text-gray-300 hover:bg-zinc-700 cursor-pointer transition-colors text-left"
>
<i className="fa-brands fa-github text-[12px]"></i>
<span className="text-sm">Login to GitHub Copilot</span>
</button>
)}
{copilotLoginState.status === "idle" && copilotLoggedIn && (
<div className="w-full flex items-center justify-between px-3 pt-1 pb-1 text-left">
<div className="flex items-center gap-2 text-green-400">
<i className="fa-brands fa-github text-[12px]"></i>
<span className="text-sm">Copilot Connected</span>
</div>
<button
onClick={handleCopilotLogout}
className="text-[11px] text-gray-500 hover:text-red-400 cursor-pointer transition-colors"
>
Logout
</button>
</div>
)}
{copilotLoginState.status === "polling" && (
<div className="w-full flex items-center gap-2 px-3 pt-1 pb-1 text-gray-400 text-left">
<i className="fa fa-spinner fa-spin text-[12px]"></i>
<span className="text-sm">Starting login...</span>
</div>
)}
{copilotLoginState.status === "waiting" && (
<div className="w-full flex flex-col gap-1 px-3 pt-1 pb-1 text-left">
<div className="flex items-center gap-2 text-green-300">
<i className="fa fa-key text-[12px]"></i>
<span className="text-sm font-bold">{copilotLoginState.userCode}</span>
</div>
<div className="text-[11px] text-gray-400">
Code copied! Paste it on GitHub.
</div>
<div className="flex items-center justify-between text-gray-400">
<div className="flex items-center gap-2">
<i className="fa fa-spinner fa-spin text-[10px]"></i>
<span className="text-[11px]">Waiting for authorization...</span>
</div>
<button
onClick={handleCopilotLoginCancel}
className="text-[11px] text-red-400 hover:text-red-300 cursor-pointer transition-colors"
>
Cancel
</button>
</div>
</div>
)}
{copilotLoginState.status === "complete" && (
<div className="w-full flex items-center gap-2 px-3 pt-1 pb-1 text-green-400 text-left">
<i className="fa fa-check text-[12px]"></i>
<span className="text-sm">Logged in to GitHub Copilot!</span>
</div>
)}
{copilotLoginState.status === "error" && (
<div className="w-full flex items-center gap-2 px-3 pt-1 pb-1 text-red-400 text-left">
<i className="fa fa-triangle-exclamation text-[12px]"></i>
<span className="text-sm">{copilotLoginState.message}</span>
</div>
)}
<div className="border-t border-gray-600 my-1" />
<button
onClick={handleConfigureClick}
className="w-full flex items-center gap-2 px-3 pt-1 pb-2 text-gray-300 hover:bg-zinc-700 cursor-pointer transition-colors text-left"
Expand Down
20 changes: 20 additions & 0 deletions frontend/app/store/wshclientapi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,26 @@ class RpcApiType {
return client.wshRpcCall("controllerresync", data, opts);
}

// command "copilotdeviceloginpoll" [call]
CopilotDeviceLoginPollCommand(client: WshClient, data: CopilotDeviceLoginPollData, opts?: RpcOpts): Promise<CopilotDeviceLoginPollRtnData> {
return client.wshRpcCall("copilotdeviceloginpoll", data, opts);
}

// command "copilotdeviceloginstart" [call]
CopilotDeviceLoginStartCommand(client: WshClient, opts?: RpcOpts): Promise<CopilotDeviceLoginStartRtnData> {
return client.wshRpcCall("copilotdeviceloginstart", null, opts);
}

// command "copilotdeviceloginstatus" [call]
CopilotDeviceLoginStatusCommand(client: WshClient, opts?: RpcOpts): Promise<CopilotDeviceLoginStatusRtnData> {
return client.wshRpcCall("copilotdeviceloginstatus", null, opts);
}

// command "copilotdevicelogout" [call]
CopilotDeviceLogoutCommand(client: WshClient, opts?: RpcOpts): Promise<void> {
return client.wshRpcCall("copilotdevicelogout", null, opts);
}

// command "createblock" [call]
CreateBlockCommand(client: WshClient, data: CommandCreateBlockData, opts?: RpcOpts): Promise<ORef> {
return client.wshRpcCall("createblock", data, opts);
Expand Down
24 changes: 24 additions & 0 deletions frontend/types/gotypes.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -800,6 +800,30 @@ declare global {
keepalivesenttime?: number;
};

// wshrpc.CopilotDeviceLoginPollData
type CopilotDeviceLoginPollData = {
};

// wshrpc.CopilotDeviceLoginPollRtnData
type CopilotDeviceLoginPollRtnData = {
status: string;
error?: string;
modename?: string;
};

// wshrpc.CopilotDeviceLoginStartRtnData
type CopilotDeviceLoginStartRtnData = {
usercode: string;
verificationuri: string;
expiresin: number;
};

// wshrpc.CopilotDeviceLoginStatusRtnData
type CopilotDeviceLoginStatusRtnData = {
loggedin: boolean;
error?: string;
};

// wshrpc.CpuDataRequest
type CpuDataRequest = {
id: string;
Expand Down
Loading