mirror of
https://github.com/Kilo-Org/kilocode.git
synced 2026-10-29 18:52:45 +08:00
fix(vscode): surface abnormal session endings
This commit is contained in:
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user