plugin-gentleman 1.1.5 → 1.1.6

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/components.tsx CHANGED
@@ -1,9 +1,9 @@
1
1
  // @ts-nocheck
2
2
  /** @jsxImportSource @opentui/solid */
3
3
  import type { TuiThemeCurrent } from "@opencode-ai/plugin/tui"
4
- import { createSignal, onCleanup, createEffect } from "solid-js"
4
+ import { createSignal, onCleanup, createEffect, createMemo } from "solid-js"
5
5
  import type { Cfg } from "./config"
6
- import { getOSName, getProviders } from "./detection"
6
+ import { detectPrimaryStackContext, getOSName, getProviders, type DetectedStack } from "./detection"
7
7
  import {
8
8
  pupilPositionFrames,
9
9
  eyeSquinted,
@@ -14,7 +14,7 @@ import {
14
14
  mustachiMustacheOnly,
15
15
  zoneColors,
16
16
  } from "./ascii-frames"
17
- import { busyPhrases } from "./phrases"
17
+ import { pickBusyPhrase } from "./phrases"
18
18
 
19
19
  export type SemanticZone = "monocle" | "eyes" | "mustache" | "tongue" | "unknown"
20
20
 
@@ -114,6 +114,79 @@ const getMessageCost = (message: any): number => {
114
114
  )
115
115
  }
116
116
 
