@typetorch/framework 0.5.5 → 0.5.7

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 (35) hide show
  1. package/README.md +102 -15
  2. package/out/devtools/admin-ui.d.ts +22 -0
  3. package/out/devtools/admin-ui.luau +108 -1
  4. package/out/devtools/build-actions.d.ts +14 -0
  5. package/out/devtools/build-actions.luau +25 -0
  6. package/out/devtools/claude-ui.d.ts +5 -0
  7. package/out/devtools/claude-ui.luau +37 -7
  8. package/out/devtools/client.d.ts +5 -0
  9. package/out/devtools/client.luau +148 -43
  10. package/out/devtools/commands.d.ts +115 -0
  11. package/out/devtools/commands.luau +504 -0
  12. package/out/devtools/layout.d.ts +11 -0
  13. package/out/devtools/layout.luau +22 -0
  14. package/out/devtools/on-demand.d.ts +6 -0
  15. package/out/devtools/on-demand.luau +12 -0
  16. package/out/devtools/palette-commands.d.ts +55 -0
  17. package/out/devtools/palette-commands.luau +667 -0
  18. package/out/devtools/palette-rules.d.ts +57 -0
  19. package/out/devtools/palette-rules.luau +103 -0
  20. package/out/devtools/palette.d.ts +26 -0
  21. package/out/devtools/palette.luau +469 -0
  22. package/out/devtools/panes.d.ts +21 -1
  23. package/out/devtools/panes.luau +111 -14
  24. package/out/runtime/dev-gui.d.ts +8 -0
  25. package/out/runtime/dev-gui.luau +29 -0
  26. package/out/runtime/devtools-loader.d.ts +6 -0
  27. package/out/runtime/devtools-loader.luau +227 -0
  28. package/out/runtime/start.luau +7 -6
  29. package/out/servers.d.ts +2 -2
  30. package/out/servers.luau +2 -2
  31. package/out/version.d.ts +1 -1
  32. package/out/version.luau +1 -1
  33. package/package.json +1 -1
  34. /package/out/{devtools/roll-call.d.ts → roll-call.d.ts} +0 -0
  35. /package/out/{devtools/roll-call.luau → roll-call.luau} +0 -0
package/README.md CHANGED
@@ -9,6 +9,21 @@ It ships **inside every artifact**, so each generation runs its own fresh copy a
9
9
  code. It needs the TypeTorch kernel in the place (`@typetorch/kernel`), which calls your `Server/boot` and
10
10
  `Client/boot` modules.
11
11
 
12
+ ### How it fits with the other TypeTorch packages
13
+
14
+ | Package | Role | Where it runs |
15
+ |---|---|---|
16
+ | `@typetorch/kernel` | Baked into the place. Picks the branch, loads the payload, calls `boot(kernel)`, swaps generations, owns the stable remotes, `persist`, roles, settings and the health window. | the place (server and client) |
17
+ | `@typetorch/framework` (this) | Modules, DI, lifecycle, troves, network, hot assets, analytics and the dev menu, on top of the kernel API (`src/kernel.ts` types it). | inside every payload |
18
+ | `@typetorch/transformer` | roblox-ts compiler plugin: constructor dependency ids, network guards, `Dependency<T>()` / `Lazy<T>` / `TypeTorch.module<T>()` ids and user macros. Its `runtime-kit/` is copied here as `src/reflection/`. | your compile (Node) |
19
+ | `@typetorch/cli` (`typetorch`, alias `tt`) | Builds the payload (`typetorch build`), deploys and hot-swaps it, rolls back, syncs hot assets, writes the signed settings (`liveConfig`, analytics, experiments), migrates Flamework games. | your machine / CI |
20
+ | `@typetorch/dev-server` | `remote-claude`: the dev menu's Claude tab and Logs > Upload talk to it. | a dev's machine |
21
+
22
+ The framework never talks to the CLI directly: the CLI builds and signs payloads and settings, the kernel loads them,
23
+ and the framework reads what the kernel hands it. Features that need a newer kernel check `TypeTorch.features` and say
24
+ "needs kernel x.y.z" on older ones; the dev menu flags kernels older than `LATEST_KERNEL` (`src/devtools/health.ts`,
25
+ now 0.5.3) with a Migrate issue.
26
+
12
27
  ## Install
13
28
 
14
29
  ```sh
@@ -84,7 +99,7 @@ mapping.
84
99
  ```ts
85
100
  // src/server/boot.ts
86
101
  import { startServer, type ServerKernel } from "@typetorch/framework";
