@hachej/boring-workspace 0.1.23 → 0.1.24

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,9 +1,9 @@
1
1
  import { PiPackageSource, PiExtensionFactory, CreateAgentAppOptions, ProvisionWorkspaceRuntimeOptions } from '@hachej/boring-agent/server';
2
2
  export { PiPackageSource as WorkspacePiPackageSource } from '@hachej/boring-agent/server';
3
3
  import { FastifyInstance } from 'fastify';
4
- import { W as WorkspaceServerPlugin, S as ServerBootstrapOptions, a as WorkspaceProvisioningContribution, b as WorkspaceRouteContribution, c as createInMemoryBridge } from './createInMemoryBridge-CYNW1h_o.js';
5
- export { d as ServerWorkspaceRuntimeProvisioningInput } from './createInMemoryBridge-CYNW1h_o.js';
6
- import './ui-bridge-CT18yqwN.js';
4
+ import { W as WorkspaceServerPlugin, S as ServerBootstrapOptions, a as WorkspaceProvisioningContribution, b as WorkspaceRouteContribution, c as createInMemoryBridge } from './createInMemoryBridge-DLckqafe.js';
5
+ export { d as ServerWorkspaceRuntimeProvisioningInput } from './createInMemoryBridge-DLckqafe.js';
6
+ import './ui-bridge-Bdgl2hR8.js';
7
7
 
