Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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/quick-local-prompts.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"kilo-code": patch
---

Show the first Agent Manager Local prompt and progress indicator immediately while its session is being created.
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ import type {
ManagedSessionState,
SectionState,
SessionInfo,
SessionCreatedMessage,
BranchInfo,
} from "../src/types/messages"
import { IndexingProvider } from "../src/context/indexing"
Expand Down Expand Up @@ -1085,22 +1086,25 @@ const AgentManagerContent: Component = () => {
// backend follow-ups). Dedups HTTP + SSE firing together.
const unsubCreate = vscode.onMessage((msg) => {
if (msg.type !== "sessionCreated") return
const created = msg as { type: string; session: { id: string } }
if (localSessionIDs().includes(created.session.id)) return
const created = msg as SessionCreatedMessage
const pending = created.draftID && localSessionIDs().includes(created.draftID) ? created.draftID : undefined
if (!pending && localSessionIDs().includes(created.session.id)) return
if (worktreeSessionIds().has(created.session.id)) return
const pending = selection() === LOCAL ? activePendingId() : undefined

const active = activePendingId()
const focus = !pending || (selection() === LOCAL && pending === active)
if (pending) {
setLocalSessionIDs((prev) => prev.map((id) => (id === pending ? created.session.id : id)))
tabOrderSync.replaceOrAppend(LOCAL, pending, created.session.id)
setActivePendingId(undefined)
if (pending === active) setActivePendingId(undefined)
} else {
saveTabMemory()
setLocalSessionIDs((prev) => [...prev, created.session.id])
tabOrderSync.append(LOCAL, created.session.id)
setSelection(LOCAL)
}
vscode.postMessage({ type: "agentManager.persistSession", sessionId: created.session.id })
session.selectSession(created.session.id)
if (focus) session.selectSession(created.session.id)
})

// Mark sessions loaded as soon as the session context receives data (even if empty)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ export const WorkingIndicator: Component = () => {

return (
<div class="working-indicator-slot">
<Show when={session.status() !== "idle" && !blocked()}>
<Show when={session.submitting() || (session.status() !== "idle" && !blocked())}>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

SUGGESTION: Elapsed timer won't count during the draft-submission phase

The elapsed createEffect at line 22–38 gates itself on status === 'idle' || !since (line 26). When submitting() is true but no real session has been created yet, status() reads only currentSessionID() (not the draftSessionID fallback), so it returns 'idle' and the timer short-circuits immediately. The spinner will show (submitting() === true) but the elapsed counter stays at 0s.

This is a minor cosmetic gap rather than a logic bug — the busySince timestamp is correctly set via startSubmission — but users will see the spinner appear without the elapsed timer counting until the backend session is created and the status transitions away from 'idle'.

Consider updating the createEffect guard to also continue when submitting() is true:

if ((status === 'idle' && !session.submitting()) || !since) {
  setElapsed(0)
  return
}

<div class="working-indicator">
<Spinner />
<span class="working-text">{statusText()}</span>
Expand Down
145 changes: 127 additions & 18 deletions packages/kilo-vscode/webview-ui/src/context/session.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,7 @@ interface SessionContextValue {
closeReason: Accessor<SessionCloseReason | undefined>
statusText: Accessor<string | undefined>
busySince: Accessor<number | undefined>
submitting: Accessor<boolean>
loading: Accessor<boolean>
loadingOlderMessages: Accessor<boolean>
hasOlderMessages: Accessor<boolean>
Expand Down Expand Up @@ -300,6 +301,8 @@ export const SessionProvider: ParentComponent = (props) => {
const [statusMap, setStatusMap] = createStore<Record<string, SessionStatusInfo>>({})
const [closeMap, setCloseMap] = createStore<Record<string, SessionCloseReason | undefined>>({})
const [busySinceMap, setBusySinceMap] = createStore<Record<string, number>>({})
const [submissionMap, setSubmissionMap] = createStore<Record<string, number>>({})
const pendingSubmissions = new Map<string, string>()

const idle: SessionStatusInfo = { type: "idle" }

Expand All @@ -320,9 +323,13 @@ export const SessionProvider: ParentComponent = (props) => {
}),
)
const busySince = () => {
const id = currentSessionID()
const id = currentSessionID() ?? draftSessionID()
return id ? busySinceMap[id] : undefined
}
const submitting = () => {
const id = currentSessionID() ?? draftSessionID()
return id ? (submissionMap[id] ?? 0) > 0 : false
}

const [loading, setLoading] = createSignal(false)
const [loaded, setLoaded] = createSignal<Set<string>>(new Set())
Expand Down Expand Up @@ -429,6 +436,43 @@ export const SessionProvider: ParentComponent = (props) => {
// Prevents handleMessagesLoaded from wiping them when it replaces the array.
const pendingOptimistic = new Map<string, Set<string>>()

const startSubmission = (sid: string, messageID: string) => {
pendingSubmissions.set(messageID, sid)
setSubmissionMap(sid, (count = 0) => count + 1)
if (!busySinceMap[sid]) setBusySinceMap(sid, Date.now())
}
const finishSubmission = (messageID: string) => {
const sid = pendingSubmissions.get(messageID)
if (!sid) return
pendingSubmissions.delete(messageID)
const count = submissionMap[sid] ?? 0
if (count > 1) {
setSubmissionMap(sid, count - 1)
return
}
setSubmissionMap(
produce((map) => {
delete map[sid]
}),
)
if ((statusMap[sid] ?? idle).type !== "idle") return
setBusySinceMap(
produce((map) => {
delete map[sid]
}),
)
}
const confirmSubmissions = (sid: string) => {
for (const [id, scope] of pendingSubmissions) {
if (scope === sid) pendingSubmissions.delete(id)
}
setSubmissionMap(
produce((map) => {
delete map[sid]
}),
)
}

// Store for sessions, messages, parts, todos, modelSelections, agentSelections
const [store, setStore] = createStore<SessionStore>({
sessions: {},
Expand Down Expand Up @@ -1029,6 +1073,63 @@ export const SessionProvider: ParentComponent = (props) => {
batch(() => {
setStore("sessions", session.id, session)

if (draftID && submissionMap[draftID]) {
const submissions = submissionMap[draftID]
for (const [id, scope] of pendingSubmissions) {
if (scope === draftID) pendingSubmissions.set(id, session.id)
}
setSubmissionMap(session.id, (count = 0) => count + submissions)
setSubmissionMap(
produce((map) => {
delete map[draftID]
}),
)
if (busySinceMap[draftID] && !busySinceMap[session.id]) {
setBusySinceMap(session.id, busySinceMap[draftID])
}
setBusySinceMap(
produce((map) => {
delete map[draftID]
}),
)
}

const drafts = draftID ? store.messages[draftID] : undefined
if (draftID && drafts?.length) {
const current = store.messages[session.id] ?? []
const ids = new Set(current.map((message) => message.id))
const promoted = drafts
.filter((message) => !ids.has(message.id))
.map((message) => ({ ...message, sessionID: session.id }))
setStore("messages", session.id, [...current, ...promoted])
setStore(
"messages",
produce((messages) => {
delete messages[draftID]
}),
)

const pending = pendingOptimistic.get(draftID)
if (pending) {
const merged = pendingOptimistic.get(session.id) ?? new Set<string>()
for (const id of pending) merged.add(id)
pendingOptimistic.set(session.id, merged)
pendingOptimistic.delete(draftID)
}
setLoaded((prev) => {
if (prev.has(session.id)) return prev
const next = new Set(prev)
next.add(session.id)
return next
})
patchPage(session.id, { initialLoaded: true, lastMutation: "append" })
setPages(
produce((state) => {
delete state[draftID]
}),
)
}

// Only initialize messages if none exist yet — a cloud session import
// (handleCloudSessionImported) may have already populated messages for
// this session ID. The SSE session.created event can race with the
Expand Down Expand Up @@ -1395,6 +1496,7 @@ export const SessionProvider: ParentComponent = (props) => {
message?: string,
next?: number,
) {
confirmSubmissions(sessionID)
const prev = statusMap[sessionID] ?? { type: "idle" }
const info: SessionStatusInfo =
newStatus === "retry"
Expand All @@ -1406,7 +1508,7 @@ export const SessionProvider: ParentComponent = (props) => {
// Track busy start time and discard the previous turn's terminal state.
if (prev.type === "idle" && newStatus !== "idle") {
clearClose(sessionID)
setBusySinceMap(sessionID, Date.now())
if (!busySinceMap[sessionID]) setBusySinceMap(sessionID, Date.now())
}
if (newStatus === "idle") {
setBusySinceMap(
Expand Down Expand Up @@ -1536,12 +1638,14 @@ export const SessionProvider: ParentComponent = (props) => {
* by listening for the same sendMessageFailed event.
*/
function handleSendMessageFailed(message: SendMessageFailedMessage) {
if (message.sessionID && message.messageID) {
pendingOptimistic.get(message.sessionID)?.delete(message.messageID)
const sid = message.sessionID ?? message.draftID
if (message.messageID) finishSubmission(message.messageID)
if (sid && message.messageID) {
pendingOptimistic.get(sid)?.delete(message.messageID)
stash.remove(message.messageID)
batch(() => {
setStore("messages", message.sessionID!, (msgs = []) => msgs.filter((m) => m.id !== message.messageID))
dropMessageTools(message.sessionID!, message.messageID!)
setStore("messages", sid, (msgs = []) => msgs.filter((m) => m.id !== message.messageID))
dropMessageTools(sid, message.messageID!)
setStore(
"parts",
produce((parts) => {
Expand Down Expand Up @@ -1673,6 +1777,7 @@ export const SessionProvider: ParentComponent = (props) => {

function handleSessionDeleted(sessionID: string) {
pendingOptimistic.delete(sessionID)
confirmSubmissions(sessionID)
batch(() => {
// Collect message IDs so we can clean up their parts (store + stash)
const msgs = store.messages[sessionID] ?? []
Expand Down Expand Up @@ -1997,12 +2102,14 @@ export const SessionProvider: ParentComponent = (props) => {
for (const q of scopedQuestions(sid)) {
rejectQuestion(q.id)
}
if (sid) {
clearClose(sid)
addOptimistic(sid, messageID, text, files)
}

const scope = draftID ?? sid
if (scope) {
clearClose(scope)
addOptimistic(scope, messageID, text, files)
startSubmission(scope, messageID)
if (!sid) setDraftSessionID(scope)
}
const agent = promptAgent(scope)

vscode.postMessage({
Expand Down Expand Up @@ -2063,12 +2170,13 @@ export const SessionProvider: ParentComponent = (props) => {
rejectQuestion(q.id)
}

if (sid) {
clearClose(sid)
addOptimistic(sid, messageID, `/${command} ${args}`.trim(), files)
}

const scope = draftID ?? sid
if (scope) {
clearClose(scope)
addOptimistic(scope, messageID, `/${command} ${args}`.trim(), files)
startSubmission(scope, messageID)
if (!sid) setDraftSessionID(scope)
}
const agent = promptAgent(scope)

vscode.postMessage({
Expand Down Expand Up @@ -2334,7 +2442,7 @@ export const SessionProvider: ParentComponent = (props) => {
}

const pageState = () => {
const id = currentSessionID()
const id = currentSessionID() ?? draftSessionID()
return id ? (pages[id] ?? emptyPageState) : emptyPageState
}

Expand All @@ -2343,7 +2451,7 @@ export const SessionProvider: ParentComponent = (props) => {
const messageMutation = () => pageState().lastMutation

const messages = () => {
const id = currentSessionID()
const id = currentSessionID() ?? draftSessionID()
return id ? store.messages[id] || [] : []
}

Expand Down Expand Up @@ -2387,7 +2495,7 @@ export const SessionProvider: ParentComponent = (props) => {
})

const visibleMessages = createMemo(() => {
const id = currentSessionID()
const id = currentSessionID() ?? draftSessionID()
return id ? visible(id) : []
})

Expand Down Expand Up @@ -2526,6 +2634,7 @@ export const SessionProvider: ParentComponent = (props) => {
closeReason,
statusText,
busySince,
submitting,
loading,
loadingOlderMessages,
hasOlderMessages,
Expand Down
Loading