87
- import { BUILD } from "../shared/build";
102
+ import { BUILD } from "../shared/build"; // written by `typetorch build`
88
103
  export function boot(kernel: ServerKernel) {
89
104
  return startServer(kernel, { modules: [script.Parent!.FindFirstChild("services")!], build: BUILD });
90
105
  }
@@ -96,6 +111,10 @@ interface ServerToClient { coins: { changed(total: number): void } }
96
111
  export const network = createNetwork<ClientToServer, ServerToClient>();
97
112
 
98
113
  // src/server/services/coin.service.ts
114
+ import { Module, Service, type OnStart } from "@typetorch/framework";
115
+ import { network } from "../../shared/net";
116
+ import { ScoreService } from "./score.service";
117
+
99
118
  @Service()
100
119
  export class CoinService extends Module implements OnStart {
101
120
  constructor(private readonly score: ScoreService) { // injected
@@ -103,13 +122,22 @@ export class CoinService extends Module implements OnStart {
103
122
  }
104
123
  onStart() {
105
124
  this.trove.add(network.server.coins.collect.on((player, coinId) => this.score.add(player, 1)));
125
+ this.trove.add(network.server.coins.balance.handle((player) => [this.score.get(player)]));
106
126
  }
107
127
  }
108
128
  ```
109
129
 
130
+ `startClient(kernel, options)` is the same for `src/client/boot.ts` with your controllers folder. `StartOptions`:
131
+ `modules` (folders whose ModuleScripts are required, recursively, so their decorators register), `build` (the
132
+ generated build info, shown in the dev menu) and `devtools` (default `true`; `false` leaves the dev menu out). Both
133
+ return the generation's stop function, which the kernel calls on a swap.
134
+
110
135
  - **Modules:** `@Service()` (server) and `@Controller()` (client) classes extending `Module`. Constructor parameters
111
136
  are other modules, injected by type. Lifecycle: `OnInit` (sequential, dependencies first), `OnStart` (spawned),
112
137
  `OnStop` (reverse order), `OnTick`, `OnPhysics`, `OnRender`, `OnPlayerAdded` (replays players already in the server).
138
+ `@Service({ loadOrder: -1 })` breaks ties between modules with no dependency relation (lower first, default 0).
139
+ `this.ctx` has the realm, artifact, branch, channel, generation, build info, the raw kernel, `persist` and
140
+ `playerState`. `observePlayers(trove, (player, playerTrove) => ...)` is `OnPlayerAdded` for code outside a module.
113
141
  - **Modules outside the constructor:** `Dependency<T>()` (or `TypeTorch.module<T>()`) returns the running module of
114
142
  type T from anywhere: a method, a plain class a module built, a command handler. `TypeTorch.tryModule<T>()` returns
115
143
  undefined instead of throwing. They work once every module is constructed (from onInit on); in a constructor, a
@@ -157,10 +185,25 @@ export class CoinService extends Module implements OnStart {
157
185
  build picker (Server > Branch) and a Claude prompt. The sidebar, top to bottom: Artifact, Server, Network, Modules,
158
186
  Manage, Logs, Dex, Claude. The window can be dragged by its header and resized from its corner (double-tap the
159
187
  header to reset).
188
+ - **Command palette (`Ctrl+K`, `Cmd+K` on a Mac, or the search button in the menu header):** opens the dev menu if it
189
+ is closed and searches every command the dev may run now (fuzzy: `swb` finds *Switch branch*). Up / Down pick,
190
+ Enter runs, Shift+Enter opens a page in a split, Escape closes; recent commands come first on an empty query. Never
191
+ fires while typing in chat or a text box. Commands: open any page; split right / down, open the pane in a new window,
192
+ close it, dock all windows; Reload, Roll back, switch branch, load a build here, join a server on a branch or build
193
+ (the Branch tab's rules and words, `devtools/build-actions.ts`), New server and Join a listed server (owners); find a
194
+ player by name, display name or UserId and open them in Manage > Players or their Kick / Ban card (owners); copy the
195
+ job id, artifact id, commit, branch or place version (the copy popup); "Ask Claude: ..." puts the query in the Claude
196
+ draft. Anything that swaps what runs on the server takes a second Enter or tap to confirm. The kernel and the server
197
+ re-check every request. Core: `devtools/commands.ts` (pure, Lune-tested by `scripts/test-commands.luau`).
160
198
  - **Server TPS (framework 0.4.2, kernel 0.4.2):** Server > Status has a *TPS* line next to Memory and Lua heap: the
161
199
  last minute's average, its slowest second and the physics FPS (`59.9 avg, 52.0 min, physics 60`; the warning colour
162
200
  under 50 on average). Older kernels show `needs kernel 0.4.2`. The same numbers ride on fleet heartbeats (the
163
201
  explorer's fleet table).
202
+ - **Devtools reach devs only (kernel 0.5.6+):** the kernel leaves `devtools` off the shared client tree of a framework
203
+ with `devtools/on-demand.ts`. `runtime/devtools-loader.ts` then clones the folder, minus the server-only modules, into
204
+ each dev's PlayerGui (join, each generation, dev status changes), and that client copies it into its own tree and
205
+ starts it. On older kernels the folder stays in the tree as before. A failure costs a dev the menu, with a warning, and
206
+ never the generation.
164
207
  - **Dev-only tools (framework 0.4.1, `devtools/access.ts`):** Claude, Logs > Upload, Dex edits on the server, Network
165
208
  packet blocking and the hot-swap sound work under dev rules (private, reserved and Studio servers on a dev branch)
166
209
  for every dev, and on a public server an owner switched to a dev branch (Server > Branch) for owners only. Everywhere
@@ -198,10 +241,19 @@ export class CoinService extends Module implements OnStart {
198
241
  - **Owners and devs:** two roles (no admins: an older kernel's or typetorch.json's "admin" counts as a dev). The Manage
199
242
  group (Players, Servers, Bans) is for owners only.
200
243
  - **Remote Claude** (a dev-only tool: dev rules, or owners on a public server an owner switched to a dev branch):
201
- while `typetorch remote-claude` (`@typetorch/dev-server`) runs on a dev's machine, allowlisted devs prompt Claude
202
- Code from the Claude tab. Each dev pairs once by pasting the pairing code printed by
244
+ while `typetorch remote-claude` (alias `typetorch dev`; it runs `@typetorch/dev-server`) runs on a dev's machine,
245
+ allowlisted devs prompt Claude Code from the Claude tab. The allowlist is `--users`; without it the dev-server uses
246
+ the owners in the game's `typetorch.json`. Each dev pairs once by pasting the pairing code printed by
203
247
  typetorch-dev-server; the game server keeps the session URL and the tokens in memory and never sends them to clients.
204
- No Roblox secret is needed.
248
+ No Roblox secret is needed. (The Studio plugin's Claude tab, with Apply / Deploy to dev / Discard, is a separate
249
+ client of the same dev-server, started by `typetorch serve --claude`; it doesn't go through the framework.)
250
+ - **Remote debug (framework 0.5.0, kernel 0.5.0, `devtools/remote-debug.ts`):** owners can read a fleet server from
251
+ the backend's explorer (`/servers/<JobId>`). The kernel polls the backend, checks the op, the rate and that the
252
+ caller is an owner, then hands the framework's ops to the handler installed through `kernel.onRemoteDebug`. Read-only,
253
+ with the dev menu's own readers and limits: `modules`, `state` (Modules > State queries, rate-limited per caller),
254
+ `assets` (the last hot-asset sync), `network` (per-remote counters, no packet capture), `dex.children` (8 nodes, 200
255
+ rows each at most) and `dex.props`. There are no edit ops. Older kernels don't have the hook, so nothing is
256
+ installed. It is part of the devtools server, so `devtools: false` leaves it out too.
205
257
 
206
258
  ## Runtime API: `TypeTorch`
207
259
 
@@ -358,10 +410,15 @@ rebuild(shop.get());
358
410
  - **AssetSync** is a server built-in. It runs on every generation start, before any module loads (top-level code
359
411
  included). For each manifest key:
360
412
  1. it keeps the live copy that already has the manifest's version;
361
- 2. else it adopts the place's own copy, if its `TypeTorchAssetHash` matches (or if the manifest's optional
362
- `placeVersion` is this server's place version);
363
- 3. else it runs `InsertService:LoadAssetVersion(ver)` (all loads in parallel), strips any scripts, places the copy
364
- at its path (creating missing Folders), tags it, then destroys the copy it replaces.
413
+ 2. else it adopts the place's own copy, if its `TypeTorchAssetHash` matches, or if the place holds exactly the
414
+ copies the manifest was exported from: the manifest's optional `placeVersion` is this server's place version, or
415
+ the place's backup build (`ServerStorage.TypeTorchBackup`, attributes `HotAssetsPlaceVersion` /
416
+ `HotAssetsSavedAs`) vouches that this place version is a save of that one which changed nothing outside the
417
+ kernel slots (the CLI's backup refresh);
418
+ 3. else it runs `InsertService:LoadAssetVersion(ver)` (all loads in parallel), strips any scripts, gives back the
419
+ CollectionService tags the place's copy has (uploaded versions arrive without them, so a game that finds its UI
420
+ by tag keeps working), places the copy at its path (creating missing Folders), tags it, then destroys the copy it
421
+ replaces.
365
422
 
366
423
  It holds the start for at most 8 s. Loads still running after that keep going, swap in when done and fire
367
424
  `changed`. A failed load keeps the old copy (on a new server, the place's copy) and shows under Server > Status >
@@ -373,7 +430,8 @@ rebuild(shop.get());
373
430
  `TypeTorchAssetPath` (stamped by AssetSync) is the live one. So a template cloned into PlayerGui never shows up in
374
431
  `get()`, and AssetSync never destroys it.
375
432
  - **Dev menu:** Modules > Assets (Server | Client) lists each key's source (kept, baked, loaded, failed), version,
376
- load time and last error.
433
+ load time and last error. With analytics on, each generation also sends one `tech` / `assets` row (the place match
434
+ and the counts per source; `src/analytics/SCHEMA.md`).
377
435
 
378
436
  ## Analytics: `AnalyticsEngine`
379
437
 
@@ -420,7 +478,9 @@ const variant = analytics.experiment("onboarding", ["short", "long"]); // same a
420
478
  - **Experiments:** `experiment(...)` is deterministic per player and name (they keep their variant in every session),
421
479
  may yield briefly the first time (until the player's id loads), and stamps the variant on the player's later events.
422
480
  The settings turn one off (`active: false`: everyone gets the first variant), sets `weights` or forces a
423
- `variant`, live.
481
+ `variant`, live. `players` (framework 0.5.3+) forces a variant for single players, keyed by UserId or pid, over
482
+ everything else (QA, support); a client only ever gets its own entry. The CLI writes these with `typetorch
483
+ experiment weights | settle | stop | resume | clear | force | unforce`.
424
484
  - **First-ever session:** for new players (a share of them, `recordShare`), the client records character and camera
425
485
  about 10 times a second, every input (never while a TextBox or the chat has focus; text boxes only say which box was
426
486
  used), buttons pressed and hovered, screens, prompts and deaths, from the join until 60 s after the first input.
@@ -493,8 +553,11 @@ bun install
493
553
  bun run build # rbxtsc --type package -> out/
494
554
  ```
495
555
 
496
- - **Runtime of the transformer:** `src/reflection/` (`Reflect`, `Modding`, `t`) is a copy of
497
- `@typetorch/transformer`'s `runtime-kit/`; keep the two identical.
556
+ - **Runtime of the transformer:** `src/reflection/reflect.ts` and `modding.ts` are copies of
557
+ `@typetorch/transformer`'s `runtime-kit/` (the source of truth); keep them identical. The framework compiles itself
558
+ without the transformer (only `rbxts-transform-debug`): it declares macros, which expand in the game's compile.
559
+ - **Versions:** bump `src/version.ts` (`FRAMEWORK_VERSION`, shown in uploaded log headers) with package.json, and
560
+ `LATEST_KERNEL` in `src/devtools/health.ts` when the framework starts relying on a newer kernel.
498
561
  - **Tests (Lune, offline):** `scripts/test-*.luau`; each file's header has its command (Lune is pinned in the
499
562
  kernel's and the template's `rokit.toml`). `scripts/test-generations.luau` takes a game's built payload:
