@oh-my-pi/pi-coding-agent 13.12.0 → 13.12.4

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.
Files changed (37) hide show
  1. package/CHANGELOG.md +47 -0
  2. package/package.json +7 -7
  3. package/src/capability/context-file.ts +2 -0
  4. package/src/capability/extension-module.ts +1 -0
  5. package/src/capability/hook.ts +1 -0
  6. package/src/capability/index.ts +21 -10
  7. package/src/capability/instruction.ts +1 -0
  8. package/src/capability/mcp.ts +1 -0
  9. package/src/capability/prompt.ts +1 -0
  10. package/src/capability/rule.ts +1 -0
  11. package/src/capability/skill.ts +1 -0
  12. package/src/capability/slash-command.ts +1 -0
  13. package/src/capability/tool.ts +1 -0
  14. package/src/capability/types.ts +10 -0
  15. package/src/cli/commands/init-xdg.ts +2 -2
  16. package/src/cli/config-cli.ts +1 -1
  17. package/src/config/settings-schema.ts +42 -0
  18. package/src/extensibility/custom-tools/types.ts +3 -0
  19. package/src/extensibility/extensions/loader.ts +5 -1
  20. package/src/extensibility/plugins/loader.ts +23 -5
  21. package/src/extensibility/plugins/manager.ts +14 -0
  22. package/src/extensibility/plugins/types.ts +4 -0
  23. package/src/extensibility/skills.ts +7 -1
  24. package/src/ipy/kernel.ts +4 -5
  25. package/src/modes/components/diff.ts +2 -7
  26. package/src/modes/components/extensions/state-manager.ts +3 -2
  27. package/src/modes/components/settings-defs.ts +55 -5
  28. package/src/modes/components/settings-selector.ts +5 -0
  29. package/src/modes/controllers/command-controller.ts +8 -2
  30. package/src/patch/diff.ts +9 -1
  31. package/src/patch/index.ts +56 -9
  32. package/src/sdk.ts +28 -29
  33. package/src/session/agent-session.ts +3 -3
  34. package/src/session/compaction/compaction.ts +10 -0
  35. package/src/session/session-manager.ts +13 -5
  36. package/src/tools/output-meta.ts +25 -19
  37. package/src/tools/path-utils.ts +11 -0
package/CHANGELOG.md CHANGED
@@ -2,6 +2,53 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [13.12.4] - 2026-03-15
6
+ ### Added
7
+
8
+ - Exposed `settings` instance in `CustomToolContext` for session-specific configuration access
9
+
10
+ ### Changed
11
+
12
+ - Improved artifact spill configuration to use session settings with schema defaults as fallback
13
+ - Refactored type annotations for better type safety in tool result handling
14
+
15
+ ## [13.12.2] - 2026-03-15
16
+
17
+ ### Added
18
+
19
+ - Added `compaction.thresholdTokens` setting as a fixed token limit alternative to percentage-based compaction threshold
20
+ - Added more artifact spill threshold options (1 KB to 1 MB) with size descriptions
21
+ - Added more artifact tail bytes and tail lines options with descriptions
22
+ - Added `toExtensionId` capability method to enable granular disabling of individual capabilities by ID
23
+ - Added support for disabling specific capabilities (skills, tools, hooks, rules, prompts, instructions, slash commands, MCP servers, extension modules, and context files) via `disabledExtensions` setting
24
+ - Added `includeDisabled` and `disabledExtensions` options to `LoadOptions` for capability loading
25
+ - Added plugin manifest support for `extensions` entry points to allow plugins to contribute extension modules
26
+ - Added `extensions` field to plugin features for feature-specific extension entry points
27
+ - Added automatic discovery of extension modules from installed plugins during extension loading
28
+ - Added `disabledExtensions` setting to allow disabling specific extensions and skills by ID
29
+ - Added support for filtering skills by disabled extension IDs with `skill:` prefix
30
+
31
+ ### Changed
32
+
33
+ - Changed capability loading to filter out disabled items based on extension IDs before returning results
34
+ - Changed plugin loader to support `extensions` as a manifest entry type alongside tools, hooks, and commands
35
+ - Changed extension discovery to include extension entry points from all enabled plugins
36
+ - Changed context file path handling to use `path.basename()` for consistent cross-platform filename extraction
37
+
38
+ ### Fixed
39
+
40
+ - Fixed skill loading to properly respect disabled skill names when loading from custom directories
41
+
42
+ ## [13.12.1] - 2026-03-15
43
+ ### Added
44
+
45
+ - Support for move-only operations that preserve exact bytes including binary files
46
+
47
+ ### Fixed
48
+
49
+ - Fixed handling of file moves when no edits are specified, now correctly preserves binary content
50
+ - Fixed validation to reject move operations where source and destination paths are identical
51
+
5
52
  ## [13.12.0] - 2026-03-14
