@pi-unipi/unipi 0.1.11 → 0.1.12
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 +1 -1
- package/packages/autocomplete/src/constants.ts +8 -11
- package/packages/core/index.ts +1 -0
- package/packages/subagents/src/__tests__/badge-generation.test.ts +244 -0
- package/packages/subagents/src/index.ts +28 -7
- package/packages/utility/src/commands.ts +40 -3
- package/packages/utility/src/index.ts +16 -9
- package/packages/utility/src/tui/badge-settings-tui.ts +388 -0
- package/packages/utility/src/tui/badge-settings.ts +5 -0
- package/packages/workflow/index.ts +1 -1
package/package.json
CHANGED
|
@@ -93,16 +93,17 @@ export const COMMAND_REGISTRY: Record<string, string> = {
|
|
|
93
93
|
"unipi:mcp-settings": "mcp",
|
|
94
94
|
"unipi:mcp-reload": "mcp",
|
|
95
95
|
|
|
96
|
-
// utility (
|
|
96
|
+
// utility (10 commands)
|
|
97
97
|
"unipi:continue": "utility",
|
|
98
98
|
"unipi:reload": "utility",
|
|
99
99
|
"unipi:status": "utility",
|
|
100
100
|
"unipi:cleanup": "utility",
|
|
101
101
|
"unipi:env": "utility",
|
|
102
102
|
"unipi:doctor": "utility",
|
|
103
|
-
"unipi:name
|
|
103
|
+
"unipi:badge-name": "utility",
|
|
104
104
|
"unipi:badge-gen": "utility",
|
|
105
105
|
"unipi:badge-toggle": "utility",
|
|
106
|
+
"unipi:badge-settings": "utility",
|
|
106
107
|
|
|
107
108
|
// ask-user (1 command)
|
|
108
109
|
"unipi:ask-user-settings": "ask-user",
|
|
@@ -136,11 +137,9 @@ export const COMMAND_REGISTRY: Record<string, string> = {
|
|
|
136
137
|
"unipi:notify-set-tg": "notify",
|
|
137
138
|
"unipi:notify-test": "notify",
|
|
138
139
|
|
|
139
|
-
// kanboard (
|
|
140
|
+
// kanboard (3 commands)
|
|
140
141
|
"unipi:kanboard": "kanboard",
|
|
141
142
|
"unipi:kanboard-doctor": "kanboard",
|
|
142
|
-
"unipi:kanboard-settings": "kanboard",
|
|
143
|
-
"unipi:name-gen": "kanboard",
|
|
144
143
|
};
|
|
145
144
|
|
|
146
145
|
// ─── Description Map ─────────────────────────────────────────────────
|
|
@@ -190,11 +189,12 @@ export const COMMAND_DESCRIPTIONS: Record<string, string> = {
|
|
|
190
189
|
"unipi:cleanup": "Clean up old sessions and cache",
|
|
191
190
|
"unipi:env": "Show environment info",
|
|
192
191
|
"unipi:doctor": "Run diagnostics",
|
|
193
|
-
"unipi:name
|
|
192
|
+
"unipi:badge-name": "Toggle session name badge overlay",
|
|
194
193
|
"unipi:badge-gen": "Generate session name via background agent",
|
|
195
194
|
"unipi:badge-toggle": "Configure badge settings (autoGen, badgeEnabled, agentTool)",
|
|
196
|
-
"unipi:
|
|
197
|
-
"unipi:kanboard
|
|
195
|
+
"unipi:badge-settings": "Configure badge settings via TUI overlay",
|
|
196
|
+
"unipi:kanboard": "Start the kanboard visualization server",
|
|
197
|
+
"unipi:kanboard-doctor": "Diagnose and fix kanboard parser issues",
|
|
198
198
|
|
|
199
199
|
"unipi:ask-user-settings": "Configure ask-user settings",
|
|
200
200
|
|
|
@@ -219,9 +219,6 @@ export const COMMAND_DESCRIPTIONS: Record<string, string> = {
|
|
|
219
219
|
"unipi:notify-set-tg": "Set up Telegram bot notifications",
|
|
220
220
|
"unipi:notify-test": "Test all enabled notification platforms",
|
|
221
221
|
|
|
222
|
-
"unipi:kanboard": "Start the kanboard visualization server",
|
|
223
|
-
"unipi:kanboard-doctor": "Diagnose and fix kanboard parser issues",
|
|
224
|
-
|
|
225
222
|
"unipi:milestone-onboard": "Create MILESTONES.md from existing workflow docs",
|
|
226
223
|
"unipi:milestone-update": "Sync MILESTONES.md with completed work",
|
|
227
224
|
};
|
package/packages/core/index.ts
CHANGED
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Test: Badge Generation Flow
|
|
3
|
+
*
|
|
4
|
+
* Tests the full badge name generation flow to identify and verify
|
|
5
|
+
* fixes for "Generating session name..." getting stuck.
|
|
6
|
+
*
|
|
7
|
+
* BUG 1 — Tool mismatch:
|
|
8
|
+
* Background agent was told to "Call the set_session_name tool" but the tool
|
|
9
|
+
* doesn't exist in the agent's session (only builtin tools available).
|
|
10
|
+
* FIX: Changed prompt to output title directly, parse in onComplete callback.
|
|
11
|
+
*
|
|
12
|
+
* BUG 2 — Wrong event bus:
|
|
13
|
+
* Cross-module events emitted via pi.events.emit() but listeners used pi.on()
|
|
14
|
+
* (extension lifecycle events) — completely different event bus.
|
|
15
|
+
* FIX: Changed all cross-module listeners to pi.events.on().
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { describe, it } from "node:test";
|
|
19
|
+
import assert from "node:assert/strict";
|
|
20
|
+
import { readFileSync, existsSync } from "node:fs";
|
|
21
|
+
import { join } from "node:path";
|
|
22
|
+
|
|
23
|
+
const ROOT = join(import.meta.dirname, "../../../..");
|
|
24
|
+
|
|
25
|
+
// ─── Helpers ────────────────────────────────────────────────────────
|
|
26
|
+
|
|
27
|
+
function readSource(relativePath: string): string {
|
|
28
|
+
const fullPath = join(ROOT, relativePath);
|
|
29
|
+
if (!existsSync(fullPath)) throw new Error(`File not found: ${fullPath}`);
|
|
30
|
+
return readFileSync(fullPath, "utf-8");
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// ─── Test: Tool availability in spawned agent ──────────────────────
|
|
34
|
+
|
|
35
|
+
describe("Badge generation — tool availability", () => {
|
|
36
|
+
it("agent-runner uses only builtin tools, NOT extension-registered tools", () => {
|
|
37
|
+
const src = readSource("packages/subagents/src/agent-runner.ts");
|
|
38
|
+
|
|
39
|
+
const builtinMatch = src.match(
|
|
40
|
+
/const BUILTIN_TOOL_NAMES\s*=\s*(\[.*?\])/s,
|
|
41
|
+
);
|
|
42
|
+
assert.ok(builtinMatch, "BUILTIN_TOOL_NAMES should be defined");
|
|
43
|
+
|
|
44
|
+
const builtinTools: string[] = eval(builtinMatch[1]);
|
|
45
|
+
assert.deepStrictEqual(builtinTools, [
|
|
46
|
+
"read", "bash", "edit", "write", "grep", "find", "ls",
|
|
47
|
+
]);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("set_session_name is NOT in the agent's tool list", () => {
|
|
51
|
+
const src = readSource("packages/subagents/src/agent-runner.ts");
|
|
52
|
+
const builtinMatch = src.match(
|
|
53
|
+
/const BUILTIN_TOOL_NAMES\s*=\s*(\[.*?\])/s,
|
|
54
|
+
);
|
|
55
|
+
const tools: string[] = eval(builtinMatch![1]);
|
|
56
|
+
assert.ok(!tools.includes("set_session_name"));
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
// ─── Test: Prompt no longer references non-existent tool ───────────
|
|
61
|
+
|
|
62
|
+
describe("Badge generation — prompt fix", () => {
|
|
63
|
+
it("prompt asks agent to OUTPUT the title directly (not call a tool)", () => {
|
|
64
|
+
const src = readSource("packages/subagents/src/index.ts");
|
|
65
|
+
|
|
66
|
+
assert.ok(
|
|
67
|
+
src.includes("Reply with ONLY the title"),
|
|
68
|
+
"Prompt should ask agent to reply with only the title",
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
assert.ok(
|
|
72
|
+
!src.includes("Call the set_session_name tool"),
|
|
73
|
+
"Prompt should NOT tell agent to call set_session_name",
|
|
74
|
+
);
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
// ─── Test: onComplete extracts name from result ────────────────────
|
|
79
|
+
|
|
80
|
+
describe("Badge generation — onComplete callback", () => {
|
|
81
|
+
it("onComplete extracts name from agent result and calls pi.setSessionName", () => {
|
|
82
|
+
const src = readSource("packages/subagents/src/index.ts");
|
|
83
|
+
|
|
84
|
+
assert.ok(
|
|
85
|
+
src.includes('record.description === "Generate session name"'),
|
|
86
|
+
"Should detect badge generation agents by description",
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
assert.ok(
|
|
90
|
+
src.includes("pi.setSessionName(name)"),
|
|
91
|
+
"Should call pi.setSessionName with extracted name",
|
|
92
|
+
);
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
// ─── Test: Cross-module event bus — the critical fix ───────────────
|
|
97
|
+
|
|
98
|
+
describe("Badge generation — event bus (CRITICAL FIX)", () => {
|
|
99
|
+
it("emitEvent uses pi.events.emit (not pi.on)", () => {
|
|
100
|
+
const src = readSource("packages/core/utils.ts");
|
|
101
|
+
|
|
102
|
+
assert.ok(
|
|
103
|
+
src.includes("pi.events.emit(eventName, payload)"),
|
|
104
|
+
"emitEvent should use pi.events.emit()",
|
|
105
|
+
);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it("subagents listens via pi.events.on (NOT pi.on)", () => {
|
|
109
|
+
const src = readSource("packages/subagents/src/index.ts");
|
|
110
|
+
|
|
111
|
+
// Must use pi.events.on for cross-module events
|
|
112
|
+
assert.ok(
|
|
113
|
+
src.includes("pi.events.on(UNIPI_EVENTS.BADGE_GENERATE_REQUEST"),
|
|
114
|
+
"Subagents should listen via pi.events.on",
|
|
115
|
+
);
|
|
116
|
+
|
|
117
|
+
// Should NOT use pi.on for custom events
|
|
118
|
+
const piOnMatch = src.match(/pi\.on\(UNIPI_EVENTS\.BADGE_GENERATE_REQUEST/g);
|
|
119
|
+
assert.ok(!piOnMatch, "Should NOT use pi.on() for cross-module events");
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it("utility BADGE_GENERATE_REQUEST listener is removed (input handler already shows overlay)", () => {
|
|
123
|
+
const src = readSource("packages/utility/src/index.ts");
|
|
124
|
+
|
|
125
|
+
// Should NOT have a separate BADGE_GENERATE_REQUEST listener
|
|
126
|
+
// The input handler already shows the overlay and emits the event
|
|
127
|
+
assert.ok(
|
|
128
|
+
!src.includes("pi.events.on(UNIPI_EVENTS.BADGE_GENERATE_REQUEST"),
|
|
129
|
+
"Utility should NOT have a separate BADGE_GENERATE_REQUEST listener",
|
|
130
|
+
);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it("workflow listens for MODULE_READY via pi.events.on (NOT pi.on)", () => {
|
|
134
|
+
const src = readSource("packages/workflow/index.ts");
|
|
135
|
+
|
|
136
|
+
assert.ok(
|
|
137
|
+
src.includes("pi.events.on(UNIPI_EVENTS.MODULE_READY"),
|
|
138
|
+
"Workflow should listen via pi.events.on",
|
|
139
|
+
);
|
|
140
|
+
|
|
141
|
+
const piOnMatch = src.match(/pi\.on\(UNIPI_EVENTS\.MODULE_READY/g);
|
|
142
|
+
assert.ok(!piOnMatch, "Should NOT use pi.on() for cross-module events");
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it("pi.on() is ONLY used for known lifecycle events", () => {
|
|
146
|
+
const subagentsSrc = readSource("packages/subagents/src/index.ts");
|
|
147
|
+
const utilitySrc = readSource("packages/utility/src/index.ts");
|
|
148
|
+
|
|
149
|
+
// These are valid lifecycle events that should use pi.on()
|
|
150
|
+
const validLifecycleEvents = [
|
|
151
|
+
"session_start", "session_shutdown", "input",
|
|
152
|
+
"tool_call", "tool_execution_start",
|
|
153
|
+
];
|
|
154
|
+
|
|
155
|
+
// Check that pi.on() is only used with lifecycle events
|
|
156
|
+
const piOnPattern = /pi\.on\("([^"]+)"/g;
|
|
157
|
+
let match;
|
|
158
|
+
while ((match = piOnPattern.exec(subagentsSrc)) !== null) {
|
|
159
|
+
assert.ok(
|
|
160
|
+
validLifecycleEvents.includes(match[1]),
|
|
161
|
+
`subagents: pi.on("${match[1]}") should be a lifecycle event, use pi.events.on() for custom events`,
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
while ((match = piOnPattern.exec(utilitySrc)) !== null) {
|
|
165
|
+
assert.ok(
|
|
166
|
+
validLifecycleEvents.includes(match[1]),
|
|
167
|
+
`utility: pi.on("${match[1]}") should be a lifecycle event`,
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
});
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
// ─── Test: Event flow ──────────────────────────────────────────────
|
|
174
|
+
|
|
175
|
+
describe("Badge generation — event flow", () => {
|
|
176
|
+
it("utility emits BADGE_GENERATE_REQUEST on first input", () => {
|
|
177
|
+
const src = readSource("packages/utility/src/index.ts");
|
|
178
|
+
|
|
179
|
+
assert.ok(src.includes("BADGE_GENERATE_REQUEST"));
|
|
180
|
+
assert.ok(src.includes('source: "input-hook"'));
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it("BADGE_GENERATE_REQUEST event is defined in core", () => {
|
|
184
|
+
const src = readSource("packages/core/events.ts");
|
|
185
|
+
assert.ok(src.includes("BADGE_GENERATE_REQUEST"));
|
|
186
|
+
});
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
// ─── Test: Model resolution ────────────────────────────────────────
|
|
190
|
+
|
|
191
|
+
describe("Badge generation — model resolution", () => {
|
|
192
|
+
it("reads generationModel from badge.json instead of hardcoding", () => {
|
|
193
|
+
const src = readSource("packages/subagents/src/index.ts");
|
|
194
|
+
|
|
195
|
+
assert.ok(!src.includes('"openai/gpt-oss-20b"'));
|
|
196
|
+
assert.ok(src.includes(".unipi/config/badge.json"));
|
|
197
|
+
assert.ok(src.includes("parsed.generationModel"));
|
|
198
|
+
});
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
// ─── Summary ───────────────────────────────────────────────────────
|
|
202
|
+
|
|
203
|
+
describe("Badge generation — ROOT CAUSE SUMMARY", () => {
|
|
204
|
+
it("BUG 1 FIXED: prompt no longer references non-existent tool", () => {
|
|
205
|
+
const src = readSource("packages/subagents/src/index.ts");
|
|
206
|
+
|
|
207
|
+
assert.ok(!src.includes("Call the set_session_name tool"),
|
|
208
|
+
"FIXED: prompt no longer tells agent to call set_session_name");
|
|
209
|
+
assert.ok(src.includes("Reply with ONLY the title"),
|
|
210
|
+
"FIXED: prompt asks agent to reply with only the title");
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
it("BUG 1 FIXED: onComplete extracts name and sets it directly", () => {
|
|
214
|
+
const src = readSource("packages/subagents/src/index.ts");
|
|
215
|
+
|
|
216
|
+
assert.ok(src.includes('record.description === "Generate session name"'),
|
|
217
|
+
"FIXED: onComplete detects badge generation agents");
|
|
218
|
+
assert.ok(src.includes("pi.setSessionName(name)"),
|
|
219
|
+
"FIXED: onComplete calls pi.setSessionName directly");
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
it("BUG 2 FIXED: cross-module events use pi.events.on, not pi.on", () => {
|
|
223
|
+
const subagentsSrc = readSource("packages/subagents/src/index.ts");
|
|
224
|
+
const utilitySrc = readSource("packages/utility/src/index.ts");
|
|
225
|
+
const workflowSrc = readSource("packages/workflow/index.ts");
|
|
226
|
+
|
|
227
|
+
// Subagents: correct event bus
|
|
228
|
+
assert.ok(
|
|
229
|
+
subagentsSrc.includes("pi.events.on(UNIPI_EVENTS.BADGE_GENERATE_REQUEST"),
|
|
230
|
+
"subagents: must use pi.events.on for BADGE_GENERATE_REQUEST",
|
|
231
|
+
);
|
|
232
|
+
|
|
233
|
+
// Utility: no duplicate listener (input handler already handles it)
|
|
234
|
+
assert.ok(
|
|
235
|
+
!utilitySrc.includes("pi.events.on(UNIPI_EVENTS.BADGE_GENERATE_REQUEST"),
|
|
236
|
+
"utility: no duplicate BADGE_GENERATE_REQUEST listener", );
|
|
237
|
+
|
|
238
|
+
// Workflow: correct event bus
|
|
239
|
+
assert.ok(
|
|
240
|
+
workflowSrc.includes("pi.events.on(UNIPI_EVENTS.MODULE_READY"),
|
|
241
|
+
"workflow: must use pi.events.on for MODULE_READY",
|
|
242
|
+
);
|
|
243
|
+
});
|
|
244
|
+
});
|
|
@@ -179,6 +179,16 @@ export default function (pi: ExtensionAPI) {
|
|
|
179
179
|
);
|
|
180
180
|
}
|
|
181
181
|
|
|
182
|
+
// Badge generation: extract name from agent result and set directly
|
|
183
|
+
if (record.description === "Generate session name" && record.result && record.status === "completed") {
|
|
184
|
+
const name = record.result.split("\n")[0]?.trim().slice(0, 50) ?? "";
|
|
185
|
+
if (name && !name.startsWith("Error") && !name.includes("error")) {
|
|
186
|
+
try {
|
|
187
|
+
pi.setSessionName(name);
|
|
188
|
+
} catch { /* best effort */ }
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
182
192
|
pi.events.emit("subagents:completed", {
|
|
183
193
|
id: record.id,
|
|
184
194
|
type: record.type,
|
|
@@ -339,20 +349,31 @@ export default function (pi: ExtensionAPI) {
|
|
|
339
349
|
});
|
|
340
350
|
|
|
341
351
|
// Listen for badge generation requests — spawn background agent
|
|
342
|
-
pi.on(UNIPI_EVENTS.BADGE_GENERATE_REQUEST
|
|
352
|
+
pi.events.on(UNIPI_EVENTS.BADGE_GENERATE_REQUEST, async (event: any) => {
|
|
343
353
|
if (!sessionCtx) return;
|
|
344
354
|
|
|
345
355
|
const summary = event?.conversationSummary ?? "";
|
|
346
356
|
const prompt = summary
|
|
347
|
-
? `Generate a concise session title (MAX 5 WORDS) for this conversation:\n\n"${summary}"\n\
|
|
348
|
-
: `Generate a concise session title (MAX 5 WORDS) for the current session.
|
|
349
|
-
|
|
350
|
-
// Try with
|
|
351
|
-
|
|
357
|
+
? `Generate a concise session title (MAX 5 WORDS) for this conversation:\n\n"${summary}"\n\nReply with ONLY the title. No quotes, no explanation, no punctuation.`
|
|
358
|
+
: `Generate a concise session title (MAX 5 WORDS) for the current session. Reply with ONLY the title. No quotes, no explanation, no punctuation.`;
|
|
359
|
+
|
|
360
|
+
// Try with configured model, fallback to inherit
|
|
361
|
+
let modelInput: string | undefined = undefined;
|
|
362
|
+
try {
|
|
363
|
+
const fs = await import("node:fs");
|
|
364
|
+
const path = await import("node:path");
|
|
365
|
+
const configPath = path.resolve(process.cwd(), ".unipi/config/badge.json");
|
|
366
|
+
if (fs.existsSync(configPath)) {
|
|
367
|
+
const parsed = JSON.parse(fs.readFileSync(configPath, "utf-8"));
|
|
368
|
+
if (typeof parsed.generationModel === "string" && parsed.generationModel !== "inherit") {
|
|
369
|
+
modelInput = parsed.generationModel;
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
} catch { /* ignore — inherit parent model */ }
|
|
352
373
|
let resolvedModel: any = undefined;
|
|
353
374
|
|
|
354
375
|
// Check if model is available
|
|
355
|
-
if (sessionCtx.modelRegistry) {
|
|
376
|
+
if (modelInput && sessionCtx.modelRegistry) {
|
|
356
377
|
const { resolveModel } = await import("./model-resolver.js");
|
|
357
378
|
const result = resolveModel(modelInput, sessionCtx.modelRegistry);
|
|
358
379
|
if (typeof result !== "string") {
|
|
@@ -22,6 +22,7 @@ import { runDiagnostics, formatDiagnosticsReport } from "./diagnostics/engine.js
|
|
|
22
22
|
import { getEnvironmentInfo, formatEnvironmentInfo } from "./tools/env.js";
|
|
23
23
|
import type { NameBadgeState } from "./tui/name-badge-state.js";
|
|
24
24
|
import { readBadgeSettings, updateBadgeSetting, formatBadgeSettings } from "./tui/badge-settings.js";
|
|
25
|
+
import { BadgeSettingsTui } from "./tui/badge-settings-tui.js";
|
|
25
26
|
|
|
26
27
|
/** Send a markdown response via pi.sendMessage */
|
|
27
28
|
function sendResponse(pi: ExtensionAPI, markdown: string): void {
|
|
@@ -36,14 +37,14 @@ function sendResponse(pi: ExtensionAPI, markdown: string): void {
|
|
|
36
37
|
}
|
|
37
38
|
|
|
38
39
|
/**
|
|
39
|
-
* Register name badge commands: /unipi:name
|
|
40
|
+
* Register name badge commands: /unipi:badge-name, /unipi:badge-gen.
|
|
40
41
|
*/
|
|
41
42
|
export function registerNameBadgeCommands(
|
|
42
43
|
pi: ExtensionAPI,
|
|
43
44
|
state: NameBadgeState,
|
|
44
45
|
): void {
|
|
45
|
-
// ─── /unipi:name
|
|
46
|
-
pi.registerCommand(`${UNIPI_PREFIX}${UTILITY_COMMANDS.
|
|
46
|
+
// ─── /unipi:badge-name — toggle badge overlay ───────────────────────────
|
|
47
|
+
pi.registerCommand(`${UNIPI_PREFIX}${UTILITY_COMMANDS.BADGE_NAME}`, {
|
|
47
48
|
description: "Toggle session name badge overlay",
|
|
48
49
|
handler: async (_args: string, ctx: ExtensionContext) => {
|
|
49
50
|
if (!ctx.hasUI) {
|
|
@@ -95,6 +96,42 @@ export function registerNameBadgeCommands(
|
|
|
95
96
|
sendResponse(pi, formatBadgeSettings(settings));
|
|
96
97
|
},
|
|
97
98
|
});
|
|
99
|
+
|
|
100
|
+
// ─── /unipi:badge-settings — TUI settings overlay ──────────────────────
|
|
101
|
+
pi.registerCommand(`${UNIPI_PREFIX}${UTILITY_COMMANDS.BADGE_SETTINGS}`, {
|
|
102
|
+
description: "Configure badge settings via TUI overlay",
|
|
103
|
+
handler: async (_args: string, ctx: ExtensionContext) => {
|
|
104
|
+
if (!ctx.hasUI) {
|
|
105
|
+
ctx.ui.notify("Badge settings require an interactive UI.", "warning");
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
ctx.ui.custom(
|
|
110
|
+
(tui: any, _theme: any, _keybindings: any, done: any) => {
|
|
111
|
+
const overlay = new BadgeSettingsTui();
|
|
112
|
+
overlay.onClose = () => done(undefined);
|
|
113
|
+
overlay.requestRender = () => tui.requestRender();
|
|
114
|
+
return {
|
|
115
|
+
render: (w: number) => overlay.render(w),
|
|
116
|
+
invalidate: () => overlay.invalidate(),
|
|
117
|
+
handleInput: (data: string) => {
|
|
118
|
+
overlay.handleInput(data);
|
|
119
|
+
tui.requestRender();
|
|
120
|
+
},
|
|
121
|
+
};
|
|
122
|
+
},
|
|
123
|
+
{
|
|
124
|
+
overlay: true,
|
|
125
|
+
overlayOptions: {
|
|
126
|
+
width: "80%",
|
|
127
|
+
minWidth: 50,
|
|
128
|
+
anchor: "center",
|
|
129
|
+
margin: 2,
|
|
130
|
+
},
|
|
131
|
+
},
|
|
132
|
+
);
|
|
133
|
+
},
|
|
134
|
+
});
|
|
98
135
|
}
|
|
99
136
|
|
|
100
137
|
/**
|
|
@@ -29,6 +29,9 @@ import { getLifecycle } from "./lifecycle/process.js";
|
|
|
29
29
|
import { getAnalyticsCollector } from "./analytics/collector.js";
|
|
30
30
|
import { registerInfoScreen } from "./info-screen.js";
|
|
31
31
|
|
|
32
|
+
/** Re-export readBadgeSettings for cross-package use */
|
|
33
|
+
export { readBadgeSettings } from "./tui/badge-settings.js";
|
|
34
|
+
|
|
32
35
|
/** Package version */
|
|
33
36
|
const VERSION = getPackageVersion(new URL(".", import.meta.url).pathname);
|
|
34
37
|
|
|
@@ -43,9 +46,10 @@ const ALL_COMMANDS = [
|
|
|
43
46
|
UTILITY_COMMANDS.CLEANUP,
|
|
44
47
|
UTILITY_COMMANDS.ENV,
|
|
45
48
|
UTILITY_COMMANDS.DOCTOR,
|
|
46
|
-
UTILITY_COMMANDS.
|
|
49
|
+
UTILITY_COMMANDS.BADGE_NAME,
|
|
47
50
|
UTILITY_COMMANDS.BADGE_GEN,
|
|
48
51
|
UTILITY_COMMANDS.BADGE_TOGGLE,
|
|
52
|
+
UTILITY_COMMANDS.BADGE_SETTINGS,
|
|
49
53
|
].map((cmd) => `unipi:${cmd}`);
|
|
50
54
|
|
|
51
55
|
/** All tools registered by this module */
|
|
@@ -66,6 +70,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
66
70
|
// Initialize name badge state
|
|
67
71
|
const nameBadgeState = new NameBadgeState();
|
|
68
72
|
|
|
73
|
+
// Capture session context for cross-event use (not needed if BADGE_GENERATE_REQUEST removed)
|
|
74
|
+
|
|
69
75
|
// Register commands
|
|
70
76
|
registerUtilityCommands(pi);
|
|
71
77
|
registerNameBadgeCommands(pi, nameBadgeState);
|
|
@@ -89,6 +95,15 @@ export default function (pi: ExtensionAPI) {
|
|
|
89
95
|
|
|
90
96
|
// Restore name badge if it was visible in previous session
|
|
91
97
|
await nameBadgeState.restore(pi, ctx);
|
|
98
|
+
|
|
99
|
+
// Write model cache for TUI components
|
|
100
|
+
if ((ctx as any).modelRegistry) {
|
|
101
|
+
const { writeModelCache } = await import("@pi-unipi/core");
|
|
102
|
+
const registry = (ctx as any).modelRegistry;
|
|
103
|
+
const models = (registry.getAvailable?.() ?? registry.getAll())
|
|
104
|
+
.map((m: any) => ({ provider: m.provider, id: m.id, name: m.name }));
|
|
105
|
+
writeModelCache(models);
|
|
106
|
+
}
|
|
92
107
|
});
|
|
93
108
|
|
|
94
109
|
// First-message hook: auto-generate session name on first user message
|
|
@@ -127,14 +142,6 @@ export default function (pi: ExtensionAPI) {
|
|
|
127
142
|
}
|
|
128
143
|
});
|
|
129
144
|
|
|
130
|
-
// Listen for badge generation requests from other modules (e.g., kanboard)
|
|
131
|
-
pi.on(UNIPI_EVENTS.BADGE_GENERATE_REQUEST as any, async (_event: any, ctx: any) => {
|
|
132
|
-
// Show badge overlay if not already visible
|
|
133
|
-
if (!nameBadgeState.isVisible() && ctx?.hasUI) {
|
|
134
|
-
await nameBadgeState.show(pi, ctx);
|
|
135
|
-
}
|
|
136
|
-
});
|
|
137
|
-
|
|
138
145
|
// Track command usage
|
|
139
146
|
pi.on("tool_call", async (event) => {
|
|
140
147
|
if (event.toolName.startsWith("unipi:")) {
|
|
@@ -0,0 +1,388 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @pi-unipi/utility — Badge Settings TUI Overlay
|
|
3
|
+
*
|
|
4
|
+
* Interactive settings overlay for badge configuration.
|
|
5
|
+
* Three settings: auto-generate toggle, badge-enabled toggle, generation model selector.
|
|
6
|
+
* Model list loaded from shared model cache (~/.unipi/config/models-cache.json).
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { Component } from "@mariozechner/pi-tui";
|
|
10
|
+
import { truncateToWidth, visibleWidth } from "@mariozechner/pi-tui";
|
|
11
|
+
import type { CachedModel } from "@pi-unipi/core";
|
|
12
|
+
import { readModelCache } from "@pi-unipi/core";
|
|
13
|
+
import {
|
|
14
|
+
readBadgeSettings,
|
|
15
|
+
writeBadgeSettings,
|
|
16
|
+
type BadgeSettings,
|
|
17
|
+
} from "./badge-settings.js";
|
|
18
|
+
|
|
19
|
+
/** ANSI escape codes */
|
|
20
|
+
const ansi = {
|
|
21
|
+
reset: "\x1b[0m",
|
|
22
|
+
bold: "\x1b[1m",
|
|
23
|
+
dim: "\x1b[2m",
|
|
24
|
+
cyan: "\x1b[36m",
|
|
25
|
+
green: "\x1b[32m",
|
|
26
|
+
yellow: "\x1b[33m",
|
|
27
|
+
red: "\x1b[31m",
|
|
28
|
+
gray: "\x1b[90m",
|
|
29
|
+
white: "\x1b[37m",
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
/** Toggle symbols */
|
|
33
|
+
const TOGGLE_ON = `${ansi.green}●${ansi.reset}`;
|
|
34
|
+
const TOGGLE_OFF = `${ansi.dim}○${ansi.reset}`;
|
|
35
|
+
|
|
36
|
+
/** Active mode */
|
|
37
|
+
type Mode = "settings" | "model-picker";
|
|
38
|
+
|
|
39
|
+
/** Setting row types */
|
|
40
|
+
interface BooleanSetting {
|
|
41
|
+
type: "boolean";
|
|
42
|
+
key: keyof BadgeSettings;
|
|
43
|
+
label: string;
|
|
44
|
+
description: string;
|
|
45
|
+
getValue: (s: BadgeSettings) => boolean;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
interface ModelSetting {
|
|
49
|
+
type: "model";
|
|
50
|
+
key: "generationModel";
|
|
51
|
+
label: string;
|
|
52
|
+
description: string;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
type SettingItem = BooleanSetting | ModelSetting;
|
|
56
|
+
|
|
57
|
+
/** Settings list */
|
|
58
|
+
const SETTINGS: SettingItem[] = [
|
|
59
|
+
{
|
|
60
|
+
type: "boolean",
|
|
61
|
+
key: "autoGen",
|
|
62
|
+
label: "Auto generate",
|
|
63
|
+
description: "Generate session name on first user message",
|
|
64
|
+
getValue: (s) => s.autoGen,
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
type: "boolean",
|
|
68
|
+
key: "badgeEnabled",
|
|
69
|
+
label: "Badge enabled",
|
|
70
|
+
description: "Show the name badge overlay",
|
|
71
|
+
getValue: (s) => s.badgeEnabled,
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
type: "model",
|
|
75
|
+
key: "generationModel",
|
|
76
|
+
label: "Generation model",
|
|
77
|
+
description: "Model to use for badge name generation",
|
|
78
|
+
},
|
|
79
|
+
];
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Badge Settings TUI overlay.
|
|
83
|
+
* Implements the Component interface for use with ctx.ui.custom().
|
|
84
|
+
*/
|
|
85
|
+
export class BadgeSettingsTui implements Component {
|
|
86
|
+
private settings: BadgeSettings;
|
|
87
|
+
private mode: Mode = "settings";
|
|
88
|
+
private selectedIndex = 0;
|
|
89
|
+
private modelScrollOffset = 0;
|
|
90
|
+
private models: CachedModel[] = [];
|
|
91
|
+
|
|
92
|
+
/** Theme reference for rendering (set externally) */
|
|
93
|
+
private _theme: any = null;
|
|
94
|
+
|
|
95
|
+
/** Callback when overlay should close */
|
|
96
|
+
onClose?: () => void;
|
|
97
|
+
|
|
98
|
+
/** Callback to request a re-render */
|
|
99
|
+
requestRender?: () => void;
|
|
100
|
+
|
|
101
|
+
constructor() {
|
|
102
|
+
this.settings = readBadgeSettings();
|
|
103
|
+
this.models = readModelCache();
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Set the pi-tui theme for styled rendering.
|
|
108
|
+
*/
|
|
109
|
+
setTheme(theme: any): void {
|
|
110
|
+
this._theme = theme;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Invalidate cached render state.
|
|
115
|
+
*/
|
|
116
|
+
invalidate(): void {
|
|
117
|
+
// No cached state to invalidate
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Handle keyboard input.
|
|
122
|
+
*/
|
|
123
|
+
handleInput(data: string): void {
|
|
124
|
+
if (this.mode === "settings") {
|
|
125
|
+
this.handleSettingsInput(data);
|
|
126
|
+
} else {
|
|
127
|
+
this.handleModelPickerInput(data);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Handle input in settings mode.
|
|
133
|
+
*/
|
|
134
|
+
private handleSettingsInput(data: string): void {
|
|
135
|
+
switch (data) {
|
|
136
|
+
case "\x1b[A": // Up arrow
|
|
137
|
+
case "k":
|
|
138
|
+
this.selectedIndex =
|
|
139
|
+
(this.selectedIndex - 1 + SETTINGS.length) % SETTINGS.length;
|
|
140
|
+
break;
|
|
141
|
+
case "\x1b[B": // Down arrow
|
|
142
|
+
case "j":
|
|
143
|
+
this.selectedIndex = (this.selectedIndex + 1) % SETTINGS.length;
|
|
144
|
+
break;
|
|
145
|
+
case " ": // Space — toggle boolean settings
|
|
146
|
+
this.toggleCurrentSetting();
|
|
147
|
+
break;
|
|
148
|
+
case "\r": // Enter — open model picker or toggle
|
|
149
|
+
if (SETTINGS[this.selectedIndex].type === "model") {
|
|
150
|
+
this.enterModelPicker();
|
|
151
|
+
} else {
|
|
152
|
+
this.toggleCurrentSetting();
|
|
153
|
+
}
|
|
154
|
+
break;
|
|
155
|
+
case "\x1b": // Escape — close and save
|
|
156
|
+
this.save();
|
|
157
|
+
this.onClose?.();
|
|
158
|
+
break;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Handle input in model picker mode.
|
|
164
|
+
*/
|
|
165
|
+
private handleModelPickerInput(data: string): void {
|
|
166
|
+
const allModels = this.getModelList();
|
|
167
|
+
|
|
168
|
+
switch (data) {
|
|
169
|
+
case "\x1b[A": // Up arrow
|
|
170
|
+
case "k":
|
|
171
|
+
this.selectedIndex =
|
|
172
|
+
(this.selectedIndex - 1 + allModels.length) % allModels.length;
|
|
173
|
+
this.adjustModelScroll(allModels.length);
|
|
174
|
+
break;
|
|
175
|
+
case "\x1b[B": // Down arrow
|
|
176
|
+
case "j":
|
|
177
|
+
this.selectedIndex = (this.selectedIndex + 1) % allModels.length;
|
|
178
|
+
this.adjustModelScroll(allModels.length);
|
|
179
|
+
break;
|
|
180
|
+
case "\r": // Enter — select model
|
|
181
|
+
this.selectModel();
|
|
182
|
+
break;
|
|
183
|
+
case "\x1b": // Escape — cancel picker
|
|
184
|
+
this.mode = "settings";
|
|
185
|
+
this.selectedIndex = SETTINGS.findIndex(
|
|
186
|
+
(s) => s.key === "generationModel",
|
|
187
|
+
);
|
|
188
|
+
break;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Toggle the currently selected boolean setting.
|
|
194
|
+
*/
|
|
195
|
+
private toggleCurrentSetting(): void {
|
|
196
|
+
const item = SETTINGS[this.selectedIndex];
|
|
197
|
+
if (item.type !== "boolean") return;
|
|
198
|
+
|
|
199
|
+
const current = item.getValue(this.settings);
|
|
200
|
+
(this.settings as any)[item.key] = !current;
|
|
201
|
+
this.save();
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Enter model picker mode.
|
|
206
|
+
*/
|
|
207
|
+
private enterModelPicker(): void {
|
|
208
|
+
this.mode = "model-picker";
|
|
209
|
+
const allModels = this.getModelList();
|
|
210
|
+
const currentModel = this.settings.generationModel;
|
|
211
|
+
this.selectedIndex = allModels.findIndex((m) => m.id === currentModel);
|
|
212
|
+
if (this.selectedIndex < 0) this.selectedIndex = 0;
|
|
213
|
+
this.modelScrollOffset = 0;
|
|
214
|
+
this.adjustModelScroll(allModels.length);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Select the current model in the picker.
|
|
219
|
+
*/
|
|
220
|
+
private selectModel(): void {
|
|
221
|
+
const allModels = this.getModelList();
|
|
222
|
+
const selected = allModels[this.selectedIndex];
|
|
223
|
+
if (selected) {
|
|
224
|
+
this.settings.generationModel = selected.id;
|
|
225
|
+
this.save();
|
|
226
|
+
}
|
|
227
|
+
this.mode = "settings";
|
|
228
|
+
this.selectedIndex = SETTINGS.findIndex((s) => s.key === "generationModel");
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Get model list with "inherit" as first entry.
|
|
233
|
+
*/
|
|
234
|
+
private getModelList(): Array<{ id: string; label: string }> {
|
|
235
|
+
const list: Array<{ id: string; label: string }> = [
|
|
236
|
+
{ id: "inherit", label: "inherit (use parent model)" },
|
|
237
|
+
];
|
|
238
|
+
for (const m of this.models) {
|
|
239
|
+
const fullId = `${m.provider}/${m.id}`;
|
|
240
|
+
list.push({
|
|
241
|
+
id: fullId,
|
|
242
|
+
label: m.name ? `${fullId} (${m.name})` : fullId,
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
return list;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Adjust scroll offset so the selected item is visible.
|
|
250
|
+
*/
|
|
251
|
+
private adjustModelScroll(totalItems: number): void {
|
|
252
|
+
// Reserve ~10 lines for visible model list
|
|
253
|
+
const visibleLines = 10;
|
|
254
|
+
if (this.selectedIndex < this.modelScrollOffset) {
|
|
255
|
+
this.modelScrollOffset = this.selectedIndex;
|
|
256
|
+
} else if (this.selectedIndex >= this.modelScrollOffset + visibleLines) {
|
|
257
|
+
this.modelScrollOffset = this.selectedIndex - visibleLines + 1;
|
|
258
|
+
}
|
|
259
|
+
// Clamp
|
|
260
|
+
this.modelScrollOffset = Math.max(
|
|
261
|
+
0,
|
|
262
|
+
Math.min(this.modelScrollOffset, totalItems - visibleLines),
|
|
263
|
+
);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Save settings to disk.
|
|
268
|
+
*/
|
|
269
|
+
private save(): void {
|
|
270
|
+
writeBadgeSettings(this.settings);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Render the overlay.
|
|
275
|
+
*/
|
|
276
|
+
render(width: number): string[] {
|
|
277
|
+
const lines: string[] = [];
|
|
278
|
+
const innerWidth = Math.max(44, width - 2);
|
|
279
|
+
|
|
280
|
+
const padVisible = (content: string, targetWidth: number): string => {
|
|
281
|
+
const vw = visibleWidth(content);
|
|
282
|
+
const pad = Math.max(0, targetWidth - vw);
|
|
283
|
+
return content + " ".repeat(pad);
|
|
284
|
+
};
|
|
285
|
+
|
|
286
|
+
const add = (s: string) =>
|
|
287
|
+
lines.push(
|
|
288
|
+
`${ansi.cyan}│${ansi.reset}` +
|
|
289
|
+
padVisible(truncateToWidth(s, innerWidth), innerWidth) +
|
|
290
|
+
`${ansi.cyan}│${ansi.reset}`,
|
|
291
|
+
);
|
|
292
|
+
|
|
293
|
+
const addEmpty = () =>
|
|
294
|
+
lines.push(
|
|
295
|
+
`${ansi.cyan}│${ansi.reset}` +
|
|
296
|
+
" ".repeat(innerWidth) +
|
|
297
|
+
`${ansi.cyan}│${ansi.reset}`,
|
|
298
|
+
);
|
|
299
|
+
|
|
300
|
+
// Top border
|
|
301
|
+
lines.push(`${ansi.cyan}╭${"─".repeat(innerWidth)}╮${ansi.reset}`);
|
|
302
|
+
|
|
303
|
+
// Header
|
|
304
|
+
add(`${ansi.bold}${ansi.cyan}Badge Settings${ansi.reset}`);
|
|
305
|
+
add(`${ansi.dim}Configure badge generation behavior${ansi.reset}`);
|
|
306
|
+
addEmpty();
|
|
307
|
+
|
|
308
|
+
// Settings list
|
|
309
|
+
for (let i = 0; i < SETTINGS.length; i++) {
|
|
310
|
+
const item = SETTINGS[i];
|
|
311
|
+
const isSelected = i === this.selectedIndex && this.mode === "settings";
|
|
312
|
+
const selector = isSelected ? `${ansi.cyan}▸${ansi.reset}` : " ";
|
|
313
|
+
|
|
314
|
+
if (item.type === "boolean") {
|
|
315
|
+
const value = item.getValue(this.settings);
|
|
316
|
+
const toggle = value ? TOGGLE_ON : TOGGLE_OFF;
|
|
317
|
+
const labelColor = isSelected ? ansi.bold : ansi.dim;
|
|
318
|
+
|
|
319
|
+
add(
|
|
320
|
+
`${selector} ${toggle} ${labelColor}${item.label}${ansi.reset}`,
|
|
321
|
+
);
|
|
322
|
+
add(` ${ansi.gray}${item.description}${ansi.reset}`);
|
|
323
|
+
} else if (item.type === "model") {
|
|
324
|
+
const labelColor = isSelected ? ansi.bold : ansi.dim;
|
|
325
|
+
const modelDisplay = this.settings.generationModel;
|
|
326
|
+
|
|
327
|
+
add(
|
|
328
|
+
`${selector} ${ansi.yellow}⚙${ansi.reset} ${labelColor}${item.label}${ansi.reset}: ${ansi.white}${modelDisplay}${ansi.reset}`,
|
|
329
|
+
);
|
|
330
|
+
add(` ${ansi.gray}${item.description}${ansi.reset}`);
|
|
331
|
+
add(` ${ansi.dim}Enter to select model${ansi.reset}`);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// Model picker (inline)
|
|
336
|
+
if (this.mode === "model-picker") {
|
|
337
|
+
addEmpty();
|
|
338
|
+
add(`${ansi.bold}${ansi.cyan}── Available Models ──${ansi.reset}`);
|
|
339
|
+
|
|
340
|
+
const allModels = this.getModelList();
|
|
341
|
+
const visibleLines = 10;
|
|
342
|
+
const start = this.modelScrollOffset;
|
|
343
|
+
const end = Math.min(start + visibleLines, allModels.length);
|
|
344
|
+
|
|
345
|
+
// Scroll indicator up
|
|
346
|
+
if (start > 0) {
|
|
347
|
+
add(` ${ansi.dim}▲ ${start} more above${ansi.reset}`);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
for (let i = start; i < end; i++) {
|
|
351
|
+
const m = allModels[i];
|
|
352
|
+
const isSelected = i === this.selectedIndex;
|
|
353
|
+
const selector = isSelected ? `${ansi.cyan}▸${ansi.reset}` : " ";
|
|
354
|
+
const labelColor = isSelected ? ansi.bold + ansi.white : ansi.dim;
|
|
355
|
+
|
|
356
|
+
add(
|
|
357
|
+
`${selector} ${labelColor}${m.label}${ansi.reset}`,
|
|
358
|
+
);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// Scroll indicator down
|
|
362
|
+
if (end < allModels.length) {
|
|
363
|
+
add(
|
|
364
|
+
` ${ansi.dim}▼ ${allModels.length - end} more below${ansi.reset}`,
|
|
365
|
+
);
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// Footer
|
|
370
|
+
addEmpty();
|
|
371
|
+
|
|
372
|
+
if (this.mode === "model-picker") {
|
|
373
|
+
add(
|
|
374
|
+
`${ansi.dim}↑↓ navigate • Enter select • Esc cancel${ansi.reset}`,
|
|
375
|
+
);
|
|
376
|
+
} else {
|
|
377
|
+
add(
|
|
378
|
+
`${ansi.dim}↑↓ navigate • Space toggle • Enter select model • Esc close${ansi.reset}`,
|
|
379
|
+
);
|
|
380
|
+
add(`${ansi.dim}Config: .unipi/config/badge.json${ansi.reset}`);
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
// Bottom border
|
|
384
|
+
lines.push(`${ansi.cyan}╰${"─".repeat(innerWidth)}╯${ansi.reset}`);
|
|
385
|
+
|
|
386
|
+
return lines;
|
|
387
|
+
}
|
|
388
|
+
}
|
|
@@ -16,6 +16,8 @@ export interface BadgeSettings {
|
|
|
16
16
|
badgeEnabled: boolean;
|
|
17
17
|
/** Enable the set_session_name tool for agents */
|
|
18
18
|
agentTool: boolean;
|
|
19
|
+
/** Model to use for badge name generation. "inherit" = parent model, or "provider/model-id" */
|
|
20
|
+
generationModel: string;
|
|
19
21
|
}
|
|
20
22
|
|
|
21
23
|
/** Default badge settings */
|
|
@@ -23,6 +25,7 @@ const DEFAULT_SETTINGS: BadgeSettings = {
|
|
|
23
25
|
autoGen: true,
|
|
24
26
|
badgeEnabled: true,
|
|
25
27
|
agentTool: true,
|
|
28
|
+
generationModel: "inherit",
|
|
26
29
|
};
|
|
27
30
|
|
|
28
31
|
/** Badge settings file name */
|
|
@@ -48,6 +51,7 @@ export function readBadgeSettings(): BadgeSettings {
|
|
|
48
51
|
autoGen: typeof parsed.autoGen === "boolean" ? parsed.autoGen : DEFAULT_SETTINGS.autoGen,
|
|
49
52
|
badgeEnabled: typeof parsed.badgeEnabled === "boolean" ? parsed.badgeEnabled : DEFAULT_SETTINGS.badgeEnabled,
|
|
50
53
|
agentTool: typeof parsed.agentTool === "boolean" ? parsed.agentTool : DEFAULT_SETTINGS.agentTool,
|
|
54
|
+
generationModel: typeof parsed.generationModel === "string" ? parsed.generationModel : DEFAULT_SETTINGS.generationModel,
|
|
51
55
|
};
|
|
52
56
|
} catch {
|
|
53
57
|
return { ...DEFAULT_SETTINGS };
|
|
@@ -97,6 +101,7 @@ export function formatBadgeSettings(settings: BadgeSettings): string {
|
|
|
97
101
|
`| Auto Generate | ${toggle(settings.autoGen)} | Generate name on first message |`,
|
|
98
102
|
`| Badge Enabled | ${toggle(settings.badgeEnabled)} | Show badge overlay |`,
|
|
99
103
|
`| Agent Tool | ${toggle(settings.agentTool)} | Allow agents to call set_session_name |`,
|
|
104
|
+
`| Generation Model | ${settings.generationModel} | Model for badge name generation |`,
|
|
100
105
|
"",
|
|
101
106
|
`Config: \`${BADGE_CONFIG_FILE}\``,
|
|
102
107
|
].join("\n");
|
|
@@ -149,7 +149,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
149
149
|
});
|
|
150
150
|
|
|
151
151
|
// Listen for ralph module ready event
|
|
152
|
-
pi.on(UNIPI_EVENTS.MODULE_READY
|
|
152
|
+
pi.events.on(UNIPI_EVENTS.MODULE_READY, (event: any) => {
|
|
153
153
|
if (event?.name === MODULES.RALPH) {
|
|
154
154
|
ralphDetected = true;
|
|
155
155
|
}
|