
Porter is a platform for deploying and managing applications on your own cloud infrastructure. Customers ship on their own AWS, GCP, or Azure accounts with the experience of a PaaS, so it's easy to inspect deployments, read logs, and debug issues.
That last one (debugging) is unsurprisingly where our users need the most help. When something breaks, the question is rarely "what broke?" Instead, our users are more likely to ask, "what changed?", "which service is unhealthy?", and "what should I look at next?"
At Porter, we're lucky enough to have a stellar support team that helps users get to the root cause of their issues, around the clock. This, however, has its limitations: chatting in Pylon or Slack pulls people out of context, especially when the details you need (deploy history, pod status, resource usage, logs) live in the Porter dashboard. That picture rarely comes from a single place. You often have to piece those details together.
To help users get that picture as quickly as possible without leaving their dashboard, we released an early version of DevOps Agent in April.
A slightly contrived example of using DevOps Agent, shown at 1.25× speed.
DevOps Agent is an AI assistant that can inspect the same things a DevOps expert would check first. It pulls from context already in their Porter dashboard, instead of making you reconstruct it in Slack.
Why not just ask ChatGPT? ChatGPT can help you reason through an infrastructure problem, but it can't see what's actually happening in your cluster unless you copy it over yourself. Inside Porter, we can provide that context through controlled tools without giving an AI chat assistant broad access to a user's cluster or secrets.
As I worked through the design and implementation of the interface, knowing we're only going to expose controlled tools was essential context. We were designing a new entry point into the work of debugging infrastructure, without asking users to do the legwork themselves.
With that in mind, two questions bubbled up during the design phase: Where should the agent live? And how much of its work should it show the end user?
First: users are trying to ship, debug, or understand their infrastructure. That might seem obvious, but they are not coming to Porter to chat with Claude. We were not trying to expose the full product through an MCP, or to make Porter something you interact with only through an AI agent. The answer sat somewhere between those two: an agent beside the dashboard, there to help with whatever was already on screen.
Second: the answer users get from DevOps Agent had to be backed by clear evidence. Infrastructure answers are only useful when users can see what the agent inspected. If a service is crashing, users need to see exactly what the agent checked before they trust its diagnosis.

An earlier pass where each thinking step showed up as its own element in the chat sidebar.
That second consideration in particular led to way more iterations on the thinking state than I expected. We tried showing only the latest tool call, but that felt too sparse. We then tried showing all checks in parallel, but that was exposing too much, with the checks looking like separate events instead of one investigation. In extreme cases, the chat sidebar became a log viewer that pushed the final response, the thing users actually cared about, further down the already narrow scrollable window.
Eventually, we landed on our current version. It cycles through calls, with a loader that flips to a check when each one finishes. Once the answer arrived, the thinking state collapsed into a panel automatically, with the full step list behind a click. It goes without saying that this is not a pattern we created: if anything, it's a fairly common pattern across LLM interfaces that cycle through thinking states. Still, we forced ourselves to evaluate whether it was worth implementing for our use case, and it definitely was.
Sorry! This component was built for a larger screen, and is only displayed on screen sizes larger than 460px.Tool calls cycle one at a time, then collapse once the answer is ready.
Funnily enough, the "collapse when done" animation takes more work than the interaction suggests. Since height: auto won't animate, we measure the streaming and collapsed heights, animate between them with the Web Animations API, then clear the inline styles. A little complex and over-the-top (and unfortunately uses a useEffect...), but worth it.
const containerRef = useRef<HTMLDivElement>(null)
const lastStreamingHeight = useRef(0)
const previousDone = useRef(done)
// While the agent is still thinking, keep track of the live panel height.
useEffect(() => {
const el = containerRef.current
if (!el || done) return
const observer = new ResizeObserver(([entry]) => {
lastStreamingHeight.current =
entry.borderBoxSize?.[0]?.blockSize ?? el.offsetHeight
})
observer.observe(el, { box: 'border-box' })
return () => observer.disconnect()
}, [done])
// When thinking finishes, animate between two measured pixel heights.
// CSS cannot animate from height: auto.
useLayoutEffect(() => {
const el = containerRef.current
if (done && !previousDone.current && el) {
const fromHeight = lastStreamingHeight.current
const toHeight = el.offsetHeight
// Freeze layout at the previous height.
el.style.height = `${fromHeight}px`
el.style.overflow = 'hidden'
requestAnimationFrame(() => {
const animation = el.animate(
[{ height: `${fromHeight}px` }, { height: `${toHeight}px` }],
{
duration: 300,
easing: 'cubic-bezier(0.23, 1, 0.32, 1)',
fill: 'forwards'
}
)
animation.onfinish = () => {
// Let normal document flow take over.
animation.cancel()
el.style.height = ''
el.style.overflow = ''
}
})
}
previousDone.current = done
}, [done])Ultimately, designing for a narrow agent let us make stronger interface decisions because we knew exactly what it was for. To help someone debug infrastructure without leaving the dashboard, we could choose which tools to surface to get users an answer they can trust with a clear record of what it looked at.