500
563
  `cd ../template && bun run payload && lune run ../framework/scripts/test-generations.luau build/payload.rbxm` boots
@@ -522,13 +585,37 @@ bun run build # rbxtsc --type package -> out/
522
585
  retries, a NetFail streak and its one log line a minute, a swap with a request in flight, shutdown; the recorder,
523
586
  screens, batching). `scripts/test-net-health.luau` checks the dev menu's Fleet API / Analytics status lines and
524
587
  Attention issues.
525
- - **Publishing:** `npm publish` runs `prepublishOnly` (clean + build). The package ships only `out/` (no
526
- `.tsbuildinfo`), `README.md` and `LICENSE`; check with `bun pm pack --dry-run`.
527
-
588
+ The others, each on the compiled `out/`: `test-hot-assets` (manifest parser, AssetSync decisions incl. the backup
589
+ vouch, live-copy rules), `test-messaging` (messaging and the server list), `test-settings` (`liveConfig` and the
590
+ settings lines), `test-detached` (`runDetached`), `test-dev-access` (the dev-only tools rule and every gate through
591
+ it), `test-remote-debug` (the remote debug ops and their limits), `test-net-inspect` (packet capture),
592
+ `test-explorer-core` (Dex), `test-toolbox-sanitize` (the Creator Store sanitizer) and `test-sha256` (the pairing-code
593
+ fingerprint). `node scripts/check-self-calls.mjs out` flags compiled functions that call themselves on their first
594
+ lines (after `bun run build`).
595
+ - **Package contents:** only `out/` (no `.tsbuildinfo`), `README.md` and `LICENSE`; check with `bun pm pack --dry-run`.
528
596
  - **Explorer class icons:** `assets/class-icons.png` (kept locally, not in the repo: it is Roblox's own texture) is
529
597
  the client's `content/textures/ClassImages.PNG` (a 2352x16 strip) repacked into a 32-column grid, because live
530
598
  clients downscale textures wider than 1024 px:
531
599
  `ffmpeg -i ClassImages.PNG -vf "untile=147x1,tile=32x5:color=0x00000000" class-icons.png`. It is uploaded as image
532
600
  `rbxassetid://97389585475400`; `scripts/gen-explorer-icons.ts` generates the index table.
533
601
 
602
+ ## Release
603
+
604
+ Publishing is tag-only, from CI (`.github/workflows/publish.yml`), with npm trusted publishing (OIDC, provenance):
605
+
606
+ ```sh
607
+ # first commit src/version.ts set to the new version (npm version needs a clean tree)
608
+ npm version 0.5.6 -m "release %s"
609
+ git push origin main --follow-tags
610
+ ```
611
+
612
+ The workflow runs on a `v*` tag, checks that the tag matches package.json and that the version isn't on npm yet, then
613
+ `npm publish --provenance` (which runs `prepublishOnly`: a clean `rbxtsc --type package` into `out/`) and creates the
614
+ GitHub release with generated notes. A prerelease version (`x.y.z-next.0`) goes to the `next` dist-tag and a GitHub
615
+ prerelease; anything else to `latest`. Trusted publishing is set on npmjs.com (@typetorch/framework > Settings >
616
+ Trusted publishing: this repo, workflow `publish.yml`, environment `npm`); an `NPM_TOKEN` repo secret is the fallback.
617
+ Don't run `npm publish` by hand. Games pick the new version up with `bun update @typetorch/framework` and their next
618
+ deploy, since the framework ships inside the payload.
619
+
620
+
534
621
  MIT licensed.
@@ -15,6 +15,28 @@ export interface AdminDeps {
15
15
  ok: boolean,
16
16
  result: unknown
17
17
  ];
18
+ /** The command palette's player lookup (client.ts): who Manage > Players shows next. */
19
+ playerFocus?: PlayerFocus;
20
+ }
21
+ /** What the command palette asked Manage > Players for: show this player, and open Kick or Ban for them. */
22
+ export interface PlayerFocusRequest {
23
+ userId: number;
24
+ action?: "kick" | "ban";
25
+ at: number;
26
+ }
27
+ /**
28
+ * The command palette's way into Manage > Players (one per client generation, client.ts): `request` remembers a player
29
+ * and tells an open Players tab; a tab that mounts later takes it after its first load. The Kick and Ban cards are the
30
+ * tab's own (reason, duration, armed button); the server re-checks every action.
31
+ */
32
+ export declare class PlayerFocus {
33
+ private pending?;
34
+ private readonly listeners;
35
+ request(userId: number, action?: "kick" | "ban"): void;
36
+ /** The waiting request (dropped after FOCUS_WAIT s), taken once. */
37
+ take(): PlayerFocusRequest | undefined;
38
+ waiting(): boolean;
39
+ listen(trove: Trove, listener: () => void): void;
18
40
  }
