fix(vscode): surface abnormal session endings

This commit is contained in:
marius-kilocode
2026-05-28 12:40:03 +02:00
parent 2ffb1ed7c8
commit 3af4c7ebab
32 changed files with 388 additions and 6 deletions
+5
View File
@@ -0,0 +1,5 @@
---
"kilo-code": patch
---
Warn when a chat turn stops unexpectedly or ends while tracked to-dos remain unfinished.
@@ -388,6 +388,7 @@ export type WebviewMessage =
message: Record<string, unknown>
}
| { type: "sessionStatus"; sessionID: string; status: string; attempt?: number; message?: string; next?: number }
| { type: "sessionTurnClosed"; sessionID: string; reason: "completed" | "error" | "interrupted" }
| {
type: "permissionRequest"
permission: {
@@ -509,6 +510,12 @@ export function mapSSEEventToWebviewMessage(event: Event, sessionID: string | un
...extra,
}
}
case "session.turn.close":
return {
type: "sessionTurnClosed",
sessionID: event.properties.sessionID,
reason: event.properties.reason,
}
case "permission.asked":
return {
type: "permissionRequest",
@@ -16,6 +16,7 @@ export function resolveEventSessionId(
case "session.updated":
return event.properties.info.id
case "session.status":
case "session.turn.close":
case "session.idle":
case "session.error":
case "todo.updated":
@@ -46,6 +46,14 @@ describe("resolveEventSessionId", () => {
expect(resolveEventSessionId(e, noLookup)).toBe("s4")
})
it("returns sessionID from session.turn.close", () => {
const e = event({
type: "session.turn.close",
properties: { sessionID: "s-turn", reason: "interrupted" },
})
expect(resolveEventSessionId(e, noLookup)).toBe("s-turn")
})
it("returns sessionID from message.updated and calls onMessageUpdated", () => {
const e = event({
type: "message.updated",
@@ -21,6 +21,7 @@ import type {
EventMessagePartUpdated,
EventMessageUpdated,
EventSessionStatus,
EventSessionTurnClose,
EventPermissionAsked,
EventPermissionReplied,
EventTodoUpdated,
@@ -309,6 +310,16 @@ describe("mapSSEEventToWebviewMessage", () => {
}
})
it("maps session.turn.close to its terminal reason", () => {
const event: EventSessionTurnClose = {
id: "evt-turn",
type: "session.turn.close",
properties: { sessionID: "sess-1", reason: "interrupted" },
}
const msg = mapSSEEventToWebviewMessage(event, "sess-1")
expect(msg).toEqual({ type: "sessionTurnClosed", sessionID: "sess-1", reason: "interrupted" })
})
it("maps permission.asked to permissionRequest", () => {
const event: EventPermissionAsked = {
type: "permission.asked",
@@ -0,0 +1,80 @@
import { describe, expect, it } from "bun:test"
import { terminal } from "../../webview-ui/src/context/session-outcome"
import type { Message, TodoItem } from "../../webview-ui/src/types/messages"
function message(finish?: string, error?: Message["error"]): Message {
return {
id: "m1",
sessionID: "s1",
role: "assistant",
createdAt: new Date(0).toISOString(),
finish,
error,
}
}
function todo(status: TodoItem["status"]): TodoItem {
return { id: status, content: status, status }
}
describe("terminal", () => {
it("returns no terminal state before a turn closes", () => {
expect(terminal({ messages: [message("stop")], todos: [] })).toBeUndefined()
})
it("hides normal completed turns", () => {
expect(terminal({ reason: "completed", messages: [message("stop")], todos: [] })).toBeUndefined()
})
it("warns when a completed turn leaves to-dos unfinished", () => {
expect(
terminal({ reason: "completed", messages: [message("stop")], todos: [todo("completed"), todo("pending")] }),
).toEqual({ kind: "incomplete", tone: "warning", finish: "stop", remaining: 1 })
})
it("treats cancelled to-dos as terminal rather than remaining work", () => {
expect(terminal({ reason: "completed", messages: [message("stop")], todos: [todo("cancelled")] })).toBeUndefined()
})
it("surfaces response limit and unknown model finishes before generic incomplete status", () => {
expect(terminal({ reason: "completed", messages: [message("length")], todos: [todo("pending")] })?.kind).toBe(
"limit",
)
expect(terminal({ reason: "completed", messages: [message("unknown")], todos: [] })?.kind).toBe("unknown")
})
it("surfaces filtered and unexpected provider finishes", () => {
expect(terminal({ reason: "completed", messages: [message("content-filter")], todos: [] })?.kind).toBe("filtered")
expect(terminal({ reason: "completed", messages: [message("other")], todos: [] })?.kind).toBe("unexpected")
})
it("surfaces interruption and failures without a rendered error", () => {
expect(terminal({ reason: "interrupted", messages: [message("stop")], todos: [todo("pending")] })).toEqual({
kind: "interrupted",
tone: "warning",
finish: "stop",
remaining: 1,
})
expect(terminal({ reason: "error", messages: [message("error")], todos: [] })?.kind).toBe("error")
})
it("does not duplicate a concrete rendered failure", () => {
expect(terminal({ reason: "error", messages: [message("error", { name: "APIError" })], todos: [] })).toBeUndefined()
})
it("retains a fallback failure when the concrete error is hidden", () => {
expect(
terminal({
reason: "error",
messages: [message("error", { name: "APIError" })],
todos: [],
hidden: () => true,
})?.kind,
).toBe("error")
})
it("reports only the latest assistant finish reason", () => {
const user: Message = { id: "u1", sessionID: "s1", role: "user", createdAt: new Date(1).toISOString() }
expect(terminal({ reason: "completed", messages: [message("length"), user], todos: [] })?.finish).toBeUndefined()
})
})
@@ -25,6 +25,7 @@ import { RevertBanner } from "./RevertBanner"
import { AccountSwitcher } from "../shared/AccountSwitcher"
import { KiloNotifications } from "./KiloNotifications"
import { WorkingIndicator } from "../shared/WorkingIndicator"
import { TurnOutcome } from "../shared/TurnOutcome"
import { QuestionDock } from "./QuestionDock"
import { Virtualizer } from "virtua/solid"
import { SuggestBar } from "./SuggestBar"
@@ -251,6 +252,7 @@ export const MessageList: Component<MessageListProps> = (props) => {
</Show>
<For each={queuedTurns()}>{(turn) => <VscodeSessionTurn turn={turn} queued />}</For>
<WorkingIndicator />
<TurnOutcome />
<For each={props.questions?.()}>{(req) => <QuestionDock request={req} />}</For>
<For each={props.suggestions?.()}>{(req) => <SuggestBar request={req} />}</For>
</Show>
@@ -0,0 +1,42 @@
import { Card } from "@kilocode/kilo-ui/card"
import { type Component, Show, createMemo } from "solid-js"
import { useSession } from "../../context/session"
import { terminal, type TerminalState } from "../../context/session-outcome"
import { useLanguage } from "../../context/language"
export const TurnOutcome: Component = () => {
const session = useSession()
const language = useLanguage()
const state = createMemo(() =>
terminal({
reason: session.closeReason(),
messages: session.visibleMessages(),
todos: session.todos(),
hidden: session.isErrorHidden,
}),
)
const label = (value: TerminalState) => {
if (value.kind === "interrupted") return language.t("session.outcome.interrupted")
if (value.kind === "error") return language.t("session.outcome.error")
if (value.kind === "limit") return language.t("session.outcome.limit")
if (value.kind === "unknown") return language.t("session.outcome.unknown")
if (value.kind === "filtered") return language.t("session.outcome.filtered")
if (value.kind === "unexpected") return language.t("session.outcome.unexpected")
return language.t("session.outcome.incomplete", { count: String(value.remaining) })
}
return (
<Show when={session.status() === "idle" && state()}>
{(value) => (
<div
class="vscode-session-turn"
role="status"
title={value().finish ? language.t("session.outcome.finish", { reason: value().finish! }) : undefined}
>
<Card variant={value().tone === "critical" ? "error" : "warning"}>{label(value())}</Card>
</div>
)}
</Show>
)
}
@@ -0,0 +1,37 @@
import type { Message, SessionCloseReason, TodoItem } from "../types/messages"
type TerminalKind = "incomplete" | "limit" | "unknown" | "filtered" | "unexpected" | "interrupted" | "error"
type TerminalTone = "warning" | "critical"
export interface TerminalState {
kind: TerminalKind
tone: TerminalTone
finish?: string
remaining: number
}
interface Input {
reason?: SessionCloseReason
messages: Message[]
todos: TodoItem[]
hidden?: (id: string) => boolean
}
export function terminal(input: Input): TerminalState | undefined {
if (!input.reason) return undefined
const last = input.messages[input.messages.length - 1]
const finish = last?.role === "assistant" ? last.finish : undefined
const remaining = input.todos.filter((item) => item.status !== "completed" && item.status !== "cancelled").length
if (input.reason === "interrupted") return { kind: "interrupted", tone: "warning", finish, remaining }
if (input.reason === "error") {
if (last?.role === "assistant" && last.error && !input.hidden?.(last.id)) return undefined
return { kind: "error", tone: "critical", finish, remaining }
}
if (finish === "length") return { kind: "limit", tone: "warning", finish, remaining }
if (finish === "unknown") return { kind: "unknown", tone: "warning", finish, remaining }
if (finish === "content-filter") return { kind: "filtered", tone: "warning", finish, remaining }
if (finish === "other") return { kind: "unexpected", tone: "warning", finish, remaining }
if (remaining > 0) return { kind: "incomplete", tone: "warning", finish, remaining }
return undefined
}
@@ -20,6 +20,7 @@ import type {
PartDelta,
SessionStatus,
SessionStatusInfo,
SessionCloseReason,
PermissionRequest,
QuestionRequest,
SuggestionRequest,
@@ -108,6 +109,7 @@ interface SessionContextValue {
// Session status
status: Accessor<SessionStatus>
statusInfo: Accessor<SessionStatusInfo>
closeReason: Accessor<SessionCloseReason | undefined>
statusText: Accessor<string | undefined>
busySince: Accessor<number | undefined>
loading: Accessor<boolean>
@@ -284,6 +286,7 @@ export const SessionProvider: ParentComponent = (props) => {
// Per-session status map — keyed by sessionID
const [statusMap, setStatusMap] = createStore<Record<string, SessionStatusInfo>>({})
const [closeMap, setCloseMap] = createStore<Record<string, SessionCloseReason | undefined>>({})
const [busySinceMap, setBusySinceMap] = createStore<Record<string, number>>({})
const idle: SessionStatusInfo = { type: "idle" }
@@ -294,6 +297,16 @@ export const SessionProvider: ParentComponent = (props) => {
return id ? (statusMap[id] ?? idle) : idle
}
const status = () => statusInfo().type as SessionStatus
const closeReason = () => {
const id = currentSessionID()
return id ? closeMap[id] : undefined
}
const clearClose = (id: string) =>
setCloseMap(
produce((map) => {
delete map[id]
}),
)
const busySince = () => {
const id = currentSessionID()
return id ? busySinceMap[id] : undefined
@@ -837,6 +850,10 @@ export const SessionProvider: ParentComponent = (props) => {
handleSessionStatus(message.sessionID, message.status, message.attempt, message.message, message.next)
break
case "sessionTurnClosed":
setCloseMap(message.sessionID, message.reason)
break
case "todoUpdated":
handleTodoUpdated(message.sessionID, message.items)
break
@@ -1316,8 +1333,9 @@ export const SessionProvider: ParentComponent = (props) => {
? { type: "offline", message: message ?? "" }
: { type: newStatus }
setStatusMap(sessionID, info)
// Track busy start time
// Track busy start time and discard the previous turn's terminal state.
if (prev.type === "idle" && newStatus !== "idle") {
clearClose(sessionID)
setBusySinceMap(sessionID, Date.now())
}
if (newStatus === "idle") {
@@ -1326,7 +1344,7 @@ export const SessionProvider: ParentComponent = (props) => {
delete map[sessionID]
}),
)
// Session is idle — any remaining pending optimistic IDs are either
// Session is idle - any remaining pending optimistic IDs are either
// already confirmed (messageCreated removed them) or orphaned (queued
// callbacks were dropped on abort). Clean up the tracking set; the
// messages themselves will be reconciled on the next messagesLoaded.
@@ -1554,6 +1572,7 @@ export const SessionProvider: ParentComponent = (props) => {
const next = session.revert ?? undefined
setStore("sessions", session.id, session)
if (prev?.messageID === next?.messageID && prev?.partID === next?.partID) return
clearClose(session.id)
resetTodos(session.id, next)
}
@@ -1670,6 +1689,7 @@ export const SessionProvider: ParentComponent = (props) => {
delete map[sessionID]
}),
)
clearClose(sessionID)
setBusySinceMap(
produce((map) => {
delete map[sessionID]
@@ -1906,7 +1926,10 @@ export const SessionProvider: ParentComponent = (props) => {
for (const q of scopedQuestions(sid)) {
rejectQuestion(q.id)
}
if (sid) addOptimistic(sid, messageID, text, files)
if (sid) {
clearClose(sid)
addOptimistic(sid, messageID, text, files)
}
const scope = draftID ?? sid
const agent = promptAgent(scope)
@@ -1969,7 +1992,10 @@ export const SessionProvider: ParentComponent = (props) => {
rejectQuestion(q.id)
}
if (sid) addOptimistic(sid, messageID, `/${command} ${args}`.trim(), files)
if (sid) {
clearClose(sid)
addOptimistic(sid, messageID, `/${command} ${args}`.trim(), files)
}
const scope = draftID ?? sid
const agent = promptAgent(scope)
@@ -2306,6 +2332,7 @@ export const SessionProvider: ParentComponent = (props) => {
function revertSession(messageID: string, partID?: string) {
const id = currentSessionID()
if (!id) return
clearClose(id)
// Restore the reverted user message's prompt text into the input.
// Dispatch as a window message so PromptInput picks it up via onMessage.
const parts = store.parts[messageID]
@@ -2423,6 +2450,7 @@ export const SessionProvider: ParentComponent = (props) => {
sessions,
status,
statusInfo,
closeReason,
statusText,
busySince,
loading,
+8
View File
@@ -1107,6 +1107,14 @@ export const dict = {
"session.status.retrying": "...إعادة المحاولة (المحاولة {{ attempt }})… {{ message }}",
"session.status.working": "...جارٍ العمل",
"session.status.offline": "انقطع الاتصال بالشبكة — جارٍ إعادة الاتصال...",
"session.outcome.incomplete": "انتهى الدور مع تبقي {{count}} مهام",
"session.outcome.limit": "تم الوصول إلى حد الاستجابة قبل الاكتمال",
"session.outcome.unknown": "انتهى الدور بدون سبب إنهاء من النموذج",
"session.outcome.filtered": "أوقف المزود هذه الاستجابة بسبب عامل تصفية المحتوى.",
"session.outcome.unexpected": "انتهت الاستجابة بشكل غير متوقع وقد تكون غير مكتملة.",
"session.outcome.interrupted": "تمت مقاطعة الدور",
"session.outcome.error": "فشل الدور",
"session.outcome.finish": "سبب الإنهاء: {{reason}}",
"ui.sessionTurn.cancel": "إلغاء",
"ui.sessionTurn.status.thinking": "...جارٍ التفكير",
+8
View File
@@ -1126,6 +1126,14 @@ export const dict = {
"session.status.retrying": "Tentando novamente (tentativa {{ attempt }})… {{ message }}",
"session.status.working": "Trabalhando…",
"session.status.offline": "Rede desconectada — reconectando...",
"session.outcome.incomplete": "Turno encerrado com {{count}} tarefas restantes",
"session.outcome.limit": "Limite de resposta atingido antes da conclusão",
"session.outcome.unknown": "Turno encerrado sem um motivo de conclusão do modelo",
"session.outcome.filtered": "O provedor interrompeu esta resposta devido a um filtro de conteúdo.",
"session.outcome.unexpected": "A resposta terminou inesperadamente e pode estar incompleta.",
"session.outcome.interrupted": "Turno interrompido",
"session.outcome.error": "Turno falhou",
"session.outcome.finish": "Motivo da conclusão: {{reason}}",
"ui.sessionTurn.cancel": "Cancelar",
"ui.sessionTurn.status.thinking": "Pensando...",
+8
View File
@@ -1132,6 +1132,14 @@ export const dict = {
"session.status.retrying": "Ponovni pokušaj (pokušaj {{ attempt }})… {{ message }}",
"session.status.working": "Radim…",
"session.status.offline": "Mreža prekinuta — ponovno povezivanje...",
"session.outcome.incomplete": "Potez završen sa {{count}} preostalih zadataka",
"session.outcome.limit": "Ograničenje odgovora dosegnuto prije završetka",
"session.outcome.unknown": "Potez završen bez razloga završetka modela",
"session.outcome.filtered": "Provajder je zaustavio ovaj odgovor zbog filtera sadržaja.",
"session.outcome.unexpected": "Odgovor je neočekivano završen i može biti nepotpun.",
"session.outcome.interrupted": "Potez prekinut",
"session.outcome.error": "Potez nije uspio",
"session.outcome.finish": "Razlog završetka: {{reason}}",
"ui.sessionTurn.cancel": "Otkaži",
"ui.sessionTurn.status.thinking": "Razmišljam...",
+8
View File
@@ -1124,6 +1124,14 @@ export const dict = {
"session.status.retrying": "Prøver igen (forsøg {{ attempt }})… {{ message }}",
"session.status.working": "Arbejder…",
"session.status.offline": "Netværk afbrudt — genopretter forbindelse...",
"session.outcome.incomplete": "Tur afsluttet, {{count}} to-dos tilbage",
"session.outcome.limit": "Svarbegrænsning nået før fuldførelse",
"session.outcome.unknown": "Tur afsluttet uden en model-afslutningsårsag",
"session.outcome.filtered": "Udbyderen stoppede dette svar på grund af et indholdsfilter.",
"session.outcome.unexpected": "Svaret sluttede uventet og kan være ufuldstændigt.",
"session.outcome.interrupted": "Tur afbrudt",
"session.outcome.error": "Tur mislykkedes",
"session.outcome.finish": "Afslutningsårsag: {{reason}}",
"ui.sessionTurn.cancel": "Annuller",
"ui.sessionTurn.status.thinking": "Tænker...",
+8
View File
@@ -1139,6 +1139,14 @@ export const dict = {
"session.status.retrying": "Erneuter Versuch ({{ attempt }})… {{ message }}",
"session.status.working": "Wird bearbeitet…",
"session.status.offline": "Netzwerk getrennt — Verbindung wird wiederhergestellt...",
"session.outcome.incomplete": "Zug beendet mit {{count}} verbleibenden To-dos",
"session.outcome.limit": "Antwortlimit vor Abschluss erreicht",
"session.outcome.unknown": "Zug ohne Modell-Abschlussgrund beendet",
"session.outcome.filtered": "Der Anbieter hat diese Antwort aufgrund eines Inhaltsfilters gestoppt.",
"session.outcome.unexpected": "Die Antwort wurde unerwartet beendet und ist möglicherweise unvollständig.",
"session.outcome.interrupted": "Zug unterbrochen",
"session.outcome.error": "Zug fehlgeschlagen",
"session.outcome.finish": "Abschlussgrund: {{reason}}",
"ui.sessionTurn.cancel": "Abbrechen",
"ui.sessionTurn.status.thinking": "Denke nach...",
@@ -1079,6 +1079,14 @@ export const dict = {
"session.status.retrying": "Retrying (attempt {{ attempt }})… {{ message }}",
"session.status.working": "Working...",
"session.status.offline": "Network disconnected — reconnecting...",
"session.outcome.incomplete": "Turn ended with {{count}} to-dos remaining.",
"session.outcome.limit": "Response reached its output limit and may be incomplete.",
"session.outcome.unknown": "Response ended without a finish reason and may be incomplete.",
"session.outcome.filtered": "The provider stopped this response because of a content filter.",
"session.outcome.unexpected": "Response ended unexpectedly and may be incomplete.",
"session.outcome.interrupted": "Turn interrupted.",
"session.outcome.error": "Turn failed.",
"session.outcome.finish": "Technical finish reason: {{reason}}",
"sidebar.session.newSession": "New Session",
"sidebar.session.newSession.tooltip": "Start a fresh conversation while keeping the current session intact.",
"sidebar.session.newSession.disabled": "This session is already new. Start chatting or create a worktree instead.",
+8
View File
@@ -1135,6 +1135,14 @@ export const dict = {
"session.status.retrying": "Reintentando (intento {{ attempt }})… {{ message }}",
"session.status.working": "Trabajando…",
"session.status.offline": "Red desconectada — reconectando...",
"session.outcome.incomplete": "Turno finalizado con {{count}} tareas pendientes",
"session.outcome.limit": "Límite de respuesta alcanzado antes de finalizar",
"session.outcome.unknown": "Turno finalizado sin motivo de finalización del modelo",
"session.outcome.filtered": "El proveedor detuvo esta respuesta debido a un filtro de contenido.",
"session.outcome.unexpected": "La respuesta terminó inesperadamente y puede estar incompleta.",
"session.outcome.interrupted": "Turno interrumpido",
"session.outcome.error": "Turno fallido",
"session.outcome.finish": "Motivo de finalización: {{reason}}",
"ui.sessionTurn.cancel": "Cancelar",
"ui.sessionTurn.status.thinking": "Pensando...",
+8
View File
@@ -1143,6 +1143,14 @@ export const dict = {
"session.status.retrying": "Nouvelle tentative (essai {{ attempt }})… {{ message }}",
"session.status.working": "En cours…",
"session.status.offline": "Réseau déconnecté — reconnexion en cours...",
"session.outcome.incomplete": "Tour terminé avec {{count}} tâches restantes",
"session.outcome.limit": "Limite de réponse atteinte avant la fin",
"session.outcome.unknown": "Tour terminé sans motif de fin du modèle",
"session.outcome.filtered": "Le fournisseur a arrêté cette réponse en raison d'un filtre de contenu.",
"session.outcome.unexpected": "La réponse s'est terminée de manière inattendue et peut être incomplète.",
"session.outcome.interrupted": "Tour interrompu",
"session.outcome.error": "Échec du tour",
"session.outcome.finish": "Motif de fin : {{reason}}",
"ui.sessionTurn.cancel": "Annuler",
"ui.sessionTurn.status.thinking": "Réflexion...",
+8
View File
@@ -1119,6 +1119,14 @@ export const dict = {
"session.status.retrying": "再試行中({{ attempt }}回目)… {{ message }}",
"session.status.working": "作業中…",
"session.status.offline": "ネットワークが切断されました — 再接続中…",
"session.outcome.incomplete": "{{count}} 個の To-Do を残してターンが終了しました",
"session.outcome.limit": "完了前に応答制限に達しました",
"session.outcome.unknown": "モデルの終了理由なしでターンが終了しました",
"session.outcome.filtered": "コンテンツフィルターにより、プロバイダーがこの応答を停止しました。",
"session.outcome.unexpected": "応答が予期せず終了したため、不完全である可能性があります。",
"session.outcome.interrupted": "ターンが中断されました",
"session.outcome.error": "ターンが失敗しました",
"session.outcome.finish": "終了理由: {{reason}}",
"ui.sessionTurn.cancel": "キャンセル",
"ui.sessionTurn.status.thinking": "考え中...",
+8
View File
@@ -1116,6 +1116,14 @@ export const dict = {
"session.status.retrying": "재시도 중 ({{ attempt }}번째 시도)… {{ message }}",
"session.status.working": "작업 중...",
"session.status.offline": "네트워크 연결 끊김 — 다시 연결 중...",
"session.outcome.incomplete": "턴이 종료되었으며 {{count}}개의 할 일이 남았습니다",
"session.outcome.limit": "완료 전에 응답 제한에 도달했습니다",
"session.outcome.unknown": "모델의 종료 이유 없이 턴이 종료되었습니다",
"session.outcome.filtered": "콘텐츠 필터로 인해 제공자가 이 응답을 중단했습니다.",
"session.outcome.unexpected": "응답이 예기치 않게 종료되었으며 불완전할 수 있습니다.",
"session.outcome.interrupted": "턴이 중단되었습니다",
"session.outcome.error": "턴이 실패했습니다",
"session.outcome.finish": "종료 이유: {{reason}}",
"ui.sessionTurn.cancel": "취소",
"ui.sessionTurn.status.thinking": "생각 중...",
+8
View File
@@ -1119,6 +1119,14 @@ export const dict = {
"session.status.retrying": "Opnieuw proberen (poging {{ attempt }})... {{ message }}",
"session.status.working": "Bezig...",
"session.status.offline": "Netwerkverbinding verbroken — opnieuw verbinden...",
"session.outcome.incomplete": "Beurt beëindigd met {{count}} resterende to-do's",
"session.outcome.limit": "Antwoordlimiet bereikt voor voltooiing",
"session.outcome.unknown": "Beurt beëindigd zonder voltooiingsreden van het model",
"session.outcome.filtered": "De provider heeft deze reactie gestopt vanwege een contentfilter.",
"session.outcome.unexpected": "Reactie is onverwacht beëindigd en is mogelijk onvolledig.",
"session.outcome.interrupted": "Beurt onderbroken",
"session.outcome.error": "Beurt mislukt",
"session.outcome.finish": "Voltooiingsreden: {{reason}}",
"ui.sessionTurn.cancel": "Annuleren",
"ui.sessionTurn.status.thinking": "Denken...",
+8
View File
@@ -1091,6 +1091,14 @@ export const dict = {
"session.status.retrying": "Prøver på nytt (forsøk {{ attempt }})… {{ message }}",
"session.status.working": "Arbeider…",
"session.status.offline": "Nettverk frakoblet — kobler til på nytt...",
"session.outcome.incomplete": "Runden ble avsluttet med {{count}} oppgaver igjen",
"session.outcome.limit": "Svarsgrensen ble nådd før fullføring",
"session.outcome.unknown": "Runden ble avsluttet uten avslutningsårsak fra modellen",
"session.outcome.filtered": "Leverandøren stoppet denne responsen på grunn av et innholdsfilter.",
"session.outcome.unexpected": "Responsen ble avsluttet uventet og kan være ufullstendig.",
"session.outcome.interrupted": "Runde avbrutt",
"session.outcome.error": "Runden feilet",
"session.outcome.finish": "Avslutningsårsak: {{reason}}",
"ui.sessionTurn.cancel": "Avbryt",
"ui.sessionTurn.status.thinking": "Tenker...",
+8
View File
@@ -1090,6 +1090,14 @@ export const dict = {
"session.status.retrying": "Ponawiam próbę ({{ attempt }})… {{ message }}",
"session.status.working": "Pracuję…",
"session.status.offline": "Odłączono od sieci — ponowne łączenie...",
"session.outcome.incomplete": "Tura zakończona, pozostało {{count}} to-dos",
"session.outcome.limit": "Osiągnięto limit odpowiedzi przed ukończeniem",
"session.outcome.unknown": "Tura zakończona bez powodu zakończenia od modelu",
"session.outcome.filtered": "Dostawca zatrzymał tę odpowiedź z powodu filtru treści.",
"session.outcome.unexpected": "Odpowiedź zakończyła się nieoczekiwanie i może być niekompletna.",
"session.outcome.interrupted": "Tura przerwana",
"session.outcome.error": "Tura nie powiodła się",
"session.outcome.finish": "Powód zakończenia: {{reason}}",
"ui.sessionTurn.cancel": "Anuluj",
"ui.sessionTurn.status.thinking": "Myślę...",
+8
View File
@@ -1129,6 +1129,14 @@ export const dict = {
"session.status.retrying": "Повторная попытка ({{ attempt }})… {{ message }}",
"session.status.working": "Работаю…",
"session.status.offline": "Сеть отключена — переподключение...",
"session.outcome.incomplete": "Раунд завершен, осталось задач: {{count}}",
"session.outcome.limit": "Достигнут лимит ответов до завершения",
"session.outcome.unknown": "Раунд завершен без указания причины моделью",
"session.outcome.filtered": "Провайдер остановил этот ответ из-за фильтра контента.",
"session.outcome.unexpected": "Ответ неожиданно завершился и может быть неполным.",
"session.outcome.interrupted": "Раунд прерван",
"session.outcome.error": "Раунд завершился с ошибкой",
"session.outcome.finish": "Причина завершения: {{reason}}",
"ui.sessionTurn.cancel": "Отмена",
"ui.sessionTurn.status.thinking": "Думаю...",
+8
View File
@@ -1114,6 +1114,14 @@ export const dict = {
"session.status.retrying": "กำลังลองใหม่ (ครั้งที่ {{ attempt }})… {{ message }}",
"session.status.working": "กำลังทำงาน...",
"session.status.offline": "เครือข่ายถูกตัดการเชื่อมต่อ — กำลังเชื่อมต่อใหม่...",
"session.outcome.incomplete": "เทิร์นสิ้นสุดโดยเหลือสิ่งที่ต้องทำอีก {{count}} รายการ",
"session.outcome.limit": "ถึงขีดจำกัดการตอบกลับก่อนเสร็จสิ้น",
"session.outcome.unknown": "เทิร์นสิ้นสุดโดยไม่มีเหตุผลการเสร็จสิ้นของโมเดล",
"session.outcome.filtered": "ผู้ให้บริการหยุดการตอบกลับนี้เนื่องจากตัวกรองเนื้อหา",
"session.outcome.unexpected": "การตอบกลับสิ้นสุดลงโดยไม่คาดคิดและอาจไม่สมบูรณ์",
"session.outcome.interrupted": "เทิร์นถูกขัดจังหวะ",
"session.outcome.error": "เทิร์นล้มเหลว",
"session.outcome.finish": "เหตุผลการเสร็จสิ้น: {{reason}}",
"ui.sessionTurn.cancel": "ยกเลิก",
"ui.sessionTurn.status.thinking": "กำลังคิด...",
+8
View File
@@ -1117,6 +1117,14 @@ export const dict = {
"session.status.retrying": "Yeniden deneniyor (deneme {{ attempt }})… {{ message }}",
"session.status.working": "Çalışıyor...",
"session.status.offline": "Ağ bağlantısı kesildi — yeniden bağlanılıyor...",
"session.outcome.incomplete": "Tur, {{count}} yapılacak öğe kalarak sona erdi",
"session.outcome.limit": "Tamamlanmadan önce yanıt sınırına ulaşıldı",
"session.outcome.unknown": "Tur, model bitiş nedeni olmadan sona erdi",
"session.outcome.filtered": "Sağlayıcı, bir içerik filtresi nedeniyle bu yanıtı durdurdu.",
"session.outcome.unexpected": "Yanıt beklenmedik bir şekilde sona erdi ve eksik olabilir.",
"session.outcome.interrupted": "Tur kesintiye uğradı",
"session.outcome.error": "Tur başarısız oldu",
"session.outcome.finish": "Bitiş nedeni: {{reason}}",
"ui.sessionTurn.cancel": "İptal",
"ui.sessionTurn.status.thinking": "Düşünüyor...",
+8
View File
@@ -1116,6 +1116,14 @@ export const dict = {
"session.status.retrying": "Повторна спроба (спроба {{ attempt }})… {{ message }}",
"session.status.working": "Працює...",
"session.status.offline": "Мережу відключено — перепідключення...",
"session.outcome.incomplete": "Хід завершився з {{count}} невиконаними завданнями",
"session.outcome.limit": "Ліміт відповідей досягнуто до завершення",
"session.outcome.unknown": "Хід завершився без причини завершення моделі",
"session.outcome.filtered": "Провайдер зупинив цю відповідь через фільтр вмісту.",
"session.outcome.unexpected": "Відповідь завершилася несподівано і може бути неповною.",
"session.outcome.interrupted": "Хід перервано",
"session.outcome.error": "Хід не вдався",
"session.outcome.finish": "Причина завершення: {{reason}}",
"ui.sessionTurn.cancel": "Скасувати",
"ui.sessionTurn.status.thinking": "Думаю...",
+8
View File
@@ -1098,6 +1098,14 @@ export const dict = {
"session.status.retrying": "正在重试(第 {{ attempt }} 次)… {{ message }}",
"session.status.working": "处理中…",
"session.status.offline": "网络已断开 — 正在重连...",
"session.outcome.incomplete": "回合已结束,尚有 {{count}} 个待办事项",
"session.outcome.limit": "未完成前已达到响应限制",
"session.outcome.unknown": "回合已结束,模型未提供结束原因",
"session.outcome.filtered": "提供商因为内容过滤器停止了此响应。",
"session.outcome.unexpected": "响应意外结束,可能不完整。",
"session.outcome.interrupted": "回合已中断",
"session.outcome.error": "回合失败",
"session.outcome.finish": "结束原因:{{reason}}",
"ui.sessionTurn.cancel": "取消",
"ui.sessionTurn.status.thinking": "思考中...",
+8
View File
@@ -1066,6 +1066,14 @@ export const dict = {
"session.status.retrying": "正在重試(第 {{ attempt }} 次)… {{ message }}",
"session.status.working": "處理中…",
"session.status.offline": "網路已斷線 — 正在重新連線...",
"session.outcome.incomplete": "回合已結束,尚有 {{count}} 個待辦事項",
"session.outcome.limit": "未完成前已達到回應限制",
"session.outcome.unknown": "回合已結束,模型未提供結束原因",
"session.outcome.filtered": "提供商因為內容過濾器停止了此回應。",
"session.outcome.unexpected": "回應意外結束,可能不完整。",
"session.outcome.interrupted": "回合已中斷",
"session.outcome.error": "回合失敗",
"session.outcome.finish": "結束原因:{{reason}}",
"ui.sessionTurn.cancel": "取消",
"ui.sessionTurn.status.thinking": "思考中...",
@@ -5,7 +5,7 @@ import type { SessionMode } from "../../context/worktree-mode"
import type { MarketplaceItem, MarketplaceInstalledMetadata } from "../marketplace"
import type { ConnectionState, ServerInfo, SessionStatus } from "./connection"
import type { FileAttachment, Part } from "./parts"
import type { CloudSessionInfo, Message, MessageLoadMode, SessionInfo } from "./sessions"
import type { CloudSessionInfo, Message, MessageLoadMode, SessionCloseReason, SessionInfo } from "./sessions"
import type { PermissionRequest } from "./permissions"
import type { QuestionRequest, SuggestionRequest, TodoItem } from "./questions"
import type { ModelSelection, Provider, ProviderAuthState } from "./providers"
@@ -112,6 +112,12 @@ export interface SessionStatusMessage {
next?: number
}
export interface SessionTurnClosedMessage {
type: "sessionTurnClosed"
sessionID: string
reason: SessionCloseReason
}
export interface SessionErrorMessage {
type: "sessionError"
sessionID?: string
@@ -925,6 +931,7 @@ export type ExtensionMessage =
| PartsUpdatedMessage
| PartRemovedMessage
| SessionStatusMessage
| SessionTurnClosedMessage
| SessionErrorMessage
| PermissionRequestMessage
| PermissionResolvedMessage
@@ -2,7 +2,7 @@
export interface TodoItem {
id: string
content: string
status: "pending" | "in_progress" | "completed"
status: "pending" | "in_progress" | "completed" | "cancelled"
}
// Question types
@@ -1,5 +1,7 @@
import type { Part, TokenUsage } from "./parts"
export type SessionCloseReason = "completed" | "error" | "interrupted"
// Message structure (simplified for webview)
export interface Message {
id: string