feat(web): define ChatEvent discriminated union, eliminate all any sites (R9)
Created web/src/lib/types.ts with discriminated unions for SSE event payloads: ChatEvent (7 variants: session, tool_use, tool_result, text_delta, text, done, error), ToolCallResult, MessageContent, and typed data shapes for live events (PlanProposedData, PlanStepEventData, QuestionRaisedData, QuestionAnsweredData, EntityTouchedData, HealthChangedData) plus WailsGlobal for the desktop bridge. Replaced all ~15 `any` sites across 7 files: - api.ts: Message.content any -> MessageContent | string; removed local ChatEvent interface (now imported from types.ts as a discriminated union); JSON.parse cast to ChatEvent. - stores/chat.ts: removed local ToolCallResult interface (imported from types.ts, re-exported for backward compat); extractApprovals accesses args with typeof guards instead of implicit any access; toChatMessages handles string|object Message.content cleanly. - stores/activity.ts: update_plan_step seq/status extracted via typeof guards instead of `as any` casts; toolActivityLabel uses a str() helper for safe string extraction from unknown args. - stores/workspace.ts: applyPlanStepEvent takes PlanStepEventData; applyEvent casts data to Record<string, unknown>; switch cases cast to typed interfaces (PlanProposedData, QuestionRaisedData, etc.) instead of `as any`; applyHealthChanged uses HealthChangedData. - Config.svelte: (window as any).wails -> typed WailsGlobal cast; catch (e: any) -> catch (e: unknown) with instanceof Error check. - utils.ts: WithoutChild/WithoutChildren `any` -> `unknown`. - vite.config.ts: authProxy proxy/proxyReq `any` -> ProxyOptions type. Result: eslint no-explicit-any warnings dropped 12 -> 0. Tests (6/6) and build pass. VERSION 0.7.10 -> 0.7.11. Plan R9 marked done.
This commit is contained in:
@@ -430,7 +430,7 @@ the gitignore comment); `build` target ensures `bin/` exists; `clean` removes `b
|
|||||||
| R6 | Inject desktop `version` from `VERSION` via ldflags; fix `Makefile` `BINARY` colliding with `oikos/` dir | S | Low | ✅ done |
|
| R6 | Inject desktop `version` from `VERSION` via ldflags; fix `Makefile` `BINARY` colliding with `oikos/` dir | S | Low | ✅ done |
|
||||||
| R7 | Add tests for `learning` (80% gate), `actuator`, `scheduler`, `domain`, `notifier`, `knowledge` | L | Low | ✅ partial — pure unit tests added for all 6 packages; remaining coverage needs integration tests (`make test-db`) |
|
| R7 | Add tests for `learning` (80% gate), `actuator`, `scheduler`, `domain`, `notifier`, `knowledge` | L | Low | ✅ partial — pure unit tests added for all 6 packages; remaining coverage needs integration tests (`make test-db`) |
|
||||||
| R8 | Add `eslint`+`prettier`+`vitest` to `web/`; wire `svelte-check`+`tsc` into CI; add `web/` CI job | M | Low | ✅ done |
|
| R8 | Add `eslint`+`prettier`+`vitest` to `web/`; wire `svelte-check`+`tsc` into CI; add `web/` CI job | M | Low | ✅ done |
|
||||||
| R9 | Define `OikosEvent` discriminated union; eliminate ~15 `any` sites in web | S | Low |
|
| R9 | Define `OikosEvent` discriminated union; eliminate ~15 `any` sites in web | S | Low | ✅ done |
|
||||||
| R10 | Replace `<svelte:component>` in `ActivityTimeline.svelte:103`; fix `state_referenced_locally` warnings | S | Low | ✅ done |
|
| R10 | Replace `<svelte:component>` in `ActivityTimeline.svelte:103`; fix `state_referenced_locally` warnings | S | Low | ✅ done |
|
||||||
| R11 | Add the 8 manually-registered `serve*` routes to `openapi.yaml` (or document the carve-out) | S | Low | ✅ done — documented the carve-out |
|
| R11 | Add the 8 manually-registered `serve*` routes to `openapi.yaml` (or document the carve-out) | S | Low | ✅ done — documented the carve-out |
|
||||||
| R12 | Add `docs/mbse/README.md` "Last verified" header + scheduled re-verification; normalize ADR 0013/0014 template | S | Low |
|
| R12 | Add `docs/mbse/README.md` "Last verified" header + scheduled re-verification; normalize ADR 0013/0014 template | S | Low |
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ export interface Message {
|
|||||||
id: string
|
id: string
|
||||||
session_id: string
|
session_id: string
|
||||||
role: string
|
role: string
|
||||||
content: any
|
content: MessageContent | string
|
||||||
created_at: string
|
created_at: string
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -102,13 +102,6 @@ export async function answerQuestion(sessionId: string, questionId: string, answ
|
|||||||
return res.ok
|
return res.ok
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ChatEvent {
|
|
||||||
type: string
|
|
||||||
data: any
|
|
||||||
session_id?: string
|
|
||||||
iteration?: number
|
|
||||||
}
|
|
||||||
|
|
||||||
export function streamChat(
|
export function streamChat(
|
||||||
message: string,
|
message: string,
|
||||||
sessionId: string | null,
|
sessionId: string | null,
|
||||||
|
|||||||
@@ -70,8 +70,8 @@ export const activityLog = derived([messages, planSteps, currentTask], ([$msgs,
|
|||||||
for (const t of $msgs[mi].tools) {
|
for (const t of $msgs[mi].tools) {
|
||||||
// Track current step from update_plan_step calls
|
// Track current step from update_plan_step calls
|
||||||
if (t.type === 'tool_use' && t.name === 'update_plan_step') {
|
if (t.type === 'tool_use' && t.name === 'update_plan_step') {
|
||||||
const s = (t.args as any)?.seq as number | undefined
|
const s = typeof t.args?.seq === 'number' ? t.args.seq : undefined
|
||||||
const status = (t.args as any)?.status as string | undefined
|
const status = typeof t.args?.status === 'string' ? t.args.status : undefined
|
||||||
if (s && status === 'running') currentStepSeq = s
|
if (s && status === 'running') currentStepSeq = s
|
||||||
} else if (t.name === 'set_goal' || t.name === 'propose_plan' || t.name === 'complete_task') {
|
} else if (t.name === 'set_goal' || t.name === 'propose_plan' || t.name === 'complete_task') {
|
||||||
currentStepSeq = 0
|
currentStepSeq = 0
|
||||||
@@ -132,7 +132,7 @@ export const activityLog = derived([messages, planSteps, currentTask], ([$msgs,
|
|||||||
for (let mi = 0; mi < $msgs.length; mi++) {
|
for (let mi = 0; mi < $msgs.length; mi++) {
|
||||||
for (const t of $msgs[mi].tools) {
|
for (const t of $msgs[mi].tools) {
|
||||||
if (t.type === 'tool_result' && t.name === 'upsert_knowledge' && !t.error) {
|
if (t.type === 'tool_result' && t.name === 'upsert_knowledge' && !t.error) {
|
||||||
const title = t.args?.title ?? ''
|
const title = typeof t.args?.title === 'string' ? t.args.title : ''
|
||||||
entries.push({
|
entries.push({
|
||||||
id: `knowledge_${mi}`,
|
id: `knowledge_${mi}`,
|
||||||
type: 'knowledge',
|
type: 'knowledge',
|
||||||
@@ -172,11 +172,12 @@ export const activityLog = derived([messages, planSteps, currentTask], ([$msgs,
|
|||||||
|
|
||||||
function toolActivityLabel(t: ToolCallResult): string {
|
function toolActivityLabel(t: ToolCallResult): string {
|
||||||
const args = t.args ?? {}
|
const args = t.args ?? {}
|
||||||
|
const str = (v: unknown): string => typeof v === 'string' ? v : ''
|
||||||
switch (t.name) {
|
switch (t.name) {
|
||||||
case 'set_goal': return 'Set goal'
|
case 'set_goal': return 'Set goal'
|
||||||
case 'propose_plan': return 'Proposed plan'
|
case 'propose_plan': return 'Proposed plan'
|
||||||
case 'search_knowledge': return `Research: ${args.query || ''}`
|
case 'search_knowledge': return `Research: ${str(args.query)}`
|
||||||
case 'get_entity': return `Lookup: ${args.slug_or_id || ''}`
|
case 'get_entity': return `Lookup: ${str(args.slug_or_id)}`
|
||||||
case 'get_entity_knowledge': return 'Check prior knowledge'
|
case 'get_entity_knowledge': return 'Check prior knowledge'
|
||||||
case 'get_relations': return 'Check relationships'
|
case 'get_relations': return 'Check relationships'
|
||||||
case 'list_lxcs': return 'List containers'
|
case 'list_lxcs': return 'List containers'
|
||||||
@@ -184,8 +185,8 @@ function toolActivityLabel(t: ToolCallResult): string {
|
|||||||
case 'get_health_summary': return 'Fleet health'
|
case 'get_health_summary': return 'Fleet health'
|
||||||
case 'get_state_snapshot': return 'State snapshot'
|
case 'get_state_snapshot': return 'State snapshot'
|
||||||
case 'run': {
|
case 'run': {
|
||||||
const purpose = args.purpose as string || ''
|
const purpose = str(args.purpose)
|
||||||
const target = (args.target as string) || ''
|
const target = str(args.target)
|
||||||
if (purpose) return purpose
|
if (purpose) return purpose
|
||||||
if (target) return `Run on ${target}`
|
if (target) return `Run on ${target}`
|
||||||
return 'Run command'
|
return 'Run command'
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
import { writable, get } from 'svelte/store'
|
import { writable, get } from 'svelte/store'
|
||||||
import { streamChat, fetchSessions, fetchMessages, deleteSession as apiDeleteSession } from '$lib/api'
|
import { streamChat, fetchSessions, fetchMessages, deleteSession as apiDeleteSession } from '$lib/api'
|
||||||
import type { ChatEvent, Session, Message } from '$lib/api'
|
import type { ChatEvent, Session, Message } from '$lib/api'
|
||||||
|
import type { ToolCallResult } from '$lib/types'
|
||||||
|
|
||||||
|
export type { ToolCallResult }
|
||||||
|
|
||||||
export interface PendingApproval {
|
export interface PendingApproval {
|
||||||
executionId: string
|
executionId: string
|
||||||
@@ -38,28 +41,21 @@ function extractApprovals(tools: ToolCallResult[]): PendingApproval[] {
|
|||||||
if (!text.includes('requires approval')) continue
|
if (!text.includes('requires approval')) continue
|
||||||
const m = text.match(APPROVAL_RE)
|
const m = text.match(APPROVAL_RE)
|
||||||
if (m) {
|
if (m) {
|
||||||
|
const args = t.args ?? {}
|
||||||
|
const purpose = typeof args.purpose === 'string' ? args.purpose : undefined
|
||||||
out.push({
|
out.push({
|
||||||
executionId: m[1],
|
executionId: m[1],
|
||||||
action: t.args?.purpose?.slice(0, 60) ?? t.args?.action ?? t.name ?? 'unknown',
|
action: purpose ? purpose.slice(0, 60) : typeof args.action === 'string' ? args.action : t.name,
|
||||||
target: t.args?.target ?? 'unknown',
|
target: typeof args.target === 'string' ? args.target : 'unknown',
|
||||||
destructive: /\bDESTRUCTIVE\b/.test(text),
|
destructive: /\bDESTRUCTIVE\b/.test(text),
|
||||||
command: t.args?.command,
|
command: typeof args.command === 'string' ? args.command : undefined,
|
||||||
purpose: t.args?.purpose
|
purpose
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ToolCallResult {
|
|
||||||
type: 'tool_use' | 'tool_result'
|
|
||||||
name: string
|
|
||||||
id?: string
|
|
||||||
args?: any
|
|
||||||
result?: any
|
|
||||||
error?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
function mid(): string {
|
function mid(): string {
|
||||||
return crypto.randomUUID()
|
return crypto.randomUUID()
|
||||||
}
|
}
|
||||||
@@ -119,11 +115,12 @@ function mergeToolCalls(raw: ToolCallResult[] | undefined): ToolCallResult[] {
|
|||||||
|
|
||||||
function toChatMessages(msgs: Message[]): ChatMessage[] {
|
function toChatMessages(msgs: Message[]): ChatMessage[] {
|
||||||
return msgs.map((m) => {
|
return msgs.map((m) => {
|
||||||
const tools = mergeToolCalls(m.content?.tool_calls)
|
const content = typeof m.content === 'string' ? { text: m.content } : m.content
|
||||||
|
const tools = mergeToolCalls(content?.tool_calls)
|
||||||
return {
|
return {
|
||||||
id: m.id,
|
id: m.id,
|
||||||
role: m.role as 'user' | 'assistant',
|
role: m.role as 'user' | 'assistant',
|
||||||
text: m.content?.text ?? (typeof m.content === 'string' ? m.content : ''),
|
text: content?.text ?? '',
|
||||||
tools,
|
tools,
|
||||||
pendingApprovals: extractApprovals(tools)
|
pendingApprovals: extractApprovals(tools)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,14 @@ import { writable, derived, get } from 'svelte/store'
|
|||||||
import { liveEvents, subscribeEvents } from './events'
|
import { liveEvents, subscribeEvents } from './events'
|
||||||
import { currentSession, sessions, loadSessions } from './chat'
|
import { currentSession, sessions, loadSessions } from './chat'
|
||||||
import { fetchPlan, fetchQuestions, type PlanStep, type SessionQuestion } from '$lib/api'
|
import { fetchPlan, fetchQuestions, type PlanStep, type SessionQuestion } from '$lib/api'
|
||||||
|
import type {
|
||||||
|
PlanProposedData,
|
||||||
|
PlanStepEventData,
|
||||||
|
QuestionRaisedData,
|
||||||
|
QuestionAnsweredData,
|
||||||
|
EntityTouchedData,
|
||||||
|
HealthChangedData
|
||||||
|
} from '$lib/types'
|
||||||
|
|
||||||
// workspace.ts is the live "what is this task doing right now" surface for the
|
// workspace.ts is the live "what is this task doing right now" surface for the
|
||||||
// TaskContextPanel: plan progress, the pinned operator question, and entities
|
// TaskContextPanel: plan progress, the pinned operator question, and entities
|
||||||
@@ -65,9 +73,9 @@ async function hydrate(sessionId: string) {
|
|||||||
questions.set(qs)
|
questions.set(qs)
|
||||||
}
|
}
|
||||||
|
|
||||||
function applyPlanStepEvent(sessionId: string, type: string, data: any) {
|
function applyPlanStepEvent(sessionId: string, type: string, data: PlanStepEventData) {
|
||||||
const stepID = data?.step_id as string | undefined
|
const stepID = data?.step_id
|
||||||
const seq = data?.seq as number | undefined
|
const seq = data?.seq
|
||||||
planSteps.update((steps) => {
|
planSteps.update((steps) => {
|
||||||
const i = steps.findIndex((s) => (stepID && s.id === stepID) || (seq != null && s.seq === seq))
|
const i = steps.findIndex((s) => (stepID && s.id === stepID) || (seq != null && s.seq === seq))
|
||||||
if (i === -1) return steps
|
if (i === -1) return steps
|
||||||
@@ -80,7 +88,7 @@ function applyPlanStepEvent(sessionId: string, type: string, data: any) {
|
|||||||
function applyEvent(ev: { type: string; correlation_id?: string | null; data?: unknown }) {
|
function applyEvent(ev: { type: string; correlation_id?: string | null; data?: unknown }) {
|
||||||
const sid = get(currentSession)
|
const sid = get(currentSession)
|
||||||
if (!sid || ev.correlation_id !== sid) return
|
if (!sid || ev.correlation_id !== sid) return
|
||||||
const data = (ev.data ?? {}) as any
|
const data = (ev.data ?? {}) as Record<string, unknown>
|
||||||
|
|
||||||
// Task fields (status/goal/outcome) live on the session row — refetch the
|
// Task fields (status/goal/outcome) live on the session row — refetch the
|
||||||
// (cheap) session list so the UI picks up the change without a
|
// (cheap) session list so the UI picks up the change without a
|
||||||
@@ -99,46 +107,49 @@ function applyEvent(ev: { type: string; correlation_id?: string | null; data?: u
|
|||||||
}
|
}
|
||||||
|
|
||||||
switch (ev.type) {
|
switch (ev.type) {
|
||||||
case 'plan.proposed':
|
case 'plan.proposed': {
|
||||||
if (Array.isArray(data.steps)) {
|
const d = data as unknown as PlanProposedData
|
||||||
const incoming = data.steps.map((s: any) => ({
|
if (Array.isArray(d.steps)) {
|
||||||
|
const incoming = d.steps.map((s) => ({
|
||||||
id: s.id, seq: s.seq, title: s.title, detail: s.detail ?? '',
|
id: s.id, seq: s.seq, title: s.title, detail: s.detail ?? '',
|
||||||
status: 'pending' as const, target_slug: s.target_slug || undefined
|
status: 'pending' as const, target_slug: s.target_slug || undefined
|
||||||
}))
|
}))
|
||||||
// The server appends rather than replaces once any step has started
|
planSteps.update((existing) => (d.appended ? [...existing, ...incoming] : incoming))
|
||||||
// (see store.go proposePlan) — mirror that here so a model that calls
|
|
||||||
// propose_plan once per step still shows the FULL running history in
|
|
||||||
// the panel, not just its latest call's single step.
|
|
||||||
planSteps.update((existing) => (data.appended ? [...existing, ...incoming] : incoming))
|
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
|
}
|
||||||
case 'plan.step.started':
|
case 'plan.step.started':
|
||||||
case 'plan.step.finished':
|
case 'plan.step.finished':
|
||||||
applyPlanStepEvent(sid, ev.type, data)
|
applyPlanStepEvent(sid, ev.type, data as unknown as PlanStepEventData)
|
||||||
break
|
break
|
||||||
case 'question.raised':
|
case 'question.raised': {
|
||||||
|
const d = data as unknown as QuestionRaisedData
|
||||||
questions.update((qs) => [
|
questions.update((qs) => [
|
||||||
{
|
{
|
||||||
id: data.question_id, prompt: data.prompt ?? '',
|
id: d.question_id, prompt: d.prompt ?? '',
|
||||||
context: { why: data.why, options: data.options, entities: data.entities },
|
context: { why: d.why, options: d.options, entities: d.entities },
|
||||||
status: 'open', created_at: new Date().toISOString()
|
status: 'open', created_at: new Date().toISOString()
|
||||||
},
|
},
|
||||||
...qs.filter((q) => q.id !== data.question_id)
|
...qs.filter((q) => q.id !== d.question_id)
|
||||||
])
|
])
|
||||||
break
|
break
|
||||||
case 'question.answered':
|
}
|
||||||
|
case 'question.answered': {
|
||||||
|
const d = data as unknown as QuestionAnsweredData
|
||||||
questions.update((qs) =>
|
questions.update((qs) =>
|
||||||
qs.map((q) => (q.id === data.question_id ? { ...q, status: 'answered', answer: data.answer } : q))
|
qs.map((q) => (q.id === d.question_id ? { ...q, status: 'answered', answer: d.answer } : q))
|
||||||
)
|
)
|
||||||
break
|
break
|
||||||
case 'entity.touched':
|
}
|
||||||
if (data.slug) {
|
case 'entity.touched': {
|
||||||
|
const d = data as unknown as EntityTouchedData
|
||||||
|
if (d.slug) {
|
||||||
const now = Date.now()
|
const now = Date.now()
|
||||||
touched.update((t) => [{ slug: data.slug, tool: data.tool ?? '', ts: now }, ...t].slice(0, TOUCHED_MAX))
|
touched.update((t) => [{ slug: d.slug, tool: d.tool ?? '', ts: now }, ...t].slice(0, TOUCHED_MAX))
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
|
}
|
||||||
case 'knowledge.recorded':
|
case 'knowledge.recorded':
|
||||||
// No dedicated store — knowledge cards refetch on completion signal.
|
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -148,11 +159,11 @@ function applyEvent(ev: { type: string; correlation_id?: string | null; data?: u
|
|||||||
// by correlation_id (health events don't carry one).
|
// by correlation_id (health events don't carry one).
|
||||||
function applyHealthChanged(ev: { type: string; data?: unknown }) {
|
function applyHealthChanged(ev: { type: string; data?: unknown }) {
|
||||||
if (ev.type !== 'health.changed') return
|
if (ev.type !== 'health.changed') return
|
||||||
const data = (ev.data ?? {}) as any
|
const data = (ev.data ?? {}) as HealthChangedData
|
||||||
if (!data.slug) return
|
if (!data.slug) return
|
||||||
const isRelevant = get(touched).some((t) => t.slug === data.slug)
|
const isRelevant = get(touched).some((t) => t.slug === data.slug)
|
||||||
if (!isRelevant) return
|
if (!isRelevant) return
|
||||||
healthDiffs.update((d) => [{ slug: data.slug, from: data.from, to: data.to, ts: Date.now() }, ...d].slice(0, 8))
|
healthDiffs.update((d) => [{ slug: data.slug, from: data.from ?? '', to: data.to ?? '', ts: Date.now() }, ...d].slice(0, 8))
|
||||||
}
|
}
|
||||||
|
|
||||||
// startWorkspace opens the global event subscription and begins tracking the
|
// startWorkspace opens the global event subscription and begins tracking the
|
||||||
|
|||||||
115
web/src/lib/types.ts
Normal file
115
web/src/lib/types.ts
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
// Discriminated unions for SSE event payloads — eliminates `any` in the
|
||||||
|
// chat and workspace stores by giving each event type a concrete data shape.
|
||||||
|
|
||||||
|
// ---- Chat SSE events (streamChat / /agent/chat) ----
|
||||||
|
|
||||||
|
export interface ChatSessionEvent {
|
||||||
|
type: 'session'
|
||||||
|
data: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ChatToolUseEvent {
|
||||||
|
type: 'tool_use'
|
||||||
|
data: { name: string; id: string; args: Record<string, unknown> }
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ChatToolResultEvent {
|
||||||
|
type: 'tool_result'
|
||||||
|
data: { id: string; result: unknown; error?: string }
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ChatTextDeltaEvent {
|
||||||
|
type: 'text_delta'
|
||||||
|
data: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ChatTextEvent {
|
||||||
|
type: 'text'
|
||||||
|
data: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ChatDoneEvent {
|
||||||
|
type: 'done'
|
||||||
|
data?: { session_id?: string }
|
||||||
|
session_id?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ChatErrorEvent {
|
||||||
|
type: 'error'
|
||||||
|
data: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ChatEvent =
|
||||||
|
| ChatSessionEvent
|
||||||
|
| ChatToolUseEvent
|
||||||
|
| ChatToolResultEvent
|
||||||
|
| ChatTextDeltaEvent
|
||||||
|
| ChatTextEvent
|
||||||
|
| ChatDoneEvent
|
||||||
|
| ChatErrorEvent
|
||||||
|
|
||||||
|
// ---- Tool call result (merged from tool_use + tool_result SSE pairs) ----
|
||||||
|
|
||||||
|
export interface ToolCallResult {
|
||||||
|
type: 'tool_use' | 'tool_result'
|
||||||
|
name: string
|
||||||
|
id?: string
|
||||||
|
args?: Record<string, unknown>
|
||||||
|
result?: unknown
|
||||||
|
error?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Message content (persisted messages from /agent/sessions/:id) ----
|
||||||
|
|
||||||
|
export interface MessageContent {
|
||||||
|
text?: string
|
||||||
|
tool_calls?: ToolCallResult[]
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Live event stream (SSE /api/v1/events/stream) ----
|
||||||
|
|
||||||
|
export interface PlanProposedData {
|
||||||
|
steps: Array<{ id: string; seq: number; title: string; detail?: string; target_slug?: string }>
|
||||||
|
appended?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PlanStepEventData {
|
||||||
|
step_id?: string
|
||||||
|
seq?: number
|
||||||
|
status?: string
|
||||||
|
execution_id?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface QuestionRaisedData {
|
||||||
|
question_id: string
|
||||||
|
prompt?: string
|
||||||
|
why?: string
|
||||||
|
options?: string[]
|
||||||
|
entities?: string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface QuestionAnsweredData {
|
||||||
|
question_id: string
|
||||||
|
answer?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface EntityTouchedData {
|
||||||
|
slug?: string
|
||||||
|
tool?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface HealthChangedData {
|
||||||
|
slug?: string
|
||||||
|
from?: string
|
||||||
|
to?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Wails desktop bridge (injected into window) ----
|
||||||
|
|
||||||
|
export interface WailsCall {
|
||||||
|
ByName: (method: string, ...args: unknown[]) => unknown
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface WailsGlobal {
|
||||||
|
Call?: WailsCall
|
||||||
|
}
|
||||||
@@ -41,9 +41,7 @@ export function debounce<T extends (...args: never[]) => void>(fn: T, wait = 300
|
|||||||
}) as T;
|
}) as T;
|
||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
export type WithoutChild<T> = T extends { child?: unknown } ? Omit<T, "child"> : T;
|
||||||
export type WithoutChild<T> = T extends { child?: any } ? Omit<T, "child"> : T;
|
export type WithoutChildren<T> = T extends { children?: unknown } ? Omit<T, "children"> : T;
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
export type WithoutChildren<T> = T extends { children?: any } ? Omit<T, "children"> : T;
|
|
||||||
export type WithoutChildrenOrChild<T> = WithoutChildren<WithoutChild<T>>;
|
export type WithoutChildrenOrChild<T> = WithoutChildren<WithoutChild<T>>;
|
||||||
export type WithElementRef<T, U extends HTMLElement = HTMLElement> = T & { ref?: U | null };
|
export type WithElementRef<T, U extends HTMLElement = HTMLElement> = T & { ref?: U | null };
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
import { fetchWithAuth, setConfig, initConfig, getConfig, clearConfig } from '$lib/config'
|
import { fetchWithAuth, setConfig, initConfig, getConfig, clearConfig } from '$lib/config'
|
||||||
import { startLogin, logout as oidcLogout, getUser, isOIDCConfigured } from '$lib/oidc'
|
import { startLogin, logout as oidcLogout, getUser, isOIDCConfigured } from '$lib/oidc'
|
||||||
import ConfigBackground from '$lib/components/ConfigBackground.svelte'
|
import ConfigBackground from '$lib/components/ConfigBackground.svelte'
|
||||||
|
import type { WailsGlobal } from '$lib/types'
|
||||||
|
|
||||||
let { onConnected, onCancel }: { onConnected: () => void; onCancel?: () => void } = $props()
|
let { onConnected, onCancel }: { onConnected: () => void; onCancel?: () => void } = $props()
|
||||||
|
|
||||||
@@ -54,7 +55,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function saveToDesktop() {
|
function saveToDesktop() {
|
||||||
const wails = (window as any).wails
|
const wails = (window as unknown as { wails?: WailsGlobal }).wails
|
||||||
if (!wails?.Call?.ByName) return
|
if (!wails?.Call?.ByName) return
|
||||||
try {
|
try {
|
||||||
wails.Call.ByName('SaveConfig', apiUrl.trim(), token.trim())
|
wails.Call.ByName('SaveConfig', apiUrl.trim(), token.trim())
|
||||||
@@ -71,8 +72,8 @@
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
await startLogin()
|
await startLogin()
|
||||||
} catch (e: any) {
|
} catch (e: unknown) {
|
||||||
error = e.message || 'OIDC login failed'
|
error = e instanceof Error ? e.message : 'OIDC login failed'
|
||||||
oidcLoggingIn = false
|
oidcLoggingIn = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
/// <reference types="vitest/config" />
|
/// <reference types="vitest/config" />
|
||||||
|
import type { ProxyOptions } from 'vite'
|
||||||
import { svelte } from '@sveltejs/vite-plugin-svelte'
|
import { svelte } from '@sveltejs/vite-plugin-svelte'
|
||||||
import tailwindcss from '@tailwindcss/vite'
|
import tailwindcss from '@tailwindcss/vite'
|
||||||
import { readFileSync, existsSync } from 'fs'
|
import { readFileSync, existsSync } from 'fs'
|
||||||
@@ -17,12 +18,12 @@ if (existsSync('../VERSION')) {
|
|||||||
// Injects OIKOS_API_TOKEN into proxied /api requests in dev — the API no
|
// Injects OIKOS_API_TOKEN into proxied /api requests in dev — the API no
|
||||||
// longer has a dev-open bypass (plans/2026-07-12-wails-desktop-app.md 0.4),
|
// longer has a dev-open bypass (plans/2026-07-12-wails-desktop-app.md 0.4),
|
||||||
// so `OIKOS_API_TOKEN=dev-token npm run dev` needs this to reach it.
|
// so `OIKOS_API_TOKEN=dev-token npm run dev` needs this to reach it.
|
||||||
function authProxy(target: string, rewrite?: (path: string) => string) {
|
function authProxy(target: string, rewrite?: (path: string) => string): ProxyOptions {
|
||||||
return {
|
return {
|
||||||
target,
|
target,
|
||||||
...(rewrite ? { rewrite } : {}),
|
...(rewrite ? { rewrite } : {}),
|
||||||
configure: (proxy: any) => {
|
configure: (proxy) => {
|
||||||
proxy.on('proxyReq', (proxyReq: any) => {
|
proxy.on('proxyReq', (proxyReq) => {
|
||||||
const token = process.env.OIKOS_API_TOKEN
|
const token = process.env.OIKOS_API_TOKEN
|
||||||
if (token) proxyReq.setHeader('Authorization', `Bearer ${token}`)
|
if (token) proxyReq.setHeader('Authorization', `Bearer ${token}`)
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user