Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions desktop/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -3403,6 +3403,7 @@ type Meta struct {
TokenMode string `json:"tokenMode"`
Goal string `json:"goal,omitempty"`
GoalStatus string `json:"goalStatus,omitempty"`
Currency string `json:"currency,omitempty"` // display currency symbol from provider pricing (e.g. "$", "¥")
}

// Meta reports the model label, readiness, any startup error, the working
Expand All @@ -3427,6 +3428,10 @@ func (a *App) MetaForTab(tabID string) Meta {
tokenMode := currentTabTokenMode(tab)
goal := currentTabGoal(tab)
goalStatus := currentTabGoalStatus(tab)
var currency string
if tab.Ctrl != nil {
currency = tab.Ctrl.CurrencySymbol()
}
return Meta{
Label: tab.Label,
Ready: tab.Ready,
Expand All @@ -3444,6 +3449,7 @@ func (a *App) MetaForTab(tabID string) Meta {
TokenMode: tokenMode,
Goal: goal,
GoalStatus: goalStatus,
Currency: currency,
}
}

Expand Down
1 change: 1 addition & 0 deletions desktop/frontend/src/lib/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -355,6 +355,7 @@ export interface Meta {
tokenMode?: TokenMode;
goal?: string;
goalStatus?: GoalStatus;
currency?: string; // display currency symbol from provider pricing (e.g. "$", "¥")
}

export type CollaborationMode = "normal" | "plan" | "goal";
Expand Down
11 changes: 8 additions & 3 deletions desktop/frontend/src/lib/useController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,8 @@ export function sameMeta(a?: Meta, b?: Meta): boolean {
a.toolApprovalMode === b.toolApprovalMode &&
a.tokenMode === b.tokenMode &&
a.goal === b.goal &&
a.goalStatus === b.goalStatus
a.goalStatus === b.goalStatus &&
a.currency === b.currency
);
}

Expand Down Expand Up @@ -639,7 +640,11 @@ export function reducer(s: State, a: Action): State {
});
return { ...s, items: finalized, running: false, turnActive: false, pendingPrompt, backgroundJobs, cancelRequested, cancellable, live: undefined, currentAssistant: undefined, approval: undefined, ask: undefined };
}
case "meta": return sameMeta(s.meta, a.meta) ? s : { ...s, meta: a.meta };
case "meta": {
if (sameMeta(s.meta, a.meta)) return s;
const sessionCurrency = a.meta?.currency || s.sessionCurrency;
return { ...s, meta: a.meta, sessionCurrency };
}
case "context": {
const sessionTokens = typeof a.context.sessionTokens === "number"
? Math.max(0, a.context.sessionTokens)
Expand Down Expand Up @@ -669,7 +674,7 @@ export function reducer(s: State, a: Action): State {
case "local_notice": return { ...s, running: false, turnActive: false, seq: s.seq + 1, items: [...s.items, { kind: "notice", id: `n${s.seq}`, level: a.level, text: a.text }] };
case "clearApproval": return { ...s, approval: undefined, pendingPrompt: false };
case "clearAsk": return { ...s, ask: undefined, pendingPrompt: false };
case "reset": return { ...initialState, meta: s.meta, context: { ...s.context, used: 0, sessionTokens: 0 }, balance: s.balance, effort: s.effort, jobs: s.jobs, sessionGen: s.sessionGen + 1 };
case "reset": return { ...initialState, meta: s.meta, sessionCurrency: s.sessionCurrency, context: { ...s.context, used: 0, sessionTokens: 0 }, balance: s.balance, effort: s.effort, jobs: s.jobs, sessionGen: s.sessionGen + 1 };
case "event": return applyEvent(s, a.e);
default: return s;
}
Expand Down
38 changes: 38 additions & 0 deletions desktop/tab_profile_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -446,6 +446,44 @@ func TestSetBypassPreservesPlanMode(t *testing.T) {
}
}

