Skip to content
Merged
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/optimistic-worktree-prompts.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"kilo-code": patch
---

Show initial Agent Manager worktree prompts immediately while they are being sent.
84 changes: 84 additions & 0 deletions packages/kilo-vscode/tests/fixtures/session-provider-activity.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ const { LanguageContext } = await import("../../webview-ui/src/context/language"
const { NotificationsProvider } = await import("../../webview-ui/src/context/notifications")
const { ProviderProvider } = await import("../../webview-ui/src/context/provider")
const { SessionProvider, useSession, useSessionVisibility } = await import("../../webview-ui/src/context/session")
const { initialMessage } = await import("../../webview-ui/agent-manager/initial-message")
const { post } = await import("../../webview-ui/src/utils/webview-message")
const { terminal } = await import("../../webview-ui/src/context/session-outcome")
const { PromptInput } = await import("../../webview-ui/src/components/chat/PromptInput")
Expand Down Expand Up @@ -359,6 +360,89 @@ try {
assert.equal(value.selected(), null)
value.sendMessage("initial pending")
assert.equal(requests().length, 0)

for (const variant of ["high", "", undefined]) {
const id = `initial-${variant ?? "default"}`
const request = initialMessage({
type: "agentManager.sendInitialMessage",
projectId: "background-project",
sessionId: id,
text: "Initial worktree prompt",
providerID: "unloaded",
modelID: "unloaded",
agent: variant === undefined ? undefined : "ask",
variant,
files: [{ mime: "image/png", url: "data:image/png;base64,cHJvbXB0", filename: "prompt.png" }],
browserFeedback: { version: 1, references: [{ id: "element", sessionId: id, selector: "button" }] },
})
assert(request)
value.setCurrentSessionID("root")
value.submit(request)
const sent = requests().at(-1)
assert(sent?.type === "sendMessage" && sent.messageID)
assert.deepEqual(sent, { ...request, messageID: sent.messageID })
assert.equal(value.currentSessionID(), "root")
assert.equal(value.isSubmitting(id), true)
const optimistic = unwrap(value.allMessages()[id]?.at(0))
assert.equal(optimistic?.id, sent.messageID)
const parts = structuredClone(unwrap(value.getParts(sent.messageID)))
assert.equal(parts.length, 2)
assert.equal(parts.find((part) => part.type === "text")?.text, request.text)
assert.equal(parts.at(1)?.type, "file")

await emit({ type: "messagesLoaded", sessionID: id, messages: [], mode: "replace" })
assert.equal(value.allMessages()[id]?.at(0)?.id, sent.messageID)
assert.deepEqual(structuredClone(unwrap(value.getParts(sent.messageID))), parts)
if (variant === undefined) {
await emit({ ...sent, type: "sendMessageFailed", error: "Test send failed" })
assert.equal(value.allMessages()[id]?.length, 0)
assert.equal(value.getParts(sent.messageID).length, 0)
assert.equal(value.isSubmitting(id), false)
} else {
await emit({ type: "messageCreated", message: optimistic })
assert.equal(value.allMessages()[id]?.length, 1)
assert.deepEqual(structuredClone(unwrap(value.getParts(sent.messageID))), parts)
for (const [index, part] of parts.entries()) {
await emit({
type: "partUpdated",
sessionID: id,
messageID: sent.messageID,
part: { ...part, id: `${id}-part-${index}` },
})
}
assert.deepEqual(
value.getParts(sent.messageID).map((part) => part.id),
[`${id}-part-0`, `${id}-part-1`],
)
const response = {
id: `${id}-response`,
sessionID: id,
role: "assistant",
parentID: sent.messageID,
createdAt: info(id).createdAt,
time: { created: 1, completed: 2 },
finish: "tool-calls",
}
await emit({ type: "messagesLoaded", sessionID: id, messages: [optimistic, response] })
assert.equal(value.isSubmitting(id), true)
const completed =
variant === "high"
? { ...response, finish: "stop" }
: { ...response, finish: undefined, time: undefined, error: { name: "UnknownError" } }
const history = { type: "messagesLoaded", sessionID: id, messages: [optimistic, completed], mode: "reconcile" }
await emit(history)
assert.equal(value.isSubmitting(id), false)
value.submit({ ...request, text: "Queued follow-up" })
const queued = requests().at(-1)
assert(queued?.type === "sendMessage")
await emit(history)
assert.equal(value.isSubmitting(id), true)
await emit({ ...queued, type: "sendMessageFailed", error: "Test send failed" })
assert.equal(value.isSubmitting(id), false)
}
value.releaseSession(id)
}
value.setCurrentSessionID(undefined)
await emit({ type: "agentsLoaded", agents: [{ name: "code" }, { name: "ask" }], defaultAgent: "code" })
await emit({ type: "recentsLoaded", recents: [auto, first, external] })
await catalog("org-a", [first.modelID, recommended.modelID, auto.modelID], recommended.modelID)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1521,7 +1521,7 @@ const AgentManagerContent: Component = () => {
// Only send a message if there's text — otherwise just clear busy state
const init = initialMessage(ev)
if (init) {
vscode.postMessage(init)
session.submit(init)
}
// Clear busy state — use worktreeId from the message directly
// to avoid race condition where managedSessions() hasn't updated yet
Expand Down
2 changes: 2 additions & 0 deletions packages/kilo-vscode/webview-ui/src/context/session-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import type {
SessionStatus,
SessionStatusInfo,
SkillInfo,
SendMessageRequest,
SuggestionRequest,
TodoItem,
ToolPart,
Expand Down Expand Up @@ -164,6 +165,7 @@ export interface SessionContextValue {
revertSession: (messageID: string, partID?: string) => void
unrevertSession: () => void
deleteQueuedMessage: (sessionID: string, messageID: string) => Promise<boolean>
submit: (input: SendMessageRequest) => void
sendMessage: (
text: string,
providerID?: string,
Expand Down
34 changes: 30 additions & 4 deletions packages/kilo-vscode/webview-ui/src/context/session.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ import type {
ExtensionMessage,
FileAttachment,
SendMessageFailedMessage,
SendMessageRequest,
McpStatusEntry,
MessageLoadMode,
ToolPart,
Expand Down Expand Up @@ -1231,6 +1232,22 @@ export const SessionProvider: ParentComponent = (props) => {
const mode = input.mode ?? "replace"
const reset = mode === "prepend"

if (submissionMap[sessionID]) {
for (const message of messages) {
if (message.role !== "assistant" || !message.parentID) continue
if (pendingSubmissions.get(message.parentID) !== sessionID) continue
if (
!message.error &&
(typeof message.time?.completed !== "number" ||
!message.finish ||
message.finish === "tool-calls" ||
message.finish === "unknown")
)
continue
finishSubmission(message.parentID)
}
}

// Reconcile fast-path: if the tail matches local state shape-wise, every
// message+part-count already agrees with the server. Skip the reactive
// store churn entirely — virtualizer and rendering stay untouched.
Expand Down Expand Up @@ -2079,6 +2096,17 @@ export const SessionProvider: ParentComponent = (props) => {
queueMicrotask(() => window.dispatchEvent(new CustomEvent("resumeAutoScroll")))
}

function submit(input: SendMessageRequest) {
const messageID = input.messageID ?? Identifier.ascending("message")
const scope = input.draftID ?? input.sessionID
if (scope) {
clearClose(scope)
addOptimistic(scope, messageID, input.text, input.files, input.review, input.browserFeedback)
startSubmission(scope, messageID)
}
vscode.postMessage({ ...input, messageID })
}

function available(selection: ModelSelection | null): selection is ModelSelection {
const resolved = resolveModelSelection({ ...environment(), override: selection })
if (selection && resolved?.providerID === selection.providerID && resolved.modelID === selection.modelID)
Expand Down Expand Up @@ -2143,17 +2171,14 @@ export const SessionProvider: ParentComponent = (props) => {
const scope = effectiveDraftID ?? sid
if (!sid && !draftID && effectiveDraftID) agentDrafts.seed(effectiveDraftID)
if (scope) {
clearClose(scope)
addOptimistic(scope, messageID, text, files, review, browserFeedback)
startSubmission(scope, messageID)
if (!sid && (!draftID || draftSessionID() === scope)) {
setUserClearedSession(false)
setDraftSessionID(scope)
}
}
const agent = promptAgent(scope)

vscode.postMessage({
submit({
type: "sendMessage",
text,
messageID,
Expand Down Expand Up @@ -2914,6 +2939,7 @@ export const SessionProvider: ParentComponent = (props) => {
revertSession,
unrevertSession,
deleteQueuedMessage,
submit,
sendMessage,
sendCommand,
abort,
Expand Down
Loading