Skip to content
Open
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
5 changes: 5 additions & 0 deletions .changeset/sunny-banks-boil.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@kilocode/cli": patch
---

Show the PID for running shell commands in the CLI.
5 changes: 5 additions & 0 deletions cli/src/state/atoms/__tests__/effects-command-output.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,14 @@ describe("Command Execution Status - CLI-Only Workaround", () => {
it("should synthesize command_output ask immediately on start and update on exit", () => {
const executionId = "test-exec-123"
const command = "sleep 10"
const pid = 4242

// Simulate command started
const startedStatus: CommandExecutionStatus = {
status: "started",
executionId,
command,
pid,
}

const startedMessage: ExtensionMessage = {
Expand All @@ -52,6 +54,7 @@ describe("Command Execution Status - CLI-Only Workaround", () => {
expect(pendingUpdates.get(executionId)).toEqual({
output: "",
command: "sleep 10",
pid,
})

// Verify synthetic command_output ask was created IMMEDIATELY
Expand All @@ -70,6 +73,7 @@ describe("Command Execution Status - CLI-Only Workaround", () => {
executionId: "test-exec-123",
command: "sleep 10",
output: "",
pid,
})

// Simulate command exited without any output
Expand All @@ -93,6 +97,7 @@ describe("Command Execution Status - CLI-Only Workaround", () => {
output: "",
command: "sleep 10",
completed: true,
pid,
})

// Verify the ask was updated to mark as complete (not partial)
Expand Down
55 changes: 42 additions & 13 deletions cli/src/state/atoms/effects.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,14 +49,23 @@ const messageBufferAtom = atom<ExtensionMessage[]>([])
*/
const isProcessingBufferAtom = atom<boolean>(false)

export type PendingOutputUpdate = { output: string; command?: string; completed?: boolean; pid?: number }

function hasNumericPid(value: unknown): value is { pid: number } {
return (
typeof value === "object" &&
value !== null &&
"pid" in value &&
typeof (value as { pid?: unknown }).pid === "number"
)
}

/**
* Map to store pending output updates for command_output asks
* Key: executionId, Value: latest output data
* Exported so extension.ts can apply pending updates when asks appear
*/
export const pendingOutputUpdatesAtom = atom<Map<string, { output: string; command?: string; completed?: boolean }>>(
new Map<string, { output: string; command?: string; completed?: boolean }>(),
)
export const pendingOutputUpdatesAtom = atom<Map<string, PendingOutputUpdate>>(new Map<string, PendingOutputUpdate>())

/**
* Map to track which commands have shown a command_output ask
Expand Down Expand Up @@ -423,23 +432,31 @@ export const messageHandlerEffectAtom = atom(null, (get, set, message: Extension
// Initialize with command info
// IMPORTANT: Store the command immediately so it's available even if no output is produced
const command = "command" in statusData ? (statusData.command as string) : undefined
const updateData: { output: string; command?: string; completed?: boolean } = {
const updateData: { output: string; command?: string; completed?: boolean; pid?: number } = {
output: "",
command: command || "", // Always set command, even if empty
}
if (hasNumericPid(statusData)) {
updateData.pid = statusData.pid
}
newPendingUpdates.set(statusData.executionId, updateData)

const askPayload: { executionId: string; command: string; output: string; pid?: number } = {
executionId: statusData.executionId,
command: command || "",
output: "",
}
if (hasNumericPid(statusData)) {
askPayload.pid = statusData.pid
}

// CLI-ONLY WORKAROUND: Immediately create a synthetic command_output ask
// This allows users to abort the command even before any output is produced
const syntheticAsk: ExtensionChatMessage = {
ts: Date.now(),
type: "ask",
ask: "command_output",
text: JSON.stringify({
executionId: statusData.executionId,
command: command || "",
output: "",
}),
text: JSON.stringify(askPayload),
partial: true, // Mark as partial since command is still running
isAnswered: false,
}
Expand Down Expand Up @@ -470,14 +487,21 @@ export const messageHandlerEffectAtom = atom(null, (get, set, message: Extension
)

// Update with new output
const existing = newPendingUpdates.get(statusData.executionId) || { output: "" }
const existing: PendingOutputUpdate = newPendingUpdates.get(statusData.executionId) || {
output: "",
}
const command = "command" in statusData ? (statusData.command as string) : existing.command
const updateData: { output: string; command?: string; completed?: boolean } = {
const updateData: { output: string; command?: string; completed?: boolean; pid?: number } = {
output: statusData.output || "",
}
if (command) {
updateData.command = command
}
if (hasNumericPid(statusData)) {
updateData.pid = statusData.pid
} else if (existing.pid !== undefined) {
updateData.pid = existing.pid
}
if (existing.completed !== undefined) {
updateData.completed = existing.completed
}
Expand Down Expand Up @@ -505,6 +529,7 @@ export const messageHandlerEffectAtom = atom(null, (get, set, message: Extension
executionId: statusData.executionId,
command: command || "",
output: statusData.output || "",
...(existing.pid !== undefined && { pid: existing.pid }),
}),
partial: true, // Still running
}
Expand Down Expand Up @@ -534,7 +559,10 @@ export const messageHandlerEffectAtom = atom(null, (get, set, message: Extension
)

// Mark as completed and ensure command is preserved
const existing = newPendingUpdates.get(statusData.executionId) || { output: "", command: "" }
const existing: PendingOutputUpdate = newPendingUpdates.get(statusData.executionId) || {
output: "",
command: "",
}
// If command wasn't set yet (shouldn't happen but defensive), try to get it from statusData
const command =
existing.command || ("command" in statusData ? (statusData.command as string) : "")
Expand Down Expand Up @@ -582,6 +610,7 @@ export const messageHandlerEffectAtom = atom(null, (get, set, message: Extension
executionId: statusData.executionId,
command: pendingUpdate?.command || "",
output: pendingUpdate?.output || "",
...(pendingUpdate?.pid !== undefined && { pid: pendingUpdate.pid }),
...(exitCode !== undefined && { exitCode }),
}),
partial: false, // Command completed
Expand Down Expand Up @@ -693,7 +722,7 @@ export const disposeServiceEffectAtom = atom(null, async (get, set) => {
set(messageBufferAtom, [])

// Clear pending output updates
set(pendingOutputUpdatesAtom, new Map<string, { output: string; command?: string; completed?: boolean }>())
set(pendingOutputUpdatesAtom, new Map<string, PendingOutputUpdate>())

// Clear command output ask tracking
set(commandOutputAskShownAtom, new Map<string, boolean>())
Expand Down
8 changes: 5 additions & 3 deletions cli/src/state/atoms/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import type {
McpServer,
ModeConfig,
} from "../../types/messages.js"
import type { PendingOutputUpdate } from "./effects.js"
import { pendingOutputUpdatesAtom } from "./effects.js"

/**
Expand Down Expand Up @@ -630,7 +631,7 @@ function reconcileMessages(
incoming: ExtensionChatMessage[],
versionMap: Map<number, number>,
streamingSet: Set<number>,
pendingUpdates?: Map<string, { output: string; command?: string; completed?: boolean }>,
pendingUpdates?: Map<string, PendingOutputUpdate>,
): ExtensionChatMessage[] {
// Create lookup map for current messages
const currentMap = new Map<number, ExtensionChatMessage>()
Expand Down Expand Up @@ -725,7 +726,7 @@ function reconcileMessages(
*/
function deduplicateCommandOutputAsks(
messages: ExtensionChatMessage[],
pendingUpdates?: Map<string, { output: string; command?: string; completed?: boolean }>,
pendingUpdates?: Map<string, PendingOutputUpdate>,
): ExtensionChatMessage[] {
const result: ExtensionChatMessage[] = []
let mostRecentUnansweredAsk: ExtensionChatMessage | null = null
Expand Down Expand Up @@ -755,7 +756,7 @@ function deduplicateCommandOutputAsks(
if (pendingUpdates && pendingUpdates.size > 0) {
// Find the active (non-completed) pending update
let activeExecutionId: string | null = null
let activeUpdate: { output: string; command?: string; completed?: boolean } | null = null
let activeUpdate: PendingOutputUpdate | null = null

for (const [execId, update] of pendingUpdates.entries()) {
if (!update.completed) {
Expand All @@ -778,6 +779,7 @@ function deduplicateCommandOutputAsks(
executionId: activeExecutionId,
command: activeUpdate.command || "",
output: activeUpdate.output || "",
...(activeUpdate.pid !== undefined && { pid: activeUpdate.pid }),
}),
partial: !activeUpdate.completed,
isAnswered: activeUpdate.completed || false,
Expand Down
9 changes: 7 additions & 2 deletions cli/src/ui/messages/extension/ask/AskCommandOutputMessage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import type { MessageComponentProps } from "../types.js"
import { getMessageIcon } from "../utils.js"
import { useTheme } from "../../../../state/hooks/useTheme.js"
import { getBoxWidth } from "../../../utils/width.js"
import type { PendingOutputUpdate } from "../../../../state/atoms/effects.js"
import { pendingOutputUpdatesAtom } from "../../../../state/atoms/effects.js"

export const AskCommandOutputMessage: React.FC<MessageComponentProps> = ({ message }) => {
Expand All @@ -16,26 +17,30 @@ export const AskCommandOutputMessage: React.FC<MessageComponentProps> = ({ messa
// Parse the message text to get initial command and executionId
let executionId = ""
let initialCommand = ""
let initialPid: number | undefined
try {
const data = JSON.parse(message.text || "{}")
executionId = data.executionId || ""
initialCommand = data.command || ""
initialPid = typeof data.pid === "number" ? data.pid : undefined
} catch {
// If parsing fails, use text directly
initialCommand = message.text || ""
}

// Get real-time output from pending updates (similar to webview's streamingOutput)
const pendingUpdate = executionId ? pendingUpdates.get(executionId) : undefined
const pendingUpdate: PendingOutputUpdate | undefined = executionId ? pendingUpdates.get(executionId) : undefined
const command = pendingUpdate?.command || initialCommand
const output = pendingUpdate?.output || ""
const pid = pendingUpdate?.pid ?? initialPid

return (
<Box flexDirection="column" marginY={1}>
<Box>
<Box width={getBoxWidth(3)} justifyContent="space-between">
<Text color={theme.semantic.info} bold>
{icon} Command Running
</Text>
{typeof pid === "number" && <Text color={theme.ui.text.secondary}>PID: {pid}</Text>}
</Box>

{command && (
Expand Down