Skip to content
Open
Show file tree
Hide file tree
Changes from 3 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
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 @@ -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,14 @@ 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
}
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 +1887,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
8 changes: 7 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,13 @@ 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 user = messages.filter((msg) => msg.info.role === "user")
const target = user.find((msg) => msg.info.id === params.messageID)
const active = user.at(0)
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: active points at the oldest user message, not the running one

Session.messages() is consumed throughout the app as chronological history, so user.at(0) selects the first prompt in the session. While the session is busy, every later user message now skips assertNotBusy(), including the currently active prompt, so this can allow deleting the turn that is still being processed. The queued/delete check needs to compare against the latest active user turn, not the first user in history.

if (!target || target.info.id <= (active?.info.id ?? "")) 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