@oh-my-pi/pi-coding-agent 8.10.13 → 8.11.14

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 CHANGED
@@ -2,6 +2,11 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [8.11.14] - 2026-01-28
6
+
7
+ ### Changed
8
+ - Rendered /skill command messages as compact skill entries instead of full prompt text
9
+
5
10
  ## [8.8.8] - 2026-01-28
6
11
 
7
12
  ### Added
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oh-my-pi/pi-coding-agent",
3
- "version": "8.10.13",
3
+ "version": "8.11.14",
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.10.13",
87
- "@oh-my-pi/pi-agent-core": "8.10.13",
88
- "@oh-my-pi/pi-ai": "8.10.13",
89
- "@oh-my-pi/pi-tui": "8.10.13",
90
- "@oh-my-pi/pi-utils": "8.10.13",
86
+ "@oh-my-pi/omp-stats": "8.11.14",
87
+ "@oh-my-pi/pi-agent-core": "8.11.14",
88
+ "@oh-my-pi/pi-ai": "8.11.14",
89
+ "@oh-my-pi/pi-tui": "8.11.14",
90
+ "@oh-my-pi/pi-utils": "8.11.14",
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(settingsManager: SettingsManager, available: Model<Api>[]): Model<Api> | undefined {
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(expandRoleAlias(configured, settingsManager), available);
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(value: string, available: Model<Api>[]): Model<Api> | undefined {
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(modelPattern: string, availableModels: Model<Api>[]): Model<Api> | undefined {
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 exactMatch = availableModels.find(m => m.id.toLowerCase() === modelPattern.toLowerCase());
97
- if (exactMatch) {
98
- return exactMatch;
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
- // Prefer alias - if multiple aliases, pick the one that sorts highest
118
- aliases.sort((a, b) => b.id.localeCompare(a.id));
119
- return aliases[0];
120
- } else {
121
- // No alias found, pick latest dated version
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
- export function parseModelPattern(pattern: string, availableModels: Model<Api>[]): ParsedModelResult {
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 = parseModelPattern(prefix, availableModels);
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 = parseModelPattern(prefix, availableModels);
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(patterns: string[], modelRegistry: ModelRegistry): Promise<ScopedModel[]> {
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 } = parseModelPattern(pattern, availableModels);
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/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 { model, warning } = parseModelPattern(parsed.model, available);
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
 
@@ -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
- await this.ctx.session.prompt(message, { streamingBehavior: "followUp" });
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 { CustomMessage } from "../../session/messages";
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
- this.ctx.chatContainer.addChild(new CustomMessageComponent(message as CustomMessage<unknown>, renderer));
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>
@@ -17,6 +17,7 @@ Performs patch operations on a file given a diff. Primary tool for modifying exi
17
17
  **Context Lines:**
18
18
  - Include enough ` `-prefixed lines to make match unique (usually 2–8 total)
19
19
  - Must exist in the file exactly as written (preserve indentation/trailing spaces)
20
+ - When editing structured blocks (nested braces, tags, indented regions), include opening and closing lines in context so the edit stays inside the block
20
21
  </instruction>
21
22
 
22
23
  <parameters>
@@ -47,6 +48,9 @@ Returns success/failure status. On failure, returns error message indicating:
47
48
  - Always read the target file before editing
48
49
  - Copy anchors and context lines verbatim (including whitespace)
49
50
  - Never use anchors as comments (no line numbers, location labels, or placeholders like `@@ @@`)
51
+ - Do not place new lines outside the intended block unless that is the explicit goal
52
+ - If an edit fails or produces broken structure, re-read the file and produce a new patch from current content—do not retry the same diff
53
+ - If indentation is wrong after editing, run the project's formatter (if available) rather than making repeated edit attempts
50
54
  </critical>
51
55
 
52
56
  <example name="create">
@@ -69,4 +73,6 @@ edit {"path":"obsolete.txt","op":"delete"}
69
73
  - Generic anchors: `import`, `export`, `describe`, `function`, `const`
70
74
  - Anchor comments: `line 207`, `top of file`, `near imports`, `...`
71
75
  - Editing without reading the file first (causes stale context errors)
76
+ - Repeating the same addition in multiple hunks (creates duplicate blocks)
77
+ - Falling back to full-file overwrites for minor changes (acceptable for major restructures or short files)
72
78
  </avoid>
@@ -1165,6 +1165,62 @@ export class AgentSession {
1165
1165
  return;
1166
1166
  }
1167
1167
 
1168
+ const userContent: (TextContent | ImageContent)[] = [{ type: "text", text: expandedText }];
1169
+ if (options?.images) {
1170
+ userContent.push(...options.images);
1171
+ }
1172
+
1173
+ await this._promptWithMessage(
1174
+ {
1175
+ role: "user",
1176
+ content: userContent,
1177
+ synthetic: options?.synthetic,
1178
+ timestamp: Date.now(),
1179
+ },
1180
+ expandedText,
1181
+ options,
1182
+ );
1183
+ }
1184
+
1185
+ async promptCustomMessage<T = unknown>(
1186
+ message: Pick<CustomMessage<T>, "customType" | "content" | "display" | "details">,
1187
+ options?: Pick<PromptOptions, "streamingBehavior" | "toolChoice">,
1188
+ ): Promise<void> {
1189
+ const textContent =
1190
+ typeof message.content === "string"
1191
+ ? message.content
1192
+ : message.content
1193
+ .filter((content): content is TextContent => content.type === "text")
1194
+ .map(content => content.text)
1195
+ .join("");
1196
+
1197
+ if (this.isStreaming) {
1198
+ if (!options?.streamingBehavior) {
1199
+ throw new Error(
1200
+ "Agent is already processing. Specify streamingBehavior ('steer' or 'followUp') to queue the message.",
1201
+ );
1202
+ }
1203
+ await this.sendCustomMessage(message, { deliverAs: options.streamingBehavior });
1204
+ return;
1205
+ }
1206
+
1207
+ const customMessage: CustomMessage<T> = {
1208
+ role: "custom",
1209
+ customType: message.customType,
1210
+ content: message.content,
1211
+ display: message.display,
1212
+ details: message.details,
1213
+ timestamp: Date.now(),
1214
+ };
1215
+
1216
+ await this._promptWithMessage(customMessage, textContent, options);
1217
+ }
1218
+
1219
+ private async _promptWithMessage(
1220
+ message: AgentMessage,
1221
+ expandedText: string,
1222
+ options?: Pick<PromptOptions, "toolChoice" | "images">,
1223
+ ): Promise<void> {
1168
1224
  // Flush any pending bash messages before the new prompt
1169
1225
  this._flushPendingBashMessages();
1170
1226
  this._flushPendingPythonMessages();
@@ -1207,17 +1263,7 @@ export class AgentSession {
1207
1263
  messages.push(planModeMessage);
1208
1264
  }
1209
1265
 
1210
- // Add user message
1211
- const userContent: (TextContent | ImageContent)[] = [{ type: "text", text: expandedText }];
1212
- if (options?.images) {
1213
- userContent.push(...options.images);
1214
- }
1215
- messages.push({
1216
- role: "user",
1217
- content: userContent,
1218
- synthetic: options?.synthetic,
1219
- timestamp: Date.now(),
1220
- });
1266
+ messages.push(message);
1221
1267
 
1222
1268
  // Inject any pending "nextTurn" messages as context alongside the user message
1223
1269
  for (const msg of this._pendingNextTurnMessages) {
@@ -15,6 +15,15 @@ import { formatOutputNotice } from "../tools/output-meta";
15
15
  const COMPACTION_SUMMARY_TEMPLATE = compactionSummaryContextPrompt;
16
16
  const BRANCH_SUMMARY_TEMPLATE = branchSummaryContextPrompt;
17
17
 
18
+ export const SKILL_PROMPT_MESSAGE_TYPE = "skill-prompt";
19
+
20
+ export interface SkillPromptDetails {
21
+ name: string;
22
+ path: string;
23
+ args?: string;
24
+ lineCount: number;
25
+ }
26
+
18
27
  function getPrunedToolResultContent(message: ToolResultMessage): (TextContent | ImageContent)[] {
19
28
  if (message.prunedAt === undefined) {
20
29
  return message.content;
@@ -5,6 +5,7 @@
5
5
  */
6
6
  import { renderPromptTemplate } from "../config/prompt-templates";
7
7
  import { parseAgentFields } from "../discovery/helpers";
8
+ import designerMd from "../prompts/agents/designer.md" with { type: "text" };
8
9
  import exploreMd from "../prompts/agents/explore.md" with { type: "text" };
9
10
  // Embed agent markdown files at build time
10
11
  import agentFrontmatterTemplate from "../prompts/agents/frontmatter.md" with { type: "text" };
@@ -37,6 +38,7 @@ function buildAgentContent(def: EmbeddedAgentDef): string {
37
38
  const EMBEDDED_AGENT_DEFS: EmbeddedAgentDef[] = [
38
39
  { fileName: "explore.md", template: exploreMd },
39
40
  { fileName: "plan.md", template: planMd },
41
+ { fileName: "designer.md", template: designerMd },
40
42
  { fileName: "reviewer.md", template: reviewerMd },
41
43
  {
42
44
  fileName: "task.md",
@@ -57,15 +59,6 @@ const EMBEDDED_AGENT_DEFS: EmbeddedAgentDef[] = [
57
59
  },
58
60
  template: taskMd,
59
61
  },
60
- {
61
- fileName: "deep_task.md",
62
- frontmatter: {
63
- name: "deep_task",
64
- description: "Deep task for comprehensive reasoning",
65
- model: "pi/slow",
66
- },
67
- template: taskMd,
68
- },
69
62
  ];
70
63
 
71
64
  const EMBEDDED_AGENTS: { name: string; content: string }[] = EMBEDDED_AGENT_DEFS.map(def => ({
@@ -134,6 +134,7 @@ function resolveModelOverride(
134
134
  settingsManager?: SettingsManager,
135
135
  ): { model?: Model<Api>; thinkingLevel?: ThinkingLevel } {
136
136
  if (modelPatterns.length === 0) return {};
137
+ const matchPreferences = { usageOrder: settingsManager?.getStorage()?.getModelUsageOrder() };
137
138
  const roles = settingsManager?.serialize().modelRoles as Record<string, string> | undefined;
138
139
  for (const pattern of modelPatterns) {
139
140
  const normalized = pattern.trim().toLowerCase();
@@ -148,7 +149,11 @@ function resolveModelOverride(
148
149
  effectivePattern = configured;
149
150
  }
150
151
  }
151
- const { model, thinkingLevel } = parseModelPattern(effectivePattern, modelRegistry.getAvailable());
152
+ const { model, thinkingLevel } = parseModelPattern(
153
+ effectivePattern,
154
+ modelRegistry.getAvailable(),
155
+ matchPreferences,
156
+ );
152
157
  if (model) {
153
158
  return { model, thinkingLevel: thinkingLevel !== "off" ? thinkingLevel : undefined };
154
159
  }