mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-10-19 07:34:58 +00:00
feat(local): set up a Claude plan from Settings and onboarding
A "Use your Claude plan" row beside the ChatGPT one opens a dialog that reads Claude Code's state live and says what to do: install it, run claude and sign in, or update it, with Check again; once ready, Add Claude models asks for both of its hosts in one egress question and makes the connection. The egress prompt now asks about every host a refusal or askEgress names at once, wording a Claude plan's two by what each gets, which also makes ChatGPT's sign-in one question. Editing a Claude plan only renames it, chat names its failures in Claude's terms, and Electron opens Claude Code's install guide and no other page of that site. Every new string is in all ten languages. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
9640a2a0e0
commit
aba52eaf79
35 changed files with 1506 additions and 53 deletions
|
|
@ -71,9 +71,10 @@ SurfSense, Studio, Markdown, llama.cpp, GGUF, Hugging Face, API, MCP, and every
|
|||
| session log | セッションログ | 세션 로그 | 会话日志 | सेशन लॉग | Sitzungsprotokoll | registro de la sesión | journal de session | log da sessão | журнал сеанса |
|
||||
| clipboard | クリップボード | 클립보드 | 剪贴板 | क्लिपबोर्ड | Zwischenablage | portapapeles | presse-papiers | área de transferência | буфер обмена |
|
||||
| embedding (the model that turns the library into vectors) | 埋め込み | 임베딩 | 嵌入 | एम्बेडिंग | Embedding | embedding | embedding | embedding | эмбеддинг |
|
||||
| sign in (to a ChatGPT account) | サインイン | 로그인 | 登录 | साइन इन | anmelden | iniciar sesión | se connecter | entrar | войти |
|
||||
| plan (a ChatGPT plan, Plus or Pro) | プラン | 플랜 | 套餐 | प्लान | Plan | plan | forfait | plano | план |
|
||||
| sign in (to a ChatGPT account, or Claude Code to a Claude plan) | サインイン | 로그인 | 登录 | साइन इन | anmelden | iniciar sesión | se connecter | entrar | войти |
|
||||
| plan (a ChatGPT plan, Plus or Pro; a Claude plan, Pro, Max or Team) | プラン | 플랜 | 套餐 | प्लान | Plan | plan | forfait | plano | план |
|
||||
| subscription (using a ChatGPT plan instead of a key) | サブスクリプション | 구독 | 订阅 | सब्सक्रिप्शन | Abo | suscripción | abonnement | assinatura | подписка |
|
||||
| terminal (where the user runs `claude`, the command that stays in English) | ターミナル | 터미널 | 终端 | टर्मिनल | Terminal | terminal (una) | terminal | terminal | терминал |
|
||||
|
||||
## What the English means
|
||||
|
||||
|
|
|
|||
|
|
@ -62,7 +62,7 @@ The GGUF download runs inside the API (`modules/llm/providers/llamacpp/download.
|
|||
|
||||
## Asking for consent
|
||||
|
||||
`request()` in `lib/api.ts` catches `egress_disabled` on a non-GET request and hands it to `EgressPrompt`, which shows that destination's copy. Allow enables the destination and the request is retried once; Cancel rethrows the refusal. The method stands in for "the user just did something", a `ponytail:` simplification, because reads also run unattended at boot and must never raise a dialog.
|
||||
`request()` in `lib/api.ts` catches `egress_disabled` on a non-GET request and hands it to `EgressPrompt`, which shows that destination's copy. A refusal naming several hosts in `hosts` is one question: the copy names the others, and Allow enables every one of them before the request is retried once; Cancel rethrows the refusal. A Claude plan's refusal, `purpose: "claude_plan"`, says what each of its two hosts gets ([`features/egress/api.ts`](../../surfsense_local/frontend/src/features/egress/api.ts)). The method stands in for "the user just did something", a `ponytail:` simplification, because reads also run unattended at boot and must never raise a dialog.
|
||||
|
||||
A refused GET therefore surfaces as an error without a prompt.
|
||||
|
||||
|
|
@ -79,6 +79,5 @@ Settings › Network lists the App updates row and every destination with its ho
|
|||
|
||||
## Known gaps
|
||||
|
||||
- `EgressPrompt` reads only `destination`, so a plugin with several hosts, such as a registry entry whose authorization server is on another host, is asked about one host at a time, though the refusal names them all in `hosts` ([plugins](plugins.md)).
|
||||
- Document scripts, Studio's Office formats and the agent's, run model-written code in the script runner's process, and that code can open connections of its own ([studio](studio.md#script-documents)).
|
||||
- The Office pack's download is allowed by its host, `downloadarchive.documentfoundation.org`, which redirects to one of TDF's mirrors; the mirror sees the user's IP address and is not asked about ([office pack](office-pack.md)).
|
||||
|
|
|
|||
|
|
@ -10,31 +10,35 @@ Part of [Use your Claude plan](README.md).
|
|||
|
||||
| File | Does |
|
||||
|---|---|
|
||||
| `claude-option.tsx` | the row: title, one-line body, and once connected "Connected as … · Max plan" with **Show models** |
|
||||
| `claude-dialog.tsx` | the nested dialog, one view per state |
|
||||
| `use-claude-status.ts` | reads `GET /llm/connections/claude`; **Check again** reads it again |
|
||||
| `api.ts` | the two routes ([`01-connection.md`](01-connection.md#routes)) |
|
||||
| `claude-option.tsx` | the row: the Claude icon, title and one-line body; **Set up** opens the dialog, or once connected "Connected through Claude Code on this computer" with **Show models** |
|
||||
| `claude-dialog.tsx` | the nested dialog, one view per state, read live each time it opens |
|
||||
| `claude-plan-edit.tsx` | the connection form for a Claude plan: rename only, no URL, key or sign-in |
|
||||
| `api.ts` | `GET /llm/connections/claude`, and connecting after one egress question for every host still off |
|
||||
|
||||
The row is mounted where the ChatGPT row is: [`add-model-options.tsx`](../../../surfsense_local/frontend/src/features/models/add-model/add-model-options.tsx) and [`model-step.tsx`](../../../surfsense_local/frontend/src/features/onboarding/model-step/model-step.tsx). The dialog uses the app's dialog component, nested in the open one, and the shared button; every string is a message, translated like the ChatGPT row's.
|
||||
The row is mounted where the ChatGPT row is: [`add-model-options.tsx`](../../../surfsense_local/frontend/src/features/models/add-model/add-model-options.tsx) and [`model-step.tsx`](../../../surfsense_local/frontend/src/features/onboarding/model-step/model-step.tsx). The dialog uses the app's dialog component, nested in the open one, and the shared button; every string is a message, translated into every language in `LOCALES`.
|
||||
|
||||
## The views
|
||||
|
||||
The dialog opens on the status read when it opens. Every view has the same opening lines: SurfSense uses the user's Claude Pro, Max or Team plan through Claude Code on this computer; the sign-in stays in Claude Code and SurfSense never sees it; usage counts toward the plan, the same limits as Claude Code.
|
||||
Every view opens with the same lines: SurfSense uses the user's Claude Pro, Max or Team plan through Claude Code on this computer; the sign-in stays in Claude Code and SurfSense never sees it; use counts toward the plan's limits, as it does in Claude Code.
|
||||
|
||||
| State | The dialog shows | Actions |
|
||||
|---|---|---|
|
||||
| `not_installed` | Step 1, install Claude Code; step 2, open a terminal, run `claude` and sign in | **Open install guide**, **Check again**, **Cancel** |
|
||||
| `signed_out` | Claude Code is installed but not signed in; run `claude` in a terminal and sign in | **Check again**, **Cancel** |
|
||||
| `too_old` | the version found and the lowest accepted | **Open update guide**, **Check again**, **Cancel** |
|
||||
| `ready` | "Connected as <email> · <plan>", where Claude Code was found and its version, and that Claude models work in chat and in the agent | **Add Claude models**, **Cancel** |
|
||||
| `not_installed` | step 1, install Claude Code; step 2, open a terminal, run `claude` and sign in | **Open install guide**, **Check again**, **Cancel** |
|
||||
| `signed_out` | Claude Code is installed but not signed in to a plan; run `claude` in a terminal and sign in | **Check again**, **Cancel** |
|
||||
| `too_old` | the version found, to update; or, when no version could be read, that SurfSense could not tell | **Check again**, **Cancel** |
|
||||
| `ready` | "Connected as <email>, <plan>", and that Claude models then work in chat, titles and Studio | **Add Claude models**, **Cancel** |
|
||||
|
||||
**Check again** reads the status and moves to the view it returns. Nothing polls: nothing changes on SurfSense's side until the user acts in their terminal.
|
||||
**Check again** reads the status again and moves to the view it returns. Nothing polls: nothing changes on SurfSense's side until the user acts in their terminal.
|
||||
|
||||
**Add Claude models** first asks about every host still off, `api.anthropic.com` for chats and `platform.claude.com` for the sign-in's refresh ([`01-connection.md`](01-connection.md#network)), in one egress prompt nested over the dialog ([egress](../../architecture/egress.md#asking-for-consent)), then posts the connection. Allow switches each host on as its own row; Cancel creates nothing. On success the dialog closes and the models appear in every picker.
|
||||
**Open install guide** opens Anthropic's guide, `https://code.claude.com/docs/en/setup`, the one page on that site Electron's link allowlist lets through ([`external-url.ts`](../../../surfsense_local/electron/src/main/external-url.ts)).
|
||||
|
||||
The prompt asks about several hosts at once, which `EgressPrompt` cannot yet: it reads only `destination`, and asks about one host at a time though the refusal names them all ([egress](../../architecture/egress.md#known-gaps)). It is taught to read `hosts` and to word each by `purpose: "claude_plan"`: the API host gets what the model is sent, chats and sources; the sign-in host gets the sign-in's refresh, never a chat. That closes the gap for ChatGPT's sign-in and plugins with several hosts too. In Settings › Network the sign-in host's row says that turning it off stops Claude once the sign-in expires.
|
||||
**Add Claude models** first asks about every host still off, `api.anthropic.com` for chats and `platform.claude.com` for the sign-in's refresh ([`01-connection.md`](01-connection.md#network)), in one egress prompt nested over the dialog ([egress](../../architecture/egress.md#asking-for-consent)), then posts the connection. Allow switches each host on as its own row; Cancel creates nothing and the dialog says which host is needed. On success the dialog closes and the models appear in every picker.
|
||||
|
||||
**Opened again** once connected, the dialog shows the `ready` view with **Remove** in place of **Add Claude models**, or the state that now applies, such as `signed_out` after the user signed out in Claude Code.
|
||||
The egress prompt asks about several hosts in one question: a refusal's `hosts` and an `askEgress` call's `also` are named in it, and Allow switches each on. A Claude plan's question, `purpose: "claude_plan"`, says what each host gets: the API host what the model is sent, chats and sources; the sign-in host only the sign-in's refresh, and that turning it off stops Claude once the sign-in expires. Other questions naming several hosts, a ChatGPT sign-in's two and a plugin's, add "Allowing also lets SurfSense reach …". In Settings › Network the sign-in host's row is labelled "Claude Code sign-in".
|
||||
|
||||
**Editing** a Claude plan from the connection list opens `claude-plan-edit.tsx`: the name is editable, the provider is shown fixed as "Claude plan", and it says that signing in, out or switching accounts happens with `claude` in a terminal. Removing it is the connection list's own delete, as for any connection.
|
||||
|
||||
**When chat fails** on a Claude plan, it says so in Claude's terms, not ChatGPT's: `claude_sign_in` asks to run `claude` and sign in, `claude_limit` says the Claude plan's limit is reached, and the model check's `claude_sign_in_required` says Claude Code is missing or signed out.
|
||||
|
||||
## Names
|
||||
|
||||
|
|
|
|||
|
|
@ -138,3 +138,14 @@ test("a link the user confirmed opens on any https host, and nothing else", () =
|
|||
assert.equal(confirmedExternalUrl("https://me:pw@app.notion.com/"), null) // pragma: allowlist secret
|
||||
assert.equal(confirmedExternalUrl("not a url"), null)
|
||||
})
|
||||
|
||||
test("opens Claude Code's install guide and nothing else on its docs site", () => {
|
||||
assert.equal(
|
||||
allowedExternalUrl("https://code.claude.com/docs/en/setup"),
|
||||
"https://code.claude.com/docs/en/setup"
|
||||
)
|
||||
assert.equal(allowedExternalUrl("https://code.claude.com/docs/en/overview"), null)
|
||||
assert.equal(allowedExternalUrl("https://code.claude.com/docs/en/setup?x=1"), null)
|
||||
assert.equal(allowedExternalUrl("http://code.claude.com/docs/en/setup"), null)
|
||||
assert.equal(allowedExternalUrl("https://claude.ai/"), null)
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
// The only links the app hands to the OS browser: the SurfSense site, the
|
||||
// project's own GitHub repository, its Discord invite, an OpenRouter model's
|
||||
// page, where OpenRouter names a speech model's voices, and OpenAI's sign-in
|
||||
// page for a ChatGPT subscription, and a plugin provider's sign-in page that
|
||||
// returns to SurfSense. Anything else is dropped so a crafted link in content
|
||||
// page for a ChatGPT subscription, Claude Code's install guide for a Claude
|
||||
// plan, and a plugin provider's sign-in page that returns to SurfSense. Anything else is dropped so a crafted link in content
|
||||
// cannot open arbitrary pages.
|
||||
const SITE_HOSTS = new Set(["surfsense.com", "www.surfsense.com"])
|
||||
const REPO_PATH = "/MODSetter/SurfSense"
|
||||
|
|
@ -11,6 +11,8 @@ const DISCORD_INVITE = "/ejRNvftDp9"
|
|||
// opens OpenRouter's own site; no other host or deeper path gets through.
|
||||
const OPENROUTER_MODEL = /^\/[\w.-]+\/[\w.:-]+$/
|
||||
const OPENAI_AUTHORIZE = "/api/accounts/authorize"
|
||||
// A Claude plan runs the user's own Claude Code, which SurfSense never installs.
|
||||
const CLAUDE_CODE_SETUP = "https://code.claude.com/docs/en/setup"
|
||||
|
||||
// Only a sign-in that returns its code to the API's own loopback listener;
|
||||
// one naming any other redirect would hand the code to someone else.
|
||||
|
|
@ -74,6 +76,7 @@ export function allowedExternalUrl(url: string): string | null {
|
|||
) {
|
||||
return parsed.href
|
||||
}
|
||||
if (parsed.href === CLAUDE_CODE_SETUP) return parsed.href
|
||||
if (isLoopbackSignIn(parsed)) return parsed.href
|
||||
if (isPluginSignIn(parsed)) return parsed.href
|
||||
return null
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ import {
|
|||
ChartHistogramIcon as ChartHistogramIconData,
|
||||
Chat01Icon as Chat01IconData,
|
||||
ChatGptIcon as ChatGptIconData,
|
||||
ClaudeIcon as ClaudeIconData,
|
||||
CheckIcon as CheckIconData,
|
||||
CheckmarkBadge02Icon as CheckmarkBadge02IconData,
|
||||
CheckmarkCircle02Icon,
|
||||
|
|
@ -144,6 +145,7 @@ export const Cards01Icon = createIcon(Cards01IconData)
|
|||
export const ChartHistogramIcon = createIcon(ChartHistogramIconData)
|
||||
export const Chat01Icon = createIcon(Chat01IconData)
|
||||
export const ChatGptIcon = createIcon(ChatGptIconData)
|
||||
export const ClaudeIcon = createIcon(ClaudeIconData)
|
||||
export const CheckCircle2Icon = createIcon(CheckmarkCircle02Icon)
|
||||
export const CheckIcon = createIcon(CheckIconData)
|
||||
export const CheckmarkBadgeIcon = createIcon(CheckmarkBadge02IconData)
|
||||
|
|
|
|||
|
|
@ -50,6 +50,18 @@ const chatErrorText: Record<ChatTurnErrorKind, () => string> = {
|
|||
defaultMessage:
|
||||
"Your ChatGPT plan’s usage limit is reached. It resets on its own; check your usage in ChatGPT’s settings.",
|
||||
}),
|
||||
claude_sign_in: () =>
|
||||
intl.formatMessage({
|
||||
id: "chat_error_claude_sign_in",
|
||||
defaultMessage:
|
||||
"Claude Code on this computer needs to sign in again: run claude in a terminal and sign in.",
|
||||
}),
|
||||
claude_limit: () =>
|
||||
intl.formatMessage({
|
||||
id: "chat_error_claude_limit",
|
||||
defaultMessage:
|
||||
"Your Claude plan’s usage limit is reached. It resets on its own.",
|
||||
}),
|
||||
network: () =>
|
||||
intl.formatMessage({
|
||||
id: "chat_error_network",
|
||||
|
|
|
|||
|
|
@ -23,6 +23,15 @@ function reason({ model, code, host }: ModelIssue): string {
|
|||
},
|
||||
{ model }
|
||||
)
|
||||
case "claude_sign_in_required":
|
||||
return intl.formatMessage(
|
||||
{
|
||||
id: "chat_model_issue_claude_sign_in_body",
|
||||
defaultMessage:
|
||||
"Couldn’t use {model}: Claude Code on this computer isn’t installed or has to sign in again.",
|
||||
},
|
||||
{ model }
|
||||
)
|
||||
case "sign_in_required":
|
||||
return intl.formatMessage(
|
||||
{
|
||||
|
|
|
|||
|
|
@ -65,6 +65,8 @@ export type ChatErrorKind =
|
|||
| "context_too_long"
|
||||
| "subscription_sign_in"
|
||||
| "subscription_limit"
|
||||
| "claude_sign_in"
|
||||
| "claude_limit"
|
||||
| "runtime_busy"
|
||||
| "network"
|
||||
| "timeout"
|
||||
|
|
|
|||
|
|
@ -8,6 +8,12 @@ export type Destination = {
|
|||
last_call_at: string | null
|
||||
}
|
||||
|
||||
/** Switch on a question's destination and every other host it named. */
|
||||
export async function allowAll(destination: string, also: string[] = []) {
|
||||
await setDestinationEnabled(destination, true)
|
||||
for (const host of also) await setDestinationEnabled(`host:${host}`, true)
|
||||
}
|
||||
|
||||
/** Search, model weights and image weights all reach this one host. */
|
||||
export const HUGGINGFACE = "host:huggingface.co"
|
||||
|
||||
|
|
@ -28,11 +34,95 @@ export function setDestinationEnabled(
|
|||
})
|
||||
}
|
||||
|
||||
// A Claude plan's two hosts: model calls, and where Claude Code refreshes its sign-in.
|
||||
const CLAUDE_API_HOST = "api.anthropic.com"
|
||||
const CLAUDE_SIGN_IN_HOST = "platform.claude.com"
|
||||
|
||||
export type DestinationCopy = {
|
||||
label: string
|
||||
title: string
|
||||
body: string
|
||||
/** Whole sentences shown after the body, one paragraph each. */
|
||||
details: string[]
|
||||
}
|
||||
|
||||
/**
|
||||
* How a destination is named in Settings › Network, and the question asked
|
||||
* before allowing it. `also` names the other hosts asked about in the same
|
||||
* question, which one Allow switches on together.
|
||||
*/
|
||||
export function describeDestination(
|
||||
destination: string,
|
||||
host: string,
|
||||
purpose?: string
|
||||
) {
|
||||
purpose?: string,
|
||||
also: string[] = []
|
||||
): DestinationCopy {
|
||||
if (purpose === "claude_plan" || host === CLAUDE_SIGN_IN_HOST)
|
||||
return describeClaudePlan([host, ...also])
|
||||
const one = describeOne(destination, host, purpose)
|
||||
return {
|
||||
...one,
|
||||
details: also.length
|
||||
? [
|
||||
intl.formatMessage(
|
||||
{
|
||||
id: "egress_prompt_also_allows_body",
|
||||
defaultMessage: "Allowing also lets SurfSense reach {hosts}.",
|
||||
},
|
||||
{ hosts: intl.formatList(also, { type: "conjunction" }) }
|
||||
),
|
||||
]
|
||||
: [],
|
||||
}
|
||||
}
|
||||
|
||||
// Each host's own sentence, since one gets chats and the other only a sign-in.
|
||||
function describeClaudePlan(hosts: string[]): DestinationCopy {
|
||||
const details = []
|
||||
if (hosts.includes(CLAUDE_API_HOST))
|
||||
details.push(
|
||||
intl.formatMessage(
|
||||
{
|
||||
id: "egress_claude_api_host_body",
|
||||
defaultMessage:
|
||||
"{host} gets what you send the model: your prompts and excerpts of your documents.",
|
||||
},
|
||||
{ host: CLAUDE_API_HOST }
|
||||
)
|
||||
)
|
||||
if (hosts.includes(CLAUDE_SIGN_IN_HOST))
|
||||
details.push(
|
||||
intl.formatMessage(
|
||||
{
|
||||
id: "egress_claude_sign_in_host_body",
|
||||
defaultMessage:
|
||||
"{host} keeps Claude Code signed in. It gets no chats, and turning it off stops Claude once the sign-in expires.",
|
||||
},
|
||||
{ host: CLAUDE_SIGN_IN_HOST }
|
||||
)
|
||||
)
|
||||
return {
|
||||
label:
|
||||
hosts[0] === CLAUDE_SIGN_IN_HOST
|
||||
? intl.formatMessage({
|
||||
id: "egress_claude_sign_in_label",
|
||||
defaultMessage: "Claude Code sign-in",
|
||||
})
|
||||
: hosts[0],
|
||||
title: intl.formatMessage({
|
||||
id: "egress_claude_prompt_title",
|
||||
defaultMessage: "Allow your Claude plan to reach Anthropic?",
|
||||
}),
|
||||
body: intl.formatMessage({
|
||||
id: "egress_claude_prompt_body",
|
||||
defaultMessage:
|
||||
"Claude Code on this computer reaches Anthropic only through SurfSense, and only the hosts you allow. You can turn each one off in Settings › Network.",
|
||||
}),
|
||||
details,
|
||||
}
|
||||
}
|
||||
|
||||
function describeOne(destination: string, host: string, purpose?: string) {
|
||||
if (destination === "app_updates") {
|
||||
return {
|
||||
label: intl.formatMessage({
|
||||
|
|
|
|||
|
|
@ -11,6 +11,8 @@ export type Pending = {
|
|||
host: string
|
||||
// What the call is for, when the host alone does not say: "plugin".
|
||||
purpose?: string
|
||||
// The other hosts asked about in the same question; Allow switches them on too.
|
||||
also?: string[]
|
||||
// What Allow does. A refused request enables the destination and is retried;
|
||||
// the updater talks to GitHub from Electron, so its consent is a pref there.
|
||||
allow: () => Promise<unknown>
|
||||
|
|
|
|||
|
|
@ -45,6 +45,97 @@ afterEach(() => {
|
|||
})
|
||||
|
||||
describe("egress prompt", () => {
|
||||
it("asks once about every host a refusal names, and Allow switches on each", async () => {
|
||||
const calls: string[] = []
|
||||
let allowed = false
|
||||
vi.stubGlobal(
|
||||
"fetch",
|
||||
vi.fn(async (input: RequestInfo | URL, init?: RequestInit) => {
|
||||
const path = String(input)
|
||||
calls.push(`${init?.method ?? "GET"} ${path}`)
|
||||
if (path.startsWith("/egress/")) {
|
||||
allowed = calls.filter((call) => call.startsWith("PUT")).length === 2
|
||||
return Response.json({})
|
||||
}
|
||||
return allowed
|
||||
? Response.json({})
|
||||
: Response.json(
|
||||
{
|
||||
detail: {
|
||||
code: "egress_disabled",
|
||||
message: "off",
|
||||
destination: "host:api.anthropic.com",
|
||||
host: "api.anthropic.com",
|
||||
hosts: ["api.anthropic.com", "platform.claude.com"],
|
||||
purpose: "claude_plan",
|
||||
},
|
||||
},
|
||||
{ status: 403 }
|
||||
)
|
||||
})
|
||||
)
|
||||
const user = userEvent.setup()
|
||||
render(<EgressPrompt />)
|
||||
|
||||
const pending = requestVoid("/llm/connections/claude", { method: "POST" })
|
||||
const prompt = await screen.findByRole("alertdialog", {
|
||||
name: "Allow your Claude plan to reach Anthropic?",
|
||||
})
|
||||
// Each host says what it gets: one the chats, the other only the sign-in.
|
||||
expect(prompt.textContent).toContain(
|
||||
"api.anthropic.com gets what you send the model"
|
||||
)
|
||||
expect(prompt.textContent).toContain(
|
||||
"platform.claude.com keeps Claude Code signed in. It gets no chats"
|
||||
)
|
||||
await user.click(screen.getByRole("button", { name: "Allow" }))
|
||||
|
||||
await expect(pending).resolves.toBeUndefined()
|
||||
expect(calls).toEqual([
|
||||
"POST /llm/connections/claude",
|
||||
"PUT /egress/host:api.anthropic.com",
|
||||
"PUT /egress/host:platform.claude.com",
|
||||
"POST /llm/connections/claude",
|
||||
])
|
||||
})
|
||||
|
||||
it("names the other hosts a plugin's refusal lists", async () => {
|
||||
const user = userEvent.setup()
|
||||
vi.stubGlobal(
|
||||
"fetch",
|
||||
vi.fn(async () =>
|
||||
Response.json(
|
||||
{
|
||||
detail: {
|
||||
code: "egress_disabled",
|
||||
message: "off",
|
||||
destination: "host:mcp.notes.example",
|
||||
host: "mcp.notes.example",
|
||||
hosts: ["mcp.notes.example", "auth.notes.example"],
|
||||
purpose: "plugin",
|
||||
},
|
||||
},
|
||||
{ status: 403 }
|
||||
)
|
||||
)
|
||||
)
|
||||
render(<EgressPrompt />)
|
||||
|
||||
// Handled from the start: Cancel rejects it before the test awaits it.
|
||||
const refused = requestVoid("/plugins/custom", { method: "POST" }).catch(
|
||||
(error: unknown) => error
|
||||
)
|
||||
const prompt = await screen.findByRole("alertdialog", {
|
||||
name: "Allow connecting to mcp.notes.example?",
|
||||
})
|
||||
|
||||
expect(prompt.textContent).toContain(
|
||||
"Allowing also lets SurfSense reach auth.notes.example."
|
||||
)
|
||||
await user.click(screen.getByRole("button", { name: "Cancel" }))
|
||||
expect(await refused).toBeInstanceOf(Error)
|
||||
})
|
||||
|
||||
it("asks about the host and, once allowed, lets the call through", async () => {
|
||||
const calls = stubApi()
|
||||
const user = userEvent.setup()
|
||||
|
|
|
|||
|
|
@ -15,11 +15,7 @@ import { Spinner } from "@/components/ui/spinner"
|
|||
import { intl } from "@/i18n/intl"
|
||||
import { type ApiError, setEgressPrompt } from "@/lib/api"
|
||||
|
||||
import {
|
||||
describeDestination,
|
||||
destinationsQueryKey,
|
||||
setDestinationEnabled,
|
||||
} from "./api"
|
||||
import { allowAll, describeDestination, destinationsQueryKey } from "./api"
|
||||
import {
|
||||
advanceEgressQueue,
|
||||
askEgress,
|
||||
|
|
@ -29,13 +25,21 @@ import {
|
|||
} from "./ask-egress"
|
||||
|
||||
function requestFrom(error: ApiError): Omit<Pending, "resolve"> {
|
||||
const { destination, host, purpose } = error.detail
|
||||
const { destination, host, hosts, purpose } = error.detail
|
||||
const key = typeof destination === "string" ? destination : ""
|
||||
const first = typeof host === "string" ? host : ""
|
||||
// Every host still to allow, the first named apart: one question for them all.
|
||||
const also = Array.isArray(hosts)
|
||||
? hosts.filter(
|
||||
(each): each is string => typeof each === "string" && each !== first
|
||||
)
|
||||
: []
|
||||
return {
|
||||
destination: key,
|
||||
host: typeof host === "string" ? host : "",
|
||||
host: first,
|
||||
purpose: typeof purpose === "string" ? purpose : undefined,
|
||||
allow: () => setDestinationEnabled(key, true),
|
||||
also,
|
||||
allow: () => allowAll(key, also),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -65,7 +69,12 @@ export function EgressPrompt() {
|
|||
}
|
||||
|
||||
const copy = question
|
||||
? describeDestination(question.destination, question.host, question.purpose)
|
||||
? describeDestination(
|
||||
question.destination,
|
||||
question.host,
|
||||
question.purpose,
|
||||
question.also
|
||||
)
|
||||
: null
|
||||
return (
|
||||
<AlertDialog
|
||||
|
|
@ -77,6 +86,11 @@ export function EgressPrompt() {
|
|||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>{copy?.title}</AlertDialogTitle>
|
||||
<AlertDialogDescription>{copy?.body}</AlertDialogDescription>
|
||||
{copy?.details.map((detail) => (
|
||||
<p key={detail} className="text-sm text-muted-foreground">
|
||||
{detail}
|
||||
</p>
|
||||
))}
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel disabled={allowing}>
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import { intl } from "@/i18n/intl"
|
|||
import type { ModelType } from "../model-type"
|
||||
import type { Connection } from "../remote/connections/api"
|
||||
import { ChatGPTOption } from "../remote/connections/chatgpt/chatgpt-option"
|
||||
import { ClaudeOption } from "../remote/connections/claude/claude-option"
|
||||
import { ServerCard } from "./server-card"
|
||||
|
||||
/**
|
||||
|
|
@ -32,6 +33,7 @@ export function AddModelOptions({
|
|||
<>
|
||||
<ServerCard onConnected={onConnected} />
|
||||
<ChatGPTOption modelType={modelType} onConnected={onConnected} />
|
||||
<ClaudeOption modelType={modelType} onConnected={onConnected} />
|
||||
<Separator />
|
||||
</>
|
||||
) : null}
|
||||
|
|
|
|||
|
|
@ -9,8 +9,11 @@ export type Connection = {
|
|||
base_url: string
|
||||
catalog_provider: string
|
||||
has_api_key: boolean
|
||||
/** `chatgpt` signs in with a ChatGPT account instead of a key. */
|
||||
auth_kind: "api_key" | "chatgpt"
|
||||
/**
|
||||
* `chatgpt` signs in with a ChatGPT account instead of a key; `claude_plan`
|
||||
* runs the user's own Claude Code, which keeps the sign-in.
|
||||
*/
|
||||
auth_kind: "api_key" | "chatgpt" | "claude_plan"
|
||||
signed_in: boolean
|
||||
account_email: string | null
|
||||
/** The slots it can fill, decided by the backend; pickers only read it. */
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { setDestinationEnabled } from "@/features/egress/api"
|
||||
import { askEgress } from "@/features/egress/ask-egress"
|
||||
import { allowAll } from "@/features/egress/api"
|
||||
import { requestJson, requestVoid } from "@/lib/api"
|
||||
|
||||
import type { ModelType } from "../../../model-type"
|
||||
|
|
@ -42,20 +42,22 @@ export class HostDeclinedError extends Error {
|
|||
|
||||
/**
|
||||
* Starts a sign-in once both hosts it reaches, OpenAI's sign-in and its API,
|
||||
* are allowed. Asked one by one up front: a refused request asks about one host
|
||||
* only, and could not tell a host the user declined from the next one.
|
||||
* are allowed: asked up front, in one question for every host still off.
|
||||
*/
|
||||
export async function startSignIn(
|
||||
target: SignInTarget
|
||||
): Promise<SignInStarted> {
|
||||
const { hosts } = await getSignInOption()
|
||||
for (const { destination, host } of hosts) {
|
||||
const [first, ...rest] = hosts
|
||||
if (first) {
|
||||
const also = rest.map(({ host }) => host)
|
||||
const allowed = await askEgress({
|
||||
destination,
|
||||
host,
|
||||
allow: () => setDestinationEnabled(destination, true),
|
||||
destination: first.destination,
|
||||
host: first.host,
|
||||
also,
|
||||
allow: () => allowAll(first.destination, also),
|
||||
})
|
||||
if (!allowed) throw new HostDeclinedError(host)
|
||||
if (!allowed) throw new HostDeclinedError(first.host)
|
||||
}
|
||||
return requestJson<SignInStarted>("/llm/connections/chatgpt/sign-in", {
|
||||
method: "POST",
|
||||
|
|
|
|||
|
|
@ -0,0 +1,67 @@
|
|||
import { askEgress } from "@/features/egress/ask-egress"
|
||||
import { allowAll } from "@/features/egress/api"
|
||||
import { requestJson } from "@/lib/api"
|
||||
|
||||
import type { ModelType } from "../../../model-type"
|
||||
import type { Connection } from "../api"
|
||||
|
||||
/** What SurfSense found of Claude Code on this computer, read live. */
|
||||
export type ClaudeState = "not_installed" | "too_old" | "signed_out" | "ready"
|
||||
|
||||
type RefusedHost = { destination: string; host: string }
|
||||
|
||||
export type ClaudeOption = {
|
||||
state: ClaudeState
|
||||
version: string | null
|
||||
email: string | null
|
||||
/** As Claude Code reports it: `pro`, `max`, `team`. */
|
||||
plan: string | null
|
||||
serves: ModelType[]
|
||||
/** The hosts a Claude plan reaches that are still off, model calls first. */
|
||||
hosts: RefusedHost[]
|
||||
}
|
||||
|
||||
/** Anthropic's own install guide, the one outside page the dialog opens. */
|
||||
export const CLAUDE_INSTALL_URL = "https://code.claude.com/docs/en/setup"
|
||||
|
||||
export const claudeOptionQueryKey = ["claude-plan-option"] as const
|
||||
|
||||
export function getClaudeOption(signal?: AbortSignal): Promise<ClaudeOption> {
|
||||
return requestJson<ClaudeOption>("/llm/connections/claude", { signal })
|
||||
}
|
||||
|
||||
/** Nothing was made: the user kept a host the plan needs switched off. */
|
||||
export class ClaudeHostDeclinedError extends Error {
|
||||
readonly host: string
|
||||
|
||||
constructor(host: string) {
|
||||
super(host)
|
||||
this.name = "ClaudeHostDeclinedError"
|
||||
this.host = host
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The Claude plan connection, once every host it reaches is allowed: asked
|
||||
* up front, in one question, so a refusal never stops it half made.
|
||||
*/
|
||||
export async function connectClaude(label: string): Promise<Connection> {
|
||||
const { hosts } = await getClaudeOption()
|
||||
const [first, ...rest] = hosts
|
||||
if (first) {
|
||||
const also = rest.map(({ host }) => host)
|
||||
const allowed = await askEgress({
|
||||
destination: first.destination,
|
||||
host: first.host,
|
||||
purpose: "claude_plan",
|
||||
also,
|
||||
allow: () => allowAll(first.destination, also),
|
||||
})
|
||||
if (!allowed) throw new ClaudeHostDeclinedError(first.host)
|
||||
}
|
||||
return requestJson<Connection>("/llm/connections/claude", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ label }),
|
||||
})
|
||||
}
|
||||
|
|
@ -0,0 +1,251 @@
|
|||
import { useState } from "react"
|
||||
import { useQuery } from "@tanstack/react-query"
|
||||
|
||||
import { Button } from "@/components/ui/button"
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog"
|
||||
import { ExternalLinkIcon } from "@/components/ui/icons"
|
||||
import { Spinner } from "@/components/ui/spinner"
|
||||
import { intl } from "@/i18n/intl"
|
||||
|
||||
import type { Connection } from "../api"
|
||||
import {
|
||||
CLAUDE_INSTALL_URL,
|
||||
ClaudeHostDeclinedError,
|
||||
claudeOptionQueryKey,
|
||||
connectClaude,
|
||||
getClaudeOption,
|
||||
type ClaudeOption,
|
||||
} from "./api"
|
||||
|
||||
function openInstallGuide() {
|
||||
if (window.surfsense?.openExternal)
|
||||
void window.surfsense.openExternal(CLAUDE_INSTALL_URL)
|
||||
else window.open(CLAUDE_INSTALL_URL, "_blank", "noopener")
|
||||
}
|
||||
|
||||
/** What to do next, for each state Claude Code can be in. */
|
||||
function StateBody({ option }: { option: ClaudeOption }) {
|
||||
switch (option.state) {
|
||||
case "not_installed":
|
||||
return (
|
||||
<ol className="flex list-decimal flex-col gap-1 pl-5 text-sm">
|
||||
<li>
|
||||
{intl.formatMessage({
|
||||
id: "models_claude_dialog_install_body",
|
||||
defaultMessage: "Install Claude Code on this computer.",
|
||||
})}
|
||||
</li>
|
||||
<li>
|
||||
{intl.formatMessage({
|
||||
id: "models_claude_dialog_sign_in_body",
|
||||
defaultMessage:
|
||||
"Open a terminal, run claude, and sign in with your Claude account.",
|
||||
})}
|
||||
</li>
|
||||
</ol>
|
||||
)
|
||||
case "signed_out":
|
||||
return (
|
||||
<p className="text-sm">
|
||||
{intl.formatMessage({
|
||||
id: "models_claude_dialog_signed_out_body",
|
||||
defaultMessage:
|
||||
"Claude Code is installed but not signed in to a Claude plan. Open a terminal, run claude, and sign in.",
|
||||
})}
|
||||
</p>
|
||||
)
|
||||
case "too_old":
|
||||
return (
|
||||
<p className="text-sm">
|
||||
{option.version
|
||||
? intl.formatMessage(
|
||||
{
|
||||
id: "models_claude_dialog_too_old_body",
|
||||
defaultMessage:
|
||||
"Claude Code {version} is too old for SurfSense. Update it, then check again.",
|
||||
},
|
||||
{ version: option.version }
|
||||
)
|
||||
: intl.formatMessage({
|
||||
id: "models_claude_dialog_unreadable_version_body",
|
||||
defaultMessage:
|
||||
"SurfSense couldn’t read which Claude Code is installed. Update it, then check again.",
|
||||
})}
|
||||
</p>
|
||||
)
|
||||
case "ready":
|
||||
return (
|
||||
<p className="text-sm">
|
||||
{intl.formatMessage(
|
||||
{
|
||||
id: "models_claude_dialog_ready_body",
|
||||
defaultMessage:
|
||||
"Connected as {email}, {plan, select, pro {Pro plan} max {Max plan} team {Team plan} other {Claude plan}}. Claude models then work in chat, titles and Studio.",
|
||||
},
|
||||
{ email: option.email ?? "", plan: option.plan ?? "other" }
|
||||
)}
|
||||
</p>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The Claude plan, set up from the user's own Claude Code. SurfSense cannot
|
||||
* sign anyone in to Claude, so this says what Claude Code needs and checks it
|
||||
* again on request; nothing polls, since nothing changes until the user acts
|
||||
* in their terminal.
|
||||
*/
|
||||
export function ClaudeDialog({
|
||||
open,
|
||||
onOpenChange,
|
||||
onConnected,
|
||||
}: {
|
||||
open: boolean
|
||||
onOpenChange: (open: boolean) => void
|
||||
onConnected: (connection: Connection) => void
|
||||
}) {
|
||||
const option = useQuery({
|
||||
queryKey: claudeOptionQueryKey,
|
||||
queryFn: ({ signal }) => getClaudeOption(signal),
|
||||
enabled: open,
|
||||
// Read again on each opening: the user may have just signed in.
|
||||
refetchOnMount: "always",
|
||||
retry: false,
|
||||
})
|
||||
const [connecting, setConnecting] = useState(false)
|
||||
const [failure, setFailure] = useState<string | null>(null)
|
||||
|
||||
const connect = async () => {
|
||||
setConnecting(true)
|
||||
setFailure(null)
|
||||
try {
|
||||
const connection = await connectClaude(
|
||||
intl.formatMessage({
|
||||
id: "models_claude_connection_label",
|
||||
defaultMessage: "Claude plan",
|
||||
})
|
||||
)
|
||||
onConnected(connection)
|
||||
onOpenChange(false)
|
||||
} catch (error) {
|
||||
setFailure(
|
||||
error instanceof ClaudeHostDeclinedError
|
||||
? intl.formatMessage(
|
||||
{
|
||||
id: "models_claude_dialog_host_declined_error",
|
||||
defaultMessage:
|
||||
"Your Claude plan needs {host}. Allow it to connect.",
|
||||
},
|
||||
{ host: error.host }
|
||||
)
|
||||
: error instanceof Error
|
||||
? error.message
|
||||
: null
|
||||
)
|
||||
} finally {
|
||||
setConnecting(false)
|
||||
}
|
||||
}
|
||||
|
||||
const ready = option.data?.state === "ready"
|
||||
return (
|
||||
<Dialog
|
||||
open={open}
|
||||
onOpenChange={onOpenChange}
|
||||
onOpenChangeComplete={(open) => !open && setFailure(null)}
|
||||
>
|
||||
<DialogContent className="sm:max-w-md">
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
{intl.formatMessage({
|
||||
id: "models_claude_dialog_title",
|
||||
defaultMessage: "Use your Claude plan",
|
||||
})}
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
{intl.formatMessage({
|
||||
id: "models_claude_dialog_body",
|
||||
defaultMessage:
|
||||
"SurfSense uses your Claude Pro, Max or Team plan through Claude Code on this computer. Your sign-in stays in Claude Code, and SurfSense never sees it. Use counts toward your plan’s limits, as it does in Claude Code.",
|
||||
})}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
{option.isPending ? (
|
||||
<Spinner
|
||||
aria-label={intl.formatMessage({
|
||||
id: "models_claude_dialog_checking_status",
|
||||
defaultMessage: "Checking Claude Code",
|
||||
})}
|
||||
/>
|
||||
) : option.data ? (
|
||||
<StateBody option={option.data} />
|
||||
) : (
|
||||
<p role="alert" className="text-sm text-destructive">
|
||||
{intl.formatMessage({
|
||||
id: "models_claude_dialog_check_error",
|
||||
defaultMessage: "Couldn’t check Claude Code on this computer.",
|
||||
})}
|
||||
</p>
|
||||
)}
|
||||
{failure ? (
|
||||
<p role="alert" className="text-sm text-destructive">
|
||||
{failure}
|
||||
</p>
|
||||
) : null}
|
||||
<DialogFooter>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={() => onOpenChange(false)}
|
||||
>
|
||||
{intl.formatMessage({
|
||||
id: "models_claude_dialog_cancel_button",
|
||||
defaultMessage: "Cancel",
|
||||
})}
|
||||
</Button>
|
||||
{option.data?.state === "not_installed" ? (
|
||||
<Button type="button" variant="outline" onClick={openInstallGuide}>
|
||||
{intl.formatMessage({
|
||||
id: "models_claude_dialog_install_guide_button",
|
||||
defaultMessage: "Open install guide",
|
||||
})}
|
||||
<ExternalLinkIcon data-icon="inline-end" />
|
||||
</Button>
|
||||
) : null}
|
||||
{ready ? (
|
||||
<Button
|
||||
type="button"
|
||||
disabled={connecting}
|
||||
onClick={() => void connect()}
|
||||
>
|
||||
{connecting ? <Spinner data-icon="inline-start" /> : null}
|
||||
{intl.formatMessage({
|
||||
id: "models_claude_dialog_add_button",
|
||||
defaultMessage: "Add Claude models",
|
||||
})}
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
type="button"
|
||||
disabled={option.isFetching}
|
||||
onClick={() => void option.refetch()}
|
||||
>
|
||||
{option.isFetching ? <Spinner data-icon="inline-start" /> : null}
|
||||
{intl.formatMessage({
|
||||
id: "models_claude_dialog_check_again_button",
|
||||
defaultMessage: "Check again",
|
||||
})}
|
||||
</Button>
|
||||
)}
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,189 @@
|
|||
import { afterEach, describe, expect, it, vi } from "vitest"
|
||||
import { cleanup, screen, waitFor } from "@testing-library/react"
|
||||
import userEvent from "@testing-library/user-event"
|
||||
|
||||
import { render } from "@/test-utils"
|
||||
|
||||
import type { Connection } from "../api"
|
||||
import { CLAUDE_INSTALL_URL, type ClaudeOption as Option } from "./api"
|
||||
import { ClaudeOption } from "./claude-option"
|
||||
|
||||
const READY: Option = {
|
||||
state: "ready",
|
||||
version: "2.1.301",
|
||||
email: "ada@example.com",
|
||||
plan: "max",
|
||||
serves: ["text_gen"],
|
||||
hosts: [],
|
||||
}
|
||||
|
||||
const CONNECTED: Connection = {
|
||||
id: 7,
|
||||
label: "Claude plan",
|
||||
provider: "openai_compatible",
|
||||
base_url: "https://api.anthropic.com",
|
||||
catalog_provider: "anthropic",
|
||||
has_api_key: false,
|
||||
auth_kind: "claude_plan",
|
||||
signed_in: false,
|
||||
account_email: null,
|
||||
serves: ["text_gen"],
|
||||
created_at: "2026-10-16T00:00:00",
|
||||
updated_at: "2026-10-16T00:00:00",
|
||||
}
|
||||
|
||||
/** The API as the dialog sees it: each status read takes the next of `states`. */
|
||||
function serve(states: Partial<Option>[], { connected = false } = {}) {
|
||||
let made = connected
|
||||
const fetchMock = vi.fn(
|
||||
async (input: RequestInfo | URL, init?: RequestInit) => {
|
||||
const path = String(input)
|
||||
if (path === "/llm/connections/claude" && init?.method === "POST") {
|
||||
made = true
|
||||
return Response.json(CONNECTED, { status: 201 })
|
||||
}
|
||||
if (path === "/llm/connections/claude")
|
||||
return Response.json({
|
||||
...READY,
|
||||
...(states.length > 1 ? states.shift() : states[0]),
|
||||
})
|
||||
if (path === "/llm/connections")
|
||||
return Response.json(made ? [CONNECTED] : [])
|
||||
return Response.json({ detail: "not found" }, { status: 404 })
|
||||
}
|
||||
)
|
||||
vi.stubGlobal("fetch", fetchMock)
|
||||
window.surfsense = {
|
||||
openExternal: vi.fn(async () => {}),
|
||||
} as unknown as typeof window.surfsense
|
||||
return fetchMock
|
||||
}
|
||||
|
||||
const setUp = () => screen.findByRole("button", { name: "Set up" })
|
||||
|
||||
afterEach(() => {
|
||||
cleanup()
|
||||
vi.unstubAllGlobals()
|
||||
delete window.surfsense
|
||||
})
|
||||
|
||||
describe("the Claude plan option beside the ChatGPT one", () => {
|
||||
it("is offered only where a Claude plan can fill the slot", async () => {
|
||||
serve([{}])
|
||||
render(<ClaudeOption modelType="video_gen" onConnected={vi.fn()} />)
|
||||
|
||||
await waitFor(() =>
|
||||
expect(
|
||||
screen.queryByRole("region", { name: "Use your Claude plan" })
|
||||
).toBeNull()
|
||||
)
|
||||
})
|
||||
|
||||
it("says to install Claude Code, and opens Anthropic’s guide", async () => {
|
||||
serve([{ state: "not_installed", version: null, email: null, plan: null }])
|
||||
const user = userEvent.setup()
|
||||
render(<ClaudeOption modelType="text_gen" onConnected={vi.fn()} />)
|
||||
|
||||
await user.click(await setUp())
|
||||
expect(
|
||||
await screen.findByText("Install Claude Code on this computer.")
|
||||
).toBeTruthy()
|
||||
await user.click(screen.getByRole("button", { name: "Open install guide" }))
|
||||
|
||||
expect(window.surfsense?.openExternal).toHaveBeenCalledWith(
|
||||
CLAUDE_INSTALL_URL
|
||||
)
|
||||
})
|
||||
|
||||
it("checks again after the user signs in in a terminal", async () => {
|
||||
serve([{ state: "signed_out", email: null, plan: null }, {}])
|
||||
const user = userEvent.setup()
|
||||
render(<ClaudeOption modelType="text_gen" onConnected={vi.fn()} />)
|
||||
|
||||
await user.click(await setUp())
|
||||
expect(
|
||||
await screen.findByText(/not signed in to a Claude plan/)
|
||||
).toBeTruthy()
|
||||
await user.click(screen.getByRole("button", { name: "Check again" }))
|
||||
|
||||
expect(
|
||||
await screen.findByText(/Connected as ada@example.com, Max plan/)
|
||||
).toBeTruthy()
|
||||
expect(
|
||||
screen.getByRole("button", { name: "Add Claude models" })
|
||||
).toBeTruthy()
|
||||
})
|
||||
|
||||
it("names the version when Claude Code is too old", async () => {
|
||||
serve([{ state: "too_old", version: "2.0.12", email: null, plan: null }])
|
||||
const user = userEvent.setup()
|
||||
render(<ClaudeOption modelType="text_gen" onConnected={vi.fn()} />)
|
||||
|
||||
await user.click(await setUp())
|
||||
|
||||
expect(
|
||||
await screen.findByText(/Claude Code 2.0.12 is too old/)
|
||||
).toBeTruthy()
|
||||
})
|
||||
|
||||
it("adds the plan and hands over the new connection", async () => {
|
||||
const fetchMock = serve([{}])
|
||||
const onConnected = vi.fn()
|
||||
const user = userEvent.setup()
|
||||
render(<ClaudeOption modelType="text_gen" onConnected={onConnected} />)
|
||||
|
||||
await user.click(await setUp())
|
||||
await user.click(
|
||||
await screen.findByRole("button", { name: "Add Claude models" })
|
||||
)
|
||||
|
||||
await waitFor(() => expect(onConnected).toHaveBeenCalledWith(CONNECTED))
|
||||
const made = fetchMock.mock.calls.find(
|
||||
([path, init]) =>
|
||||
path === "/llm/connections/claude" && init?.method === "POST"
|
||||
)
|
||||
expect(JSON.parse(String(made?.[1]?.body))).toEqual({
|
||||
label: "Claude plan",
|
||||
})
|
||||
})
|
||||
|
||||
it("makes nothing while a host it needs stays off", async () => {
|
||||
const fetchMock = serve([
|
||||
{
|
||||
hosts: [
|
||||
{ destination: "host:api.anthropic.com", host: "api.anthropic.com" },
|
||||
],
|
||||
},
|
||||
])
|
||||
const user = userEvent.setup()
|
||||
render(<ClaudeOption modelType="text_gen" onConnected={vi.fn()} />)
|
||||
|
||||
await user.click(await setUp())
|
||||
await user.click(
|
||||
await screen.findByRole("button", { name: "Add Claude models" })
|
||||
)
|
||||
|
||||
expect(
|
||||
await screen.findByText(
|
||||
"Your Claude plan needs api.anthropic.com. Allow it to connect."
|
||||
)
|
||||
).toBeTruthy()
|
||||
expect(
|
||||
fetchMock.mock.calls.some(
|
||||
([path, init]) =>
|
||||
path === "/llm/connections/claude" && init?.method === "POST"
|
||||
)
|
||||
).toBe(false)
|
||||
})
|
||||
|
||||
it("opens the models of a plan already connected", async () => {
|
||||
serve([{}], { connected: true })
|
||||
const onConnected = vi.fn()
|
||||
const user = userEvent.setup()
|
||||
render(<ClaudeOption modelType="text_gen" onConnected={onConnected} />)
|
||||
|
||||
await user.click(await screen.findByRole("button", { name: "Show models" }))
|
||||
|
||||
expect(onConnected).toHaveBeenCalledWith(CONNECTED)
|
||||
})
|
||||
})
|
||||
|
|
@ -0,0 +1,126 @@
|
|||
import { useState } from "react"
|
||||
import { useQuery } from "@tanstack/react-query"
|
||||
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { ClaudeIcon } from "@/components/ui/icons"
|
||||
import { intl } from "@/i18n/intl"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
import type { ModelType } from "../../../model-type"
|
||||
import { useRefreshModels } from "../../../models-query"
|
||||
import type { Connection } from "../api"
|
||||
import { useConnectionsServing } from "../use-connections"
|
||||
import { claudeOptionQueryKey, getClaudeOption } from "./api"
|
||||
import { ClaudeDialog } from "./claude-dialog"
|
||||
|
||||
/**
|
||||
* A Claude plan, offered beside the ChatGPT plan: it runs the user's own
|
||||
* Claude Code, so setting it up is a dialog that says what Claude Code needs.
|
||||
* Shown only where the backend says a plan fills the slot; one already
|
||||
* connected opens its models instead.
|
||||
*/
|
||||
export function ClaudeOption({
|
||||
modelType,
|
||||
onConnected,
|
||||
dense = false,
|
||||
}: {
|
||||
modelType: ModelType
|
||||
onConnected: (connection: Connection) => void
|
||||
/** Onboarding's smaller heading and button, beside its own server row. */
|
||||
dense?: boolean
|
||||
}) {
|
||||
const option = useQuery({
|
||||
queryKey: claudeOptionQueryKey,
|
||||
queryFn: ({ signal }) => getClaudeOption(signal),
|
||||
retry: false,
|
||||
})
|
||||
// The same list as the slot's, so a plan connected anywhere is reused.
|
||||
const connections = useConnectionsServing(modelType)
|
||||
const refresh = useRefreshModels()
|
||||
const [open, setOpen] = useState(false)
|
||||
|
||||
if (!option.data?.serves.includes(modelType)) return null
|
||||
|
||||
const connected = connections.data?.find(
|
||||
(connection) => connection.auth_kind === "claude_plan"
|
||||
)
|
||||
const size = dense ? "default" : "sm"
|
||||
const title = intl.formatMessage({
|
||||
id: "models_claude_option_title",
|
||||
defaultMessage: "Use your Claude plan",
|
||||
})
|
||||
|
||||
return (
|
||||
<section aria-label={title} className="flex flex-col gap-2">
|
||||
<div
|
||||
className={cn(
|
||||
"flex justify-between",
|
||||
dense ? "items-center gap-4" : "items-start gap-3"
|
||||
)}
|
||||
>
|
||||
<div className="min-w-0">
|
||||
<h3
|
||||
className={cn(
|
||||
"flex items-center gap-2 font-medium",
|
||||
dense ? "text-sm" : "text-base"
|
||||
)}
|
||||
>
|
||||
<ClaudeIcon
|
||||
aria-hidden="true"
|
||||
className="size-4 text-muted-foreground"
|
||||
/>
|
||||
{title}
|
||||
</h3>
|
||||
{/* pl-6: the icon's 16px plus the 8px gap, so it lines up with the heading text. */}
|
||||
<p className="truncate pl-6 text-xs text-muted-foreground">
|
||||
{connected
|
||||
? intl.formatMessage({
|
||||
id: "models_claude_option_connected_body",
|
||||
defaultMessage:
|
||||
"Connected through Claude Code on this computer",
|
||||
})
|
||||
: intl.formatMessage({
|
||||
id: "models_claude_option_body",
|
||||
defaultMessage:
|
||||
"Pro, Max or Team, through Claude Code on this computer.",
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
{connected ? (
|
||||
<Button
|
||||
type="button"
|
||||
size={size}
|
||||
variant="outline"
|
||||
onClick={() => onConnected(connected)}
|
||||
>
|
||||
{intl.formatMessage({
|
||||
id: "models_claude_option_show_button",
|
||||
defaultMessage: "Show models",
|
||||
})}
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
type="button"
|
||||
size={size}
|
||||
variant="outline"
|
||||
onClick={() => setOpen(true)}
|
||||
>
|
||||
{intl.formatMessage({
|
||||
id: "models_claude_option_set_up_button",
|
||||
defaultMessage: "Set up",
|
||||
})}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
<ClaudeDialog
|
||||
open={open}
|
||||
onOpenChange={setOpen}
|
||||
onConnected={(connection) => {
|
||||
void refresh()
|
||||
void connections.refetch()
|
||||
onConnected(connection)
|
||||
}}
|
||||
/>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,66 @@
|
|||
import { afterEach, describe, expect, it, vi } from "vitest"
|
||||
import { cleanup, screen, waitFor } from "@testing-library/react"
|
||||
import userEvent from "@testing-library/user-event"
|
||||
|
||||
import { render } from "@/test-utils"
|
||||
|
||||
import type { Connection } from "../api"
|
||||
import { ConnectionForm } from "../connection-form"
|
||||
|
||||
const PLAN: Connection = {
|
||||
id: 7,
|
||||
label: "Claude plan",
|
||||
provider: "openai_compatible",
|
||||
base_url: "https://api.anthropic.com",
|
||||
catalog_provider: "anthropic",
|
||||
has_api_key: false,
|
||||
auth_kind: "claude_plan",
|
||||
signed_in: false,
|
||||
account_email: null,
|
||||
serves: ["text_gen"],
|
||||
created_at: "2026-10-16T00:00:00",
|
||||
updated_at: "2026-10-16T00:00:00",
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
cleanup()
|
||||
vi.unstubAllGlobals()
|
||||
})
|
||||
|
||||
describe("editing a Claude plan connection", () => {
|
||||
it("asks for no URL or key, and only renames it", async () => {
|
||||
const fetchMock = vi.fn(
|
||||
async (input: RequestInfo | URL, init?: RequestInit) => {
|
||||
if (String(input) === "/llm/connections/7" && init?.method === "PUT")
|
||||
return Response.json({ ...PLAN, label: "Work Claude" })
|
||||
return Response.json([])
|
||||
}
|
||||
)
|
||||
vi.stubGlobal("fetch", fetchMock)
|
||||
const onSaved = vi.fn()
|
||||
const user = userEvent.setup()
|
||||
render(
|
||||
<ConnectionForm connection={PLAN} onCancel={vi.fn()} onSaved={onSaved} />
|
||||
)
|
||||
|
||||
expect(screen.queryByLabelText("Base URL")).toBeNull()
|
||||
expect(screen.queryByLabelText("API key")).toBeNull()
|
||||
expect(screen.getByText(/run claude in a terminal/)).toBeTruthy()
|
||||
const save = screen.getByRole("button", { name: "Save changes" })
|
||||
expect(save.hasAttribute("disabled")).toBe(true)
|
||||
|
||||
const name = screen.getByLabelText("Name")
|
||||
await user.clear(name)
|
||||
await user.type(name, "Work Claude")
|
||||
await user.click(save)
|
||||
|
||||
await waitFor(() =>
|
||||
expect(onSaved).toHaveBeenCalledWith({ ...PLAN, label: "Work Claude" })
|
||||
)
|
||||
const sent = fetchMock.mock.calls.find(([, init]) => init?.method === "PUT")
|
||||
expect(JSON.parse(String(sent?.[1]?.body))).toMatchObject({
|
||||
label: "Work Claude",
|
||||
base_url: "https://api.anthropic.com",
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
@ -0,0 +1,98 @@
|
|||
import { useEffect, useState, type ReactNode } from "react"
|
||||
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { DialogFooter } from "@/components/ui/dialog"
|
||||
import { FieldGroup } from "@/components/ui/field"
|
||||
import { Spinner } from "@/components/ui/spinner"
|
||||
import { intl } from "@/i18n/intl"
|
||||
|
||||
import { updateConnection, type Connection } from "../api"
|
||||
|
||||
/** The provider-picker value for a Claude plan; no manifest entry has it. */
|
||||
export const CLAUDE_PLAN_PROVIDER = "claude_plan"
|
||||
|
||||
/**
|
||||
* The connection form for a Claude plan: no URL, key or sign-in, since
|
||||
* Claude Code keeps all three. Only the name is the user's to edit here.
|
||||
*/
|
||||
export function ClaudePlanEdit({
|
||||
fields,
|
||||
label,
|
||||
connection,
|
||||
onBusyChange,
|
||||
onCancel,
|
||||
onSaved,
|
||||
}: {
|
||||
/** The form's Name and Provider fields, shared with every other provider. */
|
||||
fields: ReactNode
|
||||
label: string
|
||||
connection: Connection
|
||||
onBusyChange: (busy: boolean) => void
|
||||
onCancel: () => void
|
||||
onSaved: (connection: Connection) => void
|
||||
}) {
|
||||
const [saving, setSaving] = useState(false)
|
||||
const [saveError, setSaveError] = useState<string | null>(null)
|
||||
const renamed = label.trim() !== connection.label
|
||||
|
||||
useEffect(() => onBusyChange(saving), [saving, onBusyChange])
|
||||
|
||||
const rename = async () => {
|
||||
setSaving(true)
|
||||
setSaveError(null)
|
||||
try {
|
||||
onSaved(
|
||||
await updateConnection(connection.id, {
|
||||
label: label.trim(),
|
||||
provider: "openai_compatible",
|
||||
base_url: connection.base_url,
|
||||
allow_unverified: false,
|
||||
catalog_provider: connection.catalog_provider,
|
||||
})
|
||||
)
|
||||
} catch (error) {
|
||||
setSaveError(error instanceof Error ? error.message : null)
|
||||
} finally {
|
||||
setSaving(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<FieldGroup className="relative">
|
||||
{fields}
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{intl.formatMessage({
|
||||
id: "models_claude_plan_edit_body",
|
||||
defaultMessage:
|
||||
"Runs through Claude Code on this computer, which keeps your sign-in. To sign in or out, or to switch accounts, run claude in a terminal.",
|
||||
})}
|
||||
</p>
|
||||
{saveError ? (
|
||||
<p className="text-sm text-destructive" role="alert">
|
||||
{saveError}
|
||||
</p>
|
||||
) : null}
|
||||
</FieldGroup>
|
||||
<DialogFooter>
|
||||
<Button type="button" variant="outline" onClick={onCancel}>
|
||||
{intl.formatMessage({
|
||||
id: "models_claude_plan_edit_cancel_button",
|
||||
defaultMessage: "Cancel",
|
||||
})}
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
disabled={saving || !renamed || !label.trim()}
|
||||
onClick={() => void rename()}
|
||||
>
|
||||
{saving ? <Spinner data-icon="inline-start" /> : null}
|
||||
{intl.formatMessage({
|
||||
id: "models_claude_plan_edit_save_button",
|
||||
defaultMessage: "Save changes",
|
||||
})}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
@ -32,6 +32,7 @@ import {
|
|||
} from "./api"
|
||||
import { CHATGPT_PROVIDER } from "./chatgpt/api"
|
||||
import { ChatGPTSignIn } from "./chatgpt/chatgpt-sign-in"
|
||||
import { CLAUDE_PLAN_PROVIDER, ClaudePlanEdit } from "./claude/claude-plan-edit"
|
||||
import { fillTemplate } from "./provider-url"
|
||||
|
||||
// A local server's port is whatever its owner set, so nothing is filled in:
|
||||
|
|
@ -111,7 +112,9 @@ export function ConnectionForm({
|
|||
const [providerId, setProviderId] = useState(
|
||||
connection?.auth_kind === "chatgpt"
|
||||
? CHATGPT_PROVIDER
|
||||
: (connection?.catalog_provider ?? CUSTOM_PROVIDER)
|
||||
: connection?.auth_kind === "claude_plan"
|
||||
? CLAUDE_PLAN_PROVIDER
|
||||
: (connection?.catalog_provider ?? CUSTOM_PROVIDER)
|
||||
)
|
||||
const [accountValues, setAccountValues] = useState<Record<string, string>>({})
|
||||
const providers = useQuery({
|
||||
|
|
@ -150,10 +153,29 @@ export function ConnectionForm({
|
|||
keywords: ["chatgpt", "openai", "plus", "pro", "subscription"],
|
||||
disabled: false,
|
||||
}
|
||||
// Likewise a Claude plan, set up from its own option and only shown here.
|
||||
const isClaudePlan = connection?.auth_kind === "claude_plan"
|
||||
const claudePlanOption: ProviderOption = {
|
||||
value: CLAUDE_PLAN_PROVIDER,
|
||||
label: intl.formatMessage({
|
||||
id: "models_connection_form_claude_plan_provider_label",
|
||||
defaultMessage: "Claude plan",
|
||||
}),
|
||||
hint: intl.formatMessage({
|
||||
id: "models_connection_form_claude_plan_provider_body",
|
||||
defaultMessage: "Through Claude Code on this computer",
|
||||
}),
|
||||
keywords: ["claude", "anthropic", "pro", "max", "team"],
|
||||
disabled: false,
|
||||
}
|
||||
const providerGroups: ProviderGroup[] = [
|
||||
{
|
||||
label: null,
|
||||
items: offerChatGPT ? [customOption, chatgptOption] : [customOption],
|
||||
items: offerChatGPT
|
||||
? [customOption, chatgptOption]
|
||||
: isClaudePlan
|
||||
? [customOption, claudePlanOption]
|
||||
: [customOption],
|
||||
},
|
||||
{
|
||||
label: intl.formatMessage({
|
||||
|
|
@ -279,11 +301,11 @@ export function ConnectionForm({
|
|||
onValueChange={choose}
|
||||
isItemEqualToValue={(item, value) => item.value === value.value}
|
||||
filter={matchesProvider}
|
||||
disabled={busy || connection?.auth_kind === "chatgpt"}
|
||||
disabled={busy || offerChatGPT || isClaudePlan}
|
||||
>
|
||||
<ComboboxInput
|
||||
id={`${fieldId}-provider`}
|
||||
disabled={busy || connection?.auth_kind === "chatgpt"}
|
||||
disabled={busy || offerChatGPT || isClaudePlan}
|
||||
placeholder={intl.formatMessage({
|
||||
id: "models_connection_form_provider_placeholder",
|
||||
defaultMessage: "Search providers",
|
||||
|
|
@ -330,6 +352,19 @@ export function ConnectionForm({
|
|||
</>
|
||||
)
|
||||
|
||||
if (isClaudePlan && connection) {
|
||||
return (
|
||||
<ClaudePlanEdit
|
||||
fields={fields}
|
||||
label={label}
|
||||
connection={connection}
|
||||
onBusyChange={setBusy}
|
||||
onCancel={onCancel}
|
||||
onSaved={onSaved}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
if (isChatGPT) {
|
||||
return (
|
||||
<ChatGPTSignIn
|
||||
|
|
|
|||
|
|
@ -150,14 +150,19 @@ export function ServerModels({
|
|||
<span className="shrink-0">{connection.label}</span>
|
||||
<DotIcon aria-hidden="true" className="size-3 shrink-0" />
|
||||
<span className="truncate font-normal">
|
||||
{connection.auth_kind !== "chatgpt"
|
||||
? connection.base_url
|
||||
: connection.signed_in
|
||||
? (connection.account_email ?? connection.base_url)
|
||||
: intl.formatMessage({
|
||||
id: "models_server_models_signed_out_status",
|
||||
defaultMessage: "Signed out",
|
||||
})}
|
||||
{connection.auth_kind === "claude_plan"
|
||||
? intl.formatMessage({
|
||||
id: "models_server_models_claude_plan_status",
|
||||
defaultMessage: "Through Claude Code on this computer",
|
||||
})
|
||||
: connection.auth_kind !== "chatgpt"
|
||||
? connection.base_url
|
||||
: connection.signed_in
|
||||
? (connection.account_email ?? connection.base_url)
|
||||
: intl.formatMessage({
|
||||
id: "models_server_models_signed_out_status",
|
||||
defaultMessage: "Signed out",
|
||||
})}
|
||||
</span>
|
||||
</h3>
|
||||
<div className="flex shrink-0 items-center gap-1">
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ import { Spinner } from "@/components/ui/spinner"
|
|||
import type { LocalBuild, LocalRow } from "@/features/models/local/chat/api"
|
||||
import { useInstall } from "@/features/models/local/installs/use-install"
|
||||
import { ChatGPTOption } from "@/features/models/remote/connections/chatgpt/chatgpt-option"
|
||||
import { ClaudeOption } from "@/features/models/remote/connections/claude/claude-option"
|
||||
import { ConnectionDialog } from "@/features/models/remote/connections/connection-dialog"
|
||||
import { useConnectionsServing } from "@/features/models/remote/connections/use-connections"
|
||||
import { DeleteModelDialog } from "@/features/models/your-models/delete-model-dialog"
|
||||
|
|
@ -248,6 +249,14 @@ export function ModelStep({
|
|||
setOnServer(true)
|
||||
}}
|
||||
/>
|
||||
<ClaudeOption
|
||||
modelType={slot}
|
||||
dense
|
||||
onConnected={(connection) => {
|
||||
setOpenServerId(connection.id)
|
||||
setOnServer(true)
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
) : null}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -133,6 +133,8 @@
|
|||
"chat_composer_upload_sources_uploading_tooltip": "Wird hochgeladen …",
|
||||
"chat_error_agent_model_unsupported": "Diese Unterhaltung kann mit dem ausgewählten Modell nicht weitergehen. Starte einen neuen Chat, um es zu verwenden, oder wähle ein anderes Modell.",
|
||||
"chat_error_agent_thread_outdated": "Dieser Agent-Chat wurde gestartet, bevor jeder Chat seine eigenen Quellen hatte. Starte einen neuen Chat, um weiterzumachen.",
|
||||
"chat_error_claude_limit": "Das Nutzungslimit deines Claude-Plans ist erreicht. Es setzt sich von selbst zurück.",
|
||||
"chat_error_claude_sign_in": "Claude Code auf diesem Computer muss sich erneut anmelden: Führe claude in einem Terminal aus und melde dich an.",
|
||||
"chat_error_context_too_long": "Diese Unterhaltung ist zu lang für das Kontextfenster des Modells. Starte einen neuen Chat oder wähle ein Modell mit größerem Fenster.",
|
||||
"chat_error_interrupted": "Unterbrochen, als die App geschlossen wurde.",
|
||||
"chat_error_model_cannot_run": "SurfSense kann dieses Modell nicht ausführen. Wähle ein anderes Modell.",
|
||||
|
|
@ -172,6 +174,7 @@
|
|||
"chat_mode_picker_thread_aria": "Chatmodus {mode}. Ein Chat behält seinen Modus; starte für einen anderen einen neuen Chat.",
|
||||
"chat_mode_picker_trigger_aria": "Chatmodus {mode}. Modus wechseln.",
|
||||
"chat_model_issue_allow_button": "Erlauben …",
|
||||
"chat_model_issue_claude_sign_in_body": "{model} konnte nicht verwendet werden: Claude Code auf diesem Computer ist nicht installiert oder muss sich erneut anmelden.",
|
||||
"chat_model_issue_egress_off_body": "Das Senden von Daten an {host} ist aus.",
|
||||
"chat_model_issue_key_rejected_body": "Der Anbieter von {model} hat den Schlüssel abgelehnt.",
|
||||
"chat_model_issue_rate_limited_body": "Der Anbieter von {model} begrenzt gerade die Anfragen. Versuch es gleich noch einmal.",
|
||||
|
|
@ -283,6 +286,11 @@
|
|||
"egress_app_updates_label": "App-Updates",
|
||||
"egress_app_updates_prompt_body": "Bei der Prüfung fragt SurfSense {host} nach der neuesten Version. Dabei werden nur deine IP-Adresse und deine installierte Version gesendet. Wenn du zustimmst, wird auch die Prüfung beim Start aktiviert. Du kannst sie unter Einstellungen › Netzwerk ausschalten.",
|
||||
"egress_app_updates_prompt_title": "SurfSense erlauben, nach Updates zu suchen?",
|
||||
"egress_claude_api_host_body": "{host} erhält, was du an das Modell sendest: deine Prompts und Auszüge aus deinen Dokumenten.",
|
||||
"egress_claude_prompt_body": "Claude Code auf diesem Computer erreicht Anthropic nur über SurfSense und nur über die Hosts, die du erlaubst. Du kannst jeden einzeln unter Einstellungen › Netzwerk ausschalten.",
|
||||
"egress_claude_prompt_title": "Deinem Claude-Plan erlauben, Anthropic zu erreichen?",
|
||||
"egress_claude_sign_in_host_body": "{host} hält Claude Code angemeldet. Er erhält keine Chats, und wenn du ihn ausschaltest, funktioniert Claude nicht mehr, sobald die Anmeldung abläuft.",
|
||||
"egress_claude_sign_in_label": "Claude-Code-Anmeldung",
|
||||
"egress_connection_prompt_body": "Chats über diese Verbindung senden deine Prompts und Auszüge aus deinen Dokumenten an {host}. Du kannst das jederzeit unter Einstellungen › Netzwerk ausschalten.",
|
||||
"egress_connection_prompt_title": "Senden von Daten an {host} erlauben?",
|
||||
"egress_destination_allow_aria": "{name} erlauben",
|
||||
|
|
@ -303,6 +311,7 @@
|
|||
"egress_plugin_registry_prompt_body": "SurfSense lädt die Liste der Drittanbieter-Plugins von {host} und sendet dabei deine IP-Adresse, sonst nichts. Die Plugins von SurfSense laufen auf demselben Host, also kannst du sie damit auch verbinden. Du kannst das jederzeit unter Einstellungen › Netzwerk ausschalten.",
|
||||
"egress_plugin_registry_prompt_title": "Abrufen der Plugin-Liste erlauben?",
|
||||
"egress_prompt_allow_button": "Erlauben",
|
||||
"egress_prompt_also_allows_body": "Wenn du das erlaubst, kann SurfSense auch {hosts} erreichen.",
|
||||
"egress_prompt_cancel_button": "Abbrechen",
|
||||
"embedding_settings_body": "Das Modell, das deine Dokumente in eine Form bringt, die die Suche vergleichen kann.",
|
||||
"embedding_settings_fixed_body": "Bei der Einrichtung gewählt und für jedes Dokument verwendet, daher nicht änderbar.",
|
||||
|
|
@ -424,6 +433,30 @@
|
|||
"models_chatgpt_signed_in_unnamed_body": "Angemeldet.",
|
||||
"models_chatgpt_signed_out_body": "Abgemeldet. Melde dich erneut an, um die Modelle dieses Plans zu nutzen.",
|
||||
"models_chatgpt_waiting_status": "Schließe die Anmeldung in deinem Browser ab.",
|
||||
"models_claude_connection_label": "Claude-Plan",
|
||||
"models_claude_dialog_add_button": "Claude-Modelle hinzufügen",
|
||||
"models_claude_dialog_body": "SurfSense nutzt deinen Claude-Pro-, -Max- oder -Team-Plan über Claude Code auf diesem Computer. Deine Anmeldung bleibt in Claude Code, und SurfSense sieht sie nie. Die Nutzung zählt zu den Limits deines Plans, genau wie in Claude Code.",
|
||||
"models_claude_dialog_cancel_button": "Abbrechen",
|
||||
"models_claude_dialog_check_again_button": "Erneut prüfen",
|
||||
"models_claude_dialog_check_error": "Claude Code auf diesem Computer konnte nicht geprüft werden.",
|
||||
"models_claude_dialog_checking_status": "Claude Code wird geprüft",
|
||||
"models_claude_dialog_host_declined_error": "Dein Claude-Plan braucht {host}. Erlaube die Verbindung.",
|
||||
"models_claude_dialog_install_body": "Installiere Claude Code auf diesem Computer.",
|
||||
"models_claude_dialog_install_guide_button": "Installationsanleitung öffnen",
|
||||
"models_claude_dialog_ready_body": "Verbunden als {email}, {plan, select, pro {Pro-Plan} max {Max-Plan} team {Team-Plan} other {Claude-Plan}}. Claude-Modelle funktionieren dann im Chat, in Titeln und in Studio.",
|
||||
"models_claude_dialog_sign_in_body": "Öffne ein Terminal, führe claude aus und melde dich mit deinem Claude-Konto an.",
|
||||
"models_claude_dialog_signed_out_body": "Claude Code ist installiert, aber nicht bei einem Claude-Plan angemeldet. Öffne ein Terminal, führe claude aus und melde dich an.",
|
||||
"models_claude_dialog_title": "Deinen Claude-Plan nutzen",
|
||||
"models_claude_dialog_too_old_body": "Claude Code {version} ist zu alt für SurfSense. Aktualisiere es und prüfe dann erneut.",
|
||||
"models_claude_dialog_unreadable_version_body": "SurfSense konnte nicht erkennen, welches Claude Code installiert ist. Aktualisiere es und prüfe dann erneut.",
|
||||
"models_claude_option_body": "Pro, Max oder Team, über Claude Code auf diesem Computer.",
|
||||
"models_claude_option_connected_body": "Verbunden über Claude Code auf diesem Computer",
|
||||
"models_claude_option_set_up_button": "Einrichten",
|
||||
"models_claude_option_show_button": "Modelle anzeigen",
|
||||
"models_claude_option_title": "Deinen Claude-Plan nutzen",
|
||||
"models_claude_plan_edit_body": "Läuft über Claude Code auf diesem Computer, das deine Anmeldung speichert. Um dich an- oder abzumelden oder das Konto zu wechseln, führe claude in einem Terminal aus.",
|
||||
"models_claude_plan_edit_cancel_button": "Abbrechen",
|
||||
"models_claude_plan_edit_save_button": "Änderungen speichern",
|
||||
"models_connection_dialog_add_body": "Jeder Server, der die OpenAI API unterstützt, etwa vLLM, LM Studio oder OpenRouter.",
|
||||
"models_connection_dialog_add_title": "Server verbinden",
|
||||
"models_connection_dialog_edit_body": "Server gelten für alle Modelltypen, eine Änderung hier betrifft also alle.",
|
||||
|
|
@ -431,6 +464,8 @@
|
|||
"models_connection_form_cancel_button": "Abbrechen",
|
||||
"models_connection_form_chatgpt_provider_body": "Mit deinem ChatGPT-Konto anmelden",
|
||||
"models_connection_form_chatgpt_provider_label": "ChatGPT-Abo",
|
||||
"models_connection_form_claude_plan_provider_body": "Über Claude Code auf diesem Computer",
|
||||
"models_connection_form_claude_plan_provider_label": "Claude-Plan",
|
||||
"models_connection_form_clear_key_label": "Gespeicherten API-Schlüssel entfernen",
|
||||
"models_connection_form_custom_provider_body": "Jede OpenAI-kompatible URL",
|
||||
"models_connection_form_custom_provider_label": "Lokaler oder eigener Server",
|
||||
|
|
@ -618,6 +653,7 @@
|
|||
"models_server_card_connect_button": "Verbinden",
|
||||
"models_server_card_title": "Server verwenden",
|
||||
"models_server_models_capability_unknown_label": "Fähigkeit unbekannt",
|
||||
"models_server_models_claude_plan_status": "Über Claude Code auf diesem Computer",
|
||||
"models_server_models_current_in_use_button": "Aktiv",
|
||||
"models_server_models_edit_aria": "{server} bearbeiten",
|
||||
"models_server_models_edit_button": "Bearbeiten",
|
||||
|
|
|
|||
|
|
@ -133,6 +133,8 @@
|
|||
"chat_composer_upload_sources_uploading_tooltip": "Uploading…",
|
||||
"chat_error_agent_model_unsupported": "This thread can’t continue with the selected model. Start a new chat to use it, or choose another model.",
|
||||
"chat_error_agent_thread_outdated": "This agent chat was started before each chat kept its own sources. Start a new chat to continue.",
|
||||
"chat_error_claude_limit": "Your Claude plan’s usage limit is reached. It resets on its own.",
|
||||
"chat_error_claude_sign_in": "Claude Code on this computer needs to sign in again: run claude in a terminal and sign in.",
|
||||
"chat_error_context_too_long": "This conversation is too long for the model’s context window. Start a new chat or pick a model with a larger window.",
|
||||
"chat_error_interrupted": "Interrupted when the app closed.",
|
||||
"chat_error_model_cannot_run": "SurfSense cannot run this model. Pick another model.",
|
||||
|
|
@ -172,6 +174,7 @@
|
|||
"chat_mode_picker_thread_aria": "Chat mode {mode}. A chat keeps its mode; start a new chat for another.",
|
||||
"chat_mode_picker_trigger_aria": "Chat mode {mode}. Change mode.",
|
||||
"chat_model_issue_allow_button": "Allow…",
|
||||
"chat_model_issue_claude_sign_in_body": "Couldn’t use {model}: Claude Code on this computer isn’t installed or has to sign in again.",
|
||||
"chat_model_issue_egress_off_body": "Sending data to {host} is off.",
|
||||
"chat_model_issue_key_rejected_body": "The provider for {model} rejected its key.",
|
||||
"chat_model_issue_rate_limited_body": "The provider for {model} is limiting requests. Try again in a moment.",
|
||||
|
|
@ -283,6 +286,11 @@
|
|||
"egress_app_updates_label": "App updates",
|
||||
"egress_app_updates_prompt_body": "Checking asks {host} for the latest release. SurfSense sends your IP address and the version you are running, nothing else. Allowing also turns on the check at launch, which you can switch off in Settings › Network.",
|
||||
"egress_app_updates_prompt_title": "Allow SurfSense to check for updates?",
|
||||
"egress_claude_api_host_body": "{host} gets what you send the model: your prompts and excerpts of your documents.",
|
||||
"egress_claude_prompt_body": "Claude Code on this computer reaches Anthropic only through SurfSense, and only the hosts you allow. You can turn each one off in Settings › Network.",
|
||||
"egress_claude_prompt_title": "Allow your Claude plan to reach Anthropic?",
|
||||
"egress_claude_sign_in_host_body": "{host} keeps Claude Code signed in. It gets no chats, and turning it off stops Claude once the sign-in expires.",
|
||||
"egress_claude_sign_in_label": "Claude Code sign-in",
|
||||
"egress_connection_prompt_body": "Chats using this connection send your prompts and excerpts of your documents to {host}. You can turn this off any time in Settings › Network.",
|
||||
"egress_connection_prompt_title": "Allow sending data to {host}?",
|
||||
"egress_destination_allow_aria": "Allow {name}",
|
||||
|
|
@ -303,6 +311,7 @@
|
|||
"egress_plugin_registry_prompt_body": "SurfSense fetches the list of third-party plugins from {host}, sending your IP address and nothing else. SurfSense’s own plugins run on the same host, so this also lets you connect them. You can turn this off any time in Settings › Network.",
|
||||
"egress_plugin_registry_prompt_title": "Allow fetching the list of plugins?",
|
||||
"egress_prompt_allow_button": "Allow",
|
||||
"egress_prompt_also_allows_body": "Allowing also lets SurfSense reach {hosts}.",
|
||||
"egress_prompt_cancel_button": "Cancel",
|
||||
"embedding_settings_body": "The model that turns your documents into something search can compare.",
|
||||
"embedding_settings_fixed_body": "Chosen during onboarding setup and used for every document, so it can’t be changed.",
|
||||
|
|
@ -424,6 +433,30 @@
|
|||
"models_chatgpt_signed_in_unnamed_body": "Signed in.",
|
||||
"models_chatgpt_signed_out_body": "Signed out. Sign in again to use this plan’s models.",
|
||||
"models_chatgpt_waiting_status": "Finish signing in in your browser.",
|
||||
"models_claude_connection_label": "Claude plan",
|
||||
"models_claude_dialog_add_button": "Add Claude models",
|
||||
"models_claude_dialog_body": "SurfSense uses your Claude Pro, Max or Team plan through Claude Code on this computer. Your sign-in stays in Claude Code, and SurfSense never sees it. Use counts toward your plan’s limits, as it does in Claude Code.",
|
||||
"models_claude_dialog_cancel_button": "Cancel",
|
||||
"models_claude_dialog_check_again_button": "Check again",
|
||||
"models_claude_dialog_check_error": "Couldn’t check Claude Code on this computer.",
|
||||
"models_claude_dialog_checking_status": "Checking Claude Code",
|
||||
"models_claude_dialog_host_declined_error": "Your Claude plan needs {host}. Allow it to connect.",
|
||||
"models_claude_dialog_install_body": "Install Claude Code on this computer.",
|
||||
"models_claude_dialog_install_guide_button": "Open install guide",
|
||||
"models_claude_dialog_ready_body": "Connected as {email}, {plan, select, pro {Pro plan} max {Max plan} team {Team plan} other {Claude plan}}. Claude models then work in chat, titles and Studio.",
|
||||
"models_claude_dialog_sign_in_body": "Open a terminal, run claude, and sign in with your Claude account.",
|
||||
"models_claude_dialog_signed_out_body": "Claude Code is installed but not signed in to a Claude plan. Open a terminal, run claude, and sign in.",
|
||||
"models_claude_dialog_title": "Use your Claude plan",
|
||||
"models_claude_dialog_too_old_body": "Claude Code {version} is too old for SurfSense. Update it, then check again.",
|
||||
"models_claude_dialog_unreadable_version_body": "SurfSense couldn’t read which Claude Code is installed. Update it, then check again.",
|
||||
"models_claude_option_body": "Pro, Max or Team, through Claude Code on this computer.",
|
||||
"models_claude_option_connected_body": "Connected through Claude Code on this computer",
|
||||
"models_claude_option_set_up_button": "Set up",
|
||||
"models_claude_option_show_button": "Show models",
|
||||
"models_claude_option_title": "Use your Claude plan",
|
||||
"models_claude_plan_edit_body": "Runs through Claude Code on this computer, which keeps your sign-in. To sign in or out, or to switch accounts, run claude in a terminal.",
|
||||
"models_claude_plan_edit_cancel_button": "Cancel",
|
||||
"models_claude_plan_edit_save_button": "Save changes",
|
||||
"models_connection_dialog_add_body": "Any server that implements the OpenAI API, such as vLLM, LM Studio or OpenRouter.",
|
||||
"models_connection_dialog_add_title": "Connect a server",
|
||||
"models_connection_dialog_edit_body": "Servers are shared by every model type, so a change here applies to all of them.",
|
||||
|
|
@ -431,6 +464,8 @@
|
|||
"models_connection_form_cancel_button": "Cancel",
|
||||
"models_connection_form_chatgpt_provider_body": "Sign in with your ChatGPT account",
|
||||
"models_connection_form_chatgpt_provider_label": "ChatGPT subscription",
|
||||
"models_connection_form_claude_plan_provider_body": "Through Claude Code on this computer",
|
||||
"models_connection_form_claude_plan_provider_label": "Claude plan",
|
||||
"models_connection_form_clear_key_label": "Remove saved API key",
|
||||
"models_connection_form_custom_provider_body": "Any OpenAI-compatible URL",
|
||||
"models_connection_form_custom_provider_label": "Local or custom server",
|
||||
|
|
@ -618,6 +653,7 @@
|
|||
"models_server_card_connect_button": "Connect",
|
||||
"models_server_card_title": "Use a server",
|
||||
"models_server_models_capability_unknown_label": "Capability unknown",
|
||||
"models_server_models_claude_plan_status": "Through Claude Code on this computer",
|
||||
"models_server_models_current_in_use_button": "In use",
|
||||
"models_server_models_edit_aria": "Edit {server}",
|
||||
"models_server_models_edit_button": "Edit",
|
||||
|
|
|
|||
|
|
@ -133,6 +133,8 @@
|
|||
"chat_composer_upload_sources_uploading_tooltip": "Subiendo…",
|
||||
"chat_error_agent_model_unsupported": "Esta conversación no puede continuar con el modelo seleccionado. Inicia un chat nuevo para usarlo o elige otro modelo.",
|
||||
"chat_error_agent_thread_outdated": "Este chat del agente se inició antes de que cada chat tuviera sus propias fuentes. Inicia un chat nuevo para continuar.",
|
||||
"chat_error_claude_limit": "Has alcanzado el límite de uso de tu plan de Claude. Se restablece solo.",
|
||||
"chat_error_claude_sign_in": "Claude Code en este equipo tiene que volver a iniciar sesión: ejecuta claude en una terminal e inicia sesión.",
|
||||
"chat_error_context_too_long": "Esta conversación es demasiado larga para la ventana de contexto del modelo. Inicia un chat nuevo o elige un modelo con una ventana más grande.",
|
||||
"chat_error_interrupted": "Se interrumpió al cerrarse la aplicación.",
|
||||
"chat_error_model_cannot_run": "SurfSense no puede ejecutar este modelo. Elige otro modelo.",
|
||||
|
|
@ -172,6 +174,7 @@
|
|||
"chat_mode_picker_thread_aria": "Modo de chat {mode}. Un chat conserva su modo; inicia un chat nuevo para usar otro.",
|
||||
"chat_mode_picker_trigger_aria": "Modo de chat {mode}. Cambiar de modo.",
|
||||
"chat_model_issue_allow_button": "Permitir…",
|
||||
"chat_model_issue_claude_sign_in_body": "No se pudo usar {model}: Claude Code no está instalado en este equipo o tiene que volver a iniciar sesión.",
|
||||
"chat_model_issue_egress_off_body": "El envío de datos a {host} está desactivado.",
|
||||
"chat_model_issue_key_rejected_body": "El proveedor de {model} rechazó su clave.",
|
||||
"chat_model_issue_rate_limited_body": "El proveedor de {model} está limitando las solicitudes. Inténtalo de nuevo en un momento.",
|
||||
|
|
@ -283,6 +286,11 @@
|
|||
"egress_app_updates_label": "Actualizaciones de la app",
|
||||
"egress_app_updates_prompt_body": "Al comprobarlo, se pregunta a {host} por la última versión publicada. SurfSense envía tu dirección IP y la versión que estás usando, nada más. Permitirlo también activa la comprobación al iniciar, que puedes desactivar en Ajustes › Red.",
|
||||
"egress_app_updates_prompt_title": "¿Permitir que SurfSense busque actualizaciones?",
|
||||
"egress_claude_api_host_body": "{host} recibe lo que envías al modelo: tus prompts y extractos de tus documentos.",
|
||||
"egress_claude_prompt_body": "Claude Code en este equipo solo se conecta con Anthropic a través de SurfSense, y solo con los hosts que permitas. Puedes desactivar cada uno en Ajustes › Red.",
|
||||
"egress_claude_prompt_title": "¿Permitir que tu plan de Claude se conecte con Anthropic?",
|
||||
"egress_claude_sign_in_host_body": "{host} mantiene iniciada la sesión de Claude Code. No recibe ningún chat, y si lo desactivas, Claude deja de funcionar cuando caduque la sesión.",
|
||||
"egress_claude_sign_in_label": "Inicio de sesión de Claude Code",
|
||||
"egress_connection_prompt_body": "Los chats que usan esta conexión envían tus prompts y extractos de tus documentos a {host}. Puedes desactivarlo en cualquier momento en Ajustes › Red.",
|
||||
"egress_connection_prompt_title": "¿Permitir el envío de datos a {host}?",
|
||||
"egress_destination_allow_aria": "Permitir {name}",
|
||||
|
|
@ -303,6 +311,7 @@
|
|||
"egress_plugin_registry_prompt_body": "SurfSense descarga la lista de plugins de terceros desde {host} y solo envía tu dirección IP. Los plugins de SurfSense funcionan en el mismo host, así que esto también te permite conectarlos. Puedes desactivarlo en cualquier momento en Ajustes › Red.",
|
||||
"egress_plugin_registry_prompt_title": "¿Permitir descargar la lista de plugins?",
|
||||
"egress_prompt_allow_button": "Permitir",
|
||||
"egress_prompt_also_allows_body": "Al permitirlo, SurfSense también podrá conectarse con {hosts}.",
|
||||
"egress_prompt_cancel_button": "Cancelar",
|
||||
"embedding_settings_body": "El modelo que convierte tus documentos en algo que la búsqueda puede comparar.",
|
||||
"embedding_settings_fixed_body": "Elegido durante la configuración inicial y usado para todos los documentos, por lo que no se puede cambiar.",
|
||||
|
|
@ -424,6 +433,30 @@
|
|||
"models_chatgpt_signed_in_unnamed_body": "Sesión iniciada.",
|
||||
"models_chatgpt_signed_out_body": "Sesión cerrada. Vuelve a iniciar sesión para usar los modelos de este plan.",
|
||||
"models_chatgpt_waiting_status": "Termina de iniciar sesión en tu navegador.",
|
||||
"models_claude_connection_label": "Plan de Claude",
|
||||
"models_claude_dialog_add_button": "Añadir modelos de Claude",
|
||||
"models_claude_dialog_body": "SurfSense usa tu plan Claude Pro, Max o Team a través de Claude Code en este equipo. Tu inicio de sesión se queda en Claude Code, y SurfSense nunca lo ve. El uso cuenta para los límites de tu plan, igual que en Claude Code.",
|
||||
"models_claude_dialog_cancel_button": "Cancelar",
|
||||
"models_claude_dialog_check_again_button": "Volver a comprobar",
|
||||
"models_claude_dialog_check_error": "No se pudo comprobar Claude Code en este equipo.",
|
||||
"models_claude_dialog_checking_status": "Comprobando Claude Code",
|
||||
"models_claude_dialog_host_declined_error": "Tu plan de Claude necesita {host}. Permite que se conecte.",
|
||||
"models_claude_dialog_install_body": "Instala Claude Code en este equipo.",
|
||||
"models_claude_dialog_install_guide_button": "Abrir guía de instalación",
|
||||
"models_claude_dialog_ready_body": "Conectado como {email}, {plan, select, pro {plan Pro} max {plan Max} team {plan Team} other {plan de Claude}}. Así, los modelos de Claude funcionan en el chat, los títulos y Studio.",
|
||||
"models_claude_dialog_sign_in_body": "Abre una terminal, ejecuta claude e inicia sesión con tu cuenta de Claude.",
|
||||
"models_claude_dialog_signed_out_body": "Claude Code está instalado, pero no ha iniciado sesión en un plan de Claude. Abre una terminal, ejecuta claude e inicia sesión.",
|
||||
"models_claude_dialog_title": "Usar tu plan de Claude",
|
||||
"models_claude_dialog_too_old_body": "Claude Code {version} es demasiado antiguo para SurfSense. Actualízalo y vuelve a comprobarlo.",
|
||||
"models_claude_dialog_unreadable_version_body": "SurfSense no pudo saber qué Claude Code está instalado. Actualízalo y vuelve a comprobarlo.",
|
||||
"models_claude_option_body": "Pro, Max o Team, a través de Claude Code en este equipo.",
|
||||
"models_claude_option_connected_body": "Conectado a través de Claude Code en este equipo",
|
||||
"models_claude_option_set_up_button": "Configurar",
|
||||
"models_claude_option_show_button": "Mostrar modelos",
|
||||
"models_claude_option_title": "Usar tu plan de Claude",
|
||||
"models_claude_plan_edit_body": "Funciona a través de Claude Code en este equipo, que guarda tu inicio de sesión. Para iniciar o cerrar sesión, o para cambiar de cuenta, ejecuta claude en una terminal.",
|
||||
"models_claude_plan_edit_cancel_button": "Cancelar",
|
||||
"models_claude_plan_edit_save_button": "Guardar cambios",
|
||||
"models_connection_dialog_add_body": "Cualquier servidor que implemente la API de OpenAI, como vLLM, LM Studio u OpenRouter.",
|
||||
"models_connection_dialog_add_title": "Conectar un servidor",
|
||||
"models_connection_dialog_edit_body": "Los servidores se comparten entre todos los tipos de modelo, así que un cambio aquí afecta a todos.",
|
||||
|
|
@ -431,6 +464,8 @@
|
|||
"models_connection_form_cancel_button": "Cancelar",
|
||||
"models_connection_form_chatgpt_provider_body": "Inicia sesión con tu cuenta de ChatGPT",
|
||||
"models_connection_form_chatgpt_provider_label": "Suscripción a ChatGPT",
|
||||
"models_connection_form_claude_plan_provider_body": "A través de Claude Code en este equipo",
|
||||
"models_connection_form_claude_plan_provider_label": "Plan de Claude",
|
||||
"models_connection_form_clear_key_label": "Quitar la clave de API guardada",
|
||||
"models_connection_form_custom_provider_body": "Cualquier URL compatible con OpenAI",
|
||||
"models_connection_form_custom_provider_label": "Servidor local o personalizado",
|
||||
|
|
@ -618,6 +653,7 @@
|
|||
"models_server_card_connect_button": "Conectar",
|
||||
"models_server_card_title": "Usar un servidor",
|
||||
"models_server_models_capability_unknown_label": "Capacidad desconocida",
|
||||
"models_server_models_claude_plan_status": "A través de Claude Code en este equipo",
|
||||
"models_server_models_current_in_use_button": "En uso",
|
||||
"models_server_models_edit_aria": "Editar {server}",
|
||||
"models_server_models_edit_button": "Editar",
|
||||
|
|
|
|||
|
|
@ -133,6 +133,8 @@
|
|||
"chat_composer_upload_sources_uploading_tooltip": "Envoi…",
|
||||
"chat_error_agent_model_unsupported": "Cette conversation ne peut pas continuer avec le modèle sélectionné. Démarrez un nouveau chat pour l’utiliser, ou choisissez un autre modèle.",
|
||||
"chat_error_agent_thread_outdated": "Ce chat avec l’agent a été démarré avant que chaque chat ait ses propres sources. Démarrez un nouveau chat pour continuer.",
|
||||
"chat_error_claude_limit": "La limite d’utilisation de votre forfait Claude est atteinte. Elle se réinitialise d’elle-même.",
|
||||
"chat_error_claude_sign_in": "Claude Code sur cet ordinateur doit se reconnecter : exécutez claude dans un terminal et connectez-vous.",
|
||||
"chat_error_context_too_long": "Cette conversation est trop longue pour la fenêtre de contexte du modèle. Démarrez un nouveau chat ou choisissez un modèle avec une fenêtre plus grande.",
|
||||
"chat_error_interrupted": "Interrompu à la fermeture de l’application.",
|
||||
"chat_error_model_cannot_run": "SurfSense ne peut pas exécuter ce modèle. Choisissez un autre modèle.",
|
||||
|
|
@ -172,6 +174,7 @@
|
|||
"chat_mode_picker_thread_aria": "Mode de chat {mode}. Un chat garde son mode ; démarrez un nouveau chat pour en changer.",
|
||||
"chat_mode_picker_trigger_aria": "Mode de chat {mode}. Changer de mode.",
|
||||
"chat_model_issue_allow_button": "Autoriser…",
|
||||
"chat_model_issue_claude_sign_in_body": "Impossible d’utiliser {model} : Claude Code n’est pas installé sur cet ordinateur ou doit se reconnecter.",
|
||||
"chat_model_issue_egress_off_body": "L’envoi de données vers {host} est désactivé.",
|
||||
"chat_model_issue_key_rejected_body": "Le fournisseur de {model} a rejeté sa clé.",
|
||||
"chat_model_issue_rate_limited_body": "Le fournisseur de {model} limite les requêtes. Réessayez dans un instant.",
|
||||
|
|
@ -283,6 +286,11 @@
|
|||
"egress_app_updates_label": "Mises à jour de l’application",
|
||||
"egress_app_updates_prompt_body": "La vérification demande à {host} la dernière version publiée. SurfSense envoie votre adresse IP et la version que vous utilisez, rien d’autre. L’autoriser active aussi la vérification au démarrage, que vous pouvez désactiver dans Paramètres › Réseau.",
|
||||
"egress_app_updates_prompt_title": "Autoriser SurfSense à rechercher des mises à jour ?",
|
||||
"egress_claude_api_host_body": "{host} reçoit ce que vous envoyez au modèle : vos prompts et des extraits de vos documents.",
|
||||
"egress_claude_prompt_body": "Claude Code sur cet ordinateur n’accède à Anthropic que via SurfSense, et uniquement par les hôtes que vous autorisez. Vous pouvez désactiver chacun d’eux dans Paramètres › Réseau.",
|
||||
"egress_claude_prompt_title": "Autoriser votre forfait Claude à accéder à Anthropic ?",
|
||||
"egress_claude_sign_in_host_body": "{host} maintient la connexion de Claude Code. Il ne reçoit aucun chat, et le désactiver arrête Claude dès que la connexion expire.",
|
||||
"egress_claude_sign_in_label": "Connexion à Claude Code",
|
||||
"egress_connection_prompt_body": "Les chats utilisant cette connexion envoient vos prompts et des extraits de vos documents vers {host}. Vous pouvez désactiver cela à tout moment dans Paramètres › Réseau.",
|
||||
"egress_connection_prompt_title": "Autoriser l’envoi de données vers {host} ?",
|
||||
"egress_destination_allow_aria": "Autoriser {name}",
|
||||
|
|
@ -303,6 +311,7 @@
|
|||
"egress_plugin_registry_prompt_body": "SurfSense télécharge la liste des plugins tiers depuis {host}, en n’envoyant que votre adresse IP. Les plugins de SurfSense fonctionnent sur le même hôte, ce qui vous permet aussi de les connecter. Vous pouvez désactiver cela à tout moment dans Paramètres › Réseau.",
|
||||
"egress_plugin_registry_prompt_title": "Autoriser le téléchargement de la liste des plugins ?",
|
||||
"egress_prompt_allow_button": "Autoriser",
|
||||
"egress_prompt_also_allows_body": "Autoriser permet aussi à SurfSense d’accéder à {hosts}.",
|
||||
"egress_prompt_cancel_button": "Annuler",
|
||||
"embedding_settings_body": "Le modèle qui transforme vos documents en une forme que la recherche peut comparer.",
|
||||
"embedding_settings_fixed_body": "Choisi lors de la configuration initiale et utilisé pour chaque document, il ne peut donc pas être modifié.",
|
||||
|
|
@ -424,6 +433,30 @@
|
|||
"models_chatgpt_signed_in_unnamed_body": "Connecté.",
|
||||
"models_chatgpt_signed_out_body": "Déconnecté. Reconnectez-vous pour utiliser les modèles de ce forfait.",
|
||||
"models_chatgpt_waiting_status": "Terminez la connexion dans votre navigateur.",
|
||||
"models_claude_connection_label": "Forfait Claude",
|
||||
"models_claude_dialog_add_button": "Ajouter les modèles Claude",
|
||||
"models_claude_dialog_body": "SurfSense utilise votre forfait Claude Pro, Max ou Team via Claude Code sur cet ordinateur. Votre connexion reste dans Claude Code, et SurfSense ne la voit jamais. L’utilisation compte dans les limites de votre forfait, comme dans Claude Code.",
|
||||
"models_claude_dialog_cancel_button": "Annuler",
|
||||
"models_claude_dialog_check_again_button": "Vérifier à nouveau",
|
||||
"models_claude_dialog_check_error": "Impossible de vérifier Claude Code sur cet ordinateur.",
|
||||
"models_claude_dialog_checking_status": "Vérification de Claude Code",
|
||||
"models_claude_dialog_host_declined_error": "Votre forfait Claude a besoin de {host}. Autorisez-le à se connecter.",
|
||||
"models_claude_dialog_install_body": "Installez Claude Code sur cet ordinateur.",
|
||||
"models_claude_dialog_install_guide_button": "Ouvrir le guide d’installation",
|
||||
"models_claude_dialog_ready_body": "Connecté en tant que {email}, {plan, select, pro {forfait Pro} max {forfait Max} team {forfait Team} other {forfait Claude}}. Les modèles Claude fonctionnent alors dans le chat, les titres et Studio.",
|
||||
"models_claude_dialog_sign_in_body": "Ouvrez un terminal, exécutez claude et connectez-vous avec votre compte Claude.",
|
||||
"models_claude_dialog_signed_out_body": "Claude Code est installé, mais n’est connecté à aucun forfait Claude. Ouvrez un terminal, exécutez claude et connectez-vous.",
|
||||
"models_claude_dialog_title": "Utiliser votre forfait Claude",
|
||||
"models_claude_dialog_too_old_body": "Claude Code {version} est trop ancien pour SurfSense. Mettez-le à jour, puis vérifiez à nouveau.",
|
||||
"models_claude_dialog_unreadable_version_body": "SurfSense n’a pas pu identifier le Claude Code installé. Mettez-le à jour, puis vérifiez à nouveau.",
|
||||
"models_claude_option_body": "Pro, Max ou Team, via Claude Code sur cet ordinateur.",
|
||||
"models_claude_option_connected_body": "Connecté via Claude Code sur cet ordinateur",
|
||||
"models_claude_option_set_up_button": "Configurer",
|
||||
"models_claude_option_show_button": "Afficher les modèles",
|
||||
"models_claude_option_title": "Utiliser votre forfait Claude",
|
||||
"models_claude_plan_edit_body": "Fonctionne via Claude Code sur cet ordinateur, qui conserve votre connexion. Pour vous connecter, vous déconnecter ou changer de compte, exécutez claude dans un terminal.",
|
||||
"models_claude_plan_edit_cancel_button": "Annuler",
|
||||
"models_claude_plan_edit_save_button": "Enregistrer les modifications",
|
||||
"models_connection_dialog_add_body": "Tout serveur qui implémente l’API OpenAI, comme vLLM, LM Studio ou OpenRouter.",
|
||||
"models_connection_dialog_add_title": "Connecter un serveur",
|
||||
"models_connection_dialog_edit_body": "Les serveurs sont partagés par tous les types de modèles, une modification ici s’applique donc à tous.",
|
||||
|
|
@ -431,6 +464,8 @@
|
|||
"models_connection_form_cancel_button": "Annuler",
|
||||
"models_connection_form_chatgpt_provider_body": "Se connecter avec votre compte ChatGPT",
|
||||
"models_connection_form_chatgpt_provider_label": "Abonnement ChatGPT",
|
||||
"models_connection_form_claude_plan_provider_body": "Via Claude Code sur cet ordinateur",
|
||||
"models_connection_form_claude_plan_provider_label": "Forfait Claude",
|
||||
"models_connection_form_clear_key_label": "Supprimer la clé API enregistrée",
|
||||
"models_connection_form_custom_provider_body": "Toute URL compatible OpenAI",
|
||||
"models_connection_form_custom_provider_label": "Serveur local ou personnalisé",
|
||||
|
|
@ -618,6 +653,7 @@
|
|||
"models_server_card_connect_button": "Connecter",
|
||||
"models_server_card_title": "Utiliser un serveur",
|
||||
"models_server_models_capability_unknown_label": "Capacité inconnue",
|
||||
"models_server_models_claude_plan_status": "Via Claude Code sur cet ordinateur",
|
||||
"models_server_models_current_in_use_button": "Actif",
|
||||
"models_server_models_edit_aria": "Modifier {server}",
|
||||
"models_server_models_edit_button": "Modifier",
|
||||
|
|
|
|||
|
|
@ -133,6 +133,8 @@
|
|||
"chat_composer_upload_sources_uploading_tooltip": "अपलोड हो रहा है…",
|
||||
"chat_error_agent_model_unsupported": "यह थ्रेड चुने गए मॉडल के साथ जारी नहीं रह सकता। इसका उपयोग करने के लिए नई चैट शुरू करें, या कोई दूसरा मॉडल चुनें।",
|
||||
"chat_error_agent_thread_outdated": "यह एजेंट चैट तब शुरू हुई थी जब हर चैट के अपने स्रोत नहीं होते थे। जारी रखने के लिए नई चैट शुरू करें।",
|
||||
"chat_error_claude_limit": "आपके Claude प्लान की उपयोग सीमा पूरी हो गई है। यह अपने आप रीसेट हो जाती है।",
|
||||
"chat_error_claude_sign_in": "इस कंप्यूटर पर Claude Code को फिर से साइन इन करना होगा: किसी टर्मिनल में claude चलाएं और साइन इन करें।",
|
||||
"chat_error_context_too_long": "यह बातचीत मॉडल की कॉन्टेक्स्ट विंडो के लिए बहुत लंबी है। नई चैट शुरू करें या बड़ी विंडो वाला मॉडल चुनें।",
|
||||
"chat_error_interrupted": "ऐप बंद होने पर जवाब रुक गया।",
|
||||
"chat_error_model_cannot_run": "SurfSense यह मॉडल नहीं चला सकता। कोई दूसरा मॉडल चुनें।",
|
||||
|
|
@ -172,6 +174,7 @@
|
|||
"chat_mode_picker_thread_aria": "चैट मोड {mode}. चैट अपना मोड बनाए रखती है; दूसरे मोड के लिए नई चैट शुरू करें।",
|
||||
"chat_mode_picker_trigger_aria": "चैट मोड {mode}. मोड बदलें.",
|
||||
"chat_model_issue_allow_button": "अनुमति दें…",
|
||||
"chat_model_issue_claude_sign_in_body": "{model} का उपयोग नहीं हो सका: इस कंप्यूटर पर Claude Code इंस्टॉल नहीं है या उसे फिर से साइन इन करना होगा।",
|
||||
"chat_model_issue_egress_off_body": "{host} पर डेटा भेजना बंद है।",
|
||||
"chat_model_issue_key_rejected_body": "{model} के प्रोवाइडर ने उसकी कुंजी अस्वीकार कर दी।",
|
||||
"chat_model_issue_rate_limited_body": "{model} का प्रोवाइडर अनुरोधों को सीमित कर रहा है। थोड़ी देर में फिर कोशिश करें।",
|
||||
|
|
@ -283,6 +286,11 @@
|
|||
"egress_app_updates_label": "ऐप अपडेट",
|
||||
"egress_app_updates_prompt_body": "जांच करने पर {host} से नवीनतम रिलीज़ पूछी जाती है। SurfSense सिर्फ़ आपका IP पता और आप जो वर्शन चला रहे हैं, वही भेजता है, और कुछ नहीं। अनुमति देने पर शुरू होते समय होने वाली जांच भी चालू हो जाती है, जिसे आप सेटिंग्स › नेटवर्क में बंद कर सकते हैं।",
|
||||
"egress_app_updates_prompt_title": "SurfSense को अपडेट देखने की अनुमति दें?",
|
||||
"egress_claude_api_host_body": "आप मॉडल को जो भेजते हैं, वह {host} को मिलता है: आपके प्रॉम्प्ट और आपके दस्तावेज़ों के अंश।",
|
||||
"egress_claude_prompt_body": "इस कंप्यूटर पर Claude Code, Anthropic तक सिर्फ़ SurfSense के ज़रिए पहुंचता है, और सिर्फ़ उन होस्ट तक जिनकी आप अनुमति देते हैं। आप इनमें से हर एक को सेटिंग्स › नेटवर्क में बंद कर सकते हैं।",
|
||||
"egress_claude_prompt_title": "अपने Claude प्लान को Anthropic तक पहुंचने की अनुमति दें?",
|
||||
"egress_claude_sign_in_host_body": "{host} Claude Code को साइन इन रखता है। इसे कोई चैट नहीं मिलती, और इसे बंद करने पर साइन-इन की अवधि ख़त्म होते ही Claude काम करना बंद कर देता है।",
|
||||
"egress_claude_sign_in_label": "Claude Code साइन-इन",
|
||||
"egress_connection_prompt_body": "इस कनेक्शन से होने वाली चैट आपके प्रॉम्प्ट और आपके दस्तावेज़ों के अंश {host} को भेजती हैं। आप इसे कभी भी सेटिंग्स › नेटवर्क में बंद कर सकते हैं।",
|
||||
"egress_connection_prompt_title": "{host} को डेटा भेजने की अनुमति दें?",
|
||||
"egress_destination_allow_aria": "{name} को अनुमति दें",
|
||||
|
|
@ -303,6 +311,7 @@
|
|||
"egress_plugin_registry_prompt_body": "SurfSense थर्ड-पार्टी प्लगइन की सूची {host} से लेता है और इसके लिए सिर्फ़ आपका IP पता भेजता है। SurfSense के अपने प्लगइन भी इसी होस्ट पर चलते हैं, इसलिए इससे आप उन्हें भी कनेक्ट कर सकते हैं। आप इसे कभी भी सेटिंग्स › नेटवर्क में बंद कर सकते हैं।",
|
||||
"egress_plugin_registry_prompt_title": "प्लगइन की सूची लेने की अनुमति दें?",
|
||||
"egress_prompt_allow_button": "अनुमति दें",
|
||||
"egress_prompt_also_allows_body": "अनुमति देने से SurfSense, {hosts} तक भी पहुंच सकेगा।",
|
||||
"egress_prompt_cancel_button": "रद्द करें",
|
||||
"embedding_settings_body": "वह मॉडल जो आपके दस्तावेज़ों को ऐसे रूप में बदलता है जिसकी सर्च तुलना कर सके।",
|
||||
"embedding_settings_fixed_body": "सेटअप के दौरान चुना गया और हर दस्तावेज़ के लिए इस्तेमाल होता है, इसलिए इसे बदला नहीं जा सकता।",
|
||||
|
|
@ -424,6 +433,30 @@
|
|||
"models_chatgpt_signed_in_unnamed_body": "साइन इन है।",
|
||||
"models_chatgpt_signed_out_body": "साइन आउट है। इस प्लान के मॉडल इस्तेमाल करने के लिए फिर से साइन इन करें।",
|
||||
"models_chatgpt_waiting_status": "अपने ब्राउज़र में साइन इन पूरा करें।",
|
||||
"models_claude_connection_label": "Claude प्लान",
|
||||
"models_claude_dialog_add_button": "Claude मॉडल जोड़ें",
|
||||
"models_claude_dialog_body": "SurfSense इस कंप्यूटर पर Claude Code के ज़रिए आपके Claude Pro, Max या Team प्लान का उपयोग करता है। आपका साइन-इन Claude Code में ही रहता है, और SurfSense उसे कभी नहीं देखता। उपयोग आपके प्लान की सीमाओं में गिना जाता है, जैसे Claude Code में गिना जाता है।",
|
||||
"models_claude_dialog_cancel_button": "रद्द करें",
|
||||
"models_claude_dialog_check_again_button": "फिर से जांचें",
|
||||
"models_claude_dialog_check_error": "इस कंप्यूटर पर Claude Code की जांच नहीं हो सकी।",
|
||||
"models_claude_dialog_checking_status": "Claude Code की जांच हो रही है",
|
||||
"models_claude_dialog_host_declined_error": "आपके Claude प्लान के लिए {host} ज़रूरी है। इसे कनेक्ट करने की अनुमति दें।",
|
||||
"models_claude_dialog_install_body": "इस कंप्यूटर पर Claude Code इंस्टॉल करें।",
|
||||
"models_claude_dialog_install_guide_button": "इंस्टॉल गाइड खोलें",
|
||||
"models_claude_dialog_ready_body": "{email} के रूप में कनेक्टेड, {plan, select, pro {Pro प्लान} max {Max प्लान} team {Team प्लान} other {Claude प्लान}}। जोड़ने के बाद Claude मॉडल चैट, शीर्षकों और Studio में काम करेंगे।",
|
||||
"models_claude_dialog_sign_in_body": "एक टर्मिनल खोलें, claude चलाएं, और अपने Claude खाते से साइन इन करें।",
|
||||
"models_claude_dialog_signed_out_body": "Claude Code इंस्टॉल है, लेकिन किसी Claude प्लान में साइन इन नहीं है। एक टर्मिनल खोलें, claude चलाएं, और साइन इन करें।",
|
||||
"models_claude_dialog_title": "अपना Claude प्लान इस्तेमाल करें",
|
||||
"models_claude_dialog_too_old_body": "Claude Code {version}, SurfSense के लिए बहुत पुराना है। इसे अपडेट करें, फिर से जांचें।",
|
||||
"models_claude_dialog_unreadable_version_body": "SurfSense यह नहीं पढ़ सका कि कौन-सा Claude Code इंस्टॉल है। इसे अपडेट करें, फिर से जांचें।",
|
||||
"models_claude_option_body": "Pro, Max या Team, इस कंप्यूटर पर Claude Code के ज़रिए।",
|
||||
"models_claude_option_connected_body": "इस कंप्यूटर पर Claude Code के ज़रिए कनेक्टेड",
|
||||
"models_claude_option_set_up_button": "सेट अप करें",
|
||||
"models_claude_option_show_button": "मॉडल दिखाएं",
|
||||
"models_claude_option_title": "अपना Claude प्लान इस्तेमाल करें",
|
||||
"models_claude_plan_edit_body": "यह इस कंप्यूटर पर Claude Code के ज़रिए चलता है, जो आपका साइन-इन रखता है। साइन इन या साइन आउट करने, या खाता बदलने के लिए किसी टर्मिनल में claude चलाएं।",
|
||||
"models_claude_plan_edit_cancel_button": "रद्द करें",
|
||||
"models_claude_plan_edit_save_button": "बदलाव सहेजें",
|
||||
"models_connection_dialog_add_body": "कोई भी सर्वर जो OpenAI API लागू करता है, जैसे vLLM, LM Studio या OpenRouter।",
|
||||
"models_connection_dialog_add_title": "सर्वर कनेक्ट करें",
|
||||
"models_connection_dialog_edit_body": "सर्वर हर मॉडल प्रकार में साझा होते हैं, इसलिए यहां किया गया बदलाव सभी पर लागू होता है।",
|
||||
|
|
@ -431,6 +464,8 @@
|
|||
"models_connection_form_cancel_button": "रद्द करें",
|
||||
"models_connection_form_chatgpt_provider_body": "अपने ChatGPT खाते से साइन इन करें",
|
||||
"models_connection_form_chatgpt_provider_label": "ChatGPT सब्सक्रिप्शन",
|
||||
"models_connection_form_claude_plan_provider_body": "इस कंप्यूटर पर Claude Code के ज़रिए",
|
||||
"models_connection_form_claude_plan_provider_label": "Claude प्लान",
|
||||
"models_connection_form_clear_key_label": "सहेजी गई API कुंजी हटाएं",
|
||||
"models_connection_form_custom_provider_body": "कोई भी OpenAI-संगत URL",
|
||||
"models_connection_form_custom_provider_label": "लोकल या कस्टम सर्वर",
|
||||
|
|
@ -618,6 +653,7 @@
|
|||
"models_server_card_connect_button": "कनेक्ट करें",
|
||||
"models_server_card_title": "सर्वर इस्तेमाल करें",
|
||||
"models_server_models_capability_unknown_label": "क्षमता अज्ञात",
|
||||
"models_server_models_claude_plan_status": "इस कंप्यूटर पर Claude Code के ज़रिए",
|
||||
"models_server_models_current_in_use_button": "उपयोग में",
|
||||
"models_server_models_edit_aria": "{server} संपादित करें",
|
||||
"models_server_models_edit_button": "संपादित करें",
|
||||
|
|
|
|||
|
|
@ -133,6 +133,8 @@
|
|||
"chat_composer_upload_sources_uploading_tooltip": "アップロード中…",
|
||||
"chat_error_agent_model_unsupported": "このスレッドは選択したモデルでは続けられません。このモデルを使うには新しいチャットを始めるか、別のモデルを選んでください。",
|
||||
"chat_error_agent_thread_outdated": "このエージェントチャットは、チャットごとにソースを持つようになる前に始まりました。続けるには新しいチャットを始めてください。",
|
||||
"chat_error_claude_limit": "Claudeプランの利用上限に達しました。上限は自動的にリセットされます。",
|
||||
"chat_error_claude_sign_in": "このコンピューターのClaude Codeで再度サインインする必要があります。ターミナルでclaudeを実行してサインインしてください。",
|
||||
"chat_error_context_too_long": "この会話はモデルのコンテキストウィンドウに収まらない長さです。新しいチャットを始めるか、より大きなウィンドウのモデルを選んでください。",
|
||||
"chat_error_interrupted": "アプリが閉じられたため中断されました。",
|
||||
"chat_error_model_cannot_run": "SurfSenseではこのモデルを実行できません。別のモデルを選んでください。",
|
||||
|
|
@ -172,6 +174,7 @@
|
|||
"chat_mode_picker_thread_aria": "チャットモード: {mode}。チャットのモードは変わりません。別のモードにするには新しいチャットを始めてください。",
|
||||
"chat_mode_picker_trigger_aria": "チャットモード: {mode}。モードを変更します。",
|
||||
"chat_model_issue_allow_button": "許可…",
|
||||
"chat_model_issue_claude_sign_in_body": "{model}を使用できませんでした。このコンピューターのClaude Codeがインストールされていないか、再度サインインする必要があります。",
|
||||
"chat_model_issue_egress_off_body": "{host}へのデータ送信はオフになっています。",
|
||||
"chat_model_issue_key_rejected_body": "{model}のプロバイダーがキーを拒否しました。",
|
||||
"chat_model_issue_rate_limited_body": "{model}のプロバイダーがリクエストを制限しています。しばらくしてからもう一度お試しください。",
|
||||
|
|
@ -283,6 +286,11 @@
|
|||
"egress_app_updates_label": "アプリのアップデート",
|
||||
"egress_app_updates_prompt_body": "確認時には{host}に最新リリースを問い合わせます。SurfSenseが送信するのはIPアドレスと実行中のバージョンのみです。許可すると起動時の確認も有効になります。これは設定 › ネットワークでオフにできます。",
|
||||
"egress_app_updates_prompt_title": "SurfSenseにアップデートの確認を許可しますか?",
|
||||
"egress_claude_api_host_body": "{host}は、モデルに送信する内容(プロンプトとドキュメントの抜粋)を受け取ります。",
|
||||
"egress_claude_prompt_body": "このコンピューターのClaude Codeは、SurfSenseを経由してのみAnthropicに接続し、接続先は許可したホストに限られます。各ホストは設定 › ネットワークでオフにできます。",
|
||||
"egress_claude_prompt_title": "ClaudeプランからAnthropicへの接続を許可しますか?",
|
||||
"egress_claude_sign_in_host_body": "{host}はClaude Codeのサインイン状態を維持します。チャットは送信されません。オフにすると、サインインの有効期限が切れた時点でClaudeは動作しなくなります。",
|
||||
"egress_claude_sign_in_label": "Claude Codeのサインイン",
|
||||
"egress_connection_prompt_body": "この接続を使うチャットは、プロンプトとドキュメントの抜粋を{host}に送信します。設定 › ネットワークでいつでもオフにできます。",
|
||||
"egress_connection_prompt_title": "{host}へのデータ送信を許可しますか?",
|
||||
"egress_destination_allow_aria": "{name}を許可",
|
||||
|
|
@ -303,6 +311,7 @@
|
|||
"egress_plugin_registry_prompt_body": "SurfSenseはサードパーティのプラグインの一覧を{host}から取得します。送信するのはIPアドレスだけです。SurfSenseのプラグインも同じホストで動いているため、これらも接続できるようになります。設定 › ネットワークでいつでもオフにできます。",
|
||||
"egress_plugin_registry_prompt_title": "プラグインの一覧の取得を許可しますか?",
|
||||
"egress_prompt_allow_button": "許可",
|
||||
"egress_prompt_also_allows_body": "許可すると、SurfSenseは{hosts}にも接続できるようになります。",
|
||||
"egress_prompt_cancel_button": "キャンセル",
|
||||
"embedding_settings_body": "ドキュメントを検索で比較できる形に変換するモデルです。",
|
||||
"embedding_settings_fixed_body": "セットアップ時に選択され、すべてのドキュメントに使われているため、変更できません。",
|
||||
|
|
@ -424,6 +433,30 @@
|
|||
"models_chatgpt_signed_in_unnamed_body": "サインインしています。",
|
||||
"models_chatgpt_signed_out_body": "サインアウトしています。このプランのモデルを使うには再度サインインしてください。",
|
||||
"models_chatgpt_waiting_status": "ブラウザーでサインインを完了してください。",
|
||||
"models_claude_connection_label": "Claudeプラン",
|
||||
"models_claude_dialog_add_button": "Claudeモデルを追加",
|
||||
"models_claude_dialog_body": "SurfSenseは、このコンピューターのClaude Codeを通じてClaude Pro、MaxまたはTeamプランを使用します。サインイン情報はClaude Codeに残り、SurfSenseがそれを見ることはありません。Claude Codeと同様に、利用量はプランの上限にカウントされます。",
|
||||
"models_claude_dialog_cancel_button": "キャンセル",
|
||||
"models_claude_dialog_check_again_button": "再確認",
|
||||
"models_claude_dialog_check_error": "このコンピューターのClaude Codeを確認できませんでした。",
|
||||
"models_claude_dialog_checking_status": "Claude Codeを確認中",
|
||||
"models_claude_dialog_host_declined_error": "Claudeプランには{host}が必要です。接続を許可してください。",
|
||||
"models_claude_dialog_install_body": "このコンピューターにClaude Codeをインストールしてください。",
|
||||
"models_claude_dialog_install_guide_button": "インストールガイドを開く",
|
||||
"models_claude_dialog_ready_body": "{email}として接続済みです({plan, select, pro {Proプラン} max {Maxプラン} team {Teamプラン} other {Claudeプラン}})。追加すると、Claudeモデルをチャット、タイトル、Studioで使用できます。",
|
||||
"models_claude_dialog_sign_in_body": "ターミナルを開き、claudeを実行して、Claudeアカウントでサインインしてください。",
|
||||
"models_claude_dialog_signed_out_body": "Claude Codeはインストールされていますが、Claudeプランにサインインしていません。ターミナルを開き、claudeを実行してサインインしてください。",
|
||||
"models_claude_dialog_title": "Claudeプランを使う",
|
||||
"models_claude_dialog_too_old_body": "Claude Code {version}はSurfSenseで使うには古すぎます。更新してから、もう一度確認してください。",
|
||||
"models_claude_dialog_unreadable_version_body": "SurfSenseは、インストールされているClaude Codeのバージョンを読み取れませんでした。更新してから、もう一度確認してください。",
|
||||
"models_claude_option_body": "Pro、MaxまたはTeam。このコンピューターのClaude Codeを通じて使用します。",
|
||||
"models_claude_option_connected_body": "このコンピューターのClaude Codeで接続済み",
|
||||
"models_claude_option_set_up_button": "設定",
|
||||
"models_claude_option_show_button": "モデルを表示",
|
||||
"models_claude_option_title": "Claudeプランを使う",
|
||||
"models_claude_plan_edit_body": "サインイン情報を保持する、このコンピューターのClaude Codeを通じて動作します。サインイン、サインアウト、アカウントの切り替えは、ターミナルでclaudeを実行して行ってください。",
|
||||
"models_claude_plan_edit_cancel_button": "キャンセル",
|
||||
"models_claude_plan_edit_save_button": "変更を保存",
|
||||
"models_connection_dialog_add_body": "vLLM、LM Studio、OpenRouterなど、OpenAI APIを実装したサーバーであれば接続できます。",
|
||||
"models_connection_dialog_add_title": "サーバーを接続",
|
||||
"models_connection_dialog_edit_body": "サーバーはすべてのモデルの種類で共有されるため、ここでの変更はすべてに適用されます。",
|
||||
|
|
@ -431,6 +464,8 @@
|
|||
"models_connection_form_cancel_button": "キャンセル",
|
||||
"models_connection_form_chatgpt_provider_body": "ChatGPTアカウントでサインイン",
|
||||
"models_connection_form_chatgpt_provider_label": "ChatGPTサブスクリプション",
|
||||
"models_connection_form_claude_plan_provider_body": "このコンピューターのClaude Code経由",
|
||||
"models_connection_form_claude_plan_provider_label": "Claudeプラン",
|
||||
"models_connection_form_clear_key_label": "保存済みのAPIキーを削除",
|
||||
"models_connection_form_custom_provider_body": "OpenAI互換の任意のURL",
|
||||
"models_connection_form_custom_provider_label": "ローカルまたはカスタムサーバー",
|
||||
|
|
@ -618,6 +653,7 @@
|
|||
"models_server_card_connect_button": "接続",
|
||||
"models_server_card_title": "サーバーを使用",
|
||||
"models_server_models_capability_unknown_label": "機能不明",
|
||||
"models_server_models_claude_plan_status": "このコンピューターのClaude Code経由",
|
||||
"models_server_models_current_in_use_button": "使用中",
|
||||
"models_server_models_edit_aria": "{server}を編集",
|
||||
"models_server_models_edit_button": "編集",
|
||||
|
|
|
|||
|
|
@ -133,6 +133,8 @@
|
|||
"chat_composer_upload_sources_uploading_tooltip": "업로드 중...",
|
||||
"chat_error_agent_model_unsupported": "이 스레드는 선택한 모델로 계속할 수 없습니다. 이 모델을 쓰려면 새 채팅을 시작하거나 다른 모델을 선택하세요.",
|
||||
"chat_error_agent_thread_outdated": "이 에이전트 채팅은 채팅마다 자체 소스를 갖기 전에 시작되었습니다. 계속하려면 새 채팅을 시작하세요.",
|
||||
"chat_error_claude_limit": "Claude 플랜의 사용 한도에 도달했습니다. 한도는 자동으로 초기화됩니다.",
|
||||
"chat_error_claude_sign_in": "이 컴퓨터의 Claude Code에 다시 로그인해야 합니다. 터미널에서 claude를 실행하고 로그인하세요.",
|
||||
"chat_error_context_too_long": "이 대화는 모델의 컨텍스트 창에 담기에 너무 깁니다. 새 채팅을 시작하거나 더 큰 창을 가진 모델을 선택하세요.",
|
||||
"chat_error_interrupted": "앱이 종료되어 중단되었습니다.",
|
||||
"chat_error_model_cannot_run": "SurfSense는 이 모델을 실행할 수 없습니다. 다른 모델을 선택하세요.",
|
||||
|
|
@ -172,6 +174,7 @@
|
|||
"chat_mode_picker_thread_aria": "채팅 모드 {mode}. 채팅은 모드를 유지합니다. 다른 모드를 쓰려면 새 채팅을 시작하세요.",
|
||||
"chat_mode_picker_trigger_aria": "채팅 모드 {mode}. 모드를 변경합니다.",
|
||||
"chat_model_issue_allow_button": "허용…",
|
||||
"chat_model_issue_claude_sign_in_body": "{model}을(를) 사용할 수 없습니다. 이 컴퓨터에 Claude Code가 설치되어 있지 않거나 다시 로그인해야 합니다.",
|
||||
"chat_model_issue_egress_off_body": "{host}에 대한 데이터 전송이 꺼져 있습니다.",
|
||||
"chat_model_issue_key_rejected_body": "{model}의 제공업체가 키를 거부했습니다.",
|
||||
"chat_model_issue_rate_limited_body": "{model}의 제공업체가 요청을 제한하고 있습니다. 잠시 후 다시 시도하세요.",
|
||||
|
|
@ -283,6 +286,11 @@
|
|||
"egress_app_updates_label": "앱 업데이트",
|
||||
"egress_app_updates_prompt_body": "확인할 때 {host}에 최신 릴리스를 요청합니다. SurfSense는 IP 주소와 실행 중인 버전만 전송하며, 그 밖의 정보는 전송하지 않습니다. 허용하면 시작 시 확인도 켜지며, 설정 › 네트워크에서 끌 수 있습니다.",
|
||||
"egress_app_updates_prompt_title": "SurfSense가 업데이트를 확인하도록 허용하시겠습니까?",
|
||||
"egress_claude_api_host_body": "{host}은(는) 모델에 보내는 내용, 즉 프롬프트와 문서 발췌를 받습니다.",
|
||||
"egress_claude_prompt_body": "이 컴퓨터의 Claude Code는 SurfSense를 통해서만, 허용한 호스트로만 Anthropic에 접속합니다. 각 호스트는 설정 › 네트워크에서 끌 수 있습니다.",
|
||||
"egress_claude_prompt_title": "Claude 플랜이 Anthropic에 접속하도록 허용할까요?",
|
||||
"egress_claude_sign_in_host_body": "{host}은(는) Claude Code의 로그인 상태를 유지합니다. 채팅은 전송되지 않으며, 끄면 로그인이 만료되는 시점에 Claude가 작동을 멈춥니다.",
|
||||
"egress_claude_sign_in_label": "Claude Code 로그인",
|
||||
"egress_connection_prompt_body": "이 연결을 사용하는 채팅은 프롬프트와 문서 발췌를 {host}에 전송합니다. 설정 › 네트워크에서 언제든지 끌 수 있습니다.",
|
||||
"egress_connection_prompt_title": "{host}에 데이터 전송을 허용하시겠습니까?",
|
||||
"egress_destination_allow_aria": "{name} 허용",
|
||||
|
|
@ -303,6 +311,7 @@
|
|||
"egress_plugin_registry_prompt_body": "SurfSense는 {host}에서 서드파티 플러그인 목록을 가져오며, IP 주소 외에는 아무것도 전송하지 않습니다. SurfSense 플러그인도 같은 호스트에서 실행되므로 이를 허용하면 SurfSense 플러그인도 연결할 수 있습니다. 설정 › 네트워크에서 언제든지 끌 수 있습니다.",
|
||||
"egress_plugin_registry_prompt_title": "플러그인 목록 가져오기를 허용할까요?",
|
||||
"egress_prompt_allow_button": "허용",
|
||||
"egress_prompt_also_allows_body": "허용하면 SurfSense가 {hosts}에도 접속할 수 있습니다.",
|
||||
"egress_prompt_cancel_button": "취소",
|
||||
"embedding_settings_body": "문서를 검색에서 비교할 수 있는 형태로 바꾸는 모델입니다.",
|
||||
"embedding_settings_fixed_body": "설정 중에 선택되어 모든 문서에 사용되므로 변경할 수 없습니다.",
|
||||
|
|
@ -424,6 +433,30 @@
|
|||
"models_chatgpt_signed_in_unnamed_body": "로그인되어 있습니다.",
|
||||
"models_chatgpt_signed_out_body": "로그아웃되었습니다. 이 플랜의 모델을 사용하려면 다시 로그인하세요.",
|
||||
"models_chatgpt_waiting_status": "브라우저에서 로그인을 완료하세요.",
|
||||
"models_claude_connection_label": "Claude 플랜",
|
||||
"models_claude_dialog_add_button": "Claude 모델 추가",
|
||||
"models_claude_dialog_body": "SurfSense는 이 컴퓨터의 Claude Code를 통해 Claude Pro, Max 또는 Team 플랜을 사용합니다. 로그인 정보는 Claude Code에 남으며 SurfSense는 이를 볼 수 없습니다. Claude Code에서와 마찬가지로 사용량은 플랜 한도에 포함됩니다.",
|
||||
"models_claude_dialog_cancel_button": "취소",
|
||||
"models_claude_dialog_check_again_button": "다시 확인",
|
||||
"models_claude_dialog_check_error": "이 컴퓨터의 Claude Code를 확인할 수 없습니다.",
|
||||
"models_claude_dialog_checking_status": "Claude Code 확인 중",
|
||||
"models_claude_dialog_host_declined_error": "Claude 플랜에는 {host}이(가) 필요합니다. 연결을 허용하세요.",
|
||||
"models_claude_dialog_install_body": "이 컴퓨터에 Claude Code를 설치하세요.",
|
||||
"models_claude_dialog_install_guide_button": "설치 가이드 열기",
|
||||
"models_claude_dialog_ready_body": "{email}(으)로 연결되었습니다({plan, select, pro {Pro 플랜} max {Max 플랜} team {Team 플랜} other {Claude 플랜}}). 추가하면 채팅, 제목, Studio에서 Claude 모델을 사용할 수 있습니다.",
|
||||
"models_claude_dialog_sign_in_body": "터미널을 열고 claude를 실행한 다음 Claude 계정으로 로그인하세요.",
|
||||
"models_claude_dialog_signed_out_body": "Claude Code는 설치되어 있지만 Claude 플랜에 로그인되어 있지 않습니다. 터미널을 열고 claude를 실행한 다음 로그인하세요.",
|
||||
"models_claude_dialog_title": "Claude 플랜 사용",
|
||||
"models_claude_dialog_too_old_body": "Claude Code {version}은(는) SurfSense에서 사용하기에 너무 오래된 버전입니다. 업데이트한 다음 다시 확인하세요.",
|
||||
"models_claude_dialog_unreadable_version_body": "SurfSense가 설치된 Claude Code의 버전을 읽을 수 없습니다. 업데이트한 다음 다시 확인하세요.",
|
||||
"models_claude_option_body": "Pro, Max 또는 Team. 이 컴퓨터의 Claude Code를 통해 사용합니다.",
|
||||
"models_claude_option_connected_body": "이 컴퓨터의 Claude Code를 통해 연결됨",
|
||||
"models_claude_option_set_up_button": "설정",
|
||||
"models_claude_option_show_button": "모델 표시",
|
||||
"models_claude_option_title": "Claude 플랜 사용",
|
||||
"models_claude_plan_edit_body": "로그인 정보를 보관하는 이 컴퓨터의 Claude Code를 통해 실행됩니다. 로그인, 로그아웃 또는 계정 전환을 하려면 터미널에서 claude를 실행하세요.",
|
||||
"models_claude_plan_edit_cancel_button": "취소",
|
||||
"models_claude_plan_edit_save_button": "변경 사항 저장",
|
||||
"models_connection_dialog_add_body": "vLLM, LM Studio, OpenRouter처럼 OpenAI API를 구현한 모든 서버입니다.",
|
||||
"models_connection_dialog_add_title": "서버 연결",
|
||||
"models_connection_dialog_edit_body": "서버는 모든 모델 유형에서 공유되므로, 여기서 변경하면 전체에 적용됩니다.",
|
||||
|
|
@ -431,6 +464,8 @@
|
|||
"models_connection_form_cancel_button": "취소",
|
||||
"models_connection_form_chatgpt_provider_body": "ChatGPT 계정으로 로그인",
|
||||
"models_connection_form_chatgpt_provider_label": "ChatGPT 구독",
|
||||
"models_connection_form_claude_plan_provider_body": "이 컴퓨터의 Claude Code를 통해",
|
||||
"models_connection_form_claude_plan_provider_label": "Claude 플랜",
|
||||
"models_connection_form_clear_key_label": "저장된 API 키 제거",
|
||||
"models_connection_form_custom_provider_body": "OpenAI 호환 URL이면 모두 가능",
|
||||
"models_connection_form_custom_provider_label": "로컬 또는 사용자 지정 서버",
|
||||
|
|
@ -618,6 +653,7 @@
|
|||
"models_server_card_connect_button": "연결",
|
||||
"models_server_card_title": "서버 사용",
|
||||
"models_server_models_capability_unknown_label": "기능 알 수 없음",
|
||||
"models_server_models_claude_plan_status": "이 컴퓨터의 Claude Code를 통해",
|
||||
"models_server_models_current_in_use_button": "사용 중",
|
||||
"models_server_models_edit_aria": "{server} 편집",
|
||||
"models_server_models_edit_button": "편집",
|
||||
|
|
|
|||
|
|
@ -133,6 +133,8 @@
|
|||
"chat_composer_upload_sources_uploading_tooltip": "Enviando...",
|
||||
"chat_error_agent_model_unsupported": "Esta conversa não pode continuar com o modelo selecionado. Inicie um novo chat para usá-lo ou escolha outro modelo.",
|
||||
"chat_error_agent_thread_outdated": "Este chat do agente foi iniciado antes de cada chat ter suas próprias fontes. Inicie um novo chat para continuar.",
|
||||
"chat_error_claude_limit": "O limite de uso do seu plano do Claude foi atingido. Ele é redefinido sozinho.",
|
||||
"chat_error_claude_sign_in": "O Claude Code neste computador precisa entrar novamente: execute claude em um terminal e entre.",
|
||||
"chat_error_context_too_long": "Esta conversa é longa demais para a janela de contexto do modelo. Inicie um novo chat ou escolha um modelo com uma janela maior.",
|
||||
"chat_error_interrupted": "Interrompida quando o app foi fechado.",
|
||||
"chat_error_model_cannot_run": "O SurfSense não consegue executar este modelo. Escolha outro modelo.",
|
||||
|
|
@ -172,6 +174,7 @@
|
|||
"chat_mode_picker_thread_aria": "Modo do chat {mode}. Um chat mantém o modo; inicie um novo chat para usar outro.",
|
||||
"chat_mode_picker_trigger_aria": "Modo do chat {mode}. Trocar modo.",
|
||||
"chat_model_issue_allow_button": "Permitir…",
|
||||
"chat_model_issue_claude_sign_in_body": "Não foi possível usar {model}: o Claude Code neste computador não está instalado ou precisa entrar novamente.",
|
||||
"chat_model_issue_egress_off_body": "O envio de dados para {host} está desativado.",
|
||||
"chat_model_issue_key_rejected_body": "O provedor de {model} rejeitou a chave.",
|
||||
"chat_model_issue_rate_limited_body": "O provedor de {model} está limitando as solicitações. Tente novamente em instantes.",
|
||||
|
|
@ -283,6 +286,11 @@
|
|||
"egress_app_updates_label": "Atualizações do app",
|
||||
"egress_app_updates_prompt_body": "A verificação consulta {host} sobre a versão mais recente. O SurfSense envia seu endereço IP e a versão em uso, nada além disso. Permitir também ativa a verificação na inicialização, que você pode desativar em Configurações › Rede.",
|
||||
"egress_app_updates_prompt_title": "Permitir que o SurfSense verifique atualizações?",
|
||||
"egress_claude_api_host_body": "{host} recebe o que você envia ao modelo: seus prompts e trechos dos seus documentos.",
|
||||
"egress_claude_prompt_body": "O Claude Code neste computador acessa a Anthropic só pelo SurfSense, e só pelos hosts que você permitir. Você pode desativar cada um em Configurações › Rede.",
|
||||
"egress_claude_prompt_title": "Permitir que seu plano do Claude acesse a Anthropic?",
|
||||
"egress_claude_sign_in_host_body": "{host} mantém o Claude Code conectado. Ele não recebe nenhum chat, e desativá-lo faz o Claude parar quando o login expirar.",
|
||||
"egress_claude_sign_in_label": "Login do Claude Code",
|
||||
"egress_connection_prompt_body": "Os chats que usam esta conexão enviam seus prompts e trechos dos seus documentos para {host}. Você pode desativar isso quando quiser em Configurações › Rede.",
|
||||
"egress_connection_prompt_title": "Permitir o envio de dados para {host}?",
|
||||
"egress_destination_allow_aria": "Permitir {name}",
|
||||
|
|
@ -303,6 +311,7 @@
|
|||
"egress_plugin_registry_prompt_body": "O SurfSense baixa a lista de plugins de terceiros de {host}, enviando só seu endereço IP. Os plugins da SurfSense rodam no mesmo host, então isso também permite conectá-los. Você pode desativar isso quando quiser em Configurações › Rede.",
|
||||
"egress_plugin_registry_prompt_title": "Permitir baixar a lista de plugins?",
|
||||
"egress_prompt_allow_button": "Permitir",
|
||||
"egress_prompt_also_allows_body": "Permitir também deixa o SurfSense acessar {hosts}.",
|
||||
"egress_prompt_cancel_button": "Cancelar",
|
||||
"embedding_settings_body": "O modelo que transforma seus documentos em algo que a busca consegue comparar.",
|
||||
"embedding_settings_fixed_body": "Escolhido durante a configuração inicial e usado em todos os documentos, por isso não pode ser alterado.",
|
||||
|
|
@ -424,6 +433,30 @@
|
|||
"models_chatgpt_signed_in_unnamed_body": "Conectado.",
|
||||
"models_chatgpt_signed_out_body": "Desconectado. Entre novamente para usar os modelos deste plano.",
|
||||
"models_chatgpt_waiting_status": "Conclua o login no seu navegador.",
|
||||
"models_claude_connection_label": "Plano do Claude",
|
||||
"models_claude_dialog_add_button": "Adicionar modelos do Claude",
|
||||
"models_claude_dialog_body": "O SurfSense usa seu plano Claude Pro, Max ou Team pelo Claude Code neste computador. Seu login fica no Claude Code, e o SurfSense nunca o vê. O uso conta para os limites do seu plano, como acontece no Claude Code.",
|
||||
"models_claude_dialog_cancel_button": "Cancelar",
|
||||
"models_claude_dialog_check_again_button": "Verificar de novo",
|
||||
"models_claude_dialog_check_error": "Não foi possível verificar o Claude Code neste computador.",
|
||||
"models_claude_dialog_checking_status": "Verificando o Claude Code",
|
||||
"models_claude_dialog_host_declined_error": "Seu plano do Claude precisa de {host}. Permita que ele se conecte.",
|
||||
"models_claude_dialog_install_body": "Instale o Claude Code neste computador.",
|
||||
"models_claude_dialog_install_guide_button": "Abrir guia de instalação",
|
||||
"models_claude_dialog_ready_body": "Conectado como {email}, {plan, select, pro {plano Pro} max {plano Max} team {plano Team} other {plano do Claude}}. Os modelos do Claude passam a funcionar no chat, nos títulos e no Studio.",
|
||||
"models_claude_dialog_sign_in_body": "Abra um terminal, execute claude e entre com sua conta do Claude.",
|
||||
"models_claude_dialog_signed_out_body": "O Claude Code está instalado, mas não está conectado a um plano do Claude. Abra um terminal, execute claude e entre.",
|
||||
"models_claude_dialog_title": "Usar seu plano do Claude",
|
||||
"models_claude_dialog_too_old_body": "O Claude Code {version} é antigo demais para o SurfSense. Atualize-o e verifique de novo.",
|
||||
"models_claude_dialog_unreadable_version_body": "O SurfSense não conseguiu identificar qual Claude Code está instalado. Atualize-o e verifique de novo.",
|
||||
"models_claude_option_body": "Pro, Max ou Team, pelo Claude Code neste computador.",
|
||||
"models_claude_option_connected_body": "Conectado pelo Claude Code neste computador",
|
||||
"models_claude_option_set_up_button": "Configurar",
|
||||
"models_claude_option_show_button": "Mostrar modelos",
|
||||
"models_claude_option_title": "Usar seu plano do Claude",
|
||||
"models_claude_plan_edit_body": "Funciona pelo Claude Code neste computador, que guarda seu login. Para entrar, sair ou trocar de conta, execute claude em um terminal.",
|
||||
"models_claude_plan_edit_cancel_button": "Cancelar",
|
||||
"models_claude_plan_edit_save_button": "Salvar alterações",
|
||||
"models_connection_dialog_add_body": "Qualquer servidor que implemente a API da OpenAI, como vLLM, LM Studio ou OpenRouter.",
|
||||
"models_connection_dialog_add_title": "Conectar um servidor",
|
||||
"models_connection_dialog_edit_body": "Os servidores são compartilhados por todos os tipos de modelo, então uma mudança aqui vale para todos.",
|
||||
|
|
@ -431,6 +464,8 @@
|
|||
"models_connection_form_cancel_button": "Cancelar",
|
||||
"models_connection_form_chatgpt_provider_body": "Entre com sua conta do ChatGPT",
|
||||
"models_connection_form_chatgpt_provider_label": "Assinatura do ChatGPT",
|
||||
"models_connection_form_claude_plan_provider_body": "Pelo Claude Code neste computador",
|
||||
"models_connection_form_claude_plan_provider_label": "Plano do Claude",
|
||||
"models_connection_form_clear_key_label": "Remover a chave de API salva",
|
||||
"models_connection_form_custom_provider_body": "Qualquer URL compatível com a OpenAI",
|
||||
"models_connection_form_custom_provider_label": "Servidor local ou personalizado",
|
||||
|
|
@ -618,6 +653,7 @@
|
|||
"models_server_card_connect_button": "Conectar",
|
||||
"models_server_card_title": "Usar um servidor",
|
||||
"models_server_models_capability_unknown_label": "Capacidade desconhecida",
|
||||
"models_server_models_claude_plan_status": "Pelo Claude Code neste computador",
|
||||
"models_server_models_current_in_use_button": "Em uso",
|
||||
"models_server_models_edit_aria": "Editar {server}",
|
||||
"models_server_models_edit_button": "Editar",
|
||||
|
|
|
|||
|
|
@ -133,6 +133,8 @@
|
|||
"chat_composer_upload_sources_uploading_tooltip": "Загрузка…",
|
||||
"chat_error_agent_model_unsupported": "Эту ветку нельзя продолжить с выбранной моделью. Начните новый чат, чтобы использовать её, или выберите другую модель.",
|
||||
"chat_error_agent_thread_outdated": "Этот чат с агентом начат до того, как у каждого чата появились свои источники. Чтобы продолжить, начните новый чат.",
|
||||
"chat_error_claude_limit": "Достигнут лимит использования вашего плана Claude. Он сбрасывается сам.",
|
||||
"chat_error_claude_sign_in": "Claude Code на этом компьютере нужно снова войти: запустите claude в терминале и войдите.",
|
||||
"chat_error_context_too_long": "Этот разговор слишком длинный для контекстного окна модели. Начните новый чат или выберите модель с окном большего размера.",
|
||||
"chat_error_interrupted": "Прервано при закрытии приложения.",
|
||||
"chat_error_model_cannot_run": "SurfSense не может запустить эту модель. Выберите другую модель.",
|
||||
|
|
@ -172,6 +174,7 @@
|
|||
"chat_mode_picker_thread_aria": "Режим чата: {mode}. Чат сохраняет свой режим; для другого начните новый чат.",
|
||||
"chat_mode_picker_trigger_aria": "Режим чата: {mode}. Сменить режим.",
|
||||
"chat_model_issue_allow_button": "Разрешить…",
|
||||
"chat_model_issue_claude_sign_in_body": "Не удалось использовать {model}: Claude Code на этом компьютере не установлен или ему нужно снова войти.",
|
||||
"chat_model_issue_egress_off_body": "Отправка данных на {host} отключена.",
|
||||
"chat_model_issue_key_rejected_body": "Провайдер модели {model} отклонил её ключ.",
|
||||
"chat_model_issue_rate_limited_body": "Провайдер модели {model} ограничивает запросы. Попробуйте ещё раз через минуту.",
|
||||
|
|
@ -283,6 +286,11 @@
|
|||
"egress_app_updates_label": "Обновления приложения",
|
||||
"egress_app_updates_prompt_body": "При проверке SurfSense запрашивает у {host} последний выпуск. Отправляются только ваш IP-адрес и версия, которую вы используете, — больше ничего. Разрешение также включает проверку при запуске; её можно отключить в разделе Настройки › Сеть.",
|
||||
"egress_app_updates_prompt_title": "Разрешить SurfSense проверять обновления?",
|
||||
"egress_claude_api_host_body": "{host} получает то, что вы отправляете модели: ваши промпты и фрагменты ваших документов.",
|
||||
"egress_claude_prompt_body": "Claude Code на этом компьютере обращается к Anthropic только через SurfSense и только к тем хостам, которые вы разрешите. Каждый из них можно отключить в разделе Настройки › Сеть.",
|
||||
"egress_claude_prompt_title": "Разрешить вашему плану Claude обращаться к Anthropic?",
|
||||
"egress_claude_sign_in_host_body": "{host} поддерживает вход в Claude Code. Он не получает чаты, а если его отключить, Claude перестанет работать, когда истечёт срок входа.",
|
||||
"egress_claude_sign_in_label": "Вход в Claude Code",
|
||||
"egress_connection_prompt_body": "Чаты через это подключение отправляют ваши промпты и фрагменты ваших документов на {host}. Это можно в любой момент отключить в разделе Настройки › Сеть.",
|
||||
"egress_connection_prompt_title": "Разрешить отправку данных на {host}?",
|
||||
"egress_destination_allow_aria": "Разрешить {name}",
|
||||
|
|
@ -303,6 +311,7 @@
|
|||
"egress_plugin_registry_prompt_body": "SurfSense загружает список сторонних плагинов с {host}, передавая только ваш IP-адрес. Плагины SurfSense работают на том же хосте, поэтому так вы сможете подключать и их. Это можно в любой момент отключить в разделе Настройки › Сеть.",
|
||||
"egress_plugin_registry_prompt_title": "Разрешить загрузку списка плагинов?",
|
||||
"egress_prompt_allow_button": "Разрешить",
|
||||
"egress_prompt_also_allows_body": "Разрешение также позволит SurfSense обращаться к {hosts}.",
|
||||
"egress_prompt_cancel_button": "Отмена",
|
||||
"embedding_settings_body": "Модель, которая превращает ваши документы в то, что поиск может сравнивать.",
|
||||
"embedding_settings_fixed_body": "Выбрана при первоначальной настройке и используется для всех документов, поэтому её нельзя изменить.",
|
||||
|
|
@ -424,6 +433,30 @@
|
|||
"models_chatgpt_signed_in_unnamed_body": "Вход выполнен.",
|
||||
"models_chatgpt_signed_out_body": "Выход выполнен. Войдите снова, чтобы использовать модели этого плана.",
|
||||
"models_chatgpt_waiting_status": "Завершите вход в браузере.",
|
||||
"models_claude_connection_label": "План Claude",
|
||||
"models_claude_dialog_add_button": "Добавить модели Claude",
|
||||
"models_claude_dialog_body": "SurfSense использует ваш план Claude Pro, Max или Team через Claude Code на этом компьютере. Данные для входа остаются в Claude Code, и SurfSense их не видит. Использование расходует лимиты вашего плана, как и в Claude Code.",
|
||||
"models_claude_dialog_cancel_button": "Отмена",
|
||||
"models_claude_dialog_check_again_button": "Проверить снова",
|
||||
"models_claude_dialog_check_error": "Не удалось проверить Claude Code на этом компьютере.",
|
||||
"models_claude_dialog_checking_status": "Проверка Claude Code",
|
||||
"models_claude_dialog_host_declined_error": "Вашему плану Claude нужен {host}. Разрешите подключение.",
|
||||
"models_claude_dialog_install_body": "Установите Claude Code на этот компьютер.",
|
||||
"models_claude_dialog_install_guide_button": "Открыть инструкцию",
|
||||
"models_claude_dialog_ready_body": "Подключено: {email}, {plan, select, pro {план Pro} max {план Max} team {план Team} other {план Claude}}. Теперь модели Claude работают в чате, заголовках и Studio.",
|
||||
"models_claude_dialog_sign_in_body": "Откройте терминал, запустите claude и войдите в аккаунт Claude.",
|
||||
"models_claude_dialog_signed_out_body": "Claude Code установлен, но не подключён к плану Claude. Откройте терминал, запустите claude и войдите.",
|
||||
"models_claude_dialog_title": "Использовать план Claude",
|
||||
"models_claude_dialog_too_old_body": "Claude Code {version} слишком старый для SurfSense. Обновите его и проверьте снова.",
|
||||
"models_claude_dialog_unreadable_version_body": "SurfSense не удалось определить, какой Claude Code установлен. Обновите его и проверьте снова.",
|
||||
"models_claude_option_body": "Pro, Max или Team через Claude Code на этом компьютере.",
|
||||
"models_claude_option_connected_body": "Подключено через Claude Code на этом компьютере",
|
||||
"models_claude_option_set_up_button": "Настроить",
|
||||
"models_claude_option_show_button": "Показать модели",
|
||||
"models_claude_option_title": "Использовать план Claude",
|
||||
"models_claude_plan_edit_body": "Работает через Claude Code на этом компьютере, который хранит данные для входа. Чтобы войти, выйти или сменить аккаунт, запустите claude в терминале.",
|
||||
"models_claude_plan_edit_cancel_button": "Отмена",
|
||||
"models_claude_plan_edit_save_button": "Сохранить изменения",
|
||||
"models_connection_dialog_add_body": "Любой сервер, реализующий OpenAI API: например, vLLM, LM Studio или OpenRouter.",
|
||||
"models_connection_dialog_add_title": "Подключить сервер",
|
||||
"models_connection_dialog_edit_body": "Серверы общие для всех типов моделей, поэтому изменение здесь затронет их все.",
|
||||
|
|
@ -431,6 +464,8 @@
|
|||
"models_connection_form_cancel_button": "Отмена",
|
||||
"models_connection_form_chatgpt_provider_body": "Войти через аккаунт ChatGPT",
|
||||
"models_connection_form_chatgpt_provider_label": "Подписка ChatGPT",
|
||||
"models_connection_form_claude_plan_provider_body": "Через Claude Code на этом компьютере",
|
||||
"models_connection_form_claude_plan_provider_label": "План Claude",
|
||||
"models_connection_form_clear_key_label": "Удалить сохранённый ключ API",
|
||||
"models_connection_form_custom_provider_body": "Любой URL, совместимый с OpenAI",
|
||||
"models_connection_form_custom_provider_label": "Локальный или свой сервер",
|
||||
|
|
@ -618,6 +653,7 @@
|
|||
"models_server_card_connect_button": "Подключить",
|
||||
"models_server_card_title": "Использовать сервер",
|
||||
"models_server_models_capability_unknown_label": "Возможности неизвестны",
|
||||
"models_server_models_claude_plan_status": "Через Claude Code на этом компьютере",
|
||||
"models_server_models_current_in_use_button": "Используется",
|
||||
"models_server_models_edit_aria": "Изменить {server}",
|
||||
"models_server_models_edit_button": "Изменить",
|
||||
|
|
|
|||
|
|
@ -133,6 +133,8 @@
|
|||
"chat_composer_upload_sources_uploading_tooltip": "上传中…",
|
||||
"chat_error_agent_model_unsupported": "这段对话无法使用所选模型继续。请开始新对话来使用它,或选择其他模型。",
|
||||
"chat_error_agent_thread_outdated": "这个智能体对话创建于每个对话拥有自己的来源之前。请开始新对话以继续。",
|
||||
"chat_error_claude_limit": "你的 Claude 套餐已达到使用上限。上限会自动重置。",
|
||||
"chat_error_claude_sign_in": "这台电脑上的 Claude Code 需要重新登录:在终端中运行 claude 并登录。",
|
||||
"chat_error_context_too_long": "这段对话太长,超出了模型的上下文窗口。请开始新对话,或选择上下文窗口更大的模型。",
|
||||
"chat_error_interrupted": "应用关闭时已中断。",
|
||||
"chat_error_model_cannot_run": "SurfSense 无法运行这个模型。请选择其他模型。",
|
||||
|
|
@ -172,6 +174,7 @@
|
|||
"chat_mode_picker_thread_aria": "对话模式 {mode}。对话会保持其模式;要换模式,请开始新对话。",
|
||||
"chat_mode_picker_trigger_aria": "对话模式 {mode}。更换模式。",
|
||||
"chat_model_issue_allow_button": "允许…",
|
||||
"chat_model_issue_claude_sign_in_body": "无法使用 {model}:这台电脑上的 Claude Code 未安装或需要重新登录。",
|
||||
"chat_model_issue_egress_off_body": "向 {host} 发送数据已关闭。",
|
||||
"chat_model_issue_key_rejected_body": "{model} 的提供商拒绝了它的密钥。",
|
||||
"chat_model_issue_rate_limited_body": "{model} 的提供商正在限制请求。请稍后重试。",
|
||||
|
|
@ -283,6 +286,11 @@
|
|||
"egress_app_updates_label": "应用更新",
|
||||
"egress_app_updates_prompt_body": "检查更新会向 {host} 查询最新版本。SurfSense 只发送你的 IP 地址和正在运行的版本,别的都不发送。允许后也会开启启动时检查,你可以在“设置 › 网络”中关闭。",
|
||||
"egress_app_updates_prompt_title": "允许 SurfSense 检查更新吗?",
|
||||
"egress_claude_api_host_body": "{host} 会收到你发送给模型的内容:你的提示词和文档摘录。",
|
||||
"egress_claude_prompt_body": "这台电脑上的 Claude Code 只通过 SurfSense 访问 Anthropic,且只访问你允许的主机。你可以在“设置 › 网络”中逐个关闭。",
|
||||
"egress_claude_prompt_title": "允许你的 Claude 套餐访问 Anthropic 吗?",
|
||||
"egress_claude_sign_in_host_body": "{host} 让 Claude Code 保持登录。它不会收到任何对话;关闭后,Claude 会在登录过期时停止工作。",
|
||||
"egress_claude_sign_in_label": "Claude Code 登录",
|
||||
"egress_connection_prompt_body": "使用这个连接的对话会把你的提示词和文档摘录发送到 {host}。你随时可以在“设置 › 网络”中关闭。",
|
||||
"egress_connection_prompt_title": "允许向 {host} 发送数据吗?",
|
||||
"egress_destination_allow_aria": "允许 {name}",
|
||||
|
|
@ -303,6 +311,7 @@
|
|||
"egress_plugin_registry_prompt_body": "SurfSense 会从 {host} 获取第三方插件列表,只发送你的 IP 地址。SurfSense 自己的插件也运行在这个主机上,因此允许后你也可以连接它们。你随时可以在“设置 › 网络”中关闭。",
|
||||
"egress_plugin_registry_prompt_title": "允许获取插件列表?",
|
||||
"egress_prompt_allow_button": "允许",
|
||||
"egress_prompt_also_allows_body": "允许后,SurfSense 也能访问 {hosts}。",
|
||||
"egress_prompt_cancel_button": "取消",
|
||||
"embedding_settings_body": "将你的文档转换为可供检索比较的形式的模型。",
|
||||
"embedding_settings_fixed_body": "在初始设置时选择,并用于所有文档,因此无法更改。",
|
||||
|
|
@ -424,6 +433,30 @@
|
|||
"models_chatgpt_signed_in_unnamed_body": "已登录。",
|
||||
"models_chatgpt_signed_out_body": "已退出登录。重新登录即可使用此套餐的模型。",
|
||||
"models_chatgpt_waiting_status": "请在浏览器中完成登录。",
|
||||
"models_claude_connection_label": "Claude 套餐",
|
||||
"models_claude_dialog_add_button": "添加 Claude 模型",
|
||||
"models_claude_dialog_body": "SurfSense 通过这台电脑上的 Claude Code 使用你的 Claude Pro、Max 或 Team 套餐。你的登录信息保留在 Claude Code 中,SurfSense 永远看不到。用量会计入你的套餐限额,和在 Claude Code 中一样。",
|
||||
"models_claude_dialog_cancel_button": "取消",
|
||||
"models_claude_dialog_check_again_button": "重新检查",
|
||||
"models_claude_dialog_check_error": "无法检查这台电脑上的 Claude Code。",
|
||||
"models_claude_dialog_checking_status": "正在检查 Claude Code",
|
||||
"models_claude_dialog_host_declined_error": "你的 Claude 套餐需要 {host}。请允许它连接。",
|
||||
"models_claude_dialog_install_body": "在这台电脑上安装 Claude Code。",
|
||||
"models_claude_dialog_install_guide_button": "打开安装指南",
|
||||
"models_claude_dialog_ready_body": "已连接为 {email},{plan, select, pro {Pro 套餐} max {Max 套餐} team {Team 套餐} other {Claude 套餐}}。之后 Claude 模型可用于对话、标题和 Studio。",
|
||||
"models_claude_dialog_sign_in_body": "打开终端,运行 claude,然后用你的 Claude 账号登录。",
|
||||
"models_claude_dialog_signed_out_body": "Claude Code 已安装,但未登录 Claude 套餐。打开终端,运行 claude 并登录。",
|
||||
"models_claude_dialog_title": "使用你的 Claude 套餐",
|
||||
"models_claude_dialog_too_old_body": "Claude Code {version} 版本太旧,SurfSense 无法使用。请更新后重新检查。",
|
||||
"models_claude_dialog_unreadable_version_body": "SurfSense 无法读取已安装的 Claude Code 版本。请更新后重新检查。",
|
||||
"models_claude_option_body": "Pro、Max 或 Team,通过这台电脑上的 Claude Code。",
|
||||
"models_claude_option_connected_body": "已通过这台电脑上的 Claude Code 连接",
|
||||
"models_claude_option_set_up_button": "设置",
|
||||
"models_claude_option_show_button": "显示模型",
|
||||
"models_claude_option_title": "使用你的 Claude 套餐",
|
||||
"models_claude_plan_edit_body": "通过这台电脑上的 Claude Code 运行,登录信息由它保存。要登录、退出登录或切换账号,请在终端中运行 claude。",
|
||||
"models_claude_plan_edit_cancel_button": "取消",
|
||||
"models_claude_plan_edit_save_button": "保存更改",
|
||||
"models_connection_dialog_add_body": "任何实现了 OpenAI API 的服务器,例如 vLLM、LM Studio 或 OpenRouter。",
|
||||
"models_connection_dialog_add_title": "连接服务器",
|
||||
"models_connection_dialog_edit_body": "服务器由所有模型类型共用,所以这里的改动会对它们全部生效。",
|
||||
|
|
@ -431,6 +464,8 @@
|
|||
"models_connection_form_cancel_button": "取消",
|
||||
"models_connection_form_chatgpt_provider_body": "使用你的 ChatGPT 账号登录",
|
||||
"models_connection_form_chatgpt_provider_label": "ChatGPT 订阅",
|
||||
"models_connection_form_claude_plan_provider_body": "通过这台电脑上的 Claude Code",
|
||||
"models_connection_form_claude_plan_provider_label": "Claude 套餐",
|
||||
"models_connection_form_clear_key_label": "移除已保存的 API 密钥",
|
||||
"models_connection_form_custom_provider_body": "任何兼容 OpenAI 的 URL",
|
||||
"models_connection_form_custom_provider_label": "本地或自定义服务器",
|
||||
|
|
@ -618,6 +653,7 @@
|
|||
"models_server_card_connect_button": "连接",
|
||||
"models_server_card_title": "使用服务器",
|
||||
"models_server_models_capability_unknown_label": "能力未知",
|
||||
"models_server_models_claude_plan_status": "通过这台电脑上的 Claude Code",
|
||||
"models_server_models_current_in_use_button": "使用中",
|
||||
"models_server_models_edit_aria": "编辑 {server}",
|
||||
"models_server_models_edit_button": "编辑",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue