@objctp/opencode-better-prompt 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/LICENSE +21 -0
- package/README.md +161 -0
- package/agents/prompt-correction.md +121 -0
- package/agents/prompt-enhancement.md +91 -0
- package/agents/prompt-summarisation.md +68 -0
- package/agents/prompt-translation.md +54 -0
- package/config/better-prompt.local.md.example +88 -0
- package/opencode.json +6 -0
- package/package.json +48 -0
- package/plugins/better-prompt/agents.ts +238 -0
- package/plugins/better-prompt/catalog.ts +271 -0
- package/plugins/better-prompt/config.ts +104 -0
- package/plugins/better-prompt/format.ts +26 -0
- package/plugins/better-prompt/pipeline.ts +190 -0
- package/plugins/better-prompt/state.ts +21 -0
- package/plugins/better-prompt/tui/format.ts +42 -0
- package/plugins/better-prompt/tui/routes.tsx +310 -0
- package/plugins/better-prompt/tui/select-view.tsx +58 -0
- package/plugins/better-prompt/tui/sidebar-panel.tsx +220 -0
- package/plugins/better-prompt/types.ts +137 -0
- package/plugins/better-prompt-tui.tsx +129 -0
- package/plugins/better-prompt.ts +298 -0
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import { CONFIG_DEFAULTS } from "./config";
|
|
2
|
+
import type { Config } from "./config";
|
|
3
|
+
import { resolveModel } from "./catalog";
|
|
4
|
+
import { invokeAgent, summariseContext } from "./agents";
|
|
5
|
+
import type { PipelineDeps, PipelineResult, StageNotifier, Usage } from "./types";
|
|
6
|
+
|
|
7
|
+
// A correction is consistent when every claimed fix actually appears in the
|
|
8
|
+
// corrected text. A model that lists a fix for text it then dropped has
|
|
9
|
+
// rewritten or extracted a subset rather than applying discrete fixes — the
|
|
10
|
+
// correction should be discarded and the original kept.
|
|
11
|
+
function correctionIsConsistent(
|
|
12
|
+
corrected: string,
|
|
13
|
+
mistakes: Array<{ correction?: string }>,
|
|
14
|
+
): boolean {
|
|
15
|
+
for (const m of mistakes) {
|
|
16
|
+
const fix = m.correction;
|
|
17
|
+
if (!fix) continue;
|
|
18
|
+
if (!corrected.toLowerCase().includes(fix.toLowerCase())) {
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
return true;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export async function runPipeline(
|
|
26
|
+
deps: PipelineDeps,
|
|
27
|
+
text: string,
|
|
28
|
+
sessionID: string,
|
|
29
|
+
config: Config,
|
|
30
|
+
notify: StageNotifier,
|
|
31
|
+
currentMessageID?: string,
|
|
32
|
+
): Promise<PipelineResult> {
|
|
33
|
+
const { toast, logWarn } = deps;
|
|
34
|
+
|
|
35
|
+
let working = text;
|
|
36
|
+
let corrected: string | null = null;
|
|
37
|
+
let detectedLanguage: string | null = null;
|
|
38
|
+
let mistakes: Array<{ type: string; original: string; correction: string }> = [];
|
|
39
|
+
const totalUsage: Usage = {
|
|
40
|
+
cost: 0,
|
|
41
|
+
inputTokens: 0,
|
|
42
|
+
outputTokens: 0,
|
|
43
|
+
cacheWriteTokens: 0,
|
|
44
|
+
cacheReadTokens: 0,
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
function addUsage(u: Usage) {
|
|
48
|
+
totalUsage.cost += u.cost;
|
|
49
|
+
totalUsage.inputTokens += u.inputTokens;
|
|
50
|
+
totalUsage.outputTokens += u.outputTokens;
|
|
51
|
+
totalUsage.cacheWriteTokens += u.cacheWriteTokens;
|
|
52
|
+
totalUsage.cacheReadTokens += u.cacheReadTokens;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const { correction, translation, enhancement } = config;
|
|
56
|
+
const anyStage = correction || translation || enhancement;
|
|
57
|
+
if (!anyStage) {
|
|
58
|
+
return {
|
|
59
|
+
result: text,
|
|
60
|
+
corrected: null,
|
|
61
|
+
detectedLanguage: null,
|
|
62
|
+
mistakes: [],
|
|
63
|
+
contextSummary: "",
|
|
64
|
+
usage: totalUsage,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const skipCorrection = enhancement && !translation && correction;
|
|
69
|
+
const correctionOnlyForLanguage = translation && !correction;
|
|
70
|
+
|
|
71
|
+
// :::: Correction //
|
|
72
|
+
if (!skipCorrection && (correction || correctionOnlyForLanguage)) {
|
|
73
|
+
notify("correction", "starting");
|
|
74
|
+
const model = await resolveModel(config.correction_model, CONFIG_DEFAULTS.correction_model);
|
|
75
|
+
const correctionResult = await invokeAgent(deps, "prompt-correction", working, model);
|
|
76
|
+
addUsage(correctionResult.usage);
|
|
77
|
+
const raw = correctionResult.text;
|
|
78
|
+
|
|
79
|
+
let correctionFailed = false;
|
|
80
|
+
let correctionError = "";
|
|
81
|
+
try {
|
|
82
|
+
const fenceMatch = raw.match(/```(?:json)?\s*\n?([\s\S]*?)\n?\s*```/);
|
|
83
|
+
const cleaned = fenceMatch ? fenceMatch[1].trim() : raw.trim();
|
|
84
|
+
const parsed = JSON.parse(cleaned);
|
|
85
|
+
if (parsed.corrected && typeof parsed.corrected === "string") {
|
|
86
|
+
if (correctionOnlyForLanguage) {
|
|
87
|
+
detectedLanguage = parsed.language || null;
|
|
88
|
+
} else {
|
|
89
|
+
const candidateMistakes = Array.isArray(parsed.mistakes) ? parsed.mistakes : [];
|
|
90
|
+
// Guard: every claimed fix must appear in the corrected text; otherwise
|
|
91
|
+
// the model rewrote/extracted a subset instead of applying discrete fixes
|
|
92
|
+
// — discard and keep the original prompt.
|
|
93
|
+
if (!correctionIsConsistent(parsed.corrected, candidateMistakes)) {
|
|
94
|
+
correctionFailed = true;
|
|
95
|
+
correctionError = "discarded: dropped a claimed fix";
|
|
96
|
+
void logWarn("correction discarded — output dropped a claimed fix; keeping original");
|
|
97
|
+
} else {
|
|
98
|
+
working = parsed.corrected;
|
|
99
|
+
corrected = parsed.corrected;
|
|
100
|
+
detectedLanguage = parsed.language || null;
|
|
101
|
+
mistakes = candidateMistakes;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
} else {
|
|
105
|
+
correctionFailed = true;
|
|
106
|
+
correctionError = "missing corrected field";
|
|
107
|
+
void logWarn(`correction agent returned JSON without "corrected" field`);
|
|
108
|
+
}
|
|
109
|
+
} catch {
|
|
110
|
+
correctionFailed = true;
|
|
111
|
+
correctionError = "non-JSON response";
|
|
112
|
+
void logWarn(`correction agent returned non-JSON`, raw?.substring(0, 200));
|
|
113
|
+
}
|
|
114
|
+
if (correctionFailed) {
|
|
115
|
+
notify("correction", "error", correctionError);
|
|
116
|
+
const discarded = correctionError.startsWith("discarded");
|
|
117
|
+
await toast(
|
|
118
|
+
discarded ? "Correction discarded — keeping original" : "Correction failed — using original prompt",
|
|
119
|
+
discarded ? "info" : "error",
|
|
120
|
+
3000,
|
|
121
|
+
);
|
|
122
|
+
} else {
|
|
123
|
+
notify("correction", "complete");
|
|
124
|
+
}
|
|
125
|
+
} else {
|
|
126
|
+
notify("correction", "skipped");
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// :::: Translation //
|
|
130
|
+
if (translation) {
|
|
131
|
+
if (detectedLanguage === "en") {
|
|
132
|
+
notify("translation", "skipped");
|
|
133
|
+
} else {
|
|
134
|
+
notify("translation", "starting");
|
|
135
|
+
const model = await resolveModel(config.translation_model, CONFIG_DEFAULTS.translation_model);
|
|
136
|
+
const translationResult = await invokeAgent(deps, "prompt-translation", working, model);
|
|
137
|
+
addUsage(translationResult.usage);
|
|
138
|
+
if (translationResult.text?.trim()) {
|
|
139
|
+
working = translationResult.text;
|
|
140
|
+
}
|
|
141
|
+
notify("translation", "complete");
|
|
142
|
+
}
|
|
143
|
+
} else {
|
|
144
|
+
notify("translation", "skipped");
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// :::: Context summarisation (only with enhancement) //
|
|
148
|
+
let contextSummary = "";
|
|
149
|
+
if (enhancement) {
|
|
150
|
+
notify("context", "starting");
|
|
151
|
+
const ctxResult = await summariseContext(deps, sessionID, currentMessageID ?? "", config);
|
|
152
|
+
contextSummary = ctxResult.summary;
|
|
153
|
+
addUsage(ctxResult.usage);
|
|
154
|
+
deps.sessionContexts.set(sessionID, {
|
|
155
|
+
summary: ctxResult.summary,
|
|
156
|
+
lastMessageID: ctxResult.lastMessageID,
|
|
157
|
+
messageCount: ctxResult.messageCount,
|
|
158
|
+
});
|
|
159
|
+
notify("context", "complete");
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// :::: Enhancement //
|
|
163
|
+
if (enhancement) {
|
|
164
|
+
notify("enhancement", "starting");
|
|
165
|
+
let enhanceInput = "";
|
|
166
|
+
if (contextSummary) {
|
|
167
|
+
enhanceInput += `Conversation context: ${contextSummary}\n\n`;
|
|
168
|
+
}
|
|
169
|
+
enhanceInput += working;
|
|
170
|
+
|
|
171
|
+
const model = await resolveModel(config.enhancement_model, CONFIG_DEFAULTS.enhancement_model);
|
|
172
|
+
const enhancementResult = await invokeAgent(deps, "prompt-enhancement", enhanceInput, model);
|
|
173
|
+
addUsage(enhancementResult.usage);
|
|
174
|
+
if (enhancementResult.text?.trim()) {
|
|
175
|
+
working = enhancementResult.text;
|
|
176
|
+
}
|
|
177
|
+
notify("enhancement", "complete");
|
|
178
|
+
} else {
|
|
179
|
+
notify("enhancement", "skipped");
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return {
|
|
183
|
+
result: working,
|
|
184
|
+
corrected,
|
|
185
|
+
detectedLanguage,
|
|
186
|
+
mistakes,
|
|
187
|
+
contextSummary,
|
|
188
|
+
usage: totalUsage,
|
|
189
|
+
};
|
|
190
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { appendFileSync, mkdirSync, unlinkSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { dirname } from "node:path";
|
|
3
|
+
import type { AuditEntry, PipelineState } from "./types";
|
|
4
|
+
|
|
5
|
+
export function writeAudit(auditPath: string, entry: AuditEntry): void {
|
|
6
|
+
mkdirSync(dirname(auditPath), { recursive: true });
|
|
7
|
+
appendFileSync(auditPath, `${JSON.stringify(entry)}\n`);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function writeState(statePath: string, state: PipelineState): void {
|
|
11
|
+
mkdirSync(dirname(statePath), { recursive: true });
|
|
12
|
+
writeFileSync(statePath, `${JSON.stringify(state)}\n`);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function clearState(statePath: string): void {
|
|
16
|
+
try {
|
|
17
|
+
unlinkSync(statePath);
|
|
18
|
+
} catch {
|
|
19
|
+
// best effort
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { findModelEntry, MODEL_DEFAULTS, TIER_ALIASES, TIER_CYCLE } from "../catalog";
|
|
2
|
+
import { formatContext, formatCost } from "../format";
|
|
3
|
+
import type { ModelEntry } from "../types";
|
|
4
|
+
|
|
5
|
+
type ModelTiers = { fast: ModelEntry[]; capable: ModelEntry[]; powerful: ModelEntry[] };
|
|
6
|
+
|
|
7
|
+
export function formatModelDisplay(value: string, tiers: ModelTiers | null, key: string): string {
|
|
8
|
+
const isDefault = value === MODEL_DEFAULTS[key];
|
|
9
|
+
if (isDefault) return `${value} (inherits session model)`;
|
|
10
|
+
|
|
11
|
+
// Legacy alias: haiku → fast, sonnet → capable, opus → powerful
|
|
12
|
+
const alias = TIER_ALIASES[value];
|
|
13
|
+
if (alias) {
|
|
14
|
+
if (!tiers) return `${value} ≡ ${alias}`;
|
|
15
|
+
const tierModels = tiers[alias as "fast" | "capable" | "powerful"];
|
|
16
|
+
const best = tierModels?.[0];
|
|
17
|
+
return `${value} ≡ ${alias} ${
|
|
18
|
+
best ? `(${best.id} ${formatCost(best.cost)} ctx:${formatContext(best.context)})` : ""
|
|
19
|
+
}`;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Tier name: fast/capable/powerful
|
|
23
|
+
if (TIER_CYCLE.includes(value)) {
|
|
24
|
+
if (!tiers) return value;
|
|
25
|
+
const tierModels = tiers[value as "fast" | "capable" | "powerful"];
|
|
26
|
+
const best = tierModels?.[0];
|
|
27
|
+
return `${value} ${
|
|
28
|
+
best ? `(${best.id} ${formatCost(best.cost)} ctx:${formatContext(best.context)})` : ""
|
|
29
|
+
}`;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Explicit provider/model
|
|
33
|
+
if (tiers) {
|
|
34
|
+
const entry = findModelEntry(value, tiers);
|
|
35
|
+
if (entry) {
|
|
36
|
+
return `${value} (${entry.tier} ${formatCost(entry.cost)} ctx:${formatContext(
|
|
37
|
+
entry.context,
|
|
38
|
+
)})`;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return value;
|
|
42
|
+
}
|
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
/** @jsxImportSource @opentui/solid */
|
|
2
|
+
|
|
3
|
+
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import type { TuiPluginApi } from "@opencode-ai/plugin/tui";
|
|
6
|
+
import { createSignal } from "solid-js";
|
|
7
|
+
import { CONFIG_PATH, MODEL_FIELDS, parseConfig, updateConfig } from "../config";
|
|
8
|
+
import type { Config } from "../config";
|
|
9
|
+
import { getModelTiers, resolveTier, TIER_ALIASES, TIER_CYCLE } from "../catalog";
|
|
10
|
+
import type { ModelEntry } from "../types";
|
|
11
|
+
import { formatModelDisplay } from "./format";
|
|
12
|
+
import { SelectView, type SelectOption } from "./select-view";
|
|
13
|
+
|
|
14
|
+
export interface RouteProps {
|
|
15
|
+
api: TuiPluginApi;
|
|
16
|
+
goBack: () => void;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function auditPath(api: TuiPluginApi): string {
|
|
20
|
+
return join(api.state.path.directory, ".opencode", "better-prompt", "audit.json");
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// :::: /better-prompt:toggle :::: ///////////////////////////
|
|
24
|
+
|
|
25
|
+
export function ToggleRoute(props: RouteProps) {
|
|
26
|
+
const TOGGLE_KEYS = [
|
|
27
|
+
"enabled",
|
|
28
|
+
"correction",
|
|
29
|
+
"translation",
|
|
30
|
+
"enhancement",
|
|
31
|
+
"audit",
|
|
32
|
+
"verbose",
|
|
33
|
+
] as const;
|
|
34
|
+
|
|
35
|
+
const buildStages = (): SelectOption[] => {
|
|
36
|
+
const config = parseConfig(CONFIG_PATH);
|
|
37
|
+
return TOGGLE_KEYS.map((stage) => ({
|
|
38
|
+
title: stage,
|
|
39
|
+
description: `Currently: ${
|
|
40
|
+
(config as unknown as Record<string, unknown>)[stage] ? "ON" : "OFF"
|
|
41
|
+
}`,
|
|
42
|
+
value: stage,
|
|
43
|
+
}));
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const [stages, setStages] = createSignal<SelectOption[]>(buildStages());
|
|
47
|
+
|
|
48
|
+
return (
|
|
49
|
+
<SelectView
|
|
50
|
+
title="Better Prompt: Toggle Stage"
|
|
51
|
+
options={() => stages()}
|
|
52
|
+
onSelect={(opt) => {
|
|
53
|
+
const current = parseConfig(CONFIG_PATH);
|
|
54
|
+
const newVal = !(current as unknown as Record<string, unknown>)[opt.value];
|
|
55
|
+
updateConfig(CONFIG_PATH, { [opt.value]: newVal });
|
|
56
|
+
props.api.ui.toast({
|
|
57
|
+
variant: "success",
|
|
58
|
+
message: `${opt.value} is now ${newVal ? "ON" : "OFF"}`,
|
|
59
|
+
});
|
|
60
|
+
setStages(buildStages());
|
|
61
|
+
}}
|
|
62
|
+
onBack={props.goBack}
|
|
63
|
+
/>
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// :::: /better-prompt:config :::: ///////////////////////////
|
|
68
|
+
|
|
69
|
+
export function ConfigRoute(props: RouteProps) {
|
|
70
|
+
const [configData, setConfigData] = createSignal(parseConfig(CONFIG_PATH));
|
|
71
|
+
const [tiersData, setTiersData] = createSignal<{
|
|
72
|
+
fast: ModelEntry[];
|
|
73
|
+
capable: ModelEntry[];
|
|
74
|
+
powerful: ModelEntry[];
|
|
75
|
+
} | null>(null);
|
|
76
|
+
const [cycleIndex, setCycleIndex] = createSignal<Record<string, number>>({});
|
|
77
|
+
|
|
78
|
+
// Load model tiers asynchronously
|
|
79
|
+
getModelTiers()
|
|
80
|
+
.then(setTiersData)
|
|
81
|
+
.catch(() => {
|
|
82
|
+
props.api.ui.toast({
|
|
83
|
+
variant: "error",
|
|
84
|
+
message: "Could not load model tiers",
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
const refreshConfig = () => {
|
|
89
|
+
setConfigData(parseConfig(CONFIG_PATH));
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
const options = () => {
|
|
93
|
+
const config = configData();
|
|
94
|
+
return Object.entries(config).map(([k, v]) => {
|
|
95
|
+
const isModel = (MODEL_FIELDS as readonly string[]).includes(k);
|
|
96
|
+
|
|
97
|
+
if (isModel) {
|
|
98
|
+
const display = formatModelDisplay(String(v), tiersData(), k);
|
|
99
|
+
return {
|
|
100
|
+
title: k,
|
|
101
|
+
description: display,
|
|
102
|
+
value: k,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const isBool = typeof v === "boolean";
|
|
107
|
+
return {
|
|
108
|
+
title: k,
|
|
109
|
+
description: `${v}${isBool ? " (select to toggle)" : ""}`,
|
|
110
|
+
value: k,
|
|
111
|
+
};
|
|
112
|
+
});
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
const handleSelect = (opt: SelectOption) => {
|
|
116
|
+
const key = opt.value as keyof Config;
|
|
117
|
+
const config = configData();
|
|
118
|
+
const currentVal = config[key];
|
|
119
|
+
|
|
120
|
+
if (typeof currentVal === "boolean") {
|
|
121
|
+
updateConfig(CONFIG_PATH, { [key]: !currentVal });
|
|
122
|
+
props.api.ui.toast({
|
|
123
|
+
variant: "success",
|
|
124
|
+
message: `${key}: ${currentVal} -> ${!currentVal}`,
|
|
125
|
+
});
|
|
126
|
+
refreshConfig();
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Model field: cycle tier on Enter
|
|
131
|
+
if ((MODEL_FIELDS as readonly string[]).includes(key)) {
|
|
132
|
+
const current = String(currentVal);
|
|
133
|
+
const resolvedTier = resolveTier(current, tiersData());
|
|
134
|
+
const currentTierName = resolvedTier || TIER_ALIASES[current] || current;
|
|
135
|
+
|
|
136
|
+
const currentIdx = TIER_CYCLE.indexOf(currentTierName);
|
|
137
|
+
const nextIdx = currentIdx >= 0 ? (currentIdx + 1) % TIER_CYCLE.length : 0;
|
|
138
|
+
const next = TIER_CYCLE[nextIdx];
|
|
139
|
+
|
|
140
|
+
updateConfig(CONFIG_PATH, { [key]: next });
|
|
141
|
+
setCycleIndex({ ...cycleIndex(), [key]: 0 });
|
|
142
|
+
props.api.ui.toast({ variant: "success", message: `${key}: ${next}` });
|
|
143
|
+
refreshConfig();
|
|
144
|
+
}
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
const handleAltSelect = (opt: SelectOption) => {
|
|
148
|
+
const key = opt.value as keyof Config;
|
|
149
|
+
if (!(MODEL_FIELDS as readonly string[]).includes(key)) return;
|
|
150
|
+
|
|
151
|
+
const tierData = tiersData();
|
|
152
|
+
if (!tierData) {
|
|
153
|
+
props.api.ui.toast({
|
|
154
|
+
variant: "error",
|
|
155
|
+
message: "Model data not loaded yet",
|
|
156
|
+
});
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const config = configData();
|
|
161
|
+
const currentVal = String(config[key]);
|
|
162
|
+
const resolvedTier = resolveTier(currentVal, tierData) || "fast";
|
|
163
|
+
const modelsInTier = tierData[resolvedTier as "fast" | "capable" | "powerful"];
|
|
164
|
+
if (!modelsInTier || modelsInTier.length === 0) {
|
|
165
|
+
props.api.ui.toast({
|
|
166
|
+
variant: "error",
|
|
167
|
+
message: `No models available for tier: ${resolvedTier}`,
|
|
168
|
+
});
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const currentIdx = (cycleIndex()[key] || 0) % modelsInTier.length;
|
|
173
|
+
const picked = modelsInTier[currentIdx];
|
|
174
|
+
updateConfig(CONFIG_PATH, { [key]: picked.id });
|
|
175
|
+
setCycleIndex({ ...cycleIndex(), [key]: currentIdx + 1 });
|
|
176
|
+
props.api.ui.toast({
|
|
177
|
+
variant: "success",
|
|
178
|
+
message: `${key}: ${picked.id} (${resolvedTier})`,
|
|
179
|
+
});
|
|
180
|
+
refreshConfig();
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
return (
|
|
184
|
+
<SelectView
|
|
185
|
+
title="Better Prompt Configuration"
|
|
186
|
+
options={options}
|
|
187
|
+
onSelect={handleSelect}
|
|
188
|
+
onAltSelect={handleAltSelect}
|
|
189
|
+
onBack={props.goBack}
|
|
190
|
+
/>
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// :::: /better-prompt:audit :::: ////////////////////////////
|
|
195
|
+
|
|
196
|
+
export function AuditRoute(props: RouteProps) {
|
|
197
|
+
const path = auditPath(props.api);
|
|
198
|
+
|
|
199
|
+
if (!existsSync(path)) {
|
|
200
|
+
return (
|
|
201
|
+
<box border paddingTop={0} paddingBottom={0} paddingLeft={1} paddingRight={1}>
|
|
202
|
+
<text>No audit data available.</text>
|
|
203
|
+
</box>
|
|
204
|
+
);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const allLines = readFileSync(path, "utf8")
|
|
208
|
+
.trim()
|
|
209
|
+
.split("\n")
|
|
210
|
+
.filter((l: string) => l.trim());
|
|
211
|
+
|
|
212
|
+
if (allLines.length === 0) {
|
|
213
|
+
return (
|
|
214
|
+
<box border paddingTop={0} paddingBottom={0} paddingLeft={1} paddingRight={1}>
|
|
215
|
+
<text>Audit trail is empty.</text>
|
|
216
|
+
</box>
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const recent = allLines.slice(-10);
|
|
221
|
+
const auditOptions: SelectOption[] = [];
|
|
222
|
+
|
|
223
|
+
for (let i = 0; i < recent.length; i++) {
|
|
224
|
+
try {
|
|
225
|
+
const entry = JSON.parse(recent[i]);
|
|
226
|
+
const num = allLines.length - recent.length + i + 1;
|
|
227
|
+
const mistakes = entry.mistakes?.length ?? 0;
|
|
228
|
+
const parts: string[] = [entry.prompt.substring(0, 60)];
|
|
229
|
+
if (entry.language) parts.push(`lang: ${entry.language}`);
|
|
230
|
+
if (mistakes > 0) {
|
|
231
|
+
parts.push(`${mistakes} mistake${mistakes > 1 ? "s" : ""}`);
|
|
232
|
+
}
|
|
233
|
+
auditOptions.push({
|
|
234
|
+
title: `Entry #${num}`,
|
|
235
|
+
description: parts.join(" | "),
|
|
236
|
+
value: String(num),
|
|
237
|
+
});
|
|
238
|
+
} catch {
|
|
239
|
+
// skip malformed entries
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
auditOptions.push({
|
|
244
|
+
title: "Clear audit trail",
|
|
245
|
+
description: `${allLines.length} entries total`,
|
|
246
|
+
value: "clear",
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
return (
|
|
250
|
+
<SelectView
|
|
251
|
+
title="Audit Trail"
|
|
252
|
+
options={() => auditOptions}
|
|
253
|
+
onSelect={(opt) => {
|
|
254
|
+
if (opt.value === "clear") {
|
|
255
|
+
writeFileSync(path, "");
|
|
256
|
+
props.api.ui.toast({
|
|
257
|
+
variant: "success",
|
|
258
|
+
message: "Audit trail cleared.",
|
|
259
|
+
});
|
|
260
|
+
props.goBack();
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
const num = parseInt(opt.value, 10);
|
|
264
|
+
const line = allLines[num - 1];
|
|
265
|
+
try {
|
|
266
|
+
const entry = JSON.parse(line);
|
|
267
|
+
const details: string[] = [`#${num} ${entry.date}`, `Original: "${entry.prompt}"`];
|
|
268
|
+
if (entry.language) details.push(`Language: ${entry.language}`);
|
|
269
|
+
if (entry.corrected) {
|
|
270
|
+
details.push(`Corrected: "${entry.corrected}"`);
|
|
271
|
+
}
|
|
272
|
+
if (entry.enhanced) details.push(`Enhanced: "${entry.enhanced}"`);
|
|
273
|
+
if (entry.mistakes?.length > 0) {
|
|
274
|
+
details.push(
|
|
275
|
+
"Mistakes: " +
|
|
276
|
+
entry.mistakes
|
|
277
|
+
.map(
|
|
278
|
+
(m: { type: string; original: string; correction: string }) =>
|
|
279
|
+
`[${m.type}] "${m.original}" -> "${m.correction}"`,
|
|
280
|
+
)
|
|
281
|
+
.join("; "),
|
|
282
|
+
);
|
|
283
|
+
}
|
|
284
|
+
if (entry.models) {
|
|
285
|
+
const used: string[] = [];
|
|
286
|
+
if (entry.models.correction) {
|
|
287
|
+
used.push(`correction=${entry.models.correction}`);
|
|
288
|
+
}
|
|
289
|
+
if (entry.models.translation) {
|
|
290
|
+
used.push(`translation=${entry.models.translation}`);
|
|
291
|
+
}
|
|
292
|
+
if (entry.models.enhancement) {
|
|
293
|
+
used.push(`enhancement=${entry.models.enhancement}`);
|
|
294
|
+
}
|
|
295
|
+
if (used.length) details.push(`Models: ${used.join(", ")}`);
|
|
296
|
+
}
|
|
297
|
+
props.api.ui.toast({
|
|
298
|
+
variant: "info",
|
|
299
|
+
message: details.join("\n"),
|
|
300
|
+
duration: 8000,
|
|
301
|
+
});
|
|
302
|
+
} catch {
|
|
303
|
+
// skip
|
|
304
|
+
}
|
|
305
|
+
props.goBack();
|
|
306
|
+
}}
|
|
307
|
+
onBack={props.goBack}
|
|
308
|
+
/>
|
|
309
|
+
);
|
|
310
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/** @jsxImportSource @opentui/solid */
|
|
2
|
+
|
|
3
|
+
import { useKeyboard } from "@opentui/solid";
|
|
4
|
+
import { createSignal } from "solid-js";
|
|
5
|
+
|
|
6
|
+
export interface SelectOption {
|
|
7
|
+
title: string;
|
|
8
|
+
description: string;
|
|
9
|
+
value: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function SelectView(props: {
|
|
13
|
+
title: string;
|
|
14
|
+
options: () => SelectOption[];
|
|
15
|
+
onSelect: (opt: SelectOption) => void;
|
|
16
|
+
onAltSelect?: (opt: SelectOption) => void;
|
|
17
|
+
onBack: () => void;
|
|
18
|
+
}) {
|
|
19
|
+
const [idx, setIdx] = createSignal(0);
|
|
20
|
+
|
|
21
|
+
useKeyboard((key: { name: string }) => {
|
|
22
|
+
const opts = props.options();
|
|
23
|
+
const max = opts.length - 1;
|
|
24
|
+
if (key.name === "up" || key.name === "k") {
|
|
25
|
+
setIdx((i: number) => Math.max(0, i - 1));
|
|
26
|
+
}
|
|
27
|
+
if (key.name === "down" || key.name === "j") {
|
|
28
|
+
setIdx((i: number) => Math.min(max, i + 1));
|
|
29
|
+
}
|
|
30
|
+
if (key.name === "return") {
|
|
31
|
+
const opt = opts[idx()];
|
|
32
|
+
if (opt) props.onSelect(opt);
|
|
33
|
+
}
|
|
34
|
+
if (key.name === "space") {
|
|
35
|
+
if (props.onAltSelect) {
|
|
36
|
+
const opt = opts[idx()];
|
|
37
|
+
if (opt) props.onAltSelect(opt);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
if (key.name === "escape") {
|
|
41
|
+
props.onBack();
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
return (
|
|
46
|
+
<box border paddingTop={0} paddingBottom={0} paddingLeft={1} paddingRight={1}>
|
|
47
|
+
<text>
|
|
48
|
+
<strong>{props.title}</strong>
|
|
49
|
+
</text>
|
|
50
|
+
{props.options().map((opt: SelectOption, i: number) => (
|
|
51
|
+
<text fg={idx() === i ? "#ffffff" : "#888888"}>
|
|
52
|
+
{`${idx() === i ? "> " : " "}${opt.title} ${opt.description}`}
|
|
53
|
+
</text>
|
|
54
|
+
))}
|
|
55
|
+
<text fg="#555555">Up/Dn Navigate Enter Cycle Tier Space Pick Model Esc Back</text>
|
|
56
|
+
</box>
|
|
57
|
+
);
|
|
58
|
+
}
|