19
41
  /** The Manage sub-tabs, keyed like client.ts RENDER ("Tab/Sub"). */
20
42
  export declare function adminTabs(deps: AdminDeps): Record<string, (tab: AdminTab) => void>;
@@ -115,6 +115,64 @@ local ERRORS = {
115
115
  publish_failed = "Couldn't send",
116
116
  ["not a dev"] = "Not a dev",
117
117
  }
118
+ --* How long a palette request waits for Manage > Players to load (seconds).
119
+ local FOCUS_WAIT = 15
120
+ --* What the command palette asked Manage > Players for: show this player, and open Kick or Ban for them.
121
+ --[[
122
+ *
123
+ * The command palette's way into Manage > Players (one per client generation, client.ts): `request` remembers a player
124
+ * and tells an open Players tab; a tab that mounts later takes it after its first load. The Kick and Ban cards are the
125
+ * tab's own (reason, duration, armed button); the server re-checks every action.
126
+
127
+ ]]
128
+ local PlayerFocus
129
+ do
130
+ PlayerFocus = setmetatable({}, {
131
+ __tostring = function()
132
+ return "PlayerFocus"
133
+ end,
134
+ })
135
+ PlayerFocus.__index = PlayerFocus
136
+ function PlayerFocus.new(...)
137
+ local self = setmetatable({}, PlayerFocus)
138
+ return self:constructor(...) or self
139
+ end
140
+ function PlayerFocus:constructor()
141
+ self.listeners = {}
142
+ end
143
+ function PlayerFocus:request(userId, action)
144
+ self.pending = {
145
+ userId = userId,
146
+ action = action,
147
+ at = os.clock(),
148
+ }
149
+ for listener in self.listeners do
150
+ task.spawn(listener)
151
+ end
152
+ end
153
+ function PlayerFocus:take()
154
+ local pending = self.pending
155
+ self.pending = nil
156
+ return if pending ~= nil and os.clock() - pending.at <= FOCUS_WAIT then pending else nil
157
+ end
158
+ function PlayerFocus:waiting()
159
+ return self.pending ~= nil
160
+ end
161
+ function PlayerFocus:listen(trove, listener)
162
+ local _listeners = self.listeners
163
+ local _listener = listener
164
+ _listeners[_listener] = true
165
+ trove:add(function()
166
+ local _listeners_1 = self.listeners
167
+ local _listener_1 = listener
168
+ -- ▼ Set.delete ▼
169
+ local _valueExisted = _listeners_1[_listener_1] ~= nil
170
+ _listeners_1[_listener_1] = nil
171
+ -- ▲ Set.delete ▲
172
+ return _valueExisted
173
+ end)
174
+ end
175
+ end
118
176
  --* The Manage sub-tabs, keyed like client.ts RENDER ("Tab/Sub").
