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 +165 -40
- package/detection.ts +119 -0
- package/package.json +2 -1
- package/phrases.ts +83 -0
- package/tui.tsx +31 -6
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 {
|
|
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
|
-
|
|
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
|
-
|
|
253
|
+
providers?: ReadonlyArray<{ id: string; name: string }>
|
|
254
|
+
branch?: string | (() => string | undefined)
|
|
178
255
|
getMessages?: () => any[]
|
|
179
|
-
|
|
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 ||
|
|
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
|
|
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
|
-
|
|
217
|
-
setBlinkFrame(
|
|
218
|
-
|
|
219
|
-
setBlinkFrame(
|
|
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(() =>
|
|
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 =
|
|
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
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
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
|
|
298
|
-
const firstDelay =
|
|
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 (
|
|
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 ((
|
|
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 =
|
|
359
|
-
|
|
360
|
-
|
|
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
|
|
363
|
-
|
|
364
|
-
|
|
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={
|
|
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.
|
|
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
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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
|
},
|