sh3-core 0.15.1 → 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 (128) 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-appearance/appearanceShard.svelte.js +19 -6
  14. package/dist/app-appearance/appearanceState.svelte.js +3 -3
  15. package/dist/host.js +2 -1
  16. package/dist/layouts-shard/LayoutSaveModal.svelte +145 -0
  17. package/dist/layouts-shard/LayoutSaveModal.svelte.d.ts +12 -0
  18. package/dist/layouts-shard/LayoutsSection.svelte +142 -0
  19. package/dist/layouts-shard/LayoutsSection.svelte.d.ts +3 -0
  20. package/dist/layouts-shard/filter.d.ts +3 -0
  21. package/dist/layouts-shard/filter.js +66 -0
  22. package/dist/layouts-shard/filter.test.d.ts +1 -0
  23. package/dist/layouts-shard/filter.test.js +123 -0
  24. package/dist/layouts-shard/index.d.ts +1 -0
  25. package/dist/layouts-shard/index.js +1 -0
  26. package/dist/layouts-shard/layoutsApi.d.ts +12 -0
  27. package/dist/layouts-shard/layoutsApi.js +41 -0
  28. package/dist/layouts-shard/layoutsApi.test.d.ts +1 -0
  29. package/dist/layouts-shard/layoutsApi.test.js +74 -0
  30. package/dist/layouts-shard/layoutsShard.svelte.d.ts +11 -0
  31. package/dist/layouts-shard/layoutsShard.svelte.js +231 -0
  32. package/dist/layouts-shard/layoutsShard.svelte.test.d.ts +1 -0
  33. package/dist/layouts-shard/layoutsShard.svelte.test.js +215 -0
  34. package/dist/layouts-shard/layoutsState.svelte.d.ts +9 -0
  35. package/dist/layouts-shard/layoutsState.svelte.js +50 -0
  36. package/dist/layouts-shard/layoutsState.test.d.ts +1 -0
  37. package/dist/layouts-shard/layoutsState.test.js +43 -0
  38. package/dist/layouts-shard/types.d.ts +21 -0
  39. package/dist/layouts-shard/types.js +6 -0
  40. package/dist/{app-appearance/AppAppearanceModal.svelte → overlays/EntityAppearanceModal.svelte} +36 -31
  41. package/dist/overlays/EntityAppearanceModal.svelte.d.ts +19 -0
  42. package/dist/overlays/EntityAppearanceModal.test.d.ts +1 -0
  43. package/dist/overlays/EntityAppearanceModal.test.js +57 -0
  44. package/dist/overlays/FloatFrame.svelte +17 -0
  45. package/dist/overlays/float.d.ts +17 -1
  46. package/dist/overlays/float.js +16 -0
  47. package/dist/overlays/float.test.js +35 -0
  48. package/dist/sh3core-shard/ShellHome.svelte +3 -0
  49. package/dist/shards/activate.svelte.js +11 -2
  50. package/dist/shards/types.d.ts +33 -1
  51. package/dist/shell-shard/CommandLine.svelte +143 -0
  52. package/dist/shell-shard/CommandLine.svelte.d.ts +26 -0
  53. package/dist/shell-shard/CommandLine.svelte.test.d.ts +1 -0
  54. package/dist/shell-shard/CommandLine.svelte.test.js +43 -0
  55. package/dist/shell-shard/InputLine.svelte +17 -40
  56. package/dist/shell-shard/InputLine.svelte.d.ts +2 -0
  57. package/dist/shell-shard/ScrollbackView.svelte +10 -3
  58. package/dist/shell-shard/ScrollbackView.svelte.d.ts +1 -0
  59. package/dist/shell-shard/Terminal.svelte +93 -22
  60. package/dist/shell-shard/buffer-store.d.ts +15 -0
  61. package/dist/shell-shard/buffer-store.js +124 -0
  62. package/dist/shell-shard/buffer-store.svelte.test.d.ts +1 -0
  63. package/dist/shell-shard/buffer-store.svelte.test.js +107 -0
  64. package/dist/shell-shard/buffer-zone-state.svelte.d.ts +38 -0
  65. package/dist/shell-shard/buffer-zone-state.svelte.js +31 -0
  66. package/dist/shell-shard/contract.d.ts +7 -0
  67. package/dist/shell-shard/dispatch-custom.test.js +3 -1
  68. package/dist/shell-shard/dispatch-gating.test.js +6 -2
  69. package/dist/shell-shard/dispatch-invoke.test.js +10 -8
  70. package/dist/shell-shard/dispatch.d.ts +7 -2
  71. package/dist/shell-shard/dispatch.js +23 -27
  72. package/dist/shell-shard/display-cwd.d.ts +1 -0
  73. package/dist/shell-shard/display-cwd.js +27 -0
  74. package/dist/shell-shard/display-cwd.test.d.ts +1 -0
  75. package/dist/shell-shard/display-cwd.test.js +29 -0
  76. package/dist/shell-shard/entries/StatusEntry.svelte +2 -0
  77. package/dist/shell-shard/manifest.js +2 -1
  78. package/dist/shell-shard/manifest.test.d.ts +1 -0
  79. package/dist/shell-shard/manifest.test.js +8 -0
  80. package/dist/shell-shard/mode-buffer.svelte.d.ts +8 -0
  81. package/dist/shell-shard/mode-buffer.svelte.js +19 -0
  82. package/dist/shell-shard/mode-buffer.svelte.test.d.ts +1 -0
  83. package/dist/shell-shard/mode-buffer.svelte.test.js +25 -0
  84. package/dist/shell-shard/modes/builtin.js +2 -0
  85. package/dist/shell-shard/modes/types.d.ts +8 -0
  86. package/dist/shell-shard/protocol.d.ts +12 -6
  87. package/dist/shell-shard/replay.d.ts +3 -0
  88. package/dist/shell-shard/replay.js +44 -0
  89. package/dist/shell-shard/replay.svelte.test.d.ts +1 -0
  90. package/dist/shell-shard/replay.svelte.test.js +47 -0
  91. package/dist/shell-shard/rich-registry.d.ts +5 -0
  92. package/dist/shell-shard/rich-registry.js +25 -0
  93. package/dist/shell-shard/rich-registry.test.d.ts +1 -0
  94. package/dist/shell-shard/rich-registry.test.js +31 -0
  95. package/dist/shell-shard/scrollback.svelte.d.ts +2 -0
  96. package/dist/shell-shard/scrollback.svelte.js +23 -0
  97. package/dist/shell-shard/scrollback.svelte.test.d.ts +1 -0
  98. package/dist/shell-shard/scrollback.svelte.test.js +51 -0
  99. package/dist/shell-shard/session-client.svelte.d.ts +18 -2
  100. package/dist/shell-shard/session-client.svelte.js +21 -4
  101. package/dist/shell-shard/shellApi.d.ts +2 -1
  102. package/dist/shell-shard/shellApi.js +31 -3
  103. package/dist/shell-shard/shellApi.svelte.test.d.ts +1 -0
  104. package/dist/shell-shard/shellApi.svelte.test.js +59 -0
  105. package/dist/shell-shard/shellShard.svelte.js +11 -1
  106. package/dist/shell-shard/terminal-dispatch.test.js +3 -1
  107. package/dist/shell-shard/verbs/apps.js +7 -0
  108. package/dist/shell-shard/verbs/env.js +4 -0
  109. package/dist/shell-shard/verbs/help.js +4 -0
  110. package/dist/shell-shard/verbs/history.js +8 -1
  111. package/dist/shell-shard/verbs/index.js +0 -8
  112. package/dist/shell-shard/verbs/shards.js +4 -0
  113. package/dist/shell-shard/verbs/views.js +4 -0
  114. package/dist/shell-shard/verbs/zones.js +7 -0
  115. package/dist/version.d.ts +1 -1
  116. package/dist/version.js +1 -1
  117. package/package.json +1 -1
  118. package/dist/app-appearance/AppAppearanceModal.svelte.d.ts +0 -8
  119. package/dist/shell-shard/verbs/cat.d.ts +0 -2
  120. package/dist/shell-shard/verbs/cat.js +0 -35
  121. package/dist/shell-shard/verbs/cd.test.js +0 -56
  122. package/dist/shell-shard/verbs/ls.d.ts +0 -2
  123. package/dist/shell-shard/verbs/ls.js +0 -30
  124. package/dist/shell-shard/verbs/ls.test.js +0 -49
  125. package/dist/shell-shard/verbs/session.d.ts +0 -4
  126. package/dist/shell-shard/verbs/session.js +0 -99
  127. /package/dist/{shell-shard/verbs/cd.test.d.ts → actions/ctx-actions.svelte.test.d.ts} +0 -0
  128. /package/dist/{shell-shard/verbs/ls.test.d.ts → actions/listActionsFromEntries.test.d.ts} +0 -0