119
177
  local renderPlayers, renderServers, renderBans
120
178
  local function adminTabs(deps)
@@ -140,7 +198,7 @@ local function every(trove, interval, callback)
140
198
  while true do
141
199
  local ok, err = pcall(callback)
142
200
  if not ok then
143
- warn("[src/devtools/admin-ui.ts:162]", `[devtools] manage refresh failed: {err}`)
201
+ warn("[src/devtools/admin-ui.ts:205]", `[devtools] manage refresh failed: {err}`)
144
202
  end
145
203
  task.wait(interval)
146
204
  end
@@ -786,6 +844,7 @@ function renderPlayers(tab, deps)
786
844
  local list = page:group(4)
787
845
  local rows = {}
788
846
  local busy = false
847
+ local loaded = false
789
848
  local act = function(label, op, userId, doneText)
790
849
  if busy then
791
850
  return nil
@@ -1072,6 +1131,47 @@ function renderPlayers(tab, deps)
1072
1131
  frame.Parent = list.frame
1073
1132
  return row
1074
1133
  end
1134
+ -- The command palette's player lookup: scroll to the player and flash their row, or open Kick / Ban for them.
1135
+ local applyFocus = function()
1136
+ local focus = deps.playerFocus
1137
+ if not focus or not loaded or not focus:waiting() then
1138
+ return nil
1139
+ end
1140
+ local want = focus:take()
1141
+ if not want then
1142
+ return nil
1143
+ end
1144
+ local _userId = want.userId
1145
+ local row = rows[_userId]
1146
+ if not row then
1147
+ setResult("Not in this server", COLORS.warn)
1148
+ return nil
1149
+ end
1150
+ local content = tab.content
1151
+ local top = row.frame.AbsolutePosition.Y - content.AbsolutePosition.Y + content.CanvasPosition.Y
1152
+ content.CanvasPosition = Vector2.new(0, math.max(0, top - 8))
1153
+ row.frame.BackgroundColor3 = COLORS.button
1154
+ bump(row.frame)
1155
+ trove:add(task.delay(1.5, function()
1156
+ if row.frame.Parent then
1157
+ row.frame.BackgroundColor3 = COLORS.row
1158
+ end
1159
+ end))
1160
+ local data = row.data
1161
+ if want.action == "kick" then
1162
+ if data.can.kick then
1163
+ openKick(data)
1164
+ else
1165
+ setResult(`Can't kick {data.displayName}`, COLORS.bad)
1166
+ end
1167
+ elseif want.action == "ban" then
1168
+ if data.can.ban then
1169
+ openBan(data)
1170
+ else
1171
+ setResult(`Can't ban {data.displayName}`, COLORS.bad)
1172
+ end
1173
+ end
1174
+ end
1075
1175
  local refresh = function()