8
8
  /**
9
9
  * Directory-source entry: `{ dir, options?, hotReload? }`. Resolved via
@@ -162,8 +162,8 @@ function validateBoringField(issues, boring) {
162
162
  ));
163
163
  }
164
164
  }
165
- if (boring.id !== void 0) {
166
- issues.push(issue("INVALID_FIELD", "boring.id", "boring.id is not supported; package discovery identity comes from package.json#name"));
165
+ if (boring.id !== void 0 && (typeof boring.id !== "string" || !isValidBoringPluginId(boring.id))) {
166
+ issues.push(issue("INVALID_ID", "boring.id", "boring.id must start with a letter or number and use only letters, numbers, dot, underscore, colon, or dash"));
167
167
  }
168
168
  const front = boring.front;
169
169
  if (front !== void 0 && (typeof front !== "string" || !isSafePluginRelativePath(front))) {
@@ -177,6 +177,7 @@ function validateBoringField(issues, boring) {
177
177
  issues.push(issue("INVALID_FIELD", "boring.label", "boring.label must be a string when provided"));
178
178
  }
179
179
  return {
180
+ ...typeof boring.id === "string" ? { id: boring.id } : {},
180
181
  ...typeof boring.front === "string" ? { front: boring.front } : {},
181
182
  ...typeof boring.server === "string" || boring.server === false ? { server: boring.server } : {},
182
183
  ...typeof boring.label === "string" ? { label: boring.label } : {}
@@ -343,6 +344,8 @@ function resolveSafePluginEntryPath({
343
344
 
344
345
  // src/server/agentPlugins/scan.ts
345
346
  function pluginIdFromPackageJson(pkg, rootDir) {
347
+ const explicitId = typeof pkg.boring?.id === "string" && pkg.boring.id.trim() ? pkg.boring.id.trim() : void 0;
348
+ if (explicitId) return explicitId;
346
349
  const name = typeof pkg.name === "string" && pkg.name.trim() ? pkg.name.trim() : void 0;
347
350
  return (name ?? rootDir.split(/[\\/]/).at(-1) ?? "plugin").replace(/^@/, "").replaceAll("/", "-");
348
351
  }
@@ -526,8 +529,8 @@ import { dirname as dirname4, join as join3 } from "path";
526
529
  var PLUGIN_SIGNATURE_CACHE_FILE = ".boring-signature.json";
527
530
  function pluginFileSignature(path) {
528
531
  if (!path || !existsSync3(path)) return "missing";
529
- const stat = statSync2(path);
530
- return `${stat.mtimeMs}:${stat.size}`;
532
+ const stat2 = statSync2(path);
533
+ return `${stat2.mtimeMs}:${stat2.size}`;
531
534
  }
532
535
  function cachePath(pluginRootDir) {
533
536
  return join3(pluginRootDir, PLUGIN_SIGNATURE_CACHE_FILE);
@@ -571,8 +574,8 @@ function directorySignature(root) {
571
574
  count++;
572
575
  const path = join4(dir, entry.name);
573
576
  const rel = relative2(root, path);
574
- const stat = lstatSync(path);
575
- if (stat.isSymbolicLink()) {
577
+ const stat2 = lstatSync(path);
578
+ if (stat2.isSymbolicLink()) {
576
579
  let target;
577
580
  try {
578
581
  target = realpathSync2(path);
@@ -597,9 +600,9 @@ function directorySignature(root) {
597
600
  continue;
598
601
  }
599
602
  hash.update(rel);
600
- hash.update(String(stat.mtimeMs));
601
- hash.update(String(stat.size));
602
- if (stat.isDirectory()) {
603
+ hash.update(String(stat2.mtimeMs));
604
+ hash.update(String(stat2.size));
605
+ if (stat2.isDirectory()) {
603
606
  visit(path, depth + 1);
604
607
  }
605
608
  }
@@ -1393,7 +1396,7 @@ function createInMemoryBridge() {
1393
1396
  }
1394
1397
 
1395
1398
  // src/server/ui-control/tools/uiTools.ts
1396
- import { access } from "fs/promises";
1399
+ import { stat } from "fs/promises";
1397
1400
  import { resolve as resolve6, isAbsolute as isAbsolute4, relative as relative3, win32 } from "path";
1398
1401
  function makeError(message) {
1399
1402
  return {
@@ -1431,7 +1434,7 @@ function validatePathSyntax(relPath, workspaceRoot) {
1431
1434
  }
1432
1435
  return { ok: true };
1433
1436
  }
1434
- async function validatePath(workspaceRoot, relPath) {
1437
+ async function validateExistingPath(workspaceRoot, relPath) {
1435
1438
  const syntax = validatePathSyntax(relPath, workspaceRoot);
1436
1439
  if (!syntax.ok) return syntax;
1437
1440
  const resolved = resolve6(workspaceRoot, relPath);
@@ -1443,8 +1446,8 @@ async function validatePath(workspaceRoot, relPath) {
1443
1446
  };
1444
1447
  }
1445
1448
  try {
1446
- await access(resolved);
1447
- return { ok: true };
1449
+ const fileStat = await stat(resolved);
1450
+ return { ok: true, kind: fileStat.isDirectory() ? "dir" : "file" };
1448
1451
  } catch {
1449
1452
  return {
1450
1453
  ok: false,
@@ -1455,6 +1458,7 @@ async function validatePath(workspaceRoot, relPath) {
1455
1458
  function createGetUiStateTool(uiBridge) {
1456
1459
  return {
1457
1460
  name: "get_ui_state",
1461
+ readinessRequirements: ["ui-bridge"],
1458
1462
  description: [
1459
1463
  "Read the current workspace UI state. Returns a JSON object with:",
1460
1464
  "- workbenchOpen (boolean): is the right-side workbench pane visible?",
@@ -1508,12 +1512,13 @@ function isVerified(kind, params, state) {
1508
1512
  return true;
1509
1513
  }
1510
1514
  function createExecUiTool(uiBridge, opts = {}) {
1511
- const { workspaceRoot } = opts;
1515
+ const { workspaceRoot, resolvePathKind } = opts;
1512
1516
  const verifyDelayMs = opts.verifyDelayMs ?? 200;
1513
1517
  const verifyRetries = opts.verifyRetries ?? 2;
1514
1518
  const verifyIntervalMs = opts.verifyIntervalMs ?? 200;
1515
1519
  return {
1516
1520
  name: "exec_ui",
1521
+ readinessRequirements: ["ui-bridge"],
1517
1522
  description: [
1518
1523
  "Execute a UI command in the workspace. Use this to open files, panels,",
1519
1524
  "navigate to lines, or show notifications.",
@@ -1547,6 +1552,8 @@ function createExecUiTool(uiBridge, opts = {}) {
1547
1552
  " returned \u2014 don't give up and don't switch to the read",
1548
1553
  " tool. Repeat until openFile succeeds or no candidate",
1549
1554
  " is found.",
1555
+ " If the path is a folder, openFile reveals/selects it in",
1556
+ " the file tree instead of opening an editor tab.",
1550
1557
  " Example: {kind:'openFile', params:{path:'README.md'}}",
1551
1558
  "",
1552
1559
  " openPanel params: { id: string, component: string,",
@@ -1630,6 +1637,7 @@ function createExecUiTool(uiBridge, opts = {}) {
1630
1637
  return makeError("openSurface: meta must be an object when provided");
1631
1638
  }
1632
1639
  }
1640
+ let effectiveKind = kind;
1633
1641
  if (PATH_BEARING_KINDS.has(kind)) {
1634
1642
  const relPath = getPathParam(kind, cmdParams);
1635
1643
  if (!relPath) {
@@ -1640,14 +1648,25 @@ function createExecUiTool(uiBridge, opts = {}) {
1640
1648
  const syntax = validatePathSyntax(relPath, workspaceRoot);
1641
1649
  if (!syntax.ok) return makeError(syntax.reason);
1642
1650
  if (workspaceRoot) {
1643
- const check = await validatePath(workspaceRoot, relPath);
1651
+ const check = await validateExistingPath(workspaceRoot, relPath);
1644
1652
  if (!check.ok) {
1645
1653
  return makeError(check.reason);
1646
1654
  }
1655
+ if (kind === "openFile" && check.kind === "dir") {
1656
+ effectiveKind = "expandToFile";
1657
+ }
1658
+ } else if (resolvePathKind) {
1659
+ const pathKind = await resolvePathKind(relPath);
1660
+ if (!pathKind) {
1661
+ return makeError(`file not found at "${relPath}". Try find or grep to locate the file before retrying openFile.`);
1662
+ }
1663
+ if (kind === "openFile" && pathKind === "dir") {
1664
+ effectiveKind = "expandToFile";
1665
+ }
1647
1666
  }
1648
1667
  }
1649
1668
  try {
1650
- const command = { kind, params: cmdParams };
1669
+ const command = { kind: effectiveKind, params: cmdParams };
1651
1670
  const result = await uiBridge.postCommand(command);
1652
1671
  if (result.status === "error") {
1653
1672
  return {
@@ -1656,11 +1675,11 @@ function createExecUiTool(uiBridge, opts = {}) {
1656
1675
  details: result
1657
1676
  };
1658
1677
  }
1659
- if (verifyDelayMs > 0 && VERIFIABLE_KINDS.has(kind)) {
1678
+ if (verifyDelayMs > 0 && VERIFIABLE_KINDS.has(effectiveKind)) {
1660
1679
  await new Promise((r) => setTimeout(r, verifyDelayMs));
1661
1680
  let uiState = await uiBridge.getState();
1662
1681
  for (let i = 0; i < verifyRetries; i++) {
1663
- if (isVerified(kind, cmdParams, uiState)) break;
1682
+ if (isVerified(effectiveKind, cmdParams, uiState)) break;
1664
1683
  await new Promise((r) => setTimeout(r, verifyIntervalMs));
1665
1684
  uiState = await uiBridge.getState();
1666
1685
  }
@@ -1831,6 +1850,15 @@ function resolveWorkspacePackageRoot() {
1831
1850
  }
1832
1851
  return join7(__dirname, "../../..");
1833
1852
  }
1853
+ function readPackageVersion(packageRoot) {
1854
+ if (!packageRoot) return void 0;
1855
+ try {
1856
+ const pkg = JSON.parse(readFileSync6(join7(packageRoot, "package.json"), "utf8"));
1857
+ return typeof pkg.version === "string" && pkg.version.length > 0 ? pkg.version : void 0;
1858
+ } catch {
1859
+ return void 0;
1860
+ }
1861
+ }
1834
1862
  function nodePackageContribution(contributionId, nodePackageId, packageName, packageRoot) {
1835
1863
  if (!packageRoot || !existsSync7(join7(packageRoot, "package.json"))) return null;
1836
1864
  return {
@@ -1840,6 +1868,24 @@ function nodePackageContribution(contributionId, nodePackageId, packageName, pac
1840
1868
  }
1841
1869
  };
1842
1870
  }
1871
+ function publishedNodePackageContribution(contributionId, nodePackageId, packageName, version, expectedBins) {
1872
+ return {
1873
+ id: contributionId,
1874
+ provisioning: {
1875
+ nodePackages: [
1876
+ {
1877
+ id: nodePackageId,
1878
+ packageName,
1879
+ ...version ? { version } : {},
1880
+ ...expectedBins ? { expectedBins } : {}
1881
+ }
1882
+ ]
1883
+ }
1884
+ };
1885
+ }
1886
+ function useLocalPackageProvisioning() {
1887
+ return process.env.BORING_USE_LOCAL_PACKAGES === "1";
1888
+ }
1843
1889
  function createWorkspacePackageProvisioningContribution() {
1844
1890
  return nodePackageContribution(
1845
1891
  "boring-workspace-package",
@@ -1890,11 +1936,21 @@ function resolveBoringUiCliPackageRoot() {
1890
1936
  }
1891
1937
  }
1892
1938
  function createBoringUiCliPackageProvisioningContribution() {
1893
- return nodePackageContribution(
1939
+ const packageRoot = resolveBoringUiCliPackageRoot();
1940
+ if (useLocalPackageProvisioning()) {
1941
+ return nodePackageContribution(
1942
+ "boring-ui-cli-package",
1943
+ "boring-ui-cli",
1944
+ "@hachej/boring-ui-cli",
1945
+ packageRoot
1946
+ );
1947
+ }
1948
+ return publishedNodePackageContribution(
1894
1949
  "boring-ui-cli-package",
1895
1950
  "boring-ui-cli",
1896
1951
  "@hachej/boring-ui-cli",
1897
- resolveBoringUiCliPackageRoot()
1952
+ readPackageVersion(packageRoot) ?? readPackageVersion(resolveWorkspacePackageRoot()),
1953
+ ["boring-ui"]
1898
1954
  );
1899
1955
  }
1900
1956
  function createBoringPiPackageSource(workspaceRoot) {
@@ -2008,6 +2064,12 @@ function skillNameFromResolvedPath(path) {
2008
2064
  if (leaf.toLowerCase() !== "skill.md") return leaf;
2009
2065
  return path.split(/[\\/]/).filter(Boolean).at(-2) ?? "skill";
2010
2066
  }
2067
+ function skillPathForPiLoader(path) {
2068
+ return existsSync7(join7(path, "SKILL.md")) ? dirname7(path) : path;
2069
+ }
2070
+ function uniqueStrings(values) {
2071
+ return [...new Set(values)];
2072
+ }
2011
2073
  function readWorkspacePluginPackageRuntimePlugins(pluginDirs) {
2012
2074
  const scan = scanBoringPlugins(pluginDirs);
2013
2075
  return scan.plugins.map((plugin) => ({
@@ -2032,7 +2094,9 @@ function readWorkspacePluginPackagePiSnapshot(pluginDirs) {
2032
2094
  const scan = scanBoringPlugins(pluginDirs);
2033
2095
  const systemPromptAppend = aggregatePluginSystemPromptsFromScan(scan);
2034
2096
  return {
2035
- additionalSkillPaths: scan.plugins.flatMap((plugin) => plugin.skillPaths ?? []),
2097
+ additionalSkillPaths: uniqueStrings(
2098
+ scan.plugins.flatMap((plugin) => plugin.skillPaths ?? []).map(skillPathForPiLoader)
2099
+ ),
2036
2100
  packages: compactPiPackages(normalizeBoringPluginPiPackages(scan.plugins)),
2037
2101
  extensionPaths: scan.plugins.flatMap((plugin) => plugin.extensionPaths ?? []),
2038
2102
  ...systemPromptAppend ? { systemPromptAppend } : {}
@@ -1,6 +1,6 @@
1
1
  import { PiPackageSource, PluginSkillSource, ProvisionWorkspaceRuntimeOptions } from '@hachej/boring-agent/server';
2
2
  import { FastifyPluginAsync } from 'fastify';
3
- import { A as AgentTool, U as UiBridge } from './ui-bridge-CT18yqwN.js';
3
+ import { A as AgentTool, U as UiBridge } from './ui-bridge-Bdgl2hR8.js';
4
4
 
5
5
  type WorkspaceRuntimeProvisioning = NonNullable<ProvisionWorkspaceRuntimeOptions["plugins"][number]["provisioning"]>;
6
6
  interface WorkspaceServerPlugin {
package/dist/events.d.ts CHANGED
@@ -58,6 +58,9 @@ type WorkspacePanelMatch = {
58
58
  } | {
59
59
  param: string;
60
60
  value: unknown;
61
+ } | {
62
+ paramPrefix: string;
63
+ value: string;
61
64
  };
62
65
  /**
63
66
  * Discriminated origin metadata. Encoded as a union (rather than a
@@ -8,6 +8,8 @@
8
8
  * - `boring`: workspace/UI package discovery (front/server entrypoints and labels)
9
9
  */