@@ -0,0 +1,47 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { applyReplayEvents } from './replay';
3
+ import { ModeBuffer } from './mode-buffer.svelte';
4
+ describe('applyReplayEvents', () => {
5
+ it('folds events into a fresh bash buffer like live events', () => {
6
+ const buf = new ModeBuffer('bash');
7
+ const events = [
8
+ { seq: 5, kind: 'prompt', line: 'ls', cwd: '/', ts: 100 },
9
+ { seq: 6, kind: 'stdout', data: 'a\nb\n', ts: 101 },
10
+ { seq: 7, kind: 'exit', code: 0, signal: null, ts: 102 },
11
+ ];
12
+ applyReplayEvents(buf, events);
13
+ // prompt + coalesced text — exit code 0 is silent (matches Terminal.svelte UX).
14
+ expect(buf.scrollback.entries).toHaveLength(2);
15
+ expect(buf.locked).toBe(false);
16
+ });
17
+ it('locks the buffer if replay ends with a prompt without exit', () => {
18
+ const buf = new ModeBuffer('bash');
19
+ const events = [
20
+ { seq: 10, kind: 'prompt', line: 'sleep 5', cwd: '/', ts: 200 },
21
+ ];
22
+ applyReplayEvents(buf, events);
23
+ expect(buf.locked).toBe(true);
24
+ });
25
+ it('emits a status entry on non-zero exit', () => {
26
+ const buf = new ModeBuffer('bash');
27
+ const events = [
28
+ { seq: 1, kind: 'prompt', line: 'false', cwd: '/', ts: 1 },
29
+ { seq: 2, kind: 'exit', code: 1, signal: null, ts: 2 },
30
+ ];
31
+ applyReplayEvents(buf, events);
32
+ const last = buf.scrollback.entries[buf.scrollback.entries.length - 1];
33
+ expect(last.kind).toBe('status');
34
+ if (last.kind === 'status')
35
+ expect(last.text).toMatch(/exited \(1\)/);
36
+ });
37
+ it('routes stderr to a stderr text entry', () => {
38
+ const buf = new ModeBuffer('bash');
39
+ applyReplayEvents(buf, [
40
+ { seq: 1, kind: 'stderr', data: 'oops\n', ts: 1 },
41
+ ]);
42
+ const e = buf.scrollback.entries[0];
43
+ expect(e.kind).toBe('text');
44
+ if (e.kind === 'text')
45
+ expect(e.stream).toBe('stderr');
46
+ });
47
+ });
@@ -0,0 +1,5 @@
1
+ import type { Component } from 'svelte';
2
+ export declare function registerRichComponent(key: string, component: Component<any>): void;
3
+ export declare function lookupRichComponent(key: string): Component<any> | null;
4
+ /** Test-only seam. Do not call from production code. */
5
+ export declare function __resetRichRegistryForTests(): void;
@@ -0,0 +1,25 @@
1
+ /*
2
+ * Stable string-keyed registry mapping a componentKey to a Svelte
3
+ * component. Used by ScrollbackView (live render) and BufferStore
4
+ * (rehydrate persisted rich entries).
5
+ *
6
+ * Each rich-emitting verb registers its component once at module load.
7
+ * Keys must be globally unique within the shell-shard namespace.
8
+ */
9
+ const components = new Map();
10
+ export function registerRichComponent(key, component) {
11
+ const existing = components.get(key);
12
+ if (existing && existing !== component) {
13
+ console.warn(`[shell-shard/rich-registry] duplicate registration for key "${key}" — keeping first`);
14
+ return;
15
+ }
16
+ components.set(key, component);
17
+ }
18
+ export function lookupRichComponent(key) {
19
+ var _a;
20
+ return (_a = components.get(key)) !== null && _a !== void 0 ? _a : null;
21
+ }
22
+ /** Test-only seam. Do not call from production code. */
23
+ export function __resetRichRegistryForTests() {
24
+ components.clear();
25
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,31 @@
1
+ import { describe, it, expect, beforeEach, vi } from 'vitest';
2
+ import { registerRichComponent, lookupRichComponent, __resetRichRegistryForTests, } from './rich-registry';
3
+ const FakeComp = {};
4
+ const OtherComp = {};
5
+ describe('rich-registry', () => {
6
+ beforeEach(() => {
7
+ __resetRichRegistryForTests();
8
+ });
9
+ it('returns null for unknown keys', () => {
10
+ expect(lookupRichComponent('nope')).toBe(null);
11
+ });
12
+ it('roundtrips a registered key', () => {
13
+ registerRichComponent('test', FakeComp);
14
+ expect(lookupRichComponent('test')).toBe(FakeComp);
15
+ });
16
+ it('warns and ignores duplicate registrations of a different component', () => {
17
+ const warn = vi.spyOn(console, 'warn').mockImplementation(() => { });
18
+ registerRichComponent('dup', FakeComp);
19
+ registerRichComponent('dup', OtherComp);
20
+ expect(lookupRichComponent('dup')).toBe(FakeComp);
21
+ expect(warn).toHaveBeenCalledOnce();
22
+ warn.mockRestore();
23
+ });
24
+ it('idempotent registration of the same component is silent', () => {
25
+ const warn = vi.spyOn(console, 'warn').mockImplementation(() => { });
26
+ registerRichComponent('idem', FakeComp);
27
+ registerRichComponent('idem', FakeComp);
28
+ expect(warn).not.toHaveBeenCalled();
29
+ warn.mockRestore();
30
+ });
31
+ });
@@ -15,6 +15,7 @@ export type ScrollbackEntry = {
15
15
  } | {
16
16
  kind: 'rich';
17
17
  id: string;
18
+ componentKey?: string;
18
19
  component: RichComponent;
19
20
  props: Record<string, unknown>;
20
21
  ts: number;
@@ -32,5 +33,6 @@ export declare class Scrollback {
32
33
  constructor(cap?: number);
33
34
  push(entry: DistributiveOmit<ScrollbackEntry, 'id'>): void;
34
35
  clear(): void;
36
+ restore(entries: ScrollbackEntry[]): void;
35
37
  }
36
38
  export {};
@@ -16,6 +16,14 @@ let nextId = 0;
16
16
  function mkId() {
17
17
  return `e${++nextId}`;
18
18
  }
19
+ // Parses the numeric suffix of an id minted by mkId (`e<n>`). Returns NaN
20
+ // for any other shape so restore() can ignore it without throwing.
21
+ function parseIdNumber(id) {
22
+ if (id.length < 2 || id.charCodeAt(0) !== 101 /* 'e' */)
23
+ return NaN;
24
+ const n = Number(id.slice(1));
25
+ return Number.isInteger(n) ? n : NaN;
26
+ }
19
27
  export class Scrollback {
20
28
  constructor(cap = DEFAULT_CAP) {
21
29
  this.entries = $state([]);
@@ -40,4 +48,19 @@ export class Scrollback {
40
48
  clear() {
41
49
  this.entries.length = 0;
42
50
  }
51
+ // Replace entries from a persisted snapshot. Splices in place to keep the
52
+ // $state proxy identity stable, and bumps the module-scoped id counter
53
+ // past any restored numeric id so subsequent push()es never collide with
54
+ // hydrated keys (which would crash Svelte's keyed {#each}).
55
+ restore(entries) {
56
+ let max = 0;
57
+ for (const e of entries) {
58
+ const n = parseIdNumber(e.id);
59
+ if (Number.isFinite(n) && n > max)
60
+ max = n;
61
+ }
62
+ if (max > nextId)
63
+ nextId = max;
64
+ this.entries.splice(0, this.entries.length, ...entries);
65
+ }
43
66
  }
@@ -0,0 +1 @@
1
+ export {};
@@ -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();
@@ -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,5 +1,10 @@
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.',
@@ -8,6 +13,7 @@ export const appsVerb = {
8
13
  const apps = ctx.shell.listApps();
9
14
  ctx.scrollback.push({
10
15
  kind: 'rich',
16
+ componentKey: APPS_TABLE_KEY,
11
17
  component: AppsTable,
12
18
  props: {
13
19
  data: {
@@ -44,6 +50,7 @@ export const appVerb = {
44
50
  }
45
51
  ctx.scrollback.push({
46
52
  kind: 'rich',
53
+ componentKey: APP_CARD_KEY,
47
54
  component: AppCard,
48
55
  props: { data: { id: active.id, label: active.label, shards: [] } },
49
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,5 +1,8 @@
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',
@@ -12,6 +15,7 @@ export function makeHelpVerb() {
12
15
  .map((v) => ({ name: v.name, summary: v.summary }));
13
16
  ctx.scrollback.push({
14
17
  kind: 'rich',
18
+ componentKey: HELP_TABLE_KEY,
15
19
  component: HelpTable,
16
20
  props: {
17
21
  data: {
@@ -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,4 +1,7 @@
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.',
@@ -7,6 +10,7 @@ export const shardsVerb = {
7
10
  const shards = ctx.shell.listShards();
8
11
  ctx.scrollback.push({
9
12
  kind: 'rich',
13
+ componentKey: SHARDS_TABLE_KEY,
10
14
  component: ShardsTable,
11
15
  props: { data: { shards } },
12
16
  ts: Date.now(),
@@ -1,4 +1,7 @@
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.',
@@ -28,6 +31,7 @@ export const viewsVerb = {
28
31
  const views = ctx.shell.listViewsInCurrentLayout();
29
32
  ctx.scrollback.push({
30
33
  kind: 'rich',
34
+ componentKey: VIEWS_TABLE_KEY,
31
35
  component: ViewsTable,
32
36
  props: {
33
37
  data: {
@@ -1,5 +1,10 @@
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).',
@@ -8,6 +13,7 @@ export const zonesVerb = {
8
13
  const rows = ctx.shell.listZones(args[0]);
9
14
  ctx.scrollback.push({
10
15
  kind: 'rich',
16
+ componentKey: ZONES_TABLE_KEY,
11
17
  component: ZonesTable,
12
18
  props: { data: { rows } },
13
19
  ts: Date.now(),
@@ -32,6 +38,7 @@ export const zoneVerb = {
32
38
  const value = ctx.shell.readZone(shardId, zoneName);
33
39
  ctx.scrollback.push({
34
40
  kind: 'rich',
41
+ componentKey: ZONE_TREE_KEY,
35
42
  component: ZoneTree,
36
43
  props: { data: { value } },
37
44
  ts: Date.now(),
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.1";
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.1';
2
+ export const VERSION = '0.15.2';