sh3-core 0.15.0 → 0.15.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.
Files changed (141) hide show
  1. package/dist/actions/ctx-actions.svelte.test.js +111 -0
  2. package/dist/actions/dispatcher.svelte.js +23 -2
  3. package/dist/actions/dispatcher.test.js +33 -0
  4. package/dist/actions/listActionsFromEntries.test.js +78 -0
  5. package/dist/actions/listActive.d.ts +2 -1
  6. package/dist/actions/listActive.js +43 -17
  7. package/dist/actions/listeners.d.ts +16 -0
  8. package/dist/actions/listeners.js +68 -14
  9. package/dist/actions/programmatic-dispatch.svelte.test.d.ts +1 -0
  10. package/dist/actions/programmatic-dispatch.svelte.test.js +98 -0
  11. package/dist/actions/types.d.ts +37 -0
  12. package/dist/api.d.ts +1 -1
  13. package/dist/app/store/verbs.js +4 -0
  14. package/dist/app-appearance/appearanceShard.svelte.js +19 -6
  15. package/dist/app-appearance/appearanceState.svelte.js +3 -3
  16. package/dist/host.js +2 -1
  17. package/dist/layouts-shard/LayoutSaveModal.svelte +145 -0
  18. package/dist/layouts-shard/LayoutSaveModal.svelte.d.ts +12 -0
  19. package/dist/layouts-shard/LayoutsSection.svelte +142 -0
  20. package/dist/layouts-shard/LayoutsSection.svelte.d.ts +3 -0
  21. package/dist/layouts-shard/filter.d.ts +3 -0
  22. package/dist/layouts-shard/filter.js +66 -0
  23. package/dist/layouts-shard/filter.test.d.ts +1 -0
  24. package/dist/layouts-shard/filter.test.js +123 -0
  25. package/dist/layouts-shard/index.d.ts +1 -0
  26. package/dist/layouts-shard/index.js +1 -0
  27. package/dist/layouts-shard/layoutsApi.d.ts +12 -0
  28. package/dist/layouts-shard/layoutsApi.js +41 -0
  29. package/dist/layouts-shard/layoutsApi.test.d.ts +1 -0
  30. package/dist/layouts-shard/layoutsApi.test.js +74 -0
  31. package/dist/layouts-shard/layoutsShard.svelte.d.ts +11 -0
  32. package/dist/layouts-shard/layoutsShard.svelte.js +231 -0
  33. package/dist/layouts-shard/layoutsShard.svelte.test.d.ts +1 -0
  34. package/dist/layouts-shard/layoutsShard.svelte.test.js +215 -0
  35. package/dist/layouts-shard/layoutsState.svelte.d.ts +9 -0
  36. package/dist/layouts-shard/layoutsState.svelte.js +50 -0
  37. package/dist/layouts-shard/layoutsState.test.d.ts +1 -0
  38. package/dist/layouts-shard/layoutsState.test.js +43 -0
  39. package/dist/layouts-shard/types.d.ts +21 -0
  40. package/dist/layouts-shard/types.js +6 -0
  41. package/dist/{app-appearance/AppAppearanceModal.svelte → overlays/EntityAppearanceModal.svelte} +36 -31
  42. package/dist/overlays/EntityAppearanceModal.svelte.d.ts +19 -0
  43. package/dist/overlays/EntityAppearanceModal.test.d.ts +1 -0
  44. package/dist/overlays/EntityAppearanceModal.test.js +57 -0
  45. package/dist/overlays/FloatFrame.svelte +149 -8
  46. package/dist/overlays/FloatFrame.svelte.d.ts +1 -1
  47. package/dist/overlays/FloatLayer.svelte +2 -2
  48. package/dist/overlays/float.d.ts +38 -1
  49. package/dist/overlays/float.js +82 -0
  50. package/dist/overlays/float.test.js +394 -0
  51. package/dist/overlays/floatMaximized.svelte.d.ts +4 -0
  52. package/dist/overlays/floatMaximized.svelte.js +30 -0
  53. package/dist/runtime/runVerb-shell.test.d.ts +1 -0
  54. package/dist/runtime/runVerb-shell.test.js +231 -0
  55. package/dist/sh3core-shard/ShellHome.svelte +3 -0
  56. package/dist/sh3core-shard/sh3coreShard.svelte.d.ts +7 -0
  57. package/dist/sh3core-shard/sh3coreShard.svelte.js +23 -0
  58. package/dist/shards/activate-runtime.test.js +24 -2
  59. package/dist/shards/activate.svelte.js +18 -4
  60. package/dist/shards/types.d.ts +44 -4
  61. package/dist/shell-shard/CommandLine.svelte +143 -0
  62. package/dist/shell-shard/CommandLine.svelte.d.ts +26 -0
  63. package/dist/shell-shard/CommandLine.svelte.test.d.ts +1 -0
  64. package/dist/shell-shard/CommandLine.svelte.test.js +43 -0
  65. package/dist/shell-shard/InputLine.svelte +17 -40
  66. package/dist/shell-shard/InputLine.svelte.d.ts +2 -0
  67. package/dist/shell-shard/ScrollbackView.svelte +10 -3
  68. package/dist/shell-shard/ScrollbackView.svelte.d.ts +1 -0
  69. package/dist/shell-shard/Terminal.svelte +94 -22
  70. package/dist/shell-shard/buffer-store.d.ts +15 -0
  71. package/dist/shell-shard/buffer-store.js +124 -0
  72. package/dist/shell-shard/buffer-store.svelte.test.d.ts +1 -0
  73. package/dist/shell-shard/buffer-store.svelte.test.js +107 -0
  74. package/dist/shell-shard/buffer-zone-state.svelte.d.ts +38 -0
  75. package/dist/shell-shard/buffer-zone-state.svelte.js +31 -0
  76. package/dist/shell-shard/contract.d.ts +7 -0
  77. package/dist/shell-shard/dispatch-custom.test.js +3 -1
  78. package/dist/shell-shard/dispatch-gating.test.js +6 -2
  79. package/dist/shell-shard/dispatch-invoke.test.js +10 -8
  80. package/dist/shell-shard/dispatch.d.ts +7 -2
  81. package/dist/shell-shard/dispatch.js +23 -27
  82. package/dist/shell-shard/display-cwd.d.ts +1 -0
  83. package/dist/shell-shard/display-cwd.js +27 -0
  84. package/dist/shell-shard/display-cwd.test.d.ts +1 -0
  85. package/dist/shell-shard/display-cwd.test.js +29 -0
  86. package/dist/shell-shard/entries/StatusEntry.svelte +2 -0
  87. package/dist/shell-shard/manifest.js +2 -1
  88. package/dist/shell-shard/manifest.test.d.ts +1 -0
  89. package/dist/shell-shard/manifest.test.js +8 -0
  90. package/dist/shell-shard/mode-buffer.svelte.d.ts +8 -0
  91. package/dist/shell-shard/mode-buffer.svelte.js +19 -0
  92. package/dist/shell-shard/mode-buffer.svelte.test.d.ts +1 -0
  93. package/dist/shell-shard/mode-buffer.svelte.test.js +25 -0
  94. package/dist/shell-shard/modes/builtin.js +2 -0
  95. package/dist/shell-shard/modes/types.d.ts +8 -0
  96. package/dist/shell-shard/protocol.d.ts +12 -6
  97. package/dist/shell-shard/replay.d.ts +3 -0
  98. package/dist/shell-shard/replay.js +44 -0
  99. package/dist/shell-shard/replay.svelte.test.d.ts +1 -0
  100. package/dist/shell-shard/replay.svelte.test.js +47 -0
  101. package/dist/shell-shard/rich-registry.d.ts +5 -0
  102. package/dist/shell-shard/rich-registry.js +25 -0
  103. package/dist/shell-shard/rich-registry.test.d.ts +1 -0
  104. package/dist/shell-shard/rich-registry.test.js +31 -0
  105. package/dist/shell-shard/scrollback.svelte.d.ts +2 -0
  106. package/dist/shell-shard/scrollback.svelte.js +23 -0
  107. package/dist/shell-shard/scrollback.svelte.test.d.ts +1 -0
  108. package/dist/shell-shard/scrollback.svelte.test.js +51 -0
  109. package/dist/shell-shard/session-client.svelte.d.ts +18 -2
  110. package/dist/shell-shard/session-client.svelte.js +21 -4
  111. package/dist/shell-shard/shellApi.d.ts +2 -1
  112. package/dist/shell-shard/shellApi.js +32 -3
  113. package/dist/shell-shard/shellApi.svelte.test.d.ts +1 -0
  114. package/dist/shell-shard/shellApi.svelte.test.js +59 -0
  115. package/dist/shell-shard/shellShard.svelte.js +11 -1
  116. package/dist/shell-shard/terminal-dispatch.test.js +3 -1
  117. package/dist/shell-shard/verbs/apps.js +9 -0
  118. package/dist/shell-shard/verbs/env.js +4 -0
  119. package/dist/shell-shard/verbs/help.js +9 -1
  120. package/dist/shell-shard/verbs/help.svelte.test.d.ts +1 -0
  121. package/dist/shell-shard/verbs/help.svelte.test.js +53 -0
  122. package/dist/shell-shard/verbs/history.js +8 -1
  123. package/dist/shell-shard/verbs/index.js +0 -8
  124. package/dist/shell-shard/verbs/shards.js +5 -0
  125. package/dist/shell-shard/verbs/views.js +9 -0
  126. package/dist/shell-shard/verbs/zones.js +9 -0
  127. package/dist/verbs/types.d.ts +9 -0
  128. package/dist/version.d.ts +1 -1
  129. package/dist/version.js +1 -1
  130. package/package.json +1 -1
  131. package/dist/app-appearance/AppAppearanceModal.svelte.d.ts +0 -8
  132. package/dist/shell-shard/verbs/cat.d.ts +0 -2
  133. package/dist/shell-shard/verbs/cat.js +0 -34
  134. package/dist/shell-shard/verbs/cd.test.js +0 -56
  135. package/dist/shell-shard/verbs/ls.d.ts +0 -2
  136. package/dist/shell-shard/verbs/ls.js +0 -29
  137. package/dist/shell-shard/verbs/ls.test.js +0 -49
  138. package/dist/shell-shard/verbs/session.d.ts +0 -4
  139. package/dist/shell-shard/verbs/session.js +0 -97
  140. /package/dist/{shell-shard/verbs/cd.test.d.ts → actions/ctx-actions.svelte.test.d.ts} +0 -0
  141. /package/dist/{shell-shard/verbs/ls.test.d.ts → actions/listActionsFromEntries.test.d.ts} +0 -0
