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
@@ -0,0 +1,51 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { Scrollback } from './scrollback.svelte';
3
+ const FakeComp = {};
4
+ describe('Scrollback rich entry', () => {
5
+ it('stores componentKey alongside component for persistence', () => {
6
+ const sb = new Scrollback();
7
+ sb.push({
8
+ kind: 'rich',
9
+ componentKey: 'test-table',
10
+ component: FakeComp,
11
+ props: { foo: 1 },
12
+ ts: 0,
13
+ });
14
+ const entry = sb.entries[0];
15
+ expect(entry.kind).toBe('rich');
16
+ if (entry.kind === 'rich') {
17
+ expect(entry.componentKey).toBe('test-table');
18
+ expect(entry.component).toStrictEqual(FakeComp);
19
+ expect(entry.props).toEqual({ foo: 1 });
20
+ }
21
+ });
22
+ });
23
+ describe('Scrollback.restore', () => {
24
+ it('replaces entries and bumps id counter past max persisted id', () => {
25
+ const sb = new Scrollback();
26
+ sb.restore([
27
+ { id: 'e500', kind: 'prompt', cwd: '/', line: 'a', ts: 0 },
28
+ { id: 'e501', kind: 'prompt', cwd: '/', line: 'b', ts: 1 },
29
+ ]);
30
+ expect(sb.entries).toHaveLength(2);
31
+ sb.push({ kind: 'prompt', cwd: '/', line: 'c', ts: 2 });
32
+ expect(sb.entries).toHaveLength(3);
33
+ const ids = sb.entries.map((e) => e.id);
34
+ expect(new Set(ids).size).toBe(3);
35
+ const minted = ids[2];
36
+ expect(Number(minted.slice(1))).toBeGreaterThan(501);
37
+ });
38
+ it('preserves $state identity by mutating in place', () => {
39
+ const sb = new Scrollback();
40
+ const ref = sb.entries;
41
+ sb.restore([{ id: 'eX', kind: 'prompt', cwd: '/', line: 'a', ts: 0 }]);
42
+ expect(sb.entries).toBe(ref);
43
+ });
44
+ it('tolerates non-numeric ids without throwing', () => {
45
+ const sb = new Scrollback();
46
+ sb.restore([{ id: 'foo', kind: 'prompt', cwd: '/', line: 'a', ts: 0 }]);
47
+ sb.push({ kind: 'prompt', cwd: '/', line: 'b', ts: 1 });
48
+ expect(sb.entries).toHaveLength(2);
49
+ expect(new Set(sb.entries.map((e) => e.id)).size).toBe(2);
50
+ });
51
+ });
@@ -5,13 +5,29 @@ export declare class SessionClient {
5
5
  private ws;
6
6
  private handlers;
7
7
  private pendingHistoryLogs;
8
- private lastSeq;
9
8
  private backoffIndex;
10
9
  private closed;
11
10
  connected: boolean;
12
11
  cwd: string;
12
+ /**
13
+ * Per-user shell tenant root, fixed for the session's lifetime. Used by
14
+ * the input line to render the cwd relative to this prefix (e.g., `~`).
15
+ * Populated from the server's welcome message.
16
+ */
17
+ tenantRoot: string;
13
18
  env: Record<string, string>;
14
- history: string[];
19
+ /**
20
+ * Per-mode history bundle hydrated from the server's history-bundle reply.
21
+ * Each entry is the persisted history for that mode id. Terminal.svelte
22
+ * mirrors these slots into the corresponding ModeBuffer.history.
23
+ */
24
+ historyByMode: Record<string, string[]>;
25
+ /**
26
+ * Latest server-event seq the client has observed. Public so the boot
27
+ * path can pre-seed it from a persisted bash buffer snapshot before
28
+ * calling connect(); the WS hello then uses it as a replay cursor.
29
+ */
30
+ lastSeq: number;
15
31
  constructor(url: string);
16
32
  connect(): void;
17
33
  private scheduleReconnect;
@@ -21,13 +21,29 @@ export class SessionClient {
21
21
  this.ws = null;
22
22
  this.handlers = new Set();
23
23
  this.pendingHistoryLogs = [];
24
- this.lastSeq = 0;
25
24
  this.backoffIndex = 0;
26
25
  this.closed = false;
27
26
  this.connected = $state(false);
28
27
  this.cwd = $state('');
28
+ /**
29
+ * Per-user shell tenant root, fixed for the session's lifetime. Used by
30
+ * the input line to render the cwd relative to this prefix (e.g., `~`).
31
+ * Populated from the server's welcome message.
32
+ */
33
+ this.tenantRoot = $state('');
29
34
  this.env = $state({});
30
- this.history = $state([]);
35
+ /**
36
+ * Per-mode history bundle hydrated from the server's history-bundle reply.
37
+ * Each entry is the persisted history for that mode id. Terminal.svelte
38
+ * mirrors these slots into the corresponding ModeBuffer.history.
39
+ */
40
+ this.historyByMode = $state({});
41
+ /**
42
+ * Latest server-event seq the client has observed. Public so the boot
43
+ * path can pre-seed it from a persisted bash buffer snapshot before
44
+ * calling connect(); the WS hello then uses it as a replay cursor.
45
+ */
46
+ this.lastSeq = $state(0);
31
47
  }
32
48
  connect() {
33
49
  if (this.closed)
@@ -66,14 +82,15 @@ export class SessionClient {
66
82
  }
67
83
  if (msg.t === 'welcome') {
68
84
  this.cwd = msg.cwd;
85
+ this.tenantRoot = msg.tenantRoot;
69
86
  this.env = msg.env;
70
87
  this.lastSeq = msg.seq;
71
88
  }
72
89
  if (msg.t === 'cwd') {
73
90
  this.cwd = msg.cwd;
74
91
  }
75
- if (msg.t === 'history') {
76
- this.history = msg.lines;
92
+ if (msg.t === 'history-bundle') {
93
+ this.historyByMode = msg.byMode;
77
94
  }
78
95
  for (const h of this.handlers)
79
96
  h(msg);
@@ -1,3 +1,4 @@
1
1
  import type { ShellApi } from './registry';
2
- export declare function makeShellApiHeadless(): ShellApi;
2
+ import type { ZoneManager } from '../state/types';
3
+ export declare function makeShellApiHeadless(zones?: ZoneManager): ShellApi;
3
4
  export declare function makeShellApiForTest(): ShellApi;
@@ -15,6 +15,7 @@ import { registeredShards, listStandaloneViews } from '../shards/activate.svelte
15
15
  import { inspectActiveLayout, focusView, closeTab, popoutView, dockFloat, dockIntoActiveLayout, locateSlot as locateSlotInActiveLayout, } from '../layout/inspection';
16
16
  import { floatManager } from '../overlays/float';
17
17
  import { getUser, isAdmin } from '../auth/index';
18
+ const KNOWN_ZONES = ['ephemeral', 'session', 'workspace', 'user'];
18
19
  function collectTabEntries(node) {
19
20
  if (node.type === 'tabs') {
20
21
  return node.tabs.filter((t) => t.viewId !== null);
@@ -27,7 +28,7 @@ function collectTabEntries(node) {
27
28
  }
28
29
  return [];
29
30
  }
30
- export function makeShellApiHeadless() {
31
+ export function makeShellApiHeadless(zones) {
31
32
  return {
32
33
  listApps() {
33
34
  return listRegisteredApps().map((m) => ({ id: m.id, label: m.label }));
@@ -123,8 +124,35 @@ export function makeShellApiHeadless() {
123
124
  void closeTab(slotId);
124
125
  return { ok: true };
125
126
  },
126
- listZones(_shardId) { return []; },
127
- readZone(_shardId, _zoneName) { return null; },
127
+ listZones(shardId) {
128
+ if (!zones)
129
+ return [];
130
+ const byShard = new Map();
131
+ for (const zone of KNOWN_ZONES) {
132
+ for (const id of zones.list(zone)) {
133
+ if (shardId && id !== shardId)
134
+ continue;
135
+ let set = byShard.get(id);
136
+ if (!set) {
137
+ set = new Set();
138
+ byShard.set(id, set);
139
+ }
140
+ set.add(zone);
141
+ }
142
+ }
143
+ return Array.from(byShard.entries()).map(([id, set]) => ({
144
+ shardId: id,
145
+ zones: Array.from(set),
146
+ }));
147
+ },
148
+ readZone(shardId, zoneName) {
149
+ var _a;
150
+ if (!zones)
151
+ return null;
152
+ if (!KNOWN_ZONES.includes(zoneName))
153
+ return null;
154
+ return (_a = zones.peek(zoneName, shardId)) !== null && _a !== void 0 ? _a : null;
155
+ },
128
156
  whoAmI() {
129
157
  var _a;
130
158
  const user = getUser();
@@ -135,6 +163,7 @@ export function makeShellApiHeadless() {
135
163
  },
136
164
  setMode(_id) { return false; },
137
165
  listModes() { return []; },
166
+ getMode() { return { id: 'sh3', label: 'sh3' }; },
138
167
  };
139
168
  }
140
169
  export function makeShellApiForTest() {
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,59 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { makeShellApiHeadless } from './shellApi';
3
+ function makeMockZoneManager() {
4
+ const data = {
5
+ ephemeral: {},
6
+ session: { 'shard-a': { foo: 1 } },
7
+ workspace: { 'shard-a': { bar: 2 }, 'shard-b': { baz: 3 } },
8
+ user: { 'shard-b': { theme: 'dark' } },
9
+ };
10
+ return {
11
+ list: (zone) => Object.keys(data[zone]),
12
+ peek: (zone, shardId) => data[zone][shardId],
13
+ clear: () => { },
14
+ clearAll: () => { },
15
+ };
16
+ }
17
+ describe('shellApi listZones', () => {
18
+ it('returns empty when no ZoneManager provided (stub mode)', () => {
19
+ const api = makeShellApiHeadless();
20
+ expect(api.listZones()).toEqual([]);
21
+ });
22
+ it('returns one row per shard with the zones it has data in', () => {
23
+ var _a, _b;
24
+ const api = makeShellApiHeadless(makeMockZoneManager());
25
+ const rows = api.listZones();
26
+ const byShard = new Map(rows.map((r) => [r.shardId, r.zones]));
27
+ expect((_a = byShard.get('shard-a')) === null || _a === void 0 ? void 0 : _a.sort()).toEqual(['session', 'workspace']);
28
+ expect((_b = byShard.get('shard-b')) === null || _b === void 0 ? void 0 : _b.sort()).toEqual(['user', 'workspace']);
29
+ });
30
+ it('filters by shardId when provided', () => {
31
+ const api = makeShellApiHeadless(makeMockZoneManager());
32
+ const rows = api.listZones('shard-a');
33
+ expect(rows).toHaveLength(1);
34
+ expect(rows[0].shardId).toBe('shard-a');
35
+ expect(rows[0].zones.sort()).toEqual(['session', 'workspace']);
36
+ });
37
+ it('returns empty array for unknown shardId', () => {
38
+ const api = makeShellApiHeadless(makeMockZoneManager());
39
+ expect(api.listZones('not-a-shard')).toEqual([]);
40
+ });
41
+ });
42
+ describe('shellApi readZone', () => {
43
+ it('returns null when no ZoneManager provided', () => {
44
+ const api = makeShellApiHeadless();
45
+ expect(api.readZone('shard-a', 'workspace')).toBe(null);
46
+ });
47
+ it('returns peeked value via ZoneManager', () => {
48
+ const api = makeShellApiHeadless(makeMockZoneManager());
49
+ expect(api.readZone('shard-a', 'workspace')).toEqual({ bar: 2 });
50
+ });
51
+ it('returns null for unknown zone name', () => {
52
+ const api = makeShellApiHeadless(makeMockZoneManager());
53
+ expect(api.readZone('shard-a', 'bogus')).toBe(null);
54
+ });
55
+ it('returns null for missing shard entry', () => {
56
+ const api = makeShellApiHeadless(makeMockZoneManager());
57
+ expect(api.readZone('not-a-shard', 'workspace')).toBe(null);
58
+ });
59
+ });
@@ -24,12 +24,19 @@ import { makeShellApiHeadless } from './shellApi';
24
24
  import { focusView } from '../layout/inspection';
25
25
  import { floatManager } from '../overlays/float';
26
26
  import { getUser, isAdmin } from '../auth/index';
27
+ import { __bindZone, __unbindZone } from './buffer-zone-state.svelte';
27
28
  export { makeShellApiHeadless, makeShellApiForTest } from './shellApi';
28
29
  export const shellShard = {
29
30
  manifest,
30
31
  activate(ctx) {
31
32
  registerV1Verbs(ctx);
32
- const shell = makeShellApiHeadless();
33
+ const shell = makeShellApiHeadless(ctx.zones);
34
+ // Bind the shell-shard's workspace zone — backs scrollback persistence
35
+ // (SH8). BufferStore reads/writes through this proxy.
36
+ const zone = ctx.state({
37
+ workspace: { buffers: {} },
38
+ });
39
+ __bindZone(zone);
33
40
  // The AZERTY `²` key (top-left on FR keyboards, below Escape) opens the
34
41
  // terminal view — focusing it if already mounted, floating it otherwise.
35
42
  // Migrated from Shell.svelte's inline keydown handler as proof-of-concept
@@ -72,4 +79,7 @@ export const shellShard = {
72
79
  autostart() {
73
80
  // Intentionally empty — same pattern as __sh3core__.
74
81
  },
82
+ deactivate() {
83
+ __unbindZone();
84
+ },
75
85
  };
@@ -7,6 +7,8 @@ function scaffold(modeId) {
7
7
  ? { id: 'bash', label: 'Bash', transport: 'ws', autoRelocate: false, requiresRole: 'admin' }
8
8
  : { id: 'sh3', label: 'SH3', transport: 'none', autoRelocate: true };
9
9
  const scrollback = { push: (e) => pushed.push(e) };
10
+ const history = [];
11
+ const buffer = { scrollback, history };
10
12
  const session = {
11
13
  history: { push: vi.fn() },
12
14
  send: (m) => sent.push(m),
@@ -28,7 +30,7 @@ function scaffold(modeId) {
28
30
  mode: () => mode,
29
31
  role: () => (modeId === 'bash' ? 'admin' : 'user'),
30
32
  resolver,
31
- scrollback,
33
+ buffer: () => buffer,
32
34
  session,
33
35
  shell,
34
36
  fs,
@@ -1,12 +1,19 @@
1
1
  import AppsTable from '../rich/AppsTable.svelte';
2
2
  import AppCard from '../rich/AppCard.svelte';
3
+ import { registerRichComponent } from '../rich-registry';
4
+ const APPS_TABLE_KEY = 'apps-table';
5
+ const APP_CARD_KEY = 'app-card';
6
+ registerRichComponent(APPS_TABLE_KEY, AppsTable);
7
+ registerRichComponent(APP_CARD_KEY, AppCard);
3
8
  export const appsVerb = {
4
9
  name: 'apps',
5
10
  summary: 'List installed apps. Click a row to launch.',
11
+ programmatic: true,
6
12
  async run(ctx) {
7
13
  const apps = ctx.shell.listApps();
8
14
  ctx.scrollback.push({
9
15
  kind: 'rich',
16
+ componentKey: APPS_TABLE_KEY,
10
17
  component: AppsTable,
11
18
  props: {
12
19
  data: {
@@ -29,6 +36,7 @@ export const appsVerb = {
29
36
  export const appVerb = {
30
37
  name: 'app',
31
38
  summary: 'Show the currently active app.',
39
+ programmatic: true,
32
40
  async run(ctx) {
33
41
  const active = ctx.shell.getActiveApp();
34
42
  if (!active) {
@@ -42,6 +50,7 @@ export const appVerb = {
42
50
  }
43
51
  ctx.scrollback.push({
44
52
  kind: 'rich',
53
+ componentKey: APP_CARD_KEY,
45
54
  component: AppCard,
46
55
  props: { data: { id: active.id, label: active.label, shards: [] } },
47
56
  ts: Date.now(),
@@ -1,4 +1,7 @@
1
1
  import EnvTable from '../rich/EnvTable.svelte';
2
+ import { registerRichComponent } from '../rich-registry';
3
+ const ENV_TABLE_KEY = 'env-table';
4
+ registerRichComponent(ENV_TABLE_KEY, EnvTable);
2
5
  export const envVerb = {
3
6
  name: 'env',
4
7
  summary: 'Show the session environment.',
@@ -6,6 +9,7 @@ export const envVerb = {
6
9
  const env = ctx.session.env;
7
10
  ctx.scrollback.push({
8
11
  kind: 'rich',
12
+ componentKey: ENV_TABLE_KEY,
9
13
  component: EnvTable,
10
14
  props: { data: { env } },
11
15
  ts: Date.now(),
@@ -1,13 +1,21 @@
1
1
  import { listVerbs } from '../../shards/registry';
2
2
  import HelpTable from '../rich/HelpTable.svelte';
3
+ import { registerRichComponent } from '../rich-registry';
4
+ const HELP_TABLE_KEY = 'help-table';
5
+ registerRichComponent(HELP_TABLE_KEY, HelpTable);
3
6
  export function makeHelpVerb() {
4
7
  return {
5
8
  name: 'help',
6
9
  summary: 'List verbs or show detail for one.',
10
+ globalVerb: true,
7
11
  async run(ctx) {
8
- const rows = listVerbs().map((v) => ({ name: v.name, summary: v.summary }));
12
+ const inSh3 = ctx.shell.getMode().id === 'sh3';
13
+ const rows = listVerbs()
14
+ .filter((v) => inSh3 || v.globalVerb === true)
15
+ .map((v) => ({ name: v.name, summary: v.summary }));
9
16
  ctx.scrollback.push({
10
17
  kind: 'rich',
18
+ componentKey: HELP_TABLE_KEY,
11
19
  component: HelpTable,
12
20
  props: {
13
21
  data: {
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,53 @@
1
+ import { describe, it, expect, beforeEach } from 'vitest';
2
+ import { makeHelpVerb } from './help';
3
+ import { registerVerb, __resetViewRegistryForTest } from '../../shards/registry';
4
+ function makeCtx(modeId) {
5
+ const pushed = [];
6
+ const ctx = {
7
+ shell: { getMode: () => ({ id: modeId, label: modeId }) },
8
+ scrollback: { push: (e) => pushed.push(e) },
9
+ session: {},
10
+ cwd: '/',
11
+ dispatch: async () => { },
12
+ fs: {},
13
+ };
14
+ return { ctx, pushed };
15
+ }
16
+ const sh3Verb = { name: 'apps', summary: 'list apps', async run() { } };
17
+ const globalA = { name: 'clear', summary: 'clear scrollback', globalVerb: true, async run() { } };
18
+ const globalB = { name: 'mode', summary: 'switch mode', globalVerb: true, async run() { } };
19
+ describe('help verb', () => {
20
+ beforeEach(() => {
21
+ __resetViewRegistryForTest();
22
+ registerVerb('apps', sh3Verb, 'shell');
23
+ registerVerb('clear', globalA, 'shell');
24
+ registerVerb('mode', globalB, 'shell');
25
+ registerVerb('help', makeHelpVerb(), 'shell');
26
+ });
27
+ it('lists every registered verb in sh3 mode', async () => {
28
+ const help = makeHelpVerb();
29
+ const { ctx, pushed } = makeCtx('sh3');
30
+ await help.run(ctx, []);
31
+ const rich = pushed.find((e) => e.kind === 'rich');
32
+ const names = rich.props.data.rows.map((r) => r.name);
33
+ expect(names).toContain('apps');
34
+ expect(names).toContain('clear');
35
+ expect(names).toContain('mode');
36
+ expect(names).toContain('help');
37
+ });
38
+ it('lists only globalVerb-flagged verbs in a custom mode', async () => {
39
+ const help = makeHelpVerb();
40
+ const { ctx, pushed } = makeCtx('gemini');
41
+ await help.run(ctx, []);
42
+ const rich = pushed.find((e) => e.kind === 'rich');
43
+ const names = rich.props.data.rows.map((r) => r.name);
44
+ expect(names).toContain('clear');
45
+ expect(names).toContain('mode');
46
+ expect(names).toContain('help');
47
+ expect(names).not.toContain('apps');
48
+ });
49
+ it('is flagged globalVerb so it resolves in custom modes', () => {
50
+ const help = makeHelpVerb();
51
+ expect(help.globalVerb).toBe(true);
52
+ });
53
+ });
@@ -1,12 +1,19 @@
1
1
  import HistoryList from '../rich/HistoryList.svelte';
2
+ import { registerRichComponent } from '../rich-registry';
3
+ const HISTORY_LIST_KEY = 'history-list';
4
+ registerRichComponent(HISTORY_LIST_KEY, HistoryList);
2
5
  export const historyVerb = {
3
6
  name: 'history',
4
7
  summary: 'Show the last N history lines. Default 50.',
5
8
  async run(ctx, args) {
9
+ var _a;
6
10
  const n = args[0] ? Math.max(1, parseInt(args[0], 10) || 50) : 50;
7
- const lines = ctx.session.history.slice(-n);
11
+ const modeId = ctx.shell.getMode().id;
12
+ const allLines = (_a = ctx.session.historyByMode[modeId]) !== null && _a !== void 0 ? _a : [];
13
+ const lines = allLines.slice(-n);
8
14
  ctx.scrollback.push({
9
15
  kind: 'rich',
16
+ componentKey: HISTORY_LIST_KEY,
10
17
  component: HistoryList,
11
18
  props: {
12
19
  data: {
@@ -10,10 +10,7 @@ import { appsVerb, appVerb } from './apps';
10
10
  import { shardsVerb } from './shards';
11
11
  import { viewsVerb, openVerb, closeVerb, popoutVerb, dockVerb } from './views';
12
12
  import { zonesVerb, zoneVerb } from './zones';
13
- import { pwdVerb, cdVerb, whoamiVerb } from './session';
14
13
  import { envVerb } from './env';
15
- import { lsVerb } from './ls';
16
- import { catVerb } from './cat';
17
14
  import { resetVerb } from './reset';
18
15
  export function registerV1Verbs(ctx) {
19
16
  ctx.registerVerb(makeHelpVerb());
@@ -30,11 +27,6 @@ export function registerV1Verbs(ctx) {
30
27
  ctx.registerVerb(dockVerb);
31
28
  ctx.registerVerb(zonesVerb);
32
29
  ctx.registerVerb(zoneVerb);
33
- ctx.registerVerb(pwdVerb);
34
- ctx.registerVerb(cdVerb);
35
30
  ctx.registerVerb(envVerb);
36
- ctx.registerVerb(whoamiVerb);
37
- ctx.registerVerb(lsVerb);
38
- ctx.registerVerb(catVerb);
39
31
  ctx.registerVerb(resetVerb);
40
32
  }
@@ -1,11 +1,16 @@
1
1
  import ShardsTable from '../rich/ShardsTable.svelte';
2
+ import { registerRichComponent } from '../rich-registry';
3
+ const SHARDS_TABLE_KEY = 'shards-table';
4
+ registerRichComponent(SHARDS_TABLE_KEY, ShardsTable);
2
5
  export const shardsVerb = {
3
6
  name: 'shards',
4
7
  summary: 'List active shards.',
8
+ programmatic: true,
5
9
  async run(ctx) {
6
10
  const shards = ctx.shell.listShards();
7
11
  ctx.scrollback.push({
8
12
  kind: 'rich',
13
+ componentKey: SHARDS_TABLE_KEY,
9
14
  component: ShardsTable,
10
15
  props: { data: { shards } },
11
16
  ts: Date.now(),
@@ -1,7 +1,11 @@
1
1
  import ViewsTable from '../rich/ViewsTable.svelte';
2
+ import { registerRichComponent } from '../rich-registry';
3
+ const VIEWS_TABLE_KEY = 'views-table';
4
+ registerRichComponent(VIEWS_TABLE_KEY, ViewsTable);
2
5
  export const viewsVerb = {
3
6
  name: 'views',
4
7
  summary: 'List views currently mounted. Pass --standalone to list summonable views instead.',
8
+ programmatic: true,
5
9
  async run(ctx, args) {
6
10
  if (args.includes('--standalone')) {
7
11
  const standalones = ctx.shell.listStandaloneViews();
@@ -27,6 +31,7 @@ export const viewsVerb = {
27
31
  const views = ctx.shell.listViewsInCurrentLayout();
28
32
  ctx.scrollback.push({
29
33
  kind: 'rich',
34
+ componentKey: VIEWS_TABLE_KEY,
30
35
  component: ViewsTable,
31
36
  props: {
32
37
  data: {
@@ -52,6 +57,7 @@ export const viewsVerb = {
52
57
  export const openVerb = {
53
58
  name: 'open',
54
59
  summary: 'Open a view from any active shard into the current layout.',
60
+ programmatic: true,
55
61
  async run(ctx, args) {
56
62
  var _a;
57
63
  const viewId = args[0];
@@ -86,6 +92,7 @@ export const openVerb = {
86
92
  export const popoutVerb = {
87
93
  name: 'popout',
88
94
  summary: 'Pop a docked view out into a float by slot id.',
95
+ programmatic: true,
89
96
  async run(ctx, args) {
90
97
  var _a;
91
98
  const slotId = args[0];
@@ -120,6 +127,7 @@ export const popoutVerb = {
120
127
  export const dockVerb = {
121
128
  name: 'dock',
122
129
  summary: 'Dock a float back into the current layout by float id. Run with no args to list floats.',
130
+ programmatic: true,
123
131
  async run(ctx, args) {
124
132
  var _a;
125
133
  const floatId = args[0];
@@ -166,6 +174,7 @@ export const dockVerb = {
166
174
  export const closeVerb = {
167
175
  name: 'close',
168
176
  summary: 'Close a view by slot id.',
177
+ programmatic: true,
169
178
  async run(ctx, args) {
170
179
  var _a;
171
180
  const slotId = args[0];
@@ -1,12 +1,19 @@
1
1
  import ZonesTable from '../rich/ZonesTable.svelte';
2
2
  import ZoneTree from '../rich/ZoneTree.svelte';
3
+ import { registerRichComponent } from '../rich-registry';
4
+ const ZONES_TABLE_KEY = 'zones-table';
5
+ const ZONE_TREE_KEY = 'zone-tree';
6
+ registerRichComponent(ZONES_TABLE_KEY, ZonesTable);
7
+ registerRichComponent(ZONE_TREE_KEY, ZoneTree);
3
8
  export const zonesVerb = {
4
9
  name: 'zones',
5
10
  summary: 'List zones for the current user (optionally scoped to a shard).',
11
+ programmatic: true,
6
12
  async run(ctx, args) {
7
13
  const rows = ctx.shell.listZones(args[0]);
8
14
  ctx.scrollback.push({
9
15
  kind: 'rich',
16
+ componentKey: ZONES_TABLE_KEY,
10
17
  component: ZonesTable,
11
18
  props: { data: { rows } },
12
19
  ts: Date.now(),
@@ -16,6 +23,7 @@ export const zonesVerb = {
16
23
  export const zoneVerb = {
17
24
  name: 'zone',
18
25
  summary: 'Dump the contents of a zone as a collapsible JSON tree.',
26
+ programmatic: true,
19
27
  async run(ctx, args) {
20
28
  const [shardId, zoneName] = args;
21
29
  if (!shardId || !zoneName) {
@@ -30,6 +38,7 @@ export const zoneVerb = {
30
38
  const value = ctx.shell.readZone(shardId, zoneName);
31
39
  ctx.scrollback.push({
32
40
  kind: 'rich',
41
+ componentKey: ZONE_TREE_KEY,
33
42
  component: ZoneTree,
34
43
  props: { data: { value } },
35
44
  ts: Date.now(),
@@ -77,6 +77,15 @@ export interface ShellApi {
77
77
  id: string;
78
78
  label: string;
79
79
  }[];
80
+ /**
81
+ * Active shell mode. Returns `{ id: 'sh3', label: 'sh3' }` from headless
82
+ * contexts (no terminal view mounted) so callers can rely on a stable
83
+ * shape — Terminal.svelte overrides this with the live mode.
84
+ */
85
+ getMode(): {
86
+ id: string;
87
+ label: string;
88
+ };
80
89
  }
81
90
  export interface VerbContext {
82
91
  shell: ShellApi;
package/dist/version.d.ts CHANGED
@@ -1,2 +1,2 @@
1
1
  /** Auto-generated from package.json — do not edit manually. */
2
- export declare const VERSION = "0.15.0";
2
+ export declare const VERSION = "0.15.2";
package/dist/version.js CHANGED
@@ -1,2 +1,2 @@
1
1
  /** Auto-generated from package.json — do not edit manually. */
2
- export const VERSION = '0.15.0';
2
+ export const VERSION = '0.15.2';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sh3-core",
3
- "version": "0.15.0",
3
+ "version": "0.15.2",
4
4
  "type": "module",
5
5
  "files": [
6
6
  "dist"
@@ -1,8 +0,0 @@
1
- interface Props {
2
- appId: string;
3
- appLabel: string;
4
- close: () => void;
5
- }
6
- declare const AppAppearanceModal: import("svelte").Component<Props, {}, "">;
7
- type AppAppearanceModal = ReturnType<typeof AppAppearanceModal>;
8
- export default AppAppearanceModal;
@@ -1,2 +0,0 @@
1
- import type { Verb } from '../../verbs/types';
2
- export declare const catVerb: Verb;