返回 DeepSeek-Reasonix
wire.go
根目录 / internal / eventwire / wire.go
1 // Package eventwire defines the shared frontend JSON contract for event.Event.
2 package eventwire
3
4 import (
5 "encoding/json"
6
7 "reasonix/internal/event"
8 "reasonix/internal/provider"
9 )
10
11 // Event is the JSON-friendly form shared by event frontends.
12 // externalizable:"true" marks large string payloads the Remote protocol may
13 // offload via content refs without changing provider-visible semantics.
14 type Event struct {
15 Kind string `json:"kind"`
16 Text string `json:"text,omitempty" externalizable:"true"`
17 Detail string `json:"detail,omitempty" externalizable:"true"`
18 Code string `json:"code,omitempty"`
19 Reasoning string `json:"reasoning,omitempty" externalizable:"true"`
20 MemoryCitations []MemoryCitation `json:"memoryCitations,omitempty"`
21 Level string `json:"level,omitempty"`
22 Tool *Tool `json:"tool,omitempty"`
23 Usage *Usage `json:"usage,omitempty"`
24 Approval *Approval `json:"approval,omitempty"`
25 Ask *Ask `json:"ask,omitempty"`
26 Compaction *Compaction `json:"compaction,omitempty"`
27 Guardian *Guardian `json:"guardian,omitempty"`
28 DecisionReceipt *DecisionReceipt `json:"decisionReceipt,omitempty"`
29 Extension *ExtensionSurface `json:"extension,omitempty"`
30 Err string `json:"err,omitempty" externalizable:"true"`
31 Outcome string `json:"outcome,omitempty"`
32 Readiness *FinalReadiness `json:"readiness,omitempty"`
33 RetryAttempt int `json:"retryAttempt,omitempty"`
34 RetryMax int `json:"retryMax,omitempty"`
35 RetryScope string `json:"retryScope,omitempty"` // "headers" | "stream"; omit for older clients
36 StreamAttempt *StreamAttempt `json:"streamAttempt,omitempty"`
37 }
38
39 // StreamAttempt is the JSON form of event.StreamAttemptInfo.
40 type StreamAttempt struct {
41 ID string `json:"id"`
42 Action string `json:"action"` // begin | discard | commit
43 Attempt int `json:"attempt,omitempty"`
44 Max int `json:"max,omitempty"`
45 Reason string `json:"reason,omitempty"` // connection_reset | premature_eof | idle_timeout
46 }
47
48 // ToWire converts a typed runtime event into the shared frontend JSON contract.
49 func ToWire(e event.Event) Event {
50 w := Event{Kind: kindNames[e.Kind], Text: e.Text, Detail: e.Detail, Reasoning: e.Reasoning}
51 if len(e.MemoryCitations) > 0 {
52 w.MemoryCitations = ToWireMemoryCitations(e.MemoryCitations)
53 }
54 switch e.Kind {
55 case event.Notice:
56 w.Code = e.Code
57 if e.DecisionReceipt != nil {
58 w.DecisionReceipt = ToWireDecisionReceipt(e.DecisionReceipt)
59 }
60 if e.Level == event.LevelWarn {
61 w.Level = "warn"
62 } else {
63 w.Level = "info"
64 }
65 case event.ToolDispatch, event.ToolResult, event.ToolProgress:
66 wt := &Tool{
67 ID: e.Tool.ID, Name: e.Tool.Name, Args: e.Tool.Args,
68 ResolvedName: e.Tool.ResolvedName, CapabilityID: e.Tool.CapabilityID,
69 Output: e.Tool.Output, Err: e.Tool.Err,
70 ReadOnly: e.Tool.ReadOnly, Truncated: e.Tool.Truncated,
71 DurationMs: e.Tool.DurationMs, Partial: e.Tool.Partial,
72 ArgChars: e.Tool.ArgChars, Refreshed: e.Tool.Refreshed,
73 ParentID: e.Tool.ParentID, AttemptID: e.Tool.AttemptID,
74 Diff: e.Tool.Diff, Added: e.Tool.Added, Removed: e.Tool.Removed,
75 }
76 if e.Tool.Profile != nil {
77 wt.Profile = &Profile{Model: e.Tool.Profile.Model, Effort: e.Tool.Profile.Effort}
78 }
79 if e.Tool.Execution != nil {
80 wt.Execution = toWireShellExecution(e.Tool.Execution)
81 }
82 w.Tool = wt
83 case event.Usage:
84 if u := e.Usage; u != nil {
85 w.Usage = &Usage{
86 PromptTokens: u.PromptTokens, CompletionTokens: u.CompletionTokens,
87 TotalTokens: u.TotalTokens, CacheHitTokens: u.CacheHitTokens,
88 CacheMissTokens: u.CacheMissTokens, ReasoningTokens: u.ReasoningTokens,
89 Estimated: u.Estimated,
90 Source: e.UsageSource,
91 ContextPromptTokens: u.ContextPromptTokens,
92 ContextCompletionTokens: u.ContextCompletionTokens,
93 ContextReasoningTokens: u.ContextReasoningTokens,
94 ContextCacheHitTokens: u.ContextCacheHitTokens,
95 ContextCacheMissTokens: u.ContextCacheMissTokens,
96 SessionCacheHitTokens: e.SessionHit, SessionCacheMissTokens: e.SessionMiss,
97 }
98 if e.CacheDiagnostics != nil {
99 w.Usage.CacheDiagnostics = ToWireCacheDiagnostics(e.CacheDiagnostics)
100 }
101 if e.Pricing != nil {
102 cost := e.Pricing.Cost(u)
103 w.Usage.Cost = cost
104 w.Usage.Currency = e.Pricing.Symbol()
105 w.Usage.CostUSD = cost
106 }
107 }
108 case event.ApprovalRequest:
109 w.Approval = &Approval{
110 ID: e.Approval.ID, Tool: e.Approval.Tool, Subject: e.Approval.Subject,
111 Reason: e.Approval.Reason, Fresh: e.Approval.Fresh, Kind: e.Approval.Kind,
112 }
113 if e.Approval.Recovery != nil {
114 r := e.Approval.Recovery
115 w.Approval.Recovery = &RecoveryApproval{
116 SourceAgent: r.SourceAgent,
117 FailedTool: r.FailedTool,
118 FailedSummary: r.FailedSummary,
119 Diagnosis: r.Diagnosis,
120 NextTool: r.NextTool,
121 NextAction: r.NextAction,
122 ChangeKind: r.ChangeKind,
123 ChangeRationale: r.ChangeRationale,
124 ReviewRationale: r.ReviewRationale,
125 PlanBefore: r.PlanBefore,
126 PlanAfter: r.PlanAfter,
127 CanGrantTask: r.CanGrantTask,
128 TaskGrantScope: r.TaskGrantScope,
129 }
130 }
131 case event.AskRequest:
132 w.Ask = ToWireAsk(e.Ask)
133 case event.CompactionStarted, event.CompactionDone:
134 w.Compaction = &Compaction{
135 Trigger: e.Compaction.Trigger, Messages: e.Compaction.Messages,
136 Summary: e.Compaction.Summary, Archive: e.Compaction.Archive,
137 }
138 case event.GuardianAssessment:
139 w.Guardian = ToWireGuardian(e.Guardian)
140 case event.ExtensionSurface, event.ExtensionStatus:
141 w.Extension = ToWireExtensionSurface(e.Extension)
142 case event.TurnDone:
143 w.Outcome = e.Outcome
144 if e.Readiness != nil {
145 w.Readiness = &FinalReadiness{Attempts: e.Readiness.Attempts, Missing: append([]string(nil), e.Readiness.Missing...)}
146 }
147 if e.Err != nil {
148 w.Err = e.Err.Error()
149 }
150 case event.Retrying:
151 w.RetryAttempt = e.RetryAttempt
152 w.RetryMax = e.RetryMax
153 if e.RetryScope != "" {
154 w.RetryScope = string(e.RetryScope)
155 }
156 case event.StreamAttempt:
157 w.StreamAttempt = &StreamAttempt{
158 ID: e.StreamAttempt.ID,
159 Action: string(e.StreamAttempt.Action),
160 Attempt: e.StreamAttempt.Attempt,
161 Max: e.StreamAttempt.Max,
162 Reason: e.StreamAttempt.Reason,
163 }
164 }
165 return w
166 }
167
168 // DecisionReceipt is the JSON form of a provider-excluded user decision.
169 type DecisionReceipt struct {
170 ID string `json:"id"`
171 Kind string `json:"kind"`
172 Tool string `json:"tool,omitempty"`
173 Subject string `json:"subject,omitempty"`
174 Outcome string `json:"outcome"`
175 }
176
177 func ToWireDecisionReceipt(in *provider.DecisionReceipt) *DecisionReceipt {
178 if in == nil {
179 return nil
180 }
181 return &DecisionReceipt{ID: in.ID, Kind: in.Kind, Tool: in.Tool, Subject: in.Subject, Outcome: in.Outcome}
182 }
183
184 type FinalReadiness struct {
185 Attempts int `json:"attempts,omitempty"`
186 Missing []string `json:"missing,omitempty"`
187 }
188
189 // MemoryCitation is the JSON form of provider.MemoryCitation.
190 type MemoryCitation struct {
191 ID string `json:"id,omitempty"`
192 Source string `json:"source"`
193 LineStart int `json:"lineStart,omitempty"`
194 LineEnd int `json:"lineEnd,omitempty"`
195 Note string `json:"note,omitempty"`
196 Kind string `json:"kind,omitempty"`
197 }
198
199 // ToWireMemoryCitations converts local memory references into frontend JSON.
200 func ToWireMemoryCitations(in []provider.MemoryCitation) []MemoryCitation {
201 out := make([]MemoryCitation, 0, len(in))
202 for _, c := range in {
203 if c.Source == "" && c.ID == "" && c.Note == "" {
204 continue
205 }
206 out = append(out, MemoryCitation{
207 ID: c.ID,
208 Source: c.Source,
209 LineStart: c.LineStart,
210 LineEnd: c.LineEnd,
211 Note: c.Note,
212 Kind: c.Kind,
213 })
214 }
215 return out
216 }
217
218 // Compaction is the JSON form of an event.Compaction.
219 type Compaction struct {
220 Trigger string `json:"trigger,omitempty"`
221 Messages int `json:"messages,omitempty"`
222 Summary string `json:"summary,omitempty" externalizable:"true"`
223 Archive string `json:"archive,omitempty" externalizable:"true"`
224 }
225
226 // AskOption is one JSON-formatted choice in a structured ask request.
227 type AskOption struct {
228 Label string `json:"label"`
229 Description string `json:"description,omitempty" externalizable:"true"`
230 }
231
232 // AskQuestion is one JSON-formatted structured ask question.
233 type AskQuestion struct {
234 ID string `json:"id"`
235 Header string `json:"header,omitempty"`
236 Prompt string `json:"prompt" externalizable:"true"`
237 Options []AskOption `json:"options"`
238 Multi bool `json:"multi,omitempty"`
239 }
240
241 // Ask is the JSON form of an event.Ask.
242 type Ask struct {
243 ID string `json:"id"`
244 Questions []AskQuestion `json:"questions"`
245 }
246
247 // Profile carries the subagent model/effort resolved for a tool call.
248 type Profile struct {
249 Model string `json:"model,omitempty"`
250 Effort string `json:"effort,omitempty"`
251 }
252
253 // Tool is the JSON form of an event.Tool.
254 type Tool struct {
255 ID string `json:"id,omitempty"`
256 Name string `json:"name"`
257 Args string `json:"args,omitempty" externalizable:"true"`
258 ResolvedName string `json:"resolvedName,omitempty"`
259 CapabilityID string `json:"capabilityId,omitempty"`
260 Output string `json:"output,omitempty" externalizable:"true"`
261 Err string `json:"err,omitempty" externalizable:"true"`
262 ReadOnly bool `json:"readOnly"`
263 Truncated bool `json:"truncated,omitempty"`
264 DurationMs int64 `json:"durationMs,omitempty"`
265 Partial bool `json:"partial,omitempty"`
266 ArgChars int `json:"argChars,omitempty"`
267 Refreshed bool `json:"refreshed,omitempty"`
268 ParentID string `json:"parentId,omitempty"`
269 AttemptID string `json:"attemptId,omitempty"` // host-local stream_attempt id for speculative partials
270 Diff string `json:"diff,omitempty" externalizable:"true"`
271 Added int `json:"added,omitempty"`
272 Removed int `json:"removed,omitempty"`
273 Profile *Profile `json:"profile,omitempty"`
274 Execution *ShellExecution `json:"execution,omitempty"`
275 }
276
277 // ShellExecution is the JSON form of event.ShellExecution (local UI metadata).
278 type ShellExecution struct {
279 Kind string `json:"kind,omitempty"`
280 Shell string `json:"shell,omitempty"`
281 ShellVersion string `json:"shellVersion,omitempty"`
282 Platform string `json:"platform,omitempty"`
283 SupportsAndAnd bool `json:"supportsAndAnd"`
284 State string `json:"state,omitempty"`
285 FailurePhase string `json:"failurePhase,omitempty"`
286 ExitCode *int `json:"exitCode,omitempty"`
287 OutputTail string `json:"outputTail,omitempty"`
288 MutationRisk string `json:"mutationRisk,omitempty"`
289 Verification string `json:"verification,omitempty"`
290 DurationMs int64 `json:"durationMs,omitempty"`
291 }
292
293 func toWireShellExecution(in *event.ShellExecution) *ShellExecution {
294 if in == nil {
295 return nil
296 }
297 out := &ShellExecution{
298 Kind: in.Kind, Shell: in.Shell, ShellVersion: in.ShellVersion,
299 Platform: in.Platform, SupportsAndAnd: in.SupportsAndAnd,
300 State: in.State, FailurePhase: in.FailurePhase,
301 OutputTail: in.OutputTail, MutationRisk: in.MutationRisk,
302 Verification: in.Verification, DurationMs: in.DurationMs,
303 }
304 if in.ExitCode != nil {
305 code := *in.ExitCode
306 out.ExitCode = &code
307 }
308 return out
309 }
310
311 // Usage is the JSON form of provider usage telemetry.
312 type Usage struct {
313 PromptTokens int `json:"promptTokens"`
314 CompletionTokens int `json:"completionTokens"`
315 TotalTokens int `json:"totalTokens"`
316 CacheHitTokens int `json:"cacheHitTokens"`
317 CacheMissTokens int `json:"cacheMissTokens"`
318 ReasoningTokens int `json:"reasoningTokens,omitempty"`
319 Estimated bool `json:"estimated,omitempty"`
320 Source string `json:"source,omitempty"`
321 CacheDiagnostics *CacheDiagnostics `json:"cacheDiagnostics,omitempty"`
322 // Session-cumulative cache tokens keep status displays steadier than one-turn values.
323 SessionCacheHitTokens int `json:"sessionCacheHitTokens"`
324 SessionCacheMissTokens int `json:"sessionCacheMissTokens"`
325 // Context* fields are the latest single-request shape for gauges/rebind.
326 // When omitted, clients fall back to the billable prompt/completion totals.
327 ContextPromptTokens int `json:"contextPromptTokens,omitempty"`
328 ContextCompletionTokens int `json:"contextCompletionTokens,omitempty"`
329 ContextReasoningTokens int `json:"contextReasoningTokens,omitempty"`
330 ContextCacheHitTokens int `json:"contextCacheHitTokens,omitempty"`
331 ContextCacheMissTokens int `json:"contextCacheMissTokens,omitempty"`
332 Cost float64 `json:"cost,omitempty"`
333 Currency string `json:"currency,omitempty"`
334 // CostUSD is a compatibility alias for older consumers; it mirrors Cost.
335 CostUSD float64 `json:"costUsd,omitempty"`
336 }
337
338 // CacheDiagnostics is the JSON form of cache prefix diagnostics.
339 type CacheDiagnostics struct {
340 PrefixHash string `json:"prefixHash"`
341 PrefixChanged bool `json:"prefixChanged"`
342 PrefixChangeReasons []string `json:"prefixChangeReasons,omitempty"`
343 SystemHash string `json:"systemHash"`
344 ToolsHash string `json:"toolsHash"`
345 LogRewriteVersion int `json:"logRewriteVersion"`
346 ToolSchemaTokens int `json:"toolSchemaTokens"`
347 CacheMissTokens int `json:"cacheMissTokens"`
348 CacheHitTokens int `json:"cacheHitTokens"`
349 }
350
351 // Approval is the JSON form of an event.Approval.
352 type Approval struct {
353 ID string `json:"id"`
354 Tool string `json:"tool"`
355 Subject string `json:"subject" externalizable:"true"`
356 Reason string `json:"reason,omitempty" externalizable:"true"`
357 Fresh bool `json:"fresh,omitempty"`
358 Kind string `json:"kind,omitempty"` // tool | plan | recovery
359 Recovery *RecoveryApproval `json:"recovery,omitempty"`
360 }
361
362 // RecoveryApproval is the JSON form of an event.RecoveryApproval.
363 type RecoveryApproval struct {
364 SourceAgent string `json:"source_agent,omitempty"`
365 FailedTool string `json:"failed_tool,omitempty"`
366 FailedSummary string `json:"failed_summary,omitempty"`
367 Diagnosis string `json:"diagnosis,omitempty"`
368 NextTool string `json:"next_tool,omitempty"`
369 NextAction string `json:"next_action,omitempty"`
370 ChangeKind string `json:"change_kind,omitempty"`
371 ChangeRationale string `json:"change_rationale,omitempty"`
372 ReviewRationale string `json:"review_rationale,omitempty"`
373 PlanBefore string `json:"plan_before,omitempty"`
374 PlanAfter string `json:"plan_after,omitempty"`
375 CanGrantTask bool `json:"can_grant_task,omitempty"`
376 TaskGrantScope string `json:"task_grant_scope,omitempty"`
377 }
378
379 // Guardian is the JSON form of an event.GuardianResult.
380 type Guardian struct {
381 ID string `json:"id"`
382 Tool string `json:"tool"`
383 Subject string `json:"subject"`
384 Outcome string `json:"outcome"`
385 RiskLevel string `json:"risk_level,omitempty"`
386 UserAuthorization string `json:"user_authorization,omitempty"`
387 Rationale string `json:"rationale,omitempty" externalizable:"true"`
388 DurationMs int64 `json:"duration_ms,omitempty"`
389 Usage *Usage `json:"usage,omitempty"`
390 }
391
392 // ToWireGuardian converts an event.GuardianResult into its JSON wire form.
393 func ToWireGuardian(g event.GuardianResult) *Guardian {
394 out := &Guardian{
395 ID: g.ID,
396 Tool: g.Tool,
397 Subject: g.Subject,
398 Outcome: g.Outcome,
399 RiskLevel: g.RiskLevel,
400 UserAuthorization: g.UserAuthorization,
401 Rationale: g.Rationale,
402 DurationMs: g.DurationMs,
403 }
404 if u := g.Usage; u != nil {
405 out.Usage = &Usage{
406 PromptTokens: u.PromptTokens, CompletionTokens: u.CompletionTokens,
407 TotalTokens: u.TotalTokens, CacheHitTokens: u.CacheHitTokens,
408 CacheMissTokens: u.CacheMissTokens, ReasoningTokens: u.ReasoningTokens,
409 Estimated: u.Estimated,
410 }
411 if g.Pricing != nil {
412 cost := g.Pricing.Cost(u)
413 out.Usage.Cost = cost
414 out.Usage.Currency = g.Pricing.Symbol()
415 out.Usage.CostUSD = cost
416 }
417 }
418 return out
419 }
420
421 // ToWireAsk converts an event.Ask into its JSON wire form.
422 func ToWireAsk(a event.Ask) *Ask {
423 qs := make([]AskQuestion, len(a.Questions))
424 for i, q := range a.Questions {
425 opts := make([]AskOption, len(q.Options))
426 for j, o := range q.Options {
427 opts[j] = AskOption{Label: o.Label, Description: o.Description}
428 }
429 qs[i] = AskQuestion{ID: q.ID, Header: q.Header, Prompt: q.Prompt, Options: opts, Multi: q.Multi}
430 }
431 return &Ask{ID: a.ID, Questions: qs}
432 }
433
434 // ToWireCacheDiagnostics converts cache diagnostics into their JSON wire form.
435 func ToWireCacheDiagnostics(d *event.CacheDiagnostics) *CacheDiagnostics {
436 return &CacheDiagnostics{
437 PrefixHash: d.PrefixHash,
438 PrefixChanged: d.PrefixChanged,
439 PrefixChangeReasons: append([]string(nil), d.PrefixChangeReasons...),
440 SystemHash: d.SystemHash,
441 ToolsHash: d.ToolsHash,
442 LogRewriteVersion: d.LogRewriteVersion,
443 ToolSchemaTokens: d.ToolSchemaTokens,
444 CacheMissTokens: d.CacheMissTokens,
445 CacheHitTokens: d.CacheHitTokens,
446 }
447 }
448
449 // KindNames returns every stable frontend event kind in event.Kind order. It is
450 // the protocol-neutral source used by consumers such as the Remote schema
451 // generator; callers receive a copy and may sort it without mutating eventwire.
452 func KindNames() []string {
453 names := make([]string, 0, int(event.KindCount))
454 for kind := event.Kind(0); kind < event.KindCount; kind++ {
455 if name, ok := kindNames[kind]; ok {
456 names = append(names, name)
457 }
458 }
459 return names
460 }
461
462 // KindName returns the stable wire name of one event kind, or false for a
463 // kind outside the known set.
464 func KindName(kind event.Kind) (string, bool) {
465 name, ok := kindNames[kind]
466 return name, ok
467 }
468
469 var kindNames = map[event.Kind]string{
470 event.TurnStarted: "turn_started",
471 event.Reasoning: "reasoning",
472 event.Text: "text",
473 event.Message: "message",
474 event.ToolDispatch: "tool_dispatch",
475 event.ToolResult: "tool_result",
476 event.Usage: "usage",
477 event.Notice: "notice",
478 event.Phase: "phase",
479 event.ApprovalRequest: "approval_request",
480 event.AskRequest: "ask_request",
481 event.TurnDone: "turn_done",
482 event.CompactionStarted: "compaction_started",
483 event.CompactionDone: "compaction_done",
484 event.ToolProgress: "tool_progress",
485 event.MCPSurfaceReady: "mcp_surface_ready",
486 event.Retrying: "retrying",
487 event.Steer: "steer",
488 event.GuardianAssessment: "guardian_assessment",
489 event.ExtensionSurface: "extension_surface",
490 event.ExtensionStatus: "extension_status",
491 event.StreamAttempt: "stream_attempt",
492 }
493
494 // ExtensionSurface is the JSON form of an event.ExtensionSurfacePayload.
495 type ExtensionSurface struct {
496 PluginID string `json:"pluginId"`
497 SurfaceID string `json:"surfaceId"`
498 SessionID string `json:"sessionId,omitempty"`
499 Generation uint64 `json:"generation,omitempty"`
500 Kind string `json:"kind"`
501 Status *ExtensionStatus `json:"status,omitempty"`
502 Card *ExtensionCard `json:"card,omitempty"`
503 Form *ExtensionForm `json:"form,omitempty"`
504 Notification *ExtensionNotification `json:"notification,omitempty"`
505 }
506
507 // ExtensionStatus is the JSON form of an event.ExtensionStatusView.
508 type ExtensionStatus struct {
509 Label string `json:"label"`
510 Detail string `json:"detail,omitempty"`
511 Severity string `json:"severity,omitempty"`
512 Progress *float64 `json:"progress,omitempty"`
513 }
514
515 // ExtensionKeyValue is the JSON form of an event.ExtensionKeyValue.
516 type ExtensionKeyValue struct {
517 Key string `json:"key"`
518 Value string `json:"value"`
519 }
520
521 // ExtensionActionRef is the JSON form of an event.ExtensionActionRef.
522 type ExtensionActionRef struct {
523 ActionID string `json:"actionId"`
524 Label string `json:"label"`
525 }
526
527 // ExtensionCard is the JSON form of an event.ExtensionCardView.
528 type ExtensionCard struct {
529 Title string `json:"title,omitempty"`
530 Markdown string `json:"markdown,omitempty" externalizable:"true"`
531 Text string `json:"text,omitempty" externalizable:"true"`
532 Fields []ExtensionKeyValue `json:"fields,omitempty"`
533 Progress *float64 `json:"progress,omitempty"`
534 Actions []ExtensionActionRef `json:"actions,omitempty"`
535 }
536
537 // ExtensionFormField is the JSON form of an event.ExtensionFormField. Default
538 // travels as raw JSON: the remote schema has no "any" type, and the field is
539 // already protocol-validated JSON on arrival.
540 type ExtensionFormField struct {
541 Key string `json:"key"`
542 Label string `json:"label,omitempty"`
543 Kind string `json:"kind,omitempty"`
544 Options []string `json:"options,omitempty"`
545 Default json.RawMessage `json:"default,omitempty"`
546 Required bool `json:"required,omitempty"`
547 }
548
549 // ExtensionForm is the JSON form of an event.ExtensionFormView.
550 type ExtensionForm struct {
551 Title string `json:"title,omitempty"`
552 Message string `json:"message,omitempty" externalizable:"true"`
553 Fields []ExtensionFormField `json:"fields"`
554 }
555
556 // ExtensionNotification is the JSON form of an event.ExtensionNotificationView.
557 type ExtensionNotification struct {
558 Title string `json:"title"`
559 Body string `json:"body,omitempty" externalizable:"true"`
560 Severity string `json:"severity,omitempty"`
561 }
562
563 // ToWireExtensionSurface converts an event.ExtensionSurfacePayload into its
564 // JSON wire form. A nil payload yields nil so a malformed event never
565 // marshals a half-filled extension object.
566 func ToWireExtensionSurface(p *event.ExtensionSurfacePayload) *ExtensionSurface {
567 if p == nil {
568 return nil
569 }
570 out := &ExtensionSurface{
571 PluginID: p.PluginID, SurfaceID: p.SurfaceID, SessionID: p.SessionID,
572 Generation: p.Generation, Kind: p.Kind,
573 }
574 if s := p.Status; s != nil {
575 out.Status = &ExtensionStatus{Label: s.Label, Detail: s.Detail, Severity: s.Severity, Progress: s.Progress}
576 }
577 if c := p.Card; c != nil {
578 card := &ExtensionCard{
579 Title: c.Title, Markdown: c.Markdown, Text: c.Text, Progress: c.Progress,
580 }
581 if len(c.Fields) > 0 {
582 card.Fields = make([]ExtensionKeyValue, len(c.Fields))
583 for i, f := range c.Fields {
584 card.Fields[i] = ExtensionKeyValue{Key: f.Key, Value: f.Value}
585 }
586 }
587 if len(c.Actions) > 0 {
588 card.Actions = make([]ExtensionActionRef, len(c.Actions))
589 for i, a := range c.Actions {
590 card.Actions[i] = ExtensionActionRef{ActionID: a.ActionID, Label: a.Label}
591 }
592 }
593 out.Card = card
594 }
595 if f := p.Form; f != nil {
596 form := &ExtensionForm{Title: f.Title, Message: f.Message}
597 if len(f.Fields) > 0 {
598 form.Fields = make([]ExtensionFormField, len(f.Fields))
599 for i, field := range f.Fields {
600 wireField := ExtensionFormField{
601 Key: field.Key, Label: field.Label, Kind: field.Kind,
602 Options: append([]string(nil), field.Options...),
603 Required: field.Required,
604 }
605 if field.Default != nil {
606 // The value arrived as protocol-validated JSON, so a
607 // marshal failure here is unreachable in practice; a
608 // pathological in-memory value simply drops the default.
609 if raw, err := json.Marshal(field.Default); err == nil {
610 wireField.Default = raw
611 }
612 }
613 form.Fields[i] = wireField
614 }
615 }
616 out.Form = form
617 }
618 if n := p.Notification; n != nil {
619 out.Notification = &ExtensionNotification{Title: n.Title, Body: n.Body, Severity: n.Severity}
620 }
621 return out
622 }
623
623 lines GO