pi2dsh 0.16.0 → 0.16.2

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,5 +1,5 @@
1
1
 
2
- import { En as PiCapabilityError, F as generateBranchSummary, Tn as CapabilityLedger, b as Theme, d as ExtensionRunner, w as __runWithSubagentSessionFactory } from "./pi-coding-agent-f3_hNCpd.mjs";
2
+ import { En as PiCapabilityError, F as generateBranchSummary, Tn as CapabilityLedger, b as Theme, d as ExtensionRunner, w as __runWithSubagentSessionFactory } from "./pi-coding-agent-DXJ_Jzo0.mjs";
3
3
  import { t as getAgentDir } from "./pi-config-shim-CZ1wFzqM.mjs";
4
4
  import { $ as AssistantMessageEventStream, S as getSupportedThinkingLevels, c as builtinProviders, et as isContextOverflow, o as __runWithPiAiRuntime, r as __createPiAiRuntimeRegistry } from "./pi-ai-DQPHUR9S.mjs";
5
5
  import { W as getKeybindings, _t as stripTerminalSequences } from "./pi-tui-DruSeKmd.mjs";
@@ -7,7 +7,7 @@ import { c as resolvePiProviderAuth, i as oauthCredentialRef, l as storedOAuthCr
7
7
  import { createRequire } from "node:module";
8
8
  import { access, mkdir, readFile, writeFile } from "node:fs/promises";
9
9
  import { appendFileSync, existsSync, mkdirSync, readFileSync } from "node:fs";
10
- import { join } from "node:path";
10
+ import { basename, dirname, join, resolve } from "node:path";
11
11
  import { fileURLToPath } from "node:url";
12
12
  import { tmpdir } from "node:os";
13
13
  import { EventEmitter } from "node:events";
@@ -69,6 +69,37 @@ var PiSessionBridge = class {
69
69
  const safe = sessionId.replace(/[^a-zA-Z0-9._-]+/gu, "_");
70
70
  return join(sidecarDir(), `${safe}.jsonl`);
71
71
  }
72
+ /**
73
+ * The Pi-visible archive file for one DSH session — the established
74
+ * `<id>.jsonl` convention (getSessionFile/switchSession use the same one).
75
+ * Guaranteed to EXIST on return: Pi consumers treat the session file as the
76
+ * durable identity a conversation can be reopened by (pi-subagents guards
77
+ * its tombstone resurrect with existsSync, and Pi's SessionManager.open
78
+ * tolerates an empty file), so a merely virtual path would read as "the
79
+ * conversation is gone".
80
+ */
81
+ archiveFileFor(sessionId) {
82
+ const path = this.sidecarPath(sessionId);
83
+ if (!existsSync(path)) {
84
+ mkdirSync(sidecarDir(), { recursive: true });
85
+ appendFileSync(path, "");
86
+ }
87
+ return path;
88
+ }
89
+ /**
90
+ * The DSH session id an archive-file path names, or undefined for any path
91
+ * this bridge did not mint (a genuine Pi session file, an in-memory
92
+ * manager's undefined). The reverse of {@link archiveFileFor} — ids that
93
+ * survive its sanitization round-trip exactly, which every id this bridge
94
+ * mints does.
95
+ */
96
+ sessionIdOfArchiveFile(path) {
97
+ if (typeof path !== "string" || path.length === 0) return void 0;
98
+ const resolved = resolve(path);
99
+ if (resolve(dirname(resolved)) !== resolve(sidecarDir())) return void 0;
100
+ const base = basename(resolved);
101
+ return base.endsWith(".jsonl") ? base.slice(0, -6) : void 0;
102
+ }
72
103
  load(sessionId) {
73
104
  if (this.loaded.has(sessionId)) return;
74
105
  this.loaded.add(sessionId);
@@ -307,7 +338,7 @@ var PiSessionBridge = class {
307
338
  getCwd: () => cwd,
308
339
  getSessionDir: () => sidecarDir(),
309
340
  getSessionId: () => session.id,
310
- getSessionFile: () => this.sidecarPath(session.id),
341
+ getSessionFile: () => this.archiveFileFor(session.id),
311
342
  getLeafId: () => leafOf()?.id ?? null,
312
343
  getLeafEntry: () => leafOf(),
313
344
  getEntry: (id) => entriesOf().find((entry) => entry.id === id),
@@ -359,6 +390,26 @@ var PiSessionBridge = class {
359
390
  //#region src/subagent-bridge.ts
360
391
  let subagentSerial = 0;
361
392
  /**
393
+ * Pi built-in tool name → the DSH native tool that serves it. The two
394
+ * vocabularies coincide (read/bash/edit/write/grep); Pi's find and ls are
395
+ * both served by the host's glob.
396
+ */
397
+ function nativeToolNameOf(name) {
398
+ return name === "find" || name === "ls" ? "glob" : name;
399
+ }
400
+ /**
401
+ * The tool schemas a CHILD agent actually resolves — its scoped view when the
402
+ * service answers for the agent being built, else the global layer. On
403
+ * roster-owned surfaces the preset tools live above the global layer, so the
404
+ * scoped read is the one that sees them.
405
+ */
406
+ function childSchemas(toolsService, childCtx) {
407
+ try {
408
+ if (childCtx.agent !== void 0) return toolsService.schemas(childCtx.agent);
409
+ } catch {}
410
+ return toolsService.schemas();
411
+ }
412
+ /**
362
413
  * Cross-scope subscription base. Session events dispatch under the OWNING
363
414
  * session's scope; a package instance mounted per Agent registers listeners
364
415
  * inside its own Agent's scope and would never see a child session's events.
@@ -385,6 +436,12 @@ function piMessageText(message) {
385
436
  var PiBridgedAgentSession = class {
386
437
  agent = {};
387
438
  messages = [];
439
+ /**
440
+ * Pi's AgentSession.sessionManager surface, projected over the child's DSH
441
+ * session. getSessionFile() names the durable archive — what pi-subagents
442
+ * stores as the conversation's reopenable identity (tombstone resurrect).
443
+ */
444
+ sessionManager;
388
445
  #seed = [];
389
446
  #carried = /* @__PURE__ */ new WeakSet();
390
447
  #host;
@@ -398,8 +455,11 @@ var PiBridgedAgentSession = class {
398
455
  #model;
399
456
  #streaming = false;
400
457
  #pendingToolCalls = /* @__PURE__ */ new Set();
458
+ /** callId → tool name, so tool_execution_end can name the tool its result belongs to. */
459
+ #toolCallNames = /* @__PURE__ */ new Map();
401
460
  #sessionName = "";
402
461
  #turns = 0;
462
+ #restrictionDispose;
403
463
  /** Keeps message projections (which await attachment reads) in log order. */
404
464
  #messageProjection = Promise.resolve();
405
465
  #aborted = false;
@@ -407,6 +467,7 @@ var PiBridgedAgentSession = class {
407
467
  this.#host = host;
408
468
  this.#handle = handle;
409
469
  this.#session = handle.agent.session ?? {};
470
+ this.sessionManager = host.sessionManagerFor?.(this.#session);
410
471
  this.#tools = tools;
411
472
  this.#activeToolNames = tools.map((tool) => tool?.name).filter((name) => typeof name === "string");
412
473
  const cordis = unscopedEventContext(host.cordis);
@@ -493,6 +554,10 @@ var PiBridgedAgentSession = class {
493
554
  handler(piEvent);
494
555
  } catch {}
495
556
  };
557
+ if (this.#aborted && (type === "turn/start" || type === "step/start" || type === "request/header")) {
558
+ this.#cancelChild();
559
+ return;
560
+ }
496
561
  if (type === "turn/start") {
497
562
  this.#streaming = true;
498
563
  emit({
@@ -509,11 +574,32 @@ var PiBridgedAgentSession = class {
509
574
  }
510
575
  if (type === "tools/result" || type === "tool/result") {
511
576
  const data = event.data;
512
- this.#pendingToolCalls.delete(String(data.callId ?? ""));
577
+ const message = data.message;
578
+ const blocks = Array.isArray(message?.content) ? message.content : [];
579
+ for (const block of blocks) {
580
+ const record = block;
581
+ if (record?.type !== "tool-result") continue;
582
+ const callId = String(record.toolCallId ?? (message?.source)?.callId ?? data.callId ?? "");
583
+ this.#pendingToolCalls.delete(callId);
584
+ const isError = record.isError === true;
585
+ emit({
586
+ type: "tool_execution_end",
587
+ toolCallId: callId,
588
+ toolName: this.#toolCallNames.get(callId) ?? "",
589
+ result: {
590
+ content: record.content ?? [],
591
+ isError
592
+ },
593
+ isError
594
+ });
595
+ this.#toolCallNames.delete(callId);
596
+ }
597
+ if (blocks.length === 0) this.#pendingToolCalls.delete(String(data.callId ?? ""));
513
598
  }
514
599
  if (type === "tool/call") {
515
600
  const data = event.data;
516
601
  this.#pendingToolCalls.add(String(data.callId ?? ""));
602
+ this.#toolCallNames.set(String(data.callId ?? ""), String(data.name ?? ""));
517
603
  let args = {};
518
604
  try {
519
605
  args = JSON.parse(String(data.arguments ?? "{}"));
@@ -582,6 +668,7 @@ var PiBridgedAgentSession = class {
582
668
  return () => this.#subscribers.delete(handler);
583
669
  }
584
670
  async prompt(text) {
671
+ this.#aborted = false;
585
672
  if (this.#seed.length > 0) {
586
673
  const seeded = this.#seed;
587
674
  this.#seed = [];
@@ -645,13 +732,28 @@ var PiBridgedAgentSession = class {
645
732
  }
646
733
  abort() {
647
734
  this.#aborted = true;
735
+ this.#cancelChild();
736
+ }
737
+ /** One official Agent.cancel, contained but LOUD: a swallowed cancel is a
738
+ * child that keeps running after its parent was interrupted. */
739
+ #cancelChild() {
740
+ const cancel = this.#handle.agent.cancel;
741
+ if (cancel === void 0) {
742
+ const message = "[pi2dsh] subagent abort(): the child agent handle has no cancel — the child cannot be stopped";
743
+ console.warn(message);
744
+ this.#host.cordis.logger?.warn?.(message);
745
+ return;
746
+ }
648
747
  try {
649
- const cancel = this.#handle.agent.cancel;
650
- cancel?.({
748
+ cancel.call(this.#handle.agent, {
651
749
  kind: "hook",
652
750
  reason: "pi2dsh subagent abort()"
653
751
  });
654
- } catch {}
752
+ } catch (error) {
753
+ const message = `[pi2dsh] subagent abort(): Agent.cancel failed (${error instanceof Error ? error.message : String(error)}) — already disposed, or the child kept running`;
754
+ console.warn(message);
755
+ this.#host.cordis.logger?.warn?.(message);
756
+ }
655
757
  }
656
758
  setSessionName(name) {
657
759
  this.#sessionName = String(name);
@@ -666,14 +768,44 @@ var PiBridgedAgentSession = class {
666
768
  aborted: this.#aborted
667
769
  };
668
770
  }
771
+ /** The child scope's REAL tool schemas (native + scoped custom), in Pi tool shape. */
772
+ #scopeTools() {
773
+ const agentCtx = this.#handle.agent.ctx;
774
+ const toolsService = typeof agentCtx?.get === "function" ? agentCtx.get("tools") : void 0;
775
+ try {
776
+ return [...toolsService?.schemas?.(this.#handle.agent) ?? []];
777
+ } catch {
778
+ return [];
779
+ }
780
+ }
669
781
  getAllTools() {
670
- return [...this.#tools];
782
+ const custom = new Map(this.#tools.filter((tool) => typeof tool?.name === "string").map((tool) => [String(tool.name), tool]));
783
+ return this.#scopeTools().map((schema) => custom.get(schema.name) ?? schema);
671
784
  }
672
785
  getActiveToolNames() {
673
- return [...this.#activeToolNames];
786
+ const fromScope = this.#scopeTools().map((schema) => schema.name);
787
+ return fromScope.length > 0 ? fromScope : [...this.#activeToolNames];
788
+ }
789
+ /** Hand over the creation-time restriction so a later setActiveToolsByName
790
+ * retires it instead of intersecting with it. */
791
+ adoptRestriction(dispose) {
792
+ this.#restrictionDispose = dispose;
674
793
  }
675
794
  setActiveToolsByName(names) {
676
795
  this.#activeToolNames = names.filter((name) => typeof name === "string");
796
+ const agentCtx = this.#handle.agent.ctx;
797
+ const toolsService = typeof agentCtx?.get === "function" ? agentCtx.get("tools") : void 0;
798
+ if (toolsService === void 0) return;
799
+ try {
800
+ this.#restrictionDispose?.();
801
+ let known;
802
+ try {
803
+ known = new Set(toolsService.schemas(this.#handle.agent).map((schema) => schema.name));
804
+ } catch {
805
+ known = new Set(toolsService.schemas().map((schema) => schema.name));
806
+ }
807
+ this.#restrictionDispose = toolsService.restrict({ allow: this.#activeToolNames.map(nativeToolNameOf).filter((name) => known.has(name)) });
808
+ } catch {}
677
809
  }
678
810
  async bindExtensions(_bindings = {}) {}
679
811
  async dispose() {
@@ -708,45 +840,111 @@ function childLabel(requested, packageName) {
708
840
  async function createBridgedAgentSession(host, options) {
709
841
  const agents = host.cordis.get("agents");
710
842
  if (agents?.create === void 0) throw new Error("pi2dsh: createAgentSession() needs the DSH agent registry in the host composition");
843
+ const providedManager = options.sessionManager;
844
+ const archiveFile = typeof providedManager?.getSessionFile === "function" ? providedManager.getSessionFile() : void 0;
845
+ const resumeSessionId = host.resumeSessionIdFor?.(archiveFile);
711
846
  subagentSerial += 1;
712
847
  const sessionId = `pi2dsh-sub-${Date.now().toString(36)}-${subagentSerial}`;
713
848
  let handle;
849
+ let initialRestrictionDispose;
714
850
  try {
715
851
  const requestedModel = options.model;
716
852
  const loader = options.resourceLoader;
717
853
  const overrideText = typeof options.systemPrompt === "string" && options.systemPrompt.length > 0 ? options.systemPrompt : typeof loader?.getSystemPrompt === "function" ? loader.getSystemPrompt() : void 0;
718
854
  const appendTexts = (typeof loader?.getAppendSystemPrompt === "function" ? loader.getAppendSystemPrompt() : []).filter((text) => typeof text === "string" && text.length > 0);
719
855
  const systemPrompt = overrideText !== void 0 && overrideText.length > 0 ? [overrideText, ...appendTexts].join("\n\n") : void 0;
720
- handle = await agents.create({
856
+ const customTools = Array.isArray(options.customTools) ? [...options.customTools] : [];
857
+ const requestedNames = Array.isArray(options.tools) ? options.tools.filter((name) => typeof name === "string") : void 0;
858
+ const parentCtx = host.parentAgentContext();
859
+ const roster = (typeof parentCtx?.get === "function" ? parentCtx.get("agentPresets") : void 0) ?? host.cordis.get?.("agentPresets");
860
+ let composedPreset = roster?.composedPreset?.(parentCtx);
861
+ if (roster !== void 0 && composedPreset === void 0 && typeof roster.resolve === "function") composedPreset = await roster.resolve(void 0).then((preset) => preset?.id).catch(() => void 0);
862
+ const delegated = host.delegatedPolicyOverrides();
863
+ const route = typeof requestedModel?.id === "string" && requestedModel.id.length > 0 ? {
864
+ model: requestedModel.id,
865
+ ...typeof requestedModel.provider === "string" && requestedModel.provider.length > 0 ? { provider: requestedModel.provider } : {}
866
+ } : host.parentModelRoute?.();
867
+ const agentOptionsFragment = typeof route?.model === "string" && route.model.length > 0 ? { agentOptions: route } : {};
868
+ const setup = async (childCtx) => {
869
+ const childSession = childCtx.agent?.session;
870
+ if (typeof childSession?.append === "function") {
871
+ if (delegated.sandboxMode !== void 0) childSession.append("sandbox/mode", {
872
+ mode: delegated.sandboxMode,
873
+ source: "delegation"
874
+ });
875
+ if (delegated.approvalPolicy !== void 0) childSession.append("approval/policy", {
876
+ policy: delegated.approvalPolicy,
877
+ source: "delegation"
878
+ });
879
+ }
880
+ if (roster !== void 0) {
881
+ let joined;
882
+ try {
883
+ joined = roster.composeFrom?.(childCtx, parentCtx);
884
+ } catch {
885
+ joined = void 0;
886
+ }
887
+ if (joined === void 0 && composedPreset !== void 0 && typeof roster.mount === "function") try {
888
+ await roster.mount(childCtx, composedPreset);
889
+ joined = composedPreset;
890
+ } catch {
891
+ joined = void 0;
892
+ }
893
+ if (joined === void 0) host.cordis.logger?.warn?.("[pi2dsh] child agent joined no preset composition; on roster-owned surfaces its tools resolve against the empty global layer");
894
+ }
895
+ const restriction = requestedNames !== void 0 ? requestedNames.map(nativeToolNameOf) : options.noTools === "all" || options.noTools === true || options.noTools === "builtin" ? [] : void 0;
896
+ if (restriction !== void 0) {
897
+ const toolsService = typeof childCtx.get === "function" ? childCtx.get("tools") : void 0;
898
+ if (toolsService !== void 0) {
899
+ const known = new Set(childSchemas(toolsService, childCtx).map((schema) => schema.name));
900
+ initialRestrictionDispose = toolsService.restrict({ allow: restriction.filter((name) => known.has(name)) });
901
+ }
902
+ }
903
+ const excluded = Array.isArray(options.excludeTools) ? options.excludeTools.filter((name) => typeof name === "string") : [];
904
+ if (excluded.length > 0) {
905
+ const toolsService = typeof childCtx.get === "function" ? childCtx.get("tools") : void 0;
906
+ if (toolsService !== void 0) {
907
+ const known = new Set(childSchemas(toolsService, childCtx).map((schema) => schema.name));
908
+ toolsService.restrict({ deny: excluded.map(nativeToolNameOf).filter((name) => known.has(name)) });
909
+ }
910
+ }
911
+ if (customTools.length > 0 && options.noTools !== "all") host.registerChildTools(childCtx, customTools);
912
+ if (systemPrompt !== void 0) {
913
+ const prompt = typeof childCtx.get === "function" ? childCtx.get("systemPrompt") : void 0;
914
+ if (prompt === void 0) throw new Error("pi2dsh: createAgentSession() got a system prompt but the DSH composition has no systemPrompt service to carry it");
915
+ prompt.section({
916
+ name: "pi2dsh:subagent-system-prompt",
917
+ order: -1e6,
918
+ text: systemPrompt,
919
+ complete: true
920
+ });
921
+ }
922
+ };
923
+ if (resumeSessionId !== void 0) {
924
+ if (typeof agents.resume !== "function") throw new Error("pi2dsh: reopening a child conversation needs the DSH agent registry's persisted-resume seam, which this composition does not provide");
925
+ handle = await agents.resume({
926
+ resumeSessionId,
927
+ ...agentOptionsFragment,
928
+ setup
929
+ });
930
+ } else handle = await agents.create({
721
931
  sessionId,
722
932
  meta: {
723
933
  cwd: typeof options.cwd === "string" ? options.cwd : host.cwd(),
724
934
  origin: "subagent",
725
935
  delegationDepth: host.parentDelegationDepth() + 1,
726
- ...host.parentSessionId() !== void 0 ? { parentSession: host.parentSessionId() } : {}
936
+ ...host.parentSessionId() !== void 0 ? { parentSession: host.parentSessionId() } : {},
937
+ ...typeof composedPreset === "string" && composedPreset.length > 0 ? { agentPreset: composedPreset } : {}
727
938
  },
728
- ...typeof requestedModel?.id === "string" && requestedModel.id.length > 0 ? { agentOptions: {
729
- model: requestedModel.id,
730
- ...typeof requestedModel.provider === "string" && requestedModel.provider.length > 0 ? { provider: requestedModel.provider } : {}
731
- } } : {}
939
+ ...agentOptionsFragment,
940
+ setup
732
941
  });
733
- if (systemPrompt !== void 0) {
734
- const agentCtx = handle.agent.ctx;
735
- const prompt = typeof agentCtx?.get === "function" ? agentCtx.get("systemPrompt") : void 0;
736
- if (prompt === void 0) throw new Error("pi2dsh: createAgentSession() got a system prompt but the DSH composition has no systemPrompt service to carry it");
737
- prompt.section({
738
- name: "pi2dsh:subagent-system-prompt",
739
- order: -1e6,
740
- text: systemPrompt,
741
- complete: true
742
- });
743
- }
744
942
  } catch (error) {
745
- throw new Error(`pi2dsh: subagent creation needs the DSH host loop (model runtime) to provide the agent factory; this composition cannot run one (${error instanceof Error ? error.message : String(error)})`);
943
+ throw new Error(resumeSessionId !== void 0 ? `pi2dsh: reopening child session ${JSON.stringify(resumeSessionId)} failed — its persisted log may be gone or this composition has no session persistence (${error instanceof Error ? error.message : String(error)})` : `pi2dsh: subagent creation needs the DSH host loop (model runtime) to provide the agent factory; this composition cannot run one (${error instanceof Error ? error.message : String(error)})`);
746
944
  }
747
945
  const tools = [...Array.isArray(options.tools) ? options.tools : [], ...Array.isArray(options.customTools) ? options.customTools : []];
748
946
  const childSession = handle.agent.session;
749
- if (typeof childSession?.append === "function") try {
947
+ if (resumeSessionId === void 0 && typeof childSession?.append === "function") try {
750
948
  childSession.append("subagent/descriptor", {
751
949
  version: 2,
752
950
  mode: "continuable",
@@ -756,7 +954,9 @@ async function createBridgedAgentSession(host, options) {
756
954
  } catch (error) {
757
955
  host.cordis.logger?.warn?.(`[pi2dsh] child session could not record its subagent identity: ${error instanceof Error ? error.message : String(error)}`);
758
956
  }
759
- return { session: new PiBridgedAgentSession(host, handle, tools) };
957
+ const session = new PiBridgedAgentSession(host, handle, tools);
958
+ session.adoptRestriction(initialRestrictionDispose);
959
+ return { session };
760
960
  }
761
961
  //#endregion
762
962
  //#region src/compat/vendor/pi-open-browser.ts
@@ -2892,12 +3092,91 @@ function mountTuiSurfaceAdapter(ctx, packageName, publish, instanceKey) {
2892
3092
  }
2893
3093
  }
2894
3094
  /**
2895
- * dsh-TUI handles `/mcp` locally before it asks DSH's command registry. Until
2896
- * it exposes a reserved-command seam, keep its native status command and make
2897
- * an installed Pi MCP manager reachable as `/pi-mcp`.
3095
+ * dsh-TUI dispatches its LOCAL commands before it asks DSH's command
3096
+ * registry ("locals win on name collisions" its own commands.ts contract),
3097
+ * so a registry command sharing a local name is unreachable from that
3098
+ * surface. The host command keeps its name; the incoming command gets a
3099
+ * `pi-` source prefix — and ONLY on a composition where dsh-TUI is present;
3100
+ * everywhere else the original name stands.
3101
+ *
3102
+ * The reserved-name list is dsh-TUI's `LOCAL_COMMANDS`, which it exports in
3103
+ * code but not through its package exports map (upstream ask filed). Until
3104
+ * that lands, the list is pinned per generation from the published sources:
3105
+ * 0.8.x reserved only /mcp; 0.9.0 reserves the full set below. Generation is
3106
+ * read off the installed package's public ./package.json subpath.
2898
3107
  */
3108
+ const DSH_TUI_LOCAL_COMMANDS_0_9 = /* @__PURE__ */ new Set([
3109
+ "new",
3110
+ "clear",
3111
+ "compact",
3112
+ "resume",
3113
+ "rename",
3114
+ "rewind",
3115
+ "export",
3116
+ "btw",
3117
+ "trace",
3118
+ "context",
3119
+ "status",
3120
+ "cost",
3121
+ "config",
3122
+ "settings",
3123
+ "doctor",
3124
+ "init",
3125
+ "agents",
3126
+ "activity",
3127
+ "preset",
3128
+ "theme",
3129
+ "lang",
3130
+ "model",
3131
+ "effort",
3132
+ "thinking",
3133
+ "tokens",
3134
+ "provider",
3135
+ "login",
3136
+ "logout",
3137
+ "add-dir",
3138
+ "hooks",
3139
+ "mcp",
3140
+ "skills",
3141
+ "plugins",
3142
+ "update",
3143
+ "audit",
3144
+ "bug",
3145
+ "practice",
3146
+ "review",
3147
+ "release-notes",
3148
+ "vuln-check",
3149
+ "vim",
3150
+ "terminal-setup",
3151
+ "connect",
3152
+ "workspace",
3153
+ "help",
3154
+ "tips",
3155
+ "exit",
3156
+ "quit",
3157
+ "q",
3158
+ "deepseek"
3159
+ ]);
3160
+ const DSH_TUI_LOCAL_COMMANDS_LEGACY = /* @__PURE__ */ new Set(["mcp"]);
3161
+ /** The reserved set for one installed dsh-tui version (undefined = unknown,
3162
+ * assume the current generation). */
3163
+ function tuiLocalCommandsForVersion(version) {
3164
+ if (version === void 0) return DSH_TUI_LOCAL_COMMANDS_0_9;
3165
+ const [major = 0, minor = 0] = version.split(".").map((part) => Number.parseInt(part, 10));
3166
+ return major > 0 || minor >= 9 ? DSH_TUI_LOCAL_COMMANDS_0_9 : DSH_TUI_LOCAL_COMMANDS_LEGACY;
3167
+ }
3168
+ let cachedTuiLocalCommands;
3169
+ function dshTuiLocalCommands() {
3170
+ if (cachedTuiLocalCommands !== void 0) return cachedTuiLocalCommands;
3171
+ let version;
3172
+ try {
3173
+ version = createRequire(import.meta.url)("@deepseek-harness-tui/dsh-tui/package.json").version;
3174
+ } catch {}
3175
+ cachedTuiLocalCommands = tuiLocalCommandsForVersion(version);
3176
+ return cachedTuiLocalCommands;
3177
+ }
2899
3178
  function commandNameForDshTui(name, tuiAvailable) {
2900
- return tuiAvailable && name === "mcp" ? "pi-mcp" : name;
3179
+ return tuiAvailable && dshTuiLocalCommands().has(name) ? `pi-${name}` : name;
2901
3180
  }
2902
3181
  //#endregion
2903
3182
  //#region src/runtime.ts
@@ -5441,6 +5720,50 @@ function registerTool(ctx, state, tool) {
5441
5720
  state.toolDisposers.set(tool.name, dispose);
5442
5721
  if (isKnownImageTool(state.packageName, tool.name)) state.shared.browserSurfaces?.registerImageTool(tool.name);
5443
5722
  }
5723
+ /**
5724
+ * Register ONE Pi custom tool into a child agent's scope (createAgentSession
5725
+ * customTools). Same translation as {@link registerTool}'s definition, but:
5726
+ * the registration goes through the CHILD ctx (a scoped registration that
5727
+ * unwinds with the child, invisible to the parent and siblings), and the
5728
+ * package's own Pi tool ledger is untouched — a child session's custom tools
5729
+ * belong to that session in Pi, not to the extension's registered set.
5730
+ * Partial-result updates have no Pi-side consumer on this path yet (the
5731
+ * façade projects durable events only), so the update callback is a no-op —
5732
+ * never a fake dispatch.
5733
+ */
5734
+ function registerChildPiTool(childCtx, state, tool) {
5735
+ const normalized = normalizeToolSchema(tool.parameters);
5736
+ for (const warning of normalized.warnings) logger(childCtx).warn(`[pi2dsh] subagent tool ${tool.name}: ${warning}`);
5737
+ const definition = {
5738
+ name: tool.name,
5739
+ description: tool.description,
5740
+ parameters: normalized.schema,
5741
+ output: {
5742
+ schema: {},
5743
+ render: (_args, value) => value.content,
5744
+ presentationMeta: (_args, value) => jsonValue(value.details)
5745
+ },
5746
+ isConcurrencySafe: () => tool.executionMode === "parallel",
5747
+ async execute(args, exec) {
5748
+ const agent = exec.agent;
5749
+ const prepared = validateToolArguments({
5750
+ name: tool.name,
5751
+ parameters: tool.parameters
5752
+ }, {
5753
+ name: tool.name,
5754
+ arguments: tool.prepareArguments?.(cloneJson(args)) ?? args
5755
+ });
5756
+ const result = await normalizeToolResultForDsh(childCtx, await runInPiRuntime(state, agent, () => tool.execute(String(exec.callId), prepared, exec.signal, () => {}, contextFor(childCtx, state, agent, exec.signal))));
5757
+ if (result.terminate === true) exec.concludeTurn();
5758
+ if (result.isError === true) {
5759
+ const message = textBlocks(result.content).map((block) => block.text).filter(Boolean).join("\n");
5760
+ throw new Error(message || `Pi tool ${tool.name} failed`);
5761
+ }
5762
+ return result;
5763
+ }
5764
+ };
5765
+ childCtx.tools.register(definition);
5766
+ }
5444
5767
  function unregisterTool(state, name) {
5445
5768
  const dispose = state.toolDisposers.get(name);
5446
5769
  if (dispose === void 0) return false;
@@ -5689,9 +6012,10 @@ function normalizedPiCommandName(piName) {
5689
6012
  return normalized.length > 0 ? normalized : "pi-command";
5690
6013
  }
5691
6014
  function dshCommandName(ctx, state, piName) {
5692
- const name = commandNameForDshTui(normalizedPiCommandName(piName), state.tuiSurfaces?.available === true || optionalService(ctx, "tuiScenes") !== void 0);
6015
+ const validName = normalizedPiCommandName(piName);
6016
+ const name = commandNameForDshTui(validName, state.tuiSurfaces?.available === true || optionalService(ctx, "tuiScenes") !== void 0);
5693
6017
  if (name !== piName) {
5694
- const reason = name === "pi-mcp" ? "because dsh-TUI reserves /mcp for its native MCP status view" : "to satisfy DSH command naming";
6018
+ const reason = name === `pi-${validName}` ? `because dsh-TUI reserves /${validName} for its own local command (locals win on name collisions)` : "to satisfy DSH command naming";
5695
6019
  logger(ctx).warn(`[pi2dsh] Pi command /${piName} registered as /${name} ${reason}`);
5696
6020
  }
5697
6021
  return name;
@@ -6383,7 +6707,39 @@ async function applyPiPackage(ctx, options) {
6383
6707
  deliver: (agent, message, mode) => deliverAgentMessage(agent, message, mode),
6384
6708
  messageFromSessionEvent: (event) => messageFromSessionEvent(ctx, event),
6385
6709
  messageSource: state.messageSource,
6386
- packageName: state.packageName
6710
+ packageName: state.packageName,
6711
+ parentAgentContext: () => currentAgent(state)?.ctx,
6712
+ registerChildTools: (childCtx, tools) => {
6713
+ for (const tool of tools) registerChildPiTool(childCtx, state, tool);
6714
+ },
6715
+ delegatedPolicyOverrides: () => {
6716
+ const parent = currentAgent(state);
6717
+ const sandboxPolicy = typeof parent?.ctx?.get === "function" ? parent.ctx.get("sandboxPolicy") : void 0;
6718
+ const approval = typeof parent?.ctx?.get === "function" ? parent.ctx.get("approval") : void 0;
6719
+ const sandboxMode = sandboxPolicy?.overrideOf?.(parent?.session);
6720
+ return {
6721
+ ...sandboxMode !== void 0 ? { sandboxMode } : {},
6722
+ ...approval !== void 0 ? { approvalPolicy: "never" } : {}
6723
+ };
6724
+ },
6725
+ sessionManagerFor: (session) => {
6726
+ const typed = session;
6727
+ const cwd = typeof typed.meta?.cwd === "string" ? typed.meta.cwd : cwdOf(currentAgent(state));
6728
+ return state.bridge.readonlySessionManager(session, cwd);
6729
+ },
6730
+ resumeSessionIdFor: (file) => state.bridge.sessionIdOfArchiveFile(file),
6731
+ parentModelRoute: () => {
6732
+ const parent = currentAgent(state);
6733
+ if (parent === void 0) return void 0;
6734
+ const live = currentPiModel(state, parent);
6735
+ const model = live?.id;
6736
+ if (typeof model !== "string" || model.length === 0) return void 0;
6737
+ const provider = live?.provider;
6738
+ return {
6739
+ model,
6740
+ ...typeof provider === "string" && provider.length > 0 ? { provider } : {}
6741
+ };
6742
+ }
6387
6743
  });
6388
6744
  await registerPromptCommands(ctx, state, rootDir, options.manifest);
6389
6745
  const onExtensionError = (failure) => logger(ctx).warn(`[pi2dsh] extension entry failed and was skipped (matching Pi's per-extension error isolation): ${failure}`);
@@ -6425,6 +6781,7 @@ async function applyPiPackage(ctx, options) {
6425
6781
  const runtimeInternals = {
6426
6782
  compactionReason,
6427
6783
  resolveOfferedChoice,
6784
+ currentPiModel,
6428
6785
  dshToPiContent,
6429
6786
  expandPrompt,
6430
6787
  isKnownImageTool,
@@ -6437,4 +6794,4 @@ const runtimeInternals = {
6437
6794
  //#endregion
6438
6795
  export { registerVisionCompanions as a, overlayProviderConfig as i, mergeProviderRegistration as n, runtimeInternals as o, normalizeToolSchema as r, applyPiPackage as t };
6439
6796
 
6440
- //# sourceMappingURL=runtime-C6_pOPmL.mjs.map
6797
+ //# sourceMappingURL=runtime-t-5iWtFO.mjs.map