@hachej/boring-agent 0.1.40 → 0.1.41

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.
@@ -535,6 +535,8 @@ interface PiChatPanelProps {
535
535
  serverResourcesEnabled?: boolean;
536
536
  mentionedFiles?: string[] | (() => string[]);
537
537
  commands?: SlashCommand[];
538
+ /** Built-in slash command names to omit from the composer command registry. */
539
+ excludeBuiltinCommands?: string[];
538
540
  toolRenderers?: ToolRendererOverrides;
539
541
  createRemoteSession?: (options: RemotePiSessionOptions) => RemotePiSession;
540
542
  remoteSessionOptions?: UsePiSessionsOptions['remoteSessionOptions'];
@@ -552,7 +554,7 @@ interface PiChatPanelProps {
552
554
  onComposerStop?: () => void;
553
555
  onComposerBlockerAction?: (blocker: ComposerBlocker, action: string) => void;
554
556
  }
555
- declare function PiChatPanel({ sessionId, extraCommands, apiBaseUrl, workspaceId, storageScope, requestHeaders, storage, fetch, className, chrome, debug, showSessions, hotReloadEnabled, suggestions, emptyState, emptyPlacement, composerPlaceholder, initialDraft, autoSubmitInitialDraft, onDraftRestored, onAutoSubmitInitialDraftAccepted, onAutoSubmitInitialDraftSettled, model, defaultModel, availableModels, thinkingLevel, thinkingControl, serverResourcesEnabled, mentionedFiles, commands, toolRenderers, createRemoteSession, remoteSessionOptions, hydrateMessages, workspaceWarmupStatus, onSessionReset, onBeforeSubmit, onReloadAgentPlugins, onCommandResult, onComposerWarning, onMentionedFilesConsumed, onData, onOpenArtifact, composerBlockers, onComposerStop, onComposerBlockerAction, }: PiChatPanelProps): react_jsx_runtime.JSX.Element;
557
+ declare function PiChatPanel({ sessionId, extraCommands, apiBaseUrl, workspaceId, storageScope, requestHeaders, storage, fetch, className, chrome, debug, showSessions, hotReloadEnabled, suggestions, emptyState, emptyPlacement, composerPlaceholder, initialDraft, autoSubmitInitialDraft, onDraftRestored, onAutoSubmitInitialDraftAccepted, onAutoSubmitInitialDraftSettled, model, defaultModel, availableModels, thinkingLevel, thinkingControl, serverResourcesEnabled, mentionedFiles, commands, excludeBuiltinCommands, toolRenderers, createRemoteSession, remoteSessionOptions, hydrateMessages, workspaceWarmupStatus, onSessionReset, onBeforeSubmit, onReloadAgentPlugins, onCommandResult, onComposerWarning, onMentionedFilesConsumed, onData, onOpenArtifact, composerBlockers, onComposerStop, onComposerBlockerAction, }: PiChatPanelProps): react_jsx_runtime.JSX.Element;
556
558
 
557
559
  interface DebugDrawerProps {
558
560
  apiBaseUrl?: string;
@@ -8534,7 +8534,7 @@ function SlashCommandPicker({ query, commands, onSelect, onDismiss }) {
8534
8534
  autoComplete: "off"
8535
8535
  }
8536
8536
  ),
8537
- groups.length > 1 && /* @__PURE__ */ jsx30("div", { className: "flex flex-wrap gap-1 border-b border-border/50 px-2 py-1.5", role: "tablist", "aria-label": "Filter by plugin", children: [ALL_PLUGINS, ...groups].map((g) => {
8537
+ /* @__PURE__ */ jsx30("div", { className: "flex flex-wrap gap-1 border-b border-border/50 px-2 py-1.5", role: "tablist", "aria-label": "Filter by plugin", children: [ALL_PLUGINS, ...groups].map((g) => {
8538
8538
  const selected = plugin === g;
8539
8539
  return /* @__PURE__ */ jsx30(
8540
8540
  "button",
@@ -9173,6 +9173,7 @@ function errorMessage2(error, fallback) {
9173
9173
  import { jsx as jsx33, jsxs as jsxs31 } from "react/jsx-runtime";
9174
9174
  var DebugDrawer2 = lazy(() => import("../DebugDrawer-2PUDZ2RL.js").then((m) => ({ default: m.DebugDrawer })));
9175
9175
  var EMPTY_COMMANDS = [];
9176
+ var EMPTY_COMMAND_NAMES = [];
9176
9177
  var EMPTY_BLOCKERS = [];
9177
9178
  function PiChatPanel({
9178
9179
  sessionId,
@@ -9205,6 +9206,7 @@ function PiChatPanel({
9205
9206
  serverResourcesEnabled = true,
9206
9207
  mentionedFiles,
9207
9208
  commands = EMPTY_COMMANDS,
9209
+ excludeBuiltinCommands = EMPTY_COMMAND_NAMES,
9208
9210
  toolRenderers,
9209
9211
  createRemoteSession,
9210
9212
  remoteSessionOptions,
@@ -9363,12 +9365,16 @@ function PiChatPanel({
9363
9365
  setLocalSubmittedSessionId(void 0);
9364
9366
  }, []);
9365
9367
  const registry = useMemo12(() => {
9366
- const effectiveBuiltins = hotReloadEnabled ? builtinCommands : builtinCommands.filter((command) => command.name !== "reload");
9368
+ const excludedBuiltins = new Set(excludeBuiltinCommands);
9369
+ const effectiveBuiltins = builtinCommands.filter((command) => {
9370
+ if (!hotReloadEnabled && command.name === "reload") return false;
9371
+ return !excludedBuiltins.has(command.name);
9372
+ });
9367
9373
  const next = createCommandRegistry(effectiveBuiltins);
9368
9374
  for (const command of extraCommands ?? []) next.register(command);
9369
9375
  for (const command of commands) next.register(command);
9370
9376
  return next;
9371
- }, [apiBaseUrl, commands, extraCommands, hotReloadEnabled, normalizedRequestHeaders, serverResourcesEnabled, serverSkillsRefreshKey, storageScope]);
9377
+ }, [apiBaseUrl, commands, excludeBuiltinCommands, extraCommands, hotReloadEnabled, normalizedRequestHeaders, serverResourcesEnabled, serverSkillsRefreshKey, storageScope]);
9372
9378
  const commandsStamp = useServerCommands({
9373
9379
  registry,
9374
9380
  requestHeaders: normalizedRequestHeaders,
@@ -87,17 +87,18 @@ function getBoringAgentRuntimeEnv(paths, adapterCacheRoot = paths.cache) {
87
87
 
88
88
  // src/server/sandbox/workspacePythonEnv.ts
89
89
  function withWorkspacePythonEnv(opts) {
90
- const { workspaceRoot, env, sandboxRoot } = opts;
90
+ const { workspaceRoot, env, sandboxRoot, preserveHostHome } = opts;
91
91
  const runtimeRoot = sandboxRoot ?? workspaceRoot;
92
92
  const paths = getBoringAgentRuntimePaths(runtimeRoot);
93
93
  const baseEnv = env ?? getEnvSnapshot();
94
94
  const pathParts = getBoringAgentPathEntries(paths);
95
95
  const existingPath = baseEnv.PATH;
96
96
  if (existingPath) pathParts.push(existingPath);
97
+ const home = preserveHostHome ? baseEnv.HOME : runtimeRoot;
97
98
  return {
98
99
  ...baseEnv,
99
100
  PATH: pathParts.join(":"),
100
- HOME: runtimeRoot,
101
+ HOME: home,
101
102
  VIRTUAL_ENV: paths.venv,
102
103
  PYTHONHOME: void 0,
103
104
  BORING_AGENT_WORKSPACE_ROOT: runtimeRoot
@@ -168,7 +169,7 @@ function createDirectSandbox(opts = {}) {
168
169
  return await new Promise((resolve11, reject) => {
169
170
  const child = spawn(cmd, {
170
171
  cwd,
171
- env: withWorkspacePythonEnv({ workspaceRoot, env: opts2?.env }),
172
+ env: withWorkspacePythonEnv({ workspaceRoot, env: opts2?.env, preserveHostHome: true }),
172
173
  shell: true,
173
174
  windowsHide: true,
174
175
  detached: process.platform !== "win32"
@@ -427,9 +428,8 @@ async function ensureWritableWorkspacePath(workspaceRoot, relPath) {
427
428
  return absPath;
428
429
  }
429
430
 
430
- // src/server/workspace/createNodeWorkspace.ts
431
- var EPERM_CODE = "EPERM";
432
- var DEFAULT_WATCH_IGNORES = [
431
+ // src/server/workspace/ignore.ts
432
+ var DEFAULT_IGNORED_DIR_NAMES = [
433
433
  "node_modules",
434
434
  ".git",
435
435
  ".DS_Store",
@@ -441,12 +441,17 @@ var DEFAULT_WATCH_IGNORES = [
441
441
  ".turbo",
442
442
  "test-results"
443
443
  ];
444
+ var IGNORED_SET = new Set(DEFAULT_IGNORED_DIR_NAMES);
445
+ function isIgnoredDirName(name) {
446
+ return IGNORED_SET.has(name) || name.endsWith(".tsbuildinfo");
447
+ }
448
+
449
+ // src/server/workspace/createNodeWorkspace.ts
450
+ var EPERM_CODE = "EPERM";
444
451
  function shouldIgnoreWatchPath(root, path4) {
445
452
  const relPath = relative2(root, path4);
446
453
  const parts = relPath.split(sep);
447
- return parts.some(
448
- (part) => DEFAULT_WATCH_IGNORES.includes(part) || part.endsWith(".tsbuildinfo")
449
- );
454
+ return parts.some((part) => isIgnoredDirName(part));
450
455
  }
451
456
  function createNodeWatcher(root) {
452
457
  const listeners = /* @__PURE__ */ new Set();
@@ -6435,12 +6440,28 @@ function createPiCodingAgentHarness(opts) {
6435
6440
  );
6436
6441
  };
6437
6442
  refreshEffectiveResources();
6443
+ const piSessionCreations = /* @__PURE__ */ new Map();
6438
6444
  async function getOrCreatePiSession(sessionId, input, ctx) {
6439
6445
  const existing = piSessions.get(sessionId);
6440
6446
  if (existing) {
6441
6447
  await applyRequestedSessionOptions(existing, input);
6442
6448
  return existing;
6443
6449
  }
6450
+ const inFlight = piSessionCreations.get(sessionId);
6451
+ if (inFlight) {
6452
+ const handle = await inFlight;
6453
+ await applyRequestedSessionOptions(handle, input);
6454
+ return handle;
6455
+ }
6456
+ const creation = createPiSession(sessionId, input, ctx);
6457
+ piSessionCreations.set(sessionId, creation);
6458
+ try {
6459
+ return await creation;
6460
+ } finally {
6461
+ if (piSessionCreations.get(sessionId) === creation) piSessionCreations.delete(sessionId);
6462
+ }
6463
+ }
6464
+ async function createPiSession(sessionId, input, ctx) {
6444
6465
  const authStorage = AuthStorage.create();
6445
6466
  const modelRegistry = ModelRegistry.create(authStorage);
6446
6467
  registerConfiguredModelProviders(modelRegistry);
@@ -7588,7 +7609,8 @@ function directSpawnHook(workspaceRoot, runtime) {
7588
7609
  ...context,
7589
7610
  env: withWorkspacePythonEnv({
7590
7611
  workspaceRoot,
7591
- env: mergeRuntimeEnv(runtime, context.env)
7612
+ env: mergeRuntimeEnv(runtime, context.env),
7613
+ preserveHostHome: true
7592
7614
  })
7593
7615
  });
7594
7616
  }
@@ -7990,7 +8012,7 @@ async function listTree(workspace, dir, recursive) {
7990
8012
  if (entries.length >= MAX_ENTRIES) break;
7991
8013
  const entryPath = joinPath2(batch.parentDir, e.name);
7992
8014
  entries.push({ name: e.name, kind: e.kind, path: entryPath });
7993
- if (recursive && e.kind === "dir" && batch.depth < MAX_DEPTH) {
8015
+ if (recursive && e.kind === "dir" && batch.depth < MAX_DEPTH && !isIgnoredDirName(e.name)) {
7994
8016
  try {
7995
8017
  const subEntries = await workspace.readdir(entryPath);
7996
8018
  queue.push({ parentDir: entryPath, items: subEntries, depth: batch.depth + 1 });
@@ -10089,6 +10111,11 @@ var HarnessPiChatService = class {
10089
10111
  sessionStore;
10090
10112
  workdir;
10091
10113
  channels = /* @__PURE__ */ new Map();
10114
+ // Single-flight guard so concurrent cold callers (e.g. two browser tabs each
10115
+ // opening /events while the session is still being created) converge on one
10116
+ // LiveSessionChannel instead of racing through ensureChannel and orphaning
10117
+ // the loser's adapter subscription.
10118
+ channelCreations = /* @__PURE__ */ new Map();
10092
10119
  messageMetadata = new PiChatMessageMetadataReconciler();
10093
10120
  activePromptRuns = /* @__PURE__ */ new Map();
10094
10121
  syntheticPromptFailures = /* @__PURE__ */ new Map();
@@ -10348,10 +10375,38 @@ var HarnessPiChatService = class {
10348
10375
  async getChannel(ctx, sessionId) {
10349
10376
  const existing = this.channels.get(sessionId);
10350
10377
  if (existing) return existing;
10351
- const adapter = await this.getAdapter(ctx, sessionId, "");
10352
- return this.ensureChannel(ctx, sessionId, adapter);
10378
+ return this.createChannelOnce(sessionId, () => this.getAdapter(ctx, sessionId, ""));
10353
10379
  }
10354
10380
  async ensureChannel(ctx, sessionId, adapter) {
10381
+ const existing = this.channels.get(sessionId);
10382
+ if (existing) return existing;
10383
+ return this.createChannelOnce(sessionId, async () => adapter);
10384
+ }
10385
+ /**
10386
+ * Resolve (or create) the single LiveSessionChannel for a session, coalescing
10387
+ * concurrent cold callers onto one in-flight creation. Without this guard two
10388
+ * tabs opening /events on a not-yet-live session both fall through the channel
10389
+ * cache miss, build separate channels + adapter subscriptions, and the second
10390
+ * `this.channels.set` orphans the first — so the first tab's stream silently
10391
+ * stops receiving events.
10392
+ */
10393
+ async createChannelOnce(sessionId, resolveAdapter) {
10394
+ const inFlight = this.channelCreations.get(sessionId);
10395
+ if (inFlight) return inFlight;
10396
+ const creation = (async () => {
10397
+ const existing = this.channels.get(sessionId);
10398
+ if (existing) return existing;
10399
+ const adapter = await resolveAdapter();
10400
+ return this.buildChannel(sessionId, adapter);
10401
+ })();
10402
+ this.channelCreations.set(sessionId, creation);
10403
+ try {
10404
+ return await creation;
10405
+ } finally {
10406
+ if (this.channelCreations.get(sessionId) === creation) this.channelCreations.delete(sessionId);
10407
+ }
10408
+ }
10409
+ buildChannel(sessionId, adapter) {
10355
10410
  const existing = this.channels.get(sessionId);
10356
10411
  if (existing) return existing;
10357
10412
  const buffer = new PiChatReplayBuffer();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hachej/boring-agent",
3
- "version": "0.1.40",
3
+ "version": "0.1.41",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "description": "Pane-embeddable coding agent. Ships direct/local/vercel-sandbox execution modes behind one interface.",
@@ -74,7 +74,7 @@
74
74
  "use-stick-to-bottom": "^1.1.3",
75
75
  "yaml": "^2.8.3",
76
76
  "zod": "^3.25.76",
77
- "@hachej/boring-ui-kit": "0.1.40"
77
+ "@hachej/boring-ui-kit": "0.1.41"
78
78
  },
79
79
  "devDependencies": {
80
80
  "@antithesishq/bombadil": "0.5.0",