@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.
- package/README.md +102 -15
- package/out/devtools/admin-ui.d.ts +22 -0
- package/out/devtools/admin-ui.luau +108 -1
- package/out/devtools/build-actions.d.ts +14 -0
- package/out/devtools/build-actions.luau +25 -0
- package/out/devtools/claude-ui.d.ts +5 -0
- package/out/devtools/claude-ui.luau +37 -7
- package/out/devtools/client.d.ts +5 -0
- package/out/devtools/client.luau +148 -43
- package/out/devtools/commands.d.ts +115 -0
- package/out/devtools/commands.luau +504 -0
- package/out/devtools/layout.d.ts +11 -0
- package/out/devtools/layout.luau +22 -0
- package/out/devtools/on-demand.d.ts +6 -0
- package/out/devtools/on-demand.luau +12 -0
- package/out/devtools/palette-commands.d.ts +55 -0
- package/out/devtools/palette-commands.luau +667 -0
- package/out/devtools/palette-rules.d.ts +57 -0
- package/out/devtools/palette-rules.luau +103 -0
- package/out/devtools/palette.d.ts +26 -0
- package/out/devtools/palette.luau +469 -0
- package/out/devtools/panes.d.ts +21 -1
- package/out/devtools/panes.luau +111 -14
- package/out/runtime/dev-gui.d.ts +8 -0
- package/out/runtime/dev-gui.luau +29 -0
- package/out/runtime/devtools-loader.d.ts +6 -0
- package/out/runtime/devtools-loader.luau +227 -0
- package/out/runtime/start.luau +7 -6
- package/out/servers.d.ts +2 -2
- package/out/servers.luau +2 -2
- package/out/version.d.ts +1 -1
- package/out/version.luau +1 -1
- package/package.json +1 -1
- /package/out/{devtools/roll-call.d.ts → roll-call.d.ts} +0 -0
- /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,
|
|
202
|
-
Code from the Claude tab.
|
|
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
|
|
362
|
-
`placeVersion` is this server's place version
|
|
363
|
-
|
|
364
|
-
|
|
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
|
|
497
|
-
`@typetorch/transformer`'s `runtime-kit
|
|
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
|
-
|
|
526
|
-
|
|
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:
|
|
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
|
|
1069
|
-
|
|
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
|
}
|
package/out/devtools/client.d.ts
CHANGED
|
@@ -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;
|