@oh-my-pi/pi-coding-agent 3.4.1337 → 3.5.1337

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,26 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [3.5.1337] - 2026-01-03
6
+
7
+ ### Added
8
+
9
+ - Added session header and footer output in text mode showing version, model, provider, thinking level, and session ID
10
+ - Added Extension Control Center dashboard accessible via `/extensions` command for unified management of all providers and extensions
11
+ - Added ability to enable/disable individual extensions with persistent settings
12
+ - Added three-column dashboard layout with sidebar tree, extension list, and inspector panel
13
+ - Added fuzzy search filtering for extensions in the dashboard
14
+ - Added keyboard navigation with Tab to cycle panes, j/k for navigation, Space to toggle, Enter to expand/collapse
15
+
16
+ ### Changed
17
+
18
+ - Redesigned Extension Control Center from 3-column layout to tabbed interface with horizontal provider tabs and 2-column grid
19
+ - Replaced sidebar tree navigation with provider tabs using TAB/Shift+TAB cycling
20
+
21
+ ### Fixed
22
+
23
+ - Fixed title generation flag not resetting when starting a new session
24
+
5
25
  ## [3.4.1337] - 2026-01-03
6
26
 
7
27
  ### Added
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oh-my-pi/pi-coding-agent",
3
- "version": "3.4.1337",
3
+ "version": "3.5.1337",
4
4
  "description": "Coding agent CLI with read, bash, edit, write tools and session management",
5
5
  "type": "module",
6
6
  "ompConfig": {
@@ -39,9 +39,9 @@
39
39
  "prepublishOnly": "bun run generate-template && bun run clean && bun run build"
40
40
  },
41
41
  "dependencies": {
42
- "@oh-my-pi/pi-agent-core": "3.4.1337",
43
- "@oh-my-pi/pi-ai": "3.4.1337",
44
- "@oh-my-pi/pi-tui": "3.4.1337",
42
+ "@oh-my-pi/pi-agent-core": "3.5.1337",
43
+ "@oh-my-pi/pi-ai": "3.5.1337",
44
+ "@oh-my-pi/pi-tui": "3.5.1337",
45
45
  "@sinclair/typebox": "^0.34.46",
46
46
  "ajv": "^8.17.1",
47
47
  "chalk": "^5.5.0",
package/src/core/sdk.ts CHANGED
@@ -79,8 +79,10 @@ import {
79
79
  createLsTool,
80
80
  createReadOnlyTools,
81
81
  createReadTool,
82
+ createRulebookTool,
82
83
  createWriteTool,
83
84
  editTool,
85
+ filterRulebookRules,
84
86
  findTool,
85
87
  grepTool,
86
88
  lsTool,
@@ -604,7 +606,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
604
606
  const skills = options.skills ?? discoverSkills(cwd, agentDir, settingsManager.getSkillsSettings());
605
607
  time("discoverSkills");
606
608
 
607
- // Discover TTSR rules
609
+ // Discover rules
608
610
  const ttsrManager = createTtsrManager(settingsManager.getTtsrSettings());
609
611
  const rulesResult = loadCapability<Rule>(ruleCapability.id, { cwd });
610
612
  for (const rule of rulesResult.items) {
@@ -614,6 +616,10 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
614
616
  }
615
617
  time("discoverTtsrRules");
616
618
 
619
+ // Filter rules for the rulebook (non-TTSR, non-alwaysApply, with descriptions)
620
+ const rulebookRules = filterRulebookRules(rulesResult.items);
621
+ time("filterRulebookRules");
622
+
617
623
  const contextFiles = options.contextFiles ?? discoverContextFiles(cwd, agentDir);
618
624
  time("discoverContextFiles");
619
625
 
@@ -757,6 +763,11 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
757
763
 
758
764
  let allToolsArray: Tool[] = [...builtInTools, ...wrappedCustomTools];
759
765
 
766
+ // Add rulebook tool if there are rules with descriptions (always enabled, regardless of --tools)
767
+ if (rulebookRules.length > 0) {
768
+ allToolsArray.push(createRulebookTool(rulebookRules));
769
+ }
770
+
760
771
  // Filter out hidden tools unless explicitly requested
761
772
  if (options.explicitTools) {
762
773
  const explicitSet = new Set(options.explicitTools);
@@ -781,6 +792,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
781
792
  cwd,
782
793
  skills,
783
794
  contextFiles,
795
+ rulebookRules,
784
796
  });
785
797
  time("buildSystemPrompt");
786
798
 
@@ -791,6 +803,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
791
803
  cwd,
792
804
  skills,
793
805
  contextFiles,
806
+ rulebookRules,
794
807
  customPrompt: options.systemPrompt,
795
808
  });
