mirror of
https://github.com/Kilo-Org/kilocode.git
synced 2026-09-24 16:02:55 +08:00
Merge pull request #8435 from Kilo-Org/catrielmuller/fix-8333
feat(vscode): exponential backoff retry with cancel button for rate limiting
This commit is contained in:
@@ -41,6 +41,7 @@ import { getBusySessionCount, seedSessionStatuses } from "./session-status"
|
||||
import { retry } from "./services/cli-backend/retry"
|
||||
import { slimPart, slimParts } from "./kilo-provider/slim-metadata"
|
||||
import { matchFollowup, recordFollowup, type Followup } from "./kilo-provider/followup-session"
|
||||
import { retryable, backoff, MAX_RETRIES } from "./util/retry"
|
||||
// legacy-migration start
|
||||
import {
|
||||
checkAndShowMigrationWizard,
|
||||
@@ -530,6 +531,7 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
|
||||
break
|
||||
}
|
||||
case "abort":
|
||||
this.cancelRetry(message.sessionID ?? "")
|
||||
await this.handleAbort(message.sessionID)
|
||||
break
|
||||
case "revertSession":
|
||||
@@ -2160,6 +2162,85 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
|
||||
return { sid, dir }
|
||||
}
|
||||
|
||||
/** Abort controllers for active retry loops, keyed by session ID */
|
||||
private retryAbortControllers = new Map<string, AbortController>()
|
||||
|
||||
/**
|
||||
* Execute an SDK call with exponential backoff on HTTP errors.
|
||||
* Retries on 429, 5xx, and other retryable status codes.
|
||||
* When the response includes `Retry-After` / `Retry-After-MS` headers,
|
||||
* the delay honours that value (capped at 5 min). Otherwise uses the
|
||||
* predefined backoff schedule: 5s -> 10s -> 30s -> 60s -> 300s.
|
||||
*
|
||||
* After MAX_RETRIES (5) attempts, automatically throws the error.
|
||||
* Users can cancel via the cancel button in the UI which sends an abort
|
||||
* message — this interrupts the backoff delay and stops the retry loop.
|
||||
*
|
||||
* The webview receives `sessionStatus` messages with a countdown so the
|
||||
* user can see that a retry is in progress.
|
||||
*/
|
||||
private async withRetry(fn: () => Promise<{ error?: unknown; response: Response }>, sid: string): Promise<void> {
|
||||
const abortController = new AbortController()
|
||||
this.retryAbortControllers.set(sid, abortController)
|
||||
|
||||
try {
|
||||
for (let attempt = 1; ; attempt++) {
|
||||
if (abortController.signal.aborted) {
|
||||
// User cancelled — return normally without triggering sendMessageFailed
|
||||
return
|
||||
}
|
||||
|
||||
const result = await fn()
|
||||
if (!result.error) return
|
||||
|
||||
const status = result.response?.status ?? 0
|
||||
|
||||
// Non-retryable status codes fail immediately without retry
|
||||
if (!retryable(status)) {
|
||||
this.postMessage({ type: "sessionStatus", sessionID: sid, status: "idle" })
|
||||
throw result.error
|
||||
}
|
||||
|
||||
// Stop retrying after MAX_RETRIES attempts
|
||||
if (attempt >= MAX_RETRIES) {
|
||||
this.postMessage({ type: "sessionStatus", sessionID: sid, status: "idle" })
|
||||
throw result.error
|
||||
}
|
||||
|
||||
const delay = backoff(attempt, result.response?.headers)
|
||||
console.log(`[Kilo New] KiloProvider: Retry on ${status}, attempt ${attempt}/${MAX_RETRIES}, delay ${delay}ms`)
|
||||
|
||||
this.postMessage({
|
||||
type: "sessionStatus",
|
||||
sessionID: sid,
|
||||
status: "retry",
|
||||
attempt,
|
||||
message: `Error (${status}). Retrying...`,
|
||||
next: Date.now() + delay,
|
||||
})
|
||||
|
||||
// Wait for delay or until aborted
|
||||
await new Promise((resolve) => {
|
||||
const timer = setTimeout(resolve, delay)
|
||||
abortController.signal.addEventListener("abort", () => {
|
||||
clearTimeout(timer)
|
||||
})
|
||||
})
|
||||
}
|
||||
} finally {
|
||||
this.retryAbortControllers.delete(sid)
|
||||
}
|
||||
}
|
||||
|
||||
/** Cancel an active retry loop for a session */
|
||||
private cancelRetry(sid: string): void {
|
||||
const controller = this.retryAbortControllers.get(sid)
|
||||
if (controller) {
|
||||
controller.abort()
|
||||
this.postMessage({ type: "sessionStatus", sessionID: sid, status: "idle" })
|
||||
}
|
||||
}
|
||||
|
||||
private async handleSendMessage(
|
||||
text: string,
|
||||
messageID?: string,
|
||||
@@ -2202,18 +2283,21 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
|
||||
this.connectionService.recordMessageSessionId(messageID, resolved!.sid)
|
||||
}
|
||||
|
||||
await this.client.session.promptAsync(
|
||||
{
|
||||
sessionID: resolved!.sid,
|
||||
directory: resolved!.dir,
|
||||
messageID,
|
||||
parts,
|
||||
model: providerID && modelID ? { providerID, modelID } : undefined,
|
||||
agent,
|
||||
variant,
|
||||
editorContext,
|
||||
},
|
||||
{ throwOnError: true },
|
||||
const sid = resolved!.sid
|
||||
const dir = resolved!.dir
|
||||
await this.withRetry(
|
||||
() =>
|
||||
this.client!.session.promptAsync({
|
||||
sessionID: sid,
|
||||
directory: dir,
|
||||
messageID,
|
||||
parts,
|
||||
model: providerID && modelID ? { providerID, modelID } : undefined,
|
||||
agent,
|
||||
variant,
|
||||
editorContext,
|
||||
}),
|
||||
sid,
|
||||
)
|
||||
} catch (error) {
|
||||
console.error("[Kilo New] KiloProvider: Failed to send message:", error)
|
||||
@@ -2264,19 +2348,22 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
|
||||
|
||||
const parts = files?.map((f) => ({ type: "file" as const, mime: f.mime, url: f.url }))
|
||||
|
||||
await this.client.session.command(
|
||||
{
|
||||
sessionID: resolved!.sid,
|
||||
directory: resolved!.dir,
|
||||
command,
|
||||
arguments: args,
|
||||
messageID,
|
||||
model: providerID && modelID ? `${providerID}/${modelID}` : undefined,
|
||||
agent,
|
||||
variant,
|
||||
parts,
|
||||
},
|
||||
{ throwOnError: true },
|
||||
const sid = resolved!.sid
|
||||
const dir = resolved!.dir
|
||||
await this.withRetry(
|
||||
() =>
|
||||
this.client!.session.command({
|
||||
sessionID: sid,
|
||||
directory: dir,
|
||||
command,
|
||||
arguments: args,
|
||||
messageID,
|
||||
model: providerID && modelID ? `${providerID}/${modelID}` : undefined,
|
||||
agent,
|
||||
variant,
|
||||
parts,
|
||||
}),
|
||||
sid,
|
||||
)
|
||||
} catch (error) {
|
||||
console.error("[Kilo New] KiloProvider: Failed to send command:", error)
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
/**
|
||||
* Exponential backoff retry utilities for rate-limited API calls.
|
||||
*
|
||||
* When the CLI backend (or the upstream AI provider it proxies) returns
|
||||
* HTTP 429, retries are scheduled with exponential backoff. The delay
|
||||
* respects `Retry-After` / `Retry-After-MS` headers when present.
|
||||
*/
|
||||
|
||||
/** Backoff delays per attempt: 5s -> 10s -> 30s -> 60s -> 300s */
|
||||
const BACKOFF_DELAYS_MS = [5_000, 10_000, 30_000, 60_000, 300_000]
|
||||
|
||||
/** Maximum backoff delay in ms (5 minutes) */
|
||||
const MAX_MS = 300_000
|
||||
|
||||
/** Maximum number of retry attempts */
|
||||
const MAX_RETRIES = BACKOFF_DELAYS_MS.length
|
||||
|
||||
/** HTTP status codes that are safe to retry */
|
||||
const RETRYABLE = new Set([408, 409, 425, 429, 500, 502, 503, 504])
|
||||
|
||||
/**
|
||||
* Whether an HTTP status code is retryable.
|
||||
*/
|
||||
export function retryable(status: number): boolean {
|
||||
if (RETRYABLE.has(status)) return true
|
||||
return status >= 500
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract a retry delay (in ms) from standard response headers.
|
||||
*
|
||||
* Checks `retry-after-ms` first (milliseconds), then `retry-after`
|
||||
* (seconds or HTTP-date). Returns `null` when no usable header is found.
|
||||
*/
|
||||
export function headerDelay(headers: Headers): number | null {
|
||||
const ms = headers.get("retry-after-ms")
|
||||
if (ms) {
|
||||
const parsed = Number.parseFloat(ms)
|
||||
if (!Number.isNaN(parsed) && parsed > 0) return parsed
|
||||
}
|
||||
|
||||
const after = headers.get("retry-after")
|
||||
if (after) {
|
||||
const seconds = Number.parseFloat(after)
|
||||
if (!Number.isNaN(seconds) && seconds > 0) return Math.ceil(seconds * 1000)
|
||||
// Try HTTP-date format
|
||||
const date = Date.parse(after) - Date.now()
|
||||
if (!Number.isNaN(date) && date > 0) return Math.ceil(date)
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate backoff delay for a given attempt.
|
||||
*
|
||||
* If `headers` are provided and contain a `Retry-After` value, that
|
||||
* value is used (capped at MAX_MS). Otherwise uses the predefined
|
||||
* backoff schedule: 5s, 10s, 30s, 60s, 300s.
|
||||
*/
|
||||
export function backoff(attempt: number, headers?: Headers): number {
|
||||
if (headers) {
|
||||
const fromHeader = headerDelay(headers)
|
||||
if (fromHeader !== null) return Math.min(fromHeader, MAX_MS)
|
||||
}
|
||||
const index = Math.min(attempt - 1, BACKOFF_DELAYS_MS.length - 1)
|
||||
return BACKOFF_DELAYS_MS[index] ?? MAX_MS
|
||||
}
|
||||
|
||||
export { MAX_RETRIES, MAX_MS }
|
||||
@@ -4,14 +4,17 @@
|
||||
* Matches the v1.0.25 working indicator UX.
|
||||
*/
|
||||
|
||||
import { Component, Show, createSignal, createEffect, onCleanup } from "solid-js"
|
||||
import { type Component, Show, createSignal, createEffect, onCleanup } from "solid-js"
|
||||
import { Spinner } from "@kilocode/kilo-ui/spinner"
|
||||
import { Button } from "@kilocode/kilo-ui/button"
|
||||
import { useSession } from "../../context/session"
|
||||
import { useLanguage } from "../../context/language"
|
||||
import { useVSCode } from "../../context/vscode"
|
||||
|
||||
export const WorkingIndicator: Component = () => {
|
||||
const session = useSession()
|
||||
const language = useLanguage()
|
||||
const vscode = useVSCode()
|
||||
|
||||
const [elapsed, setElapsed] = createSignal(0)
|
||||
const [retryCountdown, setRetryCountdown] = createSignal(0)
|
||||
@@ -80,6 +83,15 @@ export const WorkingIndicator: Component = () => {
|
||||
return perms.length > 0 || questions.length > 0
|
||||
}
|
||||
|
||||
const isRetrying = () => session.statusInfo().type === "retry"
|
||||
|
||||
const handleCancelRetry = () => {
|
||||
const sid = session.currentSessionID()
|
||||
if (sid) {
|
||||
vscode.postMessage({ type: "abort", sessionID: sid })
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Show when={session.status() !== "idle" && !blocked()}>
|
||||
<div class="working-indicator">
|
||||
@@ -88,6 +100,17 @@ export const WorkingIndicator: Component = () => {
|
||||
<Show when={elapsed() > 0}>
|
||||
<span class="working-elapsed">{formatElapsed()}</span>
|
||||
</Show>
|
||||
<Show when={isRetrying()}>
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="small"
|
||||
onClick={handleCancelRetry}
|
||||
class="working-cancel"
|
||||
style={{ "font-weight": "600", color: "var(--vscode-errorForeground, #f85149)" }}
|
||||
>
|
||||
{language.t("ui.sessionTurn.cancel") || "Cancel"}
|
||||
</Button>
|
||||
</Show>
|
||||
</div>
|
||||
</Show>
|
||||
)
|
||||
|
||||
@@ -414,6 +414,10 @@ export const SessionProvider: ParentComponent = (props) => {
|
||||
|
||||
function selectModel(providerID: string, modelID: string) {
|
||||
applyModel(selectedAgentName(), { providerID, modelID })
|
||||
const sid = currentSessionID()
|
||||
if (sid) {
|
||||
setStore("messages", sid, (msgs = []) => msgs.filter((m) => !m.error))
|
||||
}
|
||||
}
|
||||
|
||||
/** The config/default model for the current mode (what settings says). */
|
||||
|
||||
+4
@@ -935,6 +935,10 @@ export const dict = {
|
||||
"session.status.retrying": "...إعادة المحاولة (المحاولة {{ attempt }})… {{ message }}",
|
||||
"session.status.working": "...جارٍ العمل",
|
||||
|
||||
"ui.sessionTurn.cancel": "إلغاء",
|
||||
"ui.sessionTurn.status.thinking": "...جارٍ التفكير",
|
||||
"ui.sessionTurn.status.consideringNextSteps": "...جارٍ التفكير في الخطوات التالية",
|
||||
|
||||
"dialog.model.noProviders": "لا يوجد موفرون",
|
||||
|
||||
"prompt.placeholder.connecting": "جارٍ الاتصال بالخادم...",
|
||||
|
||||
+4
@@ -943,6 +943,10 @@ export const dict = {
|
||||
"session.status.retrying": "Tentando novamente (tentativa {{ attempt }})… {{ message }}",
|
||||
"session.status.working": "Trabalhando…",
|
||||
|
||||
"ui.sessionTurn.cancel": "Cancelar",
|
||||
"ui.sessionTurn.status.thinking": "Pensando...",
|
||||
"ui.sessionTurn.status.consideringNextSteps": "Considerando próximos passos...",
|
||||
|
||||
"dialog.model.noProviders": "Nenhum provedor",
|
||||
|
||||
"prompt.placeholder.connecting": "Conectando ao servidor...",
|
||||
|
||||
+4
@@ -948,6 +948,10 @@ export const dict = {
|
||||
"session.status.retrying": "Ponovni pokušaj (pokušaj {{ attempt }})… {{ message }}",
|
||||
"session.status.working": "Radim…",
|
||||
|
||||
"ui.sessionTurn.cancel": "Otkaži",
|
||||
"ui.sessionTurn.status.thinking": "Razmišljam...",
|
||||
"ui.sessionTurn.status.consideringNextSteps": "Razmatram sljedeće korake...",
|
||||
|
||||
"dialog.model.noProviders": "Nema pružatelja",
|
||||
|
||||
"prompt.placeholder.connecting": "Povezivanje na server...",
|
||||
|
||||
+4
@@ -941,6 +941,10 @@ export const dict = {
|
||||
"session.status.retrying": "Prøver igen (forsøg {{ attempt }})… {{ message }}",
|
||||
"session.status.working": "Arbejder…",
|
||||
|
||||
"ui.sessionTurn.cancel": "Annuller",
|
||||
"ui.sessionTurn.status.thinking": "Tænker...",
|
||||
"ui.sessionTurn.status.consideringNextSteps": "Overvejer næste trin...",
|
||||
|
||||
"dialog.model.noProviders": "Ingen udbydere",
|
||||
|
||||
"prompt.placeholder.connecting": "Opretter forbindelse til server...",
|
||||
|
||||
+4
@@ -953,6 +953,10 @@ export const dict = {
|
||||
"session.status.retrying": "Erneuter Versuch ({{ attempt }})… {{ message }}",
|
||||
"session.status.working": "Wird bearbeitet…",
|
||||
|
||||
"ui.sessionTurn.cancel": "Abbrechen",
|
||||
"ui.sessionTurn.status.thinking": "Denke nach...",
|
||||
"ui.sessionTurn.status.consideringNextSteps": "Überlege nächste Schritte...",
|
||||
|
||||
"dialog.model.noProviders": "Keine Anbieter",
|
||||
|
||||
"prompt.placeholder.connecting": "Verbindung zum Server wird hergestellt...",
|
||||
|
||||
@@ -941,6 +941,10 @@ export const dict = {
|
||||
"session.status.retrying": "Retrying (attempt {{ attempt }})… {{ message }}",
|
||||
"session.status.working": "Working...",
|
||||
|
||||
"ui.sessionTurn.cancel": "Cancel",
|
||||
"ui.sessionTurn.status.thinking": "Thinking...",
|
||||
"ui.sessionTurn.status.consideringNextSteps": "Considering next steps...",
|
||||
|
||||
"dialog.model.noProviders": "No providers",
|
||||
|
||||
"prompt.placeholder.connecting": "Connecting to server...",
|
||||
|
||||
+4
@@ -949,6 +949,10 @@ export const dict = {
|
||||
"session.status.retrying": "Reintentando (intento {{ attempt }})… {{ message }}",
|
||||
"session.status.working": "Trabajando…",
|
||||
|
||||
"ui.sessionTurn.cancel": "Cancelar",
|
||||
"ui.sessionTurn.status.thinking": "Pensando...",
|
||||
"ui.sessionTurn.status.consideringNextSteps": "Considerando siguientes pasos...",
|
||||
|
||||
"dialog.model.noProviders": "Sin proveedores",
|
||||
|
||||
"prompt.placeholder.connecting": "Conectando al servidor...",
|
||||
|
||||
+4
@@ -955,6 +955,10 @@ export const dict = {
|
||||
"session.status.retrying": "Nouvelle tentative (essai {{ attempt }})… {{ message }}",
|
||||
"session.status.working": "En cours…",
|
||||
|
||||
"ui.sessionTurn.cancel": "Annuler",
|
||||
"ui.sessionTurn.status.thinking": "Réflexion...",
|
||||
"ui.sessionTurn.status.consideringNextSteps": "Envisage les prochaines étapes...",
|
||||
|
||||
"dialog.model.noProviders": "Aucun fournisseur",
|
||||
|
||||
"prompt.placeholder.connecting": "Connexion au serveur...",
|
||||
|
||||
+4
@@ -940,6 +940,10 @@ export const dict = {
|
||||
"session.status.retrying": "再試行中({{ attempt }}回目)… {{ message }}",
|
||||
"session.status.working": "作業中…",
|
||||
|
||||
"ui.sessionTurn.cancel": "キャンセル",
|
||||
"ui.sessionTurn.status.thinking": "考え中...",
|
||||
"ui.sessionTurn.status.consideringNextSteps": "次のステップを検討中...",
|
||||
|
||||
"dialog.model.noProviders": "プロバイダーなし",
|
||||
|
||||
"prompt.placeholder.connecting": "サーバーに接続中...",
|
||||
|
||||
+4
@@ -940,6 +940,10 @@ export const dict = {
|
||||
"session.status.retrying": "재시도 중 ({{ attempt }}번째 시도)… {{ message }}",
|
||||
"session.status.working": "작업 중...",
|
||||
|
||||
"ui.sessionTurn.cancel": "취소",
|
||||
"ui.sessionTurn.status.thinking": "생각 중...",
|
||||
"ui.sessionTurn.status.consideringNextSteps": "다음 단계 고려 중...",
|
||||
|
||||
"dialog.model.noProviders": "공급자 없음",
|
||||
|
||||
"prompt.placeholder.connecting": "서버에 연결 중...",
|
||||
|
||||
+4
@@ -942,6 +942,10 @@ export const dict = {
|
||||
"session.status.retrying": "Opnieuw proberen (poging {{ attempt }})... {{ message }}",
|
||||
"session.status.working": "Bezig...",
|
||||
|
||||
"ui.sessionTurn.cancel": "Annuleren",
|
||||
"ui.sessionTurn.status.thinking": "Denken...",
|
||||
"ui.sessionTurn.status.consideringNextSteps": "Volgende stappen overwegen...",
|
||||
|
||||
"dialog.model.noProviders": "Geen providers",
|
||||
|
||||
"prompt.placeholder.connecting": "Verbinden met server...",
|
||||
|
||||
+4
@@ -945,6 +945,10 @@ export const dict = {
|
||||
"session.status.retrying": "Prøver på nytt (forsøk {{ attempt }})… {{ message }}",
|
||||
"session.status.working": "Arbeider…",
|
||||
|
||||
"ui.sessionTurn.cancel": "Avbryt",
|
||||
"ui.sessionTurn.status.thinking": "Tenker...",
|
||||
"ui.sessionTurn.status.consideringNextSteps": "Vurderer neste steg...",
|
||||
|
||||
"dialog.model.noProviders": "Ingen leverandører",
|
||||
|
||||
"prompt.placeholder.connecting": "Kobler til server...",
|
||||
|
||||
+4
@@ -945,6 +945,10 @@ export const dict = {
|
||||
"session.status.retrying": "Ponawiam próbę ({{ attempt }})… {{ message }}",
|
||||
"session.status.working": "Pracuję…",
|
||||
|
||||
"ui.sessionTurn.cancel": "Anuluj",
|
||||
"ui.sessionTurn.status.thinking": "Myślę...",
|
||||
"ui.sessionTurn.status.consideringNextSteps": "Rozważam następne kroki...",
|
||||
|
||||
"dialog.model.noProviders": "Brak dostawców",
|
||||
|
||||
"prompt.placeholder.connecting": "Łączenie z serwerem...",
|
||||
|
||||
+4
@@ -948,6 +948,10 @@ export const dict = {
|
||||
"session.status.retrying": "Повторная попытка ({{ attempt }})… {{ message }}",
|
||||
"session.status.working": "Работаю…",
|
||||
|
||||
"ui.sessionTurn.cancel": "Отмена",
|
||||
"ui.sessionTurn.status.thinking": "Думаю...",
|
||||
"ui.sessionTurn.status.consideringNextSteps": "Продумываю следующие шаги...",
|
||||
|
||||
"dialog.model.noProviders": "Нет провайдеров",
|
||||
|
||||
"prompt.placeholder.connecting": "Подключение к серверу...",
|
||||
|
||||
+4
@@ -936,6 +936,10 @@ export const dict = {
|
||||
"session.status.retrying": "กำลังลองใหม่ (ครั้งที่ {{ attempt }})… {{ message }}",
|
||||
"session.status.working": "กำลังทำงาน...",
|
||||
|
||||
"ui.sessionTurn.cancel": "ยกเลิก",
|
||||
"ui.sessionTurn.status.thinking": "กำลังคิด...",
|
||||
"ui.sessionTurn.status.consideringNextSteps": "กำลังพิจารณาขั้นตอนถัดไป...",
|
||||
|
||||
"dialog.model.noProviders": "ไม่มีผู้ให้บริการ",
|
||||
|
||||
"prompt.placeholder.connecting": "กำลังเชื่อมต่อกับเซิร์ฟเวอร์...",
|
||||
|
||||
+4
@@ -944,6 +944,10 @@ export const dict = {
|
||||
"session.status.retrying": "Yeniden deneniyor (deneme {{ attempt }})… {{ message }}",
|
||||
"session.status.working": "Çalışıyor...",
|
||||
|
||||
"ui.sessionTurn.cancel": "İptal",
|
||||
"ui.sessionTurn.status.thinking": "Düşünüyor...",
|
||||
"ui.sessionTurn.status.consideringNextSteps": "Sonraki adımları değerlendiriyor...",
|
||||
|
||||
"dialog.model.noProviders": "Sağlayıcı yok",
|
||||
|
||||
"prompt.placeholder.connecting": "Sunucuya bağlanılıyor...",
|
||||
|
||||
+4
@@ -945,6 +945,10 @@ export const dict = {
|
||||
"session.status.retrying": "Повторна спроба (спроба {{ attempt }})… {{ message }}",
|
||||
"session.status.working": "Працює...",
|
||||
|
||||
"ui.sessionTurn.cancel": "Скасувати",
|
||||
"ui.sessionTurn.status.thinking": "Думаю...",
|
||||
"ui.sessionTurn.status.consideringNextSteps": "Обдумую наступні кроки...",
|
||||
|
||||
"dialog.model.noProviders": "Немає провайдерів",
|
||||
|
||||
"prompt.placeholder.connecting": "Підключення до сервера...",
|
||||
|
||||
+4
@@ -928,6 +928,10 @@ export const dict = {
|
||||
"session.status.retrying": "正在重试(第 {{ attempt }} 次)… {{ message }}",
|
||||
"session.status.working": "处理中…",
|
||||
|
||||
"ui.sessionTurn.cancel": "取消",
|
||||
"ui.sessionTurn.status.thinking": "思考中...",
|
||||
"ui.sessionTurn.status.consideringNextSteps": "正在考虑下一步...",
|
||||
|
||||
"dialog.model.noProviders": "无供应商",
|
||||
|
||||
"prompt.placeholder.connecting": "正在连接服务器...",
|
||||
|
||||
+4
@@ -930,6 +930,10 @@ export const dict = {
|
||||
"session.status.retrying": "正在重試(第 {{ attempt }} 次)… {{ message }}",
|
||||
"session.status.working": "處理中…",
|
||||
|
||||
"ui.sessionTurn.cancel": "取消",
|
||||
"ui.sessionTurn.status.thinking": "思考中...",
|
||||
"ui.sessionTurn.status.consideringNextSteps": "正在考慮下一步...",
|
||||
|
||||
"dialog.model.noProviders": "沒有供應商",
|
||||
|
||||
"prompt.placeholder.connecting": "正在連線至伺服器...",
|
||||
|
||||
Reference in New Issue
Block a user