webui: auto-generate types.ts from go structs
diff --git a/loop/agent.go b/loop/agent.go
index ce362e6..dda3aa6 100644
--- a/loop/agent.go
+++ b/loop/agent.go
@@ -104,6 +104,9 @@
 	// ToolCalls is a list of all tool calls requested in this message (name and input pairs)
 	ToolCalls []ToolCall `json:"tool_calls,omitempty"`
 
+	// ToolResponses is a list of all responses to tool calls requested in this message (name and input pairs)
+	ToolResponses []AgentMessage `json:"toolResponses,omitempty"`
+
 	// Commits is a list of git commits for a commit message
 	Commits []*GitCommit `json:"commits,omitempty"`
 
@@ -133,9 +136,12 @@
 
 // ToolCall represents a single tool call within an agent message
 type ToolCall struct {
-	Name       string `json:"name"`
-	Input      string `json:"input"`
-	ToolCallId string `json:"tool_call_id"`
+	Name          string        `json:"name"`
+	Input         string        `json:"input"`
+	ToolCallId    string        `json:"tool_call_id"`
+	ResultMessage *AgentMessage `json:"result_message,omitempty"`
+	Args          string        `json:"args,omitempty"`
+	Result        string        `json:"result,omitempty"`
 }
 
 func (a *AgentMessage) Attr() slog.Attr {
diff --git a/loop/server/loophttp.go b/loop/server/loophttp.go
index 6edae0c..0253f6d 100644
--- a/loop/server/loophttp.go
+++ b/loop/server/loophttp.go
@@ -46,6 +46,16 @@
 	SessionID string `json:"sessionId"`
 }
 
