Skip to content
Open
Show file tree
Hide file tree
Changes from 2 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
4 changes: 4 additions & 0 deletions packages/kilo-ui/src/components/message-part.css
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,10 @@ html[data-theme="kilo-vscode"] [data-component="bash-output"] {
font-size: var(--font-size-small);
color: var(--text-weak);
user-select: none;
display: flex;
align-items: center;
justify-content: flex-end;
gap: 4px;
}
}

Expand Down
31 changes: 30 additions & 1 deletion packages/kilo-ui/src/components/message-part.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -706,6 +706,7 @@ export function UserMessageDisplay(props: {
interrupted?: boolean
animate?: boolean
queued?: boolean
onDeleteQueued?: () => void
onRevert?: () => void
}) {
const data = useData()
Expand Down Expand Up @@ -823,10 +824,38 @@ export function UserMessageDisplay(props: {
<GrowBox animate={!!props.animate} open={!!props.queued}>
<div data-slot="user-message-queued-indicator">
<TextShimmer text={i18n.t("ui.message.queued")} />
<Show when={props.onDeleteQueued}>
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WARNING: Attachment-only queued messages never show the delete action

This button is rendered inside the surrounding Show when={text()} block. PromptInput allows sending prompts with only image/PDF attachments, so those queued messages will never render the queued indicator or delete control and cannot be removed with this feature.

<IconButton
icon="trash"
label={i18n.t("ui.message.deleteQueued")}
size="xs"
variant="ghost"
onClick={props.onDeleteQueued}
/>
</Show>
</div>
</GrowBox>
</div>

</>
</Show>
<Show when={!text() && props.queued}>
<GrowBox animate={!!props.animate} open={!!props.queued}>
<div data-slot="user-message-queued-indicator">
<TextShimmer text={i18n.t("ui.message.queued")} />
<Show when={props.onDeleteQueued}>
<IconButton
icon="trash"
label={i18n.t("ui.message.deleteQueued")}
size="xs"
variant="ghost"
onClick={props.onDeleteQueued}
/>
</Show>
</div>
</GrowBox>
</Show>
<Show when={text()}>
<>
<div data-slot="user-message-copy-wrapper" data-interrupted={props.interrupted ? "" : undefined}>
<Show when={metaHead() || metaTail()}>
<span data-slot="user-message-meta-wrap">
Expand Down
22 changes: 22 additions & 0 deletions packages/kilo-vscode/src/KiloProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -826,6 +826,9 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
case "deleteSession":
await this.handleDeleteSession(message.sessionID)
break
case "deleteMessage":
await this.handleDeleteMessage(message.sessionID, message.messageID)
break
case "renameSession":
await this.handleRenameSession(message.sessionID, message.title)
break
Expand Down Expand Up @@ -1501,6 +1504,25 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
}
}

private async handleDeleteMessage(sessionID: string, messageID: string): Promise<void> {
if (!this.client) {
this.postMessage({ type: "error", message: "Not connected to CLI backend" })
return
}

try {
const dir = this.getWorkspaceDirectory(sessionID)
await this.client.session.deleteMessage({ sessionID, messageID, directory: dir }, { throwOnError: true })
} catch (error) {
console.error("[Kilo New] KiloProvider: Failed to delete message:", error)
this.postMessage({
type: "error",
message: getErrorMessage(error) || "Failed to delete message",
sessionID,
})
}
}

/**
* Handle renaming a session.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,9 @@ export const MessageList: Component<MessageListProps> = (props) => {
sessionID={session.currentSessionID() ?? ""}
messageID={msg.id}
queued={queued()}
onDeleteQueued={
queued() ? () => session.deleteMessage(session.currentSessionID() ?? "", msg.id) : undefined
}
/>
)
}}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ interface VscodeSessionTurnProps {
sessionID: string
messageID: string
queued?: boolean
onDeleteQueued?: () => void
}

export const VscodeSessionTurn: Component<VscodeSessionTurnProps> = (props) => {
Expand Down Expand Up @@ -169,6 +170,7 @@ export const VscodeSessionTurn: Component<VscodeSessionTurnProps> = (props) => {
parts={parts() as unknown as Parameters<typeof UserMessageDisplay>[0]["parts"]}
interrupted={interrupted()}
queued={props.queued}
onDeleteQueued={props.onDeleteQueued}
onRevert={
assistantMessages().length > 0 && !session.revert()
? () => {
Expand Down
11 changes: 11 additions & 0 deletions packages/kilo-vscode/webview-ui/src/context/session.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,7 @@ interface SessionContextValue {
loadSessions: () => void
selectSession: (id: string) => void
deleteSession: (id: string) => void
deleteMessage: (sessionID: string, messageID: string) => void
renameSession: (id: string, title: string) => void
syncSession: (sessionID: string) => void

Expand Down Expand Up @@ -1643,6 +1644,15 @@ export const SessionProvider: ParentComponent = (props) => {
vscode.postMessage({ type: "deleteSession", sessionID: id })
}

function deleteMessage(sessionID: string, messageID: string) {
if (!server.isConnected()) {
console.warn("[Kilo New] Cannot delete message: not connected")
return
}
handleMessageRemoved(sessionID, messageID)
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CRITICAL: Queued deletes cannot succeed while the session is busy

onDeleteQueued is only exposed for queued prompts, which only exist while another turn is still running. The server-side delete route calls SessionPrompt.assertNotBusy(), so this request is rejected every time the button is shown. Because the message is removed optimistically before the request returns, the UI looks successful until the session reloads. This needs either a queue-aware backend delete path or a rollback instead of unconditional local removal.

vscode.postMessage({ type: "deleteMessage", sessionID, messageID })
}

function renameSession(id: string, title: string) {
if (!server.isConnected()) {
console.warn("[Kilo New] Cannot rename session: not connected")
Expand Down Expand Up @@ -1878,6 +1888,7 @@ export const SessionProvider: ParentComponent = (props) => {
loadSessions,
selectSession,
deleteSession,
deleteMessage,
renameSession,
syncSession,
cloudPreviewId,
Expand Down
7 changes: 7 additions & 0 deletions packages/kilo-vscode/webview-ui/src/types/messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1753,6 +1753,12 @@ export interface DeleteSessionRequest {
sessionID: string
}

export interface DeleteMessageRequest {
type: "deleteMessage"
sessionID: string
messageID: string
}

export interface RenameSessionRequest {
type: "renameSession"
sessionID: string
Expand Down Expand Up @@ -2364,6 +2370,7 @@ export type WebviewMessage =
| QuestionReplyRequest
| QuestionRejectRequest
| DeleteSessionRequest
| DeleteMessageRequest
| RenameSessionRequest
| RequestAutocompleteSettingsMessage
| UpdateAutocompleteSettingMessage
Expand Down
7 changes: 6 additions & 1 deletion packages/opencode/src/server/routes/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -699,7 +699,12 @@ export const SessionRoutes = lazy(() =>
),
async (c) => {
const params = c.req.valid("param")
await SessionPrompt.assertNotBusy(params.sessionID)
// kilocode_change start
const messages = await Session.messages({ sessionID: params.sessionID })
const target = messages.find((msg) => msg.info.id === params.messageID)
const newer = messages.find((msg) => msg.info.role === "user" && msg.info.id > params.messageID)
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CRITICAL: This still rejects the common single-queued-message case

Using has newer user message as the proxy for is queued misses the newest queued prompt, because the last queued user message has no later user after it. In the common case of one active turn plus one queued prompt, newer is undefined, so the route still falls back to assertNotBusy() and the delete fails. It also means the currently active user message would bypass the busy guard whenever another queued prompt exists. The check needs to identify messages after the active turn, not messages with any later user in history.

if (!target || target.info.role !== "user" || !newer) await SessionPrompt.assertNotBusy(params.sessionID)
// kilocode_change end
await Session.removeMessage({
sessionID: params.sessionID,
messageID: params.messageID,
Expand Down
1 change: 1 addition & 0 deletions packages/ui/src/i18n/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,7 @@ export const dict: Record<string, string> = {
"ui.message.duration.minutesSeconds": "{{minutes}}m {{seconds}}s",
"ui.message.interrupted": "Interrupted",
"ui.message.queued": "Queued",
"ui.message.deleteQueued": "Remove queued message",
"ui.message.attachment.alt": "attachment",

"ui.patch.action.deleted": "Deleted",
Expand Down
Loading