sh3-core 0.15.0 → 0.15.1

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.
@@ -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({
@@ -201,11 +201,16 @@ export async function activateShard(id, opts) {
201
201
  openContextMenu(opts) { shellOpenContextMenu(opts); },
202
202
  openPalette(opts) { shellOpenPalette(opts); },
203
203
  },
204
- listVerbs() {
205
- return listVerbsWithShard().map(({ verb, shardId }) => ({
204
+ listVerbs(opts) {
205
+ const all = listVerbsWithShard();
206
+ const filtered = (opts === null || opts === void 0 ? void 0 : opts.programmaticOnly)
207
+ ? all.filter(({ verb }) => verb.programmatic === true)
208
+ : all;
209
+ return filtered.map(({ verb, shardId }) => ({
206
210
  shardId,
207
211
  name: verb.name,
208
212
  summary: verb.summary,
213
+ programmatic: verb.programmatic,
209
214
  schema: verb.schema,
210
215
  }));
211
216
  },
@@ -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
  /**
@@ -143,6 +143,7 @@
143
143
  return true;
144
144
  },
145
145
  listModes: () => visibleModes.map((m) => ({ id: m.id, label: m.label })),
146
+ getMode: () => ({ id: mode.id, label: mode.label }),
146
147
  }));
147
148
 
148
149
  // wsUrl is a prop read at construction only. untrack prevents Svelte 5's
@@ -135,6 +135,7 @@ export function makeShellApiHeadless() {
135
135
  },
136
136
  setMode(_id) { return false; },
137
137
  listModes() { return []; },
138
+ getMode() { return { id: 'sh3', label: 'sh3' }; },
138
139
  };
139
140
  }
140
141
  export function makeShellApiForTest() {
@@ -3,6 +3,7 @@ import AppCard from '../rich/AppCard.svelte';
3
3
  export const appsVerb = {
4
4
  name: 'apps',
5
5
  summary: 'List installed apps. Click a row to launch.',
6
+ programmatic: true,
6
7
  async run(ctx) {
7
8
  const apps = ctx.shell.listApps();
8
9
  ctx.scrollback.push({
@@ -29,6 +30,7 @@ export const appsVerb = {
29
30
  export const appVerb = {
30
31
  name: 'app',
31
32
  summary: 'Show the currently active app.',
33
+ programmatic: true,
32
34
  async run(ctx) {
33
35
  const active = ctx.shell.getActiveApp();
34
36
  if (!active) {
@@ -1,6 +1,7 @@
1
1
  export const catVerb = {
2
2
  name: 'cat',
3
3
  summary: 'Read a file into the scrollback.',
4
+ programmatic: true,
4
5
  async run(ctx, args) {
5
6
  if (args.length === 0) {
6
7
  ctx.scrollback.push({
@@ -4,8 +4,12 @@ export function makeHelpVerb() {
4
4
  return {
5
5
  name: 'help',
6
6
  summary: 'List verbs or show detail for one.',
7
+ globalVerb: true,
7
8
  async run(ctx) {
8
- const rows = listVerbs().map((v) => ({ name: v.name, summary: v.summary }));
9
+ const inSh3 = ctx.shell.getMode().id === 'sh3';
10
+ const rows = listVerbs()
11
+ .filter((v) => inSh3 || v.globalVerb === true)
12
+ .map((v) => ({ name: v.name, summary: v.summary }));
9
13
  ctx.scrollback.push({
10
14
  kind: 'rich',
11
15
  component: HelpTable,
@@ -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,6 +1,7 @@
1
1
  export const lsVerb = {
2
2
  name: 'ls',
3
3
  summary: 'List the current directory.',
4
+ programmatic: true,
4
5
  async run(ctx) {
5
6
  let entries;
6
7
  try {
@@ -35,6 +35,7 @@ function normalizeRel(s) {
35
35
  export const pwdVerb = {
36
36
  name: 'pwd',
37
37
  summary: 'Show the current working directory.',
38
+ programmatic: true,
38
39
  async run(ctx) {
39
40
  const rel = ctx.session.cwd || '';
40
41
  const display = rel.startsWith('/') ? rel : `/${rel}`;
@@ -85,6 +86,7 @@ export const cdVerb = {
85
86
  export const whoamiVerb = {
86
87
  name: 'whoami',
87
88
  summary: 'Show the current admin user and session info.',
89
+ programmatic: true,
88
90
  async run(ctx) {
89
91
  const me = ctx.shell.whoAmI();
90
92
  ctx.scrollback.push({
@@ -2,6 +2,7 @@ import ShardsTable from '../rich/ShardsTable.svelte';
2
2
  export const shardsVerb = {
3
3
  name: 'shards',
4
4
  summary: 'List active shards.',
5
+ programmatic: true,
5
6
  async run(ctx) {
6
7
  const shards = ctx.shell.listShards();
7
8
  ctx.scrollback.push({
@@ -2,6 +2,7 @@ import ViewsTable from '../rich/ViewsTable.svelte';
2
2
  export const viewsVerb = {
3
3
  name: 'views',
4
4
  summary: 'List views currently mounted. Pass --standalone to list summonable views instead.',
5
+ programmatic: true,
5
6
  async run(ctx, args) {
6
7
  if (args.includes('--standalone')) {
7
8
  const standalones = ctx.shell.listStandaloneViews();
@@ -52,6 +53,7 @@ export const viewsVerb = {
52
53
  export const openVerb = {
53
54
  name: 'open',
54
55
  summary: 'Open a view from any active shard into the current layout.',
56
+ programmatic: true,
55
57
  async run(ctx, args) {
56
58
  var _a;
57
59
  const viewId = args[0];
@@ -86,6 +88,7 @@ export const openVerb = {
86
88
  export const popoutVerb = {
87
89
  name: 'popout',
88
90
  summary: 'Pop a docked view out into a float by slot id.',
91
+ programmatic: true,
89
92
  async run(ctx, args) {
90
93
  var _a;
91
94
  const slotId = args[0];
@@ -120,6 +123,7 @@ export const popoutVerb = {
120
123
  export const dockVerb = {
121
124
  name: 'dock',
122
125
  summary: 'Dock a float back into the current layout by float id. Run with no args to list floats.',
126
+ programmatic: true,
123
127
  async run(ctx, args) {
124
128
  var _a;
125
129
  const floatId = args[0];
@@ -166,6 +170,7 @@ export const dockVerb = {
166
170
  export const closeVerb = {
167
171
  name: 'close',
168
172
  summary: 'Close a view by slot id.',
173
+ programmatic: true,
169
174
  async run(ctx, args) {
170
175
  var _a;
171
176
  const slotId = args[0];
@@ -3,6 +3,7 @@ import ZoneTree from '../rich/ZoneTree.svelte';
3
3
  export const zonesVerb = {
4
4
  name: 'zones',
5
5
  summary: 'List zones for the current user (optionally scoped to a shard).',
6
+ programmatic: true,
6
7
  async run(ctx, args) {
7
8
  const rows = ctx.shell.listZones(args[0]);
8
9
  ctx.scrollback.push({
@@ -16,6 +17,7 @@ export const zonesVerb = {
16
17
  export const zoneVerb = {
17
18
  name: 'zone',
18
19
  summary: 'Dump the contents of a zone as a collapsible JSON tree.',
20
+ programmatic: true,
19
21
  async run(ctx, args) {
20
22
  const [shardId, zoneName] = args;
21
23
  if (!shardId || !zoneName) {
@@ -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.1";
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.1';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sh3-core",
3
- "version": "0.15.0",
3
+ "version": "0.15.1",
4
4
  "type": "module",
5
5
  "files": [
6
6
  "dist"