@creative-dswork/dscode 0.2.0 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/dscode.mjs CHANGED
@@ -601,6 +601,123 @@ var init_theme = __esm({
601
601
  }
602
602
  });
603
603
 
604
+ // src/permissions/fuzzy.ts
605
+ function deriveFuzzyPattern(toolName) {
606
+ if (!toolName) return null;
607
+ const match = toolName.match(/^(mcp__[^_]+(?:_[^_]+)*?)__/);
608
+ if (match) {
609
+ return match[1] + "__*";
610
+ }
611
+ return toolName;
612
+ }
613
+ function deriveFuzzyArgPattern(toolName, args) {
614
+ if (args == null) return null;
615
+ if (toolName === "bash") {
616
+ const cmd = typeof args === "string" ? args : JSON.stringify(args);
617
+ const firstWord = cmd.match(/^"?(\w+)/);
618
+ if (firstWord) {
619
+ return `^"${firstWord[1]}`;
620
+ }
621
+ return null;
622
+ }
623
+ if (toolName === "read_file" || toolName === "write_file" || toolName === "edit" || toolName === "grep" || toolName === "glob" || toolName === "list_files") {
624
+ const a = args;
625
+ const path = typeof a.path === "string" ? a.path : null;
626
+ if (!path) return null;
627
+ const lastSlash = path.lastIndexOf("/");
628
+ const dir = lastSlash >= 0 ? path.slice(0, lastSlash + 1) : "";
629
+ if (!dir) return null;
630
+ const escaped = dir.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
631
+ return `${escaped}[^"]*`;
632
+ }
633
+ return null;
634
+ }
635
+ function describeFuzzyArgPattern(toolName, args) {
636
+ if (toolName === "bash") {
637
+ const cmd = typeof args === "string" ? args : "";
638
+ const firstWord = cmd.match(/^"?(\w+)/);
639
+ return firstWord ? `${firstWord[1]}.*` : null;
640
+ }
641
+ if (toolName === "read_file" || toolName === "write_file" || toolName === "edit" || toolName === "grep" || toolName === "glob" || toolName === "list_files") {
642
+ const a = args;
643
+ const path = typeof a.path === "string" ? a.path : null;
644
+ if (!path) return null;
645
+ const lastSlash = path.lastIndexOf("/");
646
+ const dir = lastSlash >= 0 ? path.slice(0, lastSlash + 1) : "";
647
+ return dir ? `${dir}*` : null;
648
+ }
649
+ return null;
650
+ }
651
+ var init_fuzzy = __esm({
652
+ "src/permissions/fuzzy.ts"() {
653
+ "use strict";
654
+ }
655
+ });
656
+
657
+ // src/permissions/fuzzy-llm.ts
658
+ import { complete } from "@mariozechner/pi-ai";
659
+ function cacheKey(toolName, args) {
660
+ const argsStr = typeof args === "string" ? args.slice(0, 80) : JSON.stringify(args).slice(0, 80);
661
+ return `${toolName}:${argsStr}`;
662
+ }
663
+ function prefetchLlmSuggestions(model, toolName, args, preview) {
664
+ const key = cacheKey(toolName, args);
665
+ if (suggestionCache.has(key)) return;
666
+ if (!model) return;
667
+ const prompt = buildPrompt(toolName, args, preview);
668
+ complete(model, {
669
+ systemPrompt: "Respond with JSON only. No explanation.",
670
+ messages: [{ role: "user", content: prompt, timestamp: Date.now() }]
671
+ }).then((msg) => {
672
+ const suggestions = parseSuggestions(msg);
673
+ if (suggestions.length > 0) {
674
+ suggestionCache.set(key, suggestions);
675
+ }
676
+ }).catch(() => {
677
+ });
678
+ }
679
+ function getLlmSuggestions(toolName, args) {
680
+ return suggestionCache.get(cacheKey(toolName, args)) ?? [];
681
+ }
682
+ function buildPrompt(toolName, args, preview) {
683
+ return `The user is granting permission for this tool call. Suggest 1-2 fuzzy patterns that would match similar future calls.
684
+
685
+ Tool: ${toolName}
686
+ Args: ${JSON.stringify(args)}
687
+ Preview: ${preview}
688
+
689
+ Respond with ONLY a JSON array. Each item: {"label":"<human description>","toolPattern":"<glob or null>","argPattern":"<regex or null>"}.
690
+ Example for "bash" with "git push origin main":
691
+ [{"label":"All git push","toolPattern":null,"argPattern":"^\\"git push"},{"label":"All git commands","toolPattern":null,"argPattern":"^\\"git"}]
692
+
693
+ Example for "read_file" with path "/src/components/Button.tsx":
694
+ [{"label":"All .tsx in components","toolPattern":null,"argPattern":"/src/components/[^\\"]*\\\\.tsx"},{"label":"All files in components","toolPattern":null,"argPattern":"/src/components/"}]
695
+
696
+ Rules:
697
+ - toolPattern: null means same tool. Use "mcp__server__*" only for MCP tools.
698
+ - argPattern: regex matching JSON-stringified args. null means no arg restriction.
699
+ - Max 2 suggestions. Prefer specific over broad.`;
700
+ }
701
+ function parseSuggestions(msg) {
702
+ try {
703
+ const text = typeof msg.content === "string" ? msg.content : Array.isArray(msg.content) ? msg.content.filter((b) => b.type === "text").map((b) => b.text).join("") : "";
704
+ const match = text.match(/\[[\s\S]*\]/);
705
+ if (!match) return [];
706
+ const parsed = JSON.parse(match[0]);
707
+ if (!Array.isArray(parsed)) return [];
708
+ return parsed.filter((s) => typeof s.label === "string" && s.label.length > 0 && s.label.length < 60).slice(0, 2);
709
+ } catch {
710
+ return [];
711
+ }
712
+ }
713
+ var suggestionCache;
714
+ var init_fuzzy_llm = __esm({
715
+ "src/permissions/fuzzy-llm.ts"() {
716
+ "use strict";
717
+ suggestionCache = /* @__PURE__ */ new Map();
718
+ }
719
+ });
720
+
604
721
  // src/drivers/vision/reader.ts
605
722
  import { readFileSync as readFileSync14 } from "node:fs";
606
723
  import { exec, execSync as execSync3 } from "node:child_process";
@@ -3461,6 +3578,8 @@ var init_web_backend = __esm({
3461
3578
  init_models();
3462
3579
  init_config();
3463
3580
  init_commands();
3581
+ init_fuzzy();
3582
+ init_fuzzy_llm();
3464
3583
  init_mcp_browser();
3465
3584
  init_at_file_resolver();
3466
3585
  init_display();
@@ -3480,6 +3599,7 @@ var init_web_backend = __esm({
3480
3599
  // Pending permission state
3481
3600
  permissionResolve = null;
3482
3601
  currentPermissionTool = "";
3602
+ currentPermissionArgs = null;
3483
3603
  // Image state
3484
3604
  pendingImages = [];
3485
3605
  // Message accumulation for the current assistant turn
@@ -3602,11 +3722,17 @@ var init_web_backend = __esm({
3602
3722
  }
3603
3723
  // ── UiBackend Permission ──
3604
3724
  getPromptPermission() {
3605
- return (toolName, preview, _args) => {
3725
+ return (toolName, preview, args) => {
3606
3726
  return new Promise((resolve9) => {
3607
3727
  this.currentPermissionTool = toolName;
3728
+ this.currentPermissionArgs = args;
3608
3729
  this.permissionResolve = resolve9;
3609
- this.broadcast({ type: "permission_prompt", toolName, preview });
3730
+ const fuzzy = deriveFuzzyPattern(toolName);
3731
+ const fuzzyArgDesc = describeFuzzyArgPattern(toolName, args);
3732
+ const llmSuggestions = getLlmSuggestions(toolName, args);
3733
+ this.broadcast({ type: "permission_prompt", toolName, preview, fuzzyPattern: fuzzy, fuzzyArgDesc, llmSuggestions: llmSuggestions.length > 0 ? llmSuggestions : void 0 });
3734
+ const model = resolveModel(this.config.provider, this.config.modelId);
3735
+ prefetchLlmSuggestions(model, toolName, args, preview);
3610
3736
  });
3611
3737
  };
3612
3738
  }
@@ -3760,10 +3886,34 @@ var init_web_backend = __esm({
3760
3886
  this.permissionResolve = null;
3761
3887
  const isAlways = cmd.decision === "always_allow" || cmd.decision === "always_allow_save";
3762
3888
  const isSave = cmd.decision === "always_allow_save";
3889
+ const isSessionGrant = cmd.decision === "always_allow" && !isSave || cmd.decision === "allow" && !!cmd.sessionGrantPattern;
3890
+ const sessionPattern = cmd.sessionGrantPattern;
3763
3891
  resolve9({
3764
3892
  decision: isAlways ? "allow" : cmd.decision,
3765
3893
  rememberForSession: isAlways,
3766
- persistRule: isSave ? { tool: this.currentPermissionTool, decision: "allow" } : void 0
3894
+ sessionGrantPattern: isSessionGrant ? sessionPattern : void 0,
3895
+ persistRule: isSave ? (() => {
3896
+ const mode = cmd.fuzzyMode ?? 0;
3897
+ if (mode === 2) {
3898
+ const fuzzyArg = deriveFuzzyArgPattern(this.currentPermissionTool, this.currentPermissionArgs);
3899
+ return fuzzyArg ? { tool: this.currentPermissionTool, argPattern: fuzzyArg, decision: "allow" } : { tool: this.currentPermissionTool, decision: "allow" };
3900
+ }
3901
+ if (mode >= 3) {
3902
+ try {
3903
+ const p = JSON.parse(cmd.toolNamePattern || "{}");
3904
+ return {
3905
+ tool: p.toolPattern ?? this.currentPermissionTool,
3906
+ argPattern: p.argPattern ?? void 0,
3907
+ decision: "allow"
3908
+ };
3909
+ } catch {
3910
+ }
3911
+ }
3912
+ if (mode === 1) {
3913
+ return { tool: cmd.toolNamePattern ?? this.currentPermissionTool, decision: "allow" };
3914
+ }
3915
+ return { tool: this.currentPermissionTool, decision: "allow" };
3916
+ })() : void 0
3767
3917
  });
3768
3918
  }
3769
3919
  break;
@@ -7383,7 +7533,7 @@ var ToolRegistry = class {
7383
7533
  if (this.deferredToolNames.size === 0) return "";
7384
7534
  const grouped = /* @__PURE__ */ new Map();
7385
7535
  for (const name of this.deferredToolNames) {
7386
- const prefix = name.startsWith("mcp_") ? name.split("_").slice(0, 2).join("_") : "other";
7536
+ const prefix = name.startsWith("mcp__") ? name.split("__").slice(0, 2).join("__") : "other";
7387
7537
  if (!grouped.has(prefix)) grouped.set(prefix, []);
7388
7538
  grouped.get(prefix).push(name);
7389
7539
  }
@@ -7834,6 +7984,7 @@ var PermissionManager = class {
7834
7984
  rules;
7835
7985
  denyRegexes;
7836
7986
  sessionGrants = /* @__PURE__ */ new Set();
7987
+ sessionGrantPatterns = [];
7837
7988
  promptUser;
7838
7989
  defaultDecision;
7839
7990
  onBeforePrompt;
@@ -7873,6 +8024,11 @@ var PermissionManager = class {
7873
8024
  if (this.sessionGrants.has(toolName)) {
7874
8025
  return void 0;
7875
8026
  }
8027
+ for (const grant of this.sessionGrantPatterns) {
8028
+ if (this.matchesTool(grant.pattern, toolName)) {
8029
+ return void 0;
8030
+ }
8031
+ }
7876
8032
  const decision = this.evaluate(toolName, argsStr);
7877
8033
  switch (decision.decision) {
7878
8034
  case "allow":
@@ -7895,7 +8051,14 @@ var PermissionManager = class {
7895
8051
  this.rules.sort((a, b) => b.priority - a.priority);
7896
8052
  }
7897
8053
  if (result.rememberForSession) {
7898
- this.sessionGrants.add(toolName);
8054
+ if (result.sessionGrantPattern) {
8055
+ this.sessionGrantPatterns.push({
8056
+ pattern: result.sessionGrantPattern,
8057
+ regex: result.sessionGrantPattern.includes("*") ? this.compileToolPattern(result.sessionGrantPattern) : null
8058
+ });
8059
+ } else {
8060
+ this.sessionGrants.add(toolName);
8061
+ }
7899
8062
  }
7900
8063
  if (result.decision === "deny") {
7901
8064
  return { block: true, reason: result.denyReason ?? "Denied by user" };
@@ -7909,17 +8072,23 @@ var PermissionManager = class {
7909
8072
  }
7910
8073
  revokeGrant(toolName) {
7911
8074
  this.sessionGrants.delete(toolName);
8075
+ this.sessionGrantPatterns = this.sessionGrantPatterns.filter(
8076
+ (g) => g.pattern !== toolName
8077
+ );
7912
8078
  }
7913
8079
  getSessionGrants() {
7914
- return Array.from(this.sessionGrants);
8080
+ const exact = Array.from(this.sessionGrants);
8081
+ const patterns = this.sessionGrantPatterns.map((g) => g.pattern);
8082
+ return [...exact, ...patterns];
7915
8083
  }
7916
- persistRule(rule) {
8084
+ persistRule(rule, toolNamePattern) {
8085
+ const toolName = toolNamePattern ?? rule.tool;
7917
8086
  const settings = loadScopedSettings(projectSettingsPath(this.projectPath));
7918
8087
  const permissions = settings.permissions ?? {};
7919
8088
  if (rule.decision === "allow") {
7920
8089
  const allow = Array.isArray(permissions.allow) ? [...permissions.allow] : [];
7921
- if (!allow.includes(rule.tool)) {
7922
- allow.push(rule.tool);
8090
+ if (!allow.includes(toolName)) {
8091
+ allow.push(toolName);
7923
8092
  }
7924
8093
  saveProjectSettings(this.projectPath, {
7925
8094
  ...settings,
@@ -7930,8 +8099,8 @@ var PermissionManager = class {
7930
8099
  });
7931
8100
  } else if (rule.decision === "deny") {
7932
8101
  const deny = Array.isArray(permissions.deny) ? [...permissions.deny] : [];
7933
- if (!deny.includes(rule.tool)) {
7934
- deny.push(rule.tool);
8102
+ if (!deny.includes(toolName)) {
8103
+ deny.push(toolName);
7935
8104
  }
7936
8105
  saveProjectSettings(this.projectPath, {
7937
8106
  ...settings,
@@ -8939,7 +9108,7 @@ var MCPManager = class {
8939
9108
  for (const def of client.getAllToolDefs()) {
8940
9109
  const visibility = def._meta?.ui?.visibility;
8941
9110
  if (visibility && visibility.length === 1 && visibility[0] === "app") {
8942
- names.push(`mcp_${serverName}_${def.name}`);
9111
+ names.push(`mcp__${serverName}__${def.name}`);
8943
9112
  }
8944
9113
  }
8945
9114
  }
@@ -9023,7 +9192,7 @@ var MCPManager = class {
9023
9192
  this.clients.delete(name);
9024
9193
  }
9025
9194
  if (this.driverRegistry) {
9026
- this.driverRegistry.unregister(`mcp_${name}`);
9195
+ this.driverRegistry.unregister(`mcp__${name}`);
9027
9196
  }
9028
9197
  state.status = "connecting";
9029
9198
  try {
@@ -9109,7 +9278,7 @@ var MCPManager = class {
9109
9278
  }
9110
9279
  this.clients.delete(name);
9111
9280
  if (this.driverRegistry) {
9112
- this.driverRegistry.unregister(`mcp_${name}`);
9281
+ this.driverRegistry.unregister(`mcp__${name}`);
9113
9282
  }
9114
9283
  }
9115
9284
  const cfg = this.configs.find((c2) => c2.name === name);
@@ -9150,7 +9319,7 @@ var MCPManager = class {
9150
9319
  }
9151
9320
  this.clients.delete(name);
9152
9321
  if (this.driverRegistry) {
9153
- this.driverRegistry.unregister(`mcp_${name}`);
9322
+ this.driverRegistry.unregister(`mcp__${name}`);
9154
9323
  }
9155
9324
  state.status = "disconnected";
9156
9325
  state.error = void 0;
@@ -9159,7 +9328,7 @@ var MCPManager = class {
9159
9328
  state.refreshError = void 0;
9160
9329
  }
9161
9330
  buildAgentTool(serverName, def, client) {
9162
- const toolName = `mcp_${serverName}_${def.name}`;
9331
+ const toolName = `mcp__${serverName}__${def.name}`;
9163
9332
  if (def.alwaysLoad) {
9164
9333
  this.alwaysLoadToolNames.add(toolName);
9165
9334
  }
@@ -9239,7 +9408,7 @@ var MCPManager = class {
9239
9408
  const agentTools = tools.map((t) => this.buildAgentTool(serverName, t, client));
9240
9409
  const description = this.configs.find((c2) => c2.name === serverName)?.description ?? "";
9241
9410
  const driver = {
9242
- name: `mcp_${serverName}`,
9411
+ name: `mcp__${serverName}`,
9243
9412
  description: `MCP: ${description || serverName}`,
9244
9413
  tools: agentTools,
9245
9414
  source: "mcp"
@@ -9667,6 +9836,8 @@ function escapeAttr(s) {
9667
9836
  // src/ui/tui-app.ts
9668
9837
  init_models();
9669
9838
  init_theme();
9839
+ init_fuzzy();
9840
+ init_fuzzy_llm();
9670
9841
  import {
9671
9842
  ProcessTerminal,
9672
9843
  TUI,
@@ -9742,8 +9913,13 @@ var ConversationView = class {
9742
9913
  renderedToolCount = 0;
9743
9914
  tui;
9744
9915
  draftBlocks = /* @__PURE__ */ new Map();
9745
- activePermission = null;
9916
+ _activePermission = null;
9746
9917
  permSelected = 0;
9918
+ permSubMode = false;
9919
+ _permSubModeType = "save";
9920
+ permSubSelected = 0;
9921
+ lastSubNavAt = 0;
9922
+ lastCancelSubAt = 0;
9747
9923
  imageTheme = {
9748
9924
  fallbackColor: c.dim
9749
9925
  };
@@ -9761,8 +9937,12 @@ var ConversationView = class {
9761
9937
  this.currentAssistantText = "";
9762
9938
  this.toolEntries = [];
9763
9939
  this.renderedToolCount = 0;
9764
- this.activePermission = null;
9940
+ this._activePermission = null;
9765
9941
  this.permSelected = 0;
9942
+ this.permSubMode = false;
9943
+ this._permSubModeType = "save";
9944
+ this.permSubSelected = 0;
9945
+ this.lastSubNavAt = 0;
9766
9946
  this.draftBlocks.clear();
9767
9947
  while (this.box.children.length > 0) {
9768
9948
  this.box.removeChild(this.box.children[0]);
@@ -9967,9 +10147,12 @@ var ConversationView = class {
9967
10147
  }
9968
10148
  this.render();
9969
10149
  }
9970
- showPermissionPrompt(toolName, preview) {
9971
- this.activePermission = { toolName, preview };
10150
+ showPermissionPrompt(toolName, preview, fuzzyPattern, fuzzyArgDesc) {
10151
+ this._activePermission = { toolName, preview, fuzzyPattern: fuzzyPattern ?? null, fuzzyArgDesc: fuzzyArgDesc ?? null };
9972
10152
  this.permSelected = 0;
10153
+ this.permSubMode = false;
10154
+ this._permSubModeType = "save";
10155
+ this.permSubSelected = 0;
9973
10156
  this.render();
9974
10157
  }
9975
10158
  permNavigate(direction) {
@@ -9977,11 +10160,52 @@ var ConversationView = class {
9977
10160
  this.render();
9978
10161
  }
9979
10162
  permSelect() {
9980
- return this.activePermission ? PERM_OPTIONS[this.permSelected] : null;
10163
+ return this._activePermission ? PERM_OPTIONS[this.permSelected] : null;
10164
+ }
10165
+ isInSubMode() {
10166
+ return this.permSubMode;
10167
+ }
10168
+ get activePermission() {
10169
+ return this._activePermission;
10170
+ }
10171
+ get permSubModeType() {
10172
+ return this._permSubModeType;
10173
+ }
10174
+ enterSubMode(type = "save", _fuzzy) {
10175
+ this.permSubMode = true;
10176
+ this._permSubModeType = type;
10177
+ this.permSubSelected = 0;
10178
+ this.lastSubNavAt = 0;
10179
+ this.render();
10180
+ }
10181
+ cancelSubMode() {
10182
+ this.permSubMode = false;
10183
+ this.permSubSelected = 0;
10184
+ this.lastCancelSubAt = Date.now();
10185
+ this.render();
10186
+ }
10187
+ /** True if sub-mode was cancelled within the last 200ms — used to debounce double-firing Escape. */
10188
+ get justCancelledSubMode() {
10189
+ return Date.now() - this.lastCancelSubAt < 200;
10190
+ }
10191
+ permSubNavigate(direction) {
10192
+ const now = Date.now();
10193
+ if (now - this.lastSubNavAt < 120) return;
10194
+ this.lastSubNavAt = now;
10195
+ const fa = this._activePermission?.fuzzyArgDesc;
10196
+ const llm = this._activePermission?.llmSuggestions;
10197
+ const count = (fa ? 3 : 2) + (llm ? llm.length : 0);
10198
+ this.permSubSelected = (this.permSubSelected + direction + count) % count;
10199
+ this.render();
10200
+ }
10201
+ permSubSelect() {
10202
+ return this.permSubSelected;
9981
10203
  }
9982
10204
  clearPermissionPrompt() {
9983
- this.activePermission = null;
10205
+ this._activePermission = null;
9984
10206
  this.permSelected = 0;
10207
+ this.permSubMode = false;
10208
+ this.permSubSelected = 0;
9985
10209
  }
9986
10210
  pushText(content) {
9987
10211
  this.blocks.push({ type: "text", content });
@@ -9989,12 +10213,15 @@ var ConversationView = class {
9989
10213
  renderPermPrompt() {
9990
10214
  const lines = [];
9991
10215
  const maxLineLen = 60;
9992
- if (!this.activePermission) return lines;
10216
+ if (!this._activePermission) return lines;
10217
+ if (this.permSubMode) {
10218
+ return this.renderPermSubOptions(lines);
10219
+ }
9993
10220
  lines.push("");
9994
10221
  lines.push(c.yellow.bold(" Permissions \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
9995
- lines.push(c.yellow(` Tool: ${this.activePermission.toolName}`));
9996
- if (this.activePermission.preview) {
9997
- for (const pl of this.activePermission.preview.split("\n").slice(0, 6)) {
10222
+ lines.push(c.yellow(` Tool: ${this._activePermission.toolName}`));
10223
+ if (this._activePermission.preview) {
10224
+ for (const pl of this._activePermission.preview.split("\n").slice(0, 6)) {
9998
10225
  lines.push(c.dim(` ${pl.slice(0, maxLineLen)}`));
9999
10226
  }
10000
10227
  }
@@ -10008,7 +10235,64 @@ var ConversationView = class {
10008
10235
  lines.push(`${prefix} ${label} ${hint}`);
10009
10236
  }
10010
10237
  lines.push(c.dim(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
10011
- lines.push(c.dim(" \u2191\u2193 to navigate Enter to confirm A/I shortcuts Esc to deny"));
10238
+ lines.push(c.dim(" \u2191\u2193 to navigate Enter to confirm A/I/S shortcuts Esc to deny"));
10239
+ return lines;
10240
+ }
10241
+ renderPermSubOptions(lines) {
10242
+ const fp = this._activePermission?.fuzzyPattern;
10243
+ const tn = this._activePermission?.toolName ?? "";
10244
+ const fa = this._activePermission?.fuzzyArgDesc;
10245
+ const isMcp = tn.startsWith("mcp__");
10246
+ const isSession = this._permSubModeType === "session" || this._permSubModeType === "allow";
10247
+ if (isSession) {
10248
+ const fuzzyLabel2 = isMcp ? `fuzzy: ${fp}` : `fuzzy: ${tn} (all calls)`;
10249
+ const subOptions2 = [
10250
+ { label: `exact: ${tn}`, key: "1" },
10251
+ { label: fuzzyLabel2, key: "2" }
10252
+ ];
10253
+ lines.push("");
10254
+ lines.push(c.yellow.bold(" Always Allow \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
10255
+ lines.push(c.dim(" Choose exact or fuzzy pattern:"));
10256
+ lines.push("");
10257
+ for (let i = 0; i < subOptions2.length; i++) {
10258
+ const opt = subOptions2[i];
10259
+ const selected = i === this.permSubSelected;
10260
+ const prefix = selected ? c.cyan(" \u25B6") : " ";
10261
+ const label = selected ? c.bold(c.magenta(opt.label)) : c.dim(opt.label);
10262
+ lines.push(`${prefix} ${label}`);
10263
+ }
10264
+ lines.push(c.dim(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
10265
+ lines.push(c.dim(" \u2191\u2193 to choose Enter to confirm Esc to cancel"));
10266
+ return lines;
10267
+ }
10268
+ const exactLabel = isMcp ? `exact: ${tn}` : `exact: ${tn} (this call)`;
10269
+ const fuzzyLabel = isMcp ? `fuzzy: ${fp ?? ""}` : `fuzzy: ${tn} (all calls)`;
10270
+ const subOptions = [
10271
+ { label: exactLabel, key: "1" },
10272
+ { label: fuzzyLabel, key: "2" }
10273
+ ];
10274
+ if (fa) {
10275
+ subOptions.push({ label: `fuzzy args: ${tn} ${fa}`, key: "3" });
10276
+ }
10277
+ const llm = this._activePermission?.llmSuggestions;
10278
+ if (llm && llm.length > 0) {
10279
+ for (let i = 0; i < llm.length; i++) {
10280
+ subOptions.push({ label: `[AI] ${llm[i].label}`, key: `${3 + i}` });
10281
+ }
10282
+ }
10283
+ lines.push("");
10284
+ lines.push(c.yellow.bold(" Save Rule \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
10285
+ lines.push(c.dim(" Choose exact or fuzzy pattern:"));
10286
+ lines.push("");
10287
+ for (let i = 0; i < subOptions.length; i++) {
10288
+ const opt = subOptions[i];
10289
+ const selected = i === this.permSubSelected;
10290
+ const prefix = selected ? c.cyan(" \u25B6") : " ";
10291
+ const label = selected ? c.bold(c.magenta(opt.label)) : c.dim(opt.label);
10292
+ lines.push(`${prefix} ${label}`);
10293
+ }
10294
+ lines.push(c.dim(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
10295
+ lines.push(c.dim(" \u2191\u2193 to choose Enter to confirm Esc to cancel"));
10012
10296
  return lines;
10013
10297
  }
10014
10298
  render() {
@@ -10067,7 +10351,7 @@ var ConversationView = class {
10067
10351
  this.box.addChild(liveText);
10068
10352
  this.liveComponents.push(liveText);
10069
10353
  }
10070
- if (this.activePermission) {
10354
+ if (this._activePermission) {
10071
10355
  const permText = new Text(this.renderPermPrompt().join("\n"));
10072
10356
  this.box.addChild(permText);
10073
10357
  this.liveComponents.push(permText);
@@ -10384,7 +10668,16 @@ var TuiApp = class {
10384
10668
  async showPermissionPrompt(toolName, preview, args) {
10385
10669
  this.pendingPermissionContext = { toolName, args };
10386
10670
  this.permissionExplainMode = false;
10387
- this.conversation.showPermissionPrompt(toolName, preview);
10671
+ const fuzzy = deriveFuzzyPattern(toolName);
10672
+ const fuzzyArgDesc = describeFuzzyArgPattern(toolName, args);
10673
+ const llmSuggestions = getLlmSuggestions(toolName, args);
10674
+ this.conversation.showPermissionPrompt(toolName, preview, fuzzy, fuzzyArgDesc);
10675
+ if (llmSuggestions.length > 0) {
10676
+ const ap = this.conversation.activePermission;
10677
+ if (ap) ap.llmSuggestions = llmSuggestions;
10678
+ }
10679
+ const model = resolveModel(this.deps.config.provider, this.deps.config.modelId);
10680
+ prefetchLlmSuggestions(model, toolName, args, preview);
10388
10681
  return new Promise((resolve9) => {
10389
10682
  this.resolvePermission = resolve9;
10390
10683
  });
@@ -10428,13 +10721,21 @@ var TuiApp = class {
10428
10721
  return;
10429
10722
  }
10430
10723
  if (option === "always_allow") {
10431
- this.resolvePermissionChoice({ decision: "allow", rememberForSession: true });
10724
+ const fuzzy2 = deriveFuzzyPattern(this.pendingPermissionContext?.toolName ?? "");
10725
+ if (fuzzy2 && fuzzy2 !== this.pendingPermissionContext?.toolName) {
10726
+ this.conversation.enterSubMode("session", fuzzy2);
10727
+ } else {
10728
+ this.resolvePermissionChoice({ decision: "allow", rememberForSession: true });
10729
+ }
10432
10730
  return;
10433
10731
  }
10434
10732
  if (option === "always_allow_save") {
10435
- const ctx = this.pendingPermissionContext;
10436
- const persistRule = ctx ? this.buildPersistedRule(ctx.toolName, ctx.args, "saved from permission prompt") : void 0;
10437
- this.resolvePermissionChoice({ decision: "allow", rememberForSession: true, persistRule });
10733
+ const fuzzy2 = deriveFuzzyPattern(this.pendingPermissionContext?.toolName ?? "");
10734
+ if (fuzzy2) {
10735
+ this.conversation.enterSubMode("save", fuzzy2);
10736
+ } else {
10737
+ this.saveExactRule();
10738
+ }
10438
10739
  return;
10439
10740
  }
10440
10741
  if (option === "explain") {
@@ -10445,7 +10746,65 @@ var TuiApp = class {
10445
10746
  this.tui.requestRender(true);
10446
10747
  return;
10447
10748
  }
10448
- this.resolvePermissionChoice({ decision: "allow" });
10749
+ const fuzzy = deriveFuzzyPattern(this.pendingPermissionContext?.toolName ?? "");
10750
+ if (fuzzy && fuzzy !== this.pendingPermissionContext?.toolName) {
10751
+ this.conversation.enterSubMode("allow", fuzzy);
10752
+ } else {
10753
+ this.resolvePermissionChoice({ decision: "allow" });
10754
+ }
10755
+ }
10756
+ saveExactRule() {
10757
+ const ctx = this.pendingPermissionContext;
10758
+ const persistRule = ctx ? this.buildPersistedRule(ctx.toolName, ctx.args, "saved from permission prompt") : void 0;
10759
+ this.resolvePermissionChoice({ decision: "allow", rememberForSession: true, persistRule });
10760
+ }
10761
+ applyFuzzySaveOption(subIdx) {
10762
+ const ctx = this.pendingPermissionContext;
10763
+ if (!ctx) {
10764
+ this.resolvePermissionChoice({ decision: "deny" });
10765
+ return;
10766
+ }
10767
+ if (subIdx === 0) {
10768
+ this.saveExactRule();
10769
+ } else if (subIdx === 1) {
10770
+ const fuzzy = deriveFuzzyPattern(ctx.toolName);
10771
+ this.resolvePermissionChoice({ decision: "allow", rememberForSession: true, persistRule: fuzzy ? { tool: fuzzy, decision: "allow" } : void 0 });
10772
+ } else if (subIdx === 2) {
10773
+ const fuzzyArg = deriveFuzzyArgPattern(ctx.toolName, ctx.args);
10774
+ this.resolvePermissionChoice({ decision: "allow", rememberForSession: true, persistRule: fuzzyArg ? { tool: ctx.toolName, argPattern: fuzzyArg, decision: "allow" } : void 0 });
10775
+ } else {
10776
+ const llm = this.conversation.activePermission?.llmSuggestions;
10777
+ const s = llm ? llm[subIdx - 3] : null;
10778
+ if (s) {
10779
+ this.resolvePermissionChoice({
10780
+ decision: "allow",
10781
+ rememberForSession: true,
10782
+ persistRule: {
10783
+ tool: s.toolPattern ?? ctx.toolName,
10784
+ argPattern: s.argPattern ?? void 0,
10785
+ decision: "allow"
10786
+ }
10787
+ });
10788
+ } else {
10789
+ this.saveExactRule();
10790
+ }
10791
+ }
10792
+ }
10793
+ applySessionGrantOption(subIdx) {
10794
+ if (subIdx === 0) {
10795
+ this.resolvePermissionChoice({ decision: "allow", rememberForSession: true });
10796
+ } else {
10797
+ const fuzzy = deriveFuzzyPattern(this.pendingPermissionContext?.toolName ?? "");
10798
+ this.resolvePermissionChoice({ decision: "allow", rememberForSession: true, sessionGrantPattern: fuzzy ?? void 0 });
10799
+ }
10800
+ }
10801
+ applyAllowOption(subIdx) {
10802
+ if (subIdx === 0) {
10803
+ this.resolvePermissionChoice({ decision: "allow" });
10804
+ } else {
10805
+ const fuzzy = deriveFuzzyPattern(this.pendingPermissionContext?.toolName ?? "");
10806
+ this.resolvePermissionChoice({ decision: "allow", rememberForSession: true, sessionGrantPattern: fuzzy ?? void 0 });
10807
+ }
10449
10808
  }
10450
10809
  handleInput(data) {
10451
10810
  if (this.permissionExplainMode) {
@@ -10459,6 +10818,43 @@ var TuiApp = class {
10459
10818
  return false;
10460
10819
  }
10461
10820
  if (this.resolvePermission) {
10821
+ if (this.conversation.isInSubMode()) {
10822
+ if (matchesKey(data, Key.up)) {
10823
+ this.conversation.permSubNavigate(-1);
10824
+ return true;
10825
+ }
10826
+ if (matchesKey(data, Key.down)) {
10827
+ this.conversation.permSubNavigate(1);
10828
+ return true;
10829
+ }
10830
+ if (matchesKey(data, Key.enter) || matchesKey(data, Key.return)) {
10831
+ const idx = this.conversation.permSubSelect();
10832
+ const t = this.conversation.permSubModeType;
10833
+ if (t === "session") this.applySessionGrantOption(idx);
10834
+ else if (t === "allow") this.applyAllowOption(idx);
10835
+ else this.applyFuzzySaveOption(idx);
10836
+ return true;
10837
+ }
10838
+ if (matchesKey(data, Key.escape) || matchesKey(data, "ctrl+c") || data === "") {
10839
+ this.conversation.cancelSubMode();
10840
+ return true;
10841
+ }
10842
+ if (data === "1") {
10843
+ const t = this.conversation.permSubModeType;
10844
+ if (t === "session") this.applySessionGrantOption(0);
10845
+ else if (t === "allow") this.applyAllowOption(0);
10846
+ else this.applyFuzzySaveOption(0);
10847
+ return true;
10848
+ }
10849
+ if (data === "2") {
10850
+ const t = this.conversation.permSubModeType;
10851
+ if (t === "session") this.applySessionGrantOption(1);
10852
+ else if (t === "allow") this.applyAllowOption(1);
10853
+ else this.applyFuzzySaveOption(1);
10854
+ return true;
10855
+ }
10856
+ return true;
10857
+ }
10462
10858
  if (matchesKey(data, Key.up)) {
10463
10859
  if (this.canNavigateMenu("up")) {
10464
10860
  this.conversation.permNavigate(-1);
@@ -10479,7 +10875,11 @@ var TuiApp = class {
10479
10875
  return true;
10480
10876
  }
10481
10877
  if (matchesKey(data, Key.escape) || matchesKey(data, "ctrl+c") || data === "") {
10482
- this.resolvePermissionChoice({ decision: "deny" });
10878
+ if (this.conversation.isInSubMode()) {
10879
+ this.conversation.cancelSubMode();
10880
+ } else if (!this.conversation.justCancelledSubMode) {
10881
+ this.resolvePermissionChoice({ decision: "deny" });
10882
+ }
10483
10883
  return true;
10484
10884
  }
10485
10885
  const shortcut = findPermOptionByKey(data);