1076
1176
  local _binding_1 = deps.call("admin.players")
1077
1177
  local ok = _binding_1[1]
@@ -1137,6 +1237,12 @@ function renderPlayers(tab, deps)
1137
1237
  row.frame:Destroy()
1138
1238
  rows[userId] = nil
1139
1239
  end
1240
+ loaded = true
1241
+ applyFocus()
1242
+ end
1243
+ local _result = deps.playerFocus
1244
+ if _result ~= nil then
1245
+ _result:listen(trove, applyFocus)
1140
1246
  end
1141
1247
  every(trove, PLAYERS_REFRESH, refresh)
1142
1248
  end
@@ -2656,4 +2762,5 @@ end
2656
2762
  return {
2657
2763
  adminTabs = adminTabs,
2658
2764
  migrateControl = migrateControl,
2765
+ PlayerFocus = PlayerFocus,
2659
2766
  }
@@ -38,6 +38,20 @@ export declare function buildAction(ctx: PickerContext, entry: {
38
38
  channel: string;
39
39
  }): BuildAction;
40
40
  export declare function buildLabel(action: BuildAction): string;
41
+ /**
42
+ * The "pin" op's payload for a build the viewer may pin (`buildAction` "pin"): an A/B experiment pin when an owner pins
43
+ * on an unsigned public server whose kernel has experiments (0.2.3+), else the plain asset id. The Branch tab and the
44
+ * command palette both send this.
45
+ */
46
+ export declare function pinPayload(ctx: PickerContext, assetId: number): number | {
47
+ assetId: number;
48
+ experiment: true;
49
+ };
50
+ /**
51
+ * Whether an action swaps what runs on THIS server (every player on it): the owner's switch or load ("here"), the stored
52
+ * switch and the dev pin. "join" moves only you. The command palette makes all of them two-step.
53
+ */
54
+ export declare function swapsServer(action: BranchAction | BuildAction): boolean;
41
55
  /** Kernels before 0.3.4 on prod servers: in-game loads can't reach them, only `typetorch pin`. */
