Skip to content

feat(agent-manager): run setup scripts in panel terminal - #12703

Merged
marius-kilocode merged 5 commits into
mainfrom
feat/agent-manager-setup-terminal
Jul 30, 2026
Merged

feat(agent-manager): run setup scripts in panel terminal#12703
marius-kilocode merged 5 commits into
mainfrom
feat/agent-manager-setup-terminal

Conversation

@marius-kilocode

Copy link
Copy Markdown
Collaborator

Agent Manager setup scripts currently run as VS Code tasks even when the terminal destination is set to the Agent Manager panel. That hides provisioning output outside the worktree context, makes the displayed destination disagree with execution, and leaves failures difficult to inspect.

This routes setup through the same authenticated PTY runtime used by Run scripts when Agent Manager panel is selected, while preserving the integrated task path for VS Code terminal. Each worktree receives an independent Setup terminal with live and replayable output, explicit stop behavior, failure annotations, process-tree cleanup, and best-effort session creation after failures or timeouts. Panel-local destination state is authoritative from the first selection, including restored panels and multi-window setting echoes.

The side terminal reveals ambiently during provisioning, restores the previous layout after success unless the user interacted with it, and remains open on failure. Setup tabs created while their worktree is in the background are activated within that context without replacing an existing user-selected terminal.

Closes #12649

Agent Manager Setup terminal showing a failed setup script and retained output

@marius-kilocode
marius-kilocode force-pushed the feat/agent-manager-setup-terminal branch from c49acd6 to 59bfcfd Compare July 30, 2026 16:21
/>
</Show>
<Show when={!contextEmpty() && !history()}>
<Show when={showDetailStack()}>

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.

CRITICAL: The gate change drops the unassigned-session case, blanking the detail pane

The old gate was !contextEmpty() && !history(). contextEmpty() explicitly return false for a null selection (line 672), so the detail stack did render for unassigned sessions. The new gate is showTerminalStack(history(), selection()) = !history && selection !== null, which is false whenever selection() is null.

selectUnassigned() sets setSelection(null) and is reachable from the sidebar and focusSidebarItem. Both ChatView and the read-only banner live inside this stack (lines 2494-2501), and readOnly() (line 723) is defined as selection() === null && !!session.currentSessionID() — so it is now dead code and clicking an unassigned session renders nothing. The sibling <Show when={contextEmpty()}> at 2373 is also false in that state, so there is no fallback.

Secondary effect: the stack also hosts renderTerminalLayer and the side-terminal host, so selecting an unassigned session unmounts every live xterm — contradicting the invariant in the comment right below ("Keep terminal tabs mounted so output streams across worktree switches").

Suggest preserving the old semantics, e.g. !history && (selection !== null || !contextEmpty()).


Reply with @kilocode-bot fix it to have Kilo Code address this issue.

{/* Session-less context (e.g. a worktree mid-provisioning): the
empty state lives in the main pane so the side terminal
panel can render next to it. */}
<Show when={contextEmpty()}>

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.

WARNING: Two empty states now render at the same time

The pre-existing empty state at line 2373 (<Show when={contextEmpty()}> directly under .am-detail) was kept, and the detail stack is no longer gated on !contextEmpty(). Whenever contextEmpty() && selection() !== null && !history() — exactly the new provisioning flow this PR targets — both blocks render: the outer generic "No sessions" + New Session button, and this inner spinner/progress block. .am-detail is a flex column and .am-empty-state is flex: 1, so the user sees two stacked panels.

Worse, the outer button calls handleAddSession, which now early-returns on settingUpSelection() (line ~1899) — a visibly enabled button that silently does nothing. One of the two blocks should be removed.


Reply with @kilocode-bot fix it to have Kilo Code address this issue.

const sel = selection()
// A live Setup script terminal shows progress and failures on its own
// tab; never cover it with the blocking overlay.
if (typeof sel === "string" && sel !== LOCAL && hasSetupTerminal(nsKey(sel), terms.sides())) return null

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.

WARNING: This suppresses every later overlay for the worktree, not just the setup-progress one

hasSetupTerminal matches any Setup record regardless of state, and an exited/failed Setup tab is deliberately retained in terminal state until the user closes it. Because this early-return sits above the state.active check, once a worktree has run an embedded setup script every subsequent worktreeSetup overlay for that worktree is swallowed — including the genuine error paths from createSessionInWorktree ("Failed to create session: …", "Not connected to CLI backend").