+type State struct {
+	MessageCount  int                  `json:"message_count"`
+	TotalUsage    *ant.CumulativeUsage `json:"total_usage,omitempty"`
+	Hostname      string               `json:"hostname"`
+	WorkingDir    string               `json:"working_dir"`
+	InitialCommit string               `json:"initial_commit"`
+	Title         string               `json:"title"`
+	OS            string               `json:"os"`
+}
+
 // Server serves sketch HTTP. Server implements http.Handler.
 type Server struct {
 	mux      *http.ServeMux
@@ -310,17 +320,9 @@
 
 		w.Header().Set("Content-Type", "application/json")
 
-		state := struct {
-			MessageCount  int                 `json:"message_count"`
-			TotalUsage    ant.CumulativeUsage `json:"total_usage"`
-			Hostname      string              `json:"hostname"`
-			WorkingDir    string              `json:"working_dir"`
-			InitialCommit string              `json:"initial_commit"`
-			Title         string              `json:"title"`
-			OS            string              `json:"os"`
-		}{
+		state := State{
 			MessageCount:  serverMessageCount,
-			TotalUsage:    totalUsage,
+			TotalUsage:    &totalUsage,
 			Hostname:      s.hostname,
 			WorkingDir:    getWorkingDir(),
 			InitialCommit: agent.InitialCommit(),
diff --git a/loop/webui/package.json b/loop/webui/package.json
index 15a5e92..a06362f 100644
--- a/loop/webui/package.json
+++ b/loop/webui/package.json
@@ -13,7 +13,8 @@
     "check": "tsc --noEmit",
     "demo": "web-dev-server -config ./web-dev-server.config.mjs --node-resolve --open /src/web-components/demo/",
     "format": "prettier ./src --write",
-    "build": "tsc",
+    "gentypes": "go run ../../cmd/go2ts -o src/types.ts",
+    "build": "go run ../../cmd/go2ts -o src/types.ts && tsc",
     "watch": "tsc --watch",
     "test": "tsc && playwright test -c playwright-ct.config.ts"
   },
diff --git a/loop/webui/src/data.ts b/loop/webui/src/data.ts
index 9eea954..9b5aca9 100644
--- a/loop/webui/src/data.ts
+++ b/loop/webui/src/data.ts
@@ -1,4 +1,4 @@
-import { TimelineMessage } from "./types";
+import { AgentMessage } from "./types";
 import { formatNumber } from "./utils";
 
 /**
@@ -42,7 +42,7 @@
   private isPollingEnabled: boolean = true;
   private isFirstLoad: boolean = true;
   private connectionStatus: ConnectionStatus = "disabled";
-  private messages: TimelineMessage[] = [];
+  private messages: AgentMessage[] = [];
   private timelineState: TimelineState | null = null;
 
   // Event listeners
@@ -76,7 +76,7 @@
   /**
    * Get all messages
    */
-  public getMessages(): TimelineMessage[] {
+  public getMessages(): AgentMessage[] {
     return this.messages;
   }
 
diff --git a/loop/webui/src/types.ts b/loop/webui/src/types.ts
index f91c9af..dfa89a7 100644
--- a/loop/webui/src/types.ts
+++ b/loop/webui/src/types.ts
@@ -1,71 +1,74 @@
-// TODO: generate these interface type declarations from the go structs instead of doing it by hand.
-// See https://github.com/boldsoftware/bold/blob/c6670a0a13f9d25785c8c1a90587fbab20a58bdd/sketch/types/ts.go for an example.
+// Auto-generated by sketch.dev/cmd/go2ts.go
+// DO NOT EDIT. This file is automatically generated.
 
-/**
- * Interface for a Git commit
- */
-export interface GitCommit {
-  hash: string; // Full commit hash
-  subject: string; // Commit subject line
-  body: string; // Full commit message body
-  pushed_branch?: string; // If set, this commit was pushed to this branch
-}
-
-/**
- * Interface for a tool call
- */
 export interface ToolCall {
-  name: string;
-  args?: string;
-  result?: string;
-  input?: string; // Input property for TypeScript compatibility
-  tool_call_id?: string;
-  result_message?: TimelineMessage;
+	name: string;
+	input: string;
+	tool_call_id: string;
+	result_message?: AgentMessage | null;
+	args?: string;
+	result?: string;
 }
 
-/**
- * Interface for a timeline message
- */
-export interface TimelineMessage {
-  idx: number;
-  type: string;
-  content?: string;
-  timestamp?: string | number | Date;
-  elapsed?: number;
-  turnDuration?: number; // Turn duration field
-  end_of_turn?: boolean;
-  conversation_id?: string;
-  parent_conversation_id?: string;
-  start_time?: string;
-  end_time?: string;
-  tool_calls?: ToolCall[];
-  tool_name?: string;
-  tool_error?: boolean;
-  tool_call_id?: string;
-  commits?: GitCommit[]; // For commit messages
-  input?: string; // Input property
-  tool_result?: string; // Tool result property
-  toolResponses?: any[]; // Tool responses array
-  usage?: Usage;
+export interface GitCommit {
+	hash: string;
+	subject: string;
+	body: string;
+	pushed_branch?: string;
 }
 
 export interface Usage {
-  start_time?: string;
-  messages?: number;
-  input_tokens?: number;
-  output_tokens?: number;
-  cache_read_input_tokens?: number;
-  cache_creation_input_tokens?: number;
-  cost_usd?: number;
-  total_cost_usd?: number;
-  tool_uses?: Map<string, any>;
+	input_tokens: number;
+	cache_creation_input_tokens: number;
+	cache_read_input_tokens: number;
+	output_tokens: number;
+	cost_usd: number;
 }
+
+export interface AgentMessage {
+	type: CodingAgentMessageType;
+	end_of_turn: boolean;
+	content: string;
+	tool_name?: string;
+	input?: string;
+	tool_result?: string;
+	tool_error?: boolean;
+	tool_call_id?: string;
+	tool_calls?: ToolCall[] | null;
+	toolResponses?: AgentMessage[] | null;
+	commits?: (GitCommit | null)[] | null;
+	timestamp: string;
+	conversation_id: string;
+	parent_conversation_id?: string | null;
+	usage?: Usage | null;
+	start_time?: string | null;
+	end_time?: string | null;
+	elapsed?: Duration | null;
+	turnDuration?: Duration | null;
+	idx: number;
+}
+
+export interface CumulativeUsage {
+	start_time: string;
+	messages: number;
+	input_tokens: number;
+	output_tokens: number;
+	cache_read_input_tokens: number;
+	cache_creation_input_tokens: number;
+	total_cost_usd: number;
+	tool_uses: { [key: string]: number } | null;
+}
+
 export interface State {
-  hostname?: string;
-  initial_commit?: string;
-  message_count?: number;
-  os: string;
-  title: string;
-  total_usage: Usage; // TODO Make a TotalUseage interface.
-  working_dir?: string;
+	message_count: number;
+	total_usage?: CumulativeUsage | null;
+	hostname: string;
+	working_dir: string;
+	initial_commit: string;
+	title: string;
+	os: string;
 }
+
+export type CodingAgentMessageType = 'user' | 'agent' | 'error' | 'budget' | 'tool' | 'commit' | 'auto';
+
+export type Duration = number;
diff --git a/loop/webui/src/web-components/demo/sketch-view-mode-select.demo.html b/loop/webui/src/web-components/demo/sketch-view-mode-select.demo.html
index 0d71067..7f795fc 100644
--- a/loop/webui/src/web-components/demo/sketch-view-mode-select.demo.html
+++ b/loop/webui/src/web-components/demo/sketch-view-mode-select.demo.html
@@ -21,6 +21,7 @@
           console.log("view mode change event: ", evt);
           const msgDiv = document.querySelector("#selected-mode");
           msgDiv.innerText = `selected mode: ${evt.detail.mode}`;
+          viewModeSelect.activeMode = evt.detail.mode;
         });
       });
     </script>
