@rizal72/oc-models-pro 0.1.1 → 0.1.2
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/package.json +5 -1
- package/tui.tsx +60 -5
package/package.json
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rizal72/oc-models-pro",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "OpenCode v2 model picker with prices, context, intelligence, Go credit and the cost of a request",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/rizal72/opencode-plugins.git"
|
|
9
|
+
},
|
|
6
10
|
"license": "MIT",
|
|
7
11
|
"files": [
|
|
8
12
|
"*.ts",
|
package/tui.tsx
CHANGED
|
@@ -250,15 +250,27 @@ function modelRow(row: Row, mine: boolean, favourite: boolean, f: Fit): string {
|
|
|
250
250
|
|
|
251
251
|
const TRACE = join(tmpdir(), "oc-models-pro.log")
|
|
252
252
|
|
|
253
|
+
/** The tail is the part worth reading; the file is trimmed at startup, never during a session. */
|
|
254
|
+
const TRACE_LINES = 1000
|
|
255
|
+
|
|
253
256
|
function trace(message: string): void {
|
|
254
257
|
try {
|
|
255
258
|
appendFileSync(TRACE, new Date().toISOString() + " " + message + "\n")
|
|
256
259
|
} catch {}
|
|
257
260
|
}
|
|
258
261
|
|
|
262
|
+
/** Keeps the log to its last `TRACE_LINES` lines, so it neither grows without bound nor loses the recent past. */
|
|
263
|
+
function trimTrace(): void {
|
|
264
|
+
try {
|
|
265
|
+
const lines = readFileSync(TRACE, "utf8").split("\n")
|
|
266
|
+
if (lines.length > TRACE_LINES) writeFileSync(TRACE, lines.slice(-TRACE_LINES).join("\n"))
|
|
267
|
+
} catch {}
|
|
268
|
+
}
|
|
269
|
+
|
|
259
270
|
export default Plugin.define({
|
|
260
271
|
id: "oc-models-pro",
|
|
261
272
|
setup(context: any) {
|
|
273
|
+
trimTrace()
|
|
262
274
|
const ui = context.ui
|
|
263
275
|
const themeColour = (...paths: string[][]): any => {
|
|
264
276
|
for (const path of paths) {
|
|
@@ -417,6 +429,13 @@ export default Plugin.define({
|
|
|
417
429
|
}
|
|
418
430
|
|
|
419
431
|
const Panel = (props: { panel: any }) => {
|
|
432
|
+
// Who had the focus before the panel took it: the prompt itself. The renderer can hand it back,
|
|
433
|
+
// and nothing else in the API can — no host command focuses the prompt from a plugin.
|
|
434
|
+
const previousFocus = context.renderer?.currentFocusedRenderable
|
|
435
|
+
trace(
|
|
436
|
+
"panel: previous focus=" +
|
|
437
|
+
(previousFocus ? (previousFocus.constructor?.name ?? "?") : "none"),
|
|
438
|
+
)
|
|
420
439
|
const [selected, setSelected] = createSignal<string | undefined>(undefined)
|
|
421
440
|
/** The panel's width, in columns, and the layout it buys — reactive: the host resizes it. */
|
|
422
441
|
const width = createMemo(() => Math.floor(Number(props.panel.width) || 0), undefined, { equals: Object.is })
|
|
@@ -448,8 +467,31 @@ export default Plugin.define({
|
|
|
448
467
|
let field: any
|
|
449
468
|
/** Diagnostic: more than one mount per open means the panel is being rebuilt under us. */
|
|
450
469
|
let inputMounts = 0
|
|
451
|
-
/**
|
|
452
|
-
|
|
470
|
+
/**
|
|
471
|
+
* Who the host's renderer has focus on: the truth the blur/refocus guessing could only infer.
|
|
472
|
+
* Clicking a row or the field moves it without necessarily firing a blur on the input.
|
|
473
|
+
*/
|
|
474
|
+
const holdsField = (): boolean => Boolean(field) && context.renderer?.currentFocusedRenderable === field
|
|
475
|
+
/**
|
|
476
|
+
* Hands the focus back to whoever held it before the panel opened — the prompt. The host does not
|
|
477
|
+
* do it for a panel closed by a click, and there is no command that focuses the prompt: this is
|
|
478
|
+
* the renderer's own method, and it is what makes a click on a row behave like `esc`.
|
|
479
|
+
*/
|
|
480
|
+
const focusPrompt = (): void => {
|
|
481
|
+
try {
|
|
482
|
+
if (!previousFocus || previousFocus.isDestroyed || !context.renderer?.focusRenderable) {
|
|
483
|
+
trace("focus: no target")
|
|
484
|
+
return
|
|
485
|
+
}
|
|
486
|
+
context.renderer.focusRenderable(previousFocus)
|
|
487
|
+
trace(
|
|
488
|
+
"focus: restored, current=" +
|
|
489
|
+
(context.renderer.currentFocusedRenderable === previousFocus ? "yes" : "no"),
|
|
490
|
+
)
|
|
491
|
+
} catch (error) {
|
|
492
|
+
fail("focus", error)
|
|
493
|
+
}
|
|
494
|
+
}
|
|
453
495
|
const clear = (): void => {
|
|
454
496
|
setQuery("")
|
|
455
497
|
if (field && !field.isDestroyed) field.value = ""
|
|
@@ -600,7 +642,13 @@ export default Plugin.define({
|
|
|
600
642
|
selectable().find((entry) => prefKey(entry.providerID, entry.modelID) === wanted)
|
|
601
643
|
if (!row) return
|
|
602
644
|
trace("choose: " + prefKey(row.providerID, row.modelID))
|
|
645
|
+
// Esc closes with the search field focused and the host hands the prompt its focus back; a
|
|
646
|
+
// click leaves it on the row, so the field takes it again before the panel goes away.
|
|
647
|
+
if (field && !field.isDestroyed) field.focus?.()
|
|
603
648
|
props.panel.close()
|
|
649
|
+
// The host returns the prompt's focus on its own only most of the time, and never while its
|
|
650
|
+
// unmount is still running: the saved renderable takes it back once the panel is gone.
|
|
651
|
+
if (props.panel.presentation === "panel") setTimeout(focusPrompt, 40)
|
|
604
652
|
void (async () => {
|
|
605
653
|
if (!(await selectModel(row))) {
|
|
606
654
|
// The native never fails here — it writes the selection itself — so this is the one
|
|
@@ -692,6 +740,8 @@ export default Plugin.define({
|
|
|
692
740
|
description: "",
|
|
693
741
|
})),
|
|
694
742
|
})) as string | undefined
|
|
743
|
+
// A dialog takes the focus and does not hand it back either: the same command, once it closed.
|
|
744
|
+
setTimeout(focusPrompt, 40)
|
|
695
745
|
// Cancelling leaves the model without a variant, and the question will come back, as in
|
|
696
746
|
// the native picker.
|
|
697
747
|
if (chosen === undefined) return
|
|
@@ -762,7 +812,8 @@ export default Plugin.define({
|
|
|
762
812
|
fitted()
|
|
763
813
|
hints()
|
|
764
814
|
const element = field
|
|
765
|
-
if (
|
|
815
|
+
if (!element || element.isDestroyed || !props.panel.focused || holdsField()) return
|
|
816
|
+
trace("reflow: field takes the focus back")
|
|
766
817
|
element.focus()
|
|
767
818
|
})
|
|
768
819
|
|
|
@@ -824,6 +875,12 @@ export default Plugin.define({
|
|
|
824
875
|
borderColor={BORDER}
|
|
825
876
|
paddingLeft={1}
|
|
826
877
|
paddingRight={1}
|
|
878
|
+
onMouseDown={() => {
|
|
879
|
+
// A click inside the field moves the host's focus elsewhere; take it back a tick later.
|
|
880
|
+
setTimeout(() => {
|
|
881
|
+
if (field && !field.isDestroyed) field.focus()
|
|
882
|
+
}, 1)
|
|
883
|
+
}}
|
|
827
884
|
>
|
|
828
885
|
<input
|
|
829
886
|
ref={(element: any) => {
|
|
@@ -838,8 +895,6 @@ export default Plugin.define({
|
|
|
838
895
|
if (!element.isDestroyed) element.focus()
|
|
839
896
|
}, 1)
|
|
840
897
|
}}
|
|
841
|
-
onFocus={() => (focused = true)}
|
|
842
|
-
onBlur={() => (focused = false)}
|
|
843
898
|
placeholder="Search (ctrl+u clear)"
|
|
844
899
|
flexGrow={1}
|
|
845
900
|
placeholderColor={themeColour(["text", "faint"])}
|