@dolusoft/command-console 1.0.1 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -53,15 +53,22 @@ const open = ref(true)
53
53
  - **Recall, completion and context** are always on. `entries`, `recall` and `context` are the console's own unless
54
54
  bound with `v-model:*`; `initialState` gives the first value of an unbound slice.
55
55
  - **Props:** `commands`, `prompt`, `initialState`, `createId`, `maxEntries`, `beforeRun`, `onError`, `formatError`,
56
- `isSensitive`, `recallLimit`, `shouldRecord`, `labels`, `autofocus`, and `entries` / `recall` / `context`.
56
+ `isSensitive`, `recallLimit`, `shouldRecord`, `labels`, `autofocus`, `entries` / `recall` / `context`, and
57
+ `console`.
58
+ - **A console that outlives the component:** `:console="pageConsole"` draws a console created elsewhere with every
59
+ optional feature, and never disposes it, so a panel mounted again shows the same entries and live input. Its options
60
+ are its creator's: with `console`, `commands` to `shouldRecord` and `entries` / `recall` / `context` throw a
61
+ `TypeError`; `labels` and `autofocus` apply.
57
62
  - **Events:** `update:entries`, `update:recall`, `update:context` (for `v-model`, each resolved against the value last
58
63
  emitted, so two changes in one tick are both kept), `close` (a command's `close` effect) and `submit`
59
64
  (`{ inputId, sensitive }`, never the line; for Enter and the exposed `run`).
60
65
  - **Slots:** `block-text`, `block-json`, `block-table`, `block-list`, `block-message`, `block-custom` (`{ block, entry }`,
61
66
  inside a `div.cc-block`), `prompt`, `busy`, `completion`. A slot replaces content, never the element that carries the
62
67
  hooks.
63
- - **Labels:** every text the component shows is in `labels`, over English defaults; `messages` merges one level deep
64
- and a message may be a function of its parameters.
68
+ - **Labels:** every text the component shows is in `labels`, over English defaults; `messages` and `actors` merge one
69
+ level deep and a message may be a function of its parameters. A line from the assistant or an external agent
70
+ (`InputEntry.actor`) shows `labels.actors[actor]` (`AI`, `Agent`) in a `span.cc-actor` before its prompt, and its
71
+ entry has `data-actor`.
65
72
  - **Expose:** `focus()`, `run(line)`, `cancel()`, `reset()`, `clear()`, `console` and `state` (unwrapped by the
66
73
  template ref: `consoleRef.value.state.running`).
67
74
 
@@ -97,7 +104,8 @@ there.
97
104
  `useCommandConsole(options)` gives the same console without markup: `{ console, state }`, where `state` is a read-only
98
105
  `ShallowRef` of the console's state. A value option (`commands`, `prompt`, `state`, `maxEntries`, `recallLimit`) takes
99
106
  a value, a ref or a getter; a function option takes the function or a ref to it. `features` and `initialState` are read
100
- once, and the console is disposed with the calling scope. The optional features come from
107
+ once, and the console is disposed with the calling scope; `useCommandConsole({ console })` mirrors a console created
108
+ elsewhere, takes no other option and never disposes it. The optional features come from
101
109
  `@dolusoft/command-console-core`:
102
110
 
103
111
  ```ts
@@ -75,6 +75,45 @@ var useStickToBottom = (log, entries) => {
75
75
  };
76
76
  };
77
77
  // src/labels.ts
78
+ /**
79
+ * The default text of each `operation-failed` code: one short sentence each,
80
+ * saying whether the command ran where the user cannot tell.
81
+ */
82
+ var operationTexts = {
83
+ "invalid-args": "Invalid options.",
84
+ "unknown-command": "Unknown command.",
85
+ "not-allowed": "This command is not allowed here.",
86
+ "target-not-found": "The target no longer exists.",
87
+ "id-conflict": "The command came back changed; it did not run.",
88
+ "unsupported-version": "Unsupported protocol version.",
89
+ "too-large": "The command is too large to send.",
90
+ busy: "Another command is running.",
91
+ timeout: "No answer in time; the command did not run.",
92
+ failed: "The command failed.",
93
+ "connection-lost": "No connection; the command did not run.",
94
+ cancelled: "The command was cancelled."
95
+ };
96
+ /** What is wrong with an option, after `Invalid option <path>: `. */
97
+ var reasonTexts = {
98
+ type: "wrong type",
99
+ required: "missing",
100
+ enum: "not one of the allowed values",
101
+ minimum: "too small",
102
+ maximum: "too large",
103
+ minLength: "too short",
104
+ maxLength: "too long",
105
+ pattern: "wrong format",
106
+ maxItems: "too many values",
107
+ additional: "unknown",
108
+ limit: "too long or too deeply nested"
109
+ };
110
+ /** The default `invalid-args` text; a sensitive line names no option. */
111
+ var invalidArgsText = ({ path, reason }) => {
112
+ if (path === null || path === "") return "Invalid options.";
113
+ if (reason === "required") return `Missing option: ${path}.`;
114
+ if (reason === "additional") return `Unknown option: ${path}.`;
115
+ return `Invalid option ${path}: ${reasonTexts[reason]}.`;
116
+ };
78
117
  /** The English defaults (README: Labels). */
79
118
  var defaultLabels = {
80
119
  input: "Command",
@@ -88,12 +127,18 @@ var defaultLabels = {
88
127
  "command-not-found": (params) => params.name === null ? "command not found" : `${params.name}: command not found`,
89
128
  "command-failed": "The command failed.",
90
129
  "unterminated-quote": "Unterminated quote.",
91
- interrupted: "Interrupted."
130
+ interrupted: "Interrupted.",
131
+ "invalid-args": invalidArgsText,
132
+ "operation-failed": (params) => operationTexts[params.code]
133
+ },
134
+ actors: {
135
+ assistant: "AI",
136
+ "external-agent": "Agent"
92
137
  }
93
138
  };
94
139
  /**
95
- * The defaults with `labels` over them; `messages` merges one level deep, so
96
- * overriding one message keeps the others.
140
+ * The defaults with `labels` over them; `messages` and `actors` merge one
141
+ * level deep, so overriding one message keeps the others.
97
142
  */
98
143
  var resolveLabels = (labels) => ({
99
144
  ...defaultLabels,
@@ -101,6 +146,10 @@ var resolveLabels = (labels) => ({
101
146
  messages: {
102
147
  ...defaultLabels.messages,
103
148
  ...labels?.messages
149
+ },
150
+ actors: {
151
+ ...defaultLabels.actors,
152
+ ...labels?.actors
104
153
  }
105
154
  });
106
155
  /** The text of a message block, from `labels.messages`. */
@@ -165,17 +214,22 @@ var renderBlock = (block, entry, labels, slots) => {
165
214
  /** `span.cc-prompt` with the `prompt` slot inside, or the prompt text. */
166
215
  var renderPrompt = (prompt, slots) => h("span", { class: "cc-prompt" }, slots.prompt ? slots.prompt({ prompt }) : prompt);
167
216
  var renderEntry = (entry, labels, slots) => {
168
- if (entry.kind === "input") return h("div", {
169
- key: entry.id,
170
- class: "cc-entry",
171
- "data-kind": "input",
172
- "data-status": entry.status,
173
- "data-redacted": entry.redacted ? "" : void 0
174
- }, [
175
- renderPrompt(entry.prompt, slots),
176
- h("span", { class: "cc-line" }, entry.redacted || entry.text === null ? labels.redacted : entry.text),
177
- entry.status === "cancelled" ? h("span", { class: "cc-cancelled" }, labels.cancelledMarker) : null
178
- ]);
217
+ if (entry.kind === "input") {
218
+ const actor = entry.actor === "assistant" || entry.actor === "external-agent" ? entry.actor : void 0;
219
+ return h("div", {
220
+ key: entry.id,
221
+ class: "cc-entry",
222
+ "data-kind": "input",
223
+ "data-status": entry.status,
224
+ "data-redacted": entry.redacted ? "" : void 0,
225
+ "data-actor": actor
226
+ }, [
227
+ actor ? h("span", { class: "cc-actor" }, labels.actors[actor]) : null,
228
+ renderPrompt(entry.prompt, slots),
229
+ h("span", { class: "cc-line" }, entry.redacted || entry.text === null ? labels.redacted : entry.text),
230
+ entry.status === "cancelled" ? h("span", { class: "cc-cancelled" }, labels.cancelledMarker) : null
231
+ ]);
232
+ }
179
233
  return h("div", {
180
234
  key: entry.id,
181
235
  class: "cc-entry",
@@ -207,23 +261,31 @@ var valueKeys = [
207
261
  "recallLimit"
208
262
  ];
209
263
  /**
210
- * Creates a console from `options` and keeps it in step with them.
211
- *
212
- * A value option (`commands`, `prompt`, `state`, `maxEntries`,
213
- * `recallLimit`) may be a plain value, a ref or a getter. A function option
214
- * (`beforeRun`, `onError`, …) may be the function or a ref that holds it: a
215
- * bare function is the option itself, never a getter. `features` and
216
- * `initialState` are read once.
217
- *
218
- * @example
219
- * ```ts
220
- * const { console, state } = useCommandConsole({
221
- * features: consoleFeatures({ recallFeature }),
222
- * commands: () => commands.value
223
- * })
224
- * ```
264
+ * Mirrors `console`'s state into a `shallowRef` until the effect scope ends.
265
+ * With `stopOptions` the composable created the console: it stops passing
266
+ * options on and disposes it. Without, it only unsubscribes: the console is
267
+ * its creator's (C-84).
225
268
  */
226
- function useCommandConsole(options) {
269
+ var mirror = (console, stopOptions) => {
270
+ const state = shallowRef(console.getState());
271
+ const unsubscribe = console.subscribe((next) => {
272
+ state.value = next;
273
+ });
274
+ onScopeDispose(() => {
275
+ unsubscribe();
276
+ if (stopOptions) {
277
+ stopOptions();
278
+ console.dispose();
279
+ state.value = console.getState();
280
+ }
281
+ });
282
+ return {
283
+ console,
284
+ state: shallowReadonly(state)
285
+ };
286
+ };
287
+ /** Creates a console from `options` and disposes it with the scope. */
288
+ var ownConsole = (options) => {
227
289
  const resolve = () => {
228
290
  const resolved = {};
229
291
  for (const key of valueKeys) resolved[key] = toValue(options[key]);
@@ -235,27 +297,41 @@ function useCommandConsole(options) {
235
297
  features: options.features,
236
298
  initialState: toValue(options.initialState)
237
299
  });
238
- const state = shallowRef(console.getState());
239
- const unsubscribe = console.subscribe((next) => {
240
- state.value = next;
241
- });
242
- const stop = watch(resolve, (next) => {
300
+ return mirror(console, watch(resolve, (next) => {
243
301
  console.setOptions((old) => ({
244
302
  ...next,
245
303
  features: old.features,
246
304
  initialState: old.initialState
247
305
  }));
248
- });
249
- onScopeDispose(() => {
250
- stop();
251
- unsubscribe();
252
- console.dispose();
253
- state.value = console.getState();
254
- });
255
- return {
256
- console,
257
- state: shallowReadonly(state)
258
- };
306
+ }));
307
+ };
308
+ /**
309
+ * Creates a console from `options` and keeps it in step with them; or, given
310
+ * `{ console }` alone, mirrors a console created elsewhere and leaves its
311
+ * options and its life to its creator (C-84).
312
+ *
313
+ * A value option (`commands`, `prompt`, `state`, `maxEntries`,
314
+ * `recallLimit`) may be a plain value, a ref or a getter. A function option
315
+ * (`beforeRun`, `onError`, …) may be the function or a ref that holds it: a
316
+ * bare function is the option itself, never a getter. `features` and
317
+ * `initialState` are read once. `console` with any other option throws a
318
+ * `TypeError`.
319
+ *
320
+ * @example
321
+ * ```ts
322
+ * const { console, state } = useCommandConsole({
323
+ * features: consoleFeatures({ recallFeature }),
324
+ * commands: () => commands.value
325
+ * })
326
+ * ```
327
+ */
328
+ function useCommandConsole(options) {
329
+ if (options.console !== void 0) {
330
+ const others = Object.keys(options).filter((key) => key !== "console" && options[key] !== void 0);
331
+ if (others.length > 0) throw new TypeError(`useCommandConsole: ${others.join(", ")} cannot be given with console; the options belong to whoever created it.`);
332
+ return mirror(options.console, null);
333
+ }
334
+ return ownConsole(options);
259
335
  }
260
336
  // src/command-console.vue?vue&type=script&setup=true&lang.ts
261
337
  var _hoisted_1 = ["data-busy", "data-context-depth"];
@@ -282,7 +358,8 @@ var command_console_default = /* @__PURE__ */ defineComponent({
282
358
  name: "CommandConsole",
283
359
  __name: "command-console",
284
360
  props: {
285
- commands: {},
361
+ console: { default: void 0 },
362
+ commands: { default: void 0 },
286
363
  prompt: { default: void 0 },
287
364
  initialState: { default: void 0 },
288
365
  createId: {
@@ -358,25 +435,50 @@ var command_console_default = /* @__PURE__ */ defineComponent({
358
435
  if (props.context !== void 0) state.context = props.context;
359
436
  return state;
360
437
  };
361
- const { console, state } = useCommandConsole({
362
- features,
363
- commands: () => props.commands,
364
- prompt: () => props.prompt,
365
- state: controlled,
366
- maxEntries: () => props.maxEntries,
367
- recallLimit: () => props.recallLimit,
368
- initialState: props.initialState,
369
- createId: toRef(props, "createId"),
370
- beforeRun: toRef(props, "beforeRun"),
371
- onError: toRef(props, "onError"),
372
- formatError: toRef(props, "formatError"),
373
- isSensitive: toRef(props, "isSensitive"),
374
- shouldRecord: toRef(props, "shouldRecord"),
375
- onEntriesChange: report("entries", (value) => emit("update:entries", value)),
376
- onRecallChange: report("recall", (value) => emit("update:recall", value)),
377
- onContextChange: report("context", (value) => emit("update:context", value)),
378
- onClose: () => emit("close")
379
- });
438
+ const consoleOptionProps = [
439
+ "commands",
440
+ "prompt",
441
+ "initialState",
442
+ "createId",
443
+ "maxEntries",
444
+ "beforeRun",
445
+ "onError",
446
+ "formatError",
447
+ "isSensitive",
448
+ "recallLimit",
449
+ "shouldRecord",
450
+ "entries",
451
+ "recall",
452
+ "context"
453
+ ];
454
+ const useConsole = () => {
455
+ if (props.console !== void 0) {
456
+ const given = consoleOptionProps.filter((name) => props[name] !== void 0);
457
+ if (given.length > 0) throw new TypeError(`CommandConsole: ${given.join(", ")} cannot be given with console; the options belong to whoever created it.`);
458
+ return useCommandConsole({ console: props.console });
459
+ }
460
+ if (props.commands === void 0) throw new TypeError("CommandConsole: commands or console is required.");
461
+ return useCommandConsole({
462
+ features,
463
+ commands: () => props.commands ?? [],
464
+ prompt: () => props.prompt,
465
+ state: controlled,
466
+ maxEntries: () => props.maxEntries,
467
+ recallLimit: () => props.recallLimit,
468
+ initialState: props.initialState,
469
+ createId: toRef(props, "createId"),
470
+ beforeRun: toRef(props, "beforeRun"),
471
+ onError: toRef(props, "onError"),
472
+ formatError: toRef(props, "formatError"),
473
+ isSensitive: toRef(props, "isSensitive"),
474
+ shouldRecord: toRef(props, "shouldRecord"),
475
+ onEntriesChange: report("entries", (value) => emit("update:entries", value)),
476
+ onRecallChange: report("recall", (value) => emit("update:recall", value)),
477
+ onContextChange: report("context", (value) => emit("update:context", value)),
478
+ onClose: () => emit("close")
479
+ });
480
+ };
481
+ const { console: instance, state } = useConsole();
380
482
  const labels = computed(() => resolveLabels(props.labels));
381
483
  const listId = useId();
382
484
  const running = computed(() => state.value.running);
@@ -389,7 +491,7 @@ var command_console_default = /* @__PURE__ */ defineComponent({
389
491
  const prompt = computed(() => {
390
492
  state.value.context;
391
493
  props.prompt;
392
- return console.getPrompt();
494
+ return instance.getPrompt();
393
495
  });
394
496
  /**
395
497
  * The recalled line, for the status element (README: Recall): a screen reader
@@ -457,7 +559,7 @@ var command_console_default = /* @__PURE__ */ defineComponent({
457
559
  value: element.value,
458
560
  caret
459
561
  };
460
- console.editInput(element.value, caret);
562
+ instance.editInput(element.value, caret);
461
563
  };
462
564
  const onInput = (event) => {
463
565
  reportEdit(event.currentTarget);
@@ -475,7 +577,7 @@ var command_console_default = /* @__PURE__ */ defineComponent({
475
577
  const submitting = (action) => {
476
578
  const before = new Set(state.value.entries.map((entry) => entry.id));
477
579
  const result = action();
478
- const added = console.getState().entries.find((entry) => entry.kind === "input" && entry.status === "submitted" && !before.has(entry.id));
580
+ const added = instance.getState().entries.find((entry) => entry.kind === "input" && entry.status === "submitted" && !before.has(entry.id));
479
581
  if (added) {
480
582
  scroll.stick();
481
583
  const submit = {
@@ -490,23 +592,23 @@ var command_console_default = /* @__PURE__ */ defineComponent({
490
592
  const element = event.currentTarget;
491
593
  const key = keyInputOf(event, element, rootElement.value);
492
594
  if (!key.isComposing) onCaret(event);
493
- if (submitting(() => console.handleKey(key))) event.preventDefault();
595
+ if (submitting(() => instance.handleKey(key))) event.preventDefault();
494
596
  };
495
597
  __expose({
496
598
  focus: () => {
497
599
  focusInput();
498
600
  },
499
- run: (line) => submitting(() => console.run(line)),
601
+ run: (line) => submitting(() => instance.run(line)),
500
602
  cancel: () => {
501
- console.cancel();
603
+ instance.cancel();
502
604
  },
503
605
  reset: () => {
504
- console.reset();
606
+ instance.reset();
505
607
  },
506
608
  clear: () => {
507
- console.clearEntries();
609
+ instance.clearEntries();
508
610
  },
509
- console,
611
+ console: instance,
510
612
  state
511
613
  });
512
614
  return (_ctx, _cache) => {
@@ -1 +1 @@
1
- {"version":3,"file":"command-console.js","names":["_useSlots","_openBlock","_createElementBlock","_unref","_createElementVNode","_createBlock","_resolveDynamicComponent","_renderSlot","_createTextVNode","_toDisplayString","_Fragment","_sfc_main","CommandConsole"],"sources":["../src/dom/focus.ts","../src/dom/selection.ts","../src/dom/keys.ts","../src/dom/scroll.ts","../src/labels.ts","../src/parts/completions-view.ts","../src/parts/block-view.ts","../src/parts/entry-view.ts","../src/use-command-console.ts","../src/command-console.vue","../src/command-console.vue","../src/index.ts"],"sourcesContent":["// A click anywhere in the console focuses the input (README: DOM and styling,\n// C-51), unless the user is selecting text or clicked something of its own.\n\nconst interactive =\n 'a, button, input, select, textarea, [tabindex], [contenteditable]'\n\n/**\n * Whether a click on `target` inside `root` should focus the input: not when\n * text is selected, not inside an interactive element (the input included:\n * a click on it is the browser's).\n */\nexport const clickFocusesInput = (\n target: EventTarget | null,\n root: Element\n): boolean => {\n const selection = root.ownerDocument.getSelection()\n if (selection && !selection.isCollapsed && selection.toString() !== '') {\n return false\n }\n const element =\n target instanceof Element\n ? target\n : target instanceof Node\n ? target.parentElement\n : null\n const found = element?.closest(interactive)\n return !(found && root.contains(found))\n}\n","// Copy is never taken over (README: Keyboard, D-07): with text selected in\n// the input, or in the console's part of the document, Ctrl+C stays the\n// browser's.\nimport type { KeyInput } from '@dolusoft/command-console-core'\n\n/** Whether the document has a non-empty selection inside `root`. */\nconst hasSelectionIn = (root: Element | null): boolean => {\n const selection = root?.ownerDocument.getSelection()\n if (!root || !selection || selection.isCollapsed) {\n return false\n }\n if (selection.toString() === '') {\n return false\n }\n const { anchorNode, focusNode } = selection\n return (\n (anchorNode !== null && root.contains(anchorNode)) ||\n (focusNode !== null && root.contains(focusNode))\n )\n}\n\n/** Where text is selected, as `KeyInput.selection` reports it. */\nexport const selectionOf = (\n input: HTMLInputElement,\n root: Element | null\n): KeyInput['selection'] => {\n if (\n input.selectionStart !== null &&\n input.selectionStart !== input.selectionEnd\n ) {\n return 'input'\n }\n return hasSelectionIn(root) ? 'document' : 'none'\n}\n","// KeyboardEvent → KeyInput (core README: State): the adapter's whole share of\n// the key map. Core never sees a DOM event.\nimport type { KeyInput } from '@dolusoft/command-console-core'\n\nimport { selectionOf } from './selection'\n\n/** The key press `event` on `input`, inside the console `root`. */\nexport const keyInputOf = (\n event: KeyboardEvent,\n input: HTMLInputElement,\n root: Element | null\n): KeyInput => ({\n key: event.key,\n ctrlKey: event.ctrlKey,\n shiftKey: event.shiftKey,\n altKey: event.altKey,\n metaKey: event.metaKey,\n // 229: the key an IME holds while it composes (older engines do not set\n // `isComposing` on the keydown that starts a composition).\n isComposing: event.isComposing || event.keyCode === 229,\n selection: selectionOf(input, root)\n})\n","// Scroll (README: DOM and styling, Q-08, C-58): the log sticks to the bottom\n// while the user is within 2 px of it, and after the user's own submit; new\n// output does not move a view the user scrolled up. `scrollTop` is the only\n// style-related write of the package (P5).\nimport { nextTick, onBeforeUnmount, onMounted, type ShallowRef } from 'vue'\n\n/** How close to the bottom still counts as at the bottom, in px. */\nconst slack = 2\n\nexport interface StickToBottom {\n /** The user's own submit: stick again and show the bottom now. */\n stick(): void\n /** The log's `scroll` event. */\n onScroll(): void\n}\n\nexport const useStickToBottom = (\n log: Readonly<ShallowRef<HTMLElement | null>>,\n entries: Readonly<ShallowRef<HTMLElement | null>>\n): StickToBottom => {\n let stuck = true\n let observer: ResizeObserver | null = null\n let mounted = false\n\n const toBottom = () => {\n const element = log.value\n if (mounted && element) {\n element.scrollTop = element.scrollHeight\n }\n }\n\n onMounted(() => {\n mounted = true\n // One observer on the entries; a late callback after unmount does\n // nothing (D-04).\n if (typeof ResizeObserver !== 'undefined' && entries.value) {\n observer = new ResizeObserver(() => {\n if (stuck) {\n toBottom()\n }\n })\n observer.observe(entries.value)\n }\n })\n\n onBeforeUnmount(() => {\n mounted = false\n observer?.disconnect()\n observer = null\n })\n\n return {\n stick() {\n stuck = true\n void nextTick(toBottom)\n },\n onScroll() {\n const element = log.value\n if (element) {\n stuck =\n element.scrollHeight - element.scrollTop - element.clientHeight <=\n slack\n }\n }\n }\n}\n","// Every text the component renders comes from here (P7, C-47): English\n// defaults, merged one level deep with the `labels` prop.\nimport type { MessageBlock, MessageParams } from '@dolusoft/command-protocol'\n\nimport type { ConsoleLabels, ConsoleLabelsInput } from './contract'\n\n/** The English defaults (README: Labels). */\nconst defaultLabels: ConsoleLabels = {\n input: 'Command',\n inputDescription:\n 'Enter runs the command. Tab completes; on an empty line it leaves the console.',\n log: 'Console output',\n redacted: '(input hidden)',\n cancelledMarker: '^C',\n completions: 'Suggestions',\n busy: 'Running…',\n messages: {\n 'command-not-found': params =>\n params.name === null\n ? 'command not found'\n : `${params.name}: command not found`,\n 'command-failed': 'The command failed.',\n 'unterminated-quote': 'Unterminated quote.',\n interrupted: 'Interrupted.'\n }\n}\n\n/**\n * The defaults with `labels` over them; `messages` merges one level deep, so\n * overriding one message keeps the others.\n */\nexport const resolveLabels = (labels?: ConsoleLabelsInput): ConsoleLabels => ({\n ...defaultLabels,\n ...labels,\n messages: { ...defaultLabels.messages, ...labels?.messages }\n})\n\n/** The text of a message block, from `labels.messages`. */\nexport const messageText = (\n labels: ConsoleLabels,\n block: MessageBlock\n): string => {\n const label = labels.messages[block.code] as\n string | ((params: MessageParams[typeof block.code]) => string)\n if (typeof label === 'string') {\n return label\n }\n // A library-made block of a code with parameters always carries them;\n // `{}` keeps a consumer-made block without them from throwing.\n return label((block.params ?? {}) as never)\n}\n","// The completion list (README: DOM and styling, Completion): a named listbox,\n// one option per candidate, rendered only while a session is open. The ids tie\n// it to the input's `aria-controls` and `aria-activedescendant` (C-48).\nimport { h, type VNode, type VNodeArrayChildren } from 'vue'\n\nimport type { CompletionSession } from '@dolusoft/command-console-core'\n\nimport type { ConsoleSlots } from '../contract'\n\n/** The id of candidate `index` of the list `listId`. */\nexport const optionId = (listId: string, index: number): string =>\n `${listId}-${index}`\n\nexport const renderCompletions = (\n session: CompletionSession,\n listId: string,\n label: string,\n slots: Pick<ConsoleSlots, 'completion'>\n): VNode =>\n h(\n 'ul',\n {\n class: 'cc-completions',\n role: 'listbox',\n id: listId,\n 'aria-label': label\n },\n session.candidates.map((candidate, index) => {\n const active = index === session.active\n return h(\n 'li',\n {\n key: `${index}:${candidate}`,\n class: 'cc-completion',\n role: 'option',\n id: optionId(listId, index),\n 'aria-selected': active ? 'true' : 'false'\n },\n slots.completion\n ? (slots.completion({\n candidate,\n index,\n active\n }) as VNodeArrayChildren)\n : candidate\n )\n })\n )\n","// One output block (README: DOM and styling): `.cc-block` with its `data-type`,\n// in the element of its type, or a `div` around the consumer's `block-<type>`\n// slot (C-67). All text goes through Vue's escaping; there is no HTML (S-08).\nimport { h, type VNode, type VNodeArrayChildren } from 'vue'\n\nimport type {\n JsonPrimitive,\n OutputBlock,\n OutputEntry\n} from '@dolusoft/command-protocol'\n\nimport type { BlockSlotProps, ConsoleLabels, ConsoleSlots } from '../contract'\nimport { messageText } from '../labels'\n\n/** The `block-<type>` slots the component was given. */\nexport type BlockSlots = Pick<\n ConsoleSlots,\n | 'block-text'\n | 'block-json'\n | 'block-table'\n | 'block-list'\n | 'block-message'\n | 'block-custom'\n>\n\nconst json = (value: unknown) => JSON.stringify(value, null, 2)\n\n/** A table cell: a missing key and `null` are empty. */\nconst cellText = (value: JsonPrimitive | undefined): string =>\n value === undefined || value === null ? '' : String(value)\n\nconst content = (block: OutputBlock, labels: ConsoleLabels): VNode => {\n const hooks = { class: 'cc-block', 'data-type': block.type }\n switch (block.type) {\n case 'text':\n return h('pre', hooks, block.text)\n case 'json':\n return h('pre', hooks, json(block.value))\n case 'table':\n return h('table', hooks, [\n h(\n 'thead',\n h(\n 'tr',\n block.columns.map(column => h('th', { scope: 'col' }, column.label))\n )\n ),\n h(\n 'tbody',\n block.rows.map(row =>\n h(\n 'tr',\n block.columns.map(column => h('td', cellText(row[column.key])))\n )\n )\n )\n ])\n case 'list':\n return h(\n 'ul',\n hooks,\n block.items.map(item => h('li', item))\n )\n case 'message':\n return h('div', hooks, messageText(labels, block))\n case 'custom':\n return h('pre', hooks, json(block))\n }\n}\n\n/** Renders `block` of `entry`, through its slot when one is given. */\nexport const renderBlock = (\n block: OutputBlock,\n entry: OutputEntry,\n labels: ConsoleLabels,\n slots: BlockSlots\n): VNode => {\n const slot = slots[`block-${block.type}`] as\n ((props: BlockSlotProps) => unknown) | undefined\n if (slot) {\n return h(\n 'div',\n { class: 'cc-block', 'data-type': block.type },\n slot({ block, entry }) as VNodeArrayChildren\n )\n }\n return content(block, labels)\n}\n","// One entry of the log (README: DOM and styling): an ended input line (its\n// prompt, its text or `labels.redacted`, the cancelled marker) or the blocks of\n// an output.\nimport { h, type VNode, type VNodeArrayChildren } from 'vue'\n\nimport type { ConsoleEntry } from '@dolusoft/command-protocol'\n\nimport { renderBlock, type BlockSlots } from './block-view'\nimport type { ConsoleLabels, ConsoleSlots } from '../contract'\n\n/** The slots an entry may use. */\nexport type EntrySlots = BlockSlots & Pick<ConsoleSlots, 'prompt'>\n\n/** `span.cc-prompt` with the `prompt` slot inside, or the prompt text. */\nexport const renderPrompt = (\n prompt: string,\n slots: Pick<ConsoleSlots, 'prompt'>\n): VNode =>\n h(\n 'span',\n { class: 'cc-prompt' },\n slots.prompt ? (slots.prompt({ prompt }) as VNodeArrayChildren) : prompt\n )\n\nexport const renderEntry = (\n entry: ConsoleEntry,\n labels: ConsoleLabels,\n slots: EntrySlots\n): VNode => {\n if (entry.kind === 'input') {\n return h(\n 'div',\n {\n key: entry.id,\n class: 'cc-entry',\n 'data-kind': 'input',\n 'data-status': entry.status,\n 'data-redacted': entry.redacted ? '' : undefined\n },\n [\n renderPrompt(entry.prompt, slots),\n h(\n 'span',\n { class: 'cc-line' },\n entry.redacted || entry.text === null ? labels.redacted : entry.text\n ),\n entry.status === 'cancelled'\n ? h('span', { class: 'cc-cancelled' }, labels.cancelledMarker)\n : null\n ]\n )\n }\n return h(\n 'div',\n {\n key: entry.id,\n class: 'cc-entry',\n 'data-kind': 'output',\n 'data-tone': entry.tone,\n 'aria-live': entry.quiet ? 'off' : undefined\n },\n entry.blocks.map(block => renderBlock(block, entry, labels, slots))\n )\n}\n","// useCommandConsole (README: The composable): a console without markup. It\n// creates the console once, passes reactive option changes to `setOptions`,\n// mirrors every notification into a `shallowRef` and disposes the console with\n// its effect scope. It does no DOM work, so a consumer can draw a template of\n// its own. The Vue package keeps no console state of its own (P15).\nimport {\n onScopeDispose,\n shallowReadonly,\n shallowRef,\n toValue,\n unref,\n watch\n} from 'vue'\n\nimport {\n createConsole,\n type ConsoleOptions,\n type ConsoleState,\n type OptionalFeatures\n} from '@dolusoft/command-console-core'\n\nimport type {\n UseCommandConsoleOptions,\n UseCommandConsoleReturn\n} from './contract'\n\n/** The options that are functions: a ref may hold one, a getter may not. */\nconst functionKeys = [\n 'onEntriesChange',\n 'onRecallChange',\n 'onContextChange',\n 'createId',\n 'beforeRun',\n 'onError',\n 'formatError',\n 'onClose',\n 'isSensitive',\n 'shouldRecord'\n] as const\n\n/** The options that are values: a ref or a getter may give them. */\nconst valueKeys = [\n 'commands',\n 'prompt',\n 'state',\n 'maxEntries',\n 'recallLimit'\n] as const\n\n/**\n * Creates a console from `options` and keeps it in step with them.\n *\n * A value option (`commands`, `prompt`, `state`, `maxEntries`,\n * `recallLimit`) may be a plain value, a ref or a getter. A function option\n * (`beforeRun`, `onError`, …) may be the function or a ref that holds it: a\n * bare function is the option itself, never a getter. `features` and\n * `initialState` are read once.\n *\n * @example\n * ```ts\n * const { console, state } = useCommandConsole({\n * features: consoleFeatures({ recallFeature }),\n * commands: () => commands.value\n * })\n * ```\n */\nexport function useCommandConsole<\n // `{}` and not its constraint, as `createConsole` (ADR 0002).\n // eslint-disable-next-line @typescript-eslint/no-empty-object-type\n T extends Partial<OptionalFeatures> = {}\n>(options: UseCommandConsoleOptions<T>): UseCommandConsoleReturn<T> {\n const resolve = (): ConsoleOptions<T> => {\n const resolved: Record<string, unknown> = {}\n for (const key of valueKeys) {\n resolved[key] = toValue(options[key])\n }\n for (const key of functionKeys) {\n resolved[key] = unref(options[key])\n }\n return resolved as unknown as ConsoleOptions<T>\n }\n\n const console = createConsole<T>({\n ...resolve(),\n features: options.features,\n initialState: toValue(options.initialState)\n })\n\n const state = shallowRef<ConsoleState>(console.getState())\n const unsubscribe = console.subscribe(next => {\n state.value = next\n })\n\n // Every reactive read of `resolve` is a dependency: a change of any option\n // hands the whole set to the console. The features stay those it was\n // created with.\n const stop = watch(resolve, next => {\n console.setOptions(old => ({\n ...next,\n features: old.features,\n initialState: old.initialState\n }))\n })\n\n onScopeDispose(() => {\n stop()\n unsubscribe()\n console.dispose()\n // The snapshot goes with the console: what it held (the live input\n // among it) is not kept.\n state.value = console.getState()\n })\n\n return {\n console: console,\n state: shallowReadonly(state)\n }\n}\n","<script setup lang=\"ts\">\n// <CommandConsole> (README: The component): the composable's console drawn as\n// the DOM of `contract/dom.ts` (README: DOM and styling). The component holds\n// no console state (P15): it renders the snapshot and forwards DOM events to\n// core actions. What it keeps is DOM-bound: element refs, the \"stuck to\n// bottom\" flag, the last value it emitted per controlled slice and the caret\n// it last reported.\nimport {\n computed,\n onMounted,\n toRef,\n useId,\n useTemplateRef,\n watch,\n type FunctionalComponent\n} from 'vue'\n\nimport {\n completionFeature,\n consoleFeatures,\n contextFeature,\n recallFeature,\n type OptionalFeatures,\n type Updater\n} from '@dolusoft/command-console-core'\nimport type { ConsoleEntry, ContextFrame } from '@dolusoft/command-protocol'\n\nimport type {\n ConsoleEmits,\n ConsoleProps,\n ConsoleSlots,\n ConsoleSubmit\n} from './contract'\nimport { clickFocusesInput } from './dom/focus'\nimport { keyInputOf } from './dom/keys'\nimport { useStickToBottom } from './dom/scroll'\nimport { resolveLabels } from './labels'\nimport { renderCompletions, optionId } from './parts/completions-view'\nimport { renderEntry, renderPrompt } from './parts/entry-view'\nimport { useCommandConsole } from './use-command-console'\n\ndefineOptions({ name: 'CommandConsole' })\n\nconst props = withDefaults(defineProps<ConsoleProps>(), {\n prompt: undefined,\n initialState: undefined,\n createId: undefined,\n maxEntries: undefined,\n beforeRun: undefined,\n onError: undefined,\n formatError: undefined,\n isSensitive: undefined,\n recallLimit: undefined,\n shouldRecord: undefined,\n labels: undefined,\n autofocus: false,\n entries: undefined,\n recall: undefined,\n context: undefined\n})\n\nconst emit = defineEmits<ConsoleEmits>()\n\nconst slots = defineSlots<ConsoleSlots>()\n\n// The component always uses every optional feature (README: The component).\nconst features: OptionalFeatures = consoleFeatures({\n recallFeature,\n completionFeature,\n contextFeature\n})\n\n// --- v-model (core README: Controlled slices, C-61) -------------------------\n// An updater is resolved against the value this component last emitted for\n// the slice, else the prop: a second action in the same tick builds on the\n// first one's value, not on a prop the parent has not re-rendered yet. A new\n// prop value clears it.\ntype Slice = 'entries' | 'recall' | 'context'\ninterface SliceValues {\n entries: ConsoleEntry[]\n recall: string[]\n context: ContextFrame[]\n}\nconst lastEmitted: Partial<SliceValues> = {}\nfor (const slice of ['entries', 'recall', 'context'] as const) {\n watch(\n () => props[slice],\n () => {\n delete lastEmitted[slice]\n },\n { flush: 'sync' }\n )\n}\nconst report =\n <K extends Slice>(slice: K, send: (value: SliceValues[K]) => void) =>\n (updater: Updater<SliceValues[K]>) => {\n const base = (lastEmitted[slice] ?? props[slice] ?? []) as SliceValues[K]\n const value = typeof updater === 'function' ? updater(base) : updater\n lastEmitted[slice] = value\n send(value)\n }\n\n/** The slices bound with `v-model`; the others are the console's. */\nconst controlled = () => {\n const state: Partial<SliceValues> = {}\n if (props.entries !== undefined) {\n state.entries = props.entries\n }\n if (props.recall !== undefined) {\n state.recall = props.recall\n }\n if (props.context !== undefined) {\n state.context = props.context\n }\n return state\n}\n\nconst { console, state } = useCommandConsole({\n features,\n commands: () => props.commands,\n prompt: () => props.prompt,\n state: controlled,\n maxEntries: () => props.maxEntries,\n recallLimit: () => props.recallLimit,\n initialState: props.initialState,\n createId: toRef(props, 'createId'),\n beforeRun: toRef(props, 'beforeRun'),\n onError: toRef(props, 'onError'),\n formatError: toRef(props, 'formatError'),\n isSensitive: toRef(props, 'isSensitive'),\n shouldRecord: toRef(props, 'shouldRecord'),\n onEntriesChange: report('entries', value => emit('update:entries', value)),\n onRecallChange: report('recall', value => emit('update:recall', value)),\n onContextChange: report('context', value => emit('update:context', value)),\n onClose: () => emit('close')\n})\n\n// --- What the template draws ------------------------------------------------\n\nconst labels = computed(() => resolveLabels(props.labels))\nconst listId = useId()\n\nconst running = computed(() => state.value.running)\nconst session = computed(() => {\n const completion = state.value.completion\n return completion?.status === 'open' ? completion : null\n})\nconst activeId = computed(() =>\n session.value && session.value.active >= 0\n ? optionId(listId, session.value.active)\n : undefined\n)\n/** The prompt of the live line: the top context frame's, or the root's. */\nconst prompt = computed(() => {\n void state.value.context\n void props.prompt\n return console.getPrompt()\n})\n/**\n * The recalled line, for the status element (README: Recall): a screen reader\n * does not announce a scripted value change in a focused field.\n */\nconst recalled = computed(() => {\n const { recall, recallCursor } = state.value\n return recallCursor !== null ? (recall[recallCursor] ?? '') : ''\n})\n\nconst entriesView: FunctionalComponent = () =>\n state.value.entries.map(entry => renderEntry(entry, labels.value, slots))\nconst completionsView: FunctionalComponent = () =>\n session.value\n ? renderCompletions(session.value, listId, labels.value.completions, slots)\n : null\nconst promptView: FunctionalComponent = () => renderPrompt(prompt.value, slots)\n\n// --- Elements ---------------------------------------------------------------\n\nconst rootElement = useTemplateRef<HTMLDivElement>('root')\nconst logElement = useTemplateRef<HTMLDivElement>('log')\nconst entriesElement = useTemplateRef<HTMLDivElement>('entries')\nconst inputElement = useTemplateRef<HTMLInputElement>('input')\n\nconst scroll = useStickToBottom(logElement, entriesElement)\n\nconst focusInput = () => inputElement.value?.focus()\n\nonMounted(() => {\n if (props.autofocus) {\n focusInput()\n }\n})\n\n// C-51: a click anywhere in the console focuses the input, unless text is\n// selected or the click was on something interactive.\nconst onRootClick = (event: MouseEvent) => {\n if (rootElement.value && clickFocusesInput(event.target, rootElement.value)) {\n focusInput()\n }\n}\n\n// --- The input element (README: DOM and styling, C-54) ----------------------\n// The element is keyed by `input.revision`. When it is replaced, the new\n// one gets focus only if the old one had it, and the caret goes to the end.\n// Its value is written as a DOM property here, never bound in the template:\n// Vue mirrors a bound `value` into the `value` attribute, which would put\n// the typed line into the DOM.\n\nlet hadFocus = false\nwatch(\n () => state.value.input.revision,\n () => {\n const element = inputElement.value\n hadFocus =\n element !== null && element.ownerDocument.activeElement === element\n // The element about to go may hold a sensitive line (C-36).\n if (element) {\n element.value = ''\n }\n },\n { flush: 'pre' }\n)\n\n/** The value and caret this component last reported (a user edit). */\nlet reported = { value: '', caret: 0 }\n\n/**\n * Writes the console's value into the element. A write by the console\n * (completion, recall) also sets the caret it chose; the user's own edit is\n * left alone, so a selection the user made stays.\n */\nconst syncElement = (replaced: boolean) => {\n const element = inputElement.value\n if (!element) {\n return\n }\n const { value, caret } = state.value.input\n if (element.value !== value) {\n element.value = value\n }\n if (replaced) {\n if (hadFocus) {\n element.focus()\n }\n hadFocus = false\n element.setSelectionRange(value.length, value.length)\n return\n }\n if (\n element.ownerDocument.activeElement !== element ||\n (value === reported.value && caret === reported.caret)\n ) {\n return\n }\n if (element.selectionStart !== caret || element.selectionEnd !== caret) {\n element.setSelectionRange(caret, caret)\n }\n}\nonMounted(() => {\n syncElement(false)\n})\nwatch(\n () => state.value.input,\n (next, previous) => {\n syncElement(next.revision !== previous.revision)\n },\n { flush: 'post' }\n)\n\n/** Reports the element's value and caret to the console (a user edit). */\nconst reportEdit = (element: HTMLInputElement) => {\n const caret = element.selectionStart ?? element.value.length\n reported = { value: element.value, caret }\n console.editInput(element.value, caret)\n}\n\nconst onInput = (event: Event) => {\n reportEdit(event.currentTarget as HTMLInputElement)\n}\n\n// A caret move the user made (click, arrow keys, selection).\nconst onCaret = (event: Event) => {\n const element = event.currentTarget as HTMLInputElement\n const caret = element.selectionStart ?? element.value.length\n if (element.value !== state.value.input.value) {\n return\n }\n if (caret !== state.value.input.caret) {\n reportEdit(element)\n }\n}\n\n// --- Submits (C-64) ---------------------------------------------------------\n\n/**\n * Runs `action` and emits `submit` for the input entry it added, if any;\n * keeps the log at the bottom after the user's own submit (C-58).\n */\nconst submitting = <T,>(action: () => T): T => {\n const before = new Set(state.value.entries.map(entry => entry.id))\n const result = action()\n const added = console\n .getState()\n .entries.find(\n entry =>\n entry.kind === 'input' &&\n entry.status === 'submitted' &&\n !before.has(entry.id)\n )\n if (added) {\n scroll.stick()\n const submit: ConsoleSubmit = {\n inputId: added.id,\n sensitive: added.kind === 'input' && added.redacted\n }\n emit('submit', submit)\n }\n return result\n}\n\nconst onKeydown = (event: KeyboardEvent) => {\n const element = event.currentTarget as HTMLInputElement\n const key = keyInputOf(event, element, rootElement.value)\n if (!key.isComposing) {\n // The caret may have moved without an event the console saw.\n onCaret(event)\n }\n const handled = submitting(() => console.handleKey(key))\n if (handled) {\n event.preventDefault()\n }\n}\n\n// --- Expose ------------------------------------------------------------------\n\nconst exposed = {\n focus: () => {\n focusInput()\n },\n run: (line: string) => submitting(() => console.run(line)),\n cancel: () => {\n console.cancel()\n },\n reset: () => {\n console.reset()\n },\n clear: () => {\n console.clearEntries()\n },\n console,\n state\n}\ndefineExpose(exposed)\n</script>\n\n<template>\n <div\n ref=\"root\"\n class=\"cc-console\"\n :data-busy=\"running ? '' : undefined\"\n :data-context-depth=\"state.context.length\"\n @click=\"onRootClick\"\n >\n <div\n ref=\"log\"\n class=\"cc-log\"\n role=\"log\"\n :aria-label=\"labels.log\"\n @scroll=\"scroll.onScroll\"\n >\n <div ref=\"entries\" class=\"cc-entries\">\n <component :is=\"entriesView\" />\n </div>\n </div>\n <div class=\"cc-status\" role=\"status\">\n <template v-if=\"running\"\n ><slot name=\"busy\" :running=\"running\">{{ labels.busy }}</slot></template\n ><template v-else>{{ recalled }}</template>\n </div>\n <component :is=\"completionsView\" />\n <div class=\"cc-input-line\">\n <component :is=\"promptView\" />\n <input\n :key=\"state.input.revision\"\n ref=\"input\"\n class=\"cc-input\"\n type=\"text\"\n role=\"combobox\"\n aria-autocomplete=\"list\"\n :aria-expanded=\"session ? 'true' : 'false'\"\n :aria-controls=\"session ? listId : undefined\"\n :aria-activedescendant=\"activeId\"\n :aria-label=\"labels.input\"\n :aria-description=\"labels.inputDescription\"\n autocomplete=\"off\"\n autocorrect=\"off\"\n autocapitalize=\"off\"\n spellcheck=\"false\"\n :readonly=\"running !== null\"\n @input=\"onInput\"\n @keydown=\"onKeydown\"\n @keyup=\"onCaret\"\n @click=\"onCaret\"\n @selectionchange=\"onCaret\"\n />\n </div>\n </div>\n</template>\n","<script setup lang=\"ts\">\n// <CommandConsole> (README: The component): the composable's console drawn as\n// the DOM of `contract/dom.ts` (README: DOM and styling). The component holds\n// no console state (P15): it renders the snapshot and forwards DOM events to\n// core actions. What it keeps is DOM-bound: element refs, the \"stuck to\n// bottom\" flag, the last value it emitted per controlled slice and the caret\n// it last reported.\nimport {\n computed,\n onMounted,\n toRef,\n useId,\n useTemplateRef,\n watch,\n type FunctionalComponent\n} from 'vue'\n\nimport {\n completionFeature,\n consoleFeatures,\n contextFeature,\n recallFeature,\n type OptionalFeatures,\n type Updater\n} from '@dolusoft/command-console-core'\nimport type { ConsoleEntry, ContextFrame } from '@dolusoft/command-protocol'\n\nimport type {\n ConsoleEmits,\n ConsoleProps,\n ConsoleSlots,\n ConsoleSubmit\n} from './contract'\nimport { clickFocusesInput } from './dom/focus'\nimport { keyInputOf } from './dom/keys'\nimport { useStickToBottom } from './dom/scroll'\nimport { resolveLabels } from './labels'\nimport { renderCompletions, optionId } from './parts/completions-view'\nimport { renderEntry, renderPrompt } from './parts/entry-view'\nimport { useCommandConsole } from './use-command-console'\n\ndefineOptions({ name: 'CommandConsole' })\n\nconst props = withDefaults(defineProps<ConsoleProps>(), {\n prompt: undefined,\n initialState: undefined,\n createId: undefined,\n maxEntries: undefined,\n beforeRun: undefined,\n onError: undefined,\n formatError: undefined,\n isSensitive: undefined,\n recallLimit: undefined,\n shouldRecord: undefined,\n labels: undefined,\n autofocus: false,\n entries: undefined,\n recall: undefined,\n context: undefined\n})\n\nconst emit = defineEmits<ConsoleEmits>()\n\nconst slots = defineSlots<ConsoleSlots>()\n\n// The component always uses every optional feature (README: The component).\nconst features: OptionalFeatures = consoleFeatures({\n recallFeature,\n completionFeature,\n contextFeature\n})\n\n// --- v-model (core README: Controlled slices, C-61) -------------------------\n// An updater is resolved against the value this component last emitted for\n// the slice, else the prop: a second action in the same tick builds on the\n// first one's value, not on a prop the parent has not re-rendered yet. A new\n// prop value clears it.\ntype Slice = 'entries' | 'recall' | 'context'\ninterface SliceValues {\n entries: ConsoleEntry[]\n recall: string[]\n context: ContextFrame[]\n}\nconst lastEmitted: Partial<SliceValues> = {}\nfor (const slice of ['entries', 'recall', 'context'] as const) {\n watch(\n () => props[slice],\n () => {\n delete lastEmitted[slice]\n },\n { flush: 'sync' }\n )\n}\nconst report =\n <K extends Slice>(slice: K, send: (value: SliceValues[K]) => void) =>\n (updater: Updater<SliceValues[K]>) => {\n const base = (lastEmitted[slice] ?? props[slice] ?? []) as SliceValues[K]\n const value = typeof updater === 'function' ? updater(base) : updater\n lastEmitted[slice] = value\n send(value)\n }\n\n/** The slices bound with `v-model`; the others are the console's. */\nconst controlled = () => {\n const state: Partial<SliceValues> = {}\n if (props.entries !== undefined) {\n state.entries = props.entries\n }\n if (props.recall !== undefined) {\n state.recall = props.recall\n }\n if (props.context !== undefined) {\n state.context = props.context\n }\n return state\n}\n\nconst { console, state } = useCommandConsole({\n features,\n commands: () => props.commands,\n prompt: () => props.prompt,\n state: controlled,\n maxEntries: () => props.maxEntries,\n recallLimit: () => props.recallLimit,\n initialState: props.initialState,\n createId: toRef(props, 'createId'),\n beforeRun: toRef(props, 'beforeRun'),\n onError: toRef(props, 'onError'),\n formatError: toRef(props, 'formatError'),\n isSensitive: toRef(props, 'isSensitive'),\n shouldRecord: toRef(props, 'shouldRecord'),\n onEntriesChange: report('entries', value => emit('update:entries', value)),\n onRecallChange: report('recall', value => emit('update:recall', value)),\n onContextChange: report('context', value => emit('update:context', value)),\n onClose: () => emit('close')\n})\n\n// --- What the template draws ------------------------------------------------\n\nconst labels = computed(() => resolveLabels(props.labels))\nconst listId = useId()\n\nconst running = computed(() => state.value.running)\nconst session = computed(() => {\n const completion = state.value.completion\n return completion?.status === 'open' ? completion : null\n})\nconst activeId = computed(() =>\n session.value && session.value.active >= 0\n ? optionId(listId, session.value.active)\n : undefined\n)\n/** The prompt of the live line: the top context frame's, or the root's. */\nconst prompt = computed(() => {\n void state.value.context\n void props.prompt\n return console.getPrompt()\n})\n/**\n * The recalled line, for the status element (README: Recall): a screen reader\n * does not announce a scripted value change in a focused field.\n */\nconst recalled = computed(() => {\n const { recall, recallCursor } = state.value\n return recallCursor !== null ? (recall[recallCursor] ?? '') : ''\n})\n\nconst entriesView: FunctionalComponent = () =>\n state.value.entries.map(entry => renderEntry(entry, labels.value, slots))\nconst completionsView: FunctionalComponent = () =>\n session.value\n ? renderCompletions(session.value, listId, labels.value.completions, slots)\n : null\nconst promptView: FunctionalComponent = () => renderPrompt(prompt.value, slots)\n\n// --- Elements ---------------------------------------------------------------\n\nconst rootElement = useTemplateRef<HTMLDivElement>('root')\nconst logElement = useTemplateRef<HTMLDivElement>('log')\nconst entriesElement = useTemplateRef<HTMLDivElement>('entries')\nconst inputElement = useTemplateRef<HTMLInputElement>('input')\n\nconst scroll = useStickToBottom(logElement, entriesElement)\n\nconst focusInput = () => inputElement.value?.focus()\n\nonMounted(() => {\n if (props.autofocus) {\n focusInput()\n }\n})\n\n// C-51: a click anywhere in the console focuses the input, unless text is\n// selected or the click was on something interactive.\nconst onRootClick = (event: MouseEvent) => {\n if (rootElement.value && clickFocusesInput(event.target, rootElement.value)) {\n focusInput()\n }\n}\n\n// --- The input element (README: DOM and styling, C-54) ----------------------\n// The element is keyed by `input.revision`. When it is replaced, the new\n// one gets focus only if the old one had it, and the caret goes to the end.\n// Its value is written as a DOM property here, never bound in the template:\n// Vue mirrors a bound `value` into the `value` attribute, which would put\n// the typed line into the DOM.\n\nlet hadFocus = false\nwatch(\n () => state.value.input.revision,\n () => {\n const element = inputElement.value\n hadFocus =\n element !== null && element.ownerDocument.activeElement === element\n // The element about to go may hold a sensitive line (C-36).\n if (element) {\n element.value = ''\n }\n },\n { flush: 'pre' }\n)\n\n/** The value and caret this component last reported (a user edit). */\nlet reported = { value: '', caret: 0 }\n\n/**\n * Writes the console's value into the element. A write by the console\n * (completion, recall) also sets the caret it chose; the user's own edit is\n * left alone, so a selection the user made stays.\n */\nconst syncElement = (replaced: boolean) => {\n const element = inputElement.value\n if (!element) {\n return\n }\n const { value, caret } = state.value.input\n if (element.value !== value) {\n element.value = value\n }\n if (replaced) {\n if (hadFocus) {\n element.focus()\n }\n hadFocus = false\n element.setSelectionRange(value.length, value.length)\n return\n }\n if (\n element.ownerDocument.activeElement !== element ||\n (value === reported.value && caret === reported.caret)\n ) {\n return\n }\n if (element.selectionStart !== caret || element.selectionEnd !== caret) {\n element.setSelectionRange(caret, caret)\n }\n}\nonMounted(() => {\n syncElement(false)\n})\nwatch(\n () => state.value.input,\n (next, previous) => {\n syncElement(next.revision !== previous.revision)\n },\n { flush: 'post' }\n)\n\n/** Reports the element's value and caret to the console (a user edit). */\nconst reportEdit = (element: HTMLInputElement) => {\n const caret = element.selectionStart ?? element.value.length\n reported = { value: element.value, caret }\n console.editInput(element.value, caret)\n}\n\nconst onInput = (event: Event) => {\n reportEdit(event.currentTarget as HTMLInputElement)\n}\n\n// A caret move the user made (click, arrow keys, selection).\nconst onCaret = (event: Event) => {\n const element = event.currentTarget as HTMLInputElement\n const caret = element.selectionStart ?? element.value.length\n if (element.value !== state.value.input.value) {\n return\n }\n if (caret !== state.value.input.caret) {\n reportEdit(element)\n }\n}\n\n// --- Submits (C-64) ---------------------------------------------------------\n\n/**\n * Runs `action` and emits `submit` for the input entry it added, if any;\n * keeps the log at the bottom after the user's own submit (C-58).\n */\nconst submitting = <T,>(action: () => T): T => {\n const before = new Set(state.value.entries.map(entry => entry.id))\n const result = action()\n const added = console\n .getState()\n .entries.find(\n entry =>\n entry.kind === 'input' &&\n entry.status === 'submitted' &&\n !before.has(entry.id)\n )\n if (added) {\n scroll.stick()\n const submit: ConsoleSubmit = {\n inputId: added.id,\n sensitive: added.kind === 'input' && added.redacted\n }\n emit('submit', submit)\n }\n return result\n}\n\nconst onKeydown = (event: KeyboardEvent) => {\n const element = event.currentTarget as HTMLInputElement\n const key = keyInputOf(event, element, rootElement.value)\n if (!key.isComposing) {\n // The caret may have moved without an event the console saw.\n onCaret(event)\n }\n const handled = submitting(() => console.handleKey(key))\n if (handled) {\n event.preventDefault()\n }\n}\n\n// --- Expose ------------------------------------------------------------------\n\nconst exposed = {\n focus: () => {\n focusInput()\n },\n run: (line: string) => submitting(() => console.run(line)),\n cancel: () => {\n console.cancel()\n },\n reset: () => {\n console.reset()\n },\n clear: () => {\n console.clearEntries()\n },\n console,\n state\n}\ndefineExpose(exposed)\n</script>\n\n<template>\n <div\n ref=\"root\"\n class=\"cc-console\"\n :data-busy=\"running ? '' : undefined\"\n :data-context-depth=\"state.context.length\"\n @click=\"onRootClick\"\n >\n <div\n ref=\"log\"\n class=\"cc-log\"\n role=\"log\"\n :aria-label=\"labels.log\"\n @scroll=\"scroll.onScroll\"\n >\n <div ref=\"entries\" class=\"cc-entries\">\n <component :is=\"entriesView\" />\n </div>\n </div>\n <div class=\"cc-status\" role=\"status\">\n <template v-if=\"running\"\n ><slot name=\"busy\" :running=\"running\">{{ labels.busy }}</slot></template\n ><template v-else>{{ recalled }}</template>\n </div>\n <component :is=\"completionsView\" />\n <div class=\"cc-input-line\">\n <component :is=\"promptView\" />\n <input\n :key=\"state.input.revision\"\n ref=\"input\"\n class=\"cc-input\"\n type=\"text\"\n role=\"combobox\"\n aria-autocomplete=\"list\"\n :aria-expanded=\"session ? 'true' : 'false'\"\n :aria-controls=\"session ? listId : undefined\"\n :aria-activedescendant=\"activeId\"\n :aria-label=\"labels.input\"\n :aria-description=\"labels.inputDescription\"\n autocomplete=\"off\"\n autocorrect=\"off\"\n autocapitalize=\"off\"\n spellcheck=\"false\"\n :readonly=\"running !== null\"\n @input=\"onInput\"\n @keydown=\"onKeydown\"\n @keyup=\"onCaret\"\n @click=\"onCaret\"\n @selectionchange=\"onCaret\"\n />\n </div>\n </div>\n</template>\n","// @dolusoft/command-console: a headless command console for Vue 3.\n// `useCommandConsole` is the console without markup; `CommandConsole` draws\n// it as the DOM of `contract/dom.ts`. No CSS (P5).\nimport CommandConsole from './command-console.vue'\n\nexport { CommandConsole }\nexport default CommandConsole\n\nexport { useCommandConsole } from './use-command-console'\n\nexport type * from './contract'\n"],"mappings":"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"}
1
+ {"version":3,"file":"command-console.js","names":["_useSlots","_openBlock","_createElementBlock","_unref","_createElementVNode","_createBlock","_resolveDynamicComponent","_renderSlot","_createTextVNode","_toDisplayString","_Fragment","_sfc_main","CommandConsole"],"sources":["../src/dom/focus.ts","../src/dom/selection.ts","../src/dom/keys.ts","../src/dom/scroll.ts","../src/labels.ts","../src/parts/completions-view.ts","../src/parts/block-view.ts","../src/parts/entry-view.ts","../src/use-command-console.ts","../src/command-console.vue","../src/command-console.vue","../src/index.ts"],"sourcesContent":["// A click anywhere in the console focuses the input (README: DOM and styling,\n// C-51), unless the user is selecting text or clicked something of its own.\n\nconst interactive =\n 'a, button, input, select, textarea, [tabindex], [contenteditable]'\n\n/**\n * Whether a click on `target` inside `root` should focus the input: not when\n * text is selected, not inside an interactive element (the input included:\n * a click on it is the browser's).\n */\nexport const clickFocusesInput = (\n target: EventTarget | null,\n root: Element\n): boolean => {\n const selection = root.ownerDocument.getSelection()\n if (selection && !selection.isCollapsed && selection.toString() !== '') {\n return false\n }\n const element =\n target instanceof Element\n ? target\n : target instanceof Node\n ? target.parentElement\n : null\n const found = element?.closest(interactive)\n return !(found && root.contains(found))\n}\n","// Copy is never taken over (README: Keyboard, D-07): with text selected in\n// the input, or in the console's part of the document, Ctrl+C stays the\n// browser's.\nimport type { KeyInput } from '@dolusoft/command-console-core'\n\n/** Whether the document has a non-empty selection inside `root`. */\nconst hasSelectionIn = (root: Element | null): boolean => {\n const selection = root?.ownerDocument.getSelection()\n if (!root || !selection || selection.isCollapsed) {\n return false\n }\n if (selection.toString() === '') {\n return false\n }\n const { anchorNode, focusNode } = selection\n return (\n (anchorNode !== null && root.contains(anchorNode)) ||\n (focusNode !== null && root.contains(focusNode))\n )\n}\n\n/** Where text is selected, as `KeyInput.selection` reports it. */\nexport const selectionOf = (\n input: HTMLInputElement,\n root: Element | null\n): KeyInput['selection'] => {\n if (\n input.selectionStart !== null &&\n input.selectionStart !== input.selectionEnd\n ) {\n return 'input'\n }\n return hasSelectionIn(root) ? 'document' : 'none'\n}\n","// KeyboardEvent → KeyInput (core README: State): the adapter's whole share of\n// the key map. Core never sees a DOM event.\nimport type { KeyInput } from '@dolusoft/command-console-core'\n\nimport { selectionOf } from './selection'\n\n/** The key press `event` on `input`, inside the console `root`. */\nexport const keyInputOf = (\n event: KeyboardEvent,\n input: HTMLInputElement,\n root: Element | null\n): KeyInput => ({\n key: event.key,\n ctrlKey: event.ctrlKey,\n shiftKey: event.shiftKey,\n altKey: event.altKey,\n metaKey: event.metaKey,\n // 229: the key an IME holds while it composes (older engines do not set\n // `isComposing` on the keydown that starts a composition).\n isComposing: event.isComposing || event.keyCode === 229,\n selection: selectionOf(input, root)\n})\n","// Scroll (README: DOM and styling, Q-08, C-58): the log sticks to the bottom\n// while the user is within 2 px of it, and after the user's own submit; new\n// output does not move a view the user scrolled up. `scrollTop` is the only\n// style-related write of the package (P5).\nimport { nextTick, onBeforeUnmount, onMounted, type ShallowRef } from 'vue'\n\n/** How close to the bottom still counts as at the bottom, in px. */\nconst slack = 2\n\nexport interface StickToBottom {\n /** The user's own submit: stick again and show the bottom now. */\n stick(): void\n /** The log's `scroll` event. */\n onScroll(): void\n}\n\nexport const useStickToBottom = (\n log: Readonly<ShallowRef<HTMLElement | null>>,\n entries: Readonly<ShallowRef<HTMLElement | null>>\n): StickToBottom => {\n let stuck = true\n let observer: ResizeObserver | null = null\n let mounted = false\n\n const toBottom = () => {\n const element = log.value\n if (mounted && element) {\n element.scrollTop = element.scrollHeight\n }\n }\n\n onMounted(() => {\n mounted = true\n // One observer on the entries; a late callback after unmount does\n // nothing (D-04).\n if (typeof ResizeObserver !== 'undefined' && entries.value) {\n observer = new ResizeObserver(() => {\n if (stuck) {\n toBottom()\n }\n })\n observer.observe(entries.value)\n }\n })\n\n onBeforeUnmount(() => {\n mounted = false\n observer?.disconnect()\n observer = null\n })\n\n return {\n stick() {\n stuck = true\n void nextTick(toBottom)\n },\n onScroll() {\n const element = log.value\n if (element) {\n stuck =\n element.scrollHeight - element.scrollTop - element.clientHeight <=\n slack\n }\n }\n }\n}\n","// Every text the component renders comes from here (P7, C-47): English\n// defaults, merged one level deep with the `labels` prop.\nimport type {\n ErrorCode,\n InputErrorCode,\n MessageBlock,\n MessageParams\n} from '@dolusoft/command-protocol'\n\nimport type { ConsoleLabels, ConsoleLabelsInput } from './contract'\n\n/**\n * The default text of each `operation-failed` code: one short sentence each,\n * saying whether the command ran where the user cannot tell.\n */\nconst operationTexts: Record<ErrorCode, string> = {\n 'invalid-args': 'Invalid options.',\n 'unknown-command': 'Unknown command.',\n 'not-allowed': 'This command is not allowed here.',\n 'target-not-found': 'The target no longer exists.',\n 'id-conflict': 'The command came back changed; it did not run.',\n 'unsupported-version': 'Unsupported protocol version.',\n 'too-large': 'The command is too large to send.',\n busy: 'Another command is running.',\n timeout: 'No answer in time; the command did not run.',\n failed: 'The command failed.',\n 'connection-lost': 'No connection; the command did not run.',\n cancelled: 'The command was cancelled.'\n}\n\n/** What is wrong with an option, after `Invalid option <path>: `. */\nconst reasonTexts: Record<InputErrorCode, string> = {\n type: 'wrong type',\n required: 'missing',\n enum: 'not one of the allowed values',\n minimum: 'too small',\n maximum: 'too large',\n minLength: 'too short',\n maxLength: 'too long',\n pattern: 'wrong format',\n maxItems: 'too many values',\n additional: 'unknown',\n limit: 'too long or too deeply nested'\n}\n\n/** The default `invalid-args` text; a sensitive line names no option. */\nconst invalidArgsText = ({\n path,\n reason\n}: MessageParams['invalid-args']): string => {\n if (path === null || path === '') {\n return 'Invalid options.'\n }\n if (reason === 'required') {\n return `Missing option: ${path}.`\n }\n if (reason === 'additional') {\n return `Unknown option: ${path}.`\n }\n return `Invalid option ${path}: ${reasonTexts[reason]}.`\n}\n\n/** The English defaults (README: Labels). */\nconst defaultLabels: ConsoleLabels = {\n input: 'Command',\n inputDescription:\n 'Enter runs the command. Tab completes; on an empty line it leaves the console.',\n log: 'Console output',\n redacted: '(input hidden)',\n cancelledMarker: '^C',\n completions: 'Suggestions',\n busy: 'Running…',\n messages: {\n 'command-not-found': params =>\n params.name === null\n ? 'command not found'\n : `${params.name}: command not found`,\n 'command-failed': 'The command failed.',\n 'unterminated-quote': 'Unterminated quote.',\n interrupted: 'Interrupted.',\n 'invalid-args': invalidArgsText,\n 'operation-failed': params => operationTexts[params.code]\n },\n actors: {\n assistant: 'AI',\n 'external-agent': 'Agent'\n }\n}\n\n/**\n * The defaults with `labels` over them; `messages` and `actors` merge one\n * level deep, so overriding one message keeps the others.\n */\nexport const resolveLabels = (labels?: ConsoleLabelsInput): ConsoleLabels => ({\n ...defaultLabels,\n ...labels,\n messages: { ...defaultLabels.messages, ...labels?.messages },\n actors: { ...defaultLabels.actors, ...labels?.actors }\n})\n\n/** The text of a message block, from `labels.messages`. */\nexport const messageText = (\n labels: ConsoleLabels,\n block: MessageBlock\n): string => {\n const label = labels.messages[block.code] as\n string | ((params: MessageParams[typeof block.code]) => string)\n if (typeof label === 'string') {\n return label\n }\n // A library-made block of a code with parameters always carries them;\n // `{}` keeps a consumer-made block without them from throwing.\n return label((block.params ?? {}) as never)\n}\n","// The completion list (README: DOM and styling, Completion): a named listbox,\n// one option per candidate, rendered only while a session is open. The ids tie\n// it to the input's `aria-controls` and `aria-activedescendant` (C-48).\nimport { h, type VNode, type VNodeArrayChildren } from 'vue'\n\nimport type { CompletionSession } from '@dolusoft/command-console-core'\n\nimport type { ConsoleSlots } from '../contract'\n\n/** The id of candidate `index` of the list `listId`. */\nexport const optionId = (listId: string, index: number): string =>\n `${listId}-${index}`\n\nexport const renderCompletions = (\n session: CompletionSession,\n listId: string,\n label: string,\n slots: Pick<ConsoleSlots, 'completion'>\n): VNode =>\n h(\n 'ul',\n {\n class: 'cc-completions',\n role: 'listbox',\n id: listId,\n 'aria-label': label\n },\n session.candidates.map((candidate, index) => {\n const active = index === session.active\n return h(\n 'li',\n {\n key: `${index}:${candidate}`,\n class: 'cc-completion',\n role: 'option',\n id: optionId(listId, index),\n 'aria-selected': active ? 'true' : 'false'\n },\n slots.completion\n ? (slots.completion({\n candidate,\n index,\n active\n }) as VNodeArrayChildren)\n : candidate\n )\n })\n )\n","// One output block (README: DOM and styling): `.cc-block` with its `data-type`,\n// in the element of its type, or a `div` around the consumer's `block-<type>`\n// slot (C-67). All text goes through Vue's escaping; there is no HTML (S-08).\nimport { h, type VNode, type VNodeArrayChildren } from 'vue'\n\nimport type {\n JsonPrimitive,\n OutputBlock,\n OutputEntry\n} from '@dolusoft/command-protocol'\n\nimport type { BlockSlotProps, ConsoleLabels, ConsoleSlots } from '../contract'\nimport { messageText } from '../labels'\n\n/** The `block-<type>` slots the component was given. */\nexport type BlockSlots = Pick<\n ConsoleSlots,\n | 'block-text'\n | 'block-json'\n | 'block-table'\n | 'block-list'\n | 'block-message'\n | 'block-custom'\n>\n\nconst json = (value: unknown) => JSON.stringify(value, null, 2)\n\n/** A table cell: a missing key and `null` are empty. */\nconst cellText = (value: JsonPrimitive | undefined): string =>\n value === undefined || value === null ? '' : String(value)\n\nconst content = (block: OutputBlock, labels: ConsoleLabels): VNode => {\n const hooks = { class: 'cc-block', 'data-type': block.type }\n switch (block.type) {\n case 'text':\n return h('pre', hooks, block.text)\n case 'json':\n return h('pre', hooks, json(block.value))\n case 'table':\n return h('table', hooks, [\n h(\n 'thead',\n h(\n 'tr',\n block.columns.map(column => h('th', { scope: 'col' }, column.label))\n )\n ),\n h(\n 'tbody',\n block.rows.map(row =>\n h(\n 'tr',\n block.columns.map(column => h('td', cellText(row[column.key])))\n )\n )\n )\n ])\n case 'list':\n return h(\n 'ul',\n hooks,\n block.items.map(item => h('li', item))\n )\n case 'message':\n return h('div', hooks, messageText(labels, block))\n case 'custom':\n return h('pre', hooks, json(block))\n }\n}\n\n/** Renders `block` of `entry`, through its slot when one is given. */\nexport const renderBlock = (\n block: OutputBlock,\n entry: OutputEntry,\n labels: ConsoleLabels,\n slots: BlockSlots\n): VNode => {\n const slot = slots[`block-${block.type}`] as\n ((props: BlockSlotProps) => unknown) | undefined\n if (slot) {\n return h(\n 'div',\n { class: 'cc-block', 'data-type': block.type },\n slot({ block, entry }) as VNodeArrayChildren\n )\n }\n return content(block, labels)\n}\n","// One entry of the log (README: DOM and styling): an ended input line (its\n// prompt, its text or `labels.redacted`, the cancelled marker) or the blocks of\n// an output.\nimport { h, type VNode, type VNodeArrayChildren } from 'vue'\n\nimport type { ConsoleEntry } from '@dolusoft/command-protocol'\n\nimport { renderBlock, type BlockSlots } from './block-view'\nimport type { ConsoleLabels, ConsoleSlots } from '../contract'\n\n/** The slots an entry may use. */\nexport type EntrySlots = BlockSlots & Pick<ConsoleSlots, 'prompt'>\n\n/** `span.cc-prompt` with the `prompt` slot inside, or the prompt text. */\nexport const renderPrompt = (\n prompt: string,\n slots: Pick<ConsoleSlots, 'prompt'>\n): VNode =>\n h(\n 'span',\n { class: 'cc-prompt' },\n slots.prompt ? (slots.prompt({ prompt }) as VNodeArrayChildren) : prompt\n )\n\nexport const renderEntry = (\n entry: ConsoleEntry,\n labels: ConsoleLabels,\n slots: EntrySlots\n): VNode => {\n if (entry.kind === 'input') {\n // C-85: a line from the assistant or an agent says so, as text before\n // the prompt, so a screen reader reads it with the line.\n const actor =\n entry.actor === 'assistant' || entry.actor === 'external-agent'\n ? entry.actor\n : undefined\n return h(\n 'div',\n {\n key: entry.id,\n class: 'cc-entry',\n 'data-kind': 'input',\n 'data-status': entry.status,\n 'data-redacted': entry.redacted ? '' : undefined,\n 'data-actor': actor\n },\n [\n actor ? h('span', { class: 'cc-actor' }, labels.actors[actor]) : null,\n renderPrompt(entry.prompt, slots),\n h(\n 'span',\n { class: 'cc-line' },\n entry.redacted || entry.text === null ? labels.redacted : entry.text\n ),\n entry.status === 'cancelled'\n ? h('span', { class: 'cc-cancelled' }, labels.cancelledMarker)\n : null\n ]\n )\n }\n return h(\n 'div',\n {\n key: entry.id,\n class: 'cc-entry',\n 'data-kind': 'output',\n 'data-tone': entry.tone,\n 'aria-live': entry.quiet ? 'off' : undefined\n },\n entry.blocks.map(block => renderBlock(block, entry, labels, slots))\n )\n}\n","// useCommandConsole (README: The composable): a console without markup. It\n// creates the console once, passes reactive option changes to `setOptions`,\n// mirrors every notification into a `shallowRef` and disposes the console with\n// its effect scope. Given a console created elsewhere (C-84), it only mirrors\n// it: the options and the console's life are its creator's. It does no DOM\n// work, so a consumer can draw a template of its own. The Vue package keeps no\n// console state of its own (P15).\nimport {\n onScopeDispose,\n shallowReadonly,\n shallowRef,\n toValue,\n unref,\n watch\n} from 'vue'\n\nimport {\n createConsole,\n type Console,\n type ConsoleOptions,\n type ConsoleState,\n type OptionalFeatures\n} from '@dolusoft/command-console-core'\n\nimport type {\n UseCommandConsoleOptions,\n UseCommandConsoleReturn,\n UseExternalConsoleOptions\n} from './contract'\n\n/** The options that are functions: a ref may hold one, a getter may not. */\nconst functionKeys = [\n 'onEntriesChange',\n 'onRecallChange',\n 'onContextChange',\n 'createId',\n 'beforeRun',\n 'onError',\n 'formatError',\n 'onClose',\n 'isSensitive',\n 'shouldRecord'\n] as const\n\n/** The options that are values: a ref or a getter may give them. */\nconst valueKeys = [\n 'commands',\n 'prompt',\n 'state',\n 'maxEntries',\n 'recallLimit'\n] as const\n\n/**\n * Mirrors `console`'s state into a `shallowRef` until the effect scope ends.\n * With `stopOptions` the composable created the console: it stops passing\n * options on and disposes it. Without, it only unsubscribes: the console is\n * its creator's (C-84).\n */\nconst mirror = <T extends Partial<OptionalFeatures>>(\n console: Console<T>,\n stopOptions: (() => void) | null\n): UseCommandConsoleReturn<T> => {\n const state = shallowRef<ConsoleState>(console.getState())\n const unsubscribe = console.subscribe(next => {\n state.value = next\n })\n onScopeDispose(() => {\n unsubscribe()\n if (stopOptions) {\n stopOptions()\n console.dispose()\n // The snapshot goes with the console: what it held (the live input\n // among it) is not kept.\n state.value = console.getState()\n }\n })\n return {\n console: console,\n state: shallowReadonly(state)\n }\n}\n\n/** Creates a console from `options` and disposes it with the scope. */\nconst ownConsole = <T extends Partial<OptionalFeatures>>(\n options: UseCommandConsoleOptions<T>\n): UseCommandConsoleReturn<T> => {\n const resolve = (): ConsoleOptions<T> => {\n const resolved: Record<string, unknown> = {}\n for (const key of valueKeys) {\n resolved[key] = toValue(options[key])\n }\n for (const key of functionKeys) {\n resolved[key] = unref(options[key])\n }\n return resolved as unknown as ConsoleOptions<T>\n }\n\n const console = createConsole<T>({\n ...resolve(),\n features: options.features,\n initialState: toValue(options.initialState)\n })\n\n // Every reactive read of `resolve` is a dependency: a change of any option\n // hands the whole set to the console. The features stay those it was\n // created with.\n const stop = watch(resolve, next => {\n console.setOptions(old => ({\n ...next,\n features: old.features,\n initialState: old.initialState\n }))\n })\n\n return mirror(console, stop)\n}\n\n/**\n * Creates a console from `options` and keeps it in step with them; or, given\n * `{ console }` alone, mirrors a console created elsewhere and leaves its\n * options and its life to its creator (C-84).\n *\n * A value option (`commands`, `prompt`, `state`, `maxEntries`,\n * `recallLimit`) may be a plain value, a ref or a getter. A function option\n * (`beforeRun`, `onError`, …) may be the function or a ref that holds it: a\n * bare function is the option itself, never a getter. `features` and\n * `initialState` are read once. `console` with any other option throws a\n * `TypeError`.\n *\n * @example\n * ```ts\n * const { console, state } = useCommandConsole({\n * features: consoleFeatures({ recallFeature }),\n * commands: () => commands.value\n * })\n * ```\n */\nexport function useCommandConsole<\n // `{}` and not its constraint, as `createConsole` (ADR 0002).\n // eslint-disable-next-line @typescript-eslint/no-empty-object-type\n T extends Partial<OptionalFeatures> = {}\n>(\n options: UseCommandConsoleOptions<T> | UseExternalConsoleOptions<T>\n): UseCommandConsoleReturn<T> {\n if (options.console !== undefined) {\n const others = Object.keys(options).filter(\n key =>\n key !== 'console' &&\n (options as Record<string, unknown>)[key] !== undefined\n )\n if (others.length > 0) {\n throw new TypeError(\n `useCommandConsole: ${others.join(', ')} cannot be given with console; the options belong to whoever created it.`\n )\n }\n return mirror(options.console, null)\n }\n return ownConsole(options as UseCommandConsoleOptions<T>)\n}\n","<script setup lang=\"ts\">\n// <CommandConsole> (README: The component): the composable's console, or one\n// created elsewhere (C-84), drawn as the DOM of `contract/dom.ts` (README: DOM\n// and styling). The component holds\n// no console state (P15): it renders the snapshot and forwards DOM events to\n// core actions. What it keeps is DOM-bound: element refs, the \"stuck to\n// bottom\" flag, the last value it emitted per controlled slice and the caret\n// it last reported.\nimport {\n computed,\n onMounted,\n toRef,\n useId,\n useTemplateRef,\n watch,\n type FunctionalComponent\n} from 'vue'\n\nimport {\n completionFeature,\n consoleFeatures,\n contextFeature,\n recallFeature,\n type OptionalFeatures,\n type Updater\n} from '@dolusoft/command-console-core'\nimport type { ConsoleEntry, ContextFrame } from '@dolusoft/command-protocol'\n\nimport type {\n ConsoleEmits,\n ConsoleProps,\n ConsoleSlots,\n ConsoleSubmit\n} from './contract'\nimport { clickFocusesInput } from './dom/focus'\nimport { keyInputOf } from './dom/keys'\nimport { useStickToBottom } from './dom/scroll'\nimport { resolveLabels } from './labels'\nimport { renderCompletions, optionId } from './parts/completions-view'\nimport { renderEntry, renderPrompt } from './parts/entry-view'\nimport { useCommandConsole } from './use-command-console'\n\ndefineOptions({ name: 'CommandConsole' })\n\nconst props = withDefaults(defineProps<ConsoleProps>(), {\n console: undefined,\n commands: undefined,\n prompt: undefined,\n initialState: undefined,\n createId: undefined,\n maxEntries: undefined,\n beforeRun: undefined,\n onError: undefined,\n formatError: undefined,\n isSensitive: undefined,\n recallLimit: undefined,\n shouldRecord: undefined,\n labels: undefined,\n autofocus: false,\n entries: undefined,\n recall: undefined,\n context: undefined\n})\n\nconst emit = defineEmits<ConsoleEmits>()\n\nconst slots = defineSlots<ConsoleSlots>()\n\n// The component always uses every optional feature (README: The component).\nconst features: OptionalFeatures = consoleFeatures({\n recallFeature,\n completionFeature,\n contextFeature\n})\n\n// --- v-model (core README: Controlled slices, C-61) -------------------------\n// An updater is resolved against the value this component last emitted for\n// the slice, else the prop: a second action in the same tick builds on the\n// first one's value, not on a prop the parent has not re-rendered yet. A new\n// prop value clears it.\ntype Slice = 'entries' | 'recall' | 'context'\ninterface SliceValues {\n entries: ConsoleEntry[]\n recall: string[]\n context: ContextFrame[]\n}\nconst lastEmitted: Partial<SliceValues> = {}\nfor (const slice of ['entries', 'recall', 'context'] as const) {\n watch(\n () => props[slice],\n () => {\n delete lastEmitted[slice]\n },\n { flush: 'sync' }\n )\n}\nconst report =\n <K extends Slice>(slice: K, send: (value: SliceValues[K]) => void) =>\n (updater: Updater<SliceValues[K]>) => {\n const base = (lastEmitted[slice] ?? props[slice] ?? []) as SliceValues[K]\n const value = typeof updater === 'function' ? updater(base) : updater\n lastEmitted[slice] = value\n send(value)\n }\n\n/** The slices bound with `v-model`; the others are the console's. */\nconst controlled = () => {\n const state: Partial<SliceValues> = {}\n if (props.entries !== undefined) {\n state.entries = props.entries\n }\n if (props.recall !== undefined) {\n state.recall = props.recall\n }\n if (props.context !== undefined) {\n state.context = props.context\n }\n return state\n}\n\n// C-84: the props that are options of the console. With an external console\n// they are its creator's, so giving one is an error, not a silent no-op.\nconst consoleOptionProps = [\n 'commands',\n 'prompt',\n 'initialState',\n 'createId',\n 'maxEntries',\n 'beforeRun',\n 'onError',\n 'formatError',\n 'isSensitive',\n 'recallLimit',\n 'shouldRecord',\n 'entries',\n 'recall',\n 'context'\n] as const\n\nconst useConsole = () => {\n if (props.console !== undefined) {\n const given = consoleOptionProps.filter(name => props[name] !== undefined)\n if (given.length > 0) {\n throw new TypeError(\n `CommandConsole: ${given.join(', ')} cannot be given with console; the options belong to whoever created it.`\n )\n }\n // Read once: the component draws this console for its whole life.\n return useCommandConsole({ console: props.console })\n }\n if (props.commands === undefined) {\n throw new TypeError('CommandConsole: commands or console is required.')\n }\n return useCommandConsole({\n features,\n commands: () => props.commands ?? [],\n prompt: () => props.prompt,\n state: controlled,\n maxEntries: () => props.maxEntries,\n recallLimit: () => props.recallLimit,\n initialState: props.initialState,\n createId: toRef(props, 'createId'),\n beforeRun: toRef(props, 'beforeRun'),\n onError: toRef(props, 'onError'),\n formatError: toRef(props, 'formatError'),\n isSensitive: toRef(props, 'isSensitive'),\n shouldRecord: toRef(props, 'shouldRecord'),\n onEntriesChange: report('entries', value => emit('update:entries', value)),\n onRecallChange: report('recall', value => emit('update:recall', value)),\n onContextChange: report('context', value => emit('update:context', value)),\n onClose: () => emit('close')\n })\n}\n\nconst { console: instance, state } = useConsole()\n\n// --- What the template draws ------------------------------------------------\n\nconst labels = computed(() => resolveLabels(props.labels))\nconst listId = useId()\n\nconst running = computed(() => state.value.running)\nconst session = computed(() => {\n const completion = state.value.completion\n return completion?.status === 'open' ? completion : null\n})\nconst activeId = computed(() =>\n session.value && session.value.active >= 0\n ? optionId(listId, session.value.active)\n : undefined\n)\n/** The prompt of the live line: the top context frame's, or the root's. */\nconst prompt = computed(() => {\n void state.value.context\n void props.prompt\n return instance.getPrompt()\n})\n/**\n * The recalled line, for the status element (README: Recall): a screen reader\n * does not announce a scripted value change in a focused field.\n */\nconst recalled = computed(() => {\n const { recall, recallCursor } = state.value\n return recallCursor !== null ? (recall[recallCursor] ?? '') : ''\n})\n\nconst entriesView: FunctionalComponent = () =>\n state.value.entries.map(entry => renderEntry(entry, labels.value, slots))\nconst completionsView: FunctionalComponent = () =>\n session.value\n ? renderCompletions(session.value, listId, labels.value.completions, slots)\n : null\nconst promptView: FunctionalComponent = () => renderPrompt(prompt.value, slots)\n\n// --- Elements ---------------------------------------------------------------\n\nconst rootElement = useTemplateRef<HTMLDivElement>('root')\nconst logElement = useTemplateRef<HTMLDivElement>('log')\nconst entriesElement = useTemplateRef<HTMLDivElement>('entries')\nconst inputElement = useTemplateRef<HTMLInputElement>('input')\n\nconst scroll = useStickToBottom(logElement, entriesElement)\n\nconst focusInput = () => inputElement.value?.focus()\n\nonMounted(() => {\n if (props.autofocus) {\n focusInput()\n }\n})\n\n// C-51: a click anywhere in the console focuses the input, unless text is\n// selected or the click was on something interactive.\nconst onRootClick = (event: MouseEvent) => {\n if (rootElement.value && clickFocusesInput(event.target, rootElement.value)) {\n focusInput()\n }\n}\n\n// --- The input element (README: DOM and styling, C-54) ----------------------\n// The element is keyed by `input.revision`. When it is replaced, the new\n// one gets focus only if the old one had it, and the caret goes to the end.\n// Its value is written as a DOM property here, never bound in the template:\n// Vue mirrors a bound `value` into the `value` attribute, which would put\n// the typed line into the DOM.\n\nlet hadFocus = false\nwatch(\n () => state.value.input.revision,\n () => {\n const element = inputElement.value\n hadFocus =\n element !== null && element.ownerDocument.activeElement === element\n // The element about to go may hold a sensitive line (C-36).\n if (element) {\n element.value = ''\n }\n },\n { flush: 'pre' }\n)\n\n/** The value and caret this component last reported (a user edit). */\nlet reported = { value: '', caret: 0 }\n\n/**\n * Writes the console's value into the element. A write by the console\n * (completion, recall) also sets the caret it chose; the user's own edit is\n * left alone, so a selection the user made stays.\n */\nconst syncElement = (replaced: boolean) => {\n const element = inputElement.value\n if (!element) {\n return\n }\n const { value, caret } = state.value.input\n if (element.value !== value) {\n element.value = value\n }\n if (replaced) {\n if (hadFocus) {\n element.focus()\n }\n hadFocus = false\n element.setSelectionRange(value.length, value.length)\n return\n }\n if (\n element.ownerDocument.activeElement !== element ||\n (value === reported.value && caret === reported.caret)\n ) {\n return\n }\n if (element.selectionStart !== caret || element.selectionEnd !== caret) {\n element.setSelectionRange(caret, caret)\n }\n}\nonMounted(() => {\n syncElement(false)\n})\nwatch(\n () => state.value.input,\n (next, previous) => {\n syncElement(next.revision !== previous.revision)\n },\n { flush: 'post' }\n)\n\n/** Reports the element's value and caret to the console (a user edit). */\nconst reportEdit = (element: HTMLInputElement) => {\n const caret = element.selectionStart ?? element.value.length\n reported = { value: element.value, caret }\n instance.editInput(element.value, caret)\n}\n\nconst onInput = (event: Event) => {\n reportEdit(event.currentTarget as HTMLInputElement)\n}\n\n// A caret move the user made (click, arrow keys, selection).\nconst onCaret = (event: Event) => {\n const element = event.currentTarget as HTMLInputElement\n const caret = element.selectionStart ?? element.value.length\n if (element.value !== state.value.input.value) {\n return\n }\n if (caret !== state.value.input.caret) {\n reportEdit(element)\n }\n}\n\n// --- Submits (C-64) ---------------------------------------------------------\n\n/**\n * Runs `action` and emits `submit` for the input entry it added, if any;\n * keeps the log at the bottom after the user's own submit (C-58).\n */\nconst submitting = <T,>(action: () => T): T => {\n const before = new Set(state.value.entries.map(entry => entry.id))\n const result = action()\n const added = instance\n .getState()\n .entries.find(\n entry =>\n entry.kind === 'input' &&\n entry.status === 'submitted' &&\n !before.has(entry.id)\n )\n if (added) {\n scroll.stick()\n const submit: ConsoleSubmit = {\n inputId: added.id,\n sensitive: added.kind === 'input' && added.redacted\n }\n emit('submit', submit)\n }\n return result\n}\n\nconst onKeydown = (event: KeyboardEvent) => {\n const element = event.currentTarget as HTMLInputElement\n const key = keyInputOf(event, element, rootElement.value)\n if (!key.isComposing) {\n // The caret may have moved without an event the console saw.\n onCaret(event)\n }\n const handled = submitting(() => instance.handleKey(key))\n if (handled) {\n event.preventDefault()\n }\n}\n\n// --- Expose ------------------------------------------------------------------\n\nconst exposed = {\n focus: () => {\n focusInput()\n },\n run: (line: string) => submitting(() => instance.run(line)),\n cancel: () => {\n instance.cancel()\n },\n reset: () => {\n instance.reset()\n },\n clear: () => {\n instance.clearEntries()\n },\n console: instance,\n state\n}\ndefineExpose(exposed)\n</script>\n\n<template>\n <div\n ref=\"root\"\n class=\"cc-console\"\n :data-busy=\"running ? '' : undefined\"\n :data-context-depth=\"state.context.length\"\n @click=\"onRootClick\"\n >\n <div\n ref=\"log\"\n class=\"cc-log\"\n role=\"log\"\n :aria-label=\"labels.log\"\n @scroll=\"scroll.onScroll\"\n >\n <div ref=\"entries\" class=\"cc-entries\">\n <component :is=\"entriesView\" />\n </div>\n </div>\n <div class=\"cc-status\" role=\"status\">\n <template v-if=\"running\"\n ><slot name=\"busy\" :running=\"running\">{{ labels.busy }}</slot></template\n ><template v-else>{{ recalled }}</template>\n </div>\n <component :is=\"completionsView\" />\n <div class=\"cc-input-line\">\n <component :is=\"promptView\" />\n <input\n :key=\"state.input.revision\"\n ref=\"input\"\n class=\"cc-input\"\n type=\"text\"\n role=\"combobox\"\n aria-autocomplete=\"list\"\n :aria-expanded=\"session ? 'true' : 'false'\"\n :aria-controls=\"session ? listId : undefined\"\n :aria-activedescendant=\"activeId\"\n :aria-label=\"labels.input\"\n :aria-description=\"labels.inputDescription\"\n autocomplete=\"off\"\n autocorrect=\"off\"\n autocapitalize=\"off\"\n spellcheck=\"false\"\n :readonly=\"running !== null\"\n @input=\"onInput\"\n @keydown=\"onKeydown\"\n @keyup=\"onCaret\"\n @click=\"onCaret\"\n @selectionchange=\"onCaret\"\n />\n </div>\n </div>\n</template>\n","<script setup lang=\"ts\">\n// <CommandConsole> (README: The component): the composable's console, or one\n// created elsewhere (C-84), drawn as the DOM of `contract/dom.ts` (README: DOM\n// and styling). The component holds\n// no console state (P15): it renders the snapshot and forwards DOM events to\n// core actions. What it keeps is DOM-bound: element refs, the \"stuck to\n// bottom\" flag, the last value it emitted per controlled slice and the caret\n// it last reported.\nimport {\n computed,\n onMounted,\n toRef,\n useId,\n useTemplateRef,\n watch,\n type FunctionalComponent\n} from 'vue'\n\nimport {\n completionFeature,\n consoleFeatures,\n contextFeature,\n recallFeature,\n type OptionalFeatures,\n type Updater\n} from '@dolusoft/command-console-core'\nimport type { ConsoleEntry, ContextFrame } from '@dolusoft/command-protocol'\n\nimport type {\n ConsoleEmits,\n ConsoleProps,\n ConsoleSlots,\n ConsoleSubmit\n} from './contract'\nimport { clickFocusesInput } from './dom/focus'\nimport { keyInputOf } from './dom/keys'\nimport { useStickToBottom } from './dom/scroll'\nimport { resolveLabels } from './labels'\nimport { renderCompletions, optionId } from './parts/completions-view'\nimport { renderEntry, renderPrompt } from './parts/entry-view'\nimport { useCommandConsole } from './use-command-console'\n\ndefineOptions({ name: 'CommandConsole' })\n\nconst props = withDefaults(defineProps<ConsoleProps>(), {\n console: undefined,\n commands: undefined,\n prompt: undefined,\n initialState: undefined,\n createId: undefined,\n maxEntries: undefined,\n beforeRun: undefined,\n onError: undefined,\n formatError: undefined,\n isSensitive: undefined,\n recallLimit: undefined,\n shouldRecord: undefined,\n labels: undefined,\n autofocus: false,\n entries: undefined,\n recall: undefined,\n context: undefined\n})\n\nconst emit = defineEmits<ConsoleEmits>()\n\nconst slots = defineSlots<ConsoleSlots>()\n\n// The component always uses every optional feature (README: The component).\nconst features: OptionalFeatures = consoleFeatures({\n recallFeature,\n completionFeature,\n contextFeature\n})\n\n// --- v-model (core README: Controlled slices, C-61) -------------------------\n// An updater is resolved against the value this component last emitted for\n// the slice, else the prop: a second action in the same tick builds on the\n// first one's value, not on a prop the parent has not re-rendered yet. A new\n// prop value clears it.\ntype Slice = 'entries' | 'recall' | 'context'\ninterface SliceValues {\n entries: ConsoleEntry[]\n recall: string[]\n context: ContextFrame[]\n}\nconst lastEmitted: Partial<SliceValues> = {}\nfor (const slice of ['entries', 'recall', 'context'] as const) {\n watch(\n () => props[slice],\n () => {\n delete lastEmitted[slice]\n },\n { flush: 'sync' }\n )\n}\nconst report =\n <K extends Slice>(slice: K, send: (value: SliceValues[K]) => void) =>\n (updater: Updater<SliceValues[K]>) => {\n const base = (lastEmitted[slice] ?? props[slice] ?? []) as SliceValues[K]\n const value = typeof updater === 'function' ? updater(base) : updater\n lastEmitted[slice] = value\n send(value)\n }\n\n/** The slices bound with `v-model`; the others are the console's. */\nconst controlled = () => {\n const state: Partial<SliceValues> = {}\n if (props.entries !== undefined) {\n state.entries = props.entries\n }\n if (props.recall !== undefined) {\n state.recall = props.recall\n }\n if (props.context !== undefined) {\n state.context = props.context\n }\n return state\n}\n\n// C-84: the props that are options of the console. With an external console\n// they are its creator's, so giving one is an error, not a silent no-op.\nconst consoleOptionProps = [\n 'commands',\n 'prompt',\n 'initialState',\n 'createId',\n 'maxEntries',\n 'beforeRun',\n 'onError',\n 'formatError',\n 'isSensitive',\n 'recallLimit',\n 'shouldRecord',\n 'entries',\n 'recall',\n 'context'\n] as const\n\nconst useConsole = () => {\n if (props.console !== undefined) {\n const given = consoleOptionProps.filter(name => props[name] !== undefined)\n if (given.length > 0) {\n throw new TypeError(\n `CommandConsole: ${given.join(', ')} cannot be given with console; the options belong to whoever created it.`\n )\n }\n // Read once: the component draws this console for its whole life.\n return useCommandConsole({ console: props.console })\n }\n if (props.commands === undefined) {\n throw new TypeError('CommandConsole: commands or console is required.')\n }\n return useCommandConsole({\n features,\n commands: () => props.commands ?? [],\n prompt: () => props.prompt,\n state: controlled,\n maxEntries: () => props.maxEntries,\n recallLimit: () => props.recallLimit,\n initialState: props.initialState,\n createId: toRef(props, 'createId'),\n beforeRun: toRef(props, 'beforeRun'),\n onError: toRef(props, 'onError'),\n formatError: toRef(props, 'formatError'),\n isSensitive: toRef(props, 'isSensitive'),\n shouldRecord: toRef(props, 'shouldRecord'),\n onEntriesChange: report('entries', value => emit('update:entries', value)),\n onRecallChange: report('recall', value => emit('update:recall', value)),\n onContextChange: report('context', value => emit('update:context', value)),\n onClose: () => emit('close')\n })\n}\n\nconst { console: instance, state } = useConsole()\n\n// --- What the template draws ------------------------------------------------\n\nconst labels = computed(() => resolveLabels(props.labels))\nconst listId = useId()\n\nconst running = computed(() => state.value.running)\nconst session = computed(() => {\n const completion = state.value.completion\n return completion?.status === 'open' ? completion : null\n})\nconst activeId = computed(() =>\n session.value && session.value.active >= 0\n ? optionId(listId, session.value.active)\n : undefined\n)\n/** The prompt of the live line: the top context frame's, or the root's. */\nconst prompt = computed(() => {\n void state.value.context\n void props.prompt\n return instance.getPrompt()\n})\n/**\n * The recalled line, for the status element (README: Recall): a screen reader\n * does not announce a scripted value change in a focused field.\n */\nconst recalled = computed(() => {\n const { recall, recallCursor } = state.value\n return recallCursor !== null ? (recall[recallCursor] ?? '') : ''\n})\n\nconst entriesView: FunctionalComponent = () =>\n state.value.entries.map(entry => renderEntry(entry, labels.value, slots))\nconst completionsView: FunctionalComponent = () =>\n session.value\n ? renderCompletions(session.value, listId, labels.value.completions, slots)\n : null\nconst promptView: FunctionalComponent = () => renderPrompt(prompt.value, slots)\n\n// --- Elements ---------------------------------------------------------------\n\nconst rootElement = useTemplateRef<HTMLDivElement>('root')\nconst logElement = useTemplateRef<HTMLDivElement>('log')\nconst entriesElement = useTemplateRef<HTMLDivElement>('entries')\nconst inputElement = useTemplateRef<HTMLInputElement>('input')\n\nconst scroll = useStickToBottom(logElement, entriesElement)\n\nconst focusInput = () => inputElement.value?.focus()\n\nonMounted(() => {\n if (props.autofocus) {\n focusInput()\n }\n})\n\n// C-51: a click anywhere in the console focuses the input, unless text is\n// selected or the click was on something interactive.\nconst onRootClick = (event: MouseEvent) => {\n if (rootElement.value && clickFocusesInput(event.target, rootElement.value)) {\n focusInput()\n }\n}\n\n// --- The input element (README: DOM and styling, C-54) ----------------------\n// The element is keyed by `input.revision`. When it is replaced, the new\n// one gets focus only if the old one had it, and the caret goes to the end.\n// Its value is written as a DOM property here, never bound in the template:\n// Vue mirrors a bound `value` into the `value` attribute, which would put\n// the typed line into the DOM.\n\nlet hadFocus = false\nwatch(\n () => state.value.input.revision,\n () => {\n const element = inputElement.value\n hadFocus =\n element !== null && element.ownerDocument.activeElement === element\n // The element about to go may hold a sensitive line (C-36).\n if (element) {\n element.value = ''\n }\n },\n { flush: 'pre' }\n)\n\n/** The value and caret this component last reported (a user edit). */\nlet reported = { value: '', caret: 0 }\n\n/**\n * Writes the console's value into the element. A write by the console\n * (completion, recall) also sets the caret it chose; the user's own edit is\n * left alone, so a selection the user made stays.\n */\nconst syncElement = (replaced: boolean) => {\n const element = inputElement.value\n if (!element) {\n return\n }\n const { value, caret } = state.value.input\n if (element.value !== value) {\n element.value = value\n }\n if (replaced) {\n if (hadFocus) {\n element.focus()\n }\n hadFocus = false\n element.setSelectionRange(value.length, value.length)\n return\n }\n if (\n element.ownerDocument.activeElement !== element ||\n (value === reported.value && caret === reported.caret)\n ) {\n return\n }\n if (element.selectionStart !== caret || element.selectionEnd !== caret) {\n element.setSelectionRange(caret, caret)\n }\n}\nonMounted(() => {\n syncElement(false)\n})\nwatch(\n () => state.value.input,\n (next, previous) => {\n syncElement(next.revision !== previous.revision)\n },\n { flush: 'post' }\n)\n\n/** Reports the element's value and caret to the console (a user edit). */\nconst reportEdit = (element: HTMLInputElement) => {\n const caret = element.selectionStart ?? element.value.length\n reported = { value: element.value, caret }\n instance.editInput(element.value, caret)\n}\n\nconst onInput = (event: Event) => {\n reportEdit(event.currentTarget as HTMLInputElement)\n}\n\n// A caret move the user made (click, arrow keys, selection).\nconst onCaret = (event: Event) => {\n const element = event.currentTarget as HTMLInputElement\n const caret = element.selectionStart ?? element.value.length\n if (element.value !== state.value.input.value) {\n return\n }\n if (caret !== state.value.input.caret) {\n reportEdit(element)\n }\n}\n\n// --- Submits (C-64) ---------------------------------------------------------\n\n/**\n * Runs `action` and emits `submit` for the input entry it added, if any;\n * keeps the log at the bottom after the user's own submit (C-58).\n */\nconst submitting = <T,>(action: () => T): T => {\n const before = new Set(state.value.entries.map(entry => entry.id))\n const result = action()\n const added = instance\n .getState()\n .entries.find(\n entry =>\n entry.kind === 'input' &&\n entry.status === 'submitted' &&\n !before.has(entry.id)\n )\n if (added) {\n scroll.stick()\n const submit: ConsoleSubmit = {\n inputId: added.id,\n sensitive: added.kind === 'input' && added.redacted\n }\n emit('submit', submit)\n }\n return result\n}\n\nconst onKeydown = (event: KeyboardEvent) => {\n const element = event.currentTarget as HTMLInputElement\n const key = keyInputOf(event, element, rootElement.value)\n if (!key.isComposing) {\n // The caret may have moved without an event the console saw.\n onCaret(event)\n }\n const handled = submitting(() => instance.handleKey(key))\n if (handled) {\n event.preventDefault()\n }\n}\n\n// --- Expose ------------------------------------------------------------------\n\nconst exposed = {\n focus: () => {\n focusInput()\n },\n run: (line: string) => submitting(() => instance.run(line)),\n cancel: () => {\n instance.cancel()\n },\n reset: () => {\n instance.reset()\n },\n clear: () => {\n instance.clearEntries()\n },\n console: instance,\n state\n}\ndefineExpose(exposed)\n</script>\n\n<template>\n <div\n ref=\"root\"\n class=\"cc-console\"\n :data-busy=\"running ? '' : undefined\"\n :data-context-depth=\"state.context.length\"\n @click=\"onRootClick\"\n >\n <div\n ref=\"log\"\n class=\"cc-log\"\n role=\"log\"\n :aria-label=\"labels.log\"\n @scroll=\"scroll.onScroll\"\n >\n <div ref=\"entries\" class=\"cc-entries\">\n <component :is=\"entriesView\" />\n </div>\n </div>\n <div class=\"cc-status\" role=\"status\">\n <template v-if=\"running\"\n ><slot name=\"busy\" :running=\"running\">{{ labels.busy }}</slot></template\n ><template v-else>{{ recalled }}</template>\n </div>\n <component :is=\"completionsView\" />\n <div class=\"cc-input-line\">\n <component :is=\"promptView\" />\n <input\n :key=\"state.input.revision\"\n ref=\"input\"\n class=\"cc-input\"\n type=\"text\"\n role=\"combobox\"\n aria-autocomplete=\"list\"\n :aria-expanded=\"session ? 'true' : 'false'\"\n :aria-controls=\"session ? listId : undefined\"\n :aria-activedescendant=\"activeId\"\n :aria-label=\"labels.input\"\n :aria-description=\"labels.inputDescription\"\n autocomplete=\"off\"\n autocorrect=\"off\"\n autocapitalize=\"off\"\n spellcheck=\"false\"\n :readonly=\"running !== null\"\n @input=\"onInput\"\n @keydown=\"onKeydown\"\n @keyup=\"onCaret\"\n @click=\"onCaret\"\n @selectionchange=\"onCaret\"\n />\n </div>\n </div>\n</template>\n","// @dolusoft/command-console: a headless command console for Vue 3.\n// `useCommandConsole` is the console without markup; `CommandConsole` draws\n// it as the DOM of `contract/dom.ts`. No CSS (P5).\nimport CommandConsole from './command-console.vue'\n\nexport { CommandConsole }\nexport default CommandConsole\n\nexport { useCommandConsole } from './use-command-console'\n\nexport type * from './contract'\n"],"mappings":";;;AAGA,IAAM,cACJ;;;;;;AAOF,IAAa,qBACX,QACA,SACY;CACZ,MAAM,YAAY,KAAK,cAAc,aAAa;CAClD,IAAI,aAAa,CAAC,UAAU,eAAe,UAAU,SAAS,MAAM,IAClE,OAAO;CAQT,MAAM,SALJ,kBAAkB,UACd,SACA,kBAAkB,OAChB,OAAO,gBACP,KAAA,EACe,QAAQ,WAAW;CAC1C,OAAO,EAAE,SAAS,KAAK,SAAS,KAAK;AACvC;;;ACrBA,IAAM,kBAAkB,SAAkC;CACxD,MAAM,YAAY,MAAM,cAAc,aAAa;CACnD,IAAI,CAAC,QAAQ,CAAC,aAAa,UAAU,aACnC,OAAO;CAET,IAAI,UAAU,SAAS,MAAM,IAC3B,OAAO;CAET,MAAM,EAAE,YAAY,cAAc;CAClC,OACG,eAAe,QAAQ,KAAK,SAAS,UAAU,KAC/C,cAAc,QAAQ,KAAK,SAAS,SAAS;AAElD;;AAGA,IAAa,eACX,OACA,SAC0B;CAC1B,IACE,MAAM,mBAAmB,QACzB,MAAM,mBAAmB,MAAM,cAE/B,OAAO;CAET,OAAO,eAAe,IAAI,IAAI,aAAa;AAC7C;;;AC1BA,IAAa,cACX,OACA,OACA,UACc;CACd,KAAK,MAAM;CACX,SAAS,MAAM;CACf,UAAU,MAAM;CAChB,QAAQ,MAAM;CACd,SAAS,MAAM;CAGf,aAAa,MAAM,eAAe,MAAM,YAAY;CACpD,WAAW,YAAY,OAAO,IAAI;AACpC;;;ACdA,IAAM,QAAQ;AASd,IAAa,oBACX,KACA,YACkB;CAClB,IAAI,QAAQ;CACZ,IAAI,WAAkC;CACtC,IAAI,UAAU;CAEd,MAAM,iBAAiB;EACrB,MAAM,UAAU,IAAI;EACpB,IAAI,WAAW,SACb,QAAQ,YAAY,QAAQ;CAEhC;CAEA,gBAAgB;EACd,UAAU;EAGV,IAAI,OAAO,mBAAmB,eAAe,QAAQ,OAAO;GAC1D,WAAW,IAAI,qBAAqB;IAClC,IAAI,OACF,SAAS;GAEb,CAAC;GACD,SAAS,QAAQ,QAAQ,KAAK;EAChC;CACF,CAAC;CAED,sBAAsB;EACpB,UAAU;EACV,UAAU,WAAW;EACrB,WAAW;CACb,CAAC;CAED,OAAO;EACL,QAAQ;GACN,QAAQ;GACR,SAAc,QAAQ;EACxB;EACA,WAAW;GACT,MAAM,UAAU,IAAI;GACpB,IAAI,SACF,QACE,QAAQ,eAAe,QAAQ,YAAY,QAAQ,gBACnD;EAEN;CACF;AACF;;;;;;AClDA,IAAM,iBAA4C;CAChD,gBAAgB;CAChB,mBAAmB;CACnB,eAAe;CACf,oBAAoB;CACpB,eAAe;CACf,uBAAuB;CACvB,aAAa;CACb,MAAM;CACN,SAAS;CACT,QAAQ;CACR,mBAAmB;CACnB,WAAW;AACb;;AAGA,IAAM,cAA8C;CAClD,MAAM;CACN,UAAU;CACV,MAAM;CACN,SAAS;CACT,SAAS;CACT,WAAW;CACX,WAAW;CACX,SAAS;CACT,UAAU;CACV,YAAY;CACZ,OAAO;AACT;;AAGA,IAAM,mBAAmB,EACvB,MACA,aAC2C;CAC3C,IAAI,SAAS,QAAQ,SAAS,IAC5B,OAAO;CAET,IAAI,WAAW,YACb,OAAO,mBAAmB,KAAK;CAEjC,IAAI,WAAW,cACb,OAAO,mBAAmB,KAAK;CAEjC,OAAO,kBAAkB,KAAK,IAAI,YAAY,QAAQ;AACxD;;AAGA,IAAM,gBAA+B;CACnC,OAAO;CACP,kBACE;CACF,KAAK;CACL,UAAU;CACV,iBAAiB;CACjB,aAAa;CACb,MAAM;CACN,UAAU;EACR,sBAAqB,WACnB,OAAO,SAAS,OACZ,sBACA,GAAG,OAAO,KAAK;EACrB,kBAAkB;EAClB,sBAAsB;EACtB,aAAa;EACb,gBAAgB;EAChB,qBAAoB,WAAU,eAAe,OAAO;CACtD;CACA,QAAQ;EACN,WAAW;EACX,kBAAkB;CACpB;AACF;;;;;AAMA,IAAa,iBAAiB,YAAgD;CAC5E,GAAG;CACH,GAAG;CACH,UAAU;EAAE,GAAG,cAAc;EAAU,GAAG,QAAQ;CAAS;CAC3D,QAAQ;EAAE,GAAG,cAAc;EAAQ,GAAG,QAAQ;CAAO;AACvD;;AAGA,IAAa,eACX,QACA,UACW;CACX,MAAM,QAAQ,OAAO,SAAS,MAAM;CAEpC,IAAI,OAAO,UAAU,UACnB,OAAO;CAIT,OAAO,MAAO,MAAM,UAAU,CAAC,CAAW;AAC5C;;;ACvGA,IAAa,YAAY,QAAgB,UACvC,GAAG,OAAO,GAAG;AAEf,IAAa,qBACX,SACA,QACA,OACA,UAEA,EACE,MACA;CACE,OAAO;CACP,MAAM;CACN,IAAI;CACJ,cAAc;AAChB,GACA,QAAQ,WAAW,KAAK,WAAW,UAAU;CAC3C,MAAM,SAAS,UAAU,QAAQ;CACjC,OAAO,EACL,MACA;EACE,KAAK,GAAG,MAAM,GAAG;EACjB,OAAO;EACP,MAAM;EACN,IAAI,SAAS,QAAQ,KAAK;EAC1B,iBAAiB,SAAS,SAAS;CACrC,GACA,MAAM,aACD,MAAM,WAAW;EAChB;EACA;EACA;CACF,CAAC,IACD,SACN;AACF,CAAC,CACH;;ACtBF,IAAM,QAAQ,UAAmB,KAAK,UAAU,OAAO,MAAM,CAAC;;AAG9D,IAAM,YAAY,UAChB,UAAU,KAAA,KAAa,UAAU,OAAO,KAAK,OAAO,KAAK;AAE3D,IAAM,WAAW,OAAoB,WAAiC;CACpE,MAAM,QAAQ;EAAE,OAAO;EAAY,aAAa,MAAM;CAAK;CAC3D,QAAQ,MAAM,MAAd;EACE,KAAK,QACH,OAAO,EAAE,OAAO,OAAO,MAAM,IAAI;EACnC,KAAK,QACH,OAAO,EAAE,OAAO,OAAO,KAAK,MAAM,KAAK,CAAC;EAC1C,KAAK,SACH,OAAO,EAAE,SAAS,OAAO,CACvB,EACE,SACA,EACE,MACA,MAAM,QAAQ,KAAI,WAAU,EAAE,MAAM,EAAE,OAAO,MAAM,GAAG,OAAO,KAAK,CAAC,CACrE,CACF,GACA,EACE,SACA,MAAM,KAAK,KAAI,QACb,EACE,MACA,MAAM,QAAQ,KAAI,WAAU,EAAE,MAAM,SAAS,IAAI,OAAO,IAAI,CAAC,CAAC,CAChE,CACF,CACF,CACF,CAAC;EACH,KAAK,QACH,OAAO,EACL,MACA,OACA,MAAM,MAAM,KAAI,SAAQ,EAAE,MAAM,IAAI,CAAC,CACvC;EACF,KAAK,WACH,OAAO,EAAE,OAAO,OAAO,YAAY,QAAQ,KAAK,CAAC;EACnD,KAAK,UACH,OAAO,EAAE,OAAO,OAAO,KAAK,KAAK,CAAC;CACtC;AACF;;AAGA,IAAa,eACX,OACA,OACA,QACA,UACU;CACV,MAAM,OAAO,MAAM,SAAS,MAAM;CAElC,IAAI,MACF,OAAO,EACL,OACA;EAAE,OAAO;EAAY,aAAa,MAAM;CAAK,GAC7C,KAAK;EAAE;EAAO;CAAM,CAAC,CACvB;CAEF,OAAO,QAAQ,OAAO,MAAM;AAC9B;;;ACzEA,IAAa,gBACX,QACA,UAEA,EACE,QACA,EAAE,OAAO,YAAY,GACrB,MAAM,SAAU,MAAM,OAAO,EAAE,OAAO,CAAC,IAA2B,MACpE;AAEF,IAAa,eACX,OACA,QACA,UACU;CACV,IAAI,MAAM,SAAS,SAAS;EAG1B,MAAM,QACJ,MAAM,UAAU,eAAe,MAAM,UAAU,mBAC3C,MAAM,QACN,KAAA;EACN,OAAO,EACL,OACA;GACE,KAAK,MAAM;GACX,OAAO;GACP,aAAa;GACb,eAAe,MAAM;GACrB,iBAAiB,MAAM,WAAW,KAAK,KAAA;GACvC,cAAc;EAChB,GACA;GACE,QAAQ,EAAE,QAAQ,EAAE,OAAO,WAAW,GAAG,OAAO,OAAO,MAAM,IAAI;GACjE,aAAa,MAAM,QAAQ,KAAK;GAChC,EACE,QACA,EAAE,OAAO,UAAU,GACnB,MAAM,YAAY,MAAM,SAAS,OAAO,OAAO,WAAW,MAAM,IAClE;GACA,MAAM,WAAW,cACb,EAAE,QAAQ,EAAE,OAAO,eAAe,GAAG,OAAO,eAAe,IAC3D;EACN,CACF;CACF;CACA,OAAO,EACL,OACA;EACE,KAAK,MAAM;EACX,OAAO;EACP,aAAa;EACb,aAAa,MAAM;EACnB,aAAa,MAAM,QAAQ,QAAQ,KAAA;CACrC,GACA,MAAM,OAAO,KAAI,UAAS,YAAY,OAAO,OAAO,QAAQ,KAAK,CAAC,CACpE;AACF;;;ACxCA,IAAM,eAAe;CACnB;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;AACF;;AAGA,IAAM,YAAY;CAChB;CACA;CACA;CACA;CACA;AACF;;;;;;;AAQA,IAAM,UACJ,SACA,gBAC+B;CAC/B,MAAM,QAAQ,WAAyB,QAAQ,SAAS,CAAC;CACzD,MAAM,cAAc,QAAQ,WAAU,SAAQ;EAC5C,MAAM,QAAQ;CAChB,CAAC;CACD,qBAAqB;EACnB,YAAY;EACZ,IAAI,aAAa;GACf,YAAY;GACZ,QAAQ,QAAQ;GAGhB,MAAM,QAAQ,QAAQ,SAAS;EACjC;CACF,CAAC;CACD,OAAO;EACI;EACT,OAAO,gBAAgB,KAAK;CAC9B;AACF;;AAGA,IAAM,cACJ,YAC+B;CAC/B,MAAM,gBAAmC;EACvC,MAAM,WAAoC,CAAC;EAC3C,KAAK,MAAM,OAAO,WAChB,SAAS,OAAO,QAAQ,QAAQ,IAAI;EAEtC,KAAK,MAAM,OAAO,cAChB,SAAS,OAAO,MAAM,QAAQ,IAAI;EAEpC,OAAO;CACT;CAEA,MAAM,UAAU,cAAiB;EAC/B,GAAG,QAAQ;EACX,UAAU,QAAQ;EAClB,cAAc,QAAQ,QAAQ,YAAY;CAC5C,CAAC;CAaD,OAAO,OAAO,SARD,MAAM,UAAS,SAAQ;EAClC,QAAQ,YAAW,SAAQ;GACzB,GAAG;GACH,UAAU,IAAI;GACd,cAAc,IAAI;EACpB,EAAE;CACJ,CAEuB,CAAI;AAC7B;;;;;;;;;;;;;;;;;;;;;AAsBA,SAAgB,kBAKd,SAC4B;CAC5B,IAAI,QAAQ,YAAY,KAAA,GAAW;EACjC,MAAM,SAAS,OAAO,KAAK,OAAO,CAAC,CAAC,QAClC,QACE,QAAQ,aACP,QAAoC,SAAS,KAAA,CAClD;EACA,IAAI,OAAO,SAAS,GAClB,MAAM,IAAI,UACR,sBAAsB,OAAO,KAAK,IAAI,EAAE,yEAC1C;EAEF,OAAO,OAAO,QAAQ,SAAS,IAAI;CACrC;CACA,OAAO,WAAW,OAAsC;AAC1D;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;ECnHA,MAAM,QAAQ;EAoBd,MAAM,OAAO;EAEb,MAAM,QAAQA,SAAA;EAGd,MAAM,WAA6B,gBAAgB;GACjD;GACA;GACA;EACF,CAAC;EAaD,MAAM,cAAoC,CAAC;EAC3C,KAAK,MAAM,SAAS;GAAC;GAAW;GAAU;EAAS,GACjD,YACQ,MAAM,cACN;GACJ,OAAO,YAAY;EACrB,GACA,EAAE,OAAO,OAAO,CAClB;EAEF,MAAM,UACc,OAAU,UAC3B,YAAqC;GACpC,MAAM,OAAQ,YAAY,UAAU,MAAM,UAAU,CAAC;GACrD,MAAM,QAAQ,OAAO,YAAY,aAAa,QAAQ,IAAI,IAAI;GAC9D,YAAY,SAAS;GACrB,KAAK,KAAK;EACZ;;EAGF,MAAM,mBAAmB;GACvB,MAAM,QAA8B,CAAC;GACrC,IAAI,MAAM,YAAY,KAAA,GACpB,MAAM,UAAU,MAAM;GAExB,IAAI,MAAM,WAAW,KAAA,GACnB,MAAM,SAAS,MAAM;GAEvB,IAAI,MAAM,YAAY,KAAA,GACpB,MAAM,UAAU,MAAM;GAExB,OAAO;EACT;EAIA,MAAM,qBAAqB;GACzB;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;EACF;EAEA,MAAM,mBAAmB;GACvB,IAAI,MAAM,YAAY,KAAA,GAAW;IAC/B,MAAM,QAAQ,mBAAmB,QAAO,SAAQ,MAAM,UAAU,KAAA,CAAS;IACzE,IAAI,MAAM,SAAS,GACjB,MAAM,IAAI,UACR,mBAAmB,MAAM,KAAK,IAAI,EAAE,yEACtC;IAGF,OAAO,kBAAkB,EAAE,SAAS,MAAM,QAAQ,CAAC;GACrD;GACA,IAAI,MAAM,aAAa,KAAA,GACrB,MAAM,IAAI,UAAU,kDAAkD;GAExE,OAAO,kBAAkB;IACvB;IACA,gBAAgB,MAAM,YAAY,CAAC;IACnC,cAAc,MAAM;IACpB,OAAO;IACP,kBAAkB,MAAM;IACxB,mBAAmB,MAAM;IACzB,cAAc,MAAM;IACpB,UAAU,MAAM,OAAO,UAAU;IACjC,WAAW,MAAM,OAAO,WAAW;IACnC,SAAS,MAAM,OAAO,SAAS;IAC/B,aAAa,MAAM,OAAO,aAAa;IACvC,aAAa,MAAM,OAAO,aAAa;IACvC,cAAc,MAAM,OAAO,cAAc;IACzC,iBAAiB,OAAO,YAAW,UAAS,KAAK,kBAAkB,KAAK,CAAC;IACzE,gBAAgB,OAAO,WAAU,UAAS,KAAK,iBAAiB,KAAK,CAAC;IACtE,iBAAiB,OAAO,YAAW,UAAS,KAAK,kBAAkB,KAAK,CAAC;IACzE,eAAe,KAAK,OAAO;GAC7B,CAAC;EACH;EAEA,MAAM,EAAE,SAAS,UAAU,UAAU,WAAW;EAIhD,MAAM,SAAS,eAAe,cAAc,MAAM,MAAM,CAAC;EACzD,MAAM,SAAS,MAAM;EAErB,MAAM,UAAU,eAAe,MAAM,MAAM,OAAO;EAClD,MAAM,UAAU,eAAe;GAC7B,MAAM,aAAa,MAAM,MAAM;GAC/B,OAAO,YAAY,WAAW,SAAS,aAAa;EACtD,CAAC;EACD,MAAM,WAAW,eACf,QAAQ,SAAS,QAAQ,MAAM,UAAU,IACrC,SAAS,QAAQ,QAAQ,MAAM,MAAM,IACrC,KAAA,CACN;;EAEA,MAAM,SAAS,eAAe;GAC5B,MAAW,MAAM;GACjB,MAAW;GACX,OAAO,SAAS,UAAU;EAC5B,CAAC;;;;;EAKD,MAAM,WAAW,eAAe;GAC9B,MAAM,EAAE,QAAQ,iBAAiB,MAAM;GACvC,OAAO,iBAAiB,OAAQ,OAAO,iBAAiB,KAAM;EAChE,CAAC;EAED,MAAM,oBACJ,MAAM,MAAM,QAAQ,KAAI,UAAS,YAAY,OAAO,OAAO,OAAO,KAAK,CAAC;EAC1E,MAAM,wBACJ,QAAQ,QACJ,kBAAkB,QAAQ,OAAO,QAAQ,OAAO,MAAM,aAAa,KAAK,IACxE;EACN,MAAM,mBAAwC,aAAa,OAAO,OAAO,KAAK;EAI9E,MAAM,cAAc,eAA+B,MAAM;EACzD,MAAM,aAAa,eAA+B,KAAK;EACvD,MAAM,iBAAiB,eAA+B,SAAS;EAC/D,MAAM,eAAe,eAAiC,OAAO;EAE7D,MAAM,SAAS,iBAAiB,YAAY,cAAc;EAE1D,MAAM,mBAAmB,aAAa,OAAO,MAAM;EAEnD,gBAAgB;GACd,IAAI,MAAM,WACR,WAAW;EAEf,CAAC;EAID,MAAM,eAAe,UAAsB;GACzC,IAAI,YAAY,SAAS,kBAAkB,MAAM,QAAQ,YAAY,KAAK,GACxE,WAAW;EAEf;EASA,IAAI,WAAW;EACf,YACQ,MAAM,MAAM,MAAM,gBAClB;GACJ,MAAM,UAAU,aAAa;GAC7B,WACE,YAAY,QAAQ,QAAQ,cAAc,kBAAkB;GAE9D,IAAI,SACF,QAAQ,QAAQ;EAEpB,GACA,EAAE,OAAO,MAAM,CACjB;;EAGA,IAAI,WAAW;GAAE,OAAO;GAAI,OAAO;EAAE;;;;;;EAOrC,MAAM,eAAe,aAAsB;GACzC,MAAM,UAAU,aAAa;GAC7B,IAAI,CAAC,SACH;GAEF,MAAM,EAAE,OAAO,UAAU,MAAM,MAAM;GACrC,IAAI,QAAQ,UAAU,OACpB,QAAQ,QAAQ;GAElB,IAAI,UAAU;IACZ,IAAI,UACF,QAAQ,MAAM;IAEhB,WAAW;IACX,QAAQ,kBAAkB,MAAM,QAAQ,MAAM,MAAM;IACpD;GACF;GACA,IACE,QAAQ,cAAc,kBAAkB,WACvC,UAAU,SAAS,SAAS,UAAU,SAAS,OAEhD;GAEF,IAAI,QAAQ,mBAAmB,SAAS,QAAQ,iBAAiB,OAC/D,QAAQ,kBAAkB,OAAO,KAAK;EAE1C;EACA,gBAAgB;GACd,YAAY,KAAK;EACnB,CAAC;EACD,YACQ,MAAM,MAAM,QACjB,MAAM,aAAa;GAClB,YAAY,KAAK,aAAa,SAAS,QAAQ;EACjD,GACA,EAAE,OAAO,OAAO,CAClB;;EAGA,MAAM,cAAc,YAA8B;GAChD,MAAM,QAAQ,QAAQ,kBAAkB,QAAQ,MAAM;GACtD,WAAW;IAAE,OAAO,QAAQ;IAAO;GAAM;GACzC,SAAS,UAAU,QAAQ,OAAO,KAAK;EACzC;EAEA,MAAM,WAAW,UAAiB;GAChC,WAAW,MAAM,aAAiC;EACpD;EAGA,MAAM,WAAW,UAAiB;GAChC,MAAM,UAAU,MAAM;GACtB,MAAM,QAAQ,QAAQ,kBAAkB,QAAQ,MAAM;GACtD,IAAI,QAAQ,UAAU,MAAM,MAAM,MAAM,OACtC;GAEF,IAAI,UAAU,MAAM,MAAM,MAAM,OAC9B,WAAW,OAAO;EAEtB;;;;;EAQA,MAAM,cAAkB,WAAuB;GAC7C,MAAM,SAAS,IAAI,IAAI,MAAM,MAAM,QAAQ,KAAI,UAAS,MAAM,EAAE,CAAC;GACjE,MAAM,SAAS,OAAO;GACtB,MAAM,QAAQ,SACX,SAAS,CAAA,CACT,QAAQ,MACP,UACE,MAAM,SAAS,WACf,MAAM,WAAW,eACjB,CAAC,OAAO,IAAI,MAAM,EAAE,CACxB;GACF,IAAI,OAAO;IACT,OAAO,MAAM;IACb,MAAM,SAAwB;KAC5B,SAAS,MAAM;KACf,WAAW,MAAM,SAAS,WAAW,MAAM;IAC7C;IACA,KAAK,UAAU,MAAM;GACvB;GACA,OAAO;EACT;EAEA,MAAM,aAAa,UAAyB;GAC1C,MAAM,UAAU,MAAM;GACtB,MAAM,MAAM,WAAW,OAAO,SAAS,YAAY,KAAK;GACxD,IAAI,CAAC,IAAI,aAEP,QAAQ,KAAK;GAGf,IADgB,iBAAiB,SAAS,UAAU,GAAG,CACnD,GACF,MAAM,eAAe;EAEzB;EAqBA,SAAa;GAhBX,aAAa;IACX,WAAW;GACb;GACA,MAAM,SAAiB,iBAAiB,SAAS,IAAI,IAAI,CAAC;GAC1D,cAAc;IACZ,SAAS,OAAO;GAClB;GACA,aAAa;IACX,SAAS,MAAM;GACjB;GACA,aAAa;IACX,SAAS,aAAa;GACxB;GACA,SAAS;GACT;EAEW,CAAO;;GAIlB,OAAAC,UAAA,GAAAC,mBAkDM,OAAA;IAjDJ,KAAI;IACJ,OAAM;IACL,aAAW,QAAA,QAAO,KAAQ,KAAA;IAC1B,sBAAoBC,MAAA,KAAA,CAAK,CAAC,QAAQ;IAClC,SAAO;;IAERC,mBAUM,OAAA;KATJ,KAAI;KACJ,OAAM;KACN,MAAK;KACJ,cAAY,OAAA,MAAO;KACnB,UAAM,OAAA,OAAA,OAAA,MAAE,GAAA,SAAAD,MAAA,MAAA,CAAM,CAAC,YAAPA,MAAA,MAAA,CAAM,CAAC,SAAQ,GAAA,IAAA;I,IAExBC,mBAEM,OAFN,YAEM,EADJH,UAAA,GAAAI,YAA+BC,wBAAf,WAAW,CAAA,E;IAG/BF,mBAIM,OAJN,YAIM,CAHY,QAAA,QACbG,WAA6D,KAAA,QAAA,QAAA,EAA1C,SAAS,QAAA,MAAO,SAA0B,CAArBC,gBAAAC,gBAAA,OAAA,MAAO,IAAI,GAAA,CAAA,C,iBACrDR,UAAA,GAAAC,mBAA0CQ,UAAA,EAAA,KAAA,EAAA,GAAA,CAAtBF,gBAAAC,gBAAA,SAAA,KAAQ,GAAA,CAAA,C;KAE/BR,UAAA,GAAAI,YAAmCC,wBAAnB,eAAe,CAAA;IAC/BF,mBAyBM,OAzBN,YAyBM,EAxBJH,UAAA,GAAAI,YAA8BC,wBAAd,UAAU,CAAA,KAC1BL,UAAA,GAAAC,mBAsBE,SAAA;KArBC,KAAKC,MAAA,KAAA,CAAK,CAAC,MAAM;KAClB,KAAI;KACJ,OAAM;KACN,MAAK;KACL,MAAK;KACL,qBAAkB;KACjB,iBAAe,QAAA,QAAO,SAAA;KACtB,iBAAe,QAAA,QAAUA,MAAA,MAAA,IAAS,KAAA;KAClC,yBAAuB,SAAA;KACvB,cAAY,OAAA,MAAO;KACnB,oBAAkB,OAAA,MAAO;KAC1B,cAAa;KACb,aAAY;KACZ,gBAAe;KACf,YAAW;KACV,UAAU,QAAA,UAAO;KACV;KACE;KACT,SAAO;KACP,SAAO;KACP,mBAAiB;;;;;;;AEnb1B,IAAA,cAAeS"}
@@ -26,6 +26,7 @@ declare const __VLS_base: import("vue").DefineComponent<ConsoleProps, {
26
26
  prompt: string;
27
27
  maxEntries: number;
28
28
  recallLimit: number;
29
+ commands: import("@dolusoft/command-console-core").CommandDefinition[];
29
30
  initialState: Partial<import("@dolusoft/command-console-core").ControllableState>;
30
31
  createId: () => string;
31
32
  beforeRun: (info: import("@dolusoft/command-console-core").RunInfo, signal: AbortSignal) => import("@dolusoft/command-console-core").BeforeRunResult | Promise<import("@dolusoft/command-console-core").BeforeRunResult>;
@@ -33,6 +34,7 @@ declare const __VLS_base: import("vue").DefineComponent<ConsoleProps, {
33
34
  formatError: (error: unknown, info: import("@dolusoft/command-console-core").RunInfo) => import("@dolusoft/command-protocol").CommandResponse;
34
35
  isSensitive: (line: string) => boolean;
35
36
  shouldRecord: (line: string) => boolean;
37
+ console: import("@dolusoft/command-console-core").Console<OptionalFeatures>;
36
38
  entries: ConsoleEntry[];
37
39
  recall: string[];
38
40
  context: ContextFrame[];
@@ -1,8 +1,8 @@
1
1
  import type { MaybeRef, MaybeRefOrGetter, ShallowRef } from 'vue';
2
2
  import type { BeforeRunResult, CommandDefinition, Console, ConsoleOptions, ConsoleState, ControllableState, OptionalFeatures, RunInfo, RunningCommand } from '@dolusoft/command-console-core';
3
- import type { CommandResponse, ConsoleEntry, ContextFrame, CustomBlock, JsonBlock, ListBlock, MessageBlock, MessageCode, MessageParams, OutputBlock, OutputEntry, TableBlock, TextBlock } from '@dolusoft/command-protocol';
3
+ import type { Actor, CommandResponse, ConsoleEntry, ContextFrame, CustomBlock, JsonBlock, ListBlock, MessageBlock, MessageCode, MessageParams, OutputBlock, OutputEntry, TableBlock, TextBlock } from '@dolusoft/command-protocol';
4
4
  export type { BeforeRunResult, CommandDefinition, CommandRequest, CommandReturn, CompletionRequest, CompletionResult, CompletionSession, Console, ConsoleOptions, ConsoleState, ControllableState, InputState, KeyInput, OptionalFeatures, RunContext, RunInfo, RunningCommand, Updater } from '@dolusoft/command-console-core';
5
- export type { CommandResponse, CommandSpec, ConsoleEffect, ConsoleEntry, ContextFrame, CustomBlock, InputEntry, JsonBlock, JsonPrimitive, JsonValue, ListBlock, MessageBlock, MessageCode, MessageParams, OutputBlock, OutputEntry, TableBlock, TableColumn, TextBlock } from '@dolusoft/command-protocol';
5
+ export type { Actor, CommandResponse, CommandSpec, ConsoleEffect, ConsoleEntry, ContextFrame, CustomBlock, InputEntry, JsonBlock, JsonPrimitive, JsonValue, ListBlock, MessageBlock, MessageCode, MessageParams, OutputBlock, OutputEntry, TableBlock, TableColumn, TextBlock } from '@dolusoft/command-protocol';
6
6
  /** The text of one message code: a string, or a function of its parameters. */
7
7
  export type MessageLabels = {
8
8
  [C in MessageCode]: string | ((params: MessageParams[C]) => string);
@@ -28,18 +28,37 @@ export interface ConsoleLabels {
28
28
  busy: string;
29
29
  /** The text of each library-made message block. */
30
30
  messages: MessageLabels;
31
+ /**
32
+ * Shown before the prompt of a line that came from the assistant or an
33
+ * external agent (C-85). Default `'AI'` and `'Agent'`.
34
+ */
35
+ actors: ActorLabels;
31
36
  }
37
+ /** The label of each actor other than the user (C-85). */
38
+ export type ActorLabels = Record<Exclude<Actor, 'user'>, string>;
32
39
  /**
33
- * The `labels` prop: any part of `ConsoleLabels`. `messages` is merged one
34
- * level deep, so overriding one message keeps the others.
40
+ * The `labels` prop: any part of `ConsoleLabels`. `messages` and `actors`
41
+ * are merged one level deep, so overriding one message keeps the others.
35
42
  */
36
- export type ConsoleLabelsInput = Partial<Omit<ConsoleLabels, 'messages'>> & {
43
+ export type ConsoleLabelsInput = Partial<Omit<ConsoleLabels, 'messages' | 'actors'>> & {
37
44
  messages?: Partial<MessageLabels>;
45
+ actors?: Partial<ActorLabels>;
38
46
  };
39
47
  /** The props of `<CommandConsole>`. */
40
48
  export interface ConsoleProps {
41
- /** The commands; validated on every change (C-56). */
42
- commands: CommandDefinition[];
49
+ /**
50
+ * A console created elsewhere, with every optional feature (C-84): the
51
+ * component draws it and never disposes it, so a remount shows the same
52
+ * entries and input. Read once. Its options are its creator's: with it,
53
+ * the console-option props (`commands` to `shouldRecord`) and the
54
+ * controlled slices throw; `labels` and `autofocus` apply.
55
+ */
56
+ console?: Console<OptionalFeatures>;
57
+ /**
58
+ * The commands; validated on every change (C-56). Required without
59
+ * `console`.
60
+ */
61
+ commands?: CommandDefinition[];
43
62
  /** The root prompt. Default `'>'`. */
44
63
  prompt?: string;
45
64
  /** The first value of each uncontrolled slice; read once. */
@@ -173,10 +192,25 @@ export type UseCommandConsoleOptions<T extends Partial<OptionalFeatures> = {}> =
173
192
  commands: MaybeRefOrGetter<CommandDefinition[]>;
174
193
  /** Read once, when the console is created. */
175
194
  initialState?: MaybeRefOrGetter<Partial<ControllableState> | undefined>;
195
+ /** Only in `UseExternalConsoleOptions`, without the other options. */
196
+ console?: never;
197
+ };
198
+ /**
199
+ * The options of `useCommandConsole` over a console created elsewhere
200
+ * (C-84): the console alone. Its options belong to whoever created it; the
201
+ * composable only mirrors its state and never disposes it.
202
+ */
203
+ export type UseExternalConsoleOptions<T extends Partial<OptionalFeatures> = {}> = {
204
+ console: Console<T>;
205
+ } & {
206
+ [K in Exclude<keyof UseCommandConsoleOptions<T>, 'console'>]?: never;
176
207
  };
177
208
  /** What `useCommandConsole` returns. */
178
209
  export interface UseCommandConsoleReturn<T extends Partial<OptionalFeatures> = {}> {
179
- /** The console; disposed with the effect scope. */
210
+ /**
211
+ * The console; disposed with the effect scope when the composable created
212
+ * it.
213
+ */
180
214
  console: Console<T>;
181
215
  /** The state after every action (C-68). */
182
216
  state: Readonly<ShallowRef<ConsoleState>>;
@@ -1,8 +1,8 @@
1
1
  import type { MessageBlock } from '@dolusoft/command-protocol';
2
2
  import type { ConsoleLabels, ConsoleLabelsInput } from './contract.js';
3
3
  /**
4
- * The defaults with `labels` over them; `messages` merges one level deep, so
5
- * overriding one message keeps the others.
4
+ * The defaults with `labels` over them; `messages` and `actors` merge one
5
+ * level deep, so overriding one message keeps the others.
6
6
  */
7
7
  export declare const resolveLabels: (labels?: ConsoleLabelsInput) => ConsoleLabels;
8
8
  /** The text of a message block, from `labels.messages`. */
@@ -1,13 +1,16 @@
1
1
  import { type OptionalFeatures } from '@dolusoft/command-console-core';
2
- import type { UseCommandConsoleOptions, UseCommandConsoleReturn } from './contract.js';
2
+ import type { UseCommandConsoleOptions, UseCommandConsoleReturn, UseExternalConsoleOptions } from './contract.js';
3
3
  /**
4
- * Creates a console from `options` and keeps it in step with them.
4
+ * Creates a console from `options` and keeps it in step with them; or, given
5
+ * `{ console }` alone, mirrors a console created elsewhere and leaves its
6
+ * options and its life to its creator (C-84).
5
7
  *
6
8
  * A value option (`commands`, `prompt`, `state`, `maxEntries`,
7
9
  * `recallLimit`) may be a plain value, a ref or a getter. A function option
8
10
  * (`beforeRun`, `onError`, …) may be the function or a ref that holds it: a
9
11
  * bare function is the option itself, never a getter. `features` and
10
- * `initialState` are read once.
12
+ * `initialState` are read once. `console` with any other option throws a
13
+ * `TypeError`.
11
14
  *
12
15
  * @example
13
16
  * ```ts
@@ -17,4 +20,4 @@ import type { UseCommandConsoleOptions, UseCommandConsoleReturn } from './contra
17
20
  * })
18
21
  * ```
19
22
  */
20
- export declare function useCommandConsole<T extends Partial<OptionalFeatures> = {}>(options: UseCommandConsoleOptions<T>): UseCommandConsoleReturn<T>;
23
+ export declare function useCommandConsole<T extends Partial<OptionalFeatures> = {}>(options: UseCommandConsoleOptions<T> | UseExternalConsoleOptions<T>): UseCommandConsoleReturn<T>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dolusoft/command-console",
3
- "version": "1.0.1",
3
+ "version": "1.1.0",
4
4
  "description": "A headless command console for Vue 3: a scrollback above one input line, with recall, Tab completion, a context stack and redaction of sensitive lines. Ships no CSS.",
5
5
  "private": false,
6
6
  "type": "module",
@@ -40,8 +40,8 @@
40
40
  ],
41
41
  "license": "MIT",
42
42
  "dependencies": {
43
- "@dolusoft/command-console-core": "1.0.1",
44
- "@dolusoft/command-protocol": "1.0.1"
43
+ "@dolusoft/command-console-core": "1.1.0",
44
+ "@dolusoft/command-protocol": "1.1.0"
45
45
  },
46
46
  "devDependencies": {
47
47
  "vue": "^3.5.43"