webui: auto-generate types.ts from go structs
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 {