mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-09-10 10:35:51 +00:00
59391aebf7
Preserve workflow activity rows across active stream content and tool boundaries, and type the generated chat SSE route metadata that drives model activity rows.
195 lines
5.3 KiB
Go
195 lines
5.3 KiB
Go
// Generates TypeScript types for frontend consumption from Go types.
|
|
//
|
|
// Currently this focuses on Pulse Assistant chat SSE event payloads.
|
|
// Source of truth: internal/ai/chat event data structs.
|
|
//
|
|
// Usage:
|
|
//
|
|
// go run ./scripts/generate-types.go
|
|
//
|
|
// Output:
|
|
//
|
|
// frontend-modern/src/api/generated/aiChatEvents.ts
|
|
//go:build ignore
|
|
|
|
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"reflect"
|
|
"sort"
|
|
"strings"
|
|
|
|
"github.com/rcourtman/pulse-go-rewrite/internal/ai/chat"
|
|
)
|
|
|
|
type tsTypeDef struct {
|
|
Name string
|
|
Body string
|
|
}
|
|
|
|
func main() {
|
|
outPath := filepath.Join("frontend-modern", "src", "api", "generated", "aiChatEvents.ts")
|
|
|
|
defs := []reflect.Type{
|
|
reflect.TypeOf(chat.ContentData{}),
|
|
reflect.TypeOf(chat.ThinkingData{}),
|
|
reflect.TypeOf(chat.WorkflowStateData{}),
|
|
reflect.TypeOf(chat.SessionData{}),
|
|
reflect.TypeOf(chat.ToolStartData{}),
|
|
reflect.TypeOf(chat.ToolProgressData{}),
|
|
reflect.TypeOf(chat.ToolCancelData{}),
|
|
reflect.TypeOf(chat.ToolEndData{}),
|
|
reflect.TypeOf(chat.ApprovalPlanData{}),
|
|
reflect.TypeOf(chat.ApprovalContextConfidenceData{}),
|
|
reflect.TypeOf(chat.ApprovalPreflightData{}),
|
|
reflect.TypeOf(chat.ApprovalNeededData{}),
|
|
reflect.TypeOf(chat.QuestionData{}),
|
|
reflect.TypeOf(chat.Question{}),
|
|
reflect.TypeOf(chat.QuestionOption{}),
|
|
reflect.TypeOf(chat.DoneData{}),
|
|
reflect.TypeOf(chat.ErrorData{}),
|
|
}
|
|
|
|
tsDefs := make([]tsTypeDef, 0, len(defs)+4)
|
|
for _, t := range defs {
|
|
tsDefs = append(tsDefs, tsTypeDef{Name: t.Name(), Body: renderInterface(t)})
|
|
}
|
|
|
|
// Sort for stable output.
|
|
sort.Slice(tsDefs, func(i, j int) bool { return tsDefs[i].Name < tsDefs[j].Name })
|
|
|
|
var buf bytes.Buffer
|
|
buf.WriteString("// Code generated by scripts/generate-types.go; DO NOT EDIT.\n")
|
|
buf.WriteString("// Source: internal/ai/chat event payload structs.\n\n")
|
|
buf.WriteString("/* eslint-disable */\n\n")
|
|
|
|
for _, d := range tsDefs {
|
|
buf.WriteString(d.Body)
|
|
buf.WriteString("\n\n")
|
|
}
|
|
|
|
// Stream event union matches internal/api/contract_test.go snapshots.
|
|
buf.WriteString("export type AIChatStreamEvent =\n")
|
|
buf.WriteString(" | { type: 'session'; data: SessionData }\n")
|
|
buf.WriteString(" | { type: 'content'; data: ContentData }\n")
|
|
buf.WriteString(" | { type: 'thinking'; data: ThinkingData }\n")
|
|
buf.WriteString(" | { type: 'workflow_state'; data: WorkflowStateData }\n")
|
|
buf.WriteString(" | { type: 'tool_start'; data: ToolStartData }\n")
|
|
buf.WriteString(" | { type: 'tool_progress'; data: ToolProgressData }\n")
|
|
buf.WriteString(" | { type: 'tool_cancel'; data: ToolCancelData }\n")
|
|
buf.WriteString(" | { type: 'tool_end'; data: ToolEndData }\n")
|
|
buf.WriteString(" | { type: 'approval_needed'; data: ApprovalNeededData }\n")
|
|
// QuestionData is wrapped by the backend as {question_id, questions} plus session_id in some callers.
|
|
// The contract test covers {question_id, questions}; the UI currently expects session_id too.
|
|
// Keep session_id optional for backward compatibility.
|
|
buf.WriteString(" | { type: 'question'; data: QuestionData & { session_id?: string } }\n")
|
|
buf.WriteString(" | { type: 'done'; data?: DoneData }\n")
|
|
buf.WriteString(" | { type: 'error'; data: ErrorData };\n")
|
|
|
|
if err := os.MkdirAll(filepath.Dir(outPath), 0o755); err != nil {
|
|
fatal(err)
|
|
}
|
|
if err := os.WriteFile(outPath, buf.Bytes(), 0o644); err != nil {
|
|
fatal(err)
|
|
}
|
|
}
|
|
|
|
func fatal(err error) {
|
|
fmt.Fprintln(os.Stderr, err.Error())
|
|
os.Exit(1)
|
|
}
|
|
|
|
func renderInterface(t reflect.Type) string {
|
|
if t.Kind() != reflect.Struct {
|
|
return ""
|
|
}
|
|
|
|
var b strings.Builder
|
|
b.WriteString("export interface ")
|
|
b.WriteString(t.Name())
|
|
b.WriteString(" {\n")
|
|
|
|
for i := 0; i < t.NumField(); i++ {
|
|
f := t.Field(i)
|
|
if f.PkgPath != "" { // unexported
|
|
continue
|
|
}
|
|
|
|
jsonName, optional := jsonFieldName(f)
|
|
if jsonName == "-" || jsonName == "" {
|
|
continue
|
|
}
|
|
|
|
b.WriteString(" ")
|
|
b.WriteString(jsonName)
|
|
if optional {
|
|
b.WriteString("?: ")
|
|
} else {
|
|
b.WriteString(": ")
|
|
}
|
|
b.WriteString(goTypeToTS(f.Type))
|
|
b.WriteString(";\n")
|
|
}
|
|
|
|
b.WriteString("}")
|
|
return b.String()
|
|
}
|
|
|
|
func jsonFieldName(f reflect.StructField) (name string, optional bool) {
|
|
tag := f.Tag.Get("json")
|
|
if tag == "" {
|
|
// Default to lower_snake? The backend always uses json tags for these structs.
|
|
return "", false
|
|
}
|
|
parts := strings.Split(tag, ",")
|
|
name = parts[0]
|
|
for _, p := range parts[1:] {
|
|
if p == "omitempty" {
|
|
optional = true
|
|
break
|
|
}
|
|
}
|
|
// Pointers are optional in practice.
|
|
if f.Type.Kind() == reflect.Pointer {
|
|
optional = true
|
|
}
|
|
return name, optional
|
|
}
|
|
|
|
func goTypeToTS(t reflect.Type) string {
|
|
switch t.Kind() {
|
|
case reflect.String:
|
|
return "string"
|
|
case reflect.Bool:
|
|
return "boolean"
|
|
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
|
return "number"
|
|
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
|
return "number"
|
|
case reflect.Float32, reflect.Float64:
|
|
return "number"
|
|
case reflect.Slice, reflect.Array:
|
|
return goTypeToTS(t.Elem()) + "[]"
|
|
case reflect.Pointer:
|
|
return goTypeToTS(t.Elem())
|
|
case reflect.Struct:
|
|
// Named structs become references.
|
|
if t.Name() != "" {
|
|
return t.Name()
|
|
}
|
|
// Fallback anonymous struct shape.
|
|
return "Record<string, unknown>"
|
|
case reflect.Map:
|
|
// We only need map for generic payloads; keep unknown to avoid lying.
|
|
return "Record<string, unknown>"
|
|
case reflect.Interface:
|
|
return "unknown"
|
|
default:
|
|
return "unknown"
|
|
}
|
|
}
|