@typetorch/framework 0.5.6 → 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 CHANGED
@@ -185,6 +185,16 @@ return the generation's stop function, which the kernel calls on a swap.
185
185
  build picker (Server > Branch) and a Claude prompt. The sidebar, top to bottom: Artifact, Server, Network, Modules,
186
186
  Manage, Logs, Dex, Claude. The window can be dragged by its header and resized from its corner (double-tap the
187
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`).
188
198
  - **Server TPS (framework 0.4.2, kernel 0.4.2):** Server > Status has a *TPS* line next to Memory and Lua heap: the
189
199
  last minute's average, its slowest second and the physics FPS (`59.9 avg, 52.0 min, physics 60`; the warning colour
190
200
  under 50 on average). Older kernels show `needs kernel 0.4.2`. The same numbers ride on fleet heartbeats (the
@@ -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
  }
@@ -17,6 +17,7 @@ local branchLabel = _build_actions.branchLabel
17
17
  local buildAction = _build_actions.buildAction
18
18
  local buildLabel = _build_actions.buildLabel
19
19
  local buildName = _build_actions.buildName
20
+ local pinPayload = _build_actions.pinPayload
20
21
  local requestSwitch = _build_actions.requestSwitch
21
22
  local showCliNote = _build_actions.showCliNote
22
23
  local switchedText = _build_actions.switchedText
@@ -52,6 +53,12 @@ local renderClaudeChat = TS.import(script, script.Parent, "claude-ui").renderCla
52
53
  local _admin_ui = TS.import(script, script.Parent, "admin-ui")
53
54
  local adminTabs = _admin_ui.adminTabs
54
55
  local migrateControl = _admin_ui.migrateControl
56
+ local PlayerFocus = _admin_ui.PlayerFocus
57
+ local _commands = TS.import(script, script.Parent, "commands")
58
+ local isPaletteHotkey = _commands.isPaletteHotkey
59
+ local sanitizeRecent = _commands.sanitizeRecent
60
+ local createPalette = TS.import(script, script.Parent, "palette").createPalette
61
+ local paletteCommands = TS.import(script, script.Parent, "palette-commands").paletteCommands
55
62
  local renderNetworkInspector = TS.import(script, script.Parent, "network-inspector").renderNetworkInspector
56
63
  local renderAssetsTab = TS.import(script, script.Parent, "assets-ui").renderAssetsTab
57
64
  local renderStateTab = TS.import(script, script.Parent, "state-ui").renderStateTab
@@ -426,7 +433,7 @@ local function every(trove, interval, callback)
426
433
  while true do
427
434
  local ok, err = pcall(callback)
428
435
  if not ok then
429
- warn("[src/devtools/client.ts:368]", `[devtools] refresh failed: {err}`)
436
+ warn("[src/devtools/client.ts:374]", `[devtools] refresh failed: {err}`)
430
437
  end
431
438
  task.wait(interval)
432
439
  end
@@ -2109,7 +2116,6 @@ local function startDevtoolsClient(kernel, dispatcher, trove)
2109
2116
  if showCliNote(ctx) then
2110
2117
  body:text("Use the CLI: typetorch pin", COLORS.dim)
2111
2118
  end
2112
- local abInPlace = ctx.serverType == "public" and isOwner and ctx.experiments and not ctx.signedOnly
2113
2119
  if ctx.serverType == "public" and isOwner and not ctx.experiments and not ctx.signedOnly and not switchKernel then
2114
2120
  body:text("A/B needs kernel 0.2.3", COLORS.dim)
2115
2121
  end
@@ -2240,10 +2246,7 @@ local function startDevtoolsClient(kernel, dispatcher, trove)
2240
2246
  end
2241
2247
  return nil
2242
2248
  end
2243
- act(`Loading {short}`, "pin", if abInPlace then {
2244
- assetId = entry.assetId,
2245
- experiment = true,
2246
- } else entry.assetId)
2249
+ act(`Loading {short}`, "pin", pinPayload(ctx, entry.assetId))
2247
2250
  end,
2248
2251
  })
2249
2252
  expandable(loadRow, entry.assetId)
@@ -2321,6 +2324,8 @@ local function startDevtoolsClient(kernel, dispatcher, trove)
2321
2324
  imageInbox = imageInbox,
2322
2325
  })
2323
2326
  end
2327
+ --* The command palette's player lookup into Manage > Players (admin-ui.ts).
2328
+ local playerFocus = PlayerFocus.new()
2324
2329
  --* Keys: a tab name, or "Tab/Sub" for tabs with sub-tabs.
2325
2330
  local _object_1 = {
2326
2331
  Artifact = renderArtifact,
@@ -2348,6 +2353,7 @@ local function startDevtoolsClient(kernel, dispatcher, trove)
2348
2353
  for _k, _v in adminTabs({
2349
2354
  kernel = kernel,
2350
2355
  call = call,
2356
+ playerFocus = playerFocus,
2351
2357
  }) do
2352
2358
  _object_1[_k] = _v
2353
2359
  end
@@ -2369,6 +2375,8 @@ local function startDevtoolsClient(kernel, dispatcher, trove)
2369
2375
  -- Window ----------------------------------------------------------------------------------------------------------
2370
2376
  local ui
2371
2377
  local wm
2378
+ --* The command palette (Ctrl+K, palette.ts), built with the menu.
2379
+ local palette
2372
2380
  local isOpen = false
2373
2381
  local dev = false
2374
2382
  --* This player is an owner (framework 0.3.2: the Manage group shows only for owners).
@@ -2522,7 +2530,7 @@ local function startDevtoolsClient(kernel, dispatcher, trove)
2522
2530
  })
2523
2531
  end)
2524
2532
  if not ok then
2525
- warn("[src/devtools/client.ts:1748]", `[devtools] {key} tab failed: {err}`)
2533
+ warn("[src/devtools/client.ts:1758]", `[devtools] {key} tab failed: {err}`)
2526
2534
  page:text(`This tab failed: {err}`, COLORS.bad)
2527
2535
  end
2528
2536
  end
@@ -2577,20 +2585,55 @@ local function startDevtoolsClient(kernel, dispatcher, trove)
2577
2585
  end
2578
2586
  isOpen = false
2579
2587
  state.open = false
2580
- local _result = wm
2588
+ local _result = palette
2581
2589
  if _result ~= nil then
2582
- _result.fadeOut()
2590
+ _result.close()
2591
+ end
2592
+ local _result_1 = wm
2593
+ if _result_1 ~= nil then
2594
+ _result_1.fadeOut()
2583
2595
  end
2584
2596
  popOut(ui.window, function()
2585
2597
  if not isOpen then
2586
- local _result_1 = wm
2587
- if _result_1 ~= nil then
2588
- _result_1.unmount()
2598
+ local _result_2 = wm
2599
+ if _result_2 ~= nil then
2600
+ _result_2.unmount()
2589
2601
  end
2590
2602
  end
2591
2603
  end)
2592
2604
  end
2593
2605
  local open
2606
+ local openPalette
2607
+ local commands = paletteCommands({
2608
+ kernel = kernel,
2609
+ call = call,
2610
+ wm = function()
2611
+ return wm
2612
+ end,
2613
+ pages = function()
2614
+ return paneHost.picks()
2615
+ end,
2616
+ owner = function()
2617
+ return owner
2618
+ end,
2619
+ playerFocus = playerFocus,
2620
+ swapText = swapText,
2621
+ serverText = serverText,
2622
+ report = function(text, color)
2623
+ local _result = palette
2624
+ if _result ~= nil then
2625
+ _result = _result.report(text, color)
2626
+ end
2627
+ return _result
2628
+ end,
2629
+ refresh = function()
2630
+ local _result = palette
2631
+ if _result ~= nil then
2632
+ _result = _result.refresh()
2633
+ end
2634
+ return _result
2635
+ end,
2636
+ })
2594
2637
  local build = function()
2595
2638
  local playerGui = Players.LocalPlayer:WaitForChild("PlayerGui")
2596
2639
  sweepStaleMenus(playerGui, kernel.generation)
@@ -2656,7 +2699,7 @@ local function startDevtoolsClient(kernel, dispatcher, trove)
2656
2699
  }, header), `{kernel.artifact.id} #{kernel.generation}`, 14, COLORS.dim, Enum.Font.Code)
2657
2700
  subtitle.TextWrapped = false
2658
2701
  subtitle.TextTruncate = Enum.TextTruncate.AtEnd
2659
- subtitle.Size = UDim2.new(1, -170, 1, 0)
2702
+ subtitle.Size = UDim2.new(1, -212, 1, 0)
2660
2703
  subtitle.Position = UDim2.fromOffset(114, 0)
2661
2704
  local closeButton = style(make("TextButton", {
2662
2705
  AutoButtonColor = true,
@@ -2669,6 +2712,40 @@ local function startDevtoolsClient(kernel, dispatcher, trove)
2669
2712
  closeButton.Size = UDim2.fromOffset(34, 34)
2670
2713
  corner(closeButton, 6)
2671
2714
  closeButton.Activated:Connect(close)
2715
+ -- The command palette's button (touch has no Ctrl+K): a magnifier drawn from Frames (no glyphs).
2716
+ local searchButton = make("TextButton", {
2717
+ Name = "Search",
2718
+ Text = "",
2719
+ AutoButtonColor = true,
2720
+ BackgroundColor3 = COLORS.button,
2721
+ BorderSizePixel = 0,
2722
+ AnchorPoint = Vector2.new(1, 0.5),
2723
+ Position = UDim2.new(1, -50, 0.5, 0),
2724
+ Size = UDim2.fromOffset(34, 34),
2725
+ }, header)
2726
+ corner(searchButton, 6)
2727
+ local lens = make("Frame", {
2728
+ BackgroundTransparency = 1,
2729
+ Position = UDim2.fromOffset(9, 8),
2730
+ Size = UDim2.fromOffset(12, 12),
2731
+ }, searchButton)
2732
+ corner(lens, 6)
2733
+ make("UIStroke", {
2734
+ Color = COLORS.text,
2735
+ Thickness = 2,
2736
+ ApplyStrokeMode = Enum.ApplyStrokeMode.Border,
2737
+ }, lens)
2738
+ make("Frame", {
2739
+ BackgroundColor3 = COLORS.text,
2740
+ BorderSizePixel = 0,
2741
+ AnchorPoint = Vector2.new(0.5, 0.5),
2742
+ Position = UDim2.fromOffset(23, 23),
2743
+ Size = UDim2.fromOffset(8, 2),
2744
+ Rotation = 45,
2745
+ }, searchButton)
2746
+ searchButton.Activated:Connect(function()
2747
+ return openPalette()
2748
+ end)
2672
2749
  -- Sidebar (script-free ScrollingFrame): tabs, and for tabs with sub-tabs an indented group of children right
2673
2750
  -- under them (visible while that tab is selected). Clicking a group header opens its first child.
2674
2751
  local tabs = scrolling(window, {
@@ -2824,6 +2901,26 @@ local function startDevtoolsClient(kernel, dispatcher, trove)
2824
2901
  return layout
2825
2902
  end,
2826
2903
  host = paneHost,
2904
+ covered = function()
2905
+ local _result = palette
2906
+ if _result ~= nil then
2907
+ _result = _result.isOpen()
2908
+ end
2909
+ return _result == true
2910
+ end,
2911
+ })
2912
+ palette = createPalette({
2913
+ gui = gui,
2914
+ trove = trove,
2915
+ registry = commands.registry,
2916
+ context = commands.context,
2917
+ recent = function()
2918
+ return sanitizeRecent(state.recentCommands)
2919
+ end,
2920
+ setRecent = function(list)
2921
+ state.recentCommands = list
2922
+ end,
2923
+ opened = commands.load,
2827
2924
  })
2828
2925
  -- Placement: drag by the header, resize by the grip, double-tap the header to reset. Saved across swaps.
2829
2926
  local viewport = function()
@@ -2998,6 +3095,20 @@ local function startDevtoolsClient(kernel, dispatcher, trove)
2998
3095
  _result.show()
2999
3096
  end
3000
3097
  end
3098
+ --* Ctrl+K / Cmd+K and the header's search button: the command palette (opens the menu first).
3099
+ openPalette = function()
3100
+ if not dev then
3101
+ return nil
3102
+ end
3103
+ open()
3104
+ if not isOpen then
3105
+ return nil
3106
+ end
3107
+ local _result = palette
3108
+ if _result ~= nil then
3109
+ _result.open()
3110
+ end
3111
+ end
3001
3112
  local setDev = function(value)
3002
3113
  if value == dev then
3003
3114
  return nil
@@ -3121,6 +3232,25 @@ local function startDevtoolsClient(kernel, dispatcher, trove)
3121
3232
  open()
3122
3233
  end
3123
3234
  end)
3235
+ -- Ctrl+K (Cmd+K on a Mac: Super / Meta). Never while typing in chat or a TextBox (`processed`).
3236
+ trove:connect(UserInputService.InputBegan, function(input, processed)
3237
+ if not dev then
3238
+ return nil
3239
+ end
3240
+ local held = UserInputService:IsKeyDown(Enum.KeyCode.LeftControl) or UserInputService:IsKeyDown(Enum.KeyCode.RightControl) or UserInputService:IsKeyDown(Enum.KeyCode.LeftSuper) or UserInputService:IsKeyDown(Enum.KeyCode.RightSuper) or UserInputService:IsKeyDown(Enum.KeyCode.LeftMeta) or UserInputService:IsKeyDown(Enum.KeyCode.RightMeta)
3241
+ if not isPaletteHotkey(input.KeyCode.Name, held, processed) then
3242
+ return nil
3243
+ end
3244
+ local _result = palette
3245
+ if _result ~= nil then
3246
+ _result = _result.isOpen()
3247
+ end
3248
+ if _result then
3249
+ palette.close()
3250
+ else
3251
+ openPalette()
3252
+ end
3253
+ end)
3124
3254
  return {
3125
3255
  started = function()
3126
3256
  -- Its own thread: off dev rules it waits for the server's answer (op "access").
@@ -3129,7 +3259,7 @@ local function startDevtoolsClient(kernel, dispatcher, trove)
3129
3259
  return playReloadSound()
3130
3260
  end)
3131
3261
  if not ok then
3132
- warn("[src/devtools/client.ts:2169]", `[devtools] reload sound failed: {err}`)
3262
+ warn("[src/devtools/client.ts:2270]", `[devtools] reload sound failed: {err}`)
3133
3263
  end
3134
3264
  end))
3135
3265
  end,