@oh-my-pi/pi-coding-agent 8.10.13 → 8.12.1
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/CHANGELOG.md +5 -0
- package/package.json +6 -6
- package/src/commit/model-selection.ts +22 -8
- package/src/config/model-resolver.ts +117 -16
- package/src/cursor.ts +1 -6
- package/src/main.ts +8 -2
- package/src/modes/components/countdown-timer.ts +9 -0
- package/src/modes/components/hook-selector.ts +3 -0
- package/src/modes/components/skill-message.ts +92 -0
- package/src/modes/controllers/input-controller.ts +17 -1
- package/src/modes/utils/ui-helpers.ts +11 -2
- package/src/prompts/agents/designer.md +75 -0
- package/src/prompts/system/custom-system-prompt.md +36 -30
- package/src/prompts/system/system-prompt.md +18 -15
- package/src/prompts/tools/patch.md +6 -0
- package/src/session/agent-session.ts +57 -11
- package/src/session/messages.ts +9 -0
- package/src/system-prompt.ts +292 -0
- package/src/task/agents.ts +2 -9
- package/src/task/executor.ts +6 -1
- package/src/tools/fetch.ts +1 -4
- package/src/tools/gemini-image.ts +1 -3
- package/src/tools/ssh.ts +1 -3
package/CHANGELOG.md
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@oh-my-pi/pi-coding-agent",
|
|
3
|
-
"version": "8.
|
|
3
|
+
"version": "8.12.1",
|
|
4
4
|
"description": "Coding agent CLI with read, bash, edit, write tools and session management",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"ompConfig": {
|
|
@@ -83,11 +83,11 @@
|
|
|
83
83
|
"test": "bun test"
|
|
84
84
|
},
|
|
85
85
|
"dependencies": {
|
|
86
|
-
"@oh-my-pi/omp-stats": "8.
|
|
87
|
-
"@oh-my-pi/pi-agent-core": "8.
|
|
88
|
-
"@oh-my-pi/pi-ai": "8.
|
|
89
|
-
"@oh-my-pi/pi-tui": "8.
|
|
90
|
-
"@oh-my-pi/pi-utils": "8.
|
|
86
|
+
"@oh-my-pi/omp-stats": "8.12.1",
|
|
87
|
+
"@oh-my-pi/pi-agent-core": "8.12.1",
|
|
88
|
+
"@oh-my-pi/pi-ai": "8.12.1",
|
|
89
|
+
"@oh-my-pi/pi-tui": "8.12.1",
|
|
90
|
+
"@oh-my-pi/pi-utils": "8.12.1",
|
|
91
91
|
"@openai/agents": "^0.4.4",
|
|
92
92
|
"@sinclair/typebox": "^0.34.48",
|
|
93
93
|
"ajv": "^8.17.1",
|
|
@@ -11,9 +11,10 @@ export async function resolvePrimaryModel(
|
|
|
11
11
|
},
|
|
12
12
|
): Promise<{ model: Model<Api>; apiKey: string }> {
|
|
13
13
|
const available = modelRegistry.getAvailable();
|
|
14
|
+
const matchPreferences = { usageOrder: settingsManager.getStorage()?.getModelUsageOrder() };
|
|
14
15
|
const model = override
|
|
15
|
-
? resolveModelFromString(expandRoleAlias(override, settingsManager), available)
|
|
16
|
-
: resolveModelFromSettings(settingsManager, available);
|
|
16
|
+
? resolveModelFromString(expandRoleAlias(override, settingsManager), available, matchPreferences)
|
|
17
|
+
: resolveModelFromSettings(settingsManager, available, matchPreferences);
|
|
17
18
|
if (!model) {
|
|
18
19
|
throw new Error("No model available for commit generation");
|
|
19
20
|
}
|
|
@@ -34,15 +35,16 @@ export async function resolveSmolModel(
|
|
|
34
35
|
fallbackApiKey: string,
|
|
35
36
|
): Promise<{ model: Model<Api>; apiKey: string }> {
|
|
36
37
|
const available = modelRegistry.getAvailable();
|
|
38
|
+
const matchPreferences = { usageOrder: settingsManager.getStorage()?.getModelUsageOrder() };
|
|
37
39
|
const role = settingsManager.getModelRole("smol");
|
|
38
|
-
const roleModel = role ? resolveModelFromString(role, available) : undefined;
|
|
40
|
+
const roleModel = role ? resolveModelFromString(role, available, matchPreferences) : undefined;
|
|
39
41
|
if (roleModel) {
|
|
40
42
|
const apiKey = await modelRegistry.getApiKey(roleModel);
|
|
41
43
|
if (apiKey) return { model: roleModel, apiKey };
|
|
42
44
|
}
|
|
43
45
|
|
|
44
46
|
for (const pattern of SMOL_MODEL_PRIORITY) {
|
|
45
|
-
const candidate = parseModelPattern(pattern, available).model;
|
|
47
|
+
const candidate = parseModelPattern(pattern, available, matchPreferences).model;
|
|
46
48
|
if (!candidate) continue;
|
|
47
49
|
const apiKey = await modelRegistry.getApiKey(candidate);
|
|
48
50
|
if (apiKey) return { model: candidate, apiKey };
|
|
@@ -51,23 +53,35 @@ export async function resolveSmolModel(
|
|
|
51
53
|
return { model: fallbackModel, apiKey: fallbackApiKey };
|
|
52
54
|
}
|
|
53
55
|
|
|
54
|
-
function resolveModelFromSettings(
|
|
56
|
+
function resolveModelFromSettings(
|
|
57
|
+
settingsManager: SettingsManager,
|
|
58
|
+
available: Model<Api>[],
|
|
59
|
+
matchPreferences: { usageOrder?: string[] },
|
|
60
|
+
): Model<Api> | undefined {
|
|
55
61
|
const roles = ["commit", "smol", "default"];
|
|
56
62
|
for (const role of roles) {
|
|
57
63
|
const configured = settingsManager.getModelRole(role);
|
|
58
64
|
if (!configured) continue;
|
|
59
|
-
const resolved = resolveModelFromString(
|
|
65
|
+
const resolved = resolveModelFromString(
|
|
66
|
+
expandRoleAlias(configured, settingsManager),
|
|
67
|
+
available,
|
|
68
|
+
matchPreferences,
|
|
69
|
+
);
|
|
60
70
|
if (resolved) return resolved;
|
|
61
71
|
}
|
|
62
72
|
return available[0];
|
|
63
73
|
}
|
|
64
74
|
|
|
65
|
-
function resolveModelFromString(
|
|
75
|
+
function resolveModelFromString(
|
|
76
|
+
value: string,
|
|
77
|
+
available: Model<Api>[],
|
|
78
|
+
matchPreferences: { usageOrder?: string[] },
|
|
79
|
+
): Model<Api> | undefined {
|
|
66
80
|
const parsed = parseModelString(value);
|
|
67
81
|
if (parsed) {
|
|
68
82
|
return available.find(model => model.provider === parsed.provider && model.id === parsed.id);
|
|
69
83
|
}
|
|
70
|
-
return parseModelPattern(value, available).model;
|
|
84
|
+
return parseModelPattern(value, available, matchPreferences).model;
|
|
71
85
|
}
|
|
72
86
|
|
|
73
87
|
function expandRoleAlias(value: string, settingsManager: SettingsManager): string {
|
|
@@ -60,6 +60,76 @@ export function formatModelString(model: Model<Api>): string {
|
|
|
60
60
|
return `${model.provider}/${model.id}`;
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
+
export interface ModelMatchPreferences {
|
|
64
|
+
/** Most-recently-used model keys (provider/modelId) to prefer when ambiguous. */
|
|
65
|
+
usageOrder?: string[];
|
|
66
|
+
/** Providers to deprioritize when no recent usage is available. */
|
|
67
|
+
deprioritizeProviders?: string[];
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
interface ModelPreferenceContext {
|
|
71
|
+
modelUsageRank: Map<string, number>;
|
|
72
|
+
providerUsageRank: Map<string, number>;
|
|
73
|
+
deprioritizedProviders: Set<string>;
|
|
74
|
+
modelOrder: Map<string, number>;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function buildPreferenceContext(
|
|
78
|
+
availableModels: Model<Api>[],
|
|
79
|
+
preferences: ModelMatchPreferences | undefined,
|
|
80
|
+
): ModelPreferenceContext {
|
|
81
|
+
const modelUsageRank = new Map<string, number>();
|
|
82
|
+
const providerUsageRank = new Map<string, number>();
|
|
83
|
+
const usageOrder = preferences?.usageOrder ?? [];
|
|
84
|
+
for (let i = 0; i < usageOrder.length; i += 1) {
|
|
85
|
+
const key = usageOrder[i];
|
|
86
|
+
if (!modelUsageRank.has(key)) {
|
|
87
|
+
modelUsageRank.set(key, i);
|
|
88
|
+
}
|
|
89
|
+
const parsed = parseModelString(key);
|
|
90
|
+
if (parsed && !providerUsageRank.has(parsed.provider)) {
|
|
91
|
+
providerUsageRank.set(parsed.provider, i);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const deprioritizedProviders = new Set(preferences?.deprioritizeProviders ?? ["openrouter"]);
|
|
96
|
+
const modelOrder = new Map<string, number>();
|
|
97
|
+
for (let i = 0; i < availableModels.length; i += 1) {
|
|
98
|
+
modelOrder.set(formatModelString(availableModels[i]), i);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return { modelUsageRank, providerUsageRank, deprioritizedProviders, modelOrder };
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function pickPreferredModel(candidates: Model<Api>[], context: ModelPreferenceContext): Model<Api> {
|
|
105
|
+
if (candidates.length <= 1) return candidates[0];
|
|
106
|
+
return [...candidates].sort((a, b) => {
|
|
107
|
+
const aKey = formatModelString(a);
|
|
108
|
+
const bKey = formatModelString(b);
|
|
109
|
+
const aUsage = context.modelUsageRank.get(aKey);
|
|
110
|
+
const bUsage = context.modelUsageRank.get(bKey);
|
|
111
|
+
if (aUsage !== undefined || bUsage !== undefined) {
|
|
112
|
+
return (aUsage ?? Number.POSITIVE_INFINITY) - (bUsage ?? Number.POSITIVE_INFINITY);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const aProviderUsage = context.providerUsageRank.get(a.provider);
|
|
116
|
+
const bProviderUsage = context.providerUsageRank.get(b.provider);
|
|
117
|
+
if (aProviderUsage !== undefined || bProviderUsage !== undefined) {
|
|
118
|
+
return (aProviderUsage ?? Number.POSITIVE_INFINITY) - (bProviderUsage ?? Number.POSITIVE_INFINITY);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const aDeprioritized = context.deprioritizedProviders.has(a.provider);
|
|
122
|
+
const bDeprioritized = context.deprioritizedProviders.has(b.provider);
|
|
123
|
+
if (aDeprioritized !== bDeprioritized) {
|
|
124
|
+
return aDeprioritized ? 1 : -1;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const aOrder = context.modelOrder.get(aKey) ?? 0;
|
|
128
|
+
const bOrder = context.modelOrder.get(bKey) ?? 0;
|
|
129
|
+
return aOrder - bOrder;
|
|
130
|
+
})[0];
|
|
131
|
+
}
|
|
132
|
+
|
|
63
133
|
/**
|
|
64
134
|
* Helper to check if a model ID looks like an alias (no date suffix)
|
|
65
135
|
* Dates are typically in format: -20241022 or -20250929
|
|
@@ -77,7 +147,11 @@ function isAlias(id: string): boolean {
|
|
|
77
147
|
* Try to match a pattern to a model from the available models list.
|
|
78
148
|
* Returns the matched model or undefined if no match found.
|
|
79
149
|
*/
|
|
80
|
-
function tryMatchModel(
|
|
150
|
+
function tryMatchModel(
|
|
151
|
+
modelPattern: string,
|
|
152
|
+
availableModels: Model<Api>[],
|
|
153
|
+
context: ModelPreferenceContext,
|
|
154
|
+
): Model<Api> | undefined {
|
|
81
155
|
// Check for provider/modelId format (provider is everything before the first /)
|
|
82
156
|
const slashIndex = modelPattern.indexOf("/");
|
|
83
157
|
if (slashIndex !== -1) {
|
|
@@ -93,9 +167,9 @@ function tryMatchModel(modelPattern: string, availableModels: Model<Api>[]): Mod
|
|
|
93
167
|
}
|
|
94
168
|
|
|
95
169
|
// Check for exact ID match (case-insensitive)
|
|
96
|
-
const
|
|
97
|
-
if (
|
|
98
|
-
return
|
|
170
|
+
const exactMatches = availableModels.filter(m => m.id.toLowerCase() === modelPattern.toLowerCase());
|
|
171
|
+
if (exactMatches.length > 0) {
|
|
172
|
+
return pickPreferredModel(exactMatches, context);
|
|
99
173
|
}
|
|
100
174
|
|
|
101
175
|
// No exact match - fall back to partial matching
|
|
@@ -114,14 +188,19 @@ function tryMatchModel(modelPattern: string, availableModels: Model<Api>[]): Mod
|
|
|
114
188
|
const datedVersions = matches.filter(m => !isAlias(m.id));
|
|
115
189
|
|
|
116
190
|
if (aliases.length > 0) {
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
datedVersions.sort((a, b) => b.id.localeCompare(a.id));
|
|
191
|
+
return pickPreferredModel(aliases, context);
|
|
192
|
+
}
|
|
193
|
+
if (datedVersions.length === 0) return undefined;
|
|
194
|
+
|
|
195
|
+
if (datedVersions.length === 1) {
|
|
123
196
|
return datedVersions[0];
|
|
124
197
|
}
|
|
198
|
+
|
|
199
|
+
const sortedById = [...datedVersions].sort((a, b) => b.id.localeCompare(a.id));
|
|
200
|
+
const topId = sortedById[0]?.id;
|
|
201
|
+
if (!topId) return undefined;
|
|
202
|
+
const topCandidates = sortedById.filter(model => model.id === topId);
|
|
203
|
+
return pickPreferredModel(topCandidates, context);
|
|
125
204
|
}
|
|
126
205
|
|
|
127
206
|
export interface ParsedModelResult {
|
|
@@ -145,9 +224,13 @@ export interface ParsedModelResult {
|
|
|
145
224
|
*
|
|
146
225
|
* @internal Exported for testing
|
|
147
226
|
*/
|
|
148
|
-
|
|
227
|
+
function parseModelPatternWithContext(
|
|
228
|
+
pattern: string,
|
|
229
|
+
availableModels: Model<Api>[],
|
|
230
|
+
context: ModelPreferenceContext,
|
|
231
|
+
): ParsedModelResult {
|
|
149
232
|
// Try exact match first
|
|
150
|
-
const exactMatch = tryMatchModel(pattern, availableModels);
|
|
233
|
+
const exactMatch = tryMatchModel(pattern, availableModels, context);
|
|
151
234
|
if (exactMatch) {
|
|
152
235
|
return { model: exactMatch, thinkingLevel: undefined, warning: undefined, explicitThinkingLevel: false };
|
|
153
236
|
}
|
|
@@ -164,7 +247,7 @@ export function parseModelPattern(pattern: string, availableModels: Model<Api>[]
|
|
|
164
247
|
|
|
165
248
|
if (isValidThinkingLevel(suffix)) {
|
|
166
249
|
// Valid thinking level - recurse on prefix and use this level
|
|
167
|
-
const result =
|
|
250
|
+
const result = parseModelPatternWithContext(prefix, availableModels, context);
|
|
168
251
|
if (result.model) {
|
|
169
252
|
// Only use this thinking level if no warning from inner recursion
|
|
170
253
|
const explicitThinkingLevel = !result.warning;
|
|
@@ -179,7 +262,7 @@ export function parseModelPattern(pattern: string, availableModels: Model<Api>[]
|
|
|
179
262
|
}
|
|
180
263
|
|
|
181
264
|
// Invalid suffix - recurse on prefix and warn
|
|
182
|
-
const result =
|
|
265
|
+
const result = parseModelPatternWithContext(prefix, availableModels, context);
|
|
183
266
|
if (result.model) {
|
|
184
267
|
return {
|
|
185
268
|
model: result.model,
|
|
@@ -191,6 +274,15 @@ export function parseModelPattern(pattern: string, availableModels: Model<Api>[]
|
|
|
191
274
|
return result;
|
|
192
275
|
}
|
|
193
276
|
|
|
277
|
+
export function parseModelPattern(
|
|
278
|
+
pattern: string,
|
|
279
|
+
availableModels: Model<Api>[],
|
|
280
|
+
preferences?: ModelMatchPreferences,
|
|
281
|
+
): ParsedModelResult {
|
|
282
|
+
const context = buildPreferenceContext(availableModels, preferences);
|
|
283
|
+
return parseModelPatternWithContext(pattern, availableModels, context);
|
|
284
|
+
}
|
|
285
|
+
|
|
194
286
|
/**
|
|
195
287
|
* Resolve model patterns to actual Model objects with optional thinking levels
|
|
196
288
|
* Format: "pattern:level" where :level is optional
|
|
@@ -202,8 +294,13 @@ export function parseModelPattern(pattern: string, availableModels: Model<Api>[]
|
|
|
202
294
|
* The algorithm tries to match the full pattern first, then progressively
|
|
203
295
|
* strips colon-suffixes to find a match.
|
|
204
296
|
*/
|
|
205
|
-
export async function resolveModelScope(
|
|
297
|
+
export async function resolveModelScope(
|
|
298
|
+
patterns: string[],
|
|
299
|
+
modelRegistry: ModelRegistry,
|
|
300
|
+
preferences?: ModelMatchPreferences,
|
|
301
|
+
): Promise<ScopedModel[]> {
|
|
206
302
|
const availableModels = modelRegistry.getAvailable();
|
|
303
|
+
const context = buildPreferenceContext(availableModels, preferences);
|
|
207
304
|
const scopedModels: ScopedModel[] = [];
|
|
208
305
|
|
|
209
306
|
for (const pattern of patterns) {
|
|
@@ -245,7 +342,11 @@ export async function resolveModelScope(patterns: string[], modelRegistry: Model
|
|
|
245
342
|
continue;
|
|
246
343
|
}
|
|
247
344
|
|
|
248
|
-
const { model, thinkingLevel, warning, explicitThinkingLevel } =
|
|
345
|
+
const { model, thinkingLevel, warning, explicitThinkingLevel } = parseModelPatternWithContext(
|
|
346
|
+
pattern,
|
|
347
|
+
availableModels,
|
|
348
|
+
context,
|
|
349
|
+
);
|
|
249
350
|
|
|
250
351
|
if (warning) {
|
|
251
352
|
console.warn(chalk.yellow(`Warning: ${warning}`));
|
package/src/cursor.ts
CHANGED
|
@@ -194,12 +194,7 @@ export class CursorExecHandlers implements ICursorExecHandlers {
|
|
|
194
194
|
|
|
195
195
|
async shell(args: Parameters<NonNullable<ICursorExecHandlers["shell"]>>[0]) {
|
|
196
196
|
const toolCallId = decodeToolCallId(args.toolCallId);
|
|
197
|
-
const timeoutSeconds =
|
|
198
|
-
args.timeout && args.timeout > 0
|
|
199
|
-
? args.timeout > 1000
|
|
200
|
-
? Math.ceil(args.timeout / 1000)
|
|
201
|
-
: args.timeout
|
|
202
|
-
: undefined;
|
|
197
|
+
const timeoutSeconds = args.timeout && args.timeout > 0 ? args.timeout : undefined;
|
|
203
198
|
const toolResultMessage = await executeTool(this.options, "bash", toolCallId, {
|
|
204
199
|
command: args.command,
|
|
205
200
|
workdir: args.workingDirectory || undefined,
|
package/src/main.ts
CHANGED
|
@@ -373,7 +373,10 @@ async function buildSessionOptions(
|
|
|
373
373
|
// Model from CLI (--model) - uses same fuzzy matching as --models
|
|
374
374
|
if (parsed.model) {
|
|
375
375
|
const available = modelRegistry.getAvailable();
|
|
376
|
-
const
|
|
376
|
+
const modelMatchPreferences = {
|
|
377
|
+
usageOrder: settingsManager.getStorage()?.getModelUsageOrder(),
|
|
378
|
+
};
|
|
379
|
+
const { model, warning } = parseModelPattern(parsed.model, available, modelMatchPreferences);
|
|
377
380
|
if (warning) {
|
|
378
381
|
writeStderr(chalk.yellow(`Warning: ${warning}`));
|
|
379
382
|
}
|
|
@@ -652,8 +655,11 @@ export async function main(args: string[]) {
|
|
|
652
655
|
|
|
653
656
|
let scopedModels: ScopedModel[] = [];
|
|
654
657
|
const modelPatterns = parsed.models ?? settingsManager.getEnabledModels();
|
|
658
|
+
const modelMatchPreferences = {
|
|
659
|
+
usageOrder: settingsManager.getStorage()?.getModelUsageOrder(),
|
|
660
|
+
};
|
|
655
661
|
if (modelPatterns && modelPatterns.length > 0) {
|
|
656
|
-
scopedModels = await resolveModelScope(modelPatterns, modelRegistry);
|
|
662
|
+
scopedModels = await resolveModelScope(modelPatterns, modelRegistry, modelMatchPreferences);
|
|
657
663
|
time("resolveModelScope");
|
|
658
664
|
}
|
|
659
665
|
|
|
@@ -6,6 +6,7 @@ import type { TUI } from "@oh-my-pi/pi-tui";
|
|
|
6
6
|
export class CountdownTimer {
|
|
7
7
|
private intervalId: ReturnType<typeof setInterval> | undefined;
|
|
8
8
|
private remainingSeconds: number;
|
|
9
|
+
private readonly initialMs: number;
|
|
9
10
|
|
|
10
11
|
constructor(
|
|
11
12
|
timeoutMs: number,
|
|
@@ -13,6 +14,7 @@ export class CountdownTimer {
|
|
|
13
14
|
private onTick: (seconds: number) => void,
|
|
14
15
|
private onExpire: () => void,
|
|
15
16
|
) {
|
|
17
|
+
this.initialMs = timeoutMs;
|
|
16
18
|
this.remainingSeconds = Math.ceil(timeoutMs / 1000);
|
|
17
19
|
this.onTick(this.remainingSeconds);
|
|
18
20
|
|
|
@@ -28,6 +30,13 @@ export class CountdownTimer {
|
|
|
28
30
|
}, 1000);
|
|
29
31
|
}
|
|
30
32
|
|
|
33
|
+
/** Reset the countdown to its initial value */
|
|
34
|
+
reset(): void {
|
|
35
|
+
this.remainingSeconds = Math.ceil(this.initialMs / 1000);
|
|
36
|
+
this.onTick(this.remainingSeconds);
|
|
37
|
+
this.tui?.requestRender();
|
|
38
|
+
}
|
|
39
|
+
|
|
31
40
|
dispose(): void {
|
|
32
41
|
if (this.intervalId) {
|
|
33
42
|
clearInterval(this.intervalId);
|
|
@@ -84,6 +84,9 @@ export class HookSelectorComponent extends Container {
|
|
|
84
84
|
}
|
|
85
85
|
|
|
86
86
|
handleInput(keyData: string): void {
|
|
87
|
+
// Reset countdown on any interaction
|
|
88
|
+
this.countdown?.reset();
|
|
89
|
+
|
|
87
90
|
if (matchesKey(keyData, "up") || keyData === "k") {
|
|
88
91
|
this.selectedIndex = Math.max(0, this.selectedIndex - 1);
|
|
89
92
|
this.updateList();
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import type { TextContent } from "@oh-my-pi/pi-ai";
|
|
2
|
+
import type { Component } from "@oh-my-pi/pi-tui";
|
|
3
|
+
import { Box, Container, Markdown, Spacer, Text } from "@oh-my-pi/pi-tui";
|
|
4
|
+
import { getMarkdownTheme, theme } from "../../modes/theme/theme";
|
|
5
|
+
import type { CustomMessage, SkillPromptDetails } from "../../session/messages";
|
|
6
|
+
|
|
7
|
+
export class SkillMessageComponent extends Container {
|
|
8
|
+
private message: CustomMessage<SkillPromptDetails>;
|
|
9
|
+
private box: Box;
|
|
10
|
+
private contentComponent?: Component;
|
|
11
|
+
private _expanded = false;
|
|
12
|
+
|
|
13
|
+
constructor(message: CustomMessage<SkillPromptDetails>) {
|
|
14
|
+
super();
|
|
15
|
+
this.message = message;
|
|
16
|
+
this.addChild(new Spacer(1));
|
|
17
|
+
|
|
18
|
+
this.box = new Box(1, 1, t => theme.bg("customMessageBg", t));
|
|
19
|
+
this.rebuild();
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
setExpanded(expanded: boolean): void {
|
|
23
|
+
if (this._expanded !== expanded) {
|
|
24
|
+
this._expanded = expanded;
|
|
25
|
+
this.rebuild();
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
override invalidate(): void {
|
|
30
|
+
super.invalidate();
|
|
31
|
+
this.rebuild();
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
private rebuild(): void {
|
|
35
|
+
if (this.contentComponent) {
|
|
36
|
+
this.removeChild(this.contentComponent);
|
|
37
|
+
this.contentComponent = undefined;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
this.removeChild(this.box);
|
|
41
|
+
this.addChild(this.box);
|
|
42
|
+
this.box.clear();
|
|
43
|
+
|
|
44
|
+
const label = theme.fg("customMessageLabel", theme.bold("[skill]"));
|
|
45
|
+
this.box.addChild(new Text(label, 0, 0));
|
|
46
|
+
this.box.addChild(new Spacer(1));
|
|
47
|
+
|
|
48
|
+
const details = this.message.details;
|
|
49
|
+
const args = details?.args?.trim();
|
|
50
|
+
const infoLines = [
|
|
51
|
+
`Skill: ${details?.name ?? "unknown"}`,
|
|
52
|
+
args ? `Args: ${args}` : undefined,
|
|
53
|
+
details?.path ? `Path: ${details.path}` : undefined,
|
|
54
|
+
typeof details?.lineCount === "number" ? `Prompt: ${details.lineCount} lines` : undefined,
|
|
55
|
+
].filter((line): line is string => Boolean(line));
|
|
56
|
+
|
|
57
|
+
this.box.addChild(
|
|
58
|
+
new Markdown(infoLines.join("\n"), 0, 0, getMarkdownTheme(), {
|
|
59
|
+
color: (value: string) => theme.fg("customMessageText", value),
|
|
60
|
+
}),
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
if (!this._expanded) {
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const text = this.extractText();
|
|
68
|
+
if (!text) {
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
this.box.addChild(new Spacer(1));
|
|
73
|
+
const promptHeader = theme.fg("customMessageLabel", theme.bold("Prompt"));
|
|
74
|
+
this.box.addChild(new Text(promptHeader, 0, 0));
|
|
75
|
+
this.box.addChild(new Spacer(1));
|
|
76
|
+
|
|
77
|
+
this.contentComponent = new Markdown(text, 0, 0, getMarkdownTheme(), {
|
|
78
|
+
color: (value: string) => theme.fg("customMessageText", value),
|
|
79
|
+
});
|
|
80
|
+
this.box.addChild(this.contentComponent);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
private extractText(): string {
|
|
84
|
+
if (typeof this.message.content === "string") {
|
|
85
|
+
return this.message.content;
|
|
86
|
+
}
|
|
87
|
+
return this.message.content
|
|
88
|
+
.filter((c): c is TextContent => c.type === "text")
|
|
89
|
+
.map(c => c.text)
|
|
90
|
+
.join("\n");
|
|
91
|
+
}
|
|
92
|
+
}
|
|
@@ -7,6 +7,7 @@ import { nanoid } from "nanoid";
|
|
|
7
7
|
import { theme } from "../../modes/theme/theme";
|
|
8
8
|
import type { InteractiveModeContext } from "../../modes/types";
|
|
9
9
|
import type { AgentSessionEvent } from "../../session/agent-session";
|
|
10
|
+
import { SKILL_PROMPT_MESSAGE_TYPE, type SkillPromptDetails } from "../../session/messages";
|
|
10
11
|
import { readImageFromClipboard } from "../../utils/clipboard";
|
|
11
12
|
import { resizeImage } from "../../utils/image-resize";
|
|
12
13
|
import { generateSessionTitle, setTerminalTitle } from "../../utils/title-generator";
|
|
@@ -329,7 +330,22 @@ export class InputController {
|
|
|
329
330
|
metaLines.push(`User: ${args}`);
|
|
330
331
|
}
|
|
331
332
|
const message = `${body}\n\n---\n\n${metaLines.join("\n")}`;
|
|
332
|
-
|
|
333
|
+
const skillName = commandName.slice("skill:".length);
|
|
334
|
+
const details: SkillPromptDetails = {
|
|
335
|
+
name: skillName || commandName,
|
|
336
|
+
path: skillPath,
|
|
337
|
+
args: args || undefined,
|
|
338
|
+
lineCount: body ? body.split("\n").length : 0,
|
|
339
|
+
};
|
|
340
|
+
await this.ctx.session.promptCustomMessage(
|
|
341
|
+
{
|
|
342
|
+
customType: SKILL_PROMPT_MESSAGE_TYPE,
|
|
343
|
+
content: message,
|
|
344
|
+
display: true,
|
|
345
|
+
details,
|
|
346
|
+
},
|
|
347
|
+
{ streamingBehavior: "followUp" },
|
|
348
|
+
);
|
|
333
349
|
} catch (err) {
|
|
334
350
|
this.ctx.showError(`Failed to load skill: ${err instanceof Error ? err.message : String(err)}`);
|
|
335
351
|
}
|
|
@@ -9,11 +9,12 @@ import { CustomMessageComponent } from "../../modes/components/custom-message";
|
|
|
9
9
|
import { DynamicBorder } from "../../modes/components/dynamic-border";
|
|
10
10
|
import { PythonExecutionComponent } from "../../modes/components/python-execution";
|
|
11
11
|
import { ReadToolGroupComponent } from "../../modes/components/read-tool-group";
|
|
12
|
+
import { SkillMessageComponent } from "../../modes/components/skill-message";
|
|
12
13
|
import { ToolExecutionComponent } from "../../modes/components/tool-execution";
|
|
13
14
|
import { UserMessageComponent } from "../../modes/components/user-message";
|
|
14
15
|
import { theme } from "../../modes/theme/theme";
|
|
15
16
|
import type { CompactionQueuedMessage, InteractiveModeContext } from "../../modes/types";
|
|
16
|
-
import type
|
|
17
|
+
import { type CustomMessage, SKILL_PROMPT_MESSAGE_TYPE, type SkillPromptDetails } from "../../session/messages";
|
|
17
18
|
import type { SessionContext } from "../../session/session-manager";
|
|
18
19
|
|
|
19
20
|
type TextBlock = { type: "text"; text: string };
|
|
@@ -94,9 +95,17 @@ export class UiHelpers {
|
|
|
94
95
|
case "hookMessage":
|
|
95
96
|
case "custom": {
|
|
96
97
|
if (message.display) {
|
|
98
|
+
if (message.customType === SKILL_PROMPT_MESSAGE_TYPE) {
|
|
99
|
+
const component = new SkillMessageComponent(message as CustomMessage<SkillPromptDetails>);
|
|
100
|
+
component.setExpanded(this.ctx.toolOutputExpanded);
|
|
101
|
+
this.ctx.chatContainer.addChild(component);
|
|
102
|
+
break;
|
|
103
|
+
}
|
|
97
104
|
const renderer = this.ctx.session.extensionRunner?.getMessageRenderer(message.customType);
|
|
98
105
|
// Both HookMessage and CustomMessage have the same structure, cast for compatibility
|
|
99
|
-
|
|
106
|
+
const component = new CustomMessageComponent(message as CustomMessage<unknown>, renderer);
|
|
107
|
+
component.setExpanded(this.ctx.toolOutputExpanded);
|
|
108
|
+
this.ctx.chatContainer.addChild(component);
|
|
100
109
|
}
|
|
101
110
|
break;
|
|
102
111
|
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: designer
|
|
3
|
+
description: UI/UX specialist for design implementation, review, and visual refinement
|
|
4
|
+
spawns: explore
|
|
5
|
+
model: google-gemini-cli/gemini-3-pro, gemini-3-pro, gemini-3, pi/default
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
<role>Senior design engineer with 10+ years shipping production interfaces. You implement UI, conduct design reviews, and refine components. Your work is distinctive—never generic.</role>
|
|
9
|
+
|
|
10
|
+
<critical>
|
|
11
|
+
You CAN and SHOULD make file edits, create components, and run commands. This is your primary function.
|
|
12
|
+
Before implementing: identify the aesthetic direction, existing patterns, and design tokens in use.
|
|
13
|
+
</critical>
|
|
14
|
+
|
|
15
|
+
<strengths>
|
|
16
|
+
- Translating design intent into working UI code
|
|
17
|
+
- Identifying UX issues: unclear states, missing feedback, poor hierarchy
|
|
18
|
+
- Accessibility: contrast, focus states, semantic markup, screen reader compatibility
|
|
19
|
+
- Visual consistency: spacing, typography, color usage, component patterns
|
|
20
|
+
- Responsive design and layout structure
|
|
21
|
+
</strengths>
|
|
22
|
+
|
|
23
|
+
<procedure>
|
|
24
|
+
## Implementation
|
|
25
|
+
1. Read existing components, tokens, and patterns—reuse before inventing
|
|
26
|
+
2. Identify the aesthetic direction (minimal, bold, editorial, etc.)
|
|
27
|
+
3. Implement with explicit states: loading, empty, error, disabled, hover, focus
|
|
28
|
+
4. Verify accessibility: contrast, focus rings, semantic HTML
|
|
29
|
+
5. Test responsive behavior
|
|
30
|
+
|
|
31
|
+
## Review
|
|
32
|
+
1. Read the files under review
|
|
33
|
+
2. Check for UX issues, accessibility gaps, visual inconsistencies
|
|
34
|
+
3. Cite file, line, and concrete issue—no vague feedback
|
|
35
|
+
4. Suggest specific fixes with code when applicable
|
|
36
|
+
</procedure>
|
|
37
|
+
|
|
38
|
+
<directives>
|
|
39
|
+
- Prefer edits to existing files over creating new ones
|
|
40
|
+
- Keep changes minimal and consistent with existing code style
|
|
41
|
+
- NEVER create documentation files (*.md) unless explicitly requested
|
|
42
|
+
- Be concise. No filler or ceremony.
|
|
43
|
+
- Follow the main agent's instructions.
|
|
44
|
+
</directives>
|
|
45
|
+
|
|
46
|
+
<avoid>
|
|
47
|
+
## AI Slop Patterns
|
|
48
|
+
These are fingerprints of generic AI-generated interfaces. Avoid them:
|
|
49
|
+
- **Glassmorphism everywhere**: blur effects, glass cards, glow borders used decoratively
|
|
50
|
+
- **Cyan-on-dark with purple gradients**: the 2024 AI color palette
|
|
51
|
+
- **Gradient text on metrics/headings**: decorative without meaning
|
|
52
|
+
- **Card grids with identical cards**: icon + heading + text, repeated endlessly
|
|
53
|
+
- **Cards nested inside cards**: visual noise, flatten the hierarchy
|
|
54
|
+
- **Large rounded-corner icons above every heading**: templated, adds no value
|
|
55
|
+
- **Hero metric layouts**: big number, small label, gradient accent—overused
|
|
56
|
+
- **Same spacing everywhere**: no rhythm, monotonous
|
|
57
|
+
- **Center-aligned everything**: left-align with asymmetry feels more designed
|
|
58
|
+
- **Modals for everything**: lazy pattern, rarely the best solution
|
|
59
|
+
- **Overused fonts**: Inter, Roboto, Open Sans, system defaults
|
|
60
|
+
- **Pure black (#000) or pure white (#fff)**: always tint neutrals
|
|
61
|
+
- **Gray text on colored backgrounds**: use a shade of the background instead
|
|
62
|
+
- **Bounce/elastic easing**: dated, tacky—use exponential easing (ease-out-quart/expo)
|
|
63
|
+
|
|
64
|
+
## UX Anti-Patterns
|
|
65
|
+
- Missing states (loading, empty, error)
|
|
66
|
+
- Redundant information (heading restates intro text)
|
|
67
|
+
- Every button styled as primary—hierarchy matters
|
|
68
|
+
- Empty states that just say "nothing here" instead of guiding the user
|
|
69
|
+
</avoid>
|
|
70
|
+
|
|
71
|
+
<critical>
|
|
72
|
+
Every interface should make someone ask "how was this made?" not "which AI made this?"
|
|
73
|
+
Commit to a clear aesthetic direction and execute with precision.
|
|
74
|
+
Keep going until the implementation is complete. This matters.
|
|
75
|
+
</critical>
|