117
+ const ellipsize = (value: string, maxLength: number): string => {
118
+ if (value.length <= maxLength) return value
119
+ if (maxLength <= 3) return value.slice(0, maxLength)
120
+ return `${value.slice(0, maxLength - 3)}...`
121
+ }
122
+
123
+ const resolveProp = <T,>(value: T | (() => T) | undefined): T | undefined => {
124
+ if (typeof value === "function") {
125
+ return (value as () => T)()
126
+ }
127
+ return value
128
+ }
129
+
130
+ const rightEyeStackMark: Record<DetectedStack, string> = {
131
+ react: "R",
132
+ angular: "A",
133
+ vue: "V",
134
+ node: "N",
135
+ go: "G",
136
+ python: "P",
137
+ dotnet: "D",
138
+ svelte: "S",
139
+ nextjs: "X",
140
+ rust: "U",
141
+ }
142
+
143
+ const replaceCharAt = (line: string, index: number, value: string): string => {
144
+ if (index < 0 || index >= line.length) return line
145
+ return `${line.slice(0, index)}${value}${line.slice(index + 1)}`
146
+ }
147
+
148
+ const applyRightEyeContextualMark = (frame: string[], stack: DetectedStack | undefined): string[] => {
149
+ if (!stack) return frame
150
+ const marker = rightEyeStackMark[stack]
151
+ if (!marker) return frame
152
+
153
+ return frame.map((line, idx) => {
154
+ if (idx < 2 || idx > 3) return line
155
+ return replaceCharAt(line, 20, marker)
156
+ })
157
+ }
158
+
159
+ type MustachiVisualState = "idle" | "thinking" | "working"
160
+
161
+ const getRuntimeVisualHint = (runtimeContext: any): MustachiVisualState | undefined => {
162
+ try {
163
+ const runtime = runtimeContext?.runtime ?? runtimeContext
164
+ const status = String(runtime?.status ?? runtime?.state ?? runtime?.phase ?? "").toLowerCase()
165
+ const runningSignal = runtime?.running ?? runtimeContext?.session?.running
166
+
167
+ if (runningSignal === true) return "working"
168
+ if (status.includes("work") || status.includes("run") || status.includes("load") || status.includes("generat")) {
169
+ return "working"
170
+ }
171
+ if (status.includes("think") || status.includes("reason") || status.includes("plan")) {
172
+ return "thinking"
173
+ }
174
+ return
175
+ } catch {
176
+ return
177
+ }
178
+ }
179
+
180
+ const resolveVisualState = (input: {
181
+ isBusy: boolean
182
+ runtimeHint?: MustachiVisualState
183
+ expressiveCycle: boolean
184
+ }): MustachiVisualState => {
185
+ if (input.isBusy || input.runtimeHint === "working") return "working"
186
+ if (input.expressiveCycle || input.runtimeHint === "thinking") return "thinking"
187
+ return "idle"
188
+ }
189
+
117
190
  const ProgressBar = (props: {
118
191
  theme?: TuiThemeCurrent
119
192
  totalTokens: number
@@ -121,6 +194,7 @@ const ProgressBar = (props: {
121
194
  contextLimit?: number
122
195
  }) => {
123
196
  const safeLimit = Math.max(0, toNumber(props.contextLimit))
197
+ const hasContextLimit = safeLimit > 0
124
198
  const usagePct = getPct(props.totalTokens, safeLimit)
125
199
  const barWidth = 18
126
200
  const filled = Math.round((usagePct / 100) * barWidth)
@@ -129,7 +203,9 @@ const ProgressBar = (props: {
129
203
  return (
130
204
  <box flexDirection="column" alignItems="center" marginTop={1}>
131
205
  <text fg={props.theme?.textMuted ?? zoneColors.mustache}>Tokens: {formatTokens(props.totalTokens)}</text>
132
- <text fg={props.theme?.accent ?? zoneColors.monocle}>Usage: {usagePct}% {bar}</text>
206
+ {hasContextLimit && (
207
+ <text fg={props.theme?.accent ?? zoneColors.monocle}>Usage: {usagePct}% {bar}</text>
208
+ )}
133
209
  <text fg={props.theme?.textMuted ?? zoneColors.mustache}>Cost: {formatCost(props.totalCost)}</text>
134
210
  </box>
135
211
  )
@@ -174,19 +250,40 @@ export const SidebarMustachi = (props: {
174
250
  theme: TuiThemeCurrent
175
251
  config: Cfg
176
252
  isBusy?: boolean
177
- branch?: string
253
+ providers?: ReadonlyArray<{ id: string; name: string }>
254
+ branch?: string | (() => string | undefined)
178
255
  getMessages?: () => any[]
179
- contextLimit?: number
256
+ runtimeContext?: any | (() => any)
257
+ contextLimit?: number | (() => number | undefined)
180
258
  }) => {
181
259
  const [pupilIndex, setPupilIndex] = createSignal(0)
182
260
  const [blinkFrame, setBlinkFrame] = createSignal(0)
183
261
  const [tongueFrame, setTongueFrame] = createSignal(0)
184
262
  const [busyPhrase, setBusyPhrase] = createSignal("")
185
263
  const [expressiveCycle, setExpressiveCycle] = createSignal(false)
264
+ const [phraseCycle, setPhraseCycle] = createSignal(0)
265
+
266
+ const detectedStack = createMemo(() => {
267
+ return detectPrimaryStackContext({
268
+ providers: props.providers,
269
+ runtimeContext: resolveProp(props.runtimeContext),
270
+ messages: props.getMessages?.(),
271
+ })
272
+ })
273
+
274
+ const runtimeHint = createMemo(() => getRuntimeVisualHint(resolveProp(props.runtimeContext)))
275
+
276
+ const visualState = createMemo<MustachiVisualState>(() => {
277
+ return resolveVisualState({
278
+ isBusy: !!props.isBusy,
279
+ runtimeHint: runtimeHint(),
280
+ expressiveCycle: expressiveCycle(),
281
+ })
282
+ })
186
283
 
187
284
  // Animation: pupil movement (look around) - random transitions, not a sequence
188
285
  createEffect(() => {
189
- if (!props.config.animations || props.isBusy || expressiveCycle()) {
286
+ if (!props.config.animations || visualState() !== "idle") {
190
287
  setPupilIndex(0)
191
288
  return
192
289
  }
@@ -210,17 +307,22 @@ export const SidebarMustachi = (props: {
210
307
  createEffect(() => {
211
308
  if (!props.config.animations) return
212
309
 
213
- const blinkSequence = async () => {
310
+ const timeoutIds = new Set<NodeJS.Timeout>()
311
+ const schedule = (fn: () => void, delay: number) => {
312
+ const timeoutId = setTimeout(() => {
313
+ timeoutIds.delete(timeoutId)
314
+ fn()
315
+ }, delay)
316
+ timeoutIds.add(timeoutId)
317
+ }
318
+
319
+ const blinkSequence = () => {
214
320
  // Open -> half -> closed -> half -> open (normal eyelid motion)
215
321
  setBlinkFrame(0)
216
- await new Promise(r => setTimeout(r, 100))
217
- setBlinkFrame(1)
218
- await new Promise(r => setTimeout(r, 80))
219
- setBlinkFrame(2)
220
- await new Promise(r => setTimeout(r, 80))
221
- setBlinkFrame(1)
222
- await new Promise(r => setTimeout(r, 80))
223
- setBlinkFrame(0)
322
+ schedule(() => setBlinkFrame(1), 100)
323
+ schedule(() => setBlinkFrame(2), 180)
324
+ schedule(() => setBlinkFrame(1), 260)
325
+ schedule(() => setBlinkFrame(0), 340)
224
326
  }
225
327
 
226
328
  const interval = setInterval(() => {
@@ -231,7 +333,13 @@ export const SidebarMustachi = (props: {
231
333
  }
232
334
  }, 2000) // Natural cadence: check every 2s for blink
233
335
 
234
- onCleanup(() => clearInterval(interval))
336
+ onCleanup(() => {
337
+ clearInterval(interval)
338
+ for (const timeoutId of timeoutIds) {
339
+ clearTimeout(timeoutId)
340
+ }
341
+ timeoutIds.clear()
342
+ })
235
343
  })
236
344
 
237
345
  // Busy/expressive state animation: tongue + single rotating phrase
@@ -244,7 +352,7 @@ export const SidebarMustachi = (props: {
244
352
  return
245
353
  }
246
354
 
247
- const shouldShowExpression = props.isBusy || expressiveCycle()
355
+ const shouldShowExpression = visualState() !== "idle"
248
356
 
249
357
  if (!shouldShowExpression) {
250
358
  setTongueFrame(0)
@@ -263,13 +371,13 @@ export const SidebarMustachi = (props: {
263
371
  }
264
372
  tongueTimeoutId = setTimeout(growTongue, 200)
265
373
 
266
- // Pick a single random phrase for this expressive cycle/state
267
- const pickRandomPhrase = () => {
268
- const randomIndex = Math.floor(Math.random() * busyPhrases.length)
269
- return busyPhrases[randomIndex]
270
- }
271
-
272
- setBusyPhrase(pickRandomPhrase())
374
+ const nextCycle = phraseCycle() + 1
375
+ setPhraseCycle(nextCycle)
376
+ setBusyPhrase(previous => pickBusyPhrase({
377
+ framework: detectedStack(),
378
+ cycle: nextCycle,
379
+ previous,
380
+ }))
273
381
 
274
382
  onCleanup(() => {
275
383
  if (tongueTimeoutId !== undefined) {
@@ -282,6 +390,7 @@ export const SidebarMustachi = (props: {
282
390
  // This ensures tongue + phrases are visibly demonstrated even if runtime busy state is unreliable
283
391
  createEffect(() => {
284
392
  if (!props.config.animations || props.isBusy) return
393
+ if (runtimeHint() === "working" || runtimeHint() === "thinking") return
285
394
 
286
395
  let cycleEndTimeout: NodeJS.Timeout | undefined
287
396
 
@@ -294,8 +403,8 @@ export const SidebarMustachi = (props: {
294
403
  }, 8000)
295
404
  }
296
405
 
297
- // First cycle after 30-45s, then every 45-60s (calm, occasional expressiveness)
298
- const firstDelay = 30000 + Math.random() * 15000
406
+ // First cycle after 45-60s, then every 45-60s (calm, occasional expressiveness)
407
+ const firstDelay = 45000 + Math.random() * 15000
299
408
  const firstTimeout = setTimeout(triggerExpressiveCycle, firstDelay)
300
409
 
301
410
  const interval = setInterval(() => {
@@ -321,7 +430,7 @@ export const SidebarMustachi = (props: {
321
430
  let eyeFrame = pupilPositionFrames[pupilIndex()]
322
431
 
323
432
  // Apply squint if busy/expressive
324
- if (props.isBusy || expressiveCycle()) {
433
+ if (visualState() !== "idle") {
325
434
  eyeFrame = eyeSquinted
326
435
  }
327
436
 
@@ -330,6 +439,8 @@ export const SidebarMustachi = (props: {
330
439
  eyeFrame = eyeBlinkHalf
331
440
  } else if (blinkFrame() === 2) {
332
441
  eyeFrame = eyeBlinkClosed
442
+ } else {
443
+ eyeFrame = applyRightEyeContextualMark(eyeFrame, detectedStack())
333
444
  }
334
445
 
335
446
  // Add eyes with zone metadata
@@ -345,7 +456,7 @@ export const SidebarMustachi = (props: {
345
456
  })
346
457
 
347
458
  // Add tongue if expressive (mark as tongue zone for pink color)
348
- if ((props.isBusy || expressiveCycle()) && tongueFrame() > 0) {
459
+ if (visualState() !== "idle" && tongueFrame() > 0) {
349
460
  const tongueLines = tongueFrames[tongueFrame()]
350
461
  tongueLines.forEach(line => {
351
462
  lines.push({ content: line, zone: "tongue" })
@@ -355,13 +466,27 @@ export const SidebarMustachi = (props: {
355
466
  return lines
356
467
  }
357
468
 
358
- const branchLabel = props.branch?.trim()
359
- const messages = props.getMessages?.() ?? []
360
- const assistantMessages = messages.filter((message: any) => getMessageRole(message) === "assistant")
469
+ const branchLabel = createMemo(() => {
470
+ const value = resolveProp(props.branch)?.trim()
471
+ if (!value) return ""
472
+ return ellipsize(value, 24)
473
+ })
474
+
475
+ const resolvedContextLimit = createMemo(() => resolveProp(props.contextLimit))
476
+
477
+ const assistantMessages = createMemo(() => {
478
+ const messages = props.getMessages?.() ?? []
479
+ return messages.filter((message: any) => getMessageRole(message) === "assistant")
480
+ })
481
+
482
+ const contextTokens = createMemo(() => {
483
+ const lastAssistantWithTokens = [...assistantMessages()].reverse().find((message: any) => hasTokenData(message))
484
+ return getContextTokens(lastAssistantWithTokens)
485
+ })
361
486
 
362
- const lastAssistantWithTokens = [...assistantMessages].reverse().find((message: any) => hasTokenData(message))
363
- const contextTokens = getContextTokens(lastAssistantWithTokens)
364
- const totalCost = assistantMessages.reduce((sum: number, message: any) => sum + getMessageCost(message), 0)
487
+ const totalCost = createMemo(() => {
488
+ return assistantMessages().reduce((sum: number, message: any) => sum + getMessageCost(message), 0)
489
+ })
365
490
 
366
491
  return (
367
492
  <box flexDirection="column" alignItems="center">
@@ -372,16 +497,16 @@ export const SidebarMustachi = (props: {
372
497
  return <text fg={color}>{paddedLine}</text>
373
498
  })}
374
499
 
375
- {branchLabel && (
376
- <text fg={props.theme?.textMuted ?? zoneColors.mustache}>⎇ {branchLabel}</text>
500
+ {branchLabel() && (
501
+ <text fg={props.theme?.textMuted ?? zoneColors.mustache}>⎇ {branchLabel()}</text>
377
502
  )}
378
503
 
379
504
  {props.config.show_metrics && (
380
505
  <ProgressBar
381
506
  theme={props.theme}
382
- totalTokens={contextTokens}
383
- totalCost={totalCost}
384
- contextLimit={props.contextLimit}
507
+ totalTokens={contextTokens()}
508
+ totalCost={totalCost()}
509
+ contextLimit={resolvedContextLimit()}
385
510
  />
386
511
  )}
387
512
 
package/detection.ts CHANGED
@@ -2,6 +2,18 @@
2
2
 
3
3
  import { readFileSync } from "node:fs"
4
4
 
5
+ export type DetectedStack =
6
+ | "react"
7
+ | "angular"
8
+ | "vue"
9
+ | "node"
10
+ | "go"
11
+ | "python"
12
+ | "dotnet"
13
+ | "svelte"
14
+ | "nextjs"
15
+ | "rust"
16
+
5
17
  // Helper to detect OS name
6
18
  export const getOSName = (): string => {
7
19
  try {
@@ -59,3 +71,110 @@ export const getProviders = (providers: ReadonlyArray<{ id: string; name: string
59
71
  // Return compact comma-separated list
60
72
  return Array.from(names).sort().join(", ")
61
73
  }
74
+
75
+ type StackDetectionInput = {
76
+ providers?: ReadonlyArray<{ id: string; name: string }>
77
+ runtimeContext?: any
78
+ messages?: any[]
79
+ }
80
+
81
+ const stackRules: Record<DetectedStack, RegExp[]> = {
82
+ react: [/\breact\b/, /\btsx\b/, /\bjsx\b/],
83
+ angular: [/\bangular\b/, /\bnx\b/],
84
+ vue: [/\bvue\b/, /\bnuxt\b/],
85
+ node: [/\bnode\b/, /\bnodejs\b/, /\bexpress\b/, /\bnestjs\b/],
86
+ go: [/\bgolang\b/, /\bgo\s+mod\b/, /\bgo\.sum\b/, /\bgin\b/, /\bfiber\b/],
87
+ python: [/\bpython\b/, /\.py\b/, /\bdjango\b/, /\bfastapi\b/],
88
+ dotnet: [/\.net\b/, /\bdotnet\b/, /\bc#\b/, /\basp\.net\b/],
89
+ svelte: [/\bsvelte\b/, /\bsveltekit\b/],
90
+ nextjs: [/\bnext\.js\b/, /\bnextjs\b/],
91
+ rust: [/\brust\b/, /\baxum\b/, /\bactix\b/],
92
+ }
93
+
94
+ const stackPriority: DetectedStack[] = [
95
+ "nextjs",
96
+ "react",
97
+ "angular",
98
+ "vue",
99
+ "svelte",
100
+ "node",
101
+ "go",
102
+ "python",
103
+ "dotnet",
104
+ "rust",
105
+ ]
106
+
107
+ const pushHint = (bucket: string[], value: unknown) => {
108
+ if (typeof value === "string" && value.trim()) {
109
+ bucket.push(value.toLowerCase())
110
+ }
111
+ }
112
+
113
+ const extractText = (value: unknown): string => {
114
+ if (typeof value === "string") return value
115
+ if (Array.isArray(value)) {
116
+ return value.map(item => extractText(item)).filter(Boolean).join(" ")
117
+ }
118
+ if (value && typeof value === "object") {
119
+ const item = value as Record<string, unknown>
120
+ return [extractText(item.text), extractText(item.content), extractText(item.value)]
121
+ .filter(Boolean)
122
+ .join(" ")
123
+ }
124
+ return ""
125
+ }
126
+
127
+ export const detectPrimaryStackContext = (input: StackDetectionInput): DetectedStack | undefined => {
128
+ try {
129
+ const hints: string[] = []
130
+ const runtime = input.runtimeContext
131
+ const model = runtime?.model ?? runtime?.runtime?.model
132
+ const metadata = runtime?.metadata ?? runtime?.runtime?.metadata
133
+
134
+ for (const provider of input.providers ?? []) {
135
+ pushHint(hints, provider.id)
136
+ pushHint(hints, provider.name)
137
+ }
138
+
139
+ pushHint(hints, model?.id)
140
+ pushHint(hints, model?.name)
141
+ pushHint(hints, runtime?.provider)
142
+ pushHint(hints, metadata?.framework)
143
+ pushHint(hints, metadata?.stack)
144
+ pushHint(hints, metadata?.language)
145
+
146
+ const recentMessages = (input.messages ?? []).slice(-6)
147
+ for (const message of recentMessages) {
148
+ pushHint(hints, message?.model)
149
+ pushHint(hints, message?.provider)
150
+ pushHint(hints, extractText(message?.content))
151
+ pushHint(hints, extractText(message?.message?.content))
152
+ }
153
+
154
+ if (!hints.length) return
155
+
156
+ const scores = new Map<DetectedStack, number>()
157
+ const corpus = hints.join(" ")
158
+ for (const stack of stackPriority) {
159
+ let score = 0
160
+ for (const pattern of stackRules[stack]) {
161
+ if (pattern.test(corpus)) score += 1
162
+ }
163
+ if (score > 0) scores.set(stack, score)
164
+ }
165
+
166
+ let best: DetectedStack | undefined
167
+ let bestScore = 0
168
+ for (const stack of stackPriority) {
169
+ const score = scores.get(stack) ?? 0
170
+ if (score > bestScore) {
171
+ best = stack
172
+ bestScore = score
173
+ }
174
+ }
175
+
176
+ return best
177
+ } catch {
178
+ return
179
+ }
180
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://json.schemastore.org/package.json",
3
3
  "name": "plugin-gentleman",
4
- "version": "1.1.5",
4
+ "version": "1.1.6",
5
5
  "description": "OpenCode TUI plugin featuring Mustachi - an animated ASCII mascot with eyes, mustache, and optional motivational phrases during busy states",
6
6
  "type": "module",
7
7
  "exports": {
@@ -14,6 +14,7 @@
14
14
  "show_detected": true,
15
15
  "show_os": true,
16
16
  "show_providers": true,
17
+ "show_metrics": true,
17
18
  "animations": true
18
19
  }
19
20
  }
package/phrases.ts CHANGED
@@ -1,6 +1,8 @@
1
1
  // Motivational phrases for busy/loading state
2
2
  // Add new phrases here to expand the library
3
3
 
4
+ import type { DetectedStack } from "./detection"
5
+
4
6
  export const busyPhrases = [
5
7
  // Original classics
6
8
  "Ponete las pilas, hermano...",
@@ -76,3 +78,84 @@ export const busyPhrases = [
76
78
  "Estoy cerrando con moñito, bancame un toque",
77
79
  "¿Cuál es el colmo de un electricista? No encontrar su corriente de trabajo",
78
80
  ]
81
+
82
+ export const frameworkBusyPhrases: Partial<Record<DetectedStack, string[]>> = {
83
+ react: [
84
+ "Ajustando hooks y estado...",
85
+ "Renderizando componentes con onda...",
86
+ "Sincronizando props y efectos...",
87
+ ],
88
+ angular: [
89
+ "Ordenando módulos y providers...",
90
+ "Inyectando dependencias como relojito...",
91
+ "Acomodando el template de Angular...",
92
+ ],
93
+ vue: [
94
+ "Ajustando refs y reactividad...",
95
+ "Cocinando un composable fino...",
96
+ "Vue está pensando en voz baja...",
97
+ ],
98
+ node: [
99
+ "Encolando eventos del loop...",
100
+ "Levantando handlers de Node...",
101
+ "Conectando rutas del backend...",
102
+ ],
103
+ go: [
104
+ "Compilando goroutines con paciencia...",
105
+ "Ajustando canales y concurrencia...",
106
+ "Go está afilando la respuesta...",
107
+ ],
108
+ python: [
109
+ "Ordenando imports y scripts...",
110
+ "Entrenando al snake para correr más...",
111
+ "Ajustando funciones con estilo Python...",
112
+ ],
113
+ dotnet: [
114
+ "Alineando servicios de .NET...",
115
+ "Puliendo capas con C#...",
116
+ "Ensamblando pipeline de ASP.NET...",
117
+ ],
118
+ svelte: [
119
+ "Svelte está reaccionando en silencio...",
120
+ "Ajustando stores y transiciones...",
121
+ "Compilando magia liviana...",
122
+ ],
123
+ nextjs: [
124
+ "Pre-renderizando rutas de Next...",
125
+ "Ajustando server components...",
126
+ "Hidratando la app con elegancia...",
127
+ ],
128
+ rust: [
129
+ "Prestando atención al borrow checker...",
130
+ "Afinando ownership sin piedad...",
131
+ "Oxidando bugs, una línea a la vez...",
132
+ ],
133
+ }
134
+
135
+ const hashSeed = (value: string): number => {
136
+ let hash = 0
137
+ for (let i = 0; i < value.length; i++) {
138
+ hash = (hash * 31 + value.charCodeAt(i)) >>> 0
139
+ }
140
+ return hash
141
+ }
142
+
143
+ export const pickBusyPhrase = (input: {
144
+ framework?: DetectedStack
145
+ cycle: number
146
+ previous?: string
147
+ }): string => {
148
+ const frameworkPool = input.framework ? frameworkBusyPhrases[input.framework] : undefined
149
+ const pool = frameworkPool && frameworkPool.length > 0 ? [...frameworkPool, ...busyPhrases] : busyPhrases
150
+
151
+ if (!pool.length) return ""
152
+
153
+ const seed = hashSeed(input.framework || "generic")
154
+ let index = Math.abs(seed + input.cycle * 7) % pool.length
155
+
156
+ if (pool.length > 1 && input.previous && pool[index] === input.previous) {
157
+ index = (index + 1) % pool.length
158
+ }
159
+
160
+ return pool[index]
161
+ }
package/tui.tsx CHANGED
@@ -12,6 +12,29 @@ const rec = (value: unknown) => {
12
12
  return Object.fromEntries(Object.entries(value))
13
13
  }
14
14
 
15
+ const toNumber = (value: unknown): number | undefined => {
16
+ if (typeof value !== "number") return
17
+ if (!Number.isFinite(value) || value <= 0) return
18
+ return value
19
+ }
20
+
21
+ const getContextLimitFromRuntime = (ctxValue: any): number | undefined => {
22
+ const runtimeMetadata = ctxValue?.runtime?.metadata
23
+ const metadata = ctxValue?.metadata
24
+ const model = ctxValue?.model
25
+
26
+ return (
27
+ toNumber(runtimeMetadata?.contextLimit) ??
28
+ toNumber(runtimeMetadata?.context_limit) ??
29
+ toNumber(metadata?.contextLimit) ??
30
+ toNumber(metadata?.context_limit) ??
31
+ toNumber(model?.contextLimit) ??
32
+ toNumber(model?.context_limit) ??
33
+ toNumber(ctxValue?.contextLimit) ??
34
+ toNumber(ctxValue?.context_limit)
35
+ )
36
+ }
37
+
15
38
  const tui: TuiPlugin = async (api, options) => {
16
39
  const boot = cfg(rec(options))
17
40
  if (!boot.enabled) return
@@ -61,17 +84,19 @@ const tui: TuiPlugin = async (api, options) => {
61
84
  return <DetectedEnv theme={ctx.theme.current} providers={api.state.provider} config={value()} />
62
85
  },
63
86
  sidebar_content(ctx) {
64
- // Extract sessionID from ctx.value parameter
65
- const sessionID = ctx.value?.sessionID
66
-
67
87
  return (
68
88
  <SidebarMustachi
69
89
  theme={ctx.theme.current}
70
90
  config={value()}
71
91
  isBusy={isBusy()}
72
- branch={api.state.vcs?.branch}
73
- getMessages={() => sessionID ? api.state.session.messages(sessionID) : []}
74
- contextLimit={1_000_000}
92
+ providers={api.state.provider}
93
+ branch={() => api.state.vcs?.branch}
94
+ getMessages={() => {
95
+ const sessionID = ctx.value?.sessionID
96
+ return sessionID ? api.state.session.messages(sessionID) : []
97
+ }}
98
+ runtimeContext={() => ctx.value}
99
+ contextLimit={() => getContextLimitFromRuntime(ctx.value)}
75
100
  />
76
101
  )
77
102
  },