eleven / ocho Public

web: shellverbs.js — the behaviour seam (docs/shell-modules.md § The verbs seam) (#955)

The shellstate trick applied to behaviour: an empty registry that
shell.js fills with one Object.assign at boot (hoisted function decls),
so extracted modules call verbs.openThread(id) with no import cycle and
no per-module hook soup. Registration only; no logic in the seam.
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Paul Campbell 49a2290dbd47edd2423b1eef4b8f41eded614b37 parent 5e53f38
3 files changed, +34 −0
  • docs/shell-modules.md +15 −0
  • web/static/shell.js +10 −0
  • web/static/shellverbs.js +9 −0
diff --git a/docs/shell-modules.md b/docs/shell-modules.md
index 5d4b489..7293f01 100644
--- a/docs/shell-modules.md
+++ b/docs/shell-modules.md
@@ -38,6 +38,21 @@ So the seam is `web/static/shellstate.js`:
- Nothing else lives in shellstate.js. It is a place for shared *state*, not
shared *behaviour*; helpers keep moving into their own modules.
+## The verbs seam
+
+The state seam unblocked modules that *read*; the remaining clusters also
+*act* — they open threads, reload the list, raise modals. Those verbs can't be
+imported from shell.js (it's the entry module; importing it back is a cycle),
+and injecting them per-module scales into hook soup.
+
+So the same trick again: `web/static/shellverbs.js` exports one empty
+registry object, and shell.js populates it in a single `Object.assign` at
+boot. Modules call `verbs.openThread(id)`. Registration only — no logic in
+the seam, nothing imported by it, no cycles. When a verb's implementation
+later moves into its own module, the registry entry moves with it and no
+caller churns. (searchui.js's `wireSearch` predates this seam and stays as
+is.)
+
## Rules for a slice
1. Verbatim move — refactor in a separate PR from relocation, never both.
diff --git a/web/static/shell.js b/web/static/shell.js
index bcf2562..ba3e0e7 100644
--- a/web/static/shell.js
+++ b/web/static/shell.js
@@ -133,6 +133,7 @@ import "/static/apps/admin.js"; // the admin panel (admin-only; opened via /admi
import "/static/apps/appstudio.js"; // make + manage your own member apps (docs/member-apps.md)
import { openMemoryPanel } from "/static/apps/memory.js"; // /memory — the AI-memory scratchpad (docs/eleven-memory.md)
import { setHost as setChatAppHost, setConnected as setChatAppConnected, appendNotice, setNoticeTyping, feedGlideToBottom, scrollToMessage } from "/static/apps/chat.js";
+import { verbs } from "/static/shellverbs.js"; // the behaviour seam (docs/shell-modules.md)
import { scheduleMessageSearch, wireSearch, searchIdx } from "/static/searchui.js"; // message search over the local store (#586)
import { loadMemberApps, setMemberSeeder, setSurfaceRefresh, setMemberAdmin, recentLocalDeploy, pushTheme } from "/static/sandbox.js"; // member-built apps, run sandboxed (docs/member-apps.md)
import { THEME_META, THEME_ORDER, currentTheme, currentAppearance, setTheme, setAppearance, currentTextSize, setTextSize, onThemeApplied } from "/static/theme.js"; // applies the saved theme/text-size on import
@@ -176,6 +177,15 @@ import {
} from "/static/store.js";
import { anyComposerHasText, formatAsYouTypePref, setFormatAsYouType, classicComposerPref, setClassicComposer } from "/static/composer.js";
+// Populate the behaviour seam: the core verbs extracted modules call through
+// (function declarations hoist, so these references are valid here).
+Object.assign(verbs, {
+ openThread, loadThreads, renderThreadList, renderActiveSurfaces,
+ paintThreadHeader, modal, toast, displayName, threadAvatarFor,
+ announceJoin, announceLeave, announceRemove, sendChatText,
+ loadBlobBytes, chatAppFor, chatAppLoadOlder, stashAppPane,
+});
+
onThemeApplied(pushTheme); // live-retint mounted sandbox frames on theme change
wireSearch({ openThread, chatAppFor, chatAppLoadOlder }); // navigation + thread predicate + scroll-back paging
diff --git a/web/static/shellverbs.js b/web/static/shellverbs.js
new file mode 100644
index 0000000..5ab4178
--- /dev/null
+++ b/web/static/shellverbs.js
@@ -0,0 +1,9 @@
+// The shell's behaviour seam (docs/shell-modules.md § The verbs seam) — the
+// shellstate.js idea applied to the core verbs. This registry starts empty;
+// shell.js populates it in one Object.assign at boot (function declarations
+// hoist, so the assignment can sit right after the imports). Extracted
+// modules call verbs.openThread(id) etc. Registration only — no logic lives
+// here, nothing is imported here, so there is never an import cycle. When a
+// verb's implementation later gets its own module home, the registry entry
+// follows it and callers don't churn.
+export const verbs = {};