@franzzemen/brokenstock-shell 0.9.0 → 0.9.2

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.
@@ -3010,8 +3010,70 @@ const HELP_USAGE = 'help [command]\n\n' +
3010
3010
  ' Arguments:\n' +
3011
3011
  ' [command] Optional command name to show detailed help for';
3012
3012
 
3013
+ /*
3014
+ Created by Franz Zemen 2026-10-18
3015
+ License Type: UNLICENSED
3016
+
3017
+ A question a command leaves open in the terminal (live-trading-foundation.prd.md D24).
3018
+
3019
+ A command can ask instead of acting: "Send SIM · Buy 100 NVDA? (Enter or yes to send, Esc to
3020
+ cancel)". The next line typed — including a bare Enter — is the answer and goes to the command,
3021
+ not to the command parser. Esc cancels. The arming code is asked the same way, with the input
3022
+ masked so the code never appears on screen or in the terminal's history.
3023
+
3024
+ Plain logic, no Angular: `CommandService` holds one of these and the terminal reads it.
3025
+ */
3026
+ /** True for the answers that mean yes: a bare Enter, `y`, `yes`. */
3027
+ function isYes(text) {
3028
+ const t = text.trim().toLowerCase();
3029
+ return t === '' || t === 'y' || t === 'yes';
3030
+ }
3031
+ /** What the history shows for an answer: the text, a masked run, or "(Enter)". */
3032
+ function answerEcho(prompt, text) {
3033
+ if (text === '')
3034
+ return '(Enter)';
3035
+ return prompt.masked ? '•'.repeat(text.length) : text;
3036
+ }
3037
+ /** Holds at most one open question. Asking again cancels the one before it. */
3038
+ class PendingPromptState {
3039
+ #current = null;
3040
+ get current() {
3041
+ return this.#current;
3042
+ }
3043
+ ask(prompt) {
3044
+ const before = this.#current;
3045
+ this.#current = prompt;
3046
+ if (before && before !== prompt)
3047
+ before.cancel?.();
3048
+ }
3049
+ /**
3050
+ * Takes the open question and closes it, so an answer that asks again opens a fresh one rather
3051
+ * than being closed afterwards.
3052
+ */
3053
+ take() {
3054
+ const p = this.#current;
3055
+ this.#current = null;
3056
+ return p;
3057
+ }
3058
+ cancel() {
3059
+ const p = this.#current;
3060
+ if (!p)
3061
+ return false;
3062
+ this.#current = null;
3063
+ p.cancel?.();
3064
+ return true;
3065
+ }
3066
+ }
3067
+
3013
3068
  const CommandInputSchema = z.string().trim().min(1, 'Command cannot be empty');
