@pencil-agent/nano-pencil 1.14.2 → 1.14.3

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.
@@ -1,6 +1,6 @@
1
1
  {
2
- "version": "1.14.2",
3
- "commitHash": "62cd264",
2
+ "version": "1.14.3",
3
+ "commitHash": "1a4f55a",
4
4
  "branch": "main",
5
- "builtAt": "2026-05-26T18:24:58.388Z"
5
+ "builtAt": "2026-05-27T01:43:40.032Z"
6
6
  }
@@ -5,6 +5,7 @@
5
5
  * [HERE]: builtin-extensions.ts - built-in extension registry for NanoPencil
6
6
  */
7
7
  export type BuiltinExtensionRiskLevel = "passive" | "command" | "tool" | "background" | "write-capable";
8
+ export type BuiltinExtensionTestContract = "lifecycle" | "external-process" | "resource-discovery" | "write-guard";
8
9
  export interface BuiltinExtension {
9
10
  id: string;
10
11
  category: "default" | "optional" | "package";
@@ -14,6 +15,9 @@ export interface BuiltinExtension {
14
15
  startsTimers: boolean;
15
16
  writesWorkspace: boolean;
16
17
  externalProcess: boolean;
18
+ resourceDiscovery?: boolean;
19
+ testContracts?: readonly BuiltinExtensionTestContract[];
20
+ testFiles?: readonly string[];
17
21
  }
18
22
  export declare const builtInExtensions: readonly BuiltinExtension[];
19
23
  /**
@@ -33,29 +33,29 @@ const BUNDLED_RECAP_EXTENSION = join(__dirname, "extensions", "defaults", "recap
33
33
  const BUNDLED_DEBUG_EXTENSION = join(__dirname, "extensions", "defaults", "debug", "index.js");
34
34
  const BUNDLED_MCP_EXTENSION = join(__dirname, "extensions", "defaults", "mcp", "index.js");
35
35
  export const builtInExtensions = [
36
- { id: "diagnostics", category: "default", defaultEnabled: true, riskLevel: "background", requiresUI: false, startsTimers: true, writesWorkspace: false, externalProcess: false },
37
- { id: "sal", category: "default", defaultEnabled: true, riskLevel: "background", requiresUI: false, startsTimers: false, writesWorkspace: false, externalProcess: false },
36
+ { id: "diagnostics", category: "default", defaultEnabled: true, riskLevel: "background", requiresUI: false, startsTimers: true, writesWorkspace: false, externalProcess: false, testContracts: ["lifecycle"], testFiles: ["test/diagnostic-buffer-throttle.test.ts", "test/diagnostics-runtime.test.ts"] },
37
+ { id: "sal", category: "default", defaultEnabled: true, riskLevel: "background", requiresUI: false, startsTimers: false, writesWorkspace: false, externalProcess: false, testContracts: ["lifecycle"], testFiles: ["test/sal-lifecycle.test.ts"] },
38
38
  { id: "token-save", category: "default", defaultEnabled: true, riskLevel: "tool", requiresUI: false, startsTimers: false, writesWorkspace: false, externalProcess: false },
39
- { id: "nanomem", category: "package", defaultEnabled: true, riskLevel: "background", requiresUI: false, startsTimers: false, writesWorkspace: false, externalProcess: false },
40
- { id: "link-world", category: "default", defaultEnabled: true, riskLevel: "tool", requiresUI: false, startsTimers: false, writesWorkspace: false, externalProcess: true },
41
- { id: "browser", category: "default", defaultEnabled: true, riskLevel: "tool", requiresUI: false, startsTimers: false, writesWorkspace: false, externalProcess: true },
39
+ { id: "nanomem", category: "package", defaultEnabled: true, riskLevel: "background", requiresUI: false, startsTimers: false, writesWorkspace: false, externalProcess: false, testContracts: ["lifecycle"], testFiles: ["packages/mem-core/test/extension-commands.test.ts"] },
40
+ { id: "link-world", category: "default", defaultEnabled: true, riskLevel: "tool", requiresUI: false, startsTimers: false, writesWorkspace: false, externalProcess: true, resourceDiscovery: true, testContracts: ["external-process", "resource-discovery"], testFiles: ["test/link-world-extension-registration.test.ts"] },
41
+ { id: "browser", category: "default", defaultEnabled: true, riskLevel: "tool", requiresUI: false, startsTimers: false, writesWorkspace: false, externalProcess: true, resourceDiscovery: true, testContracts: ["external-process", "resource-discovery"], testFiles: ["test/browser-extension-registration.test.ts"] },
42
42
  { id: "security-audit", category: "default", defaultEnabled: true, riskLevel: "tool", requiresUI: false, startsTimers: false, writesWorkspace: false, externalProcess: false },
43
- { id: "soul", category: "default", defaultEnabled: true, riskLevel: "background", requiresUI: false, startsTimers: false, writesWorkspace: false, externalProcess: false },
44
- { id: "presence", category: "default", defaultEnabled: true, riskLevel: "background", requiresUI: true, startsTimers: true, writesWorkspace: false, externalProcess: false },
43
+ { id: "soul", category: "default", defaultEnabled: true, riskLevel: "passive", requiresUI: false, startsTimers: false, writesWorkspace: false, externalProcess: false },
44
+ { id: "presence", category: "default", defaultEnabled: true, riskLevel: "background", requiresUI: true, startsTimers: true, writesWorkspace: false, externalProcess: false, testContracts: ["lifecycle"], testFiles: ["test/presence-opening.test.ts", "test/presence-locale.test.ts"] },
45
45
  { id: "interview", category: "default", defaultEnabled: true, riskLevel: "tool", requiresUI: false, startsTimers: false, writesWorkspace: false, externalProcess: false },
46
- { id: "grub", category: "default", defaultEnabled: true, riskLevel: "background", requiresUI: false, startsTimers: false, writesWorkspace: false, externalProcess: true },
47
- { id: "loop", category: "default", defaultEnabled: true, riskLevel: "background", requiresUI: false, startsTimers: true, writesWorkspace: false, externalProcess: false },
46
+ { id: "grub", category: "default", defaultEnabled: true, riskLevel: "background", requiresUI: false, startsTimers: false, writesWorkspace: false, externalProcess: true, testContracts: ["lifecycle", "external-process"], testFiles: ["test/grub-controller.test.ts"] },
47
+ { id: "loop", category: "default", defaultEnabled: true, riskLevel: "background", requiresUI: false, startsTimers: true, writesWorkspace: false, externalProcess: false, testContracts: ["lifecycle"], testFiles: ["test/loop-lifecycle.test.ts"] },
48
48
  { id: "plan", category: "default", defaultEnabled: true, riskLevel: "tool", requiresUI: false, startsTimers: false, writesWorkspace: false, externalProcess: false },
49
- { id: "discipline", category: "default", defaultEnabled: true, riskLevel: "tool", requiresUI: false, startsTimers: false, writesWorkspace: false, externalProcess: false },
50
- { id: "subagent", category: "default", defaultEnabled: true, riskLevel: "tool", requiresUI: false, startsTimers: false, writesWorkspace: false, externalProcess: true },
51
- { id: "team", category: "default", defaultEnabled: true, riskLevel: "background", requiresUI: false, startsTimers: false, writesWorkspace: false, externalProcess: true },
52
- { id: "idle-think", category: "default", defaultEnabled: true, riskLevel: "background", requiresUI: true, startsTimers: true, writesWorkspace: false, externalProcess: true },
49
+ { id: "discipline", category: "default", defaultEnabled: true, riskLevel: "tool", requiresUI: false, startsTimers: false, writesWorkspace: false, externalProcess: false, resourceDiscovery: true, testContracts: ["resource-discovery"], testFiles: ["test/discipline-extension.test.ts", "test/extension-smoke.test.ts"] },
50
+ { id: "subagent", category: "default", defaultEnabled: true, riskLevel: "tool", requiresUI: false, startsTimers: false, writesWorkspace: false, externalProcess: true, testContracts: ["external-process"], testFiles: ["test/subagent-parser.test.ts", "test/worktree-manager.test.ts"] },
51
+ { id: "team", category: "default", defaultEnabled: true, riskLevel: "background", requiresUI: false, startsTimers: false, writesWorkspace: false, externalProcess: true, testContracts: ["lifecycle", "external-process"], testFiles: ["test/team-runtime.test.ts"] },
52
+ { id: "idle-think", category: "default", defaultEnabled: true, riskLevel: "background", requiresUI: true, startsTimers: true, writesWorkspace: false, externalProcess: true, testContracts: ["lifecycle", "external-process"], testFiles: ["test/idle-think-runtime.test.ts", "test/extension-smoke.test.ts"] },
53
53
  { id: "btw", category: "default", defaultEnabled: true, riskLevel: "command", requiresUI: false, startsTimers: false, writesWorkspace: false, externalProcess: false },
54
54
  { id: "recap", category: "default", defaultEnabled: true, riskLevel: "command", requiresUI: false, startsTimers: false, writesWorkspace: false, externalProcess: false },
55
55
  { id: "debug", category: "default", defaultEnabled: true, riskLevel: "command", requiresUI: false, startsTimers: false, writesWorkspace: false, externalProcess: false },
56
- { id: "mcp", category: "default", defaultEnabled: true, riskLevel: "command", requiresUI: false, startsTimers: false, writesWorkspace: false, externalProcess: true },
57
- { id: "simplify", category: "optional", defaultEnabled: false, riskLevel: "write-capable", requiresUI: false, startsTimers: false, writesWorkspace: true, externalProcess: true },
58
- { id: "export-html", category: "optional", defaultEnabled: false, riskLevel: "write-capable", requiresUI: false, startsTimers: false, writesWorkspace: true, externalProcess: false },
56
+ { id: "mcp", category: "default", defaultEnabled: true, riskLevel: "command", requiresUI: false, startsTimers: false, writesWorkspace: false, externalProcess: true, resourceDiscovery: true, testContracts: ["external-process", "resource-discovery"], testFiles: ["test/resource-discovery-contract.test.ts"] },
57
+ { id: "simplify", category: "optional", defaultEnabled: false, riskLevel: "write-capable", requiresUI: false, startsTimers: false, writesWorkspace: true, externalProcess: true, testContracts: ["external-process", "write-guard"], testFiles: ["test/simplify-extension.test.ts"] },
58
+ { id: "export-html", category: "optional", defaultEnabled: false, riskLevel: "write-capable", requiresUI: false, startsTimers: false, writesWorkspace: true, externalProcess: false, testContracts: ["write-guard"], testFiles: ["test/extension-smoke.test.ts", "test/export-html-branch-navigation.test.ts"] },
59
59
  ];
60
60
  /** Find package root from current module location (containing package.json with nano-pencil related name) */
61
61
  function findPackageRoot(startDir) {
@@ -5,6 +5,16 @@
5
5
  * [HERE]: core/i18n/slash-commands.ts - English slash command translations
6
6
  */
7
7
  export declare const slashCommands: {
8
+ categories: {
9
+ core: string;
10
+ model: string;
11
+ memory: string;
12
+ session: string;
13
+ workflow: string;
14
+ agents: string;
15
+ tools: string;
16
+ admin: string;
17
+ };
8
18
  settings: string;
9
19
  model: string;
10
20
  "agent-loop": string;
@@ -24,6 +34,7 @@ export declare const slashCommands: {
24
34
  usage: string;
25
35
  changelog: string;
26
36
  hotkeys: string;
37
+ resources: string;
27
38
  fork: string;
28
39
  tree: string;
29
40
  login: string;
@@ -5,16 +5,26 @@
5
5
  * [HERE]: core/i18n/slash-commands.ts - English slash command translations
6
6
  */
7
7
  export const slashCommands = {
8
+ categories: {
9
+ core: "Core",
10
+ model: "Models",
11
+ memory: "Memory",
12
+ session: "Sessions",
13
+ workflow: "Workflows",
14
+ agents: "Agents",
15
+ tools: "Tools",
16
+ admin: "Admin",
17
+ },
8
18
  settings: "Open settings menu",
9
19
  model: "Select model (opens selector UI)",
10
- "agent-loop": "Set standard or weak-model-compatible agent loop for this session",
11
- "scoped-models": "Enable/disable models for Ctrl+P cycling",
20
+ "agent-loop": "Choose how the agent keeps working through a task",
21
+ "scoped-models": "Choose which models appear in quick switching",
12
22
  apikey: "Update API key for current provider",
13
23
  mcp: "Manage MCP servers (list, enable, disable)",
14
24
  soul: "Show AI personality and stats (Soul)",
15
25
  persona: "Switch AI persona/personality pack",
16
26
  memory: "Show project memory and knowledge (NanoMem)",
17
- dream: "Consolidate project memory (NanoMem)",
27
+ dream: "Refresh long-term project memory (NanoMem)",
18
28
  export: "Export session to HTML file",
19
29
  share: "Share session as a secret GitHub gist",
20
30
  copy: "Copy last agent message to clipboard",
@@ -24,6 +34,7 @@ export const slashCommands = {
24
34
  usage: "Show token usage and cost stats",
25
35
  changelog: "Show changelog entries",
26
36
  hotkeys: "Show all keyboard shortcuts",
37
+ resources: "Show loaded extensions, prompts, skills, and themes",
27
38
  fork: "Create a new fork from a previous message",
28
39
  tree: "Navigate session tree (switch branches)",
29
40
  login: "Login with OAuth provider",
@@ -34,7 +45,7 @@ export const slashCommands = {
34
45
  compact: "Manually compact the session context",
35
46
  resume: "Resume a different session",
36
47
  reload: "Reload extensions, skills, prompts, and themes",
37
- "link-world": "Install link-world for internet access (Twitter, YouTube, etc.)",
48
+ "link-world": "Set up internet access tools",
38
49
  quit: "Quit NanoPencil",
39
50
  language: "Switch language (English/Chinese)",
40
51
  };
@@ -5,6 +5,16 @@
5
5
  * [HERE]: core/i18n/slash-commands.zh.ts - Chinese slash command translations
6
6
  */
7
7
  export declare const slashCommands: {
8
+ categories: {
9
+ core: string;
10
+ model: string;
11
+ memory: string;
12
+ session: string;
13
+ workflow: string;
14
+ agents: string;
15
+ tools: string;
16
+ admin: string;
17
+ };
8
18
  settings: string;
9
19
  model: string;
10
20
  "agent-loop": string;
@@ -24,6 +34,7 @@ export declare const slashCommands: {
24
34
  usage: string;
25
35
  changelog: string;
26
36
  hotkeys: string;
37
+ resources: string;
27
38
  fork: string;
28
39
  tree: string;
29
40
  login: string;
@@ -5,16 +5,26 @@
5
5
  * [HERE]: core/i18n/slash-commands.zh.ts - Chinese slash command translations
6
6
  */
7
7
  export const slashCommands = {
8
+ categories: {
9
+ core: "核心",
10
+ model: "模型",
11
+ memory: "记忆",
12
+ session: "会话",
13
+ workflow: "工作流",
14
+ agents: "Agent",
15
+ tools: "工具",
16
+ admin: "管理",
17
+ },
8
18
  settings: "打开设置菜单",
9
19
  model: "选择模型(打开选择器界面)",
10
- "agent-loop": "设置本会话的 standard/弱模型兼容 agent loop",
11
- "scoped-models": "启用/禁用 Ctrl+P 循环的模型",
20
+ "agent-loop": "选择 agent 推进任务的方式",
21
+ "scoped-models": "选择快速切换里出现的模型",
12
22
  apikey: "更新当前提供商的 API 密钥",
13
23
  mcp: "管理 MCP 服务器(列出、启用、禁用)",
14
24
  soul: "显示 AI 人格和统计(灵魂)",
15
25
  persona: "切换 AI 人格/个性包",
16
26
  memory: "显示项目记忆和知识(纳米记忆)",
17
- dream: "整合项目记忆(纳米记忆)",
27
+ dream: "刷新长期项目记忆(纳米记忆)",
18
28
  export: "将会话导出为 HTML 文件",
19
29
  share: "将会话分享为保密的 GitHub gist",
20
30
  copy: "复制上一条 AI 消息到剪贴板",
@@ -24,6 +34,7 @@ export const slashCommands = {
24
34
  usage: "显示 token 使用量和费用统计",
25
35
  changelog: "显示更新日志条目",
26
36
  hotkeys: "显示所有键盘快捷键",
37
+ resources: "显示已加载的扩展、提示、技能和主题",
27
38
  fork: "从上一条消息创建新分支",
28
39
  tree: "导航会话树(切换分支)",
29
40
  login: "通过 OAuth 提供商登录",
@@ -34,7 +45,7 @@ export const slashCommands = {
34
45
  compact: "手动压缩会话上下文",
35
46
  resume: "恢复其他会话",
36
47
  reload: "重新加载扩展、技能、提示和主题",
37
- "link-world": "安装 link-world 以获得互联网访问(Twitter、YouTube 等)",
48
+ "link-world": "设置联网访问工具",
38
49
  quit: "退出 NanoPencil",
39
50
  language: "切换语言(English/中文)",
40
51
  };
@@ -7,11 +7,12 @@
7
7
  import type { ResourceLoader } from "../config/resource-loader.js";
8
8
  import type { ExtensionRunner } from "../extensions/index.js";
9
9
  import type { PromptTemplate } from "../prompt/prompt-templates.js";
10
- import { getLocalizedCommands, type SlashCommandInfo } from "../slash-commands.js";
10
+ import { getLocalizedCommands, type SlashCommandInfo, type SlashCommandCategory } from "../slash-commands.js";
11
11
  export interface SessionSlashCommandDescriptor {
12
12
  name: string;
13
13
  description?: string;
14
14
  source: "builtin" | SlashCommandInfo["source"];
15
+ category?: SlashCommandCategory;
15
16
  }
16
17
  type Translate = Parameters<typeof getLocalizedCommands>[0];
17
18
  export interface SlashCommandCatalogSource {
@@ -1,4 +1,4 @@
1
- import { BUILTIN_SLASH_COMMANDS, getLocalizedCommands, } from "../slash-commands.js";
1
+ import { BUILTIN_SLASH_COMMANDS, getLocalizedCommands, inferSlashCommandCategory, } from "../slash-commands.js";
2
2
  function normalizeLocation(source) {
3
3
  if (source === "user" || source === "project" || source === "path") {
4
4
  return source;
@@ -16,6 +16,7 @@ function getExtensionCommands(runner, reservedBuiltins) {
16
16
  name: command.name,
17
17
  description: command.description,
18
18
  path: extensionPath,
19
+ category: inferSlashCommandCategory(command.name, "extension"),
19
20
  })) ?? []);
20
21
  }
21
22
  export function buildSessionSlashCommands(source, translate) {
@@ -23,22 +24,26 @@ export function buildSessionSlashCommands(source, translate) {
23
24
  name: command.name,
24
25
  description: command.description,
25
26
  source: "builtin",
27
+ category: command.category,
26
28
  }));
27
29
  const reservedBuiltins = getReservedBuiltinNames();
28
30
  const extensionCommands = getExtensionCommands(source.extensionRunner, reservedBuiltins).map((command) => ({
29
31
  name: command.name,
30
32
  description: command.description,
31
33
  source: "extension",
34
+ category: command.category,
32
35
  }));
33
36
  const promptCommands = source.promptTemplates.map((template) => ({
34
37
  name: template.name,
35
38
  description: template.description,
36
39
  source: "prompt",
40
+ category: inferSlashCommandCategory(template.name, "prompt"),
37
41
  }));
38
42
  const skillCommands = source.resourceLoader.getSkills().skills.map((skill) => ({
39
43
  name: `skill:${skill.name}`,
40
44
  description: skill.description,
41
45
  source: "skill",
46
+ category: inferSlashCommandCategory(skill.name, "skill"),
42
47
  }));
43
48
  return [
44
49
  ...builtins,
@@ -53,12 +58,14 @@ export function buildExtensionSlashCommands(source) {
53
58
  name: command.name,
54
59
  description: command.description,
55
60
  source: "extension",
61
+ category: command.category,
56
62
  path: command.path,
57
63
  }));
58
64
  const templates = source.promptTemplates.map((template) => ({
59
65
  name: template.name,
60
66
  description: template.description,
61
67
  source: "prompt",
68
+ category: inferSlashCommandCategory(template.name, "prompt"),
62
69
  location: normalizeLocation(template.source),
63
70
  path: template.filePath,
64
71
  }));
@@ -68,6 +75,7 @@ export function buildExtensionSlashCommands(source) {
68
75
  name: `skill:${skill.name}`,
69
76
  description: skill.description,
70
77
  source: "skill",
78
+ category: inferSlashCommandCategory(skill.name, "skill"),
71
79
  location: normalizeLocation(skill.source),
72
80
  path: skill.filePath,
73
81
  }));
@@ -1,25 +1,33 @@
1
1
  /**
2
- * [WHO]: SlashCommandInfo, BuiltinSlashCommand, slashCommand definitions, getLocalizedCommands()
2
+ * [WHO]: SlashCommandInfo, BuiltinSlashCommand, slashCommand definitions, category helpers, getLocalizedCommands()
3
3
  * [FROM]: No external dependencies
4
4
  * [TO]: Consumed by modes/interactive/interactive-mode.ts, modes/acp/acp-mode.ts
5
5
  * [HERE]: core/slash-commands.ts - slash command types and registry
6
6
  */
7
7
  export type SlashCommandSource = "extension" | "prompt" | "skill";
8
8
  export type SlashCommandLocation = "user" | "project" | "path";
9
+ export type SlashCommandCategory = "core" | "model" | "memory" | "session" | "workflow" | "agents" | "tools" | "admin";
9
10
  export interface SlashCommandInfo {
10
11
  name: string;
11
12
  description?: string;
12
13
  source: SlashCommandSource;
14
+ category?: SlashCommandCategory;
13
15
  location?: SlashCommandLocation;
14
16
  path?: string;
15
17
  }
16
18
  export interface BuiltinSlashCommand {
17
19
  name: string;
18
20
  descriptionKey: string;
21
+ category: SlashCommandCategory;
19
22
  }
20
23
  export declare const BUILTIN_SLASH_COMMANDS: ReadonlyArray<BuiltinSlashCommand>;
24
+ export declare function inferSlashCommandCategory(name: string, source?: SlashCommandSource): SlashCommandCategory;
25
+ export declare function getSlashCommandCategoryLabel(category: SlashCommandCategory, t: (key: string) => string): string;
26
+ export declare function formatSlashCommandDescription(description: string | undefined, category: SlashCommandCategory | undefined, t: (key: string) => string): string | undefined;
21
27
  export interface LocalizedSlashCommand {
22
28
  name: string;
23
29
  description: string;
30
+ category: SlashCommandCategory;
31
+ categoryLabel: string;
24
32
  }
25
33
  export declare function getLocalizedCommands(t: (key: string) => string): LocalizedSlashCommand[];
@@ -1,40 +1,85 @@
1
1
  export const BUILTIN_SLASH_COMMANDS = [
2
- { name: "settings", descriptionKey: "slash.settings" },
3
- { name: "model", descriptionKey: "slash.model" },
4
- { name: "agent-loop", descriptionKey: "slash.agent-loop" },
5
- { name: "scoped-models", descriptionKey: "slash.scoped-models" },
6
- { name: "apikey", descriptionKey: "slash.apikey" },
7
- { name: "mcp", descriptionKey: "slash.mcp" },
8
- { name: "soul", descriptionKey: "slash.soul" },
9
- { name: "persona", descriptionKey: "slash.persona" },
10
- { name: "memory", descriptionKey: "slash.memory" },
11
- { name: "dream", descriptionKey: "slash.dream" },
12
- { name: "export", descriptionKey: "slash.export" },
13
- { name: "share", descriptionKey: "slash.share" },
14
- { name: "copy", descriptionKey: "slash.copy" },
15
- { name: "name", descriptionKey: "slash.name" },
16
- { name: "session", descriptionKey: "slash.session" },
17
- { name: "status", descriptionKey: "slash.status" },
18
- { name: "usage", descriptionKey: "slash.usage" },
19
- { name: "changelog", descriptionKey: "slash.changelog" },
20
- { name: "hotkeys", descriptionKey: "slash.hotkeys" },
21
- { name: "fork", descriptionKey: "slash.fork" },
22
- { name: "tree", descriptionKey: "slash.tree" },
23
- { name: "login", descriptionKey: "slash.login" },
24
- { name: "logout", descriptionKey: "slash.logout" },
25
- { name: "new", descriptionKey: "slash.new" },
26
- { name: "update", descriptionKey: "slash.update" },
27
- { name: "reinstall", descriptionKey: "slash.reinstall" },
28
- { name: "compact", descriptionKey: "slash.compact" },
29
- { name: "resume", descriptionKey: "slash.resume" },
30
- { name: "reload", descriptionKey: "slash.reload" },
31
- { name: "link-world", descriptionKey: "slash.link-world" },
32
- { name: "language", descriptionKey: "slash.language" },
33
- { name: "quit", descriptionKey: "slash.quit" },
2
+ { name: "settings", descriptionKey: "slash.settings", category: "core" },
3
+ { name: "model", descriptionKey: "slash.model", category: "model" },
4
+ { name: "agent-loop", descriptionKey: "slash.agent-loop", category: "model" },
5
+ { name: "scoped-models", descriptionKey: "slash.scoped-models", category: "model" },
6
+ { name: "apikey", descriptionKey: "slash.apikey", category: "model" },
7
+ { name: "mcp", descriptionKey: "slash.mcp", category: "tools" },
8
+ { name: "soul", descriptionKey: "slash.soul", category: "memory" },
9
+ { name: "persona", descriptionKey: "slash.persona", category: "core" },
10
+ { name: "memory", descriptionKey: "slash.memory", category: "memory" },
11
+ { name: "dream", descriptionKey: "slash.dream", category: "memory" },
12
+ { name: "export", descriptionKey: "slash.export", category: "tools" },
13
+ { name: "share", descriptionKey: "slash.share", category: "tools" },
14
+ { name: "copy", descriptionKey: "slash.copy", category: "core" },
15
+ { name: "name", descriptionKey: "slash.name", category: "session" },
16
+ { name: "session", descriptionKey: "slash.session", category: "session" },
17
+ { name: "status", descriptionKey: "slash.status", category: "core" },
18
+ { name: "usage", descriptionKey: "slash.usage", category: "core" },
19
+ { name: "changelog", descriptionKey: "slash.changelog", category: "core" },
20
+ { name: "hotkeys", descriptionKey: "slash.hotkeys", category: "core" },
21
+ { name: "resources", descriptionKey: "slash.resources", category: "core" },
22
+ { name: "fork", descriptionKey: "slash.fork", category: "session" },
23
+ { name: "tree", descriptionKey: "slash.tree", category: "session" },
24
+ { name: "login", descriptionKey: "slash.login", category: "model" },
25
+ { name: "logout", descriptionKey: "slash.logout", category: "model" },
26
+ { name: "new", descriptionKey: "slash.new", category: "session" },
27
+ { name: "update", descriptionKey: "slash.update", category: "admin" },
28
+ { name: "reinstall", descriptionKey: "slash.reinstall", category: "admin" },
29
+ { name: "compact", descriptionKey: "slash.compact", category: "session" },
30
+ { name: "resume", descriptionKey: "slash.resume", category: "session" },
31
+ { name: "reload", descriptionKey: "slash.reload", category: "admin" },
32
+ { name: "link-world", descriptionKey: "slash.link-world", category: "tools" },
33
+ { name: "language", descriptionKey: "slash.language", category: "core" },
34
+ { name: "quit", descriptionKey: "slash.quit", category: "core" },
34
35
  ];
36
+ export function inferSlashCommandCategory(name, source) {
37
+ if (source === "prompt")
38
+ return "workflow";
39
+ if (source === "skill")
40
+ return "tools";
41
+ if (name === "dream" || name === "memory" || name.startsWith("mem-"))
42
+ return "memory";
43
+ if (name === "team" || name.startsWith("team:") || name === "subagent" || name.startsWith("subagent:"))
44
+ return "agents";
45
+ if (name === "grub" ||
46
+ name === "loop" ||
47
+ name === "plan" ||
48
+ name.startsWith("plan:") ||
49
+ name === "recap" ||
50
+ name === "btw" ||
51
+ name === "interview" ||
52
+ name === "grill-me" ||
53
+ name === "simplify") {
54
+ return "workflow";
55
+ }
56
+ if (name === "browser" || name === "figma" || name === "link-world" || name === "export")
57
+ return "tools";
58
+ if (name === "debug" ||
59
+ name === "set-locale" ||
60
+ name === "report-issue" ||
61
+ name === "tokensave" ||
62
+ name === "security" ||
63
+ name.startsWith("security-") ||
64
+ name.startsWith("sal:")) {
65
+ return "admin";
66
+ }
67
+ return "tools";
68
+ }
69
+ export function getSlashCommandCategoryLabel(category, t) {
70
+ return t(`slash.categories.${category}`);
71
+ }
72
+ export function formatSlashCommandDescription(description, category, t) {
73
+ if (!description || !category)
74
+ return description;
75
+ const label = getSlashCommandCategoryLabel(category, t);
76
+ return label && !label.startsWith("slash.") ? `${label} · ${description}` : description;
77
+ }
35
78
  export function getLocalizedCommands(t) {
36
79
  return BUILTIN_SLASH_COMMANDS.map((cmd) => ({
37
80
  name: cmd.name,
38
81
  description: t(cmd.descriptionKey),
82
+ category: cmd.category,
83
+ categoryLabel: getSlashCommandCategoryLabel(cmd.category, t),
39
84
  }));
40
85
  }
@@ -328,7 +328,7 @@ export default function browserExtension(api) {
328
328
  ensureBrowserWorkspace();
329
329
  });
330
330
  api.registerCommand("browser", {
331
- description: "Browser Harness setup, status, reload, and workspace information",
331
+ description: "Set up or inspect browser automation tools",
332
332
  getArgumentCompletions: (argumentPrefix) => {
333
333
  const prefix = argumentPrefix.trim();
334
334
  const values = ["install", "status", "setup", "reload", "workspace", "help"]
@@ -5,7 +5,7 @@
5
5
  * [HERE]: extensions/defaults/debug/index.ts - system diagnostics with three-layer analysis through full agent loop
6
6
  */
7
7
  import { Box, Container, Spacer, Text } from "@pencil-agent/tui";
8
- import { collectSystemInfo, collectModelInfo, collectSessionInfo, collectConfigInfo, collectGitInfo, collectAgentState, sanitizeForLLM, formatDiagnosticData, } from "./collectors.js";
8
+ import { collectSystemInfo, collectModelInfo, collectSessionInfo, collectConfigInfo, collectGitInfo, collectAgentState, collectPreferencesInfo, sanitizeForLLM, formatDiagnosticData, } from "./collectors.js";
9
9
  const DEBUG_MESSAGE_TYPE = "debug";
10
10
  const DEBUG_PROMPT_PREFIX = "[DEBUG:";
11
11
  const DEBUG_TAG = "[DEBUG]";
@@ -55,6 +55,8 @@ function parseDebugArgs(args) {
55
55
  return { subcommand: "session" };
56
56
  if (trimmed === "model")
57
57
  return { subcommand: "model" };
58
+ if (trimmed === "preferences")
59
+ return { subcommand: "preferences" };
58
60
  return { subcommand: "full", issueDescription: args.trim() || undefined };
59
61
  }
60
62
  // ============================================================================
@@ -132,6 +134,15 @@ async function handleQuickSub(subcommand, ctx, api) {
132
134
  : `> Collection failed: ${info.error}`;
133
135
  break;
134
136
  }
137
+ case "preferences": {
138
+ const info = await collectPreferencesInfo(ctx);
139
+ result = info.data
140
+ ? `| Preferences | |\n|---|---|\n${Object.entries(info.data)
141
+ .map(([k, v]) => `| ${k} | ${typeof v === "string" ? v : JSON.stringify(v)} |`)
142
+ .join("\n")}`
143
+ : `> Collection failed: ${info.error}`;
144
+ break;
145
+ }
135
146
  }
136
147
  api.sendMessage({
137
148
  customType: DEBUG_MESSAGE_TYPE,
@@ -170,7 +181,7 @@ export default async function debugExtension(api) {
170
181
  return container;
171
182
  });
172
183
  api.on("before_agent_start", (event) => {
173
- if (!isDebugPrompt(event.prompt))
184
+ if (!isDebugPrompt(event.prompt) || event.prompt !== pendingDiagnosticPrompt)
174
185
  return;
175
186
  return { appendSystemPrompt: DEBUG_SYSTEM_PROMPT };
176
187
  });
@@ -180,12 +191,26 @@ export default async function debugExtension(api) {
180
191
  }
181
192
  });
182
193
  api.registerCommand("debug", {
183
- description: "Run system diagnostics (/debug [env|session|model|preferences|<issue>])",
194
+ description: "Check NanoPencil health or investigate an issue. Usage: /debug [env|session|model|preferences|<issue>]",
195
+ getArgumentCompletions: (argumentPrefix) => {
196
+ const prefix = argumentPrefix.trim().toLowerCase();
197
+ const values = ["env", "session", "model", "preferences"]
198
+ .filter((value) => value.startsWith(prefix))
199
+ .map((value) => ({ value, label: value }));
200
+ return values.length > 0 ? values : null;
201
+ },
184
202
  handler: (args, ctx) => handleDebugCommand(args, ctx, api),
185
203
  });
186
204
  // Register /set-locale command
187
205
  api.registerCommand("set-locale", {
188
206
  description: "Set language preference (/set-locale zh|en)",
207
+ getArgumentCompletions: (argumentPrefix) => {
208
+ const prefix = argumentPrefix.trim().toLowerCase();
209
+ const values = ["zh", "en"]
210
+ .filter((value) => value.startsWith(prefix))
211
+ .map((value) => ({ value, label: value }));
212
+ return values.length > 0 ? values : null;
213
+ },
189
214
  handler: async (args, ctx) => {
190
215
  const trimmed = args.trim().toLowerCase();
191
216
  if (trimmed !== "zh" && trimmed !== "en") {
@@ -325,7 +325,7 @@ export default function linkWorldExtension(api) {
325
325
  ensureLinkWorldWorkspace();
326
326
  });
327
327
  api.registerCommand("link-world", {
328
- description: "Inspect, install, or troubleshoot the built-in internet access integration",
328
+ description: "Set up or inspect internet access tools",
329
329
  getArgumentCompletions: (argumentPrefix) => {
330
330
  const prefix = argumentPrefix.trim();
331
331
  const values = ["status", "doctor", "version", "install", "workspace", "help"]
@@ -233,12 +233,30 @@ export function createCronScheduler(options) {
233
233
  }, WATCH_INTERVAL_MS);
234
234
  }
235
235
  async function enable() {
236
+ if (isKilled())
237
+ return;
236
238
  enabled = true;
237
239
  if (dir) {
238
240
  await acquireLock();
241
+ if (isKilled()) {
242
+ await releaseLock();
243
+ return;
244
+ }
239
245
  await loadTasks();
246
+ if (isKilled()) {
247
+ await releaseLock();
248
+ return;
249
+ }
240
250
  startWatching();
241
251
  }
252
+ if (isKilled()) {
253
+ if (watchIntervalId) {
254
+ clearInterval(watchIntervalId);
255
+ watchIntervalId = null;
256
+ }
257
+ await releaseLock();
258
+ return;
259
+ }
242
260
  intervalId = setInterval(check, TICK_MS);
243
261
  console.log("[Cron-Scheduler] Scheduler enabled and ticking");
244
262
  }
@@ -266,6 +284,7 @@ export function createCronScheduler(options) {
266
284
  },
267
285
  stop() {
268
286
  killed = true;
287
+ enabled = false;
269
288
  if (intervalId) {
270
289
  clearInterval(intervalId);
271
290
  intervalId = null;
@@ -99,7 +99,7 @@ function auditAndGateToolCall(api, event, ctx) {
99
99
  export default function securityAuditExtension(api) {
100
100
  // /security - Show security dashboard
101
101
  api.registerCommand("security", {
102
- description: "Show security audit dashboard and logs",
102
+ description: "Show recent security warnings and blocked actions",
103
103
  handler: async (_args, ctx) => {
104
104
  const logger = getLogger(ctx);
105
105
  const stats = logger.getStats();
@@ -133,7 +133,7 @@ export default function securityAuditExtension(api) {
133
133
  });
134
134
  // /security-logs - Show detailed logs
135
135
  api.registerCommand("security-logs", {
136
- description: "Show detailed security audit logs",
136
+ description: "Show detailed security event history",
137
137
  handler: async (args, ctx) => {
138
138
  const logger = getLogger(ctx);
139
139
  const limit = parseInt(args) || 50;
@@ -158,7 +158,7 @@ export default function securityAuditExtension(api) {
158
158
  });
159
159
  // /security-stats - Show statistics
160
160
  api.registerCommand("security-stats", {
161
- description: "Show security audit statistics",
161
+ description: "Show security event counts",
162
162
  handler: async (_args, ctx) => {
163
163
  const logger = getLogger(ctx);
164
164
  const stats = logger.getStats();
@@ -189,7 +189,7 @@ export default function securityAuditExtension(api) {
189
189
  });
190
190
  // /security-clear - Clear logs
191
191
  api.registerCommand("security-clear", {
192
- description: "Clear security audit logs",
192
+ description: "Clear saved security events",
193
193
  handler: async (_args, ctx) => {
194
194
  const logger = getLogger(ctx);
195
195
  logger.clear();