skaband: move timeline viewer from sketch/webui to resolve bundle conflicts
Move read-only timeline viewer functionality from sketch/webui to skaband
to eliminate custom element registration conflicts when using newsessions
with newui=true parameter.
Changes made:
- Created skaband/sketch-timeline-readonly.ts with timeline viewer functionality
- Updated skaband/messages.gohtml to use local sketch-timeline-readonly.js bundle
- Removed messages-viewer.ts from sketch/webui build configuration
- Updated Makefile to remove messages-viewer bundle extraction steps
- Added sketch-timeline-readonly.ts to skaband build.js entry points
Files created:
- skaband/sketch-timeline-readonly.ts: Timeline viewer component for archived sessions
- skaband/sketch-timeline-readonly.test.ts: Test coverage for timeline functionality
Files removed:
- sketch/webui/src/messages-viewer.ts: Relocated to skaband
- sketch/webui/src/messages-viewer.test.ts: Relocated to skaband
Build configuration updates:
- sketch/webui/esbuild.go: Remove messages-viewer.ts from bundleTs arrays
- sketch/webui/package.json: Remove messages-viewer.js export
- skaband/build.js: Add sketch-timeline-readonly.ts entry point
- Makefile: Remove messages-viewer bundle extraction from build and prepare-deploy
The timeline viewer is only used by skaband for archived session viewing,
so co-locating it with skaband eliminates the architectural issue where
both messages-viewer.js and newsessions-components.js contained overlapping
custom elements (sketch-timeline, sketch-tool-card-*, etc.) causing
registration conflicts.
This change resolves bundle conflicts on /newsessions?newui=true while
maintaining identical functionality for archived message viewing.
Co-Authored-By: sketch <hello@sketch.dev>
Change-ID: sd62ec3594672899ek
diff --git a/webui/esbuild.go b/webui/esbuild.go
index bf4cca9..246f9cb 100644
--- a/webui/esbuild.go
+++ b/webui/esbuild.go
@@ -275,7 +275,6 @@
"src/web-components/sketch-app-shell.ts",
"src/web-components/mobile-app-shell.ts",
"src/web-components/sketch-monaco-view.ts",
- "src/messages-viewer.ts",
"node_modules/monaco-editor/esm/vs/editor/editor.worker.js",
"node_modules/monaco-editor/esm/vs/language/typescript/ts.worker.js",
"node_modules/monaco-editor/esm/vs/language/html/html.worker.js",
@@ -508,7 +507,6 @@
"src/web-components/sketch-app-shell.ts",
"src/web-components/mobile-app-shell.ts",
"src/web-components/sketch-monaco-view.ts",
- "src/messages-viewer.ts",
}
metafiles := make([]string, len(bundleTs))
diff --git a/webui/package.json b/webui/package.json
index c62b08c..c2f15aa 100644
--- a/webui/package.json
+++ b/webui/package.json
@@ -7,7 +7,6 @@
"main": "dist/index.js",
"exports": {
".": "./dist/index.js",
- "./messages-viewer.js": "./dist/messages-viewer.js",
"./sketch-app-shell.js": "./dist/sketch-app-shell.js",
"./web-components/*": "./dist/web-components/*.js"
},
diff --git a/webui/src/messages-viewer.test.ts b/webui/src/messages-viewer.test.ts
deleted file mode 100644
index bd499f8..0000000
--- a/webui/src/messages-viewer.test.ts
+++ /dev/null
@@ -1,223 +0,0 @@
-/* eslint-disable @typescript-eslint/no-explicit-any */
-import { test, expect } from "@sand4rt/experimental-ct-web";
-import { State } from "./types";
-
-// Test the messages-viewer logic without importing the actual module
-// to avoid decorator issues in the test environment
-
-// Helper function to create mock session data
-function createMockSessionData(overrides: any = {}) {
- return {
- session_id: "test-session-123",
- user_name: "john.doe",
- user_email: "john.doe@example.com",
- session_state: {
- git_username: "john.doe",
- git_origin: "https://github.com/example/repo.git",
- session_id: "test-session-123",
- ...overrides.session_state,
- },
- ...overrides,
- };
-}
-
-// Helper function to create mock view data
-function createMockViewData(overrides: any = {}) {
- return {
- SessionWithData: overrides.SessionWithData || createMockSessionData(),
- Messages: [
- {
- type: "user",
- content: "Hello, this is a test user message",
- timestamp: new Date().toISOString(),
- conversation_id: "test-conv",
- idx: 1,
- hide_output: false,
- },
- {
- type: "agent",
- content: "This is an agent response",
- timestamp: new Date().toISOString(),
- conversation_id: "test-conv",
- idx: 2,
- hide_output: false,
- },
- ...(overrides.Messages || []),
- ],
- ToolResults: overrides.ToolResults || {},
- };
-}
-
-// Test the state creation logic directly
-function createStateFromViewData(viewData: any): Partial<State> {
- const sessionWithData = viewData.SessionWithData;
- return {
- session_id: sessionWithData?.session_id || "",
- git_username:
- sessionWithData?.session_state?.git_username ||
- sessionWithData?.user_name,
- git_origin: sessionWithData?.session_state?.git_origin,
- };
-}
-
-test("creates proper state object with git_username from session_state", () => {
- const mockViewData = createMockViewData();
- const state = createStateFromViewData(mockViewData);
-
- expect(state.session_id).toBe("test-session-123");
- expect(state.git_username).toBe("john.doe");
- expect(state.git_origin).toBe("https://github.com/example/repo.git");
-});
-
-test("uses git_username from session_state when available", () => {
- const mockViewData = createMockViewData({
- SessionWithData: createMockSessionData({
- user_name: "fallback.user",
- session_state: {
- git_username: "primary.user", // This should take precedence
- },
- }),
- });
-
- const state = createStateFromViewData(mockViewData);
- expect(state.git_username).toBe("primary.user");
-});
-
-test("falls back to user_name when session_state.git_username not available", () => {
- const mockViewData = createMockViewData({
- SessionWithData: createMockSessionData({
- user_name: "fallback.user",
- session_state: {
- // git_username not provided
- },
- }),
- });
-
- const state = createStateFromViewData(mockViewData);
- expect(state.git_username).toBe("fallback.user");
-});
-
-test("handles missing git username gracefully", () => {
- const mockViewData = createMockViewData({
- SessionWithData: {
- session_id: "test-session-123",
- // user_name not provided
- session_state: {
- // git_username not provided
- },
- },
- });
-
- const state = createStateFromViewData(mockViewData);
- expect(state.git_username).toBeUndefined();
-});
-
-test("message filtering logic works correctly", () => {
- const messages = [
- {
- type: "user",
- content: "Visible message",
- hide_output: false,
- timestamp: new Date().toISOString(),
- conversation_id: "test-conv",
- idx: 1,
- },
- {
- type: "agent",
- content: "Hidden message",
- hide_output: true, // This should be filtered out
- timestamp: new Date().toISOString(),
- conversation_id: "test-conv",
- idx: 2,
- },
- {
- type: "agent",
- content: "Another visible message",
- hide_output: false,
- timestamp: new Date().toISOString(),
- conversation_id: "test-conv",
- idx: 3,
- },
- ];
-
- // Test the filtering logic
- const visibleMessages = messages.filter((msg: any) => !msg.hide_output);
-
- expect(visibleMessages).toHaveLength(2);
- expect(visibleMessages[0].content).toBe("Visible message");
- expect(visibleMessages[1].content).toBe("Another visible message");
-});
-
-test("handles empty or malformed session data", () => {
- const mockViewData = {
- SessionWithData: null, // Malformed data
- Messages: [],
- ToolResults: {},
- };
-
- // Should not throw an error
- expect(() => {
- const state = createStateFromViewData(mockViewData);
- expect(state.session_id).toBe("");
- expect(state.git_username).toBeUndefined();
- }).not.toThrow();
-});
-
-test("preserves git_origin from session state", () => {
- const mockViewData = createMockViewData({
- SessionWithData: createMockSessionData({
- session_state: {
- git_origin: "https://github.com/test/repository.git",
- git_username: "test.user",
- },
- }),
- });
-
- const state = createStateFromViewData(mockViewData);
- expect(state.git_origin).toBe("https://github.com/test/repository.git");
-});
-
-test("fallback hierarchy works correctly", () => {
- // Test all combinations of the fallback hierarchy
- const testCases = [
- {
- name: "session_state.git_username takes precedence",
- sessionData: {
- user_name: "fallback",
- session_state: { git_username: "primary" },
- },
- expected: "primary",
- },
- {
- name: "user_name when session_state.git_username missing",
- sessionData: {
- user_name: "fallback",
- session_state: {},
- },
- expected: "fallback",
- },
- {
- name: "undefined when both missing",
- sessionData: {
- session_id: "test-session-123",
- // user_name not provided
- session_state: {
- // git_username not provided
- },
- },
- expected: undefined,
- },
- ];
-
- testCases.forEach(({ name, sessionData, expected }) => {
- const mockViewData = createMockViewData({
- SessionWithData:
- name === "undefined when both missing"
- ? sessionData
- : createMockSessionData(sessionData),
- });
-
- const state = createStateFromViewData(mockViewData);
- expect(state.git_username).toBe(expected);
- });
-});
diff --git a/webui/src/messages-viewer.ts b/webui/src/messages-viewer.ts
deleted file mode 100644
index c374646..0000000
--- a/webui/src/messages-viewer.ts
+++ /dev/null
@@ -1,54 +0,0 @@
-/* eslint-disable @typescript-eslint/no-explicit-any */
-import { SketchTimeline } from "./web-components/sketch-timeline";
-import { aggregateAgentMessages } from "./web-components/aggregateAgentMessages";
-import { State } from "./types";
-
-// Ensure this dependency ends up in the bundle (the "as SketchTimeline" reference below
-// is insufficient for the bundler to include it).
-// eslint-disable-next-line @typescript-eslint/no-unused-expressions
-SketchTimeline;
-
-export function renderMessagesViewer(viewData: any, container: HTMLDivElement) {
- const timelineEl = document.createElement(
- "sketch-timeline",
- ) as SketchTimeline;
- // Filter out hidden messages at the display level (matches sketch behavior)
- const visibleMessages = viewData.Messages.filter(
- (msg: any) => !msg.hide_output,
- );
- const messages = aggregateAgentMessages(visibleMessages, []);
- timelineEl.messages = messages;
- timelineEl.toolCalls = viewData.ToolResults;
- timelineEl.scrollContainer = { value: window.document.body };
-
- // Create a state object for the timeline component
- // This ensures user attribution works in archived messages
- const sessionWithData = viewData.SessionWithData;
- const state: Partial<State> = {
- session_id: sessionWithData?.session_id || "",
- // Use git_username from session state if available, fallback to UserName field, then extract from session info
- git_username:
- sessionWithData?.session_state?.git_username ||
- sessionWithData?.user_name ||
- extractGitUsername(sessionWithData),
- // Include other relevant state fields that might be available
- git_origin: sessionWithData?.session_state?.git_origin,
- };
-
- timelineEl.state = state as State;
- container.replaceWith(timelineEl);
-}
-
-// Helper function to extract git username from session data
-function extractGitUsername(sessionWithData: any): string | undefined {
- // Try to extract from session state first
- if (sessionWithData?.session_state?.git_username) {
- return sessionWithData.session_state.git_username;
- }
-
- // For older sessions, we might not have git_username stored
- // We could try to extract it from other sources, but for now return undefined
- return undefined;
-}
-
-window.globalThis.renderMessagesViewer = renderMessagesViewer;
diff --git a/webui/src/web-components/sketch-timeline-message.test.ts b/webui/src/web-components/sketch-timeline-message.test.ts
index d487fce..3ef8d5e 100644
--- a/webui/src/web-components/sketch-timeline-message.test.ts
+++ b/webui/src/web-components/sketch-timeline-message.test.ts
@@ -3,7 +3,6 @@
// NOTE: Most tests in this file are currently skipped due to TypeScript decorator
// configuration issues in the test environment. The git username attribution
// functionality has been tested manually and works correctly in runtime.
-// The core logic is tested in messages-viewer.test.ts
import { SketchTimelineMessage } from "./sketch-timeline-message";
import {
AgentMessage,