42
56
  export declare function showCliNote(ctx: PickerContext): boolean;
43
57
  /** Whether this client's player is an owner (cosmetic: the kernel re-checks). An older kernel's "admin" is a dev. */
@@ -63,6 +63,29 @@ end
63
63
  local function buildLabel(action)
64
64
  return if action == "join" then "Join" else "Load here"
65
65
  end
66
+ --[[
67
+ *
68
+ * The "pin" op's payload for a build the viewer may pin (`buildAction` "pin"): an A/B experiment pin when an owner pins
69
+ * on an unsigned public server whose kernel has experiments (0.2.3+), else the plain asset id. The Branch tab and the
70
+ * command palette both send this.
71
+
72
+ ]]
73
+ local function pinPayload(ctx, assetId)
74
+ local abInPlace = ctx.serverType == "public" and ctx.isOwner and ctx.experiments and not ctx.signedOnly
75
+ return if abInPlace then {
76
+ assetId = assetId,
77
+ experiment = true,
78
+ } else assetId
79
+ end
80
+ --[[
81
+ *
82
+ * Whether an action swaps what runs on THIS server (every player on it): the owner's switch or load ("here"), the stored
83
+ * switch and the dev pin. "join" moves only you. The command palette makes all of them two-step.
84
+
85
+ ]]
86
+ local function swapsServer(action)
87
+ return action == "here" or action == "stored" or action == "pin"
88
+ end
66
89
  --* Kernels before 0.3.4 on prod servers: in-game loads can't reach them, only `typetorch pin`.
67
90
  local function showCliNote(ctx)
68
91
  return not ctx.switchKernel and ctx.signedOnly and (ctx.isOwner or ctx.serverType ~= "public")
