@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 +12 -4
- package/dist/command-console.js +177 -75
- package/dist/command-console.js.map +1 -1
- package/dist/types/command-console.vue.d.ts +2 -0
- package/dist/types/contract.d.ts +42 -8
- package/dist/types/labels.d.ts +2 -2
- package/dist/types/use-command-console.d.ts +7 -4
- package/package.json +3 -3
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`,
|
|
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`
|
|
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
|
|
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
|
package/dist/command-console.js
CHANGED
|
@@ -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`
|
|
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")
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
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
|
-
*
|
|
211
|
-
*
|
|
212
|
-
*
|
|
213
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
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
|
-
|
|
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
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
maxEntries
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
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
|
|
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
|
-
|
|
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 =
|
|
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(() =>
|
|
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(() =>
|
|
601
|
+
run: (line) => submitting(() => instance.run(line)),
|
|
500
602
|
cancel: () => {
|
|
501
|
-
|
|
603
|
+
instance.cancel();
|
|
502
604
|
},
|
|
503
605
|
reset: () => {
|
|
504
|
-
|
|
606
|
+
instance.reset();
|
|
505
607
|
},
|
|
506
608
|
clear: () => {
|
|
507
|
-
|
|
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":"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"}
|
|
@@ -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[];
|
package/dist/types/contract.d.ts
CHANGED
|
@@ -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`
|
|
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
|
-
/**
|
|
42
|
-
|
|
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
|
-
/**
|
|
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>>;
|
package/dist/types/labels.d.ts
CHANGED
|
@@ -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`
|
|
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
|
|
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
|
|
44
|
-
"@dolusoft/command-protocol": "1.0
|
|
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"
|