Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions packages/kilo-vscode/src/KiloProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -518,6 +518,9 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
case "abort":
await this.handleAbort(message.sessionID)
break
case "abortPart":
await this.handleAbortPart(message.sessionID, message.partID)
break
case "revertSession":
this.handleRevertSession(message.sessionID, message.messageID).catch((e) =>
console.error("[Kilo New] handleRevertSession failed:", e),
Expand Down Expand Up @@ -2225,6 +2228,16 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
}
}

private async handleAbortPart(sessionID: string, partID: string): Promise<void> {
if (!this.client) return
try {
const workspaceDir = this.getWorkspaceDirectory(sessionID)
await this.client.session.abortPart({ sessionID, directory: workspaceDir, partID }, { throwOnError: true })
} catch (error) {
console.error("[Kilo New] KiloProvider: Failed to abort part:", error)
}
}

private async handleRevertSession(sessionID: string, messageID: string): Promise<void> {
if (!this.client) return
const dir = this.getWorkspaceDirectory(sessionID)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@
* Active questions and permissions are rendered in the bottom dock.
*/

import { Component, For, Show, createMemo } from "solid-js"
import { For, Show, createMemo } from "solid-js"
import type { Component } from "solid-js"
import { Dynamic } from "solid-js/web"
import { Part, PART_MAPPING, ToolRegistry } from "@kilocode/kilo-ui/message-part"
import type {
Expand All @@ -17,6 +18,9 @@ import type {
ToolPart,
} from "@kilocode/sdk/v2"
import { useData } from "@kilocode/kilo-ui/context/data"
import { IconButton } from "@kilocode/kilo-ui/icon-button"
import { useSession } from "../../context/session"
import { useLanguage } from "../../context/language"

// Tools that the upstream message-part renderer suppresses (returns null for).
// We render these ourselves via ToolRegistry when they complete,
Expand Down Expand Up @@ -65,8 +69,15 @@ function TodoToolCard(props: { part: ToolPart }) {
)
}

function canStop(part: ToolPart) {
if (part.state?.status !== "running") return false
return part.tool === "bash"
}

export const AssistantMessage: Component<AssistantMessageProps> = (props) => {
const data = useData()
const session = useSession()
const language = useLanguage()

const parts = createMemo(() => {
const stored = data.store.part?.[props.message.id]
Expand All @@ -85,6 +96,25 @@ export const AssistantMessage: Component<AssistantMessageProps> = (props) => {
return (
<Show when={isUpstreamSuppressed || PART_MAPPING[part.type]}>
<div data-component="tool-part-wrapper" data-part-type={part.type}>
<Show
when={
part.type === "tool" && canStop(part as ToolPart) && part.sessionID === session.currentSessionID()
}
>
<div data-slot="tool-part-actions">
<IconButton
icon="circle-x"
size="small"
variant="ghost"
aria-label={language.t("prompt.action.stopTool")}
onClick={(e) => {
e.stopPropagation()
const item = part as ToolPart
session.abortPart(item.sessionID, item.callID || item.id)
}}
/>
</div>
</Show>
<Show
when={isUpstreamSuppressed}
fallback={
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,11 @@ import { useSession } from "../../context/session"
import { useVSCode } from "../../context/vscode"
import type { ToolPart, Message as SDKMessage } from "@kilocode/sdk/v2"

function canStop(item: ToolPart) {
if (item.state?.status !== "running") return false
return item.tool === "bash"
}

/** Collect all tool parts from all assistant messages in a given session. */
function getSessionToolParts(store: ReturnType<typeof useData>["store"], sessionId: string): ToolPart[] {
const messages = (store.message?.[sessionId] as SDKMessage[] | undefined)?.filter((m) => m.role === "assistant")
Expand Down Expand Up @@ -128,6 +133,18 @@ const TaskToolRenderer: Component<ToolProps> = (props) => {
<Show when={subtitle()}>
<span data-slot="task-tool-subtitle">{subtitle()}</span>
</Show>
<Show when={canStop(item)}>
<IconButton
icon="circle-x"
size="small"
variant="ghost"
aria-label={i18n.t("prompt.action.stopSubtask")}
onClick={(e) => {
e.stopPropagation()
session.abortPart(childSessionId() || "", item.callID || item.id)
}}
/>
</Show>
</div>
)
}}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

import { 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"

Expand All @@ -15,23 +16,38 @@ export const WorkingIndicator: Component = () => {

const [elapsed, setElapsed] = createSignal(0)
const [retryCountdown, setRetryCountdown] = createSignal(0)
const [continued, setContinued] = createSignal<Record<string, boolean>>({})

const mark = () => {
const id = session.currentSessionID()
if (!id) return
setContinued((map) => ({ ...map, [id]: true }))
}

createEffect(() => {
const since = session.busySince()
const status = session.status()
const id = session.currentSessionID()

if (status === "idle" || !since) {
if (!id || status === "idle" || !since) {
if (id && continued()[id]) {
setContinued((map) => {
const next = { ...map }
delete next[id]
return next
})
}
setElapsed(0)
return
}

setElapsed(Math.floor((Date.now() - since) / 1000))

const id = setInterval(() => {
const timer = setInterval(() => {
setElapsed(Math.floor((Date.now() - since) / 1000))
}, 1000)

onCleanup(() => clearInterval(id))
onCleanup(() => clearInterval(timer))
})

createEffect(() => {
Expand All @@ -44,13 +60,13 @@ export const WorkingIndicator: Component = () => {
const target = info.next
setRetryCountdown(Math.max(0, Math.ceil((target - Date.now()) / 1000)))

const id = setInterval(() => {
const timer = setInterval(() => {
const remaining = Math.max(0, Math.ceil((target - Date.now()) / 1000))
setRetryCountdown(remaining)
if (remaining <= 0) clearInterval(id)
if (remaining <= 0) clearInterval(timer)
}, 1000)

onCleanup(() => clearInterval(id))
onCleanup(() => clearInterval(timer))
})

const statusText = () => {
Expand Down Expand Up @@ -80,6 +96,12 @@ export const WorkingIndicator: Component = () => {
return perms.length > 0 || questions.length > 0
}

const long = () => {
const id = session.currentSessionID()
if (!id || continued()[id]) return false
return session.status() !== "idle" && elapsed() >= 15
}

return (
<Show when={session.status() !== "idle" && !blocked()}>
<div class="working-indicator">
Expand All @@ -88,6 +110,16 @@ export const WorkingIndicator: Component = () => {
<Show when={elapsed() > 0}>
<span class="working-elapsed">{formatElapsed()}</span>
</Show>
<Show when={long()}>
<div class="working-actions">
<Button size="small" variant="ghost" onClick={mark}>
{language.t("migration.whatsNew.continue")}
</Button>
<Button size="small" variant="ghost" onClick={() => session.abort()}>
{language.t("prompt.action.stop")}
</Button>
</div>
</Show>
</div>
</Show>
)
Expand Down
10 changes: 10 additions & 0 deletions packages/kilo-vscode/webview-ui/src/context/session.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,7 @@ interface SessionContextValue {
sendMessage: (text: string, providerID?: string, modelID?: string, files?: FileAttachment[]) => void
sendCommand: (command: string, args: string, providerID?: string, modelID?: string, files?: FileAttachment[]) => void
abort: () => void
abortPart: (sessionID: string, partID: string) => void
compact: () => void
respondToPermission: (
permissionId: string,
Expand Down Expand Up @@ -1400,6 +1401,14 @@ export const SessionProvider: ParentComponent = (props) => {
})
}

function abortPart(sessionID: string, partID: string) {
vscode.postMessage({
type: "abortPart",
sessionID,
partID,
})
}

function compact() {
if (!server.isConnected()) {
console.warn("[Kilo New] Cannot compact: not connected")
Expand Down Expand Up @@ -1763,6 +1772,7 @@ export const SessionProvider: ParentComponent = (props) => {
sendMessage,
sendCommand,
abort,
abortPart,
compact,
respondToPermission,
replyToQuestion,
Expand Down
2 changes: 2 additions & 0 deletions packages/kilo-vscode/webview-ui/src/i18n/ar.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions packages/kilo-vscode/webview-ui/src/i18n/br.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions packages/kilo-vscode/webview-ui/src/i18n/bs.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions packages/kilo-vscode/webview-ui/src/i18n/da.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions packages/kilo-vscode/webview-ui/src/i18n/de.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions packages/kilo-vscode/webview-ui/src/i18n/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,8 @@ export const dict = {
"prompt.action.send": "Send",
"prompt.action.send.blocked": "Answer or dismiss the pending question first",
"prompt.action.stop": "Stop",
"prompt.action.stopTool": "Stop tool",
"prompt.action.stopSubtask": "Stop subtask",
"prompt.action.enhance": "Enhance prompt",
"prompt.action.resetModel": "Reset model to default",
"prompt.action.enhanceDescription":
Expand Down
2 changes: 2 additions & 0 deletions packages/kilo-vscode/webview-ui/src/i18n/es.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions packages/kilo-vscode/webview-ui/src/i18n/fr.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions packages/kilo-vscode/webview-ui/src/i18n/ja.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions packages/kilo-vscode/webview-ui/src/i18n/ko.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions packages/kilo-vscode/webview-ui/src/i18n/nl.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions packages/kilo-vscode/webview-ui/src/i18n/no.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions packages/kilo-vscode/webview-ui/src/i18n/pl.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions packages/kilo-vscode/webview-ui/src/i18n/ru.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions packages/kilo-vscode/webview-ui/src/i18n/th.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions packages/kilo-vscode/webview-ui/src/i18n/tr.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading