@franzzemen/brokenstock-shell 0.4.6 → 0.5.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.
|
@@ -2224,38 +2224,92 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.2", ngImpor
|
|
|
2224
2224
|
args: [{ providedIn: 'root' }]
|
|
2225
2225
|
}] });
|
|
2226
2226
|
|
|
2227
|
-
|
|
2227
|
+
/**
|
|
2228
|
+
* The choice a NEW window starts with. Shared by the whole browser.
|
|
2229
|
+
* Written on every toggle, read only at construction.
|
|
2230
|
+
*/
|
|
2231
|
+
const DEFAULT_KEY = 'shell.commandTerminalVisible';
|
|
2232
|
+
/**
|
|
2233
|
+
* THIS window's current choice. Per-window by definition, so two windows cannot
|
|
2234
|
+
* fight over it.
|
|
2235
|
+
*/
|
|
2236
|
+
const WINDOW_KEY = 'shell.commandTerminalVisible.window';
|
|
2228
2237
|
/**
|
|
2229
2238
|
* Controls visibility of the command terminal at the bottom of the shell.
|
|
2230
2239
|
*
|
|
2231
|
-
*
|
|
2232
|
-
*
|
|
2233
|
-
*
|
|
2240
|
+
* ⚠ TWO KEYS, AND THE SPLIT IS THE POINT (multiple-windows F6).
|
|
2241
|
+
*
|
|
2242
|
+
* This used to be one `localStorage` value, read once at construction and
|
|
2243
|
+
* written on every change, with nothing listening for another window. That was
|
|
2244
|
+
* correct while the app could only be open once. It is not now: two windows
|
|
2245
|
+
* would each write the shared value on every toggle, so whichever you touched
|
|
2246
|
+
* last decided what BOTH windows got on their next start — a setting that
|
|
2247
|
+
* quietly refuses to stay put, which reads as a bug in the shell rather than in
|
|
2248
|
+
* storage.
|
|
2249
|
+
*
|
|
2250
|
+
* The resolution is not "make them agree". You genuinely may want the terminal
|
|
2251
|
+
* up on the scanner window and out of the way on the other, so forcing them into
|
|
2252
|
+
* step would be the wrong fix as well as a harder one. Instead:
|
|
2253
|
+
*
|
|
2254
|
+
* - the LIVE state is per-window (`sessionStorage`), so the two never collide
|
|
2255
|
+
* and a reload still restores what this window had;
|
|
2256
|
+
* - the DEFAULT for a brand-new window is shared (`localStorage`), so your
|
|
2257
|
+
* last choice anywhere still carries into the next window and across a
|
|
2258
|
+
* browser restart, which is the behaviour the single key was providing.
|
|
2259
|
+
*
|
|
2260
|
+
* Both writes are best-effort: losing a remembered layout must never break the
|
|
2261
|
+
* screen trying to remember it.
|
|
2262
|
+
*
|
|
2263
|
+
* ⚠ `visible` is READ-ONLY to the outside and `toggle()` is the only mutator.
|
|
2264
|
+
* It used to be a writable signal persisted by an `effect`, which meant any
|
|
2265
|
+
* caller could `set()` it — and whether that got remembered depended on an
|
|
2266
|
+
* effect firing somewhere else. Persisting inside the one mutator makes it
|
|
2267
|
+
* impossible to change this without persisting it.
|
|
2234
2268
|
*/
|
|
2235
2269
|
class TerminalVisibilityService {
|
|
2236
2270
|
constructor() {
|
|
2237
|
-
this
|
|
2238
|
-
...(ngDevMode ? [{ debugName: "visible" }] : /* istanbul ignore next */ []));
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
try {
|
|
2242
|
-
localStorage.setItem(STORAGE_KEY, isVisible ? '1' : '0');
|
|
2243
|
-
}
|
|
2244
|
-
catch {
|
|
2245
|
-
// localStorage unavailable (private mode / SSR) — non-fatal.
|
|
2246
|
-
}
|
|
2247
|
-
});
|
|
2271
|
+
this.#visible = signal(this.#readInitial(), /* @ts-ignore */
|
|
2272
|
+
...(ngDevMode ? [{ debugName: "#visible" }] : /* istanbul ignore next */ []));
|
|
2273
|
+
/** Read-only on purpose — see the header. */
|
|
2274
|
+
this.visible = this.#visible.asReadonly();
|
|
2248
2275
|
}
|
|
2276
|
+
#visible;
|
|
2249
2277
|
toggle() {
|
|
2250
|
-
this
|
|
2278
|
+
const next = !this.#visible();
|
|
2279
|
+
this.#visible.set(next);
|
|
2280
|
+
this.#write(sessionStorage, WINDOW_KEY, next);
|
|
2281
|
+
this.#write(localStorage, DEFAULT_KEY, next);
|
|
2251
2282
|
}
|
|
2283
|
+
/**
|
|
2284
|
+
* This window's own choice wins; otherwise the last choice made anywhere;
|
|
2285
|
+
* otherwise visible.
|
|
2286
|
+
*
|
|
2287
|
+
* The order matters on a reload: a window that had hidden the terminal must
|
|
2288
|
+
* not have it reappear because another window showed one since.
|
|
2289
|
+
*/
|
|
2252
2290
|
#readInitial() {
|
|
2291
|
+
const own = this.#read(sessionStorage, WINDOW_KEY);
|
|
2292
|
+
if (own !== undefined)
|
|
2293
|
+
return own;
|
|
2294
|
+
const shared = this.#read(localStorage, DEFAULT_KEY);
|
|
2295
|
+
return shared ?? true;
|
|
2296
|
+
}
|
|
2297
|
+
#read(store, key) {
|
|
2253
2298
|
try {
|
|
2254
|
-
const raw =
|
|
2255
|
-
return raw === null ?
|
|
2299
|
+
const raw = store.getItem(key);
|
|
2300
|
+
return raw === null ? undefined : raw === '1';
|
|
2256
2301
|
}
|
|
2257
2302
|
catch {
|
|
2258
|
-
|
|
2303
|
+
// Storage unavailable (private mode / prerender) — fall through.
|
|
2304
|
+
return undefined;
|
|
2305
|
+
}
|
|
2306
|
+
}
|
|
2307
|
+
#write(store, key, value) {
|
|
2308
|
+
try {
|
|
2309
|
+
store.setItem(key, value ? '1' : '0');
|
|
2310
|
+
}
|
|
2311
|
+
catch {
|
|
2312
|
+
// Non-fatal by policy: a lost layout preference must not break the screen.
|
|
2259
2313
|
}
|
|
2260
2314
|
}
|
|
2261
2315
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.2", ngImport: i0, type: TerminalVisibilityService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
@@ -2264,7 +2318,7 @@ class TerminalVisibilityService {
|
|
|
2264
2318
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.2", ngImport: i0, type: TerminalVisibilityService, decorators: [{
|
|
2265
2319
|
type: Injectable,
|
|
2266
2320
|
args: [{ providedIn: 'root' }]
|
|
2267
|
-
}]
|
|
2321
|
+
}] });
|
|
2268
2322
|
|
|
2269
2323
|
/**
|
|
2270
2324
|
* Elements that mean "the user is typing", where an unmodified shortcut must not fire.
|
|
@@ -2504,9 +2558,16 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.2", ngImpor
|
|
|
2504
2558
|
args: [{ providedIn: 'root' }]
|
|
2505
2559
|
}] });
|
|
2506
2560
|
|
|
2561
|
+
/**
|
|
2562
|
+
* Names the shell handles itself, ahead of the registry. A command taking one
|
|
2563
|
+
* of these would be registered and then never reachable, so registration
|
|
2564
|
+
* refuses them outright rather than leaving a command that silently never runs.
|
|
2565
|
+
*/
|
|
2566
|
+
const RESERVED_NAMES = ['clear', 'help'];
|
|
2507
2567
|
class CommandRegistryService {
|
|
2508
2568
|
constructor() {
|
|
2509
2569
|
this.commands = new Map();
|
|
2570
|
+
this.aliases = new Map();
|
|
2510
2571
|
// Built-in shell commands registered ahead of app-provided commands so
|
|
2511
2572
|
// an app cannot accidentally shadow them.
|
|
2512
2573
|
this.register(inject(FaqCommand));
|
|
@@ -2515,8 +2576,47 @@ class CommandRegistryService {
|
|
|
2515
2576
|
this.register(cmd);
|
|
2516
2577
|
}
|
|
2517
2578
|
}
|
|
2579
|
+
/**
|
|
2580
|
+
* Register a command and claim its names.
|
|
2581
|
+
*
|
|
2582
|
+
* ⭐ THROWS on a name already taken — real name or alternate. It used to be a
|
|
2583
|
+
* bare `set`, which meant a clash silently overwrote whichever command
|
|
2584
|
+
* registered first, and the constructor's "built-ins first so an app cannot
|
|
2585
|
+
* shadow them" comment was not actually true of the map. A shadowed command
|
|
2586
|
+
* is indistinguishable from a broken one, and this is the map dispatch reads,
|
|
2587
|
+
* so the failure belongs at startup where it cannot be missed
|
|
2588
|
+
* (scanner-command-speed.prd.md D4).
|
|
2589
|
+
*/
|
|
2518
2590
|
register(command) {
|
|
2591
|
+
this.#claim(command.name, `command '${command.name}'`);
|
|
2519
2592
|
this.commands.set(command.name, command);
|
|
2593
|
+
for (const alias of command.aliases ?? []) {
|
|
2594
|
+
this.#claim(alias.name, `alias '${alias.name}' of '${command.name}'`);
|
|
2595
|
+
this.aliases.set(alias.name, { command, prependArgs: alias.prependArgs ?? [] });
|
|
2596
|
+
}
|
|
2597
|
+
}
|
|
2598
|
+
#claim(name, what) {
|
|
2599
|
+
if (RESERVED_NAMES.includes(name)) {
|
|
2600
|
+
throw new Error(`${what} collides with a reserved command name`);
|
|
2601
|
+
}
|
|
2602
|
+
if (this.commands.has(name) || this.aliases.has(name)) {
|
|
2603
|
+
throw new Error(`${what} collides with a command name already registered`);
|
|
2604
|
+
}
|
|
2605
|
+
}
|
|
2606
|
+
/**
|
|
2607
|
+
* Resolve a word the user typed to the command it names — real name or
|
|
2608
|
+
* alternate. `prependArgs` is empty except for an alternate that carries a
|
|
2609
|
+
* subcommand.
|
|
2610
|
+
*
|
|
2611
|
+
* Use this wherever a TYPED word is being looked up. {@link get} deliberately
|
|
2612
|
+
* stays blind to alternates so callers that mean "the command called X" keep
|
|
2613
|
+
* meaning exactly that.
|
|
2614
|
+
*/
|
|
2615
|
+
resolve(name) {
|
|
2616
|
+
const command = this.commands.get(name);
|
|
2617
|
+
if (command)
|
|
2618
|
+
return { command, prependArgs: [] };
|
|
2619
|
+
return this.aliases.get(name);
|
|
2520
2620
|
}
|
|
2521
2621
|
get(name) {
|
|
2522
2622
|
return this.commands.get(name);
|
|
@@ -2532,11 +2632,15 @@ class CommandRegistryService {
|
|
|
2532
2632
|
if (!prefix)
|
|
2533
2633
|
return [];
|
|
2534
2634
|
if (!isAuthenticated) {
|
|
2535
|
-
const open = ['login', ...[...this.commands.values()]
|
|
2635
|
+
const open = ['login', ...[...this.commands.values()]
|
|
2636
|
+
.filter(c => c.requiresAuth === false)
|
|
2637
|
+
.flatMap(c => [c.name, ...(c.aliases ?? []).map(a => a.name)])];
|
|
2536
2638
|
return open.filter(name => name.startsWith(prefix) && name !== prefix).sort();
|
|
2537
2639
|
}
|
|
2538
2640
|
const builtIns = ['clear', 'help'];
|
|
2539
|
-
|
|
2641
|
+
// Alternate names are offered too: typing `sca` and being offered nothing
|
|
2642
|
+
// is the exact failure that produced the ref/reference pair (D5).
|
|
2643
|
+
const registered = [...this.commands.keys(), ...this.aliases.keys()];
|
|
2540
2644
|
return [...builtIns, ...registered]
|
|
2541
2645
|
.filter(name => name.startsWith(prefix) && name !== prefix)
|
|
2542
2646
|
.sort();
|
|
@@ -2624,7 +2728,10 @@ class CommandService {
|
|
|
2624
2728
|
const input = validation.data;
|
|
2625
2729
|
const parts = tokenize(input);
|
|
2626
2730
|
const command = parts[0].toLowerCase();
|
|
2627
|
-
|
|
2731
|
+
// Resolved rather than looked up by name: a command typed under an
|
|
2732
|
+
// alternate name must still get its own input sanitiser (a password line
|
|
2733
|
+
// masked under `login` stays masked under any other word for it).
|
|
2734
|
+
const def = this.registry.resolve(command)?.command;
|
|
2628
2735
|
const displayInput = def?.sanitizeInput ? def.sanitizeInput(input) : input;
|
|
2629
2736
|
const resultOrPromise = this.#dispatch(command, parts.slice(1));
|
|
2630
2737
|
this.currentInput.set('');
|
|
@@ -2716,13 +2823,30 @@ class CommandService {
|
|
|
2716
2823
|
isHelp: true,
|
|
2717
2824
|
};
|
|
2718
2825
|
}
|
|
2719
|
-
|
|
2826
|
+
/*
|
|
2827
|
+
* Resolution, not a name lookup — `scan` and `scans` reach the scanner the
|
|
2828
|
+
* same way `scanner` does. An alternate may also carry leading words
|
|
2829
|
+
* (`scans NVDA` IS `scanner select NVDA`), which are inserted ahead of what
|
|
2830
|
+
* the user typed.
|
|
2831
|
+
*
|
|
2832
|
+
* ⭐ `command` keeps the word the USER typed. Every message below
|
|
2833
|
+
* interpolates it, and telling someone who typed `scan` that the arguments
|
|
2834
|
+
* of 'scanner' are wrong reads as though something else ran. The two-word
|
|
2835
|
+
* swap is the one deliberate exception, because there the typed first word
|
|
2836
|
+
* really was an argument.
|
|
2837
|
+
*/
|
|
2838
|
+
let resolved = this.registry.resolve(command);
|
|
2839
|
+
if (resolved) {
|
|
2840
|
+
args = [...resolved.prependArgs, ...args];
|
|
2841
|
+
}
|
|
2842
|
+
let def = resolved?.command;
|
|
2720
2843
|
if (!def) {
|
|
2721
2844
|
// Try swapping the first two tokens: "disable user foo" → "user disable foo"
|
|
2722
2845
|
const swapped = args[0]?.toLowerCase();
|
|
2723
|
-
|
|
2724
|
-
|
|
2725
|
-
|
|
2846
|
+
const swappedResolved = swapped ? this.registry.resolve(swapped) : undefined;
|
|
2847
|
+
if (swappedResolved) {
|
|
2848
|
+
def = swappedResolved.command;
|
|
2849
|
+
args = [...swappedResolved.prependArgs, command, ...args.slice(1)];
|
|
2726
2850
|
command = swapped;
|
|
2727
2851
|
}
|
|
2728
2852
|
else {
|
|
@@ -2760,7 +2884,7 @@ class CommandService {
|
|
|
2760
2884
|
return { success: true, message: CLEAR_USAGE, timestamp: new Date(), isHelp: true };
|
|
2761
2885
|
if (name === 'help')
|
|
2762
2886
|
return { success: true, message: HELP_USAGE, timestamp: new Date(), isHelp: true };
|
|
2763
|
-
const def = this.registry.
|
|
2887
|
+
const def = this.registry.resolve(name)?.command;
|
|
2764
2888
|
if (!def)
|
|
2765
2889
|
return { success: false, message: `Unknown command: ${name}`, timestamp: new Date() };
|
|
2766
2890
|
// Usage is still shown for a gated command — knowing how it works is how a
|
|
@@ -2916,11 +3040,11 @@ class MainContent {
|
|
|
2916
3040
|
this.#shortcutService.helpVisible.set(false);
|
|
2917
3041
|
}
|
|
2918
3042
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.2", ngImport: i0, type: MainContent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
2919
|
-
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.2", type: MainContent, isStandalone: true, selector: "shell-main-content", ngImport: i0, template: "<div class=\"main-content\">\n <div class=\"content-area\">\n <router-outlet />\n </div>\n @if (shortcutHelpVisible() && activeShortcuts()) {\n <div class=\"shortcut-overlay\" (click)=\"closeShortcutHelp()\">\n <div class=\"shortcut-panel\" (click)=\"$event.stopPropagation()\">\n <div class=\"shortcut-title text-secondary-info\">{{ activeShortcuts()!.groupId }} shortcuts</div>\n @for (s of activeShortcuts()!.shortcuts; track s.key) {\n <div class=\"shortcut-row\">\n <kbd class=\"shortcut-key\">Alt+{{ formatKey(s.key) }}</kbd>\n <span class=\"shortcut-desc text-text-primary\">{{ s.description }}</span>\n </div>\n }\n <div class=\"shortcut-row shortcut-dismiss\">\n <kbd class=\"shortcut-key\">Alt+?</kbd>\n <span class=\"shortcut-desc text-secondary-info\">or</span>\n <kbd class=\"shortcut-key\">Esc</kbd>\n <span class=\"shortcut-desc text-secondary-info\">dismiss</span>\n </div>\n </div>\n </div>\n }\n @if (isThinking()) {\n <div class=\"thinking-overlay\">\n <mat-spinner diameter=\"48\" />\n </div>\n }\n</div>\n", styles: [".main-content{position:relative;height:100%;overflow:hidden}.content-area{height:100%;overflow-y:auto;padding:16px}.shortcut-overlay{position:absolute;inset:0;z-index:15;display:flex;align-items:center;justify-content:center;background-color:#0a0e1480}.shortcut-panel{background-color:var(--color-terminal-bg);border:1px solid var(--color-surface-border);border-radius:6px;padding:14px 18px;font-size:.8rem;box-shadow:0 4px 16px #00000080;min-width:220px}.shortcut-title{font-size:.7rem;text-transform:uppercase;letter-spacing:.05em;margin-bottom:8px;font-weight:500}.shortcut-row{display:flex;align-items:center;gap:10px;padding:3px 0}.shortcut-row.shortcut-dismiss{margin-top:6px;border-top:1px solid var(--color-surface-border);padding-top:8px}.shortcut-key{font-family:Roboto Mono,monospace;font-size:.75rem;background-color:#ffffff14;border:1px solid var(--color-surface-border);border-radius:3px;padding:1px 6px;min-width:70px;text-align:center}.shortcut-desc{font-size:.78rem}.thinking-overlay{position:absolute;inset:0;z-index:20;background-color:#0a0e14b3;display:flex;align-items:center;justify-content:center;pointer-events:all;cursor:wait}\n"], dependencies: [{ kind: "directive", type: RouterOutlet, selector: "router-outlet", inputs: ["name", "routerOutletData"], outputs: ["activate", "deactivate", "attach", "detach"], exportAs: ["outlet"] }, { kind: "ngmodule", type: MatProgressSpinnerModule }, { kind: "component", type: i1$1.MatProgressSpinner, selector: "mat-progress-spinner, mat-spinner", inputs: ["color", "mode", "value", "diameter", "strokeWidth"], exportAs: ["matProgressSpinner"] }], changeDetection: i0.ChangeDetectionStrategy.Eager }); }
|
|
3043
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.2", type: MainContent, isStandalone: true, selector: "shell-main-content", ngImport: i0, template: "<div class=\"main-content\">\n <div class=\"content-area\">\n <router-outlet />\n </div>\n @if (shortcutHelpVisible() && activeShortcuts()) {\n <div class=\"shortcut-overlay\" (click)=\"closeShortcutHelp()\">\n <div class=\"shortcut-panel\" (click)=\"$event.stopPropagation()\">\n <div class=\"shortcut-title text-secondary-info\">{{ activeShortcuts()!.groupId }} shortcuts</div>\n @for (s of activeShortcuts()!.shortcuts; track s.key) {\n <div class=\"shortcut-row\">\n <kbd class=\"shortcut-key\">Alt+{{ formatKey(s.key) }}</kbd>\n <span class=\"shortcut-desc text-text-primary\">{{ s.description }}</span>\n </div>\n }\n <!--\n Esc is documented rather than added (scanner-command-speed.prd.md D24/E4).\n It has always put the cursor in the command box, from anywhere and even\n while typing in a field \u2014 it was simply never written down, which is why\n it read as a missing feature. Worded for what it actually does: it also\n closes this panel, and it does not check whether a dialog is open.\n -->\n <div class=\"shortcut-row\">\n <kbd class=\"shortcut-key\">Esc</kbd>\n <span class=\"shortcut-desc text-text-primary\">Put the cursor in the command box, from anywhere</span>\n </div>\n <div class=\"shortcut-row shortcut-dismiss\">\n <kbd class=\"shortcut-key\">Alt+?</kbd>\n <span class=\"shortcut-desc text-secondary-info\">or</span>\n <kbd class=\"shortcut-key\">Esc</kbd>\n <span class=\"shortcut-desc text-secondary-info\">dismiss</span>\n </div>\n </div>\n </div>\n }\n @if (isThinking()) {\n <div class=\"thinking-overlay\">\n <mat-spinner diameter=\"48\" />\n </div>\n }\n</div>\n", styles: [".main-content{position:relative;height:100%;overflow:hidden}.content-area{height:100%;overflow-y:auto;padding:16px}.shortcut-overlay{position:absolute;inset:0;z-index:15;display:flex;align-items:center;justify-content:center;background-color:#0a0e1480}.shortcut-panel{background-color:var(--color-terminal-bg);border:1px solid var(--color-surface-border);border-radius:6px;padding:14px 18px;font-size:.8rem;box-shadow:0 4px 16px #00000080;min-width:220px}.shortcut-title{font-size:.7rem;text-transform:uppercase;letter-spacing:.05em;margin-bottom:8px;font-weight:500}.shortcut-row{display:flex;align-items:center;gap:10px;padding:3px 0}.shortcut-row.shortcut-dismiss{margin-top:6px;border-top:1px solid var(--color-surface-border);padding-top:8px}.shortcut-key{font-family:Roboto Mono,monospace;font-size:.75rem;background-color:#ffffff14;border:1px solid var(--color-surface-border);border-radius:3px;padding:1px 6px;min-width:70px;text-align:center}.shortcut-desc{font-size:.78rem}.thinking-overlay{position:absolute;inset:0;z-index:20;background-color:#0a0e14b3;display:flex;align-items:center;justify-content:center;pointer-events:all;cursor:wait}\n"], dependencies: [{ kind: "directive", type: RouterOutlet, selector: "router-outlet", inputs: ["name", "routerOutletData"], outputs: ["activate", "deactivate", "attach", "detach"], exportAs: ["outlet"] }, { kind: "ngmodule", type: MatProgressSpinnerModule }, { kind: "component", type: i1$1.MatProgressSpinner, selector: "mat-progress-spinner, mat-spinner", inputs: ["color", "mode", "value", "diameter", "strokeWidth"], exportAs: ["matProgressSpinner"] }], changeDetection: i0.ChangeDetectionStrategy.Eager }); }
|
|
2920
3044
|
}
|
|
2921
3045
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.2", ngImport: i0, type: MainContent, decorators: [{
|
|
2922
3046
|
type: Component,
|
|
2923
|
-
args: [{ selector: 'shell-main-content', imports: [RouterOutlet, MatProgressSpinnerModule], changeDetection: ChangeDetectionStrategy.Eager, template: "<div class=\"main-content\">\n <div class=\"content-area\">\n <router-outlet />\n </div>\n @if (shortcutHelpVisible() && activeShortcuts()) {\n <div class=\"shortcut-overlay\" (click)=\"closeShortcutHelp()\">\n <div class=\"shortcut-panel\" (click)=\"$event.stopPropagation()\">\n <div class=\"shortcut-title text-secondary-info\">{{ activeShortcuts()!.groupId }} shortcuts</div>\n @for (s of activeShortcuts()!.shortcuts; track s.key) {\n <div class=\"shortcut-row\">\n <kbd class=\"shortcut-key\">Alt+{{ formatKey(s.key) }}</kbd>\n <span class=\"shortcut-desc text-text-primary\">{{ s.description }}</span>\n </div>\n }\n <div class=\"shortcut-row shortcut-dismiss\">\n <kbd class=\"shortcut-key\">Alt+?</kbd>\n <span class=\"shortcut-desc text-secondary-info\">or</span>\n <kbd class=\"shortcut-key\">Esc</kbd>\n <span class=\"shortcut-desc text-secondary-info\">dismiss</span>\n </div>\n </div>\n </div>\n }\n @if (isThinking()) {\n <div class=\"thinking-overlay\">\n <mat-spinner diameter=\"48\" />\n </div>\n }\n</div>\n", styles: [".main-content{position:relative;height:100%;overflow:hidden}.content-area{height:100%;overflow-y:auto;padding:16px}.shortcut-overlay{position:absolute;inset:0;z-index:15;display:flex;align-items:center;justify-content:center;background-color:#0a0e1480}.shortcut-panel{background-color:var(--color-terminal-bg);border:1px solid var(--color-surface-border);border-radius:6px;padding:14px 18px;font-size:.8rem;box-shadow:0 4px 16px #00000080;min-width:220px}.shortcut-title{font-size:.7rem;text-transform:uppercase;letter-spacing:.05em;margin-bottom:8px;font-weight:500}.shortcut-row{display:flex;align-items:center;gap:10px;padding:3px 0}.shortcut-row.shortcut-dismiss{margin-top:6px;border-top:1px solid var(--color-surface-border);padding-top:8px}.shortcut-key{font-family:Roboto Mono,monospace;font-size:.75rem;background-color:#ffffff14;border:1px solid var(--color-surface-border);border-radius:3px;padding:1px 6px;min-width:70px;text-align:center}.shortcut-desc{font-size:.78rem}.thinking-overlay{position:absolute;inset:0;z-index:20;background-color:#0a0e14b3;display:flex;align-items:center;justify-content:center;pointer-events:all;cursor:wait}\n"] }]
|
|
3047
|
+
args: [{ selector: 'shell-main-content', imports: [RouterOutlet, MatProgressSpinnerModule], changeDetection: ChangeDetectionStrategy.Eager, template: "<div class=\"main-content\">\n <div class=\"content-area\">\n <router-outlet />\n </div>\n @if (shortcutHelpVisible() && activeShortcuts()) {\n <div class=\"shortcut-overlay\" (click)=\"closeShortcutHelp()\">\n <div class=\"shortcut-panel\" (click)=\"$event.stopPropagation()\">\n <div class=\"shortcut-title text-secondary-info\">{{ activeShortcuts()!.groupId }} shortcuts</div>\n @for (s of activeShortcuts()!.shortcuts; track s.key) {\n <div class=\"shortcut-row\">\n <kbd class=\"shortcut-key\">Alt+{{ formatKey(s.key) }}</kbd>\n <span class=\"shortcut-desc text-text-primary\">{{ s.description }}</span>\n </div>\n }\n <!--\n Esc is documented rather than added (scanner-command-speed.prd.md D24/E4).\n It has always put the cursor in the command box, from anywhere and even\n while typing in a field \u2014 it was simply never written down, which is why\n it read as a missing feature. Worded for what it actually does: it also\n closes this panel, and it does not check whether a dialog is open.\n -->\n <div class=\"shortcut-row\">\n <kbd class=\"shortcut-key\">Esc</kbd>\n <span class=\"shortcut-desc text-text-primary\">Put the cursor in the command box, from anywhere</span>\n </div>\n <div class=\"shortcut-row shortcut-dismiss\">\n <kbd class=\"shortcut-key\">Alt+?</kbd>\n <span class=\"shortcut-desc text-secondary-info\">or</span>\n <kbd class=\"shortcut-key\">Esc</kbd>\n <span class=\"shortcut-desc text-secondary-info\">dismiss</span>\n </div>\n </div>\n </div>\n }\n @if (isThinking()) {\n <div class=\"thinking-overlay\">\n <mat-spinner diameter=\"48\" />\n </div>\n }\n</div>\n", styles: [".main-content{position:relative;height:100%;overflow:hidden}.content-area{height:100%;overflow-y:auto;padding:16px}.shortcut-overlay{position:absolute;inset:0;z-index:15;display:flex;align-items:center;justify-content:center;background-color:#0a0e1480}.shortcut-panel{background-color:var(--color-terminal-bg);border:1px solid var(--color-surface-border);border-radius:6px;padding:14px 18px;font-size:.8rem;box-shadow:0 4px 16px #00000080;min-width:220px}.shortcut-title{font-size:.7rem;text-transform:uppercase;letter-spacing:.05em;margin-bottom:8px;font-weight:500}.shortcut-row{display:flex;align-items:center;gap:10px;padding:3px 0}.shortcut-row.shortcut-dismiss{margin-top:6px;border-top:1px solid var(--color-surface-border);padding-top:8px}.shortcut-key{font-family:Roboto Mono,monospace;font-size:.75rem;background-color:#ffffff14;border:1px solid var(--color-surface-border);border-radius:3px;padding:1px 6px;min-width:70px;text-align:center}.shortcut-desc{font-size:.78rem}.thinking-overlay{position:absolute;inset:0;z-index:20;background-color:#0a0e14b3;display:flex;align-items:center;justify-content:center;pointer-events:all;cursor:wait}\n"] }]
|
|
2924
3048
|
}] });
|
|
2925
3049
|
|
|
2926
3050
|
class CommandTerminal {
|