Files
DeepSeek-Reasonix/desktop/wire.go
T
SivanColaandreasonix b87a878cad feat(desktop): project topic tabs and context inspector (#2925)
* feat(desktop): add project tabs and context inspector

* feat(desktop): refine project workspace UI

* fix 打开一致加载

* 顶部统一logo+左侧边栏+统一菜单

* 底部最低宽度

* update much

* update code

* update code

* feat(desktop): refine workspace layout and message actions

* fix(desktop): scope file refs to active tab root

* test(desktop): stabilize capabilities config dirs

* fix(desktop): protect sessions open in other tabs

* fix(desktop): resume scoped history in matching tab

* chore(desktop): drop committed scratch artifacts; ignore tmp/ and .codex/

The project-tabs branch carried 30 throwaway screenshots, a mock HTML,
and a codex env file into the tree because tmp/ and .codex/ were never
ignored. Remove them and add the ignore rules so scratch output stays
out of future commits.

* test(desktop): isolate AppData per test so the suite passes on Windows

The new project/topic/workspace tests only overrode HOME and
XDG_CONFIG_HOME, but desktopConfigDir() and config.MemoryUserDir()
resolve through os.UserConfigDir(), which reads %AppData% on Windows and
ignores those vars. With AppData pointed at one shared TestMain dir, the
project/topic/workspace registries leaked across tests: full-package
runs failed (project tree len 2 want 1, trash mixing other tests'
sessions, Plugins[0] reading a neighbor's MCP entry) while each test
passed alone.

Route every test through isolateDesktopUserDirs, which already sets
HOME, USERPROFILE, XDG_CONFIG_HOME, and AppData to a per-test temp dir.
go test ./... (and -race) now pass on Windows.

---------

Co-authored-by: reasonix <[email protected]>
2026-06-04 19:41:16 -07:00

207 lines
7.7 KiB
Go

package main
import "reasonix/internal/event"
// wireEvent is the JSON shape an event.Event takes when emitted to the webview.
// It mirrors the serve transport's SSE wire form field-for-field on purpose: both
// frontends consume the identical typed stream, so the React client and a browser
// SSE client can share contract types. The Kind enum becomes a stable string and
// the TurnDone error becomes a message, since neither serializes cleanly.
//
// (Kept in step with internal/serve/wire.go by hand for now — the two transports
// may diverge later; if they don't, this is the obvious thing to lift into a
// shared event.ToWire.)
type wireEvent struct {
Kind string `json:"kind"`
Text string `json:"text,omitempty"`
Reasoning string `json:"reasoning,omitempty"`
Level string `json:"level,omitempty"`
Tool *wireTool `json:"tool,omitempty"`
Usage *wireUsage `json:"usage,omitempty"`
Approval *wireApproval `json:"approval,omitempty"`
Ask *wireAsk `json:"ask,omitempty"`
Compaction *wireCompaction `json:"compaction,omitempty"`
Err string `json:"err,omitempty"`
RetryAttempt int `json:"retryAttempt,omitempty"`
RetryMax int `json:"retryMax,omitempty"`
}
// wireCompaction is the JSON form of an event.Compaction. On a compaction_started
// event only Trigger is set; compaction_done carries the rest (an aborted pass
// leaves Summary empty so the frontend drops its placeholder).
type wireCompaction struct {
Trigger string `json:"trigger,omitempty"`
Messages int `json:"messages,omitempty"`
Summary string `json:"summary,omitempty"`
Archive string `json:"archive,omitempty"`
}
type wireAskOption struct {
Label string `json:"label"`
Description string `json:"description,omitempty"`
}
type wireAskQuestion struct {
ID string `json:"id"`
Header string `json:"header,omitempty"`
Prompt string `json:"prompt"`
Options []wireAskOption `json:"options"`
Multi bool `json:"multi,omitempty"`
}
type wireAsk struct {
ID string `json:"id"`
Questions []wireAskQuestion `json:"questions"`
}
type wireTool struct {
ID string `json:"id,omitempty"`
Name string `json:"name"`
Args string `json:"args,omitempty"`
Output string `json:"output,omitempty"`
Err string `json:"err,omitempty"`
ReadOnly bool `json:"readOnly"`
Truncated bool `json:"truncated,omitempty"`
Partial bool `json:"partial,omitempty"`
ParentID string `json:"parentId,omitempty"`
}
type wireUsage struct {
PromptTokens int `json:"promptTokens"`
CompletionTokens int `json:"completionTokens"`
TotalTokens int `json:"totalTokens"`
CacheHitTokens int `json:"cacheHitTokens"`
CacheMissTokens int `json:"cacheMissTokens"`
ReasoningTokens int `json:"reasoningTokens,omitempty"`
CacheDiagnostics *wireCacheDiagnostics `json:"cacheDiagnostics,omitempty"`
// Session-cumulative cache tokens — the status line shows the aggregate
// hit-rate Σhit/Σ(hit+miss), steadier than the single-turn CacheHitTokens.
SessionCacheHitTokens int `json:"sessionCacheHitTokens"`
SessionCacheMissTokens int `json:"sessionCacheMissTokens"`
Cost float64 `json:"cost,omitempty"`
Currency string `json:"currency,omitempty"`
// CostUSD is kept for older frontend/status consumers. It mirrors Cost and
// does not imply USD.
CostUSD float64 `json:"costUsd,omitempty"`
}
type wireCacheDiagnostics struct {
PrefixHash string `json:"prefixHash"`
PrefixChanged bool `json:"prefixChanged"`
PrefixChangeReasons []string `json:"prefixChangeReasons,omitempty"`
SystemHash string `json:"systemHash"`
ToolsHash string `json:"toolsHash"`
LogRewriteVersion int `json:"logRewriteVersion"`
ToolSchemaTokens int `json:"toolSchemaTokens"`
CacheMissTokens int `json:"cacheMissTokens"`
CacheHitTokens int `json:"cacheHitTokens"`
}
type wireApproval struct {
ID string `json:"id"`
Tool string `json:"tool"`
Subject string `json:"subject"`
}
// kindNames maps the event.Kind enum to stable wire strings.
var kindNames = map[event.Kind]string{
event.TurnStarted: "turn_started",
event.Reasoning: "reasoning",
event.Text: "text",
event.Message: "message",
event.ToolDispatch: "tool_dispatch",
event.ToolResult: "tool_result",
event.Usage: "usage",
event.Notice: "notice",
event.Phase: "phase",
event.ApprovalRequest: "approval_request",
event.AskRequest: "ask_request",
event.TurnDone: "turn_done",
event.CompactionStarted: "compaction_started",
event.CompactionDone: "compaction_done",
event.ToolProgress: "tool_progress",
event.Retrying: "retrying",
}
// toWireAsk converts an event.Ask into its JSON wire form.
func toWireAsk(a event.Ask) *wireAsk {
qs := make([]wireAskQuestion, len(a.Questions))
for i, q := range a.Questions {
opts := make([]wireAskOption, len(q.Options))
for j, o := range q.Options {
opts[j] = wireAskOption{Label: o.Label, Description: o.Description}
}
qs[i] = wireAskQuestion{ID: q.ID, Header: q.Header, Prompt: q.Prompt, Options: opts, Multi: q.Multi}
}
return &wireAsk{ID: a.ID, Questions: qs}
}
// toWire converts an event.Event into its JSON wire form.
func toWire(e event.Event) wireEvent {
w := wireEvent{Kind: kindNames[e.Kind], Text: e.Text, Reasoning: e.Reasoning}
switch e.Kind {
case event.Notice:
if e.Level == event.LevelWarn {
w.Level = "warn"
} else {
w.Level = "info"
}
case event.ToolDispatch, event.ToolResult, event.ToolProgress:
w.Tool = &wireTool{
ID: e.Tool.ID, Name: e.Tool.Name, Args: e.Tool.Args,
Output: e.Tool.Output, Err: e.Tool.Err,
ReadOnly: e.Tool.ReadOnly, Truncated: e.Tool.Truncated,
Partial: e.Tool.Partial, ParentID: e.Tool.ParentID,
}
case event.Usage:
if u := e.Usage; u != nil {
w.Usage = &wireUsage{
PromptTokens: u.PromptTokens, CompletionTokens: u.CompletionTokens,
TotalTokens: u.TotalTokens, CacheHitTokens: u.CacheHitTokens,
CacheMissTokens: u.CacheMissTokens, ReasoningTokens: u.ReasoningTokens,
SessionCacheHitTokens: e.SessionHit, SessionCacheMissTokens: e.SessionMiss,
}
if e.CacheDiagnostics != nil {
w.Usage.CacheDiagnostics = toWireCacheDiagnostics(e.CacheDiagnostics)
}
if e.Pricing != nil {
cost := e.Pricing.Cost(u)
w.Usage.Cost = cost
w.Usage.Currency = e.Pricing.Symbol()
w.Usage.CostUSD = cost
}
}
case event.ApprovalRequest:
w.Approval = &wireApproval{ID: e.Approval.ID, Tool: e.Approval.Tool, Subject: e.Approval.Subject}
case event.AskRequest:
w.Ask = toWireAsk(e.Ask)
case event.CompactionStarted, event.CompactionDone:
w.Compaction = &wireCompaction{
Trigger: e.Compaction.Trigger, Messages: e.Compaction.Messages,
Summary: e.Compaction.Summary, Archive: e.Compaction.Archive,
}
case event.TurnDone:
if e.Err != nil {
w.Err = e.Err.Error()
}
case event.Retrying:
w.RetryAttempt = e.RetryAttempt
w.RetryMax = e.RetryMax
}
return w
}
func toWireCacheDiagnostics(d *event.CacheDiagnostics) *wireCacheDiagnostics {
return &wireCacheDiagnostics{
PrefixHash: d.PrefixHash,
PrefixChanged: d.PrefixChanged,
PrefixChangeReasons: append([]string(nil), d.PrefixChangeReasons...),
SystemHash: d.SystemHash,
ToolsHash: d.ToolsHash,
LogRewriteVersion: d.LogRewriteVersion,
ToolSchemaTokens: d.ToolSchemaTokens,
CacheMissTokens: d.CacheMissTokens,
CacheHitTokens: d.CacheHitTokens,
}
}