10
10
  interface BoringPackageBoringField {
11
+ /** Optional stable plugin id. Defaults to package.json#name normalized for package discovery. */
12
+ id?: string;
11
13
  /** Browser entry that default-exports a BoringFrontFactory. */
12
14
  front?: string;
13
15
  /** Workspace/UI support server entry. Set false to disable convention lookup. */
package/dist/plugin.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { ComponentType, ReactNode } from 'react';
2
- import { P as PaneProps, a as PanelConfig, S as SurfaceOpenRequest, c as SurfacePanelResolution } from './surface-COYagY2m.js';
3
- export { W as WORKSPACE_OPEN_PATH_SURFACE_KIND } from './surface-COYagY2m.js';
4
- export { B as BoringPackageBoringField, a as BoringPackagePiField, b as BoringPackagePiSource, c as BoringPackagePiSourceObject, d as BoringPluginManifestErrorCode, e as BoringPluginManifestIssue, f as BoringPluginManifestValidationResult, g as BoringPluginPackageJson, i as isSafePluginRelativePath, h as isValidBoringPluginId, v as validateBoringPluginManifest } from './manifest-CyNNdfYz.js';
2
+ import { P as PaneProps, a as PanelConfig, S as SurfaceOpenRequest, c as SurfacePanelResolution } from './surface-CEEkd81D.js';
3
+ export { W as WORKSPACE_OPEN_PATH_SURFACE_KIND } from './surface-CEEkd81D.js';
4
+ export { B as BoringPackageBoringField, a as BoringPackagePiField, b as BoringPackagePiSource, c as BoringPackagePiSourceObject, d as BoringPluginManifestErrorCode, e as BoringPluginManifestIssue, f as BoringPluginManifestValidationResult, g as BoringPluginPackageJson, i as isSafePluginRelativePath, h as isValidBoringPluginId, v as validateBoringPluginManifest } from './manifest-C2vVgH_e.js';
5
5
  import 'dockview-react';
6
6
 
7
7
  type CatalogBadge = {
@@ -69,6 +69,10 @@ interface LeftTabParams {
69
69
  searchQuery?: string;
70
70
  bridge?: unknown;
71
71
  chromeless?: boolean;
72
+ revealFileTreeRequest?: {
73
+ path: string;
74
+ seq: number;
75
+ } | null;
72
76
  }
73
77
 
74
78
  interface BoringFrontPanelRegistration<T = unknown> {
@@ -85,6 +89,7 @@ interface BoringFrontPanelRegistration<T = unknown> {
85
89
  essential?: boolean;
86
90
  lazy?: boolean;
87
91
  chromeless?: boolean;
92
+ supportsFullPage?: boolean;
88
93
  source?: string;
89
94
  }
90
95
  interface BoringFrontPanelCommandRegistration {
package/dist/plugin.js CHANGED
@@ -183,8 +183,8 @@ function validateBoringField(issues, boring) {
183
183
  ));
184
184
  }
185
185
  }
186
- if (boring.id !== void 0) {
187
- issues.push(issue("INVALID_FIELD", "boring.id", "boring.id is not supported; package discovery identity comes from package.json#name"));
186
+ if (boring.id !== void 0 && (typeof boring.id !== "string" || !isValidBoringPluginId(boring.id))) {
187
+ issues.push(issue("INVALID_ID", "boring.id", "boring.id must start with a letter or number and use only letters, numbers, dot, underscore, colon, or dash"));
188
188
  }
189
189
  const front = boring.front;
190
190
  if (front !== void 0 && (typeof front !== "string" || !isSafePluginRelativePath(front))) {
@@ -198,6 +198,7 @@ function validateBoringField(issues, boring) {
198
198
  issues.push(issue("INVALID_FIELD", "boring.label", "boring.label must be a string when provided"));
199
199
  }
200
200
  return {
201
+ ...typeof boring.id === "string" ? { id: boring.id } : {},
201
202
  ...typeof boring.front === "string" ? { front: boring.front } : {},
202
203
  ...typeof boring.server === "string" || boring.server === false ? { server: boring.server } : {},
203
204
  ...typeof boring.label === "string" ? { label: boring.label } : {}
package/dist/server.d.ts CHANGED
@@ -1,8 +1,8 @@
1
- export { S as ServerBootstrapOptions, e as ServerBootstrapResult, a as WorkspaceProvisioningContribution, b as WorkspaceRouteContribution, W as WorkspaceServerPlugin, f as bootstrapServer, c as createInMemoryBridge, g as defineServerPlugin, v as validateServerPlugin } from './createInMemoryBridge-CYNW1h_o.js';
1
+ export { S as ServerBootstrapOptions, e as ServerBootstrapResult, a as WorkspaceProvisioningContribution, b as WorkspaceRouteContribution, W as WorkspaceServerPlugin, f as bootstrapServer, c as createInMemoryBridge, g as defineServerPlugin, v as validateServerPlugin } from './createInMemoryBridge-DLckqafe.js';
2
2
  import { FastifyRequest, FastifyInstance } from 'fastify';
3
- import { U as UiBridge, A as AgentTool } from './ui-bridge-CT18yqwN.js';
4
- export { C as CommandResult, a as UiCommand, b as UiState } from './ui-bridge-CT18yqwN.js';
5
- import { B as BoringPackageBoringField, a as BoringPackagePiField } from './manifest-CyNNdfYz.js';
3
+ import { U as UiBridge, A as AgentTool } from './ui-bridge-Bdgl2hR8.js';
4
+ export { C as CommandResult, a as UiCommand, b as UiState } from './ui-bridge-Bdgl2hR8.js';
5
+ import { B as BoringPackageBoringField, a as BoringPackagePiField } from './manifest-C2vVgH_e.js';
6
6
  export { PiPackageSource as WorkspacePiPackageSource } from '@hachej/boring-agent/server';
7
7
 
8
8
  interface UiRoutesOptions {
@@ -29,6 +29,13 @@ interface ExecUiToolOptions {
29
29
  * but existence is left to the frontend/remote filesystem.
30
30
  */
31
31
  workspaceRoot?: string;
32
+ /**
33
+ * Optional workspace-backed stat hook for modes where the host path is not
34
+ * directly stat-able (for example remote sandboxes). When provided, path-
35
+ * bearing UI commands can still reject missing paths and convert folders to
36
+ * file-tree reveal commands.
37
+ */
38
+ resolvePathKind?: (relPath: string) => Promise<"file" | "dir" | null>;
32
39
  /**
33
40
  * After dispatching a state-changing command (openFile, openPanel,
34
41
  * openSurface, closePanel), wait this many ms before the first state
package/dist/server.js CHANGED
@@ -165,7 +165,7 @@ data: ${JSON.stringify({ v: UI_BRIDGE_PROTOCOL_VERSION })}
165
165
  }
166
166
 
167
167
  // src/server/ui-control/tools/uiTools.ts
168
- import { access } from "fs/promises";
168
+ import { stat } from "fs/promises";
169
169
  import { resolve, isAbsolute, relative, win32 } from "path";
170
170
  function makeError(message) {
171
171
  return {
@@ -203,7 +203,7 @@ function validatePathSyntax(relPath, workspaceRoot) {
203
203
  }
204
204
  return { ok: true };
205
205
  }
206
- async function validatePath(workspaceRoot, relPath) {
206
+ async function validateExistingPath(workspaceRoot, relPath) {
207
207
  const syntax = validatePathSyntax(relPath, workspaceRoot);
208
208
  if (!syntax.ok) return syntax;
209
209
  const resolved = resolve(workspaceRoot, relPath);
@@ -215,8 +215,8 @@ async function validatePath(workspaceRoot, relPath) {
215
215
  };
216
216
  }
217
217
  try {
218
- await access(resolved);
219
- return { ok: true };
218
+ const fileStat = await stat(resolved);
219
+ return { ok: true, kind: fileStat.isDirectory() ? "dir" : "file" };
220
220
  } catch {
221
221
  return {
222
222
  ok: false,
@@ -227,6 +227,7 @@ async function validatePath(workspaceRoot, relPath) {
227
227
  function createGetUiStateTool(uiBridge) {
228
228
  return {
229
229
  name: "get_ui_state",
230
+ readinessRequirements: ["ui-bridge"],
230
231
  description: [
231
232
  "Read the current workspace UI state. Returns a JSON object with:",
232
233
  "- workbenchOpen (boolean): is the right-side workbench pane visible?",
@@ -280,12 +281,13 @@ function isVerified(kind, params, state) {
280
281
  return true;
281
282
  }
282
283
  function createExecUiTool(uiBridge, opts = {}) {
283
- const { workspaceRoot } = opts;
284
+ const { workspaceRoot, resolvePathKind } = opts;
284
285
  const verifyDelayMs = opts.verifyDelayMs ?? 200;
285
286
  const verifyRetries = opts.verifyRetries ?? 2;
286
287
  const verifyIntervalMs = opts.verifyIntervalMs ?? 200;
287
288
  return {
288
289
  name: "exec_ui",
290
+ readinessRequirements: ["ui-bridge"],
289
291
  description: [
290
292
  "Execute a UI command in the workspace. Use this to open files, panels,",
291
293
  "navigate to lines, or show notifications.",
@@ -319,6 +321,8 @@ function createExecUiTool(uiBridge, opts = {}) {
319
321
  " returned \u2014 don't give up and don't switch to the read",
320
322
  " tool. Repeat until openFile succeeds or no candidate",
321
323
  " is found.",
324
+ " If the path is a folder, openFile reveals/selects it in",
325
+ " the file tree instead of opening an editor tab.",
322
326
  " Example: {kind:'openFile', params:{path:'README.md'}}",
323
327
  "",
324
328
  " openPanel params: { id: string, component: string,",
@@ -402,6 +406,7 @@ function createExecUiTool(uiBridge, opts = {}) {
402
406
  return makeError("openSurface: meta must be an object when provided");
403
407
  }
404
408
  }
409
+ let effectiveKind = kind;
405
410
  if (PATH_BEARING_KINDS.has(kind)) {
406
411
  const relPath = getPathParam(kind, cmdParams);
407
412
  if (!relPath) {
@@ -412,14 +417,25 @@ function createExecUiTool(uiBridge, opts = {}) {
412
417
  const syntax = validatePathSyntax(relPath, workspaceRoot);
413
418
  if (!syntax.ok) return makeError(syntax.reason);
414
419
  if (workspaceRoot) {
415
- const check = await validatePath(workspaceRoot, relPath);
420
+ const check = await validateExistingPath(workspaceRoot, relPath);
416
421
  if (!check.ok) {
417
422
  return makeError(check.reason);
418
423
  }
424
+ if (kind === "openFile" && check.kind === "dir") {
425
+ effectiveKind = "expandToFile";
426
+ }
427
+ } else if (resolvePathKind) {
428
+ const pathKind = await resolvePathKind(relPath);
429
+ if (!pathKind) {
430
+ return makeError(`file not found at "${relPath}". Try find or grep to locate the file before retrying openFile.`);
431
+ }
432
+ if (kind === "openFile" && pathKind === "dir") {
433
+ effectiveKind = "expandToFile";
434
+ }
419
435
  }
420
436
  }
421
437
  try {
422
- const command = { kind, params: cmdParams };
438
+ const command = { kind: effectiveKind, params: cmdParams };
423
439
  const result = await uiBridge.postCommand(command);
424
440
  if (result.status === "error") {
425
441
  return {
@@ -428,11 +444,11 @@ function createExecUiTool(uiBridge, opts = {}) {
428
444
  details: result
429
445
  };
430
446
  }
431
- if (verifyDelayMs > 0 && VERIFIABLE_KINDS.has(kind)) {
447
+ if (verifyDelayMs > 0 && VERIFIABLE_KINDS.has(effectiveKind)) {
432
448
  await new Promise((r) => setTimeout(r, verifyDelayMs));
433
449
  let uiState = await uiBridge.getState();
434
450
  for (let i = 0; i < verifyRetries; i++) {
435
- if (isVerified(kind, cmdParams, uiState)) break;
451
+ if (isVerified(effectiveKind, cmdParams, uiState)) break;
436
452
  await new Promise((r) => setTimeout(r, verifyIntervalMs));
437
453
  uiState = await uiBridge.getState();
438
454
  }
@@ -854,8 +870,8 @@ function validateBoringField(issues, boring) {
854
870
  ));
855
871
  }
856
872
  }
857
- if (boring.id !== void 0) {
858
- issues.push(issue("INVALID_FIELD", "boring.id", "boring.id is not supported; package discovery identity comes from package.json#name"));
873
+ if (boring.id !== void 0 && (typeof boring.id !== "string" || !isValidBoringPluginId(boring.id))) {
874
+ issues.push(issue("INVALID_ID", "boring.id", "boring.id must start with a letter or number and use only letters, numbers, dot, underscore, colon, or dash"));
859
875
  }
860
876
  const front = boring.front;
861
877
  if (front !== void 0 && (typeof front !== "string" || !isSafePluginRelativePath(front))) {
@@ -869,6 +885,7 @@ function validateBoringField(issues, boring) {
869
885
  issues.push(issue("INVALID_FIELD", "boring.label", "boring.label must be a string when provided"));
870
886
  }
871
887
  return {
888
+ ...typeof boring.id === "string" ? { id: boring.id } : {},
872
889
  ...typeof boring.front === "string" ? { front: boring.front } : {},
873
890
  ...typeof boring.server === "string" || boring.server === false ? { server: boring.server } : {},
874
891
  ...typeof boring.label === "string" ? { label: boring.label } : {}
@@ -998,6 +1015,8 @@ function resolveContainedPluginPath(rootDir, value, options = {}) {
998
1015
 
999
1016
  // src/server/agentPlugins/scan.ts
1000
1017
  function pluginIdFromPackageJson(pkg, rootDir) {
1018
+ const explicitId = typeof pkg.boring?.id === "string" && pkg.boring.id.trim() ? pkg.boring.id.trim() : void 0;
1019
+ if (explicitId) return explicitId;
1001
1020
  const name = typeof pkg.name === "string" && pkg.name.trim() ? pkg.name.trim() : void 0;
1002
1021
  return (name ?? rootDir.split(/[\\/]/).at(-1) ?? "plugin").replace(/^@/, "").replaceAll("/", "-");
1003
1022
  }
@@ -1177,8 +1196,8 @@ import { dirname as dirname4, join as join3 } from "path";
1177
1196
  var PLUGIN_SIGNATURE_CACHE_FILE = ".boring-signature.json";
1178
1197
  function pluginFileSignature(path) {
1179
1198
  if (!path || !existsSync3(path)) return "missing";
1180
- const stat = statSync2(path);
1181
- return `${stat.mtimeMs}:${stat.size}`;
1199
+ const stat2 = statSync2(path);
1200
+ return `${stat2.mtimeMs}:${stat2.size}`;
1182
1201
  }
1183
1202
  function cachePath(pluginRootDir) {
1184
1203
  return join3(pluginRootDir, PLUGIN_SIGNATURE_CACHE_FILE);
@@ -1245,8 +1264,8 @@ function directorySignature(root) {
1245
1264
  count++;
1246
1265
  const path = join4(dir, entry.name);
1247
1266
  const rel = relative3(root, path);
1248
- const stat = lstatSync(path);
1249
- if (stat.isSymbolicLink()) {
1267
+ const stat2 = lstatSync(path);
1268
+ if (stat2.isSymbolicLink()) {
1250
1269
  let target;
1251
1270
  try {
1252
1271
  target = realpathSync2(path);
@@ -1271,9 +1290,9 @@ function directorySignature(root) {
1271
1290
  continue;
1272
1291
  }
1273
1292
  hash.update(rel);
1274
- hash.update(String(stat.mtimeMs));
1275
- hash.update(String(stat.size));
1276
- if (stat.isDirectory()) {
1293
+ hash.update(String(stat2.mtimeMs));
1294
+ hash.update(String(stat2.size));
1295
+ if (stat2.isDirectory()) {
1277
1296
  visit(path, depth + 1);
1278
1297
  }
1279
1298
  }
package/dist/shared.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- export { A as AgentTool, C as CommandResult, J as JSONSchema, T as ToolExecContext, c as ToolResult, U as UiBridge, a as UiCommand, b as UiState } from './ui-bridge-CT18yqwN.js';
2
- export { C as CommandConfig, P as PaneProps, a as PanelConfig, b as PanelRegistration, S as SurfaceOpenRequest, c as SurfacePanelResolution, d as SurfaceResolverConfig, e as SurfaceResolverRegistration, W as WORKSPACE_OPEN_PATH_SURFACE_KIND, f as definePanel } from './surface-COYagY2m.js';
1
+ export { A as AgentTool, C as CommandResult, J as JSONSchema, T as ToolExecContext, c as ToolResult, U as UiBridge, a as UiCommand, b as UiState } from './ui-bridge-Bdgl2hR8.js';
2
+ export { C as CommandConfig, P as PaneProps, a as PanelConfig, b as PanelRegistration, S as SurfaceOpenRequest, c as SurfacePanelResolution, d as SurfaceResolverConfig, e as SurfaceResolverRegistration, W as WORKSPACE_OPEN_PATH_SURFACE_KIND, f as definePanel } from './surface-CEEkd81D.js';
3
3
  import 'react';
4
4
  import 'dockview-react';
5
5
 
@@ -46,6 +46,7 @@ interface PanelConfig<T = any> {
46
46
  requiresCapabilities?: string[];
47
47
  essential?: boolean;
48
48
  chromeless?: boolean;
49
+ supportsFullPage?: boolean;
49
50
  /** Source: "builtin" | "app" */
50
51
  source?: string;
51
52
  pluginId?: string;
package/dist/testing.d.ts CHANGED
@@ -230,6 +230,7 @@ declare interface PanelConfig<T = any> {
230
230
  requiresCapabilities?: string[];
231
231
  essential?: boolean;
232
232
  chromeless?: boolean;
233
+ supportsFullPage?: boolean;
233
234
  /** Source: "builtin" | "app" */
234
235
  source?: string;
235
236
  pluginId?: string;