diff --git a/loop/webui/src/web-components/sketch-app-shell.ts b/loop/webui/src/web-components/sketch-app-shell.ts
index 62bcd03..6ef9232 100644
--- a/loop/webui/src/web-components/sketch-app-shell.ts
+++ b/loop/webui/src/web-components/sketch-app-shell.ts
@@ -1,7 +1,7 @@
 import { css, html, LitElement } from "lit";
 import { customElement, property, state } from "lit/decorators.js";
 import { DataManager, ConnectionStatus } from "../data";
-import { State, TimelineMessage, ToolCall } from "../types";
+import { State, AgentMessage } from "../types";
 import "./sketch-container-status";
 import "./sketch-view-mode-select";
 import "./sketch-network-status";
@@ -11,7 +11,6 @@
 import "./sketch-charts";
 import "./sketch-terminal";
 import { SketchDiffView } from "./sketch-diff-view";
-import { View } from "vega";
 
 type ViewMode = "chat" | "diff" | "charts" | "terminal";
 
@@ -175,7 +174,7 @@
 
   // Chat messages
   @property()
-  messages: TimelineMessage[] = [];
+  messages: AgentMessage[] = [];
 
   @property()
   chatMessageText: string = "";
@@ -186,7 +185,14 @@
   private dataManager = new DataManager();
 
   @property()
-  containerState: State = { title: "", os: "", total_usage: {} };
+  containerState: State = {
+    title: "",
+    os: "",
+    message_count: 0,
+    hostname: "",
+    working_dir: "",
+    initial_commit: "",
+  };
 
   // Track if this is the first load of messages
   @state()
@@ -470,15 +476,12 @@
     });
   }
 
