@fungi.computer/nacre 0.0.3 → 0.1.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.
@@ -105,7 +105,7 @@ export declare const appDirectoryEntrySchema: z.ZodObject<{
105
105
  agent?: (Readonly<{
106
106
  order?: number | undefined;
107
107
  }> & Readonly<{
108
- keybind?: string | undefined;
108
+ keybind? /** The selected Computers and what the App may do on each. */: string | undefined;
109
109
  }>) | undefined;
110
110
  help?: Readonly<{
111
111
  order?: number | undefined;
@@ -298,7 +298,7 @@ export declare const appDirectoryResponseSchema: z.ZodLazy<z.ZodObject<{
298
298
  agent?: (Readonly<{
299
299
  order?: number | undefined;
300
300
  }> & Readonly<{
301
- keybind?: string | undefined;
301
+ keybind? /** The selected Computers and what the App may do on each. */: string | undefined;
302
302
  }>) | undefined;
303
303
  help?: Readonly<{
304
304
  order?: number | undefined;
@@ -58,11 +58,26 @@ export type AppCommandDeclaration = Readonly<{
58
58
  }>>;
59
59
  }>;
60
60
  import type { AppHostHandlers, AppHostReservedChord } from "@fungi.computer/agar/host";
61
- import { type WhistleExecutionInput, type WhistleExecutionOutcome, type WhistleContributionLease, type WhistleCommand, type WhistleMenuProjection, type WhistleRuntime, type WhistleSnapshot } from "@fungi.computer/whistle";
61
+ import { type WhistleExecutionInput, type WhistleExecutionOutcome, type WhistleContributionLease, type WhistleCommand, type WhistleCommandKey, type WhistleMenuProjection, type WhistleProjectionRow, type WhistleRuntime, type WhistleSnapshot } from "@fungi.computer/whistle";
62
62
  /** Select the shell command-palette chord for a Mac or non-Mac keyboard. */
63
63
  export declare function commandPaletteReservedChordForPlatform(mac: boolean): AppHostReservedChord;
64
64
  /** Select the shell command-palette chord from the embedding browser user agent. */
65
65
  export declare function commandPaletteReservedChordForUserAgent(userAgent: string): AppHostReservedChord;
66
+ /** One App command, named by its surface and its declared command id. */
67
+ export type AppCommandTarget = Readonly<{
68
+ /** The App surface, such as `installation:inkcap`. */
69
+ surfaceId: string;
70
+ /** The command id the App's Release declares. */
71
+ commandId: string;
72
+ }>;
73
+ /**
74
+ * The typed Whistle identity of a command a mounted App surface contributed:
75
+ * each mounted surface owns the Whistle namespace named by its surface id.
76
+ *
77
+ * @param target - The App surface and its declared command id.
78
+ * @returns The key every snapshot row of that command carries as `command`.
79
+ */
80
+ export declare function appSurfaceCommandKey(target: AppCommandTarget): WhistleCommandKey;
66
81
  /** Whistle source of Release-advertised rows for Apps not yet mounted. */
67
82
  export declare const appDirectorySourceId = "app-directory";
68
83
  /** The command side of one mounted App's host handlers. */
@@ -76,10 +91,19 @@ export type AppSurfaceCommandMount = Readonly<{
76
91
  }>;
77
92
  /** Commands owned by one pane, projected to globals plus its focus scope. */
78
93
  export type PaneCommands = Readonly<{
79
- /** Qualify a locally contributed id in this pane's own namespace. */
80
- commandId: (id: string) => string;
81
- /** Contribute or replace commands owned by this pane through the returned lease. */
82
- contribute: (commands: readonly WhistleCommand<never>[]) => WhistleContributionLease;
94
+ /**
95
+ * The key of a command this pane contributed.
96
+ *
97
+ * @param id - The command's local id.
98
+ * @param group - The group it was contributed under, if any.
99
+ */
100
+ command: (id: string, group?: string) => WhistleCommandKey;
101
+ /**
102
+ * Contribute or replace commands owned by this pane through the returned
103
+ * lease. A group gives one embedded source (such as a Parakeet contribution)
104
+ * its own namespace inside the pane, so its ids never collide with others.
105
+ */
106
+ contribute: (commands: readonly WhistleCommand<never>[], group?: string) => WhistleContributionLease;
83
107
  /** Run a command by id. */
84
108
  execute: WhistleRuntime["execute"];
85
109
  /** Read the commands and bindings currently available. */
@@ -110,6 +134,11 @@ export type AppSurfaceCommandHost = Readonly<{
110
134
  appTitle: string;
111
135
  command: AppCommandDeclaration;
112
136
  }>[], run: (surfaceId: string, commandId: string) => Promise<void>) => () => void;
137
+ /**
138
+ * The App command a snapshot row runs: a mounted App's own row, or a
139
+ * directory row that opens its App first. Shell and pane rows have none.
140
+ */
141
+ readonly appCommand: (row: Pick<WhistleProjectionRow, "command" | "sourceId">) => AppCommandTarget | undefined;
113
142
  /** Read the menu rows belonging to the focused App. */
114
143
  readonly menu: () => readonly WhistleMenuProjection[];
115
144
  /**
@@ -118,7 +147,7 @@ export type AppSurfaceCommandHost = Readonly<{
118
147
  */
119
148
  readonly active: () => WhistleSnapshot;
120
149
  /** Execute an active row, an agent row, or an exact programmatic action. */
121
- readonly execute: (commandId: string, input: WhistleExecutionInput) => Promise<WhistleExecutionOutcome>;
150
+ readonly execute: (command: WhistleCommandKey, input: WhistleExecutionInput) => Promise<WhistleExecutionOutcome>;
122
151
  /** Read the complete renderer-neutral graph for host consumers. */
123
152
  readonly snapshot: () => WhistleSnapshot;
124
153
  /** Observe graph changes, including contribution replacement and disposal. */
@@ -1,6 +1,11 @@
1
1
  import { AppError } from "@fungi.computer/agar/errors";
2
- import { createWhistle, scopeSnapshot, } from "@fungi.computer/whistle";
2
+ import { createWhistle, sameWhistleCommand, scopeSnapshot, } from "@fungi.computer/whistle";
3
3
  const emptyMenu = Object.freeze([]);
4
+ /** The shell command an App's reserved command-palette chord runs. */
5
+ const commandPaletteCommand = Object.freeze({
6
+ namespace: "shell",
7
+ id: "show-command-palette",
8
+ });
4
9
  const commandPaletteReservedChord = Object.freeze({
5
10
  altKey: false,
6
11
  ctrlKey: false,
@@ -21,6 +26,16 @@ export function commandPaletteReservedChordForPlatform(mac) {
21
26
  export function commandPaletteReservedChordForUserAgent(userAgent) {
22
27
  return commandPaletteReservedChordForPlatform(/Mac|iPhone|iPad/u.test(userAgent));
23
28
  }
29
+ /**
30
+ * The typed Whistle identity of a command a mounted App surface contributed:
31
+ * each mounted surface owns the Whistle namespace named by its surface id.
32
+ *
33
+ * @param target - The App surface and its declared command id.
34
+ * @returns The key every snapshot row of that command carries as `command`.
35
+ */
36
+ export function appSurfaceCommandKey(target) {
37
+ return Object.freeze({ namespace: target.surfaceId, id: target.commandId });
38
+ }
24
39
  /** Whistle source of Release-advertised rows for Apps not yet mounted. */
25
40
  export const appDirectorySourceId = "app-directory";
26
41
  /** Bindings an App may never claim: the leader chord crosses every iframe. */
@@ -154,7 +169,7 @@ export function createAppSurfaceCommandHost(whistle = createWhistle()) {
154
169
  }
155
170
  return {
156
171
  sourceId: mount.id,
157
- namespace: encodeURIComponent(mount.id),
172
+ namespace: mount.id,
158
173
  scope: mount.id,
159
174
  commands: commands.map((command) => {
160
175
  // Agar carries a static availability fact; Whistle evaluates availability
@@ -231,7 +246,9 @@ export function createAppSurfaceCommandHost(whistle = createWhistle()) {
231
246
  throw new Error("App reserved chord is unavailable");
232
247
  if (input.repeat)
233
248
  return;
234
- const outcome = await execute(input.id, { origin: "browser" });
249
+ const outcome = await execute(commandPaletteCommand, {
250
+ origin: "browser",
251
+ });
235
252
  if (outcome.status !== "handled")
236
253
  throw new Error("App reserved chord could not run");
237
254
  };
@@ -247,11 +264,11 @@ export function createAppSurfaceCommandHost(whistle = createWhistle()) {
247
264
  });
248
265
  const agentCommands = Object.freeze({
249
266
  snapshot: agentSnapshot,
250
- execute: (commandId, input) => {
267
+ execute: (command, input) => {
251
268
  if (input.origin !== "agent" ||
252
- !agentSnapshot().agent.some((row) => row.commandId === commandId))
269
+ !agentSnapshot().agent.some((row) => sameWhistleCommand(row.command, command)))
253
270
  return Promise.resolve({ status: "missing" });
254
- return execute(commandId, input);
271
+ return execute(command, input);
255
272
  },
256
273
  });
257
274
  return Object.freeze({
@@ -277,7 +294,9 @@ export function createAppSurfaceCommandHost(whistle = createWhistle()) {
277
294
  throw new Error("Pane command mount ID must not be blank");
278
295
  if (mounts.has(paneId) || panes.has(paneId))
279
296
  throw new Error("Pane command mount ID is already active");
280
- const namespace = `pane-${encodeURIComponent(paneId)}`;
297
+ const namespace = `pane:${paneId}`;
298
+ // A group is a namespace nested under the pane's own.
299
+ const groupNamespace = (group) => group === undefined ? namespace : `${namespace}/${group}`;
281
300
  const ownedCommands = new Map();
282
301
  const state = {
283
302
  disposed: false,
@@ -317,16 +336,18 @@ export function createAppSurfaceCommandHost(whistle = createWhistle()) {
317
336
  throw new Error("Pane commands cannot bind the reserved leader chord");
318
337
  };
319
338
  return Object.freeze({
320
- commandId: (id) => `${namespace}:${id}`,
321
- contribute: (commands) => {
339
+ command: (id, group) => Object.freeze({ namespace: groupNamespace(group), id }),
340
+ contribute: (commands, group) => {
322
341
  assertPaneLive();
323
342
  checkBindings(commands);
343
+ const contributed = groupNamespace(group);
324
344
  const underlying = whistle.contribute({
325
345
  sourceId: `pane:${paneId}`,
326
- namespace,
346
+ namespace: contributed,
327
347
  scope: paneId,
328
348
  commands,
329
349
  });
350
+ const keys = (next) => next.map((command) => Object.freeze({ namespace: contributed, id: command.id }));
330
351
  let active = true;
331
352
  const lease = Object.freeze({
332
353
  update: (replacement) => {
@@ -334,7 +355,7 @@ export function createAppSurfaceCommandHost(whistle = createWhistle()) {
334
355
  return;
335
356
  checkBindings(replacement);
336
357
  underlying.update(replacement);
337
- ownedCommands.set(lease, new Set(replacement.map((command) => `${namespace}:${command.id}`)));
358
+ ownedCommands.set(lease, keys(replacement));
338
359
  },
339
360
  dispose: () => {
340
361
  if (!active)
@@ -346,10 +367,10 @@ export function createAppSurfaceCommandHost(whistle = createWhistle()) {
346
367
  },
347
368
  });
348
369
  state.leases.add(lease);
349
- ownedCommands.set(lease, new Set(commands.map((command) => `${namespace}:${command.id}`)));
370
+ ownedCommands.set(lease, keys(commands));
350
371
  return lease;
351
372
  },
352
- execute: (commandId, input) => {
373
+ execute: (command, input) => {
353
374
  const snapshot = state.snapshot;
354
375
  const rows = input.origin === "agent"
355
376
  ? [snapshot.agent]
@@ -364,16 +385,16 @@ export function createAppSurfaceCommandHost(whistle = createWhistle()) {
364
385
  // A catalog action can be callable from an admitted row without a menu
365
386
  // or palette projection. Its contribution still belongs to this pane.
366
387
  const owned = input.origin !== "agent" &&
367
- [...ownedCommands.values()].some((ids) => ids.has(commandId));
388
+ [...ownedCommands.values()].some((keys) => keys.some((key) => sameWhistleCommand(key, command)));
368
389
  const admitted = owned ||
369
- rows.some((projection) => projection.some((row) => row.commandId === commandId));
390
+ rows.some((projection) => projection.some((row) => sameWhistleCommand(row.command, command)));
370
391
  const human = input.origin !== "programmatic" && input.origin !== "agent";
371
392
  if (state.disposed ||
372
393
  disposed ||
373
394
  !admitted ||
374
395
  (human && focusedMount !== paneId))
375
396
  return Promise.resolve({ status: "missing" });
376
- return whistle.execute(commandId, input);
397
+ return whistle.execute(command, input);
377
398
  },
378
399
  snapshot: () => state.snapshot,
379
400
  subscribe: (listener) => {
@@ -432,18 +453,24 @@ export function createAppSurfaceCommandHost(whistle = createWhistle()) {
432
453
  shellContribution = undefined;
433
454
  };
434
455
  };
456
+ // Directory rows share one namespace; each row's local id is an opaque
457
+ // index whose App command target this map holds.
458
+ let directoryTargets = new Map();
435
459
  const registerDirectoryCommands = (rows, run) => {
436
460
  assertLive();
437
461
  directoryContribution?.dispose();
462
+ const targets = new Map();
438
463
  const contribution = whistle.contribute({
439
464
  sourceId: appDirectorySourceId,
440
- namespace: "app-directory",
441
- commands: rows.flatMap(({ surfaceId, appTitle, command }) => {
465
+ namespace: appDirectorySourceId,
466
+ commands: rows.flatMap(({ surfaceId, appTitle, command }, index) => {
442
467
  const palette = command.projections?.palette;
443
468
  if (palette === undefined)
444
469
  return [];
470
+ const id = `entry-${String(index)}`;
471
+ targets.set(id, Object.freeze({ surfaceId, commandId: command.id }));
445
472
  const projected = {
446
- id: `${encodeURIComponent(surfaceId)}-${encodeURIComponent(command.id)}`,
473
+ id,
447
474
  title: command.title,
448
475
  category: `${appTitle} · ${command.category}`,
449
476
  projections: {
@@ -457,32 +484,44 @@ export function createAppSurfaceCommandHost(whistle = createWhistle()) {
457
484
  }),
458
485
  });
459
486
  directoryContribution = contribution;
487
+ directoryTargets = targets;
460
488
  let active = true;
461
489
  return () => {
462
490
  if (!active)
463
491
  return;
464
492
  active = false;
465
493
  contribution.dispose();
466
- if (directoryContribution === contribution)
494
+ if (directoryContribution === contribution) {
467
495
  directoryContribution = undefined;
496
+ directoryTargets = new Map();
497
+ }
468
498
  };
469
499
  };
500
+ const appCommand = (row) => {
501
+ if (row.sourceId === appDirectorySourceId)
502
+ return row.command.namespace === appDirectorySourceId
503
+ ? directoryTargets.get(row.command.id)
504
+ : undefined;
505
+ return mounts.has(row.sourceId) && row.command.namespace === row.sourceId
506
+ ? Object.freeze({ surfaceId: row.sourceId, commandId: row.command.id })
507
+ : undefined;
508
+ };
470
509
  const menu = () => focusedMenu;
471
- const isActive = (commandId) => {
510
+ const isActive = (command) => {
472
511
  const { bindings, palette, slash, help, menu: rows } = activeSnapshot;
473
- return [bindings, palette, slash, help, rows].some((projection) => projection.some((row) => row.commandId === commandId));
512
+ return [bindings, palette, slash, help, rows].some((projection) => projection.some((row) => sameWhistleCommand(row.command, command)));
474
513
  };
475
- const execute = async (commandId, input) => {
514
+ const execute = async (command, input) => {
476
515
  if (input.origin === "agent" &&
477
516
  !whistle
478
517
  .snapshot()
479
- .agent.some((command) => command.commandId === commandId))
518
+ .agent.some((row) => sameWhistleCommand(row.command, command)))
480
519
  return { status: "missing" };
481
520
  if (input.origin !== "programmatic" &&
482
521
  input.origin !== "agent" &&
483
- !isActive(commandId))
522
+ !isActive(command))
484
523
  return { status: "missing" };
485
- return whistle.execute(commandId, input);
524
+ return whistle.execute(command, input);
486
525
  };
487
526
  const dispose = () => {
488
527
  if (disposed)
@@ -501,6 +540,7 @@ export function createAppSurfaceCommandHost(whistle = createWhistle()) {
501
540
  shellContribution = undefined;
502
541
  directoryContribution?.dispose();
503
542
  directoryContribution = undefined;
543
+ directoryTargets = new Map();
504
544
  mounts.clear();
505
545
  focusedMount = null;
506
546
  focusedMenu = emptyMenu;
@@ -515,6 +555,7 @@ export function createAppSurfaceCommandHost(whistle = createWhistle()) {
515
555
  focus,
516
556
  registerShellCommands,
517
557
  registerDirectoryCommands,
558
+ appCommand,
518
559
  menu,
519
560
  active: () => activeSnapshot,
520
561
  execute,
package/dist/keys.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { WhistleBindingProjection, WhistleExecutionInput, WhistleExecutionOutcome, WhistleSnapshot } from "@fungi.computer/whistle";
1
+ import type { WhistleBindingProjection, WhistleCommandKey, WhistleExecutionInput, WhistleExecutionOutcome, WhistleSnapshot } from "@fungi.computer/whistle";
2
2
  /** One physical chord parsed from a Whistle binding such as `alt+left`. */
3
3
  export type ShellChord = Readonly<{
4
4
  /** The key's normalized name, such as left or escape. */
@@ -37,7 +37,7 @@ export type ShellKeyHost = Readonly<{
37
37
  /** Observe command changes; call the returned disposer to stop listening. */
38
38
  subscribe: (listener: () => void) => () => void;
39
39
  /** Run a command through the host with the original input origin. */
40
- execute: (commandId: string, input: WhistleExecutionInput) => Promise<WhistleExecutionOutcome>;
40
+ execute: (command: WhistleCommandKey, input: WhistleExecutionInput) => Promise<WhistleExecutionOutcome>;
41
41
  }>;
42
42
  /**
43
43
  * Dispatch the active view's single-chord bindings from the shell document.
package/dist/keys.js CHANGED
@@ -103,9 +103,9 @@ function isEditing(event) {
103
103
  export function installShellKeys(root, host, mac) {
104
104
  let chords = [];
105
105
  const refresh = () => {
106
- chords = host.active().bindings.flatMap(({ binding, commandId }) => {
106
+ chords = host.active().bindings.flatMap(({ binding, command }) => {
107
107
  const chord = parseChord(binding, mac);
108
- return chord === null ? [] : [{ commandId, chord }];
108
+ return chord === null ? [] : [{ command, chord }];
109
109
  });
110
110
  };
111
111
  const onKeyDown = (event) => {
@@ -117,7 +117,7 @@ export function installShellKeys(root, host, mac) {
117
117
  return;
118
118
  event.preventDefault();
119
119
  void host
120
- .execute(match.commandId, { origin: "keyboard" })
120
+ .execute(match.command, { origin: "keyboard" })
121
121
  .catch((cause) => {
122
122
  console.warn("Shell key command failed", cause);
123
123
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fungi.computer/nacre",
3
- "version": "0.0.3",
3
+ "version": "0.1.1",
4
4
  "author": "Levi <levi@technomancy.dev>",
5
5
  "description": "The shell client for hosting Apps: workbench, command host, keys, commands, window manager and App directory rows",
6
6
  "type": "module",
@@ -52,11 +52,11 @@
52
52
  "LICENSE"
53
53
  ],
54
54
  "dependencies": {
55
- "@fungi.computer/agar": "0.4.0",
55
+ "@fungi.computer/agar": "0.6.0",
56
56
  "@fungi.computer/ear-tag": "0.1.0",
57
57
  "@fungi.computer/gills": "0.2.0",
58
- "@fungi.computer/puffball": "0.2.0",
59
- "@fungi.computer/whistle": "0.2.0",
58
+ "@fungi.computer/puffball": "0.3.0",
59
+ "@fungi.computer/whistle": "0.3.0",
60
60
  "effect": "4.0.2",
61
61
  "zod": "4.4.3"
62
62
  },