3014
3069
  class CommandService {
3070
+ #prompts;
3071
+ /**
3072
+ * True while a command (or an answer) runs. A question it asks is written to the history only
3073
+ * after the command's own lines, so it reads as the last thing said, under the preview it is about.
3074
+ */
3075
+ #running;
3076
+ #deferredQuestion;
3015
3077
  constructor() {
3016
3078
  this.history = signal([], /* @ts-ignore */
3017
3079
  ...(ngDevMode ? [{ debugName: "history" }] : /* istanbul ignore next */ []));
@@ -3021,6 +3083,18 @@ class CommandService {
3021
3083
  ...(ngDevMode ? [{ debugName: "isProcessing" }] : /* istanbul ignore next */ []));
3022
3084
  this.helpOverlay = signal(null, /* @ts-ignore */
3023
3085
  ...(ngDevMode ? [{ debugName: "helpOverlay" }] : /* istanbul ignore next */ []));
3086
+ /**
3087
+ * The question a command left open, or null (live-trading-foundation.prd.md D24). While set, the
3088
+ * next line — a bare Enter included — is its answer, not a command; Esc cancels.
3089
+ */
3090
+ this.pending = signal(null, /* @ts-ignore */
3091
+ ...(ngDevMode ? [{ debugName: "pending" }] : /* istanbul ignore next */ []));
3092
+ this.#prompts = new PendingPromptState();
3093
+ /**
3094
+ * True while a command (or an answer) runs. A question it asks is written to the history only
3095
+ * after the command's own lines, so it reads as the last thing said, under the preview it is about.
3096
+ */
3097
+ this.#running = false;
3024
3098
  this.authService = inject(AuthenticationService);
3025
3099
  this.registry = inject(CommandRegistryService);
3026
3100
  this.router = inject(Router);
@@ -3032,12 +3106,80 @@ class CommandService {
3032
3106
  effect(() => {
3033
3107
  if (!this.authService.authenticated()) {
3034
3108
  this.history.set([]);
3109
+ // A question asked for a session that has ended must not be answerable by the next one.
3110
+ this.#prompts.cancel();
3111
+ this.pending.set(null);
3035
3112
  }
3036
3113
  });
3037
3114
  }
3038
3115
  dismissHelp() {
3039
3116
  this.helpOverlay.set(null);
3040
3117
  }
3118
+ /** Leaves a question open; the next line typed answers it. A question already open is cancelled. */
3119
+ ask(prompt) {
3120
+ this.#prompts.ask(prompt);
3121
+ this.pending.set(prompt);
3122
+ if (this.#running)
3123
+ this.#deferredQuestion = prompt;
3124
+ else
3125
+ this.#writeQuestion(prompt);
3126
+ }
3127
+ #writeQuestion(prompt) {
3128
+ this.history.update(h => [...h, {
3129
+ input: '',
3130
+ result: { success: true, message: prompt.question, timestamp: new Date(), severity: 'info' },
3131
+ }]);
3132
+ }
3133
+ /** After a command's or an answer's own lines: the question it asked, if it is still open. */
3134
+ #finishRun() {
3135
+ this.#running = false;
3136
+ const q = this.#deferredQuestion;
3137
+ this.#deferredQuestion = undefined;
3138
+ if (q && this.#prompts.current === q)
3139
+ this.#writeQuestion(q);
3140
+ }
3141
+ /** Esc: closes the open question, if any. True when there was one. */
3142
+ cancelPending() {
3143
+ if (!this.#prompts.cancel())
3144
+ return false;
3145
+ this.pending.set(null);
3146
+ this.currentInput.set('');
3147
+ this.history.update(h => [...h, {
3148
+ input: '',
3149
+ result: { success: true, message: 'Cancelled — nothing was sent.', timestamp: new Date(), severity: 'info' },
3150
+ }]);
3151
+ return true;
3152
+ }
3153
+ #answer(rawInput) {
3154
+ const prompt = this.#prompts.take();
3155
+ if (!prompt)
3156
+ return;
3157
+ this.pending.set(null);
3158
+ const text = rawInput.trim();
3159
+ const echo = answerEcho(prompt, text);
3160
+ this.currentInput.set('');
3161
+ const record = (result) => {
3162
+ this.history.update(h => [...h, { input: echo, result }]);
3163
+ this.#finishRun();
3164
+ };
3165
+ this.#running = true;
3166
+ let out;
3167
+ try {
3168
+ out = prompt.answer(text);
3169
+ }
3170
+ catch (err) {
3171
+ out = { success: false, message: err?.message ?? String(err), timestamp: new Date() };
3172
+ }
3173
+ if (out instanceof Promise) {
3174
+ this.isProcessing.set(true);
3175
+ out
3176
+ .then(record, err => record({ success: false, message: err?.message ?? String(err), timestamp: new Date() }))
3177
+ .finally(() => this.isProcessing.set(false));
3178
+ }
3179
+ else {
3180
+ record(out);
3181
+ }
3182
+ }
3041
3183
  echoCommand(input) {
3042
3184
  if (this.#coalesce(input))
3043
3185
  return;
@@ -3059,6 +3201,11 @@ class CommandService {
3059
3201
  }]);
3060
3202
  }
3061
3203
  execute(rawInput) {
3204
+ // An open question takes the line, a bare Enter included (D24).
3205
+ if (this.#prompts.current) {
3206
+ this.#answer(rawInput);
3207
+ return;
3208
+ }
3062
3209
  const validation = CommandInputSchema.safeParse(rawInput);
3063
3210
  if (!validation.success) {
3064
3211
  this.history.update(h => [...h, {
@@ -3076,7 +3223,14 @@ class CommandService {
3076
3223
  // masked under `login` stays masked under any other word for it).
3077
3224
  const def = this.registry.resolve(command)?.command;
3078
3225
  const displayInput = def?.sanitizeInput ? def.sanitizeInput(input) : input;
3079
- const resultOrPromise = this.#dispatch(command, parts.slice(1));
3226
+ this.#running = true;
3227
+ let resultOrPromise;
3228
+ try {
3229
+ resultOrPromise = this.#dispatch(command, parts.slice(1));
3230
+ }
3231
+ catch (err) {
3232
+ resultOrPromise = { success: false, message: err?.message ?? String(err), timestamp: new Date() };
3233
+ }
3080
3234
  this.currentInput.set('');
3081
3235
  if (resultOrPromise instanceof Promise) {
3082
3236
  this.isProcessing.set(true);
@@ -3090,7 +3244,10 @@ class CommandService {
3090
3244
  this.history.update(h => [...h, { input: displayInput, result: historyResult, repeatCount: 1 }]);
3091
3245
  }
3092
3246
  })
3093
- .finally(() => this.isProcessing.set(false));
3247
+ .finally(() => {
3248
+ this.#finishRun();
3249
+ this.isProcessing.set(false);
3250
+ });
3094
3251
  }
3095
3252
  else {
3096
3253
  if (resultOrPromise.isHelp && resultOrPromise.message) {
@@ -3100,6 +3257,7 @@ class CommandService {
3100
3257
  if (!this.#coalesce(displayInput)) {
3101
3258
  this.history.update(h => [...h, { input: displayInput, result: historyResult, repeatCount: 1 }]);
3102
3259
  }
3260
+ this.#finishRun();
3103
3261
  }
3104
3262
  }
3105
3263
  #coalesce(input) {
@@ -3480,6 +3638,8 @@ class CommandTerminal {
3480
3638
  ...(ngDevMode ? [{ debugName: "history" }] : /* istanbul ignore next */ []));
3481
3639
  this.currentInput = this.commandService.currentInput;
3482
3640
  this.isProcessing = this.commandService.isProcessing;
3641
+ /** The question a command left open (D24): the input answers it; Esc cancels. */
3642
+ this.pending = this.commandService.pending;
3483
3643
  this.inputEl = viewChild('terminalInput', /* @ts-ignore */
3484
3644
  ...(ngDevMode ? [{ debugName: "inputEl" }] : /* istanbul ignore next */ []));
3485
3645
  this.tabCycleIndex = -1;
@@ -3524,7 +3684,8 @@ class CommandTerminal {
3524
3684
  }
3525
3685
  onSubmit() {
3526
3686
  const input = this.currentInput();
3527
- if (!input.trim())
3687
+ // With a question open, a bare Enter is an answer.
3688
+ if (!input.trim() && !this.pending())
3528
3689
  return;
3529
3690
  this.commandService.execute(input);
3530
3691
  this.tabCycleIndex = -1;
@@ -3542,6 +3703,11 @@ class CommandTerminal {
3542
3703
  dismissHelp() {
3543
3704
  this.commandService.dismissHelp();
3544
3705
  }
3706
+ /** Esc: cancels an open question first, otherwise closes the help overlay. */
3707
+ onEscape() {
3708
+ if (!this.commandService.cancelPending())
3709
+ this.dismissHelp();
3710
+ }
3545
3711
  onTab(event) {
3546
3712
  event.preventDefault();
3547
3713
  const input = this.currentInput().trim();
@@ -3591,11 +3757,11 @@ class CommandTerminal {
3591
3757
  this.inputEl()?.nativeElement.focus();
3592
3758
  }
3593
3759
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.2", ngImport: i0, type: CommandTerminal, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
3594
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.2", type: CommandTerminal, isStandalone: true, selector: "shell-command-terminal", inputs: { focusOnAppear: { classPropertyName: "focusOnAppear", publicName: "focusOnAppear", isSignal: true, isRequired: false, transformFunction: null } }, viewQueries: [{ propertyName: "inputEl", first: true, predicate: ["terminalInput"], descendants: true, isSignal: true }], ngImport: i0, template: "<div class=\"terminal\">\n <div class=\"terminal-input-row\">\n <span class=\"prompt\">$</span>\n <div class=\"input-wrapper\">\n @if (ghostText()) {\n <input\n class=\"ghost-input\"\n type=\"text\"\n [value]=\"ghostText()\"\n disabled\n tabindex=\"-1\"\n aria-hidden=\"true\"\n />\n }\n <input\n #terminalInput\n class=\"terminal-input\"\n type=\"text\"\n [ngModel]=\"currentInput()\"\n (ngModelChange)=\"onInputChange($event)\"\n (keydown.enter)=\"onSubmit()\"\n (keydown.tab)=\"onTab($any($event))\"\n (keydown.escape)=\"dismissHelp()\"\n placeholder=\"Type a command...\"\n [disabled]=\"isProcessing()\"\n autocomplete=\"off\"\n spellcheck=\"false\"\n />\n </div>\n </div>\n <div class=\"terminal-output\">\n @for (entry of history(); track entry.result.timestamp) {\n @if (entry.input) {\n <div class=\"terminal-line\">\n <span class=\"prompt\">$</span>\n <span class=\"command-text\">{{ entry.input }}@if ((entry.repeatCount ?? 1) > 1) { <span class=\"repeat-count\">(\u00D7{{ entry.repeatCount }})</span>}</span>\n </div>\n }\n @if (entry.result.message) {\n <div class=\"terminal-line\" [class]=\"getSeverityClass(entry.result)\">{{ entry.result.message }}</div>\n }\n }\n </div>\n</div>\n", styles: [".terminal{display:flex;flex-direction:column;height:100%;background-color:var(--color-terminal-bg);border-top:1px solid var(--color-surface-border);font-family:Roboto Mono,Courier New,monospace;font-size:.85rem}.terminal-output{flex:1;overflow-y:auto;padding:8px 12px;display:flex;flex-direction:column}.terminal-line{line-height:1.6;color:var(--color-text-primary);white-space:pre-wrap}.terminal-line.error{color:var(--color-loss)}.terminal-line.warning{color:var(--color-warning)}.terminal-line.info{color:var(--color-info)}.prompt{color:var(--color-terminal-prompt);margin-right:8px;font-weight:700}.command-text{color:var(--color-terminal-text)}.repeat-count{color:var(--color-secondary-info);opacity:.6;margin-left:4px}.terminal-input-row{display:flex;align-items:center;padding:8px 12px;border-bottom:1px solid var(--color-surface-border)}.input-wrapper{flex:1;display:grid}.ghost-input,.terminal-input{grid-area:1 / 1;font-family:inherit;font-size:inherit;line-height:normal;margin:0;padding:0;border:none;background:transparent;outline:none;-webkit-appearance:none;appearance:none;width:100%}.ghost-input{color:var(--color-secondary-info);opacity:.3;pointer-events:none}.terminal-input{color:var(--color-terminal-text);caret-color:var(--color-terminal-prompt)}.terminal-input::placeholder{color:var(--color-secondary-info);opacity:.5}\n"], dependencies: [{ kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$2.DefaultValueAccessor, selector: "input:not([type=checkbox]):not([ngNoCva])[formControlName],textarea:not([ngNoCva])[formControlName],input:not([type=checkbox]):not([ngNoCva])[formControl],textarea:not([ngNoCva])[formControl],input:not([type=checkbox]):not([ngNoCva])[ngModel],textarea:not([ngNoCva])[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1$2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$2.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }], changeDetection: i0.ChangeDetectionStrategy.Eager }); }
3760
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.2", type: CommandTerminal, isStandalone: true, selector: "shell-command-terminal", inputs: { focusOnAppear: { classPropertyName: "focusOnAppear", publicName: "focusOnAppear", isSignal: true, isRequired: false, transformFunction: null } }, viewQueries: [{ propertyName: "inputEl", first: true, predicate: ["terminalInput"], descendants: true, isSignal: true }], ngImport: i0, template: "<div class=\"terminal\">\n <div class=\"terminal-input-row\">\n <span class=\"prompt\">$</span>\n <div class=\"input-wrapper\">\n @if (ghostText() && !pending()) {\n <input\n class=\"ghost-input\"\n type=\"text\"\n [value]=\"ghostText()\"\n disabled\n tabindex=\"-1\"\n aria-hidden=\"true\"\n />\n }\n <input\n #terminalInput\n class=\"terminal-input\"\n [type]=\"pending()?.masked ? 'password' : 'text'\"\n [class.answering]=\"!!pending()\"\n [ngModel]=\"currentInput()\"\n (ngModelChange)=\"onInputChange($event)\"\n (keydown.enter)=\"onSubmit()\"\n (keydown.tab)=\"onTab($any($event))\"\n (keydown.escape)=\"onEscape()\"\n [placeholder]=\"pending()?.question ?? 'Type a command...'\"\n [attr.autocomplete]=\"pending()?.masked ? 'one-time-code' : 'off'\"\n [disabled]=\"isProcessing()\"\n spellcheck=\"false\"\n />\n </div>\n </div>\n <div class=\"terminal-output\">\n @for (entry of history(); track entry.result.timestamp) {\n @if (entry.input) {\n <div class=\"terminal-line\">\n <span class=\"prompt\">$</span>\n <span class=\"command-text\">{{ entry.input }}@if ((entry.repeatCount ?? 1) > 1) { <span class=\"repeat-count\">(\u00D7{{ entry.repeatCount }})</span>}</span>\n </div>\n }\n @if (entry.result.message) {\n <div class=\"terminal-line\" [class]=\"getSeverityClass(entry.result)\">{{ entry.result.message }}</div>\n }\n }\n </div>\n</div>\n", styles: [".terminal{display:flex;flex-direction:column;height:100%;background-color:var(--color-terminal-bg);border-top:1px solid var(--color-surface-border);font-family:Roboto Mono,Courier New,monospace;font-size:.85rem}.terminal-output{flex:1;overflow-y:auto;padding:8px 12px;display:flex;flex-direction:column}.terminal-line{line-height:1.6;color:var(--color-text-primary);white-space:pre-wrap}.terminal-line.error{color:var(--color-loss)}.terminal-line.warning{color:var(--color-warning)}.terminal-line.info{color:var(--color-info)}.prompt{color:var(--color-terminal-prompt);margin-right:8px;font-weight:700}.command-text{color:var(--color-terminal-text)}.repeat-count{color:var(--color-secondary-info);opacity:.6;margin-left:4px}.terminal-input-row{display:flex;align-items:center;padding:8px 12px;border-bottom:1px solid var(--color-surface-border)}.input-wrapper{flex:1;display:grid}.ghost-input,.terminal-input{grid-area:1 / 1;font-family:inherit;font-size:inherit;line-height:normal;margin:0;padding:0;border:none;background:transparent;outline:none;-webkit-appearance:none;appearance:none;width:100%}.ghost-input{color:var(--color-secondary-info);opacity:.3;pointer-events:none}.terminal-input{color:var(--color-terminal-text);caret-color:var(--color-terminal-prompt)}.terminal-input::placeholder{color:var(--color-secondary-info);opacity:.5}.terminal-input.answering::placeholder{color:var(--color-neutral);opacity:1}\n"], dependencies: [{ kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$2.DefaultValueAccessor, selector: "input:not([type=checkbox]):not([ngNoCva])[formControlName],textarea:not([ngNoCva])[formControlName],input:not([type=checkbox]):not([ngNoCva])[formControl],textarea:not([ngNoCva])[formControl],input:not([type=checkbox]):not([ngNoCva])[ngModel],textarea:not([ngNoCva])[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1$2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$2.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }], changeDetection: i0.ChangeDetectionStrategy.Eager }); }
3595
3761
  }
3596
3762
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.2", ngImport: i0, type: CommandTerminal, decorators: [{
3597
3763
  type: Component,
3598
- args: [{ selector: 'shell-command-terminal', imports: [FormsModule], changeDetection: ChangeDetectionStrategy.Eager, template: "<div class=\"terminal\">\n <div class=\"terminal-input-row\">\n <span class=\"prompt\">$</span>\n <div class=\"input-wrapper\">\n @if (ghostText()) {\n <input\n class=\"ghost-input\"\n type=\"text\"\n [value]=\"ghostText()\"\n disabled\n tabindex=\"-1\"\n aria-hidden=\"true\"\n />\n }\n <input\n #terminalInput\n class=\"terminal-input\"\n type=\"text\"\n [ngModel]=\"currentInput()\"\n (ngModelChange)=\"onInputChange($event)\"\n (keydown.enter)=\"onSubmit()\"\n (keydown.tab)=\"onTab($any($event))\"\n (keydown.escape)=\"dismissHelp()\"\n placeholder=\"Type a command...\"\n [disabled]=\"isProcessing()\"\n autocomplete=\"off\"\n spellcheck=\"false\"\n />\n </div>\n </div>\n <div class=\"terminal-output\">\n @for (entry of history(); track entry.result.timestamp) {\n @if (entry.input) {\n <div class=\"terminal-line\">\n <span class=\"prompt\">$</span>\n <span class=\"command-text\">{{ entry.input }}@if ((entry.repeatCount ?? 1) > 1) { <span class=\"repeat-count\">(\u00D7{{ entry.repeatCount }})</span>}</span>\n </div>\n }\n @if (entry.result.message) {\n <div class=\"terminal-line\" [class]=\"getSeverityClass(entry.result)\">{{ entry.result.message }}</div>\n }\n }\n </div>\n</div>\n", styles: [".terminal{display:flex;flex-direction:column;height:100%;background-color:var(--color-terminal-bg);border-top:1px solid var(--color-surface-border);font-family:Roboto Mono,Courier New,monospace;font-size:.85rem}.terminal-output{flex:1;overflow-y:auto;padding:8px 12px;display:flex;flex-direction:column}.terminal-line{line-height:1.6;color:var(--color-text-primary);white-space:pre-wrap}.terminal-line.error{color:var(--color-loss)}.terminal-line.warning{color:var(--color-warning)}.terminal-line.info{color:var(--color-info)}.prompt{color:var(--color-terminal-prompt);margin-right:8px;font-weight:700}.command-text{color:var(--color-terminal-text)}.repeat-count{color:var(--color-secondary-info);opacity:.6;margin-left:4px}.terminal-input-row{display:flex;align-items:center;padding:8px 12px;border-bottom:1px solid var(--color-surface-border)}.input-wrapper{flex:1;display:grid}.ghost-input,.terminal-input{grid-area:1 / 1;font-family:inherit;font-size:inherit;line-height:normal;margin:0;padding:0;border:none;background:transparent;outline:none;-webkit-appearance:none;appearance:none;width:100%}.ghost-input{color:var(--color-secondary-info);opacity:.3;pointer-events:none}.terminal-input{color:var(--color-terminal-text);caret-color:var(--color-terminal-prompt)}.terminal-input::placeholder{color:var(--color-secondary-info);opacity:.5}\n"] }]
3764
+ args: [{ selector: 'shell-command-terminal', imports: [FormsModule], changeDetection: ChangeDetectionStrategy.Eager, template: "<div class=\"terminal\">\n <div class=\"terminal-input-row\">\n <span class=\"prompt\">$</span>\n <div class=\"input-wrapper\">\n @if (ghostText() && !pending()) {\n <input\n class=\"ghost-input\"\n type=\"text\"\n [value]=\"ghostText()\"\n disabled\n tabindex=\"-1\"\n aria-hidden=\"true\"\n />\n }\n <input\n #terminalInput\n class=\"terminal-input\"\n [type]=\"pending()?.masked ? 'password' : 'text'\"\n [class.answering]=\"!!pending()\"\n [ngModel]=\"currentInput()\"\n (ngModelChange)=\"onInputChange($event)\"\n (keydown.enter)=\"onSubmit()\"\n (keydown.tab)=\"onTab($any($event))\"\n (keydown.escape)=\"onEscape()\"\n [placeholder]=\"pending()?.question ?? 'Type a command...'\"\n [attr.autocomplete]=\"pending()?.masked ? 'one-time-code' : 'off'\"\n [disabled]=\"isProcessing()\"\n spellcheck=\"false\"\n />\n </div>\n </div>\n <div class=\"terminal-output\">\n @for (entry of history(); track entry.result.timestamp) {\n @if (entry.input) {\n <div class=\"terminal-line\">\n <span class=\"prompt\">$</span>\n <span class=\"command-text\">{{ entry.input }}@if ((entry.repeatCount ?? 1) > 1) { <span class=\"repeat-count\">(\u00D7{{ entry.repeatCount }})</span>}</span>\n </div>\n }\n @if (entry.result.message) {\n <div class=\"terminal-line\" [class]=\"getSeverityClass(entry.result)\">{{ entry.result.message }}</div>\n }\n }\n </div>\n</div>\n", styles: [".terminal{display:flex;flex-direction:column;height:100%;background-color:var(--color-terminal-bg);border-top:1px solid var(--color-surface-border);font-family:Roboto Mono,Courier New,monospace;font-size:.85rem}.terminal-output{flex:1;overflow-y:auto;padding:8px 12px;display:flex;flex-direction:column}.terminal-line{line-height:1.6;color:var(--color-text-primary);white-space:pre-wrap}.terminal-line.error{color:var(--color-loss)}.terminal-line.warning{color:var(--color-warning)}.terminal-line.info{color:var(--color-info)}.prompt{color:var(--color-terminal-prompt);margin-right:8px;font-weight:700}.command-text{color:var(--color-terminal-text)}.repeat-count{color:var(--color-secondary-info);opacity:.6;margin-left:4px}.terminal-input-row{display:flex;align-items:center;padding:8px 12px;border-bottom:1px solid var(--color-surface-border)}.input-wrapper{flex:1;display:grid}.ghost-input,.terminal-input{grid-area:1 / 1;font-family:inherit;font-size:inherit;line-height:normal;margin:0;padding:0;border:none;background:transparent;outline:none;-webkit-appearance:none;appearance:none;width:100%}.ghost-input{color:var(--color-secondary-info);opacity:.3;pointer-events:none}.terminal-input{color:var(--color-terminal-text);caret-color:var(--color-terminal-prompt)}.terminal-input::placeholder{color:var(--color-secondary-info);opacity:.5}.terminal-input.answering::placeholder{color:var(--color-neutral);opacity:1}\n"] }]
3599
3765
  }], ctorParameters: () => [], propDecorators: { focusOnAppear: [{ type: i0.Input, args: [{ isSignal: true, alias: "focusOnAppear", required: false }] }], inputEl: [{ type: i0.ViewChild, args: ['terminalInput', { isSignal: true }] }] } });
3600
3766
 
3601
3767
  const MIN_TERMINAL_HEIGHT = 80;
@@ -4324,5 +4490,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.2", ngImpor
4324
4490
  * Generated bundle index. Do not edit.
4325
4491
  */
4326
4492
 
4327
- export { AuthenticationService, AuthenticatorError, AuthenticatorSettings, CLEAR_USAGE, CODE_STEP, CodeDialog, CommandRegistryService, CommandService, CommandTerminal, ConfirmDialog, FAQ_CONTENT_LOADER, FAQ_DEV_EDITOR, FAQ_DEV_MODE, FAQ_DEV_WORKSPACE_ROOT, FAQ_PUBLIC_PUBLISHER, FAQ_PUBLIC_SITE_BASE_URL, FaqAuthoringPanel, FaqHistoryService, FaqMarkdownRendererService, FaqModal, FaqService, HELP_USAGE, HttpFaqContentLoader, KeyboardShortcutService, Login, MainContent, PhoneLayoutService, PublicFaqIndex, PublicFaqPage, PublicFaqToc, RecoveryCodes, RegexValidatorDirective, SESSION_TOKEN_KEY, SHELL_APP_NAME, SHELL_AUTH_URLS, SHELL_COMMANDS, SHELL_MENU_ITEMS, SHELL_PHONE_MEDIA_QUERY, SHELL_POST_LOGIN_ROUTE, SHELL_SHARED_SESSION, SessionTokenStore, Shell, SideMenu, StatusBar, TOO_MANY_ATTEMPTS, TerminalFocusService, TerminalVisibilityService, ThemeService, ThinkingService, audienceAllowed, authGuard, authInterceptor, authenticatorMessage, buildFallbackChain, buildLoginBody, decideRenewal, guestGuard, matchesRoute, openCodeDialog, provideShellConfig, recoveryCodesFile, tokenize };
4493
+ export { AuthenticationService, AuthenticatorError, AuthenticatorSettings, CLEAR_USAGE, CODE_STEP, CodeDialog, CommandRegistryService, CommandService, CommandTerminal, ConfirmDialog, FAQ_CONTENT_LOADER, FAQ_DEV_EDITOR, FAQ_DEV_MODE, FAQ_DEV_WORKSPACE_ROOT, FAQ_PUBLIC_PUBLISHER, FAQ_PUBLIC_SITE_BASE_URL, FaqAuthoringPanel, FaqHistoryService, FaqMarkdownRendererService, FaqModal, FaqService, HELP_USAGE, HttpFaqContentLoader, KeyboardShortcutService, Login, MainContent, PendingPromptState, PhoneLayoutService, PublicFaqIndex, PublicFaqPage, PublicFaqToc, RecoveryCodes, RegexValidatorDirective, SESSION_TOKEN_KEY, SHELL_APP_NAME, SHELL_AUTH_URLS, SHELL_COMMANDS, SHELL_MENU_ITEMS, SHELL_PHONE_MEDIA_QUERY, SHELL_POST_LOGIN_ROUTE, SHELL_SHARED_SESSION, SessionTokenStore, Shell, SideMenu, StatusBar, TOO_MANY_ATTEMPTS, TerminalFocusService, TerminalVisibilityService, ThemeService, ThinkingService, answerEcho, audienceAllowed, authGuard, authInterceptor, authenticatorMessage, buildFallbackChain, buildLoginBody, decideRenewal, guestGuard, isYes, matchesRoute, openCodeDialog, provideShellConfig, recoveryCodesFile, tokenize };
4328
4494
  //# sourceMappingURL=franzzemen-brokenstock-shell.mjs.map