func TestMetaReportsCurrencyFromProvider(t *testing.T) {
isolateDesktopUserDirs(t)

app := NewApp()
tab := testTab("a", t.TempDir())
app.tabs = map[string]*WorkspaceTab{tab.ID: tab}
app.tabOrder = []string{tab.ID}
app.activeTabID = tab.ID
defer tab.Ctrl.Close()

// Default: no pricing configured → empty currency.
meta := app.MetaForTab(tab.ID)
if meta.Currency != "" {
t.Fatalf("default currency = %q, want empty", meta.Currency)
}

// Simulate a controller built with USD pricing (e.g. DeepSeek).
tab.Ctrl = control.New(control.Options{Label: "deepseek", CurrencySymbol: "$"})
meta = app.MetaForTab(tab.ID)
if meta.Currency != "$" {
t.Fatalf("USD currency = %q, want $", meta.Currency)
}

// Simulate a controller built with CNY pricing (e.g. MIMO).
tab.Ctrl = control.New(control.Options{Label: "mimo", CurrencySymbol: "¥"})
meta = app.MetaForTab(tab.ID)
if meta.Currency != "¥" {
t.Fatalf("CNY currency = %q, want ¥", meta.Currency)
}

// Nil controller (boot failed / tab still loading) → empty, no panic.
tab.Ctrl = nil
meta = app.MetaForTab(tab.ID)
if meta.Currency != "" {
t.Fatalf("nil ctrl currency = %q, want empty", meta.Currency)
}
}

func userConfigPathForTest() string {
if dir, err := os.UserConfigDir(); err == nil {
return dir + "/reasonix/reasonix.toml"
Expand Down
1 change: 1 addition & 0 deletions internal/boot/boot.go
Original file line number Diff line number Diff line change
Expand Up @@ -823,6 +823,7 @@ func Build(ctx context.Context, opts Options) (*control.Controller, error) {
DisableColdResumePrune: !cfg.ColdResumePruneEnabled(),
Shell: shell,
PlanModeAllowedTools: cfg.Agent.PlanModeAllowedTools,
CurrencySymbol: entry.Price.Symbol(),
OnRemember: func(rule string) control.RememberResult {
return rememberPermissionRule(root, rule)
},
Expand Down
11 changes: 11 additions & 0 deletions internal/control/controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ type Controller struct {

label string
modelRef string
currencySymbol string // display currency symbol from provider pricing (e.g. "$", "¥"); empty = default "¥"
systemPrompt string
sessionDir string
host *plugin.Host
Expand Down Expand Up @@ -287,6 +288,10 @@ type Options struct {
// matches the agent's configured [tools.shell] choice. Zero value = auto.
Shell sandbox.Shell
Classifier autoPlanClassifier
// CurrencySymbol is the display currency symbol from the provider's pricing
// configuration (e.g. "$" for USD, "¥" for CNY). Empty means no pricing was
// configured and the frontend should use its default.
CurrencySymbol string
// OnRemember, when set, is invoked with a new allow rule the user chose to
// persist to disk (e.g. "Bash(go test:*)"). The callback is wired into the
// permission Gate on EnableInteractiveApproval.
Expand Down Expand Up @@ -334,6 +339,7 @@ func New(opts Options) *Controller {
disableColdResumePrune: opts.DisableColdResumePrune,
shell: opts.Shell,
classifier: classifier,
currencySymbol: opts.CurrencySymbol,
onRemember: opts.OnRemember,
balanceURL: opts.BalanceURL,
balanceKey: opts.BalanceKey,
Expand Down Expand Up @@ -2581,6 +2587,11 @@ func (c *Controller) DisconnectMCPServer(name string) bool {
// Label returns the human-readable model label, e.g. "deepseek-flash".
func (c *Controller) Label() string { return c.label }

// CurrencySymbol returns the display currency symbol from the provider's
// pricing configuration (e.g. "$" for USD, "¥" for CNY). Empty means the
// provider had no pricing configured and the frontend should use its default.
func (c *Controller) CurrencySymbol() string { return c.currencySymbol }

// WorkspaceRoot returns the workspace root for this controller's session
// (the directory that file-writers and @-references are scoped to).
// Empty means no scoping is in effect.
Expand Down
Loading