In that flow discardWorktree then rolls the worktree back, so the user watches the worktree disappear with no error shown anywhere. Gating on a live status (running/stopping) would match the comment's stated intent ("A live Setup script terminal…") and keep error overlays working.


Reply with @kilocode-bot fix it to have Kilo Code address this issue.

export function createSideTerminal(deps: SideTerminalDeps) {
const [local, setLocal] = createSignal<TerminalDestination | undefined>(deps.saved)
const [destination, setDestination] = createSignal<TerminalDestination>(deps.saved ?? "vscode")
if (deps.saved) deps.postMessage({ type: "agentManager.terminal.destinationSelected", destination: deps.saved })

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.

WARNING: Restoring panel-local state rewrites the user's application-scoped setting

This fires on every createSideTerminal call, i.e. every webview construction/reload — not only on an explicit pick. The extension treats destinationSelected as a deliberate choice: handleDestination calls state.select() (latching local = true) and writeTerminalDestination(...) with ConfigurationTarget.Global.

So merely reopening a panel that holds a stale saved value silently reverts the global setting a user changed afterwards in the Settings UI (or that another window wrote), and broadcasts terminal.destinationChanged to every other window — the cross-window fight the choose() doc comment above says this design avoids. Before this PR a restore never wrote.

Suggest distinguishing restore from pick (separate message, or a restore: true flag that skips the config write).


Reply with @kilocode-bot fix it to have Kilo Code address this issue.

log: (message: string) => void,
): boolean {
if (message.type !== "agentManager.terminal.destinationSelected") return false
const destination = resolveTerminalDestination(message.destination)

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: Coercing an invalid IPC payload and then persisting it

resolveTerminalDestination falls back to "vscode" for anything it doesn't recognise. That's the right behaviour when reading a stale setting, but here the coerced value is written straight into the user's global settings on the next line. A malformed or unexpected destination therefore silently overwrites a real setting instead of being ignored. Returning early when message.destination is not "vscode" | "agentManager" would make this write-path safe at the trust boundary.


Reply with @kilocode-bot fix it to have Kilo Code address this issue.

const decision = ambientDecision(deps.terms.scriptStatus(ambient.terminalId), deps.selection(), ambient.contextKey)
if (decision === "wait") return
setPending(undefined)
if (decision === "hide") deps.setSidePanel(null)

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.

WARNING: The auto-hide closes whatever the side panel currently shows, not just the terminal

The effect never reads deps.sidePanel() before hiding, and cancel() is only wired to terminal-panel engagement in AgentManagerApp.tsx (hide, onTerminalDestinationOpen, onClose, onCloseOthers, onStart, onStop). It is not wired to toggleDiffPanel() / the toggleDiff keybinding, nor to openReviewTab(), all of which call setSidePanel("diff" | "pr") while a reveal is pending.

Repro: setup starts and ambiently reveals the panel → user opens the diff panel → setup exits 0 → ambientDecision still returns "hide" (it only compares selection vs contextKey) and this line closes the diff panel the user deliberately opened. That breaks the "user content is never pulled away" invariant in this module's header. if (decision === "hide" && deps.sidePanel() === "terminal") would be enough.


Reply with @kilocode-bot fix it to have Kilo Code address this issue.

}
if (status.state === "exited" && status.exitCode !== 0) {
failureWritten = true
term.writeln(`\r\n\x1b[31m[${t("agentManager.terminal.setupFailedCode")} ${status.exitCode ?? "?"}]\x1b[0m`)

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: Concatenating the exit code onto a translated string breaks word order

t() supports {{param}} templates (webview-ui/src/context/language.tsxresolveTemplate), and this dict already uses them elsewhere ("agentManager.worktree.versions": "{{count}} versions"). Gluing the number on the end forces English word order onto every locale — the new translations render as "Setup-Skript mit Exit-Code fehlgeschlagen 1" (de), "セットアップスクリプトが終了コードで失敗しました 1" (ja), "종료 코드로 설정 스크립트 실패 1" (ko), and lands even worse in the RTL locales (ar, fa).

A key like "setup script failed with exit code {{code}}" plus t(key, { code: status.exitCode ?? "?" }) fixes all locales and lets the split setupFailed / setupFailedCode pair collapse into one key.

Separately: streamed (line ~225) is a plain local, and the effect below only tracks props.status?.(). If the failed snapshot lands before the first frame and the socket then stays open — the exact case the comment above calls out ("or stay open when a background child outlives the script") — noteFailure() is never reached again and no annotation is written. Setting streamed through a signal, or calling noteFailure() from ws.onmessage, would close that gap.


Reply with @kilocode-bot fix it to have Kilo Code address this issue.

*/
export function terminalClosable(status: ScriptTerminalStatus | undefined): boolean {
if (status?.kind !== "setup") return true
return status.state !== "running" && status.state !== "stopping"

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: A Setup tab in stopping has neither a stop nor a close affordance

terminalStoppable returns true only for "running", and this returns false for both "running" and "stopping". In stopping the tab therefore renders no button at all (SortableTerminalTab.tsx gates both <Show> blocks on these predicates). If the stop never settles — wedged backend PTY, lost snapshot — the tab is permanently unclosable from the UI. Consider keeping close enabled in stopping, or keeping the stop button visible-but-disabled so the state stays legible.


Reply with @kilocode-bot fix it to have Kilo Code address this issue.

starts.push({ kind, config, done })
if (opts?.startError) return Promise.reject(opts.startError)
if (opts?.gate) return opts.gate.promise
stops.length = 0

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: A recorder that clears itself weakens future assertions

stops is reset on every start() invocation, so any test that starts twice silently loses the first handle's stop() calls and expect(ctx.stops).toEqual([...]) becomes weaker than it reads. A recorder should only append — dropping this line keeps the harness honest.

Two related test-quality notes while you're here:

  • harness() returns a handle with no kill, a shape production never produces (ScriptTerminalManager.start always returns { stop, kill }). The "times out, rejects, and stops the process tree" case therefore exercises halt()'s fallback rather than the real path; giving the shared harness a kill and covering the fallback in one dedicated test would stop it drifting.
  • The new if (message === "Setup script was stopped") return suppression in setup-script-task.ts and the embedded (agentManager) failure/timeout status: "error" post are both untested — those are exactly the paths a regression would silently break.

Reply with @kilocode-bot fix it to have Kilo Code address this issue.


Kilo runs the script automatically whenever a new worktree is created. It uses `sh` for POSIX scripts, PowerShell for `.ps1`, and `cmd.exe` for `.cmd` / `.bat`, so executable permissions are not required.

Where the script runs follows the terminal destination dropdown in the Agent Manager toolbar. **Agent Manager panel** shows live output in a named `Setup` tab in the side terminal panel. After success, the panel returns to its previous state unless you interacted with it; the retained tab remains available for review. Failures keep the panel open. **VS Code terminal** runs setup as a task in the integrated terminal. The script keeps the existing five-minute timeout; when it expires, the setup process tree is terminated and the failed tab retains its partial output.

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: The timeout sentence is only true for the panel destination

"The script keeps the existing five-minute timeout; when it expires, the setup process tree is terminated and the failed tab retains its partial output" directly follows the VS Code terminal sentence and opens destination-agnostically, so it reads as applying to both. It only holds for the Agent Manager panel path (setup-script-task.ts halt()manager.kill()). task-runner.ts merely rejects on timeout — it never calls execution.terminate() — so with VS Code terminal the task process keeps running and there is no "failed tab". Suggest scoping the termination/retained-output clause to the panel destination.


Reply with @kilocode-bot fix it to have Kilo Code address this issue.

@kilo-code-bot

kilo-code-bot Bot commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

Code Review Summary

Status: 18 Issues Found | Recommendation: Address before merge

Overview

Severity Count
CRITICAL 1
WARNING 9
SUGGESTION 8
Issue Details (click to expand)

CRITICAL

File Line Issue
webview-ui/agent-manager/AgentManagerApp.tsx 2433 showTerminalStack gate drops selection() === null, so the unassigned-session detail pane renders nothing and every live xterm unmounts

WARNING

File Line Issue
webview-ui/agent-manager/AgentManagerApp.tsx 2447 Outer (2373) and inner empty states now render simultaneously; outer New Session button is a silent no-op during setup
webview-ui/agent-manager/AgentManagerApp.tsx 681 hasSetupTerminal early-return swallows every later worktreeSetup overlay for the worktree, including real session-creation errors
webview-ui/agent-manager/terminal/side.ts 95 Replaying the saved destination on every panel construction rewrites the global setting and broadcasts to other windows
src/agent-manager/ScriptTerminalManager.ts 205 Retained failed entry is still reconciled by sync(), so the tab it preserves gets dropped by the new 15s watchdog
src/agent-manager/ScriptTerminalManager.ts 303 Any transient backend error is treated as "PTY gone": drops a live entry and orphans the process tree
src/agent-manager/setup-script-task.ts 94 status: "error" is terminal in the webview but posted mid-flow; clears busy state and blanks the later "Starting session..." overlay
src/agent-manager/AgentManagerProvider.ts 1428 clearRun resolves the project bucket via this.context?.id while creation uses projectForScript; key mismatch leaks the PTY and blocks restarts
webview-ui/agent-manager/terminal/state.ts 738 closeSide returns true for a running Setup terminal the extension refuses to close, causing focus handoff with no close
webview-ui/agent-manager/terminal/ambient.ts 67 Auto-hide closes whatever the side panel shows; cancel() isn't wired to toggleDiffPanel / openReviewTab

SUGGESTION

File Line Issue
src/agent-manager/terminal-destination.ts 58 Invalid IPC destination is coerced to "vscode" and then persisted to global settings
src/agent-manager/setup-script-task.ts 132 Hardcoded "5 minutes" contradicts configurable timeoutMs; real worst case is 2 x ms. Stop detection compares an exact string across three layers
src/agent-manager/setup-script-task.ts 63 pickSetupTask silently drops watchdogMs while PickInput still advertises it
src/agent-manager/AgentManagerProvider.ts 1500 worktreeId !== "local" clause is dead; ":local" yields an empty-string project id
webview-ui/agent-manager/terminal/TerminalTab.tsx 242 Exit code concatenated onto a translated string breaks de/ja/ko/ar word order; streamed is non-reactive so the annotation can be lost
webview-ui/agent-manager/terminal/chrome.ts 32 A Setup tab in stopping renders neither stop nor close, so a wedged stop is unclosable
tests/unit/setup-script-task.test.ts 42 Recorder clears itself; harness handle has no kill (unlike production); stop-suppression and embedded failure posts untested
packages/kilo-docs/pages/automate/agent-manager.md 246 Timeout/termination clause is only true for the panel destination -- task-runner.ts never terminates the task
Files Reviewed (55 files)
  • .changeset/smart-pandas-remain.md
  • packages/core/test/pty/pty-session.test.ts
  • packages/kilo-docs/pages/automate/agent-manager.md - 1 issue
  • packages/kilo-vscode/src/agent-manager/AgentManagerProvider.ts - 2 issues
  • packages/kilo-vscode/src/agent-manager/ScriptTerminalManager.ts - 2 issues
  • packages/kilo-vscode/src/agent-manager/SetupScriptRunner.ts
  • packages/kilo-vscode/src/agent-manager/run/manager.ts
  • packages/kilo-vscode/src/agent-manager/script-terminal-runtime.ts
  • packages/kilo-vscode/src/agent-manager/setup-script-task.ts - 3 issues
  • packages/kilo-vscode/src/agent-manager/shell-env.ts
  • packages/kilo-vscode/src/agent-manager/terminal-destination.ts - 1 issue
  • packages/kilo-vscode/src/agent-manager/terminal-routing.ts
  • packages/kilo-vscode/src/agent-manager/types.ts
  • packages/kilo-vscode/tests/unit/agent-manager-ambient-setup.test.ts
  • packages/kilo-vscode/tests/unit/agent-manager-arch.test.ts
  • packages/kilo-vscode/tests/unit/agent-manager-terminal-chrome.test.ts
  • packages/kilo-vscode/tests/unit/agent-manager-terminal-destination.test.ts
  • packages/kilo-vscode/tests/unit/agent-manager-terminal-side.test.ts
  • packages/kilo-vscode/tests/unit/agent-manager-terminal-state.test.ts
  • packages/kilo-vscode/tests/unit/script-terminal-manager.test.ts
  • packages/kilo-vscode/tests/unit/setup-script-task.test.ts - 1 issue
  • packages/kilo-vscode/webview-ui/agent-manager/AgentManagerApp.tsx - 3 issues
  • packages/kilo-vscode/webview-ui/agent-manager/i18n/*.ts (21 locales)
  • packages/kilo-vscode/webview-ui/agent-manager/terminal/SideTerminalPanel.tsx
  • packages/kilo-vscode/webview-ui/agent-manager/terminal/SortableTerminalTab.tsx
  • packages/kilo-vscode/webview-ui/agent-manager/terminal/TerminalTab.tsx - 1 issue
  • packages/kilo-vscode/webview-ui/agent-manager/terminal/ambient.ts - 1 issue
  • packages/kilo-vscode/webview-ui/agent-manager/terminal/chrome.ts - 1 issue
  • packages/kilo-vscode/webview-ui/agent-manager/terminal/index.ts
  • packages/kilo-vscode/webview-ui/agent-manager/terminal/render.tsx
  • packages/kilo-vscode/webview-ui/agent-manager/terminal/side.ts - 1 issue
  • packages/kilo-vscode/webview-ui/agent-manager/terminal/state.ts - 1 issue
  • packages/kilo-vscode/webview-ui/src/stories/agent-manager.stories.tsx
  • packages/kilo-vscode/webview-ui/src/types/messages/extension-messages.ts
  • packages/kilo-vscode/webview-ui/src/types/messages/webview-messages.ts
Notes and assumptions
  • Reviewed at 7623676. The branch was updated mid-review (59bfcfd -> 7623676, picking up unrelated diff-scope changes); all findings were re-verified against the current HEAD before publishing.
  • Verified clean: all three new i18n keys exist in all 21 locale dictionaries; no HTML <img> tags in the docs change; IconButton icon="stop" resolves to a real glyph and carries an aria-label; no new listeners/timers without onCleanup; the ${projectId}:${kind}:${worktreeId} rekey correctly fixes the previous cross-project run:<worktreeId> collision; clear(..., true) / dispose(..., true) correctly bypass the new setup close-guard; no command injection (argv arrays, no shell) and no secret leakage via the new win32 process.env copy.
  • CI-detectable issues (lint, typecheck, tests, knip, kilocode_change markers, table padding, source links, visual baselines) were intentionally not reported.

Fix these issues in Kilo Cloud


Reviewed by claude-opus-5 · Input: 86 · Output: 37.1K · Cached: 5M

Review guidance: REVIEW.md from base branch main

@marius-kilocode
marius-kilocode merged commit a7d72e2 into main Jul 30, 2026
33 checks passed
@marius-kilocode
marius-kilocode deleted the feat/agent-manager-setup-terminal branch July 30, 2026 16:51
marius-kilocode added a commit that referenced this pull request Jul 31, 2026
The setup-terminal gate introduced in #12703 changed the detail-stack
predicate from !contextEmpty() && !history() to !history && selection
!== null. An unassigned session has selection === null but a live
session showing, so contextEmpty() is false there. The new gate
dropped that case, blanking the content pane, leaving readOnly() dead
code, and unmounting every live xterm on selection.

showTerminalStack now takes contextEmpty and returns !history &&
(selection !== null || !contextEmpty), restoring the pre-#12703
semantics while keeping the provisioning worktree (empty context with a
side Setup tab) rendering the stack.

Adds a showTerminalStack regression suite covering the unassigned,
history, selected-context, provisioning, and empty cases.
t7tran pushed a commit to t7tran/kilocode that referenced this pull request Aug 14, 2026
…tup-terminal

feat(agent-manager): run setup scripts in panel terminal
t7tran pushed a commit to t7tran/kilocode that referenced this pull request Aug 14, 2026
…g#12722)

The setup-terminal gate introduced in Kilo-Org#12703 changed the detail-stack
predicate from !contextEmpty() && !history() to !history && selection
!== null. An unassigned session has selection === null but a live
session showing, so contextEmpty() is false there. The new gate
dropped that case, blanking the content pane, leaving readOnly() dead
code, and unmounting every live xterm on selection.

showTerminalStack now takes contextEmpty and returns !history &&
(selection !== null || !contextEmpty), restoring the pre-Kilo-Org#12703
semantics while keeping the provisioning worktree (empty context with a
side Setup tab) rendering the stack.

Adds a showTerminalStack regression suite covering the unassigned,
history, selected-context, provisioning, and empty cases.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Execute Agent Manager setup scripts in the embedded terminal

2 participants