opencode-tweaks 0.7.1 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -197,17 +197,21 @@ The cost settings used to live in the Tokens entry. A choice saved there before
197
197
 
198
198
  ### Model
199
199
 
200
- Model shows the model the session is using and its list price in the sidebar, under the quota. The name, provider,
201
- and reasoning variant come from the session, so the card follows `/models`. Click the `Model` header to collapse or
202
- expand the card.
200
+ Model shows the current model's list price in the sidebar, under the quota. The model name, provider, and variant
201
+ already sit on the bottom bar, so the card only carries the rates and the peak state. Click the `Model` header to
202
+ collapse or expand it.
203
203
 
204
204
  `Input price`, `Output price`, and `Cache read price` choose the rates, all in dollars per million tokens. The card
205
- uses the catalog price that applies to the session's context, so a long-context tier shows the higher rate with its
206
- threshold (`>512K`).
207
-
208
- `Peak state` adds the provider's live peak marker for schedules the plugin knows, such as DeepSeek and OpenCode Go:
209
- `peak ×2 now` during the higher window and `off-peak` otherwise. The badge on the turn cost still records the window
210
- each round started in.
205
+ draws them as a price list with the labels on the left and the values in the theme accent. `Price alignment` picks
206
+ whether the prices start on one shared column (`left`) or sit against the right edge (`right`). It uses the catalog
207
+ price that applies to the session's context, so a long-context tier shows the higher rate with its threshold
208
+ (`>512K`).
209
+
210
+ `Peak state` puts the price multiplier on the right of the card header for schedules the plugin knows, such as
211
+ DeepSeek and OpenCode Go: `×2` in the warning colour during the higher window and `×1` in the normal text colour off
212
+ it. A model without a schedule shows nothing. The marker stays visible when the card is collapsed. `Peak only` hides
213
+ `×1`, so the header stays empty at the base price. The badge on the turn cost still records the window each round
214
+ started in.
211
215
 
212
216
  ## Development
213
217
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://json.schemastore.org/package.json",
3
3
  "name": "opencode-tweaks",
4
- "version": "0.7.1",
4
+ "version": "0.8.0",
5
5
  "description": "Opt-in settings and small interface improvements for OpenCode.",
6
6
  "type": "module",
7
7
  "license": "MIT",
@@ -18,19 +18,20 @@ const ENABLED = setting("enabled")
18
18
  const INPUT = setting("input")
19
19
  const OUTPUT = setting("output")
20
20
  const CACHE = setting("cache")
21
- const VARIANT = setting("variant")
22
21
  const PEAK = setting("peak")
22
+ const PEAK_ONLY = setting("peakOnly")
23
+ const ALIGN = setting("align")
23
24
 
24
25
  /** The ref stored on a session or returned by the composer. */
25
26
  interface ModelRef {
26
27
  readonly providerID: string
27
28
  readonly id: string
28
- readonly variant?: string
29
29
  }
30
30
 
31
31
  /**
32
- * The model card is a companion to the cost figures: it names the model and
33
- * lists its catalog rates, while the cost tweak counts what the session spent.
32
+ * The model card sits under the quota and carries what the bottom bar does
33
+ * not: the current model's list price and its provider's peak state. The
34
+ * model name and variant already live on that bar, so the card stays compact.
34
35
  */