796
809
  } else {
@@ -105,6 +105,7 @@ export interface Settings {
105
105
  edit?: EditSettings;
106
106
  ttsr?: TtsrSettings;
107
107
  disabledProviders?: string[]; // Discovery provider IDs that are disabled
108
+ disabledExtensions?: string[]; // Individual extension IDs that are disabled (e.g., "skill:commit")
108
109
  }
109
110
 
110
111
  /** Deep merge settings: project/overrides take precedence, nested objects merge recursively */
@@ -594,6 +595,38 @@ export class SettingsManager {
594
595
  this.save();
595
596
  }
596
597
 
598
+ getDisabledExtensions(): string[] {
599
+ return [...(this.settings.disabledExtensions ?? [])];
600
+ }
601
+
602
+ setDisabledExtensions(extensionIds: string[]): void {
603
+ this.globalSettings.disabledExtensions = extensionIds;
604
+ this.save();
605
+ }
606
+
607
+ isExtensionEnabled(extensionId: string): boolean {
608
+ return !(this.settings.disabledExtensions ?? []).includes(extensionId);
609
+ }
610
+
611
+ enableExtension(extensionId: string): void {
612
+ const disabled = this.globalSettings.disabledExtensions ?? [];
613
+ const index = disabled.indexOf(extensionId);
614
+ if (index !== -1) {
615
+ disabled.splice(index, 1);
616
+ this.globalSettings.disabledExtensions = disabled;
617
+ this.save();
618
+ }
619
+ }
620
+
621
+ disableExtension(extensionId: string): void {
622
+ const disabled = this.globalSettings.disabledExtensions ?? [];
623
+ if (!disabled.includes(extensionId)) {
624
+ disabled.push(extensionId);
625
+ this.globalSettings.disabledExtensions = disabled;
626
+ this.save();
627
+ }
628
+ }
629
+
597
630
  getTtsrSettings(): TtsrSettings {
598
631
  return this.settings.ttsr ?? {};
599
632
  }
@@ -5,12 +5,14 @@
5
5
  import { existsSync, readFileSync } from "node:fs";
6
6
  import chalk from "chalk";
7
7
  import { contextFileCapability } from "../capability/context-file";
8
+ import type { Rule } from "../capability/rule";
8
9
  import { systemPromptCapability } from "../capability/system-prompt";
9
10
  import { getDocsPath, getExamplesPath, getReadmePath } from "../config";
10
11
  import { type ContextFile, loadSync, type SystemPrompt as SystemPromptFile } from "../discovery/index";
11
12
  import type { SkillsSettings } from "./settings-manager";
12
13
  import { formatSkillsForPrompt, loadSkills, type Skill } from "./skills";
13
14
  import type { ToolName } from "./tools/index";
15
+ import { formatRulesForPrompt } from "./tools/rulebook";
14
16
 
15
17
  /**
16
18
  * Execute a git command synchronously and return stdout or null on failure.
@@ -238,6 +240,8 @@ export interface BuildSystemPromptOptions {
238
240
  contextFiles?: Array<{ path: string; content: string; depth?: number }>;
239
241
  /** Pre-loaded skills (skips discovery if provided). */
240
242
  skills?: Skill[];
243
+ /** Pre-loaded rulebook rules (rules with descriptions, excluding TTSR and always-apply). */
244
+ rulebookRules?: Rule[];
241
245
  }
242
246
 
243
247
  /** Build the system prompt with tools, guidelines, and context */
@@ -250,6 +254,7 @@ export function buildSystemPrompt(options: BuildSystemPromptOptions = {}): strin
250
254
  cwd,
251
255
  contextFiles: providedContextFiles,
252
256
  skills: providedSkills,
257
+ rulebookRules,
253
258
  } = options;
254
259
  const resolvedCwd = cwd ?? process.cwd();
255
260
  const resolvedCustomPrompt = resolvePromptInput(customPrompt, "system prompt");
@@ -310,6 +315,11 @@ export function buildSystemPrompt(options: BuildSystemPromptOptions = {}): strin
310
315
  prompt += formatSkillsForPrompt(skills);
311
316
  }
312
317
 
318
+ // Append rules section (always enabled when rules exist)
319
+ if (rulebookRules && rulebookRules.length > 0) {
320
+ prompt += formatRulesForPrompt(rulebookRules);
321
+ }
322
+
313
323
  // Add date/time and working directory last
314
324
  prompt += `\nCurrent date and time: ${dateTime}`;
315
325
  prompt += `\nCurrent working directory: ${resolvedCwd}`;