@@ -10,6 +10,7 @@
10
10
  import { listRegisteredApps, launchApp, isAdmin, VERSION } from '../api';
11
11
  import ShellTitle from './ShellTitle.svelte';
12
12
  import ProjectsSection from '../projects-shard/ProjectsSection.svelte';
13
+ import LayoutsSection from '../layouts-shard/LayoutsSection.svelte';
13
14
  import { sessionState } from '../projects/session-state.svelte';
14
15
  import { projectsState } from '../projects-shard/projectsShard.svelte';
15
16
  import { shell } from '../shellRuntime.svelte';
@@ -82,6 +83,8 @@
82
83
 
83
84
  <ProjectsSection />
84
85
 
86
+ <LayoutsSection />
87
+
85
88
  {#if userApps.length > 0}
86
89
  <section class="shell-home-section">
87
90
  <h2 class="shell-home-section-title">Apps</h2>
@@ -1,2 +1,9 @@
1
1
  import type { Shard } from '../api';
2
+ import type { Action } from '../actions/types';
3
+ /**
4
+ * Build the palette-only float-maximize toggle action. Targets the topmost
5
+ * float (last entry in `floatManager.list()`); disabled when no floats are
6
+ * open. See spec docs/superpowers/specs/2026-05-07-float-resize-maximize-design.md.
7
+ */
8
+ export declare function buildToggleMaximizeAction(): Action;
2
9
  export declare const sh3coreShard: Shard;
@@ -31,7 +31,29 @@ import { registeredApps } from '../apps/registry.svelte';
31
31
  import { launchApp } from '../apps/lifecycle';
32
32
  import { resetActivePresetToDefault } from '../layout/store.svelte';
33
33
  import { modalManager } from '../overlays/modal';
34
+ import { floatManager } from '../overlays/float';
34
35
  import { registerAppActions } from './appActions';
36
+ /**
37
+ * Build the palette-only float-maximize toggle action. Targets the topmost
38
+ * float (last entry in `floatManager.list()`); disabled when no floats are
39
+ * open. See spec docs/superpowers/specs/2026-05-07-float-resize-maximize-design.md.
40
+ */
41
+ export function buildToggleMaximizeAction() {
42
+ return {
43
+ id: 'sh3.float.toggleMaximize',
44
+ label: 'Toggle Float Maximize',
45
+ scope: ['home', 'app'],
46
+ paletteItem: true,
47
+ contextItem: false,
48
+ defaultShortcut: 'Ctrl+Shift+M',
49
+ disabled: () => floatManager.list().length === 0,
50
+ run() {
51
+ const top = floatManager.list().at(-1);
52
+ if (top)
53
+ floatManager.toggleMaximize(top.id);
54
+ },
55
+ };
56
+ }
35
57
  export const sh3coreShard = {
36
58
  manifest: {
37
59
  id: '__sh3core__',
@@ -61,6 +83,7 @@ export const sh3coreShard = {
61
83
  import('../actions/listeners').then(({ openPalette }) => openPalette());
62
84
  },
63
85
  });
86
+ ctx.actions.register(buildToggleMaximizeAction());
64
87
  ctx.actions.register({
65
88
  id: 'sh3.app.reset-layout',
66
89
  label: 'Reset Current Layout',
@@ -44,8 +44,30 @@ describe('ctx.listVerbs / ctx.runVerb (integration)', () => {
44
44
  const list = consumerCtx.listVerbs();
45
45
  const a = list.find((v) => v.name === 'host:a');
46
46
  const b = list.find((v) => v.name === 'host:b');
47
- expect(a).toEqual({ shardId: 'host', name: 'host:a', summary: 'first verb', schema: undefined });
48
- expect(b).toEqual({ shardId: 'host', name: 'host:b', summary: 'second verb', schema: undefined });
47
+ expect(a).toEqual({ shardId: 'host', name: 'host:a', summary: 'first verb', programmatic: true, schema: undefined });
48
+ expect(b).toEqual({ shardId: 'host', name: 'host:b', summary: 'second verb', programmatic: undefined, schema: undefined });
49
+ });
50
+ it('listVerbs({ programmaticOnly: true }) returns only verbs that opted in', async () => {
51
+ registerShard({
52
+ manifest: { id: 'host', label: 'Host', version: '0.0.0', views: [] },
53
+ activate(ctx) {
54
+ ctx.registerVerb(programmaticVerb('a', 'first verb'));
55
+ ctx.registerVerb(plainVerb('b', 'second verb'));
56
+ },
57
+ });
58
+ let consumerCtx = null;
59
+ registerShard({
60
+ manifest: { id: 'consumer', label: 'C', version: '0.0.0', views: [] },
61
+ activate(ctx) {
62
+ consumerCtx = ctx;
63
+ },
64
+ });
65
+ await activateShard('host');
66
+ await activateShard('consumer');
67
+ const list = consumerCtx.listVerbs({ programmaticOnly: true });
68
+ expect(list.find((v) => v.name === 'host:a')).toBeDefined();
69
+ expect(list.find((v) => v.name === 'host:b')).toBeUndefined();
70
+ expect(list.every((v) => v.programmatic === true)).toBe(true);
49
71
  });
50
72
  it('runVerb dispatches a programmatic verb and returns captured scrollback', async () => {
51
73
  registerShard({
@@ -30,9 +30,11 @@ import { createShardKeysApi } from '../keys/client';
30
30
  import { PERMISSION_KEYS_MINT } from '../keys/types';
31
31
  import { subscribe } from '../keys/revocation-bus.svelte';
32
32
  import { register as contributionsRegister, list as contributionsList, listPoints as contributionsListPoints, onChange as contributionsOnChange, onAnyChange as contributionsOnAnyChange, } from '../contributions';
33
- import { registerAction } from '../actions/registry';
33
+ import { registerAction, listActions as listActionsFromRegistry } from '../actions/registry';
34
34
  import { makeSelectionApi, clearSelectionForShard } from '../actions/selection.svelte';
35
- import { openContextMenu as shellOpenContextMenu, openPalette as shellOpenPalette } from '../actions/listeners';
35
+ import { openContextMenu as shellOpenContextMenu, openPalette as shellOpenPalette, dispatchActionProgrammatic, } from '../actions/listeners';
36
+ import { listActionsFromEntries } from '../actions/listActive';
37
+ import { getLiveDispatcherState } from '../actions/state.svelte';
36
38
  /**
37
39
  * Reactive registry of every shard known to the host. Keys are shard ids.
38
40
  * Populated once at boot by the glob-discovery loop in main.ts (through
@@ -201,17 +203,29 @@ export async function activateShard(id, opts) {
201
203
  openContextMenu(opts) { shellOpenContextMenu(opts); },
202
204
  openPalette(opts) { shellOpenPalette(opts); },
203
205
  },
204
- listVerbs() {
205
- return listVerbsWithShard().map(({ verb, shardId }) => ({
206
+ listVerbs(opts) {
207
+ const all = listVerbsWithShard();
208
+ const filtered = (opts === null || opts === void 0 ? void 0 : opts.programmaticOnly)
209
+ ? all.filter(({ verb }) => verb.programmatic === true)
210
+ : all;
211
+ return filtered.map(({ verb, shardId }) => ({
206
212
  shardId,
207
213
  name: verb.name,
208
214
  summary: verb.summary,
215
+ programmatic: verb.programmatic,
209
216
  schema: verb.schema,
210
217
  }));
211
218
  },
212
219
  runVerb(shardId, name, args, opts) {
213
220
  return runVerbProgrammatic(shardId, name, args, opts);
214
221
  },
222
+ listActions(opts) {
223
+ const all = listActionsFromEntries(listActionsFromRegistry(), getLiveDispatcherState());
224
+ return (opts === null || opts === void 0 ? void 0 : opts.activeOnly) ? all.filter((a) => a.active) : all;
225
+ },
226
+ runAction(actionId, opts) {
227
+ return dispatchActionProgrammatic(actionId, opts);
228
+ },
215
229
  };
216
230
  entry.ctx = ctx;
217
231
  // Wire onKeyRevoked hook: subscribe to the revocation bus for this shard.
@@ -7,7 +7,7 @@ import type { Verb, VerbSchema } from '../verbs/types';
7
7
  import type { ScrollbackEntry } from '../shell-shard/scrollback.svelte';
8
8
  import type { ShardContextKeys } from '../keys/types';
9
9
  import type { ContributionsApi } from '../contributions/types';
10
- import type { ActionsApi } from '../actions/types';
10
+ import type { ActionsApi, ActionDescriptor } from '../actions/types';
11
11
  import type { TreeRootRef } from '../layout/types';
12
12
  export { PERMISSION_KEYS_MINT, type ShardContextKeys, type ApiKeyPublic, type MintOpts, ScopeEscalationError, ConsentDeniedError } from '../keys/types';
13
13
  /**
@@ -255,17 +255,25 @@ export interface ShardContext {
255
255
  /**
256
256
  * Read-only snapshot of every verb registered across every active shard.
257
257
  * Returned entries include the contributing `shardId`, the prefixed
258
- * `name`, the verb's `summary`, and (when present) its `schema`.
259
- * Order is undefined.
258
+ * `name`, the verb's `summary`, the `programmatic` flag, and (when
259
+ * present) its `schema`. Order is undefined.
260
+ *
261
+ * Pass `{ programmaticOnly: true }` to restrict the result to verbs that
262
+ * have opted in via `programmatic: true` — i.e. the verbs that are
263
+ * actually invocable through `ctx.runVerb(...)`. AI-class shards
264
+ * typically want this filter so they only surface what they can call.
260
265
  *
261
266
  * No permission gate — verb names + summaries are already visible via
262
267
  * the `help` verb. Diagnostic and AI-class shards (sh3-ai, sh3-diagnostic)
263
268
  * use this to enumerate the host's action surface.
264
269
  */
265
- listVerbs(): Array<{
270
+ listVerbs(opts?: {
271
+ programmaticOnly?: boolean;
272
+ }): Array<{
266
273
  shardId: string;
267
274
  name: string;
268
275
  summary: string;
276
+ programmatic?: boolean;
269
277
  schema?: VerbSchema;
270
278
  }>;
271
279
  /**
@@ -288,6 +296,38 @@ export interface ShardContext {
288
296
  result: unknown;
289
297
  scrollback: ScrollbackEntry[];
290
298
  }>;
299
+ /**
300
+ * Read-only snapshot of every action registered across every shard.
301
+ * Returns one descriptor per action id; the `active` flag indicates
302
+ * whether `runAction(id)` would dispatch right now (scope live, not
303
+ * disabled, has a run handler).
304
+ *
305
+ * Pass `{ activeOnly: true }` to filter to currently-dispatchable
306
+ * actions. AI-class shards typically want this filter.
307
+ *
308
+ * No permission gate — actions are already enumerable through the
309
+ * keyboard / palette / context-menu surfaces.
310
+ */
311
+ listActions(opts?: {
312
+ activeOnly?: boolean;
313
+ }): ActionDescriptor[];
314
+ /**
315
+ * Programmatically dispatch a registered action by id. Synthesizes the
316
+ * same `ActionDispatchContext` the keyboard/palette/context-menu paths
317
+ * use, with `invokedVia: 'programmatic'` and `appId / viewId / selection`
318
+ * sourced from current live state. Resolves after the action's `run`
319
+ * settles. Rejects on:
320
+ * - unknown action id,
321
+ * - action exists but is inactive (out-of-scope, disabled, submenu
322
+ * parent without `run`),
323
+ * - any error thrown by the action's `run`.
324
+ *
325
+ * `opts.signal` is stored on the dispatch context for v1 parity with
326
+ * `runVerb`; today's actions don't read it.
327
+ */
328
+ runAction(id: string, opts?: {
329
+ signal?: AbortSignal;
330
+ }): Promise<void>;
291
331
  }
292
332
  /**
293
333
  * A shard module. Shards are the fundamental unit of contribution in SH3.
@@ -0,0 +1,143 @@
1
+ <script lang="ts">
2
+ import type { Snippet } from 'svelte';
3
+
4
+ /**
5
+ * Single-line command entry widget. The chrome (focus ring, prefix slot,
6
+ * monospace font, anti-credential-fill attrs) is owned here; consumers
7
+ * supply the keybinding semantics via `onkeydown`.
8
+ *
9
+ * Two behaviors are baked in so every consumer gets them for free:
10
+ * - Selection-aware Ctrl+C: when the input has a non-empty selection,
11
+ * the keystroke is NOT forwarded to onkeydown — the browser does its
12
+ * native copy. This avoids the consumer mapping Ctrl+C to "clear
13
+ * draft" or "send SIGINT" eating a copy operation.
14
+ * - data-sh3-passthrough-modifiers on the row: the dispatcher's
15
+ * "block shortcuts in inputs" rule lets Ctrl/Alt/Meta-bearing
16
+ * shortcuts through this widget, so global bindings like Ctrl+K
17
+ * fire while the user is typing.
18
+ */
19
+ interface Props {
20
+ value: string;
21
+ prefix?: Snippet;
22
+ disabled?: boolean;
23
+ name?: string;
24
+ onkeydown?: (e: KeyboardEvent) => void;
25
+ }
26
+ let {
27
+ value = $bindable(''),
28
+ prefix,
29
+ disabled = false,
30
+ name,
31
+ onkeydown,
32
+ }: Props = $props();
33
+
34
+ let input: HTMLInputElement | null = $state(null);
35
+
36
+ function hasSelection(el: HTMLInputElement): boolean {
37
+ return el.selectionStart !== null
38
+ && el.selectionEnd !== null
39
+ && el.selectionStart !== el.selectionEnd;
40
+ }
41
+
42
+ function handleKeyDown(e: KeyboardEvent): void {
43
+ if ((e.ctrlKey || e.metaKey) && !e.altKey && e.key === 'c'
44
+ && input && hasSelection(input)) {
45
+ return;
46
+ }
47
+ onkeydown?.(e);
48
+ }
49
+
50
+ $effect(() => {
51
+ if (!disabled && input) input.focus();
52
+ });
53
+ </script>
54
+
55
+ <div
56
+ class="sh3-cmdline"
57
+ class:sh3-cmdline--disabled={disabled}
58
+ data-sh3-passthrough-modifiers
59
+ >
60
+ {#if prefix}<span class="sh3-cmdline__prefix">{@render prefix()}</span>{/if}
61
+ <!--
62
+ Two hidden inputs sit before the real one so Firefox doesn't try to
63
+ autofill saved credentials into a single visible text field. Source:
64
+ https://stackoverflow.com/a/29852908 (CC BY-SA 3.0, Bob The Janitor).
65
+ -->
66
+ <input type="text" class="sh3-cmdline__decoy" tabindex="-1" aria-hidden="true" />
67
+ <input type="password" class="sh3-cmdline__decoy" tabindex="-1" aria-hidden="true" />
68
+ <input
69
+ bind:this={input}
70
+ bind:value
71
+ type="search"
72
+ {name}
73
+ {disabled}
74
+ onkeydown={handleKeyDown}
75
+ spellcheck="false"
76
+ autocomplete="off"
77
+ autocapitalize="off"
78
+ aria-autocomplete="none"
79
+ data-1p-ignore
80
+ data-lpignore="true"
81
+ class="sh3-cmdline__input"
82
+ />
83
+ </div>
84
+
85
+ <style>
86
+ .sh3-cmdline {
87
+ display: flex;
88
+ align-items: baseline;
89
+ gap: 6px;
90
+ padding: 4px 8px;
91
+ border: 1px solid transparent;
92
+ border-radius: var(--shell-radius);
93
+ font-family: var(--shell-font-mono, monospace);
94
+ font-size: var(--shell-font-size, 13px);
95
+ line-height: 1.4;
96
+ transition: border-color var(--shell-motion-fast) var(--shell-ease-standard),
97
+ box-shadow var(--shell-motion-fast) var(--shell-ease-standard);
98
+ }
99
+ .sh3-cmdline:focus-within {
100
+ border-color: var(--shell-input-border-focus);
101
+ box-shadow: var(--shell-focus-ring);
102
+ }
103
+ .sh3-cmdline__prefix {
104
+ flex-shrink: 0;
105
+ font: inherit;
106
+ line-height: inherit;
107
+ margin: 0;
108
+ }
109
+ .sh3-cmdline__input {
110
+ flex: 1 1 auto;
111
+ padding: 0;
112
+ margin: 0;
113
+ background: transparent;
114
+ border: 0;
115
+ outline: 0;
116
+ color: inherit;
117
+ font: inherit;
118
+ line-height: inherit;
119
+ -webkit-appearance: none;
120
+ appearance: none;
121
+ }
122
+ .sh3-cmdline__input::-webkit-search-cancel-button,
123
+ .sh3-cmdline__input::-webkit-search-decoration,
124
+ .sh3-cmdline__input::-webkit-search-results-button,
125
+ .sh3-cmdline__input::-webkit-search-results-decoration {
126
+ display: none;
127
+ }
128
+ /* The .sh3-cmdline row owns the focus ring; suppress base.css's global
129
+ input:focus-visible box-shadow so the rings don't double up. */
130
+ .sh3-cmdline__input:focus,
131
+ .sh3-cmdline__input:focus-visible {
132
+ outline: none;
133
+ box-shadow: none;
134
+ border: none;
135
+ }
136
+ .sh3-cmdline__decoy {
137
+ display: none;
138
+ }
139
+ .sh3-cmdline--disabled .sh3-cmdline__input {
140
+ opacity: 0.5;
141
+ cursor: default;
142
+ }
143
+ </style>
@@ -0,0 +1,26 @@
1
+ import type { Snippet } from 'svelte';
2
+ /**
3
+ * Single-line command entry widget. The chrome (focus ring, prefix slot,
4
+ * monospace font, anti-credential-fill attrs) is owned here; consumers
5
+ * supply the keybinding semantics via `onkeydown`.
6
+ *
7
+ * Two behaviors are baked in so every consumer gets them for free:
8
+ * - Selection-aware Ctrl+C: when the input has a non-empty selection,
9
+ * the keystroke is NOT forwarded to onkeydown — the browser does its
10
+ * native copy. This avoids the consumer mapping Ctrl+C to "clear
11
+ * draft" or "send SIGINT" eating a copy operation.
12
+ * - data-sh3-passthrough-modifiers on the row: the dispatcher's
13
+ * "block shortcuts in inputs" rule lets Ctrl/Alt/Meta-bearing
14
+ * shortcuts through this widget, so global bindings like Ctrl+K
15
+ * fire while the user is typing.
16
+ */
17
+ interface Props {
18
+ value: string;
19
+ prefix?: Snippet;
20
+ disabled?: boolean;
21
+ name?: string;
22
+ onkeydown?: (e: KeyboardEvent) => void;
23
+ }
24
+ declare const CommandLine: import("svelte").Component<Props, {}, "value">;
25
+ type CommandLine = ReturnType<typeof CommandLine>;
26
+ export default CommandLine;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,43 @@
1
+ import { describe, it, expect, vi } from 'vitest';
2
+ import { render, fireEvent } from '@testing-library/svelte';
3
+ import CommandLine from './CommandLine.svelte';
4
+ function realInput(container) {
5
+ // The widget renders two FF-credential decoy inputs ahead of the real one.
6
+ return container.querySelector('input.sh3-cmdline__input');
7
+ }
8
+ describe('CommandLine selection-aware Ctrl+C', () => {
9
+ it('does NOT forward Ctrl+C to onkeydown when input has a non-empty selection', async () => {
10
+ const onkeydown = vi.fn();
11
+ const { container } = render(CommandLine, { props: { value: 'hello world', onkeydown } });
12
+ const inp = realInput(container);
13
+ inp.focus();
14
+ inp.setSelectionRange(0, 5); // select "hello"
15
+ await fireEvent.keyDown(inp, { key: 'c', ctrlKey: true });
16
+ expect(onkeydown).not.toHaveBeenCalled();
17
+ });
18
+ it('forwards Ctrl+C to onkeydown when there is no selection', async () => {
19
+ const onkeydown = vi.fn();
20
+ const { container } = render(CommandLine, { props: { value: 'hello', onkeydown } });
21
+ const inp = realInput(container);
22
+ inp.focus();
23
+ inp.setSelectionRange(3, 3); // caret only, no selection
24
+ await fireEvent.keyDown(inp, { key: 'c', ctrlKey: true });
25
+ expect(onkeydown).toHaveBeenCalledTimes(1);
26
+ });
27
+ it('forwards non-Ctrl+C keys regardless of selection', async () => {
28
+ const onkeydown = vi.fn();
29
+ const { container } = render(CommandLine, { props: { value: 'hello', onkeydown } });
30
+ const inp = realInput(container);
31
+ inp.focus();
32
+ inp.setSelectionRange(0, 5);
33
+ await fireEvent.keyDown(inp, { key: 'k', ctrlKey: true });
34
+ expect(onkeydown).toHaveBeenCalledTimes(1);
35
+ });
36
+ });
37
+ describe('CommandLine modifier passthrough attribute', () => {
38
+ it('stamps data-sh3-passthrough-modifiers so the dispatcher lets modifier shortcuts through', () => {
39
+ const { container } = render(CommandLine, { props: { value: '' } });
40
+ const row = container.querySelector('.sh3-cmdline');
41
+ expect(row.hasAttribute('data-sh3-passthrough-modifiers')).toBe(true);
42
+ });
43
+ });
@@ -1,21 +1,22 @@
1
1
  <script lang="ts">
2
2
  import type { SessionClient } from './session-client.svelte';
3
+ import CommandLine from './CommandLine.svelte';
3
4
 
4
5
  interface Props {
5
6
  cwd: string;
7
+ /** When false, hide the cwd chip in front of the prompt. */
8
+ showCwd?: boolean;
6
9
  locked: boolean; // true while a process is running
7
10
  history: string[]; // persisted history, newest last
8
11
  session: SessionClient;
9
12
  onSubmit: (line: string) => void; // called with the raw entered line
10
13
  }
11
- let { cwd, locked, history, session, onSubmit }: Props = $props();
14
+ let { cwd, showCwd = true, locked, history, session, onSubmit }: Props = $props();
12
15
 
13
16
  let draft = $state('');
14
17
  let historyIndex = $state<number | null>(null); // null = live draft
15
18
  let savedDraft = $state(''); // restored when user returns from history
16
19
 
17
- let input: HTMLInputElement | null = $state(null);
18
-
19
20
  function submit() {
20
21
  if (locked) return;
21
22
  const line = draft;
@@ -74,6 +75,8 @@
74
75
  e.preventDefault();
75
76
  navHistoryDown();
76
77
  } else if (e.ctrlKey && e.key === 'c') {
78
+ // CommandLine has already filtered this case to no-selection, so the
79
+ // user is asking to clear the draft, not copy.
77
80
  e.preventDefault();
78
81
  draft = '';
79
82
  historyIndex = null;
@@ -83,52 +86,26 @@
83
86
  submit();
84
87
  }
85
88
  }
86
-
87
- // Re-focus the input when the locked state flips to false
88
- $effect(() => {
89
- if (!locked && input) {
90
- input.focus();
91
- }
92
- });
93
89
  </script>
94
90
 
95
- <div class="shell-input" class:locked>
96
- <span class="shell-input-cwd">{cwd}</span>
97
- <span class="shell-input-arrow">❯</span>
98
- <input
99
- bind:this={input}
91
+ <div class="shell-input-row">
92
+ <CommandLine
100
93
  bind:value={draft}
101
- type="text"
102
94
  disabled={locked}
95
+ name="shell-cmdline"
103
96
  onkeydown={onKeyDown}
104
- spellcheck="false"
105
- autocomplete="off"
106
- autocapitalize="off"
107
- class="shell-input-field"
108
- />
97
+ >
98
+ {#snippet prefix()}
99
+ {#if showCwd}<span class="shell-input-cwd">{cwd}</span>{/if}
100
+ <span class="shell-input-arrow">❯</span>
101
+ {/snippet}
102
+ </CommandLine>
109
103
  </div>
110
104
 
111
105
  <style>
112
- .shell-input {
113
- display: flex;
114
- gap: 8px;
115
- padding: 4px 8px;
106
+ .shell-input-row {
116
107
  border-top: 1px solid var(--shell-border, #333);
117
- font-family: var(--shell-font-mono, monospace);
118
108
  }
119
109
  .shell-input-cwd { color: var(--shell-fg-muted, #888); }
120
- .shell-input-arrow { color: var(--shell-accent, #6cf); }
121
- .shell-input-field {
122
- padding: 0;
123
- flex: 1 1 auto;
124
- background: transparent;
125
- border: 0;
126
- outline: 0;
127
- color: var(--shell-fg, #ddd);
128
- font: inherit;
129
- }
130
- .shell-input.locked .shell-input-field {
131
- opacity: 0.5;
132
- cursor: default;
133
- }
110
+ .shell-input-arrow { color: var(--shell-accent, #6cf); margin-left: 6px; }
134
111
  </style>
@@ -1,6 +1,8 @@
1
1
  import type { SessionClient } from './session-client.svelte';
2
2
  interface Props {
3
3
  cwd: string;
4
+ /** When false, hide the cwd chip in front of the prompt. */
5
+ showCwd?: boolean;
4
6
  locked: boolean;
5
7
  history: string[];
6
8
  session: SessionClient;
@@ -4,11 +4,13 @@
4
4
  import PromptEntry from './entries/PromptEntry.svelte';
5
5
  import StatusEntry from './entries/StatusEntry.svelte';
6
6
  import RichEntry from './entries/RichEntry.svelte';
7
+ import { lookupRichComponent } from './rich-registry';
7
8
 
8
9
  interface Props {
9
10
  scrollback: Scrollback;
11
+ showPromptCwd: boolean;
10
12
  }
11
- let { scrollback }: Props = $props();
13
+ let { scrollback, showPromptCwd }: Props = $props();
12
14
 
13
15
  let container: HTMLDivElement | null = $state(null);
14
16
  let content: HTMLDivElement | null = $state(null);
@@ -48,11 +50,16 @@
48
50
  {#if entry.kind === 'text'}
49
51
  <TextEntry stream={entry.stream} chunks={entry.chunks} />
50
52
  {:else if entry.kind === 'prompt'}
51
- <PromptEntry cwd={entry.cwd} line={entry.line} />
53
+ <PromptEntry cwd={showPromptCwd ? entry.cwd : ''} line={entry.line} />
52
54
  {:else if entry.kind === 'status'}
53
55
  <StatusEntry text={entry.text} level={entry.level} />
54
56
  {:else if entry.kind === 'rich'}
55
- <RichEntry component={entry.component} componentProps={entry.props} />
57
+ {@const comp = entry.component ?? (entry.componentKey ? lookupRichComponent(entry.componentKey) : null)}
58
+ {#if comp}
59
+ <RichEntry component={comp} componentProps={entry.props} />
60
+ {:else}
61
+ <StatusEntry text={`<rich entry: ${entry.componentKey ?? 'unknown'} not registered>`} level="info" />
62
+ {/if}
56
63
  {/if}
57
64
  {/each}
58
65
  </div>
@@ -1,6 +1,7 @@
1
1
  import type { Scrollback } from './scrollback.svelte';
2
2
  interface Props {
3
3
  scrollback: Scrollback;
4
+ showPromptCwd: boolean;
4
5
  }
5
6
  declare const ScrollbackView: import("svelte").Component<Props, {}, "">;
6
7
  type ScrollbackView = ReturnType<typeof ScrollbackView>;