6
53
 
7
54
  ### Added
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@oh-my-pi/pi-coding-agent",
4
- "version": "13.12.0",
4
+ "version": "13.12.4",
5
5
  "description": "Coding agent CLI with read, bash, edit, write tools and session management",
6
6
  "homepage": "https://github.com/can1357/oh-my-pi",
7
7
  "author": "Can Boluk",
@@ -41,12 +41,12 @@
41
41
  },
42
42
  "dependencies": {
43
43
  "@mozilla/readability": "^0.6",
44
- "@oh-my-pi/omp-stats": "13.12.0",
45
- "@oh-my-pi/pi-agent-core": "13.12.0",
46
- "@oh-my-pi/pi-ai": "13.12.0",
47
- "@oh-my-pi/pi-natives": "13.12.0",
48
- "@oh-my-pi/pi-tui": "13.12.0",
49
- "@oh-my-pi/pi-utils": "13.12.0",
44
+ "@oh-my-pi/omp-stats": "13.12.4",
45
+ "@oh-my-pi/pi-agent-core": "13.12.4",
46
+ "@oh-my-pi/pi-ai": "13.12.4",
47
+ "@oh-my-pi/pi-natives": "13.12.4",
48
+ "@oh-my-pi/pi-tui": "13.12.4",
49
+ "@oh-my-pi/pi-utils": "13.12.4",
50
50
  "@sinclair/typebox": "^0.34",
51
51
  "@xterm/headless": "^6.0",
52
52
  "ajv": "^8.18",
@@ -4,6 +4,7 @@
4
4
  * System instruction files (CLAUDE.md, AGENTS.md, GEMINI.md, etc.) that provide
5
5
  * persistent guidance to the agent.
6
6
  */
7
+ import * as path from "node:path";
7
8
  import { defineCapability } from ".";
8
9
  import type { SourceMeta } from "./types";
9
10
 
@@ -33,6 +34,7 @@ export const contextFileCapability = defineCapability<ContextFile>({
33
34
  // Clamp depth >= 0: files inside config subdirectories of an ancestor (e.g. .claude/, .github/)
34
35
  // are same-scope as the ancestor itself.
35
36
  key: file => (file.level === "user" ? "user" : `project:${Math.max(0, file.depth ?? 0)}`),
37
+ toExtensionId: file => `context-file:${file.level}:${path.basename(file.path)}`,
36
38
  validate: file => {
37
39
  if (!file.path) return "Missing path";
38
40
  if (file.content === undefined) return "Missing content";
@@ -25,6 +25,7 @@ export const extensionModuleCapability = defineCapability<ExtensionModule>({
25
25
  displayName: "Extension Modules",
26
26
  description: "TypeScript/JavaScript extension modules loaded by the extension system",
27
27
  key: ext => ext.name,
28
+ toExtensionId: ext => `extension-module:${ext.name}`,
28
29
  validate: ext => {
29
30
  if (!ext.name) return "Missing name";
30
31
  if (!ext.path) return "Missing path";
@@ -29,6 +29,7 @@ export const hookCapability = defineCapability<Hook>({
29
29
  displayName: "Hooks",
30
30
  description: "Pre/post tool execution hooks",
31
31
  key: hook => `${hook.type}:${hook.tool}:${hook.name}`,
32
+ toExtensionId: hook => `hook:${hook.type}:${hook.tool}:${hook.name}`,
32
33
  validate: hook => {
33
34
  if (!hook.name) return "Missing name";
34
35
  if (!hook.path) return "Missing path";
@@ -107,6 +107,9 @@ async function loadImpl<T>(
107
107
  const allItems: Array<T & { _source: SourceMeta; _shadowed?: boolean }> = [];
108
108
  const allWarnings: string[] = [];
109
109
  const contributingProviders: string[] = [];
110
+ const disabledExtensionIds = options.includeDisabled
111
+ ? new Set<string>()
112
+ : new Set<string>(options.disabledExtensions ?? settings?.get("disabledExtensions") ?? []);
110
113
 
111
114
  const results = await Promise.all(
112
115
  providers.map(async provider => {
@@ -136,18 +139,26 @@ async function loadImpl<T>(
136
139
  allWarnings.push(...result.warnings.map(w => `[${provider.displayName}] ${w}`));
137
140
  }
138
141
 
139
- if (result.items.length > 0) {
140
- contributingProviders.push(provider.id);
142
+ let contributedItemCount = 0;
143
+ for (const item of result.items) {
144
+ const itemWithSource = item as T & { _source: SourceMeta };
145
+ if (!itemWithSource._source) {
146
+ allWarnings.push(`[${provider.displayName}] Item missing _source metadata, skipping`);
147
+ continue;
148
+ }
141
149
 
142
- for (const item of result.items) {
143
- const itemWithSource = item as T & { _source: SourceMeta };
144
- if (itemWithSource._source) {
145
- itemWithSource._source.providerName = provider.displayName;
146
- allItems.push(itemWithSource as T & { _source: SourceMeta; _shadowed?: boolean });
147
- } else {
148
- allWarnings.push(`[${provider.displayName}] Item missing _source metadata, skipping`);
149
- }
150
+ const extensionId = capability.toExtensionId?.(itemWithSource);
151
+ if (extensionId && disabledExtensionIds.has(extensionId)) {
152
+ continue;
150
153
  }
154
+
155
+ itemWithSource._source.providerName = provider.displayName;
156
+ allItems.push(itemWithSource as T & { _source: SourceMeta; _shadowed?: boolean });
157
+ contributedItemCount += 1;
158
+ }
159
+
160
+ if (contributedItemCount > 0) {
161
+ contributingProviders.push(provider.id);
151
162
  }
152
163
  }
153
164
 
@@ -27,6 +27,7 @@ export const instructionCapability = defineCapability<Instruction>({
27
27
  displayName: "Instructions",
28
28
  description: "File-specific instructions with glob pattern matching (GitHub Copilot format)",
29
29
  key: inst => inst.name,
30
+ toExtensionId: inst => `instruction:${inst.name}`,
30
31
  validate: inst => {
31
32
  if (!inst.name) return "Missing name";
32
33
  if (!inst.path) return "Missing path";
@@ -48,6 +48,7 @@ export const mcpCapability = defineCapability<MCPServer>({
48
48
  displayName: "MCP Servers",
49
49
  description: "Model Context Protocol server configurations for external tool integrations",
50
50
  key: server => server.name,
51
+ toExtensionId: server => `mcp:${server.name}`,
51
52
  validate: server => {
52
53
  if (!server.name) return "Missing server name";
53
54
  if (!server.command && !server.url) return "Must have command or url";
@@ -25,6 +25,7 @@ export const promptCapability = defineCapability<Prompt>({
25
25
  displayName: "Prompts",
26
26
  description: "Reusable prompt templates available via /prompts: menu",
27
27
  key: prompt => prompt.name,
28
+ toExtensionId: prompt => `prompt:${prompt.name}`,
28
29
  validate: prompt => {
29
30
  if (!prompt.name) return "Missing name";
30
31
  if (!prompt.path) return "Missing path";
@@ -214,6 +214,7 @@ export const ruleCapability = defineCapability<Rule>({
214
214
  displayName: "Rules",
215
215
  description: "Project-specific rules and constraints (Cursor MDC, Windsurf, Cline formats)",
216
216
  key: rule => rule.name,
217
+ toExtensionId: rule => `rule:${rule.name}`,
217
218
  validate: rule => {
218
219
  if (!rule.name) return "Missing rule name";
219
220
  if (!rule.path) return "Missing rule path";
@@ -40,6 +40,7 @@ export const skillCapability = defineCapability<Skill>({
40
40
  displayName: "Skills",
41
41
  description: "Specialized knowledge and workflow files that extend agent capabilities",
42
42
  key: skill => skill.name,
43
+ toExtensionId: skill => `skill:${skill.name}`,
43
44
  validate: skill => {
44
45
  if (!skill.name) return "Missing skill name";
45
46
  if (!skill.path) return "Missing skill path";
@@ -27,6 +27,7 @@ export const slashCommandCapability = defineCapability<SlashCommand>({
27
27
  displayName: "Slash Commands",
28
28
  description: "Custom slash commands defined as markdown files",
29
29
  key: cmd => cmd.name,
30
+ toExtensionId: cmd => `slash-command:${cmd.name}`,
30
31
  validate: cmd => {
31
32
  if (!cmd.name) return "Missing name";
32
33
  if (!cmd.path) return "Missing path";
@@ -29,6 +29,7 @@ export const toolCapability = defineCapability<CustomTool>({
29
29
  displayName: "Custom Tools",
30
30
  description: "User-defined tools that extend agent capabilities",
31
31
  key: tool => tool.name,
32
+ toExtensionId: tool => `tool:${tool.name}`,
32
33
  validate: tool => {
33
34
  if (!tool.name) return "Missing name";
34
35
  if (!tool.path) return "Missing path";
@@ -68,6 +68,10 @@ export interface LoadOptions {
68
68
  cwd?: string;
69
69
  /** Include items even if they fail validation. Default: false */
70
70
  includeInvalid?: boolean;
71
+ /** Include items disabled via settings. Default: false */
72
+ includeDisabled?: boolean;
73
+ /** Explicit disabled extension IDs to apply instead of settings. */
74
+ disabledExtensions?: string[];
71
75
  }
72
76
 
73
77
  /**
@@ -123,6 +127,12 @@ export interface Capability<T> {
123
127
  */
124
128
  validate?(item: T): string | undefined;
125
129
 
130
+ /**
131
+ * Optional disabledExtensions ID for this item.
132
+ * When present, loadCapability() can hide items disabled via settings.
133
+ */
134
+ toExtensionId?(item: T): string | undefined;
135
+
126
136
  /** Registered providers, sorted by priority (highest first) */
127
137
  providers: Provider<T>[];
128
138
  }
@@ -5,8 +5,8 @@ import * as path from "node:path";
5
5
  const APP_NAME = "omp";
6
6
 
7
7
  export async function initXdg(): Promise<void> {
8
- if (process.platform !== "linux") {
9
- console.error("XDG directory setup is only supported on Linux.");
8
+ if (process.platform !== "linux" && process.platform !== "darwin") {
9
+ console.error("XDG directory setup is only supported on Linux and macOS.");
10
10
  process.exit(1);
11
11
  }
12
12
 
@@ -397,7 +397,7 @@ ${chalk.bold("Commands:")}
397
397
  set <key> <value> Set a setting value
398
398
  reset <key> Reset a setting to its default value
399
399
  path Print the config directory path
400
- init-xdg Initialize XDG Base Directory structure (Linux only)
400
+ init-xdg Initialize XDG Base Directory structure
401
401
 
402
402
  ${chalk.bold("Options:")}
403
403
  --json Output as JSON
@@ -226,6 +226,36 @@ export const SETTINGS_SCHEMA = {
226
226
  submenu: true,
227
227
  },
228
228
  },
229
+ "tools.artifactSpillThreshold": {
230
+ type: "number",
231
+ default: 50,
232
+ ui: {
233
+ tab: "tools",
234
+ label: "Artifact spill threshold (KB)",
235
+ description: "Tool output above this size is saved as an artifact; tail is kept inline",
236
+ submenu: true,
237
+ },
238
+ },
239
+ "tools.artifactTailBytes": {
240
+ type: "number",
241
+ default: 20,
242
+ ui: {
243
+ tab: "tools",
244
+ label: "Artifact tail size (KB)",
245
+ description: "Amount of tail content kept inline when output spills to artifact",
246
+ submenu: true,
247
+ },
248
+ },
249
+ "tools.artifactTailLines": {
250
+ type: "number",
251
+ default: 500,
252
+ ui: {
253
+ tab: "tools",
254
+ label: "Artifact tail lines",
255
+ description: "Maximum lines of tail content kept inline when output spills to artifact",
256
+ submenu: true,
257
+ },
258
+ },
229
259
 
230
260
  "statusLine.showHookStatus": {
231
261
  type: "boolean",
@@ -629,6 +659,16 @@ export const SETTINGS_SCHEMA = {
629
659
  submenu: true,
630
660
  },
631
661
  },
662
+ "compaction.thresholdTokens": {
663
+ type: "number",
664
+ default: -1,
665
+ ui: {
666
+ tab: "context",
667
+ label: "Compaction Token Limit",
668
+ description: "Fixed token limit for context maintenance; overrides percentage if set",
669
+ submenu: true,
670
+ },
671
+ },
632
672
 
633
673
  "compaction.handoffSaveToDisk": {
634
674
  type: "boolean",
@@ -1518,6 +1558,7 @@ export interface CompactionSettings {
1518
1558
  enabled: boolean;
1519
1559
  strategy: "context-full" | "handoff" | "off";
1520
1560
  thresholdPercent: number;
1561
+ thresholdTokens: number;
1521
1562
  reserveTokens: number;
1522
1563
  keepRecentTokens: number;
1523
1564
  handoffSaveToDisk: boolean;
@@ -1574,6 +1615,7 @@ export interface SkillsSettings {
1574
1615
  customDirectories?: string[];
1575
1616
  ignoredSkills?: string[];
1576
1617
  includeSkills?: string[];
1618
+ disabledExtensions?: string[];
1577
1619
  }
1578
1620
 
1579
1621
  export interface CommitSettings {
@@ -10,6 +10,7 @@ import type { Component } from "@oh-my-pi/pi-tui";
10
10
  import type { Static, TSchema } from "@sinclair/typebox";
11
11
  import type { Rule } from "../../capability/rule";
12
12
  import type { ModelRegistry } from "../../config/model-registry";
13
+ import type { Settings } from "../../config/settings";
13
14
  import type { ExecOptions, ExecResult } from "../../exec/exec";
14
15
  import type { HookUIContext } from "../../extensibility/hooks/types";
15
16
  import type { Theme } from "../../modes/theme/theme";
@@ -76,6 +77,8 @@ export interface CustomToolContext {
76
77
  hasQueuedMessages(): boolean;
77
78
  /** Abort the current agent operation (fire-and-forget, does not wait) */
78
79
  abort(): void;
80
+ /** Settings instance for the current session. Prefer over the global singleton. */
81
+ settings?: Settings;
79
82
  }
80
83
 
81
84
  /** Session event passed to onSession callback */
@@ -18,6 +18,7 @@ import type { ExecOptions } from "../../exec/exec";
18
18
  import { execCommand } from "../../exec/exec";
19
19
  import type { CustomMessage } from "../../session/messages";
20
20
  import { EventBus } from "../../utils/event-bus";
21
+ import { getAllPluginExtensionPaths } from "../plugins/loader";
21
22
  import { resolvePath } from "../utils";
22
23
  import type {
23
24
  Extension,
@@ -481,7 +482,10 @@ export async function discoverAndLoadExtensions(
481
482
  addPath(ext.path);
482
483
  }
483
484
 
484
- // 2. Explicitly configured paths
485
+ // 2. Discover extension entry points from installed plugins
486
+ addPaths(await getAllPluginExtensionPaths(cwd));
487
+
488
+ // 3. Explicitly configured paths
485
489
  for (const configuredPath of configuredPaths) {
486
490
  const resolved = resolvePath(configuredPath, cwd);
487
491
 
@@ -1,8 +1,8 @@
1
1
  /**
2
- * Plugin loader - discovers and loads tools/hooks from installed plugins.
2
+ * Plugin loader - discovers and loads manifest entry points from installed plugins.
3
3
  *
4
- * Reads enabled plugins from the runtime config and loads their tools/hooks
5
- * based on manifest entries and enabled features.
4
+ * Reads enabled plugins from the runtime config and loads their
5
+ * tools/hooks/extensions/commands based on manifest entries and enabled features.
6
6
  */
7
7
  import * as fs from "node:fs";
8
8
  import * as path from "node:path";
@@ -122,10 +122,10 @@ export async function getEnabledPlugins(cwd: string): Promise<InstalledPlugin[]>
122
122
  // =============================================================================
123
123
 
124
124
  /**
125
- * Generic path resolver for plugin manifest entries (tools, hooks, commands).
125
+ * Generic path resolver for plugin manifest entries (tools, hooks, commands, extensions).
126
126
  * Handles both single-string and string[] base entries, plus feature-specific entries.
127
127
  */
128
- function resolvePluginPaths(plugin: InstalledPlugin, key: "tools" | "hooks" | "commands"): string[] {
128
+ function resolvePluginPaths(plugin: InstalledPlugin, key: "tools" | "hooks" | "commands" | "extensions"): string[] {
129
129
  const paths: string[] = [];
130
130
  const manifest = plugin.manifest;
131
131
 
@@ -188,6 +188,10 @@ export function resolvePluginCommandPaths(plugin: InstalledPlugin): string[] {
188
188
  return resolvePluginPaths(plugin, "commands");
189
189
  }
190
190
 
191
+ export function resolvePluginExtensionPaths(plugin: InstalledPlugin): string[] {
192
+ return resolvePluginPaths(plugin, "extensions");
193
+ }
194
+
191
195
  // =============================================================================
192
196
  // Aggregated Discovery
193
197
  // =============================================================================
@@ -234,6 +238,20 @@ export async function getAllPluginCommandPaths(cwd: string): Promise<string[]> {
234
238
  return paths;
235
239
  }
236
240
 
241
+ /**
242
+ * Get all extension module paths from all enabled plugins.
243
+ */
244
+ export async function getAllPluginExtensionPaths(cwd: string): Promise<string[]> {
245
+ const plugins = await getEnabledPlugins(cwd);
246
+ const paths: string[] = [];
247
+
248
+ for (const plugin of plugins) {
249
+ paths.push(...resolvePluginExtensionPaths(plugin));
250
+ }
251
+
252
+ return paths;
253
+ }
254
+
237
255
  /**
238
256
  * Get plugin settings for use in tool/hook contexts.
239
257
  * Merges global settings with project overrides.
@@ -585,6 +585,20 @@ export class PluginManager {
585
585
  }
586
586
  }
587
587
 
588
+ // Check extension entry paths exist if specified
589
+ if (manifest?.extensions) {
590
+ for (const extensionPath of manifest.extensions) {
591
+ const resolvedExtensionPath = path.join(pluginPath, extensionPath);
592
+ if (!fs.existsSync(resolvedExtensionPath)) {
593
+ checks.push({
594
+ name: `plugin:${name}:extension:${extensionPath}`,
595
+ status: "error",
596
+ message: `Extension entry "${extensionPath}" not found`,
597
+ });
598
+ }
599
+ }
600
+ }
601
+
588
602
  // Check enabled features exist in manifest
589
603
  const runtimeState = config.plugins[name];
590
604
  if (runtimeState?.enabledFeatures && manifest?.features) {
@@ -11,6 +11,8 @@ export interface PluginFeature {
11
11
  description?: string;
12
12
  /** Whether this feature is enabled by default */
13
13
  default?: boolean;
14
+ /** Additional extension entry points provided by this feature */
15
+ extensions?: string[];
14
16
  /** Additional tool entry points provided by this feature */
15
17
  tools?: string[];
16
18
  /** Additional hook entry points provided by this feature */
@@ -34,6 +36,8 @@ export interface PluginManifest {
34
36
  tools?: string;
35
37
  /** Entry point for base hooks (relative path from package root) */
36
38
  hooks?: string;
39
+ /** Extension entry points (relative paths from package root) */
40
+ extensions?: string[];
37
41
  /** Command files (relative paths from package root) */
38
42
  commands?: string[];
39
43
 
@@ -83,6 +83,7 @@ export async function loadSkills(options: LoadSkillsOptions = {}): Promise<LoadS
83
83
  customDirectories = [],
84
84
  ignoredSkills = [],
85
85
  includeSkills = [],
86
+ disabledExtensions = [],
86
87
  } = options;
87
88
 
88
89
  // Early return if skills are disabled
@@ -105,7 +106,7 @@ export async function loadSkills(options: LoadSkillsOptions = {}): Promise<LoadS
105
106
  }
106
107
 
107
108
  // Use capability API to load all skills
108
- const result = await loadCapability<CapabilitySkill>(skillCapability.id, { cwd });
109
+ const result = await loadCapability<CapabilitySkill>(skillCapability.id, { cwd, disabledExtensions });
109
110
 
110
111
  const skillMap = new Map<string, Skill>();
111
112
  const realPathSet = new Set<string>();
@@ -123,8 +124,12 @@ export async function loadSkills(options: LoadSkillsOptions = {}): Promise<LoadS
123
124
  return ignoredSkills.some(pattern => new Bun.Glob(pattern).match(name));
124
125
  }
125
126
 
127
+ const disabledSkillNames = new Set(
128
+ (disabledExtensions ?? []).filter(id => id.startsWith("skill:")).map(id => id.slice(6)),
129
+ );
126
130
  // Filter skills by source and patterns first
127
131
  const filteredSkills = result.items.filter(capSkill => {
132
+ if (disabledSkillNames.has(capSkill.name)) return false;
128
133
  if (!isSourceEnabled(capSkill._source)) return false;
129
134
  if (matchesIgnorePatterns(capSkill.name)) return false;
130
135
  if (!matchesIncludePatterns(capSkill.name)) return false;
@@ -190,6 +195,7 @@ export async function loadSkills(options: LoadSkillsOptions = {}): Promise<LoadS
190
195
  const allCustomSkills: Array<{ skill: Skill; path: string }> = [];
191
196
  for (const { expandedDir, scanResult } of customDirectoryResults) {
192
197
  for (const capSkill of scanResult.items) {
198
+ if (disabledSkillNames.has(capSkill.name)) continue;
193
199
  if (matchesIgnorePatterns(capSkill.name)) continue;
194
200
  if (!matchesIncludePatterns(capSkill.name)) continue;
195
201
  allCustomSkills.push({
package/src/ipy/kernel.ts CHANGED
@@ -274,13 +274,12 @@ export function serializeWebSocketMessage(msg: JupyterMessage): ArrayBuffer {
274
274
  metadata: msg.metadata,
275
275
  content: msg.content,
276
276
  });
277
- const msgBytes = TEXT_ENCODER.encode(msgText);
278
277
 
279
278
  const buffers = msg.buffers ?? [];
280
279
  const offsetCount = 1 + buffers.length;
281
280
  const headerSize = 4 + offsetCount * 4;
282
-
283
- let totalSize = headerSize + msgBytes.length;
281
+ const msgBytes = Buffer.byteLength(msgText);
282
+ let totalSize = headerSize + msgBytes;
284
283
  for (const buf of buffers) {
285
284
  totalSize += buf.length;
286
285
  }
@@ -293,8 +292,8 @@ export function serializeWebSocketMessage(msg: JupyterMessage): ArrayBuffer {
293
292
 
294
293
  let offset = headerSize;
295
294
  view.setUint32(4, offset, true);
296
- bytes.set(msgBytes, offset);
297
- offset += msgBytes.length;
295
+ TEXT_ENCODER.encodeInto(msgText, bytes.subarray(offset));
296
+ offset += msgBytes;
298
297
 
299
298
  for (let i = 0; i < buffers.length; i++) {
300
299
  view.setUint32(4 + (i + 1) * 4, offset, true);
@@ -23,17 +23,12 @@ function visualizeIndent(text: string, filePath?: string): string {
23
23
  const leftPadding = Math.floor(tabWidth / 2);
24
24
  const rightPadding = Math.max(0, tabWidth - leftPadding - 1);
25
25
  const tabMarker = `${DIM}${" ".repeat(leftPadding)}→${" ".repeat(rightPadding)}${DIM_OFF}`;
26
- // Normalize: collapse configured tab-width groups into tab markers, then handle remaining spaces.
27
- const normalized = indent.replaceAll("\t", indentation);
28
26
  let visible = "";
29
- let pos = 0;
30
- while (pos < normalized.length) {
31
- if (pos + tabWidth <= normalized.length && normalized.slice(pos, pos + tabWidth) === indentation) {
27
+ for (const ch of indent) {
28
+ if (ch === "\t") {
32
29
  visible += tabMarker;
33
- pos += tabWidth;
34
30
  } else {
35
31
  visible += `${DIM}·${DIM_OFF}`;
36
- pos++;
37
32
  }
38
33
  }
39
34
  return `${visible}${replaceTabs(rest, filePath)}`;
@@ -2,6 +2,7 @@
2
2
  * State manager for the Extension Control Center.
3
3
  * Handles data loading, tree building, filtering, and toggle persistence.
4
4
  */
5
+ import * as path from "node:path";
5
6
  import { logger } from "@oh-my-pi/pi-utils";
6
7
  import type { ContextFile } from "../../../capability/context-file";
7
8
  import type { ExtensionModule } from "../../../capability/extension-module";
@@ -96,7 +97,7 @@ export async function loadAllExtensions(cwd?: string, disabledIds?: string[]): P
96
97
  }
97
98
  }
98
99
 
99
- const loadOpts = cwd ? { cwd } : {};
100
+ const loadOpts = cwd ? { cwd, includeDisabled: true } : { includeDisabled: true };
100
101
 
101
102
  // Load skills
102
103
  try {
@@ -252,7 +253,7 @@ export async function loadAllExtensions(cwd?: string, disabledIds?: string[]): P
252
253
  const contextFiles = await loadCapability<ContextFile>("context-files", loadOpts);
253
254
  for (const file of contextFiles.all) {
254
255
  // Extract filename from path for display
255
- const name = file.path.split("/").pop() || file.path;
256
+ const name = path.basename(file.path);
256
257
  const id = makeExtensionId("context-file", `${file.level}:${name}`);
257
258
  const isDisabled = disabledExtensions.has(id);
258
259
  const isShadowed = (file as { _shadowed?: boolean })._shadowed;