@@ -419,6 +429,11 @@ Documentation:
419
429
  prompt += formatSkillsForPrompt(skills);
420
430
  }
421
431
 
432
+ // Append rules section (always enabled when rules exist)
433
+ if (rulebookRules && rulebookRules.length > 0) {
434
+ prompt += formatRulesForPrompt(rulebookRules);
435
+ }
436
+
422
437
  // Add date/time and working directory last
423
438
  prompt += `\nCurrent date and time: ${dateTime}`;
424
439
  prompt += `\nCurrent working directory: ${resolvedCwd}`;
@@ -4,6 +4,7 @@
4
4
 
5
5
  import type { Model } from "@oh-my-pi/pi-ai";
6
6
  import { completeSimple } from "@oh-my-pi/pi-ai";
7
+ import { logger } from "./logger";
7
8
  import type { ModelRegistry } from "./model-registry";
8
9
  import { findSmolModel } from "./model-resolver";
9
10
 
@@ -43,21 +44,35 @@ export async function generateSessionTitle(
43
44
  savedSmolModel?: string,
44
45
  ): Promise<string | null> {
45
46
  const model = await findTitleModel(registry, savedSmolModel);
46
- if (!model) return null;
47
+ if (!model) {
48
+ logger.debug("title-generator: no smol model found");
49
+ return null;
50
+ }
47
51
 
48
52
  const apiKey = await registry.getApiKey(model);
49
- if (!apiKey) return null;
53
+ if (!apiKey) {
54
+ logger.debug("title-generator: no API key for model", { provider: model.provider, id: model.id });
55
+ return null;
56
+ }
50
57
 
51
58
  // Truncate message if too long
52
59
  const truncatedMessage =
53
60
  firstMessage.length > MAX_INPUT_CHARS ? `${firstMessage.slice(0, MAX_INPUT_CHARS)}...` : firstMessage;
54
61
 
62
+ const request = {
63
+ model: `${model.provider}/${model.id}`,
64
+ systemPrompt: TITLE_SYSTEM_PROMPT,
65
+ userMessage: `<user-message>\n${truncatedMessage}\n</user-message>`,
66
+ maxTokens: 30,
67
+ };
68
+ logger.debug("title-generator: request", request);
69
+
55
70
  try {
56
71
  const response = await completeSimple(
57
72
  model,
58
73
  {
59
- systemPrompt: TITLE_SYSTEM_PROMPT,
60
- messages: [{ role: "user", content: truncatedMessage, timestamp: Date.now() }],
74
+ systemPrompt: request.systemPrompt,
75
+ messages: [{ role: "user", content: request.userMessage, timestamp: Date.now() }],
61
76
  },
62
77
  {
63
78
  apiKey,
@@ -74,13 +89,20 @@ export async function generateSessionTitle(
74
89
  }
75
90
  title = title.trim();
76
91
 
77
- if (!title || title.length > 60) {
92
+ logger.debug("title-generator: response", {
93
+ title,
94
+ usage: response.usage,
95
+ stopReason: response.stopReason,
96
+ });
97
+
98
+ if (!title) {
78
99
  return null;
79
100
  }
80
101
 
81
102
  // Clean up: remove quotes, trailing punctuation
82
103
  return title.replace(/^["']|["']$/g, "").replace(/[.!?]$/, "");
83
- } catch {
104
+ } catch (err) {
105
+ logger.debug("title-generator: error", { error: err instanceof Error ? err.message : String(err) });
84
106
  return null;
85
107
  }
86
108
  }
@@ -24,6 +24,12 @@ export { createNotebookTool, type NotebookToolDetails, notebookTool } from "./no
24
24
  export { createOutputTool, type OutputToolDetails, outputTool } from "./output";
25
25
  export { createReadTool, type ReadToolDetails, readTool } from "./read";
26
26
  export { createReportFindingTool, createSubmitReviewTool, reportFindingTool, submitReviewTool } from "./review";
27
+ export {
28
+ createRulebookTool,
29
+ filterRulebookRules,
30
+ formatRulesForPrompt,
31
+ type RulebookToolDetails,
32
+ } from "./rulebook";
27
33
  export { BUNDLED_AGENTS, createTaskTool, taskTool } from "./task/index";
28
34
  export type { TruncationResult } from "./truncate";
29
35
  export { createWebFetchTool, type WebFetchToolDetails, webFetchCustomTool, webFetchTool } from "./web-fetch";
@@ -0,0 +1,124 @@
1
+ /**
2
+ * Rulebook Tool
3
+ *
4
+ * Allows the agent to fetch full content of rules that have descriptions.
5
+ * Rules are listed in the system prompt with name + description; this tool
6
+ * retrieves the complete rule content on demand.
7
+ */
8
+
9
+ import type { AgentTool } from "@oh-my-pi/pi-agent-core";
10
+ import { Type } from "@sinclair/typebox";
11
+ import type { Rule } from "../../capability/rule";
12
+
13
+ export interface RulebookToolDetails {
14
+ type: "rulebook";
15
+ ruleName: string;
16
+ found: boolean;
17
+ content?: string;
18
+ }
19
+
20
+ const rulebookSchema = Type.Object({
21
+ name: Type.String({ description: "The name of the rule to fetch" }),
22
+ });
23
+
24
+ /**
25
+ * Create a rulebook tool with access to discovered rules.
26
+ * @param rules - Array of discovered rules (non-TTSR rules with descriptions)
27
+ */
28
+ export function createRulebookTool(rules: Rule[]): AgentTool<typeof rulebookSchema> {
29
+ // Build lookup map for O(1) access
30
+ const ruleMap = new Map<string, Rule>();
31
+ for (const rule of rules) {
32
+ ruleMap.set(rule.name, rule);
33
+ }
34
+
35
+ const ruleNames = rules.map((r) => r.name);
36
+
37
+ return {
38
+ name: "rulebook",
39
+ label: "Rulebook",
40
+ description: `Fetch the full content of a project rule by name. Use this when a rule listed in <available_rules> is relevant to your current task. Available: ${ruleNames.join(", ") || "(none)"}`,
41
+ parameters: rulebookSchema,
42
+ execute: async (_toolCallId: string, { name }: { name: string }) => {
43
+ const rule = ruleMap.get(name);
44
+
45
+ if (!rule) {
46
+ const available = ruleNames.join(", ");
47
+ return {
48
+ content: [{ type: "text", text: `Rule "${name}" not found. Available rules: ${available || "(none)"}` }],
49
+ details: {
50
+ type: "rulebook",
51
+ ruleName: name,
52
+ found: false,
53
+ } satisfies RulebookToolDetails,
54
+ };
55
+ }
56
+
57
+ return {
58
+ content: [{ type: "text", text: `# Rule: ${rule.name}\n\n${rule.content}` }],
59
+ details: {
60
+ type: "rulebook",
61
+ ruleName: name,
62
+ found: true,
63
+ content: rule.content,
64
+ } satisfies RulebookToolDetails,
65
+ };
66
+ },
67
+ };
68
+ }
69
+
70
+ /**
71
+ * Filter rules to only those suitable for the rulebook (have descriptions, no TTSR trigger).
72
+ */
73
+ export function filterRulebookRules(rules: Rule[]): Rule[] {
74
+ return rules.filter((rule) => {
75
+ // Exclude TTSR rules (handled separately by streaming)
76
+ if (rule.ttsrTrigger) return false;
77
+ // Exclude always-apply rules (already in context)
78
+ if (rule.alwaysApply) return false;
79
+ // Must have a description for agent to know when to fetch
80
+ if (!rule.description) return false;
81
+ return true;
82
+ });
83
+ }
84
+
85
+ /**
86
+ * Format rules for inclusion in the system prompt.
87
+ * Lists rule names and descriptions so the agent knows what's available.
88
+ */
89
+ export function formatRulesForPrompt(rules: Rule[]): string {
90
+ if (rules.length === 0) {
91
+ return "";
92
+ }
93
+
94
+ const lines = [
95
+ "\n\n## Available Rules",
96
+ "",
97
+ "The following project rules are available. Use the `rulebook` tool to fetch a rule's full content when it's relevant to your task.",
98
+ "",
99
+ "<available_rules>",
100
+ ];
101
+
102
+ for (const rule of rules) {
103
+ lines.push(" <rule>");
104
+ lines.push(` <name>${escapeXml(rule.name)}</name>`);
105
+ lines.push(` <description>${escapeXml(rule.description || "")}</description>`);
106
+ if (rule.globs && rule.globs.length > 0) {
107
+ lines.push(` <globs>${escapeXml(rule.globs.join(", "))}</globs>`);
108
+ }
109
+ lines.push(" </rule>");
110
+ }
111
+
112
+ lines.push("</available_rules>");
113
+
114
+ return lines.join("\n");
115
+ }
116
+
117
+ function escapeXml(str: string): string {
118
+ return str
119
+ .replace(/&/g, "&amp;")
120
+ .replace(/</g, "&lt;")
121
+ .replace(/>/g, "&gt;")
122
+ .replace(/"/g, "&quot;")
123
+ .replace(/'/g, "&apos;");
124
+ }