35
36
  export function ModelPanel(props: {
36
37
  readonly context: Context
@@ -58,8 +59,9 @@ function ModelBody(props: {
58
59
  const inputOn = () => props.store.read(modelTweak, INPUT).value === true
59
60
  const outputOn = () => props.store.read(modelTweak, OUTPUT).value === true
60
61
  const cacheOn = () => props.store.read(modelTweak, CACHE).value === true
61
- const variantOn = () => props.store.read(modelTweak, VARIANT).value === true
62
62
  const peakOn = () => props.store.read(modelTweak, PEAK).value === true
63
+ const peakOnly = () => props.store.read(modelTweak, PEAK_ONLY).value === true
64
+ const alignRight = () => props.store.read(modelTweak, ALIGN).value === "right"
63
65
 
64
66
  /** The session's model, or the composer's while the session has none yet. */
65
67
  const model = createMemo<ModelRef | undefined>(() => {
@@ -67,31 +69,21 @@ function ModelBody(props: {
67
69
  if (session?.model) return session.model
68
70
 
69
71
  const current = props.context.ui.model.current()
70
- return current
71
- ? { providerID: current.providerID, id: current.modelID, variant: current.variant }
72
- : undefined
72
+ return current ? { providerID: current.providerID, id: current.modelID } : undefined
73
73
  })
74
74
 
75
- /** The catalog entry for that model, with what the panel needs to show it. */
76
- const entry = createMemo(() => {
75
+ /** The catalog rates for that model, when the catalog knows it. */
76
+ const costs = createMemo(() => {
77
77
  const ref = model()
78
- if (!ref) return undefined
79
-
80
- const found = (props.context.data.location.model.list() ?? []).find(
81
- (candidate) =>
82
- candidate.providerID === ref.providerID &&
83
- (candidate.modelID === ref.id || candidate.id === ref.id),
84
- )
85
- const provider = (props.context.data.location.provider.list() ?? []).find(
86
- (candidate) => candidate.id === ref.providerID,
78
+ if (!ref) return []
79
+
80
+ return (
81
+ (props.context.data.location.model.list() ?? []).find(
82
+ (candidate) =>
83
+ candidate.providerID === ref.providerID &&
84
+ (candidate.modelID === ref.id || candidate.id === ref.id),
85
+ )?.cost ?? []
87
86
  )
88
-
89
- return {
90
- ref,
91
- name: found?.name ?? ref.id,
92
- provider: provider?.name,
93
- costs: found?.cost ?? [],
94
- }
95
87
  })
96
88
 
97
89
  /** The context the session has read so far, which picks a long-context tier. */
@@ -101,52 +93,52 @@ function ModelBody(props: {
101
93
  })
102
94
 
103
95
  const price = createMemo(() => {
104
- const costs = entry()?.costs
105
- return costs?.length ? priceFor(costs, contextSize()) : undefined
96
+ const list = costs()
97
+ return list.length > 0 ? priceFor(list, contextSize()) : undefined
106
98
  })
107
99
 
108
100
  const rates = createMemo(() => {
109
101
  const value = price()
110
102
  if (!value) return []
111
103
 
112
- const rows: { label: string; value: string }[] = []
113
- if (inputOn()) rows.push({ label: "in", value: formatRate(value.input) })
114
- if (outputOn()) rows.push({ label: "out", value: formatRate(value.output) })
115
- if (cacheOn()) rows.push({ label: "cache", value: formatRate(value.cache.read) })
104
+ const rows: { label: string; value: string; muted?: boolean }[] = []
105
+ if (inputOn()) rows.push({ label: "Input", value: formatRate(value.input) })
106
+ if (outputOn()) rows.push({ label: "Output", value: formatRate(value.output) })
107
+ if (cacheOn()) rows.push({ label: "Cache", value: formatRate(value.cache.read) })
116
108
  if (rows.length > 0 && value.tier) {
117
- rows.push({ label: "from", value: `>${formatTokens(value.tier.size, "compact")}` })
109
+ rows.push({
110
+ label: "From",
111
+ value: `>${formatTokens(value.tier.size, "compact")}`,
112
+ muted: true,
113
+ })
118
114
  }
119
115
 
120
116
  return rows
121
117
  })
122
118
 
123
- const variant = () => (variantOn() ? entry()?.ref.variant : undefined)
124
-
125
- const title = () => {
126
- const name = entry()?.name
127
- const selected = variant()
128
- return selected ? `${name} · ${selected}` : name
129
- }
119
+ /** The widest label, so the prices start on one column. */
120
+ const labelWidth = createMemo(() =>
121
+ rates().reduce((width, row) => Math.max(width, row.label.length), 0),
122
+ )
130
123
 
131
124
  const scheduled = () => {
132
- const ref = entry()?.ref
125
+ const ref = model()
133
126
  return ref ? hasPriceSchedule(ref) : false
134
127
  }
135
128
 
136
129
  const peak = () => {
137
- const ref = entry()?.ref
130
+ const ref = model()
138
131
  if (!ref || !peakOn() || !scheduled()) return undefined
139
132
  return priceWindowFor(ref, new Date(now()))
140
133
  }
141
134
 
142
- const stateValue = () => {
143
- if (!peakOn() || !scheduled()) return undefined
144
- const value = peak()
145
- return value ? `${value.label} ×${value.multiplier} now` : "off-peak"
146
- }
135
+ /** The price multiplier now, `1` when the catalog price is the one charged. */
136
+ const multiplier = () => peak()?.multiplier ?? 1
137
+
138
+ const showMultiplier = () => peakOn() && scheduled() && (peak() !== undefined || !peakOnly())
147
139
 
148
140
  // The live peak marker follows the clock, so refresh it while the card can
149
- // show one. Models without a schedule and a hidden peak row need no timer.
141
+ // show one. Models without a schedule and a hidden peak state need no timer.
150
142
  createEffect(() => {
151
143
  if (!peakOn() || !scheduled()) return
152
144
 
@@ -156,7 +148,7 @@ function ModelBody(props: {
156
148
  })
157
149
 
158
150
  return (
159
- <box flexDirection="column" marginTop={1}>
151
+ <box flexDirection="column">
160
152
  <box
161
153
  flexDirection="row"
162
154
  justifyContent="space-between"
@@ -165,37 +157,33 @@ function ModelBody(props: {
165
157
  <text attributes={TextAttributes.BOLD} fg={theme().text.base}>
166
158
  {collapsed() ? "▶" : "▼"} Model
167
159
  </text>
168
- <Show when={entry()?.provider}>
169
- <text fg={theme().text.muted} wrapMode="none">
170
- {entry()?.provider}
160
+ <Show when={showMultiplier()}>
161
+ <text
162
+ fg={peak() ? theme().text.feedback.warning.base : theme().text.base}
163
+ wrapMode="none"
164
+ >
165
+ {`×${multiplier()}`}
171
166
  </text>
172
167
  </Show>
173
168
  </box>
174
169
  <Show when={!collapsed()}>
175
- <Show when={entry()} fallback={<text fg={theme().text.muted}>No model selected</text>}>
176
- <text fg={theme().text.base} wrapMode="word">
177
- {title()}
178
- </text>
170
+ <Show when={model()} fallback={<text fg={theme().text.muted}>No model selected</text>}>
179
171
  <For each={rates()}>
180
172
  {(row) => (
181
- <box flexDirection="row" gap={1}>
182
- <text flexShrink={0} fg={theme().text.muted}>
183
- {row.label.padEnd(5)}
173
+ <box
174
+ flexDirection="row"
175
+ justifyContent={alignRight() ? "space-between" : "flex-start"}
176
+ gap={1}
177
+ >
178
+ <text flexShrink={0} fg={theme().text.base}>
179
+ {alignRight() ? row.label : row.label.padEnd(labelWidth())}
180
+ </text>
181
+ <text flexShrink={0} fg={row.muted ? theme().text.muted : theme().hue.accent[200]}>
182
+ {row.value}
184
183
  </text>
185
- <text fg={theme().text.base}>{row.value}</text>
186
184
  </box>
187
185
  )}
188
186
  </For>
189
- <Show when={stateValue()}>
190
- <box flexDirection="row" gap={1}>
191
- <text flexShrink={0} fg={theme().text.muted}>
192
- {"state".padEnd(5)}
193
- </text>
194
- <text fg={peak() ? theme().text.feedback.warning.base : theme().text.muted}>
195
- {stateValue()}
196
- </text>
197
- </box>
198
- </Show>
199
187
  </Show>
200
188
  </Show>
201
189
  </box>
@@ -44,20 +44,29 @@ export const modelTweak = defineTweak({
44
44
  default: true,
45
45
  },
46
46
  {
47
- id: "variant",
48
- title: "Variant",
49
- description: "Show the reasoning variant selected for the model.",
47
+ id: "peak",
48
+ title: "Peak state",
49
+ description: "Show the model's current price multiplier in the card header.",
50
50
  category: "Display",
51
51
  kind: "boolean",
52
52
  default: true,
53
53
  },
54
54
  {
55
- id: "peak",
56
- title: "Peak state",
57
- description: "Show whether the model is billed above its catalog price right now.",
55
+ id: "align",
56
+ title: "Price alignment",
57
+ description: "Start the prices on one column, or push them against the right edge.",
58
+ category: "Display",
59
+ kind: "select",
60
+ default: "right",
61
+ values: ["left", "right"],
62
+ },
63
+ {
64
+ id: "peakOnly",
65
+ title: "Peak only",
66
+ description: "Show the multiplier only during a peak window.",
58
67
  category: "Display",
59
68
  kind: "boolean",
60
- default: true,
69
+ default: false,
61
70
  },
62
71
  ],
63
72
  })