mirror of
https://github.com/cline/cline.git
synced 2026-10-24 02:02:27 +08:00
Merge branch 'main' into harrison/dat-111-desktop-privacy-link
This commit is contained in:
@@ -419,6 +419,9 @@ test.each([
|
||||
PATH: root,
|
||||
JITI_FS_CACHE: "false",
|
||||
CLINE_PLUGIN_MODE: mode,
|
||||
// Cold Windows runners can take more than the 4s default to start a
|
||||
// sandbox child; this test covers resource loading, not startup time.
|
||||
CLINE_PLUGIN_IMPORT_TIMEOUT_MS: "20000",
|
||||
CLINE_TEST_PLUGIN_RESULT_PATH: marker,
|
||||
TMPDIR: root,
|
||||
CLINE_NO_AUTO_UPDATE: "1",
|
||||
|
||||
@@ -98,10 +98,11 @@ describe("ChatTextArea model label", () => {
|
||||
it.each([
|
||||
["a new task targets Cloud", { cloudSessionsEnabled: true, cloudTaskTarget: { target: "cloud" } }],
|
||||
["a cloud task is shown", { currentCloudTask: { sessionId: "ses-1", status: "running" } }],
|
||||
])("shows the cloud model when %s", (_case, cloudState) => {
|
||||
])("shows the cloud model and hides the Plan/Act toggle when %s", (_case, cloudState) => {
|
||||
mocks.cloudState = { ...cloudState, cloudModelId: "anthropic/claude-sonnet-4.5" }
|
||||
renderTextArea()
|
||||
expect(screen.getByTitle("Open API Settings")).toHaveTextContent("cline:anthropic/claude-sonnet-4.5")
|
||||
expect(screen.queryByTestId("mode-switch")).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -116,7 +117,7 @@ describe("ChatTextArea image attachments vs. model capability", () => {
|
||||
renderTextArea()
|
||||
|
||||
expect(screen.getByPlaceholderText("Type a message")).toBeInTheDocument()
|
||||
expect(screen.getByTestId("mode-switch")).toHaveAttribute("aria-disabled", "false")
|
||||
expect(screen.getByTestId("mode-switch")).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it("still takes the image attach path on paste for a text-only model, without a refusal message", () => {
|
||||
|
||||
@@ -96,15 +96,14 @@ interface GitCommit {
|
||||
const PLAN_MODE_COLOR = "var(--vscode-activityWarningBadge-background)"
|
||||
const ACT_MODE_COLOR = "var(--vscode-focusBorder)"
|
||||
|
||||
const SwitchContainer = styled.div<{ disabled: boolean }>`
|
||||
const SwitchContainer = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background-color: transparent;
|
||||
border: 1px solid var(--vscode-input-border);
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
cursor: ${(props) => (props.disabled ? "not-allowed" : "pointer")};
|
||||
opacity: ${(props) => (props.disabled ? 0.5 : 1)};
|
||||
cursor: pointer;
|
||||
transform: scale(1);
|
||||
transform-origin: right center;
|
||||
margin-left: 0;
|
||||
@@ -233,7 +232,7 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
|
||||
} = useExtensionState()
|
||||
// Cloud sessions are Act-only (like the desktop app and the cloud dashboard).
|
||||
// While a cloud task is shown, or a new task is about to run in the cloud,
|
||||
// the toggle is pinned to Act and disabled; the stored mode is untouched.
|
||||
// the mode is pinned to Act and the toggle is hidden; the stored mode is untouched.
|
||||
const cloudActOnly =
|
||||
!!currentCloudTask || (clineMessages.length === 0 && !!cloudSessionsEnabled && cloudTaskTarget?.target === "cloud")
|
||||
const mode = cloudActOnly ? "act" : selectedMode
|
||||
@@ -1698,47 +1697,38 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
|
||||
</ModelContainer>
|
||||
</ButtonGroup>
|
||||
</div>
|
||||
{/* Tooltip for Plan/Act toggle remains outside the conditional rendering */}
|
||||
<Tooltip>
|
||||
<TooltipContent
|
||||
className="text-xs px-2 flex flex-col gap-1"
|
||||
hidden={shownTooltipMode === null}
|
||||
side="top">
|
||||
{cloudActOnly ? (
|
||||
"Cloud sessions only support Act mode for now."
|
||||
) : (
|
||||
<>
|
||||
{`In ${shownTooltipMode === "act" ? "Act" : "Plan"} mode, Cline will ${shownTooltipMode === "act" ? "complete the task immediately" : "gather information to architect a plan"}`}
|
||||
<p className="text-description/80 text-xs mb-0">
|
||||
Toggle w/ <kbd className="text-muted-foreground mx-1">{togglePlanActKeys}</kbd>
|
||||
</p>
|
||||
</>
|
||||
)}
|
||||
</TooltipContent>
|
||||
<TooltipTrigger>
|
||||
<SwitchContainer
|
||||
aria-disabled={cloudActOnly}
|
||||
data-testid="mode-switch"
|
||||
disabled={cloudActOnly}
|
||||
onClick={onModeToggle}>
|
||||
<Slider isAct={mode === "act"} isPlan={mode === "plan"} />
|
||||
{["Plan", "Act"].map((m) => (
|
||||
<div
|
||||
aria-checked={mode === m.toLowerCase()}
|
||||
className={cn(
|
||||
"pt-0.5 pb-px px-2 z-10 text-xs w-1/2 text-center bg-transparent",
|
||||
mode === m.toLowerCase() ? "text-white" : "text-input-foreground",
|
||||
)}
|
||||
key={m}
|
||||
onMouseLeave={() => setShownTooltipMode(null)}
|
||||
onMouseOver={() => setShownTooltipMode(m.toLowerCase() === "plan" ? "plan" : "act")}
|
||||
role="switch">
|
||||
{m}
|
||||
</div>
|
||||
))}
|
||||
</SwitchContainer>
|
||||
</TooltipTrigger>
|
||||
</Tooltip>
|
||||
{!cloudActOnly && (
|
||||
<Tooltip>
|
||||
<TooltipContent
|
||||
className="text-xs px-2 flex flex-col gap-1"
|
||||
hidden={shownTooltipMode === null}
|
||||
side="top">
|
||||
{`In ${shownTooltipMode === "act" ? "Act" : "Plan"} mode, Cline will ${shownTooltipMode === "act" ? "complete the task immediately" : "gather information to architect a plan"}`}
|
||||
<p className="text-description/80 text-xs mb-0">
|
||||
Toggle w/ <kbd className="text-muted-foreground mx-1">{togglePlanActKeys}</kbd>
|
||||
</p>
|
||||
</TooltipContent>
|
||||
<TooltipTrigger>
|
||||
<SwitchContainer data-testid="mode-switch" onClick={onModeToggle}>
|
||||
<Slider isAct={mode === "act"} isPlan={mode === "plan"} />
|
||||
{["Plan", "Act"].map((m) => (
|
||||
<div
|
||||
aria-checked={mode === m.toLowerCase()}
|
||||
className={cn(
|
||||
"pt-0.5 pb-px px-2 z-10 text-xs w-1/2 text-center bg-transparent",
|
||||
mode === m.toLowerCase() ? "text-white" : "text-input-foreground",
|
||||
)}
|
||||
key={m}
|
||||
onMouseLeave={() => setShownTooltipMode(null)}
|
||||
onMouseOver={() => setShownTooltipMode(m.toLowerCase() === "plan" ? "plan" : "act")}
|
||||
role="switch">
|
||||
{m}
|
||||
</div>
|
||||
))}
|
||||
</SwitchContainer>
|
||||
</TooltipTrigger>
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -225,7 +225,8 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
|
||||
// Cloud sandboxes cannot read local files; only images (sent inline) can be attached.
|
||||
const imagesOnly =
|
||||
!!currentCloudTask || (messages.length === 0 && cloudSessionsEnabled && cloudTaskTarget?.target === "cloud")
|
||||
const cloudAutoApprove =
|
||||
// Cloud sessions always auto-approve; local auto-approve settings don't apply there.
|
||||
const hideAutoApprove =
|
||||
!!currentCloudTask || (messages.length === 0 && cloudSessionsEnabled && cloudTaskTarget?.target === "cloud")
|
||||
|
||||
const selectFilesAndImages = useCallback(async () => {
|
||||
@@ -426,7 +427,7 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
|
||||
)}
|
||||
</div>
|
||||
<footer className="bg-(--vscode-sidebar-background) flex flex-col" style={{ gridRow: "2" }}>
|
||||
<AutoApproveBar cloudAutoApprove={cloudAutoApprove} />
|
||||
{!hideAutoApprove && <AutoApproveBar />}
|
||||
<ActionButtons
|
||||
chatState={chatState}
|
||||
messageHandlers={messageHandlers}
|
||||
|
||||
@@ -1,87 +0,0 @@
|
||||
import { fireEvent, render, screen } from "@testing-library/react"
|
||||
import userEvent from "@testing-library/user-event"
|
||||
import type React from "react"
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest"
|
||||
import AutoApproveBar from "./AutoApproveBar"
|
||||
|
||||
vi.mock("@vscode/webview-ui-toolkit/react", () => ({
|
||||
VSCodeCheckbox: ({ checked, children, disabled }: { checked: boolean; children: React.ReactNode; disabled: boolean }) => (
|
||||
<label>
|
||||
<input checked={checked} disabled={disabled} readOnly type="checkbox" />
|
||||
{children}
|
||||
</label>
|
||||
),
|
||||
}))
|
||||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
autoApprovalSettings: {
|
||||
version: 1,
|
||||
enabled: true,
|
||||
favorites: [],
|
||||
maxRequests: 20,
|
||||
actions: {
|
||||
readFiles: true,
|
||||
editFiles: false,
|
||||
executeSafeCommands: false,
|
||||
executeAllCommands: false,
|
||||
useBrowser: false,
|
||||
useMcp: false,
|
||||
},
|
||||
enableNotifications: false,
|
||||
},
|
||||
updateAutoApproveSettings: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock("@/context/ExtensionStateContext", () => ({
|
||||
useExtensionState: () => ({ autoApprovalSettings: mocks.autoApprovalSettings, hostName: "PyCharm Professional" }),
|
||||
}))
|
||||
|
||||
vi.mock("./AutoApproveSettingsAPI", () => ({
|
||||
updateAutoApproveSettings: mocks.updateAutoApproveSettings,
|
||||
}))
|
||||
|
||||
describe("AutoApproveBar", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
vi.stubGlobal(
|
||||
"ResizeObserver",
|
||||
class {
|
||||
observe() {}
|
||||
unobserve() {}
|
||||
disconnect() {}
|
||||
},
|
||||
)
|
||||
})
|
||||
|
||||
it("shows forced read-only cloud approvals without changing local settings", async () => {
|
||||
const localSettings = structuredClone(mocks.autoApprovalSettings)
|
||||
const user = userEvent.setup()
|
||||
const { rerender } = render(<AutoApproveBar cloudAutoApprove />)
|
||||
const trigger = screen.getByLabelText("Open auto-approve settings")
|
||||
|
||||
expect(trigger).toHaveTextContent("Auto-approve:Read, Edit, Commands, Web Fetch, MCP")
|
||||
await user.hover(trigger)
|
||||
expect(await screen.findByText(/when PyCharm is closed\./)).toBeInTheDocument()
|
||||
|
||||
fireEvent.click(trigger)
|
||||
expect(
|
||||
screen.getByText("Cloud sessions always auto-approve these actions. Your local settings are unchanged."),
|
||||
).toBeInTheDocument()
|
||||
|
||||
const checkboxes = screen.getAllByRole("checkbox")
|
||||
expect(checkboxes).toHaveLength(5)
|
||||
for (const checkbox of checkboxes) {
|
||||
expect(checkbox).toBeChecked()
|
||||
expect(checkbox).toBeDisabled()
|
||||
fireEvent.click(checkbox)
|
||||
}
|
||||
expect(mocks.updateAutoApproveSettings).not.toHaveBeenCalled()
|
||||
expect(mocks.autoApprovalSettings).toEqual(localSettings)
|
||||
|
||||
fireEvent.click(screen.getByLabelText("Close auto-approve settings"))
|
||||
rerender(<AutoApproveBar />)
|
||||
expect(screen.getByLabelText("Open auto-approve settings")).toHaveTextContent("Auto-approve:Read")
|
||||
expect(screen.queryByText(/when PyCharm is closed\./)).not.toBeInTheDocument()
|
||||
expect(mocks.autoApprovalSettings).toEqual(localSettings)
|
||||
})
|
||||
})
|
||||
@@ -1,22 +1,16 @@
|
||||
import { ChevronDownIcon, ChevronRightIcon } from "lucide-react"
|
||||
import { useRef, useState } from "react"
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"
|
||||
import { useExtensionState } from "@/context/ExtensionStateContext"
|
||||
import { editorName } from "@/utils/editorName"
|
||||
import { getAsVar, VSC_TITLEBAR_INACTIVE_FOREGROUND } from "@/utils/vscStyles"
|
||||
import AutoApproveModal from "./AutoApproveModal"
|
||||
import { ACTION_METADATA } from "./constants"
|
||||
|
||||
interface AutoApproveBarProps {
|
||||
style?: React.CSSProperties
|
||||
cloudAutoApprove?: boolean
|
||||
}
|
||||
|
||||
const cloudAutoApproveTooltip = (editor: string) =>
|
||||
`Cloud sessions always auto-approve actions so they can continue running when ${editor} is closed. Your local auto-approve settings are unchanged.`
|
||||
|
||||
const AutoApproveBar = ({ style, cloudAutoApprove = false }: AutoApproveBarProps) => {
|
||||
const { autoApprovalSettings, hostName } = useExtensionState()
|
||||
const AutoApproveBar = ({ style }: AutoApproveBarProps) => {
|
||||
const { autoApprovalSettings } = useExtensionState()
|
||||
|
||||
const [isModalVisible, setIsModalVisible] = useState(false)
|
||||
const buttonRef = useRef<HTMLDivElement>(null)
|
||||
@@ -25,11 +19,9 @@ const AutoApproveBar = ({ style, cloudAutoApprove = false }: AutoApproveBarProps
|
||||
const baseClasses = isModalVisible
|
||||
? "text-foreground truncate"
|
||||
: "text-muted-foreground group-hover:text-foreground truncate"
|
||||
const enabledActionsNames = cloudAutoApprove
|
||||
? ACTION_METADATA.map((action) => action.id)
|
||||
: Object.keys(autoApprovalSettings.actions).filter(
|
||||
(key) => autoApprovalSettings.actions[key as keyof typeof autoApprovalSettings.actions],
|
||||
)
|
||||
const enabledActionsNames = Object.keys(autoApprovalSettings.actions).filter(
|
||||
(key) => autoApprovalSettings.actions[key as keyof typeof autoApprovalSettings.actions],
|
||||
)
|
||||
const enabledActions = enabledActionsNames.map((action) => {
|
||||
return ACTION_METADATA.flatMap((a) => [a, a.subAction]).find((a) => a?.id === action)
|
||||
})
|
||||
@@ -98,37 +90,31 @@ const AutoApproveBar = ({ style, cloudAutoApprove = false }: AutoApproveBarProps
|
||||
}}
|
||||
/>
|
||||
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<div
|
||||
aria-label={isModalVisible ? "Close auto-approve settings" : "Open auto-approve settings"}
|
||||
className="group cursor-pointer pt-3 pb-3.5 pr-2 px-3.5 flex items-center justify-between gap-0"
|
||||
onClick={() => {
|
||||
setIsModalVisible((prev) => !prev)
|
||||
}}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter" || e.key === " ") {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
setIsModalVisible((prev) => !prev)
|
||||
}
|
||||
}}
|
||||
ref={buttonRef}
|
||||
tabIndex={0}>
|
||||
<div className="flex flex-nowrap items-center gap-1 min-w-0 flex-1">
|
||||
<span className="whitespace-nowrap">Auto-approve:</span>
|
||||
{getEnabledActionsText()}
|
||||
</div>
|
||||
{isModalVisible ? <ChevronDownIcon size={16} /> : <ChevronRightIcon size={16} />}
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
{cloudAutoApprove && <TooltipContent side="top">{cloudAutoApproveTooltip(editorName(hostName))}</TooltipContent>}
|
||||
</Tooltip>
|
||||
<div
|
||||
aria-label={isModalVisible ? "Close auto-approve settings" : "Open auto-approve settings"}
|
||||
className="group cursor-pointer pt-3 pb-3.5 pr-2 px-3.5 flex items-center justify-between gap-0"
|
||||
onClick={() => {
|
||||
setIsModalVisible((prev) => !prev)
|
||||
}}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter" || e.key === " ") {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
setIsModalVisible((prev) => !prev)
|
||||
}
|
||||
}}
|
||||
ref={buttonRef}
|
||||
tabIndex={0}>
|
||||
<div className="flex flex-nowrap items-center gap-1 min-w-0 flex-1">
|
||||
<span className="whitespace-nowrap">Auto-approve:</span>
|
||||
{getEnabledActionsText()}
|
||||
</div>
|
||||
{isModalVisible ? <ChevronDownIcon size={16} /> : <ChevronRightIcon size={16} />}
|
||||
</div>
|
||||
|
||||
<AutoApproveModal
|
||||
ACTION_METADATA={ACTION_METADATA}
|
||||
buttonRef={buttonRef}
|
||||
cloudAutoApprove={cloudAutoApprove}
|
||||
isVisible={isModalVisible}
|
||||
setIsVisible={setIsModalVisible}
|
||||
/>
|
||||
|
||||
@@ -12,18 +12,10 @@ interface AutoApproveModalProps {
|
||||
setIsVisible: (visible: boolean) => void
|
||||
buttonRef: React.RefObject<HTMLDivElement>
|
||||
ACTION_METADATA: ActionMetadata[]
|
||||
cloudAutoApprove?: boolean
|
||||
}
|
||||
|
||||
const AutoApproveModal: React.FC<AutoApproveModalProps> = ({
|
||||
isVisible,
|
||||
setIsVisible,
|
||||
buttonRef,
|
||||
ACTION_METADATA,
|
||||
cloudAutoApprove = false,
|
||||
}) => {
|
||||
const AutoApproveModal: React.FC<AutoApproveModalProps> = ({ isVisible, setIsVisible, buttonRef, ACTION_METADATA }) => {
|
||||
const { isChecked, updateAction } = useAutoApproveActions()
|
||||
const displayedIsChecked = cloudAutoApprove ? () => true : isChecked
|
||||
const modalRef = useRef<HTMLDivElement>(null)
|
||||
const itemsContainerRef = useRef<HTMLDivElement>(null)
|
||||
const [containerWidth, setContainerWidth] = useState(0)
|
||||
@@ -76,9 +68,7 @@ const AutoApproveModal: React.FC<AutoApproveModalProps> = ({
|
||||
maxHeight: "60vh",
|
||||
}}>
|
||||
<div className="mb-2.5 text-muted-foreground text-xs cursor-pointer" onClick={() => setIsVisible(false)}>
|
||||
{cloudAutoApprove
|
||||
? "Cloud sessions always auto-approve these actions. Your local settings are unchanged. "
|
||||
: "Let Cline take these actions without asking for approval. "}
|
||||
Let Cline take these actions without asking for approval.{" "}
|
||||
<a
|
||||
className="text-link hover:text-link-hover"
|
||||
href="https://docs.cline.bot/features/auto-approve#auto-approve"
|
||||
@@ -109,13 +99,7 @@ const AutoApproveModal: React.FC<AutoApproveModalProps> = ({
|
||||
|
||||
{/* All items in a single list - CSS Grid will handle the column distribution */}
|
||||
{ACTION_METADATA.map((action) => (
|
||||
<AutoApproveMenuItem
|
||||
action={action}
|
||||
disabled={cloudAutoApprove}
|
||||
isChecked={displayedIsChecked}
|
||||
key={action.id}
|
||||
onToggle={updateAction}
|
||||
/>
|
||||
<AutoApproveMenuItem action={action} isChecked={isChecked} key={action.id} onToggle={updateAction} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -91,6 +91,31 @@ describe("useGitHubConnection", () => {
|
||||
await waitFor(() => expect(result.current.connection).toEqual(disconnected))
|
||||
})
|
||||
|
||||
it("re-checks in the background without showing loading until the user refreshes", async () => {
|
||||
const recheck = deferred<typeof connected>()
|
||||
mocks.getGitHubConnection.mockResolvedValueOnce(disconnected).mockReturnValueOnce(recheck.promise)
|
||||
const { result } = renderHook(() => useGitHubConnection(true))
|
||||
await waitFor(() => expect(result.current.connection).toEqual(disconnected))
|
||||
|
||||
act(() => {
|
||||
window.dispatchEvent(new Event("focus"))
|
||||
})
|
||||
expect(mocks.getGitHubConnection).toHaveBeenCalledTimes(2)
|
||||
expect(result.current.loading).toBe(false)
|
||||
|
||||
act(() => {
|
||||
void result.current.refresh()
|
||||
})
|
||||
expect(mocks.getGitHubConnection).toHaveBeenCalledTimes(2)
|
||||
expect(result.current.loading).toBe(true)
|
||||
|
||||
await act(async () => {
|
||||
recheck.resolve(connected)
|
||||
})
|
||||
expect(result.current.connection).toEqual(connected)
|
||||
expect(result.current.loading).toBe(false)
|
||||
})
|
||||
|
||||
it("hides repositories during a switch and refetches even if the switch is refused", async () => {
|
||||
mocks.getGitHubConnection.mockResolvedValueOnce(connected).mockResolvedValueOnce(disconnected)
|
||||
const { result, rerender } = renderHook(() => useGitHubConnection(true))
|
||||
|
||||
@@ -25,14 +25,16 @@ export function useGitHubConnection(enabled: boolean) {
|
||||
const inFlight = useRef<{ scope: object; promise: Promise<void> } | undefined>(undefined)
|
||||
|
||||
const load = useCallback(
|
||||
async (refresh: boolean) => {
|
||||
async ({ refresh = false, userInitiated = false }: { refresh?: boolean; userInitiated?: boolean } = {}) => {
|
||||
if (!available) {
|
||||
return
|
||||
}
|
||||
if (userInitiated) {
|
||||
setLoading(true)
|
||||
}
|
||||
if (inFlight.current?.scope === scope) {
|
||||
return inFlight.current.promise
|
||||
}
|
||||
setLoading(true)
|
||||
const request = {
|
||||
scope,
|
||||
promise: CloudServiceClient.getGitHubConnection(GitHubConnectionRequest.create({ refresh }))
|
||||
@@ -72,12 +74,12 @@ export function useGitHubConnection(enabled: boolean) {
|
||||
// not-yet-connected poll ask the control plane again.
|
||||
useEffect(() => {
|
||||
setLoading(false)
|
||||
void load(false)
|
||||
void load({ userInitiated: true })
|
||||
return () => {
|
||||
inFlight.current = undefined
|
||||
}
|
||||
}, [load])
|
||||
const refresh = useCallback(() => load(true), [load])
|
||||
const refresh = useCallback(() => load({ refresh: true, userInitiated: true }), [load])
|
||||
|
||||
const needsPolling =
|
||||
available &&
|
||||
@@ -89,14 +91,14 @@ export function useGitHubConnection(enabled: boolean) {
|
||||
if (!needsPolling) {
|
||||
return
|
||||
}
|
||||
const timer = setInterval(() => void refresh(), DISCONNECTED_POLL_MS)
|
||||
const onFocus = () => void refresh()
|
||||
window.addEventListener("focus", onFocus)
|
||||
const recheck = () => void load({ refresh: true })
|
||||
const timer = setInterval(recheck, DISCONNECTED_POLL_MS)
|
||||
window.addEventListener("focus", recheck)
|
||||
return () => {
|
||||
clearInterval(timer)
|
||||
window.removeEventListener("focus", onFocus)
|
||||
window.removeEventListener("focus", recheck)
|
||||
}
|
||||
}, [needsPolling, refresh])
|
||||
}, [needsPolling, load])
|
||||
|
||||
return { connection, loading, refresh }
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { getGeneratedModelsForProvider } from "@cline/llms";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import {
|
||||
clearLiveModelsCatalogCache,
|
||||
@@ -467,9 +468,14 @@ describe("resolveProviderConfig", () => {
|
||||
url: "https://models.test/api.json",
|
||||
});
|
||||
|
||||
const [generatedId, generatedModel] =
|
||||
Object.entries(getGeneratedModelsForProvider("cline-pass")).find(([id]) =>
|
||||
id.startsWith("cline-pass/"),
|
||||
) ?? [];
|
||||
expect(fetchMock).toHaveBeenCalledTimes(2);
|
||||
expect(resolved?.knownModels?.["cline-pass/mimo-v2.5-pro"]?.name).toBe(
|
||||
"MiMo-V2.5-Pro",
|
||||
expect(generatedId).toBeDefined();
|
||||
expect(resolved?.knownModels?.[generatedId ?? ""]?.name).toBe(
|
||||
generatedModel?.name,
|
||||
);
|
||||
expect(
|
||||
resolved?.knownModels?.["vendor/live-openrouter-model"],
|
||||
|
||||
@@ -1090,10 +1090,12 @@ describe("addLocalProvider – model ID parsing via modelsSourceUrl", () => {
|
||||
// models.dev, the recommended-models feed via the live catalog, and
|
||||
// the recommended-models feed again for the featured-tier overlay
|
||||
// (separately cached; both caches are cold here).
|
||||
const generatedId = Object.keys(
|
||||
LlmsModels.getGeneratedModelsForProvider("cline-pass"),
|
||||
).find((id) => id.startsWith("cline-pass/"));
|
||||
expect(fetchMock).toHaveBeenCalledTimes(3);
|
||||
expect(models.map((model) => model.id)).toContain(
|
||||
"cline-pass/mimo-v2.5-pro",
|
||||
);
|
||||
expect(generatedId).toBeDefined();
|
||||
expect(models.map((model) => model.id)).toContain(generatedId);
|
||||
expect(models.map((model) => model.id)).not.toContain(
|
||||
"vendor/live-openrouter-model",
|
||||
);
|
||||
|
||||
@@ -1035,7 +1035,7 @@ const OPENAI_COMPATIBLE_SPEC_OVERRIDES: BuiltinSpecOverride[] = [
|
||||
description: "Z.AI's coding-focused models",
|
||||
family: "openai-compatible",
|
||||
capabilities: ["reasoning", "tools"],
|
||||
defaultModelId: "glm-5.2",
|
||||
defaultModelId: "glm-5.3",
|
||||
apiKeyEnv: ["ZHIPU_API_KEY"],
|
||||
modelsProviderId: "zai-coding-plan",
|
||||
defaults: { baseUrl: "https://api.z.ai/api/coding/paas/v4" },
|
||||
|
||||
@@ -228,12 +228,12 @@ describe("provider-ids", () => {
|
||||
await expect(getProvider("zai-coding-plan")).resolves.toMatchObject({
|
||||
id: "zai-coding-plan",
|
||||
baseUrl: "https://api.z.ai/api/coding/paas/v4",
|
||||
defaultModelId: "glm-5.2",
|
||||
defaultModelId: "glm-5.3",
|
||||
client: "openai-compatible",
|
||||
});
|
||||
|
||||
const models = await getModelsForProvider("zai-coding-plan");
|
||||
expect(Object.hasOwn(models, "glm-5.2")).toBe(true);
|
||||
expect(Object.hasOwn(models, "glm-5.3")).toBe(true);
|
||||
});
|
||||
|
||||
it("routes SAP AI Core through the SAP AI SDK provider factory", async () => {
|
||||
|
||||
+2
-2
@@ -264,10 +264,10 @@ describe("resolveBedrockModelId", () => {
|
||||
}),
|
||||
).toBe("us.anthropic.claude-sonnet-4-6");
|
||||
expect(
|
||||
resolveBedrockModelId("anthropic.claude-sonnet-4-5-20250929-v1:0", {
|
||||
resolveBedrockModelId("anthropic.claude-haiku-4-5-20251001-v1:0", {
|
||||
region: "eu-central-1",
|
||||
}),
|
||||
).toBe("eu.anthropic.claude-sonnet-4-5-20250929-v1:0");
|
||||
).toBe("eu.anthropic.claude-haiku-4-5-20251001-v1:0");
|
||||
expect(
|
||||
resolveBedrockModelId("anthropic.claude-sonnet-5", {
|
||||
region: "us-east-1",
|
||||
|
||||
Reference in New Issue
Block a user