@@ -255,6 +278,8 @@ return {
255
278
  branchLabel = branchLabel,
256
279
  buildAction = buildAction,
257
280
  buildLabel = buildLabel,
281
+ pinPayload = pinPayload,
282
+ swapsServer = swapsServer,
258
283
  showCliNote = showCliNote,
259
284
  viewerIsOwner = viewerIsOwner,
260
285
  switchOffered = switchOffered,
@@ -45,5 +45,10 @@ type Block = {
45
45
  export declare function parseMarkdown(text: string): Block[];
46
46
  /** Inline Markdown → RichText: escapes &, < and > first, then `code`, links, **bold** and *italic*. */
47
47
  export declare function inline(text: string): string;
48
+ /**
49
+ * The command palette's "Ask Claude": replaces the draft with `text` (kept in the persist store like typing) and, when
50
+ * the Claude tab is open, shows it in its message box with the caret at the end. Nothing is sent.
51
+ */
52
+ export declare function setClaudeDraft(kernel: ClientKernel, text: string): void;
48
53
  export declare function renderClaudeChat(tab: ClaudeChatTab, deps: ClaudeChatDeps): void;
49
54
  export {};
@@ -1065,19 +1065,37 @@ local function readableDetail(text)
1065
1065
  return if #out > DETAIL_CHARS then `{string.sub(out, 1, DETAIL_CHARS)}\n...` else out
1066
1066
  end
1067
1067
  -- The tab -------------------------------------------------------------------------------------------------------------
1068
- local buildClaudeChat
1069
- local function renderClaudeChat(tab, deps)
1070
- local _binding = deps
1071
- local kernel = _binding.kernel
1072
- local call = _binding.call
1073
- local trove = tab.trove
1074
- local state = kernel:persist(PERSIST_KEY, function()
1068
+ local function chatState(kernel)
1069
+ return kernel:persist(PERSIST_KEY, function()
1075
1070
  return {
1076
1071
  draft = "",
1077
1072
  attachPath = false,
1078
1073
  attachErrors = false,
1079
1074
  }
1080
1075
  end)
1076
+ end
1077
+ --* The open Claude tab's message box (one at most: Claude is a single page), for setClaudeDraft.
1078
+ local showDraft
1079
+ --[[
1080
+ *
1081
+ * The command palette's "Ask Claude": replaces the draft with `text` (kept in the persist store like typing) and, when
1082
+ * the Claude tab is open, shows it in its message box with the caret at the end. Nothing is sent.
1083
+
1084
+ ]]
1085
+ local function setClaudeDraft(kernel, text)
1086
+ chatState(kernel).draft = text
1087
+ local _result = showDraft
1088
+ if _result ~= nil then
1089
+ _result(text)
1090
+ end
1091
+ end
1092
+ local buildClaudeChat
1093
+ local function renderClaudeChat(tab, deps)
1094
+ local _binding = deps
1095
+ local kernel = _binding.kernel
1096
+ local call = _binding.call
1097
+ local trove = tab.trove
1098
+ local state = chatState(kernel)
1081
1099
  -- The chat takes the content's place in the window body (like the explorer).
1082
1100
  local content = tab.content
1083
1101
  local host = trove:add(make("Frame", {
@@ -1549,6 +1567,17 @@ function buildClaudeChat(tab, deps, host, state)
1549
1567
  state.draft = text
1550
1568
  end)
1551
1569
  fitBox()
1570
+ local myShowDraft = function(text)
1571
+ box.Text = text
1572
+ box:CaptureFocus()
1573
+ box.CursorPosition = #text + 1
1574
+ end
1575
+ showDraft = myShowDraft
1576
+ trove:add(function()
1577
+ if showDraft == myShowDraft then
1578
+ showDraft = nil
1579
+ end
1580
+ end)
1552
1581
  -- The actions row draws above the message box (the "+" menu hangs over it).
1553
1582
  local actions = make("Frame", {
1554
1583
  BackgroundTransparency = 1,
@@ -4595,5 +4624,6 @@ return {
4595
4624
  stripEmoji = stripEmoji,
4596
4625
  parseMarkdown = parseMarkdown,
4597
4626
  inline = inline,
4627
+ setClaudeDraft = setClaudeDraft,
4598
4628
  renderClaudeChat = renderClaudeChat,
4599
4629
  }
@@ -5,4 +5,9 @@ export interface DevtoolsClient {
5
5
  /** Call once the client generation is up (all modules started). */
6
6
  readonly started: () => void;
7
7
  }
8
+ /**
9
+ * Starts the dev menu for this client generation. Everything it creates or connects lives in `trove`, so a swap
10
+ * removes it; the open state, tab, sub-tab, window rectangle and options survive the swap through the kernel persist
11
+ * store.
12
+ */
8
13
  export declare function startDevtoolsClient(kernel: ClientKernel, dispatcher: ClientDispatcher, trove: Trove): DevtoolsClient;