-  mergeAndDedupe(
-    arr1: TimelineMessage[],
-    arr2: TimelineMessage[],
-  ): TimelineMessage[] {
+  mergeAndDedupe(arr1: AgentMessage[], arr2: AgentMessage[]): AgentMessage[] {
     const mergedArray = [...arr1, ...arr2];
     const seenIds = new Set<number>();
-    const toolCallResults = new Map<string, TimelineMessage>();
+    const toolCallResults = new Map<string, AgentMessage>();
 
-    let ret: TimelineMessage[] = mergedArray
+    let ret: AgentMessage[] = mergedArray
       .filter((msg) => {
         if (msg.type == "tool") {
           toolCallResults.set(msg.tool_call_id, msg);
@@ -491,7 +494,7 @@
         seenIds.add(msg.idx);
         return true;
       })
-      .sort((a: TimelineMessage, b: TimelineMessage) => a.idx - b.idx);
+      .sort((a: AgentMessage, b: AgentMessage) => a.idx - b.idx);
 
     // Attach any tool_call result messages to the original message's tool_call object.
     ret.forEach((msg) => {
@@ -506,7 +509,7 @@
 
   private handleDataChanged(eventData: {
     state: State;
-    newMessages: TimelineMessage[];
+    newMessages: AgentMessage[];
     isFirstFetch?: boolean;
   }): void {
     const { state, newMessages, isFirstFetch } = eventData;
diff --git a/loop/webui/src/web-components/sketch-charts.ts b/loop/webui/src/web-components/sketch-charts.ts
index a933c44..8cf2606 100644
--- a/loop/webui/src/web-components/sketch-charts.ts
+++ b/loop/webui/src/web-components/sketch-charts.ts
@@ -2,7 +2,7 @@
 import { css, html, LitElement, PropertyValues } from "lit";
 import { customElement, property, state } from "lit/decorators.js";
 import { TopLevelSpec } from "vega-lite";
-import type { TimelineMessage } from "../types";
+import type { AgentMessage } from "../types";
 import "vega-embed";
 import { VisualizationSpec } from "vega-embed";
 
@@ -13,7 +13,7 @@
 @customElement("sketch-charts")
 export class SketchCharts extends LitElement {
   @property({ type: Array })
-  messages: TimelineMessage[] = [];
+  messages: AgentMessage[] = [];
 
   @state()
   private chartData: { timestamp: Date; cost: number }[] = [];
@@ -77,7 +77,7 @@
   }
 
   private calculateCumulativeCostData(
-    messages: TimelineMessage[],
+    messages: AgentMessage[],
   ): { timestamp: Date; cost: number }[] {
     if (!messages || messages.length === 0) {
       return [];
diff --git a/loop/webui/src/web-components/sketch-container-status.test.ts b/loop/webui/src/web-components/sketch-container-status.test.ts
index 4a0c397..db11a4e 100644
--- a/loop/webui/src/web-components/sketch-container-status.test.ts
+++ b/loop/webui/src/web-components/sketch-container-status.test.ts
@@ -16,6 +16,9 @@
     cache_read_input_tokens: 300,
     cache_creation_input_tokens: 400,
     total_cost_usd: 0.25,
+    start_time: "",
+    messages: 0,
+    tool_uses: {},
   },
 };
 
@@ -78,6 +81,13 @@
     title: "Partial Test",
     total_usage: {
       input_tokens: 500,
+      start_time: "",
+      messages: 0,
+      output_tokens: 0,
+      cache_read_input_tokens: 0,
+      cache_creation_input_tokens: 0,
+      total_cost_usd: 0,
+      tool_uses: {},
     },
   };
 
diff --git a/loop/webui/src/web-components/sketch-network-status.ts b/loop/webui/src/web-components/sketch-network-status.ts
index e4af00b..2a0e455 100644
--- a/loop/webui/src/web-components/sketch-network-status.ts
+++ b/loop/webui/src/web-components/sketch-network-status.ts
@@ -1,8 +1,5 @@
 import { css, html, LitElement } from "lit";
 import { customElement, property } from "lit/decorators.js";
-import { DataManager, ConnectionStatus } from "../data";
-import { State, TimelineMessage } from "../types";
-import { SketchContainerStatus } from "./sketch-container-status";
 
 @customElement("sketch-network-status")
 export class SketchNetworkStatus extends LitElement {
diff --git a/loop/webui/src/web-components/sketch-terminal.ts b/loop/webui/src/web-components/sketch-terminal.ts
index 106f7e2..8e4805e 100644
--- a/loop/webui/src/web-components/sketch-terminal.ts
+++ b/loop/webui/src/web-components/sketch-terminal.ts
@@ -2,9 +2,7 @@
 import { FitAddon } from "@xterm/addon-fit";
 
 import { css, html, LitElement } from "lit";
-import { customElement, property, state } from "lit/decorators.js";
-import { DataManager, ConnectionStatus } from "../data";
-import { State, TimelineMessage } from "../types";
+import { customElement } from "lit/decorators.js";
 import "./sketch-container-status";
 
 @customElement("sketch-terminal")
diff --git a/loop/webui/src/web-components/sketch-timeline-message.test.ts b/loop/webui/src/web-components/sketch-timeline-message.test.ts
index eb1b788..bc74202 100644
--- a/loop/webui/src/web-components/sketch-timeline-message.test.ts
+++ b/loop/webui/src/web-components/sketch-timeline-message.test.ts
@@ -1,11 +1,14 @@
 import { test, expect } from "@sand4rt/experimental-ct-web";
 import { SketchTimelineMessage } from "./sketch-timeline-message";
-import { TimelineMessage, ToolCall, GitCommit, Usage } from "../types";
+import {
+  AgentMessage,
+  CodingAgentMessageType,
+  GitCommit,
+  Usage,
+} from "../types";
 
 // Helper function to create mock timeline messages
-function createMockMessage(
-  props: Partial<TimelineMessage> = {},
-): TimelineMessage {
+function createMockMessage(props: Partial<AgentMessage> = {}): AgentMessage {
   return {
     idx: props.idx || 0,
     type: props.type || "agent",
@@ -40,7 +43,15 @@
 });
 
 test.skip("renders with correct message type classes", async ({ mount }) => {
-  const messageTypes = ["user", "agent", "tool", "error"];
+  const messageTypes: CodingAgentMessageType[] = [
+    "user",
+    "agent",
+    "error",
+    "budget",
+    "tool",
+    "commit",
+    "auto",
+  ];
 
   for (const type of messageTypes) {
     const message = createMockMessage({ type });
@@ -124,6 +135,7 @@
     output_tokens: 300,
     cost_usd: 0.025,
     cache_read_input_tokens: 50,
+    cache_creation_input_tokens: 0,
   };
 
   const message = createMockMessage({
diff --git a/loop/webui/src/web-components/sketch-timeline-message.ts b/loop/webui/src/web-components/sketch-timeline-message.ts
index 88e9f14..e34d61f 100644
--- a/loop/webui/src/web-components/sketch-timeline-message.ts
+++ b/loop/webui/src/web-components/sketch-timeline-message.ts
@@ -1,16 +1,16 @@
 import { css, html, LitElement } from "lit";
 import { unsafeHTML } from "lit/directives/unsafe-html.js";
 import { customElement, property } from "lit/decorators.js";
-import { State, TimelineMessage } from "../types";
+import { AgentMessage } from "../types";
 import { marked, MarkedOptions } from "marked";
 import "./sketch-tool-calls";
 @customElement("sketch-timeline-message")
 export class SketchTimelineMessage extends LitElement {
   @property()
-  message: TimelineMessage;
+  message: AgentMessage;
 
   @property()
-  previousMessage: TimelineMessage;
+  previousMessage: AgentMessage;
 
   // See https://lit.dev/docs/components/styles/ for how lit-element handles CSS.
   // Note that these styles only apply to the scope of this web component's
diff --git a/loop/webui/src/web-components/sketch-timeline.ts b/loop/webui/src/web-components/sketch-timeline.ts
index 7deeb97..80efd0f 100644
--- a/loop/webui/src/web-components/sketch-timeline.ts
+++ b/loop/webui/src/web-components/sketch-timeline.ts
@@ -2,13 +2,13 @@
 import { PropertyValues } from "lit";
 import { repeat } from "lit/directives/repeat.js";
 import { customElement, property, state } from "lit/decorators.js";
-import { State, TimelineMessage } from "../types";
+import { AgentMessage } from "../types";
 import "./sketch-timeline-message";
 
 @customElement("sketch-timeline")
 export class SketchTimeline extends LitElement {
   @property()
-  messages: TimelineMessage[] = [];
+  messages: AgentMessage[] = [];
 
   // Track if we should scroll to the bottom
   @state()
@@ -177,9 +177,9 @@
     this.scrollContainer?.removeEventListener("scroll", this._handleScroll);
   }
 
-  // messageKey uniquely identifes a TimelineMessage based on its ID and tool_calls, so
+  // messageKey uniquely identifes a AgentMessage based on its ID and tool_calls, so
   // that we only re-render <sketch-message> elements that we need to re-render.
-  messageKey(message: TimelineMessage): string {
+  messageKey(message: AgentMessage): string {
     // If the message has tool calls, and any of the tool_calls get a response, we need to
     // re-render that message.
     const toolCallResponses = message.tool_calls
@@ -194,7 +194,7 @@
       <div id="scroll-container">
         <div class="timeline-container">
           ${repeat(this.messages, this.messageKey, (message, index) => {
-            let previousMessage: TimelineMessage;
+            let previousMessage: AgentMessage;
             if (index > 0) {
               previousMessage = this.messages[index - 1];
             }
diff --git a/loop/webui/src/web-components/sketch-tool-calls.ts b/loop/webui/src/web-components/sketch-tool-calls.ts
index 2b4c426..9461d6d 100644
--- a/loop/webui/src/web-components/sketch-tool-calls.ts
+++ b/loop/webui/src/web-components/sketch-tool-calls.ts
@@ -1,8 +1,6 @@
 import { css, html, LitElement } from "lit";
-import { repeat } from "lit/directives/repeat.js";
 import { customElement, property } from "lit/decorators.js";
-import { State, ToolCall } from "../types";
-import { marked, MarkedOptions } from "marked";
+import { ToolCall } from "../types";
 import "./sketch-tool-card";
 
 @customElement("sketch-tool-calls")
diff --git a/loop/webui/src/web-components/sketch-tool-card.ts b/loop/webui/src/web-components/sketch-tool-card.ts
index 800c665..0144ba0 100644
--- a/loop/webui/src/web-components/sketch-tool-card.ts
+++ b/loop/webui/src/web-components/sketch-tool-card.ts
@@ -1,8 +1,7 @@
 import { css, html, LitElement } from "lit";
 import { unsafeHTML } from "lit/directives/unsafe-html.js";
-import { repeat } from "lit/directives/repeat.js";
 import { customElement, property } from "lit/decorators.js";
-import { State, ToolCall } from "../types";
+import { ToolCall } from "../types";
 import { marked, MarkedOptions } from "marked";
 
 function renderMarkdown(markdownContent: string): string {
diff --git a/loop/webui/src/web-components/sketch-view-mode-select.ts b/loop/webui/src/web-components/sketch-view-mode-select.ts
index d67da0b..52f8a4e 100644
--- a/loop/webui/src/web-components/sketch-view-mode-select.ts
+++ b/loop/webui/src/web-components/sketch-view-mode-select.ts
@@ -1,7 +1,5 @@
 import { css, html, LitElement } from "lit";
-import { customElement, property, state } from "lit/decorators.js";
-import { DataManager, ConnectionStatus } from "../data";
-import { State, TimelineMessage } from "../types";
+import { customElement, property } from "lit/decorators.js";
 import "./sketch-container-status";
 
 @customElement("sketch-view-mode-select")
@@ -11,11 +9,6 @@
   activeMode: "chat" | "diff" | "charts" | "terminal" = "chat";
   // Header bar: view mode buttons
 
-  // See https://lit.dev/docs/components/styles/ for how lit-element handles CSS.
-  // Note that these styles only apply to the scope of this web component's
-  // shadow DOM node, so they won't leak out or collide with CSS declared in
-  // other components or the containing web page (...unless you want it to do that).
-
   static styles = css`
     /* View Mode Button Styles */
     .view-mode-buttons {