@weppy/roblox-mcp 2.6.4 → 2.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (79) hide show
  1. package/.claude-plugin/marketplace.json +2 -2
  2. package/CHANGELOG.md +24 -0
  3. package/README.md +22 -11
  4. package/install.ps1 +85 -19
  5. package/install.sh +60 -15
  6. package/llms-full.txt +1 -2
  7. package/llms.txt +4 -3
  8. package/package.json +1 -1
  9. package/plugins/weppy-roblox-mcp/.claude-plugin/plugin.json +1 -1
  10. package/plugins/weppy-roblox-mcp/dashboard/dist/assets/{ChangelogDetailPage-DYQthxhC.js → ChangelogDetailPage-DxktONbL.js} +1 -1
  11. package/plugins/weppy-roblox-mcp/dashboard/dist/assets/ChangelogPage-CNxAGfwG.css +1 -0
  12. package/plugins/weppy-roblox-mcp/dashboard/dist/assets/ChangelogPage-DOY-SmWM.js +1 -0
  13. package/plugins/weppy-roblox-mcp/dashboard/dist/assets/{ConfirmModal-CM3ElkBC.js → ConfirmModal-CmJCz5rg.js} +1 -1
  14. package/plugins/weppy-roblox-mcp/dashboard/dist/assets/{ConnectionPage-DdKcJZt6.js → ConnectionPage-BiiIQs7f.js} +1 -1
  15. package/plugins/weppy-roblox-mcp/dashboard/dist/assets/{GameChangeDetail-DGphCFbI.js → GameChangeDetail-D1kE2L_v.js} +1 -1
  16. package/plugins/weppy-roblox-mcp/dashboard/dist/assets/{InfoLabel-CMSu30c4.js → InfoLabel-B9KT3kfU.js} +1 -1
  17. package/plugins/weppy-roblox-mcp/dashboard/dist/assets/{OverviewPage-DM74adCU.js → OverviewPage-T2WCTHpo.js} +1 -1
  18. package/plugins/weppy-roblox-mcp/dashboard/dist/assets/PlaytestPage-CW8Xfp6b.js +11 -0
  19. package/plugins/weppy-roblox-mcp/dashboard/dist/assets/SettingsPage-Cc9Wnj8M.css +1 -0
  20. package/plugins/weppy-roblox-mcp/dashboard/dist/assets/SettingsPage-DcUt5qjG.js +1 -0
  21. package/plugins/weppy-roblox-mcp/dashboard/dist/assets/{StatusBadge-BhRLY0yK.js → StatusBadge-Bxe88ki2.js} +1 -1
  22. package/plugins/weppy-roblox-mcp/dashboard/dist/assets/{SyncPage-Col3nhBp.js → SyncPage-C09YF6D5.js} +1 -1
  23. package/plugins/weppy-roblox-mcp/dashboard/dist/assets/Tabs-876h0_zB.css +1 -0
  24. package/plugins/weppy-roblox-mcp/dashboard/dist/assets/Tabs-BNtOqYlc.js +1 -0
  25. package/plugins/weppy-roblox-mcp/dashboard/dist/assets/TierComparison-Dyf-knpe.js +1 -0
  26. package/plugins/weppy-roblox-mcp/dashboard/dist/assets/ToolsPage-B3fMk-22.css +1 -0
  27. package/plugins/weppy-roblox-mcp/dashboard/dist/assets/ToolsPage-HKbmsKMQ.js +1 -0
  28. package/plugins/weppy-roblox-mcp/dashboard/dist/assets/{TooltipText-Bnvm1FcC.js → TooltipText-Baw38jOU.js} +1 -1
  29. package/plugins/weppy-roblox-mcp/dashboard/dist/assets/UiStudioPage-Cbgl7xIC.js +16 -0
  30. package/plugins/weppy-roblox-mcp/dashboard/dist/assets/UiStudioPage-DQOd5VrU.css +1 -0
  31. package/plugins/weppy-roblox-mcp/dashboard/dist/assets/{WhatsNewPage-M1EeHSH-.js → WhatsNewPage-BlOSWgSn.js} +1 -1
  32. package/plugins/weppy-roblox-mcp/dashboard/dist/assets/index-FfT2oWAB.js +343 -0
  33. package/plugins/weppy-roblox-mcp/dashboard/dist/assets/index-Gu7et1DX.css +1 -0
  34. package/plugins/weppy-roblox-mcp/dashboard/dist/assets/sample-requests-Bx1zpYdG.css +1 -0
  35. package/plugins/weppy-roblox-mcp/dashboard/dist/assets/sample-requests-X_hsWdLX.js +1 -0
  36. package/plugins/weppy-roblox-mcp/dashboard/dist/assets/ui-studio-sample-DrNTD6yi.png +0 -0
  37. package/plugins/weppy-roblox-mcp/dashboard/dist/assets/{useLiveUptime-Dc2iJiFi.js → useLiveUptime-DhJz484L.js} +1 -1
  38. package/plugins/weppy-roblox-mcp/dashboard/dist/index.html +2 -2
  39. package/plugins/weppy-roblox-mcp/dist/index.js +450 -85
  40. package/plugins/weppy-roblox-mcp/roblox-plugin/WeppyRobloxMCP.rbxm +0 -0
  41. package/.gitattributes +0 -1
  42. package/.github/ISSUE_TEMPLATE/bug_report.yml +0 -60
  43. package/.github/ISSUE_TEMPLATE/feature_request.yml +0 -30
  44. package/.github/ISSUE_TEMPLATE/install_help.yml +0 -48
  45. package/.github/PULL_REQUEST_TEMPLATE.md +0 -16
  46. package/.github/workflows/install-test.yml +0 -140
  47. package/Dockerfile +0 -13
  48. package/docs/assets/screenshots/antigravity/antigravity_mcp_raw.png +0 -0
  49. package/docs/assets/screenshots/antigravity/antigravity_mcp_services_menu.png +0 -0
  50. package/docs/assets/screenshots/antigravity/antigravity_raw_config_menu.png +0 -0
  51. package/docs/assets/screenshots/dashboard/dashboard_changelog1.png +0 -0
  52. package/docs/assets/screenshots/dashboard/dashboard_changelog2.png +0 -0
  53. package/docs/assets/screenshots/dashboard/dashboard_connection.png +0 -0
  54. package/docs/assets/screenshots/dashboard/dashboard_overview.png +0 -0
  55. package/docs/assets/screenshots/dashboard/dashboard_playtest.png +0 -0
  56. package/docs/assets/screenshots/dashboard/dashboard_sync.png +0 -0
  57. package/docs/assets/screenshots/dashboard/dashboard_tools.png +0 -0
  58. package/docs/assets/screenshots/plugin/connection/connection-guide.png +0 -0
  59. package/docs/assets/screenshots/plugin/installation/main-screen.png +0 -0
  60. package/docs/assets/screenshots/plugin/installation/plugins-menu.png +0 -0
  61. package/docs/assets/screenshots/plugin/installation/settings-screen.png +0 -0
  62. package/docs/assets/screenshots/plugin/installation/toolbar-button.png +0 -0
  63. package/docs/assets/screenshots/plugin/license/dashboard-license-screen.png +0 -0
  64. package/docs/assets/screenshots/plugin/license/plugin-license-screen.png +0 -0
  65. package/docs/assets/screenshots/plugin/sync/sync-conflict.png +0 -0
  66. package/docs/assets/screenshots/plugin/sync/sync-main.png +0 -0
  67. package/docs/assets/screenshots/plugin/sync/sync-overview.png +0 -0
  68. package/docs/assets/screenshots/roblox-explorer/roblox-explorer-property-window.png +0 -0
  69. package/docs/assets/screenshots/roblox-explorer/roblox-explorer-screen.png +0 -0
  70. package/plugins/weppy-roblox-mcp/dashboard/dist/assets/ChangelogPage-BH87M2hn.css +0 -1
  71. package/plugins/weppy-roblox-mcp/dashboard/dist/assets/ChangelogPage-WFqQ5h8e.js +0 -1
  72. package/plugins/weppy-roblox-mcp/dashboard/dist/assets/PlaytestPage-Dq3kV_rX.js +0 -11
  73. package/plugins/weppy-roblox-mcp/dashboard/dist/assets/SettingsPage-DTv0NbEY.css +0 -1
  74. package/plugins/weppy-roblox-mcp/dashboard/dist/assets/SettingsPage-QQd8aPdd.js +0 -1
  75. package/plugins/weppy-roblox-mcp/dashboard/dist/assets/TierComparison-CYfIb0tr.js +0 -1
  76. package/plugins/weppy-roblox-mcp/dashboard/dist/assets/ToolsPage-54vIMfZg.css +0 -1
  77. package/plugins/weppy-roblox-mcp/dashboard/dist/assets/ToolsPage-Ba_6GnUZ.js +0 -1
  78. package/plugins/weppy-roblox-mcp/dashboard/dist/assets/index-B-nqZCE3.js +0 -380
  79. package/plugins/weppy-roblox-mcp/dashboard/dist/assets/index-COXUWuq2.css +0 -1
@@ -6,14 +6,14 @@
6
6
  },
7
7
  "metadata": {
8
8
  "description": "Weppy Roblox MCP — MCP server that lets AI coding agents control a live Roblox Studio session with 21 tools, 140+ actions, bidirectional sync, automated playtest, and multi-place support",
9
- "version": "2.6.4"
9
+ "version": "2.7.1"
10
10
  },
11
11
  "plugins": [
12
12
  {
13
13
  "name": "weppy-roblox-mcp",
14
14
  "source": "./plugins/weppy-roblox-mcp",
15
15
  "description": "Weppy Roblox MCP — MCP server that lets AI coding agents control a live Roblox Studio session with 21 tools, 140+ actions, bidirectional sync, automated playtest, and multi-place support",
16
- "version": "2.6.4",
16
+ "version": "2.7.1",
17
17
  "author": {
18
18
  "name": "hope1026"
19
19
  },
package/CHANGELOG.md CHANGED
@@ -3,6 +3,30 @@
3
3
  All notable changes to this project will be documented in this file.
4
4
 
5
5
 
6
+ ## [2.7.1] - 2026-05-05
7
+
8
+ ### Improvements
9
+
10
+ - **Stability hardening** — Internal reliability improvements across the MCP server and Roblox Studio plugin. No behavior changes for existing workflows.
11
+
12
+ ## [2.7.0] - 2026-05-05
13
+
14
+ ### Features
15
+
16
+ - **UI Studio launch** — A new workflow for iterating on Roblox Studio UI design together with an AI agent. From the UI Studio page in the dashboard, you can now use:
17
+ - **History tab** — Compare before/after snapshots and design suggestions for each request, side by side.
18
+ - **Analysis tab** — Snapshot comparison modal with batch selection and deletion.
19
+ - **Design Check** — Reviews your UI against design quality criteria and surfaces concrete improvements.
20
+
21
+ ### Improvements
22
+
23
+ - **More accurate property types when syncing local edits back to Studio** — When you edit synced files locally and push the changes back to Studio, the server now resolves property types via class-aware metadata instead of guessing from value shape. Edits to enum, color, or vector properties on specific instances are no longer silently saved as the wrong type.
24
+
25
+ ### Bug Fixes
26
+
27
+ - **Claude Code detection on Windows** — On Windows, Claude Code could be mis-detected and even a fresh install would fail with a confusing "already exists" error. Detection is now correct, and an existing valid registration is treated as success.
28
+
29
+ - **Dashboard tooltips no longer clipped** — Tooltips inside scrollable areas (Settings, UI Studio history) were being cut off. They now display in full regardless of the surrounding container.
6
30
 
7
31
 
8
32
 
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  > **WEPPY** is an MCP server that lets AI coding agents control a live Roblox Studio session - create and edit scripts, instances, terrain, lighting, assets, audio, and animations through natural language.
4
4
 
5
- **Action-based tool surface · Bidirectional sync · Automated playtest · Multi-place support**
5
+ **Action-based tool surface · Bidirectional sync · Automated playtest · UI Studio · Multi-place support**
6
6
 
7
7
  **English** | [한국어](https://weppyai.com/ko) | [日本語](https://weppyai.com/ja) | [Español](https://weppyai.com/es) | [Português](https://weppyai.com/pt-br) | [Bahasa Indonesia](https://weppyai.com/id) | [Deutsch](https://weppyai.com/de)
8
8
 
@@ -87,7 +87,7 @@ AI works from a synchronized local mirror, so multi-file updates stay consistent
87
87
  - Basic: one-way sync (Studio -> Local)
88
88
  - Pro: bidirectional sync + per-type Direction/Apply Mode + history + multi-place
89
89
 
90
- ![Sync workflow - Studio and local files synchronized in real time](docs/assets/screenshots/plugin/sync/sync-overview.png)
90
+ ![Sync workflow - Studio and local files synchronized in real time](https://raw.githubusercontent.com/hope1026/weppy-roblox-mcp/main/docs/assets/screenshots/plugin/sync/sync-overview.png)
91
91
 
92
92
  ### 3) Playtest: Let AI run and verify tests automatically
93
93
 
@@ -97,19 +97,29 @@ AI can control Roblox Studio playtests directly. It can start and stop Play (F5)
97
97
  - "Write a test that verifies the SpawnLocation is above the ground and run it."
98
98
  - "Validate that the script I just changed runs without errors in playtest."
99
99
 
100
- ![WEPPY Playtest Dashboard - Test history and detailed report](docs/assets/screenshots/dashboard/dashboard_playtest.png)
100
+ ![WEPPY Playtest Dashboard - Test history and detailed report](https://raw.githubusercontent.com/hope1026/weppy-roblox-mcp/main/docs/assets/screenshots/dashboard/dashboard_playtest.png)
101
101
 
102
- ### 4) WEPPY Dashboard: Monitor AI work in real time
102
+ ### 4) UI Studio: Build and inspect in-game UI
103
103
 
104
- The MCP server provides a web dashboard where you can check connection status, tool execution history, sync state, and game change logs in real time.
104
+ UI Studio lets AI agents create in-game UI that matches your game's style, or analyze the UI you already have and suggest improvements.
105
+
106
+ - Clarify the UI goal with guided questions about purpose, screen, target devices, and visual direction
107
+ - Create or refine game-style menus, HUDs, buttons, labels, image panels, and other Roblox UI elements directly in Studio
108
+ - Capture the result, compare before/after changes, and follow dashboard suggestions for layout, readability, touch targets, and safe areas
109
+
110
+ ![WEPPY UI Studio - Roblox Studio showing AI-generated in-game UI](https://raw.githubusercontent.com/hope1026/weppy-roblox-mcp/main/docs/assets/screenshots/dashboard/dashboard_ui_roblox_studio.png)
111
+
112
+ ### 5) WEPPY Dashboard: Monitor AI work in real time
113
+
114
+ The MCP server provides a web dashboard where you can check connection status, tool execution history, sync state, UI Studio history, and game change logs in real time.
105
115
 
106
116
  - Server/Plugin/Agent connection status at a glance
107
117
  - Compare every change the AI made via Before & After in Changelog
108
- - Analyze workflow with tool execution history and statistics
118
+ - Analyze workflow with tool execution history, UI Studio captures, and statistics
109
119
 
110
- ![WEPPY Dashboard Overview - Server status, recent changes, and session summary](docs/assets/screenshots/dashboard/dashboard_overview.png)
120
+ ![WEPPY Dashboard Overview - Server status, recent changes, and session summary](https://raw.githubusercontent.com/hope1026/weppy-roblox-mcp/main/docs/assets/screenshots/dashboard/dashboard_overview.png)
111
121
 
112
- ### 5) WEPPY Roblox Explorer: Browse Studio hierarchy in VSCode
122
+ ### 6) WEPPY Roblox Explorer: Browse Studio hierarchy in VSCode
113
123
 
114
124
  View the full instance tree of your Roblox Studio place directly inside VSCode. Navigate services, open synced scripts and property files, and track sync status - all without switching to Studio.
115
125
  WEPPY Roblox Explorer is a companion VSCode extension for sync data generated by WEPPY. Tree browsing works from synced files, and live sync state or direction indicators are enhanced when the local MCP server is running.
@@ -119,13 +129,14 @@ Install from [VS Code Marketplace](https://marketplace.visualstudio.com/items?it
119
129
  - Click to open synced scripts and property files
120
130
  - Multi-place support with sync status indicators
121
131
 
122
- ![WEPPY Roblox Explorer - Studio instance tree displayed in VSCode sidebar](docs/assets/screenshots/roblox-explorer/roblox-explorer-screen.png)
132
+ ![WEPPY Roblox Explorer - Studio instance tree displayed in VSCode sidebar](https://raw.githubusercontent.com/hope1026/weppy-roblox-mcp/main/docs/assets/screenshots/roblox-explorer/roblox-explorer-screen.png)
123
133
 
124
134
  ## Use Cases
125
135
 
126
136
  - **Rapid prototyping**: Describe a game mechanic in natural language and watch AI build it in Studio
127
137
  - **Bulk refactoring**: Rename a module interface and update every dependent script in one request
128
138
  - **Terrain & environment**: Generate procedural terrain, set lighting/atmosphere, place assets - all from a single prompt
139
+ - **UI design**: Generate in-game UI, capture previews, and iterate on Design Check suggestions
129
140
  - **Multi-file consistency**: AI reads the full project via Sync and applies changes across related scripts together
130
141
  - **Asset integration**: Search the Creator Store, insert models, and configure properties without leaving your editor
131
142
 
@@ -161,7 +172,7 @@ Yes. Any MCP-compatible AI client works.
161
172
  Yes. AI can create instances, write scripts, generate terrain, set up lighting, insert assets, configure physics, and more - all inside a live Roblox Studio session. It goes beyond code generation to executable actions.
162
173
 
163
174
  ### What is the difference between Basic and Pro?
164
- Basic (Free) includes MCP tool execution and one-way sync (Studio -> Local). Pro adds bidirectional sync, bulk operations, terrain generation, spatial analysis, audio/animation control, and multi-place support. See the Pro upgrade page.
175
+ Basic (Free) includes MCP tool execution and one-way sync (Studio -> Local). Pro adds bidirectional sync, UI Studio, bulk operations, terrain generation, spatial analysis, audio/animation control, and multi-place support. See the Pro upgrade page.
165
176
 
166
177
  ### How is Weppy different from other Roblox MCP servers?
167
178
  Weppy uses action-based dispatching instead of separate tools for each function. This reduces AI token consumption significantly. It also provides bidirectional project sync and multi-place support, which most alternatives lack.
@@ -171,7 +182,7 @@ The server runs on localhost only (127.0.0.1:3002). Forbidden paths (CoreGui, Co
171
182
 
172
183
  ## Pro Upgrade
173
184
 
174
- Bidirectional Sync, advanced build capabilities, and AI token efficiency - all in one upgrade.
185
+ Bidirectional Sync, UI Studio, advanced build capabilities, and AI token efficiency - all in one upgrade.
175
186
 
176
187
  [Pro Upgrade Guide](https://weppyai.com/en/plans)
177
188
 
package/install.ps1 CHANGED
@@ -112,7 +112,18 @@ function Test-McpJsonConfigured($configPath) {
112
112
  }
113
113
  }
114
114
 
115
- # Antigravity 설정에 canonical mcpServers 래퍼로 MCP 서버를 추가하고 legacy flat key를 정리
115
+ # Require an explicit `@<tag>` so the installer can upgrade legacy bare
116
+ # entries (`@weppy/roblox-mcp`) — those reuse npx cache and trap users on
117
+ # outdated versions. Tagged entries (`@latest`, `@2.6.4`, …) are preserved.
118
+ function Test-WeppyPackageSpec($value) {
119
+ if ([string]::IsNullOrWhiteSpace($value)) {
120
+ return $false
121
+ }
122
+ return $value -match '^@weppy/roblox-mcp@.+$'
123
+ }
124
+
125
+ # Add the MCP server under the canonical `mcpServers` wrapper in the Antigravity
126
+ # config and strip any legacy flat key left over from earlier versions.
116
127
  function Add-AntigravityMcpConfig($configPath) {
117
128
  $parentDir = Split-Path $configPath -Parent
118
129
  if (-not (Test-Path $parentDir)) { New-Item -ItemType Directory -Path $parentDir -Force | Out-Null }
@@ -134,7 +145,7 @@ const next = { ...config };
134
145
  delete next['weppy-roblox-mcp'];
135
146
  next.mcpServers = {
136
147
  ...(mcpServers || {}),
137
- 'weppy-roblox-mcp': { command: 'npx', args: ['-y', '@weppy/roblox-mcp'] }
148
+ 'weppy-roblox-mcp': { command: 'npx', args: ['-y', '@weppy/roblox-mcp@latest'] }
138
149
  };
139
150
  config = next;
140
151
  fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n');
@@ -153,7 +164,8 @@ function Test-AntigravityMcpConfigured($configPath) {
153
164
  $config = Get-Content -Path $configPath -Raw | ConvertFrom-Json
154
165
  $hasLegacyFlatKey = $config.PSObject.Properties.Name -contains 'weppy-roblox-mcp'
155
166
  $server = $config.mcpServers.'weppy-roblox-mcp'
156
- $hasCanonicalArgs = ($server.args -is [System.Array]) -and ($server.args.Count -eq 2) -and ($server.args[0] -eq '-y') -and ($server.args[1] -eq '@weppy/roblox-mcp')
167
+ # Accept args[1] as the weppy package regardless of whether a version tag is appended.
168
+ $hasCanonicalArgs = ($server.args -is [System.Array]) -and ($server.args.Count -eq 2) -and ($server.args[0] -eq '-y') -and (Test-WeppyPackageSpec $server.args[1])
157
169
  return ($server.command -eq 'npx') -and $hasCanonicalArgs -and (-not $hasLegacyFlatKey)
158
170
  }
159
171
  catch {
@@ -174,7 +186,8 @@ const fs = require('fs');
174
186
  const configPath = process.env.MCP_CODEX_CONFIG_PATH;
175
187
  const serverName = 'weppy-roblox-mcp';
176
188
  const expectedCommand = 'npx';
177
- const expectedArgs = ['-y', '@weppy/roblox-mcp'];
189
+ // Require an explicit `@<tag>` so the installer can upgrade legacy bare entries.
190
+ const packageSpecPattern = /^@weppy\/roblox-mcp@.+$/;
178
191
  const headerPattern = new RegExp(
179
192
  '^\\s*\\[\\s*mcp_servers\\.' + serverName.replace(/[.*+?^${}()|[\]\\\\]/g, '\\$&') + '\\s*\\]\\s*(?:#.*)?$'
180
193
  );
@@ -568,8 +581,10 @@ try {
568
581
  !parsed.hasConflict &&
569
582
  parsed.command === expectedCommand &&
570
583
  Array.isArray(parsed.args) &&
571
- parsed.args.length === expectedArgs.length &&
572
- parsed.args.every((entry, index) => entry === expectedArgs[index]);
584
+ parsed.args.length === 2 &&
585
+ parsed.args[0] === '-y' &&
586
+ typeof parsed.args[1] === 'string' &&
587
+ packageSpecPattern.test(parsed.args[1]);
573
588
 
574
589
  process.exit(isConfigured ? 0 : 1);
575
590
  } catch {
@@ -595,7 +610,7 @@ const configPath = process.env.MCP_CONFIG_PATH;
595
610
  let config = {};
596
611
  try { config = JSON.parse(fs.readFileSync(configPath, 'utf8')); } catch {}
597
612
  if (!config.mcpServers) config.mcpServers = {};
598
- config.mcpServers['weppy-roblox-mcp'] = { command: 'npx', args: ['-y', '@weppy/roblox-mcp'] };
613
+ config.mcpServers['weppy-roblox-mcp'] = { command: 'npx', args: ['-y', '@weppy/roblox-mcp@latest'] };
599
614
  fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n');
600
615
  "@
601
616
  } finally {
@@ -627,7 +642,7 @@ catch {
627
642
  # ═══════════════════════════════════
628
643
  Write-Step "1/2" "Setup Roblox Studio Plugin"
629
644
 
630
- if (Confirm-Action " Run npx -y @weppy/roblox-mcp --setup?") {
645
+ if (Confirm-Action " Run npx -y @weppy/roblox-mcp@latest --setup?") {
631
646
  try {
632
647
  $npmCommandPath = Resolve-NpmCommand
633
648
  $npmDir = Split-Path $npmCommandPath -Parent
@@ -641,9 +656,11 @@ if (Confirm-Action " Run npx -y @weppy/roblox-mcp --setup?") {
641
656
  try {
642
657
  New-Item -ItemType Directory -Path $setupWorkingDir -Force | Out-Null
643
658
  Set-Location $setupWorkingDir
644
- # stdin 파이프로 격리: irm|iex 대화형 모드에서 stdio MCP 서버가
645
- # pwsh 터미널 stdin 상속받아 hang되는 문제를 방지한다
646
- $null | & $npxPath -y @weppy/roblox-mcp --setup
659
+ # Isolate stdin with an empty pipe so the stdio MCP server does not inherit
660
+ # the interactive pwsh terminal stdin (which would cause it to hang under irm|iex).
661
+ # The @latest tag forces npx to resolve from the registry instead of
662
+ # reusing an older version pinned in the npm cache.
663
+ $null | & $npxPath -y "@weppy/roblox-mcp@latest" --setup
647
664
  if ($LASTEXITCODE -ne 0) {
648
665
  Write-Warn "Setup encountered a warning (non-blocking)"
649
666
  } else {
@@ -675,9 +692,30 @@ $notDetected = @()
675
692
 
676
693
  $claudeProjectConfig = Join-Path (Get-Location).Path '.mcp.json'
677
694
  $claudeGlobalConfig = Join-Path $env:USERPROFILE '.claude\mcp.json'
678
- $claudeCodeConfigured = (Test-McpJsonConfigured $claudeProjectConfig) -or (Test-McpJsonConfigured $claudeGlobalConfig)
679
695
  $claudeCodeCliCommand = Resolve-OptionalCliCommand 'claude'
680
696
 
697
+ # `claude mcp add` stores entries under ~/.claude.json or in local/user scope,
698
+ # so prefer `claude mcp list` as the source of truth when the CLI is available
699
+ # (the JSON path checks remain as a fallback). The entry counts as configured
700
+ # only when its args carry an explicit `@<tag>` — legacy bare entries fall
701
+ # through and get re-registered with the canonical `@latest` form.
702
+ function Test-ClaudeCliConfigured($cliCommand) {
703
+ if (-not $cliCommand) { return $false }
704
+ try {
705
+ $listOutput = & $cliCommand mcp list 2>$null
706
+ if ($LASTEXITCODE -ne 0) { return $false }
707
+ $line = $listOutput | Select-String -Pattern '^weppy-roblox-mcp:' | Select-Object -First 1
708
+ if (-not $line) { return $false }
709
+ return ($line.Line -match '@weppy/roblox-mcp@')
710
+ } catch {
711
+ return $false
712
+ }
713
+ }
714
+
715
+ $claudeCodeConfigured = (Test-ClaudeCliConfigured $claudeCodeCliCommand) `
716
+ -or (Test-McpJsonConfigured $claudeProjectConfig) `
717
+ -or (Test-McpJsonConfigured $claudeGlobalConfig)
718
+
681
719
  if ($claudeCodeConfigured) {
682
720
  $detectedNames += 'Claude Code (configured)'
683
721
  $detectedTypes += 'claude-code'
@@ -762,7 +800,7 @@ else {
762
800
 
763
801
  if ($detectedNames.Count -eq 0) {
764
802
  Write-Warn "No AI apps detected"
765
- Write-Info "Register MCP server manually: npx -y @weppy/roblox-mcp"
803
+ Write-Info "Register MCP server manually: npx -y @weppy/roblox-mcp@latest"
766
804
  }
767
805
  else {
768
806
  Write-Host ""
@@ -816,12 +854,40 @@ else {
816
854
  Write-Ok "Already configured: $appName"
817
855
  }
818
856
  elseif ($claudeCodeCliCommand) {
819
- & $claudeCodeCliCommand mcp add weppy-roblox-mcp -- npx -y "@weppy/roblox-mcp"
820
- if ($LASTEXITCODE -ne 0) {
821
- # CLI 실패 (Windows에서 -- 파싱 문제 등) JSON config에 직접 쓰기로 폴백
822
- Add-McpToConfig $claudeGlobalConfig
857
+ $claudeStderrFile = Join-Path ([System.IO.Path]::GetTempPath()) ("weppy-claude-{0}.err" -f ([System.Guid]::NewGuid().ToString("N")))
858
+ try {
859
+ # Best-effort remove any legacy bare entry so the subsequent add can
860
+ # install the canonical `@latest` form.
861
+ try { & $claudeCodeCliCommand mcp remove weppy-roblox-mcp *> $null } catch {}
862
+ & $claudeCodeCliCommand mcp add weppy-roblox-mcp -- npx -y "@weppy/roblox-mcp@latest" 2> $claudeStderrFile
863
+ $claudeExit = $LASTEXITCODE
864
+ if ($claudeExit -eq 0) {
865
+ Write-Ok "Registered: $appName"
866
+ }
867
+ else {
868
+ $stderrContent = if (Test-Path $claudeStderrFile) { Get-Content $claudeStderrFile -Raw } else { '' }
869
+ if ($stderrContent -match '(?i)already exists') {
870
+ # Already registered in another scope — not a failure
871
+ Write-Ok "Already configured: $appName"
872
+ }
873
+ else {
874
+ Write-Fail "Failed: $appName (exit=$claudeExit)"
875
+ Write-Host " CLI: $claudeCodeCliCommand" -ForegroundColor DarkGray
876
+ if ($stderrContent) {
877
+ Write-Host " stderr:" -ForegroundColor DarkGray
878
+ $stderrContent.TrimEnd("`r","`n").Split("`n") | ForEach-Object {
879
+ Write-Host " $($_.TrimEnd())" -ForegroundColor DarkGray
880
+ }
881
+ }
882
+ # Fall back to writing the JSON config directly when the CLI fails for other reasons
883
+ Add-McpToConfig $claudeGlobalConfig
884
+ Write-Ok "Registered via fallback JSON: $appName"
885
+ }
886
+ }
887
+ }
888
+ finally {
889
+ Remove-Item $claudeStderrFile -ErrorAction SilentlyContinue
823
890
  }
824
- Write-Ok "Registered: $appName"
825
891
  }
826
892
  else {
827
893
  Write-Fail "Failed: $appName (CLI/config unavailable)"
@@ -842,7 +908,7 @@ else {
842
908
  }
843
909
  elseif ($codexCliCommand) {
844
910
  try { & $codexCliCommand mcp remove weppy-roblox-mcp *> $null } catch {}
845
- & $codexCliCommand mcp add weppy-roblox-mcp -- npx -y "@weppy/roblox-mcp"
911
+ & $codexCliCommand mcp add weppy-roblox-mcp -- npx -y "@weppy/roblox-mcp@latest"
846
912
  if ($LASTEXITCODE -ne 0) {
847
913
  throw 'codex mcp add failed'
848
914
  }
package/install.sh CHANGED
@@ -98,7 +98,7 @@ const configPath = process.env.MCP_CONFIG_PATH;
98
98
  let config = {};
99
99
  try { config = JSON.parse(fs.readFileSync(configPath, "utf8")); } catch {}
100
100
  if (!config.mcpServers) config.mcpServers = {};
101
- config.mcpServers["weppy-roblox-mcp"] = { command: "npx", args: ["-y", "@weppy/roblox-mcp"] };
101
+ config.mcpServers["weppy-roblox-mcp"] = { command: "npx", args: ["-y", "@weppy/roblox-mcp@latest"] };
102
102
  fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
103
103
  '
104
104
  }
@@ -142,7 +142,7 @@ const next = { ...config };
142
142
  delete next["weppy-roblox-mcp"];
143
143
  next.mcpServers = {
144
144
  ...(mcpServers || {}),
145
- "weppy-roblox-mcp": { command: "npx", args: ["-y", "@weppy/roblox-mcp"] }
145
+ "weppy-roblox-mcp": { command: "npx", args: ["-y", "@weppy/roblox-mcp@latest"] }
146
146
  };
147
147
  config = next;
148
148
  fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
@@ -161,13 +161,17 @@ function isJsonObject(value) {
161
161
  return typeof value === "object" && value !== null && !Array.isArray(value);
162
162
  }
163
163
  function hasExpectedCommandShape(value) {
164
+ // Require an explicit `@<tag>` so the installer can upgrade legacy bare
165
+ // entries (`@weppy/roblox-mcp`) — those reuse npx cache and trap users on
166
+ // outdated versions. Tagged entries (`@latest`, `@2.6.4`, …) are preserved.
164
167
  return (
165
168
  isJsonObject(value) &&
166
169
  value.command === "npx" &&
167
170
  Array.isArray(value.args) &&
168
171
  value.args.length === 2 &&
169
172
  value.args[0] === "-y" &&
170
- value.args[1] === "@weppy/roblox-mcp"
173
+ typeof value.args[1] === "string" &&
174
+ /^@weppy\/roblox-mcp@.+$/.test(value.args[1])
171
175
  );
172
176
  }
173
177
  try {
@@ -191,7 +195,8 @@ const fs = require("fs");
191
195
  const configPath = process.env.MCP_CODEX_CONFIG_PATH;
192
196
  const serverName = "weppy-roblox-mcp";
193
197
  const expectedCommand = "npx";
194
- const expectedArgs = ["-y", "@weppy/roblox-mcp"];
198
+ // Require an explicit `@<tag>` so the installer can upgrade legacy bare entries.
199
+ const packageSpecPattern = /^@weppy\/roblox-mcp@.+$/;
195
200
  const headerPattern = new RegExp(
196
201
  "^\\s*\\[\\s*mcp_servers\\." + serverName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&") + "\\s*\\]\\s*(?:#.*)?$",
197
202
  );
@@ -585,8 +590,10 @@ try {
585
590
  !parsed.hasConflict &&
586
591
  parsed.command === expectedCommand &&
587
592
  Array.isArray(parsed.args) &&
588
- parsed.args.length === expectedArgs.length &&
589
- parsed.args.every((entry, index) => entry === expectedArgs[index]);
593
+ parsed.args.length === 2 &&
594
+ parsed.args[0] === "-y" &&
595
+ typeof parsed.args[1] === "string" &&
596
+ packageSpecPattern.test(parsed.args[1]);
590
597
 
591
598
  process.exit(isConfigured ? 0 : 1);
592
599
  } catch {
@@ -640,7 +647,7 @@ success "Node.js $(node -v) detected"
640
647
  # ═══════════════════════════════════
641
648
  step "1/2" "Setup Roblox Studio Plugin"
642
649
 
643
- if confirm " Run npx -y @weppy/roblox-mcp --setup?"; then
650
+ if confirm " Run npx -y @weppy/roblox-mcp@latest --setup?"; then
644
651
  setup_tmp_dir=""
645
652
  if setup_tmp_dir=$(mktemp -d "${TMPDIR:-/tmp}/weppy-setup-XXXXXX" 2>/dev/null); then
646
653
  :
@@ -651,7 +658,9 @@ if confirm " Run npx -y @weppy/roblox-mcp --setup?"; then
651
658
  if [ -n "${setup_tmp_dir:-}" ] && [ -d "$setup_tmp_dir" ]; then
652
659
  # stdin을 /dev/null로 격리: curl|bash 파이프 모드에서 stdio MCP 서버가
653
660
  # bash의 남은 스크립트 바이트를 소비해버리는 문제를 방지한다
654
- if (cd "$setup_tmp_dir" && npx -y @weppy/roblox-mcp --setup </dev/null); then
661
+ # The @latest tag forces npx to resolve from the registry instead of
662
+ # reusing an older version pinned in the npm cache.
663
+ if (cd "$setup_tmp_dir" && npx -y "@weppy/roblox-mcp@latest" --setup </dev/null); then
655
664
  success "Setup complete"
656
665
  else
657
666
  warn "Setup encountered a warning (non-blocking)"
@@ -671,7 +680,7 @@ fi
671
680
  step "2/2" "Register MCP with AI apps"
672
681
  printf " Automatic registration: Claude Code, Claude Desktop, Cursor, Codex CLI/App, Gemini CLI, Antigravity\n"
673
682
 
674
- MCP_COMMAND='npx -y @weppy/roblox-mcp'
683
+ MCP_COMMAND='npx -y @weppy/roblox-mcp@latest'
675
684
 
676
685
  # AI app detection
677
686
  declare -a DETECTED_NAMES=()
@@ -679,11 +688,25 @@ declare -a DETECTED_TYPES=()
679
688
  declare -a NOT_DETECTED=()
680
689
 
681
690
  # Claude Code
691
+ # `claude mcp add` stores entries under ~/.claude.json or in local/user scope,
692
+ # so prefer `claude mcp list` as the source of truth when the CLI is available
693
+ # (the JSON path checks remain as a fallback).
682
694
  CLAUDE_PROJECT_MCP_CONFIG="$PWD/.mcp.json"
683
695
  CLAUDE_GLOBAL_MCP_CONFIG="$HOME/.claude/mcp.json"
684
696
  CLAUDE_CLI_COMMAND="$(resolve_optional_cli_command claude 2>/dev/null || true)"
685
697
 
686
- if is_json_mcp_configured "$CLAUDE_PROJECT_MCP_CONFIG" || is_json_mcp_configured "$CLAUDE_GLOBAL_MCP_CONFIG"; then
698
+ is_claude_cli_configured() {
699
+ [ -n "$CLAUDE_CLI_COMMAND" ] || return 1
700
+ # The entry counts as configured only when its args carry an explicit `@tag`
701
+ # (e.g. `@latest`). Legacy bare entries fall through and get re-registered.
702
+ "$CLAUDE_CLI_COMMAND" mcp list 2>/dev/null \
703
+ | grep "^weppy-roblox-mcp:" \
704
+ | grep -q "@weppy/roblox-mcp@"
705
+ }
706
+
707
+ if is_claude_cli_configured \
708
+ || is_json_mcp_configured "$CLAUDE_PROJECT_MCP_CONFIG" \
709
+ || is_json_mcp_configured "$CLAUDE_GLOBAL_MCP_CONFIG"; then
687
710
  DETECTED_NAMES+=("Claude Code (configured)")
688
711
  DETECTED_TYPES+=("claude-code")
689
712
  elif [ -n "$CLAUDE_CLI_COMMAND" ]; then
@@ -816,12 +839,34 @@ else
816
839
 
817
840
  case "$app_type" in
818
841
  claude-code)
819
- if is_json_mcp_configured "$CLAUDE_PROJECT_MCP_CONFIG" || is_json_mcp_configured "$CLAUDE_GLOBAL_MCP_CONFIG"; then
842
+ if is_claude_cli_configured \
843
+ || is_json_mcp_configured "$CLAUDE_PROJECT_MCP_CONFIG" \
844
+ || is_json_mcp_configured "$CLAUDE_GLOBAL_MCP_CONFIG"; then
820
845
  success "Already configured: $app_name"
821
- elif [ -n "$CLAUDE_CLI_COMMAND" ] && "$CLAUDE_CLI_COMMAND" mcp add weppy-roblox-mcp -- npx -y @weppy/roblox-mcp 2>/dev/null; then
822
- success "Registered: $app_name"
846
+ elif [ -n "$CLAUDE_CLI_COMMAND" ]; then
847
+ claude_stderr_file=$(mktemp "${TMPDIR:-/tmp}/weppy-claude-XXXXXX.err" 2>/dev/null || echo "${HOME}/weppy-claude.err")
848
+ # Best-effort remove any legacy bare entry so the subsequent add can
849
+ # install the canonical `@latest` form. Ignore errors when nothing
850
+ # exists.
851
+ "$CLAUDE_CLI_COMMAND" mcp remove weppy-roblox-mcp >/dev/null 2>&1 || true
852
+ # Capture the CLI exit code immediately so it isn't overwritten by the
853
+ # subsequent grep check (which would otherwise leak its own exit code).
854
+ claude_exit_code=0
855
+ "$CLAUDE_CLI_COMMAND" mcp add weppy-roblox-mcp -- npx -y "@weppy/roblox-mcp@latest" 2>"$claude_stderr_file" || claude_exit_code=$?
856
+ if [ "$claude_exit_code" -eq 0 ]; then
857
+ success "Registered: $app_name"
858
+ elif grep -qi "already exists" "$claude_stderr_file"; then
859
+ # Already registered in another scope — not a failure
860
+ success "Already configured: $app_name"
861
+ else
862
+ fail "Failed: $app_name (exit=$claude_exit_code)"
863
+ printf " CLI: %s\n" "$CLAUDE_CLI_COMMAND"
864
+ printf " stderr:\n"
865
+ sed 's/^/ /' "$claude_stderr_file" || true
866
+ fi
867
+ rm -f "$claude_stderr_file"
823
868
  else
824
- fail "Failed: $app_name"
869
+ fail "Failed: $app_name (claude CLI not found)"
825
870
  fi
826
871
  ;;
827
872
  claude-desktop)
@@ -846,7 +891,7 @@ else
846
891
  fi
847
892
  if is_codex_config_configured "$CODEX_CONFIG"; then
848
893
  :
849
- elif [ -n "$CODEX_CLI_COMMAND" ] && "$CODEX_CLI_COMMAND" mcp add weppy-roblox-mcp -- npx -y @weppy/roblox-mcp 2>/dev/null; then
894
+ elif [ -n "$CODEX_CLI_COMMAND" ] && "$CODEX_CLI_COMMAND" mcp add weppy-roblox-mcp -- npx -y "@weppy/roblox-mcp@latest" 2>/dev/null; then
850
895
  success "Registered: $app_name"
851
896
  elif is_codex_config_configured "$CODEX_CONFIG"; then
852
897
  success "Already configured: $app_name"
package/llms-full.txt CHANGED
@@ -1,6 +1,6 @@
1
1
  # WEPPY — Canonical Web Docs
2
2
 
3
- > WEPPY is an MCP server that lets AI coding agents control a live Roblox Studio session and interact with scripts, instances, terrain, lighting, assets, audio, and animations.
3
+ > WEPPY is an MCP server that lets AI coding agents control a live Roblox Studio session and interact with scripts, instances, terrain, lighting, assets, audio, animations, and UI Studio workflows.
4
4
 
5
5
  This package intentionally does not ship a standalone full documentation surface.
6
6
 
@@ -11,4 +11,3 @@ Canonical documentation lives on the web:
11
11
  - Pro plans: https://weppyai.com/en/plans
12
12
 
13
13
  For app-specific setup, open the web install page and choose the relevant AI client guide.
14
-
package/llms.txt CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  > WEPPY is an MCP server that lets AI coding agents (Claude, Codex,
4
4
  > Cursor, Gemini) control a live Roblox Studio session — create and edit scripts,
5
- > instances, terrain, lighting, assets, audio, and animations via natural language.
5
+ > instances, terrain, lighting, assets, audio, animations, and UI Studio workflows via natural language.
6
6
 
7
7
  ## What it does
8
8
 
@@ -14,6 +14,7 @@ terrain, and more — inside a live Studio session.
14
14
  - Action-based MCP tools covering the full Roblox Studio API surface
15
15
  - Bidirectional project sync between Roblox Studio and local files (Pro)
16
16
  - Automated playtest: AI starts/stops Play/Run, injects test scripts, collects logs
17
+ - UI Studio: AI creates in-game UI that matches your game's style or analyzes existing UI and suggests improvements
17
18
  - Web dashboard: monitor AI actions, tool history, sync state, and changelog in real time at `http://localhost:3002`
18
19
  - Multi-place support (up to 3 places simultaneously)
19
20
  - Roblox Explorer VSCode extension for browsing Studio hierarchy
@@ -40,7 +41,7 @@ Safari / Firefox users should open the web install page and follow the browser-s
40
41
  ## Tiers
41
42
 
42
43
  - Basic (Free): MCP tool execution + one-way Studio -> Local sync
43
- - Pro: Bidirectional sync, bulk actions, terrain generation, spatial analysis, multi-place support, advanced tools
44
+ - Pro: Bidirectional sync, UI Studio, bulk actions, terrain generation, spatial analysis, multi-place support, advanced tools
44
45
 
45
46
  ## Detailed docs
46
47
 
@@ -56,7 +57,7 @@ Safari / Firefox users should open the web install page and follow the browser-s
56
57
  ## Optional
57
58
 
58
59
  - Roblox Explorer VSCode extension (browse synced instance tree)
59
- - Pro subscription for bidirectional sync and advanced tools
60
+ - Pro subscription for bidirectional sync, UI Studio, and advanced tools
60
61
 
61
62
  ## Links
62
63
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@weppy/roblox-mcp",
3
- "version": "2.6.4",
3
+ "version": "2.7.1",
4
4
  "description": "MCP (Model Context Protocol) server for Roblox Studio integration - enables AI coding agents to interact with Roblox Studio in real-time",
5
5
  "main": "plugins/weppy-roblox-mcp/dist/index.js",
6
6
  "type": "module",
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "weppy-roblox-mcp",
3
3
  "description": "MCP server for Roblox Studio integration - AI-powered game development with specialized agents and skills",
4
- "version": "2.6.4",
4
+ "version": "2.7.1",
5
5
  "author": {
6
6
  "name": "hope1026"
7
7
  },
@@ -1 +1 @@
1
- import{r,a as R,u as M,b as B,c as V,j as e}from"./index-B-nqZCE3.js";import{I as G}from"./InfoLabel-CMSu30c4.js";import{G as D}from"./GameChangeDetail-DGphCFbI.js";import{T as m}from"./TooltipText-Bnvm1FcC.js";function F(n){const s={scriptsModified:0,scriptsCreated:0,instancesCreated:0,instancesDeleted:0,instancesMoved:0,propertiesChanged:0,lightingChanged:!1,terrainChanged:!1,assetsInserted:0};for(const l of n)switch(l.category){case"script":l.changeType==="create"?s.scriptsCreated++:s.scriptsModified++;break;case"instance":l.changeType==="create"?s.instancesCreated++:l.changeType==="delete"?s.instancesDeleted++:l.changeType==="move"&&s.instancesMoved++;break;case"property":s.propertiesChanged++;break;case"lighting":s.lightingChanged=!0;break;case"terrain":s.terrainChanged=!0;break;case"asset":s.assetsInserted++;break}return s}function P(n){const[s,l]=r.useState(""),[a,d]=r.useState(""),[u,q]=r.useState(),[w,y]=r.useState("completed"),[I,E]=r.useState([]),[L,O]=r.useState([]),[x,f]=r.useState([]),[p,_]=r.useState(),[j,b]=r.useState(),[v,S]=r.useState(),[C,N]=r.useState({scriptsModified:0,scriptsCreated:0,instancesCreated:0,instancesDeleted:0,instancesMoved:0,propertiesChanged:0,lightingChanged:!1,terrainChanged:!1,assetsInserted:0}),[T,g]=r.useState(!0),[k,h]=r.useState(null),i=r.useCallback(async()=>{if(n){g(!0),h(null);try{const[c,o]=await Promise.all([R.get(`/api/dashboard/changelog/${n}`),R.get(`/api/dashboard/changelog/${n}/changes`)]);l(c.entryId),d(c.startTime),q(c.endTime),y(c.status),E(c.entries),O(c.failures),_(c.contextSummary),b(c.replayMetadata),S(c.verificationSummary),f(o.changes),N(F(o.changes))}catch(c){h(c instanceof Error?c.message:"Failed to load changelog detail")}finally{g(!1)}}},[n]);return r.useEffect(()=>{i()},[i]),{entryId:s,startTime:a,endTime:u,status:w,entries:I,failures:L,changes:x,changeSummary:C,contextSummary:p,replayMetadata:j,verificationSummary:v,loading:T,error:k,refresh:i}}const U={script:"📝",instance:"🧱",property:"🎨",lighting:"🌅",terrain:"⛰️",asset:"📦"};function H(n){return U[n]??"❓"}const W="_page_q2jbi_2",Y="_header_q2jbi_10",z="_backLink_q2jbi_16",J="_headerTitle_q2jbi_29",Q="_headerTime_q2jbi_37",X="_statusActive_q2jbi_44",Z="_statusCompleted_q2jbi_49",ee="_section_q2jbi_54",te="_sectionTitle_q2jbi_61",ne="_summaryGrid_q2jbi_74",ae="_summaryCard_q2jbi_80",ie="_summaryCardActive_q2jbi_94",se="_summaryIcon_q2jbi_99",ce="_summaryCount_q2jbi_105",re="_summaryLabel_q2jbi_112",oe="_contextGrid_q2jbi_121",le="_contextRow_q2jbi_127",de="_contextKey_q2jbi_133",me="_contextValue_q2jbi_141",ge="_timelineFilter_q2jbi_149",he="_filterLabel_q2jbi_156",ue="_filterSelect_q2jbi_162",ye="_timeline_q2jbi_149",xe="_timelineEntry_q2jbi_182",fe="_timelineTime_q2jbi_199",pe="_timelineIcon_q2jbi_207",_e="_timelineBody_q2jbi_212",je="_timelineSummary_q2jbi_217",be="_timelineTarget_q2jbi_224",ve="_timelineConfidence_q2jbi_231",Se="_confidenceExact_q2jbi_240",Ce="_confidencePartial_q2jbi_245",Ne="_confidenceAfterOnly_q2jbi_250",Te="_confidenceIntentOnly_q2jbi_255",ke="_confidenceUnknown_q2jbi_260",qe="_timelineExpanded_q2jbi_266",we="_empty_q2jbi_338",Ie="_loading_q2jbi_347",Ee="_error_q2jbi_356",t={page:W,header:Y,backLink:z,headerTitle:J,headerTime:Q,statusActive:X,statusCompleted:Z,section:ee,sectionTitle:te,summaryGrid:ne,summaryCard:ae,summaryCardActive:ie,summaryIcon:se,summaryCount:ce,summaryLabel:re,contextGrid:oe,contextRow:le,contextKey:de,contextValue:me,timelineFilter:ge,filterLabel:he,filterSelect:ue,timeline:ye,timelineEntry:xe,timelineTime:fe,timelineIcon:pe,timelineBody:_e,timelineSummary:je,timelineTarget:be,timelineConfidence:ve,confidenceExact:Se,confidencePartial:Ce,confidenceAfterOnly:Ne,confidenceIntentOnly:Te,confidenceUnknown:ke,timelineExpanded:qe,empty:we,loading:Ie,error:Ee},$=[{key:"script",icon:"📝",labelKey:"changelog.category.script"},{key:"instance",icon:"🧱",labelKey:"changelog.category.instance"},{key:"property",icon:"🎨",labelKey:"changelog.category.property"},{key:"lighting",icon:"🌅",labelKey:"changelog.category.lighting"},{key:"terrain",icon:"⛰️",labelKey:"changelog.category.terrain"},{key:"asset",icon:"📦",labelKey:"changelog.category.asset"}];function A(n){if(!n)return"--:--";const s=new Date(n);return`${String(s.getHours()).padStart(2,"0")}:${String(s.getMinutes()).padStart(2,"0")}`}function Le(n){if(!n)return"--:--:--";const s=new Date(n);return`${String(s.getHours()).padStart(2,"0")}:${String(s.getMinutes()).padStart(2,"0")}:${String(s.getSeconds()).padStart(2,"0")}`}function Oe(n,s){if(!n||!s)return"";const l=new Date(s).getTime()-new Date(n).getTime();return l<0?"":`${Math.round(l/6e4)}min`}function Ae(n){switch(n){case"exact":return t.confidenceExact;case"partial":return t.confidencePartial;case"after-only":return t.confidenceAfterOnly;case"intent-only":return t.confidenceIntentOnly;default:return t.confidenceUnknown}}function Ke(n,s){switch(s){case"exact":return n("changelog.detail.confidence.exact","Exact");case"partial":return n("changelog.detail.confidence.partial","Partial");case"after-only":return n("changelog.detail.confidence.afterOnly","After only");case"intent-only":return n("changelog.detail.confidence.intentOnly","Intent only");default:return n("changelog.detail.confidence.unknown","Unknown")}}function Re(n,s){switch(s){case"exact":return n("changelog.detail.confidence.exact.tooltip","Both the before and after state were confirmed for this change.");case"partial":return n("changelog.detail.confidence.partial.tooltip","Only part of the before and after state could be confirmed for this change.");case"after-only":return n("changelog.detail.confidence.afterOnly.tooltip","Only the resulting state after the change could be confirmed.");case"intent-only":return n("changelog.detail.confidence.intentOnly.tooltip","Only the requested action was recorded, not the resulting state.");default:return n("changelog.detail.confidence.unknown.tooltip","This change could not be confidently classified from the available data.")}}function Ge(){var x,f,p,_,j,b,v,S,C,N,T,g,k,h;const{t:n}=M(),{id:s}=B(),l=V(),a=P(s),[d,u]=r.useState("all"),[q,w]=r.useState(null),y=r.useMemo(()=>[...d==="all"?a.changes:a.changes.filter(c=>c.category===d)].reverse(),[a.changes,d]);if(a.loading)return e.jsx("div",{className:t.loading,children:n("common.loading","Loading...")});if(a.error)return e.jsxs("div",{className:t.error,children:[a.error,e.jsx("br",{}),e.jsxs("span",{className:t.backLink,onClick:()=>l("/changelog"),children:["←"," ",n("changelog.detail.backToList","Back to list")]})]});const I=Oe(a.startTime,a.endTime),E=a.endTime?`${A(a.startTime)} → ${A(a.endTime)} (${I})`:`${A(a.startTime)} → ${n("changelog.card.inProgress","in progress")}`,L=!!((x=a.contextSummary)!=null&&x.intent||(f=a.contextSummary)!=null&&f.testScenario||(p=a.contextSummary)!=null&&p.expectedBehavior||(_=a.contextSummary)!=null&&_.observedBehavior),O=!!((j=a.verificationSummary)!=null&&j.label||(b=a.verificationSummary)!=null&&b.status||(v=a.verificationSummary)!=null&&v.testTimestamp);return e.jsxs("div",{className:t.page,children:[e.jsxs("div",{className:t.header,children:[e.jsxs("span",{className:t.backLink,onClick:()=>l("/changelog"),children:["←"," ",n("sidebar.changelog","Changelog")]}),e.jsx("span",{className:t.headerTitle,children:"|"}),e.jsx("span",{className:t.headerTime,children:E}),e.jsx(m,{text:a.status==="active"?n("changelog.card.active.tooltip","This session is still receiving new game changes."):n("changelog.card.completed.tooltip","This session has ended and no more changes are expected."),children:e.jsx("span",{className:a.status==="active"?t.statusActive:t.statusCompleted,children:a.status==="active"?n("changelog.card.active","Active"):n("changelog.card.completed","Completed")})})]}),e.jsxs("div",{className:t.section,children:[e.jsx("div",{className:t.sectionTitle,children:e.jsx(m,{text:n("changelog.detail.changeSummary.tooltip","Counts of extracted game changes grouped by category for this session."),children:e.jsx("span",{children:n("changelog.detail.changeSummary","Change Summary")})})}),e.jsx("div",{className:t.summaryGrid,children:$.map(i=>{const c=a.changeSummary;let o;switch(i.key){case"script":o=c.scriptsModified+c.scriptsCreated;break;case"instance":o=c.instancesCreated+c.instancesDeleted+c.instancesMoved;break;case"property":o=c.propertiesChanged;break;case"lighting":o=c.lightingChanged?1:0;break;case"terrain":o=c.terrainChanged?1:0;break;case"asset":o=c.assetsInserted;break;default:o=0}const K=d===i.key;return e.jsxs("div",{className:`${t.summaryCard} ${K?t.summaryCardActive:""}`,onClick:()=>u(K?"all":i.key),children:[e.jsx("span",{className:t.summaryIcon,children:i.icon}),e.jsx("div",{className:t.summaryCount,children:o}),e.jsx("div",{className:t.summaryLabel,children:n(i.labelKey,i.key)})]},i.key)})})]}),L&&e.jsxs("div",{className:t.section,children:[e.jsx("div",{className:t.sectionTitle,children:e.jsx(m,{text:n("changelog.detail.context.tooltip","Structured execution context captured for this changelog session."),children:e.jsx("span",{children:n("changelog.detail.context.title","Context Summary")})})}),e.jsxs("div",{className:t.contextGrid,children:[((S=a.contextSummary)==null?void 0:S.intent)&&e.jsxs("div",{className:t.contextRow,children:[e.jsx("span",{className:t.contextKey,children:n("changelog.card.sessionIntent","Session intent")}),e.jsx("span",{className:t.contextValue,children:a.contextSummary.intent})]}),((C=a.contextSummary)==null?void 0:C.testScenario)&&e.jsxs("div",{className:t.contextRow,children:[e.jsx("span",{className:t.contextKey,children:n("playtest.context.why","Why this test ran")}),e.jsx("span",{className:t.contextValue,children:a.contextSummary.testScenario})]}),((N=a.contextSummary)==null?void 0:N.expectedBehavior)&&e.jsxs("div",{className:t.contextRow,children:[e.jsx("span",{className:t.contextKey,children:n("playtest.context.expected","Expected")}),e.jsx("span",{className:t.contextValue,children:a.contextSummary.expectedBehavior})]}),((T=a.contextSummary)==null?void 0:T.observedBehavior)&&e.jsxs("div",{className:t.contextRow,children:[e.jsx("span",{className:t.contextKey,children:n("playtest.context.observed","Observed")}),e.jsx("span",{className:t.contextValue,children:a.contextSummary.observedBehavior})]})]})]}),O&&e.jsxs("div",{className:t.section,children:[e.jsx("div",{className:t.sectionTitle,children:e.jsx(m,{text:n("changelog.detail.verification.tooltip","Verification signals linked to this changelog session."),children:e.jsx("span",{children:n("changelog.detail.verification.title","Verification")})})}),e.jsxs("div",{className:t.contextGrid,children:[((g=a.verificationSummary)==null?void 0:g.label)&&e.jsxs("div",{className:t.contextRow,children:[e.jsx("span",{className:t.contextKey,children:n("changelog.detail.verification.label","Result")}),e.jsx("span",{className:t.contextValue,children:a.verificationSummary.label})]}),((k=a.verificationSummary)==null?void 0:k.status)&&e.jsxs("div",{className:t.contextRow,children:[e.jsx("span",{className:t.contextKey,children:n("changelog.detail.verification.status","Status")}),e.jsx("span",{className:t.contextValue,children:a.verificationSummary.status})]}),((h=a.verificationSummary)==null?void 0:h.testTimestamp)&&e.jsxs("div",{className:t.contextRow,children:[e.jsx("span",{className:t.contextKey,children:n("changelog.detail.verification.timestamp","Recorded at")}),e.jsx("span",{className:t.contextValue,children:a.verificationSummary.testTimestamp})]})]})]}),e.jsxs("div",{className:t.section,children:[e.jsx("div",{className:t.sectionTitle,children:e.jsx(m,{text:n("changelog.detail.changeTimeline.tooltip","Chronological list of extracted game changes for this session."),children:e.jsx("span",{children:n("changelog.detail.changeTimeline","Change Timeline")})})}),e.jsxs("div",{className:t.timelineFilter,children:[e.jsx("span",{className:t.filterLabel,children:e.jsx(G,{label:`${n("changelog.detail.filterCategory","Category")}:`,tooltip:n("changelog.detail.filterCategory.tooltip","Filter the timeline to a single change category.")})}),e.jsxs("select",{className:t.filterSelect,value:d,onChange:i=>u(i.target.value),children:[e.jsx("option",{value:"all",children:n("tools.filter.all","All")}),$.map(i=>e.jsxs("option",{value:i.key,children:[i.icon," ",n(i.labelKey,i.key)]},i.key))]})]}),y.length===0?e.jsx("div",{className:t.empty,children:n("changelog.detail.noChanges","No changes in this category")}):e.jsx("div",{className:t.timeline,children:y.map((i,c)=>{const o=q===c;return e.jsxs("div",{children:[e.jsxs("div",{className:t.timelineEntry,onClick:()=>w(o?null:c),children:[e.jsx("span",{className:t.timelineTime,children:Le(i.timestamp)}),e.jsx("span",{className:t.timelineIcon,children:H(i.category)}),e.jsxs("div",{className:t.timelineBody,children:[e.jsxs("div",{className:t.timelineSummary,children:[i.summary,e.jsx(m,{text:Re(n,i.confidence),children:e.jsx("span",{className:`${t.timelineConfidence} ${Ae(i.confidence)}`,children:Ke(n,i.confidence)})})]}),e.jsx("div",{className:t.timelineTarget,children:i.target})]})]}),o&&e.jsx("div",{className:t.timelineExpanded,children:e.jsx(D,{change:i})})]},c)})})]})]})}export{Ge as Component};
1
+ import{r,a as R,u as M,b as B,c as V,j as e}from"./index-FfT2oWAB.js";import{I as G}from"./InfoLabel-B9KT3kfU.js";import{G as D}from"./GameChangeDetail-D1kE2L_v.js";import{T as m}from"./TooltipText-Baw38jOU.js";function F(n){const s={scriptsModified:0,scriptsCreated:0,instancesCreated:0,instancesDeleted:0,instancesMoved:0,propertiesChanged:0,lightingChanged:!1,terrainChanged:!1,assetsInserted:0};for(const l of n)switch(l.category){case"script":l.changeType==="create"?s.scriptsCreated++:s.scriptsModified++;break;case"instance":l.changeType==="create"?s.instancesCreated++:l.changeType==="delete"?s.instancesDeleted++:l.changeType==="move"&&s.instancesMoved++;break;case"property":s.propertiesChanged++;break;case"lighting":s.lightingChanged=!0;break;case"terrain":s.terrainChanged=!0;break;case"asset":s.assetsInserted++;break}return s}function P(n){const[s,l]=r.useState(""),[a,d]=r.useState(""),[u,q]=r.useState(),[w,y]=r.useState("completed"),[I,E]=r.useState([]),[L,O]=r.useState([]),[x,f]=r.useState([]),[p,_]=r.useState(),[j,b]=r.useState(),[v,S]=r.useState(),[C,N]=r.useState({scriptsModified:0,scriptsCreated:0,instancesCreated:0,instancesDeleted:0,instancesMoved:0,propertiesChanged:0,lightingChanged:!1,terrainChanged:!1,assetsInserted:0}),[T,g]=r.useState(!0),[k,h]=r.useState(null),i=r.useCallback(async()=>{if(n){g(!0),h(null);try{const[c,o]=await Promise.all([R.get(`/api/dashboard/changelog/${n}`),R.get(`/api/dashboard/changelog/${n}/changes`)]);l(c.entryId),d(c.startTime),q(c.endTime),y(c.status),E(c.entries),O(c.failures),_(c.contextSummary),b(c.replayMetadata),S(c.verificationSummary),f(o.changes),N(F(o.changes))}catch(c){h(c instanceof Error?c.message:"Failed to load changelog detail")}finally{g(!1)}}},[n]);return r.useEffect(()=>{i()},[i]),{entryId:s,startTime:a,endTime:u,status:w,entries:I,failures:L,changes:x,changeSummary:C,contextSummary:p,replayMetadata:j,verificationSummary:v,loading:T,error:k,refresh:i}}const U={script:"📝",instance:"🧱",property:"🎨",lighting:"🌅",terrain:"⛰️",asset:"📦"};function H(n){return U[n]??"❓"}const W="_page_q2jbi_2",Y="_header_q2jbi_10",z="_backLink_q2jbi_16",J="_headerTitle_q2jbi_29",Q="_headerTime_q2jbi_37",X="_statusActive_q2jbi_44",Z="_statusCompleted_q2jbi_49",ee="_section_q2jbi_54",te="_sectionTitle_q2jbi_61",ne="_summaryGrid_q2jbi_74",ae="_summaryCard_q2jbi_80",ie="_summaryCardActive_q2jbi_94",se="_summaryIcon_q2jbi_99",ce="_summaryCount_q2jbi_105",re="_summaryLabel_q2jbi_112",oe="_contextGrid_q2jbi_121",le="_contextRow_q2jbi_127",de="_contextKey_q2jbi_133",me="_contextValue_q2jbi_141",ge="_timelineFilter_q2jbi_149",he="_filterLabel_q2jbi_156",ue="_filterSelect_q2jbi_162",ye="_timeline_q2jbi_149",xe="_timelineEntry_q2jbi_182",fe="_timelineTime_q2jbi_199",pe="_timelineIcon_q2jbi_207",_e="_timelineBody_q2jbi_212",je="_timelineSummary_q2jbi_217",be="_timelineTarget_q2jbi_224",ve="_timelineConfidence_q2jbi_231",Se="_confidenceExact_q2jbi_240",Ce="_confidencePartial_q2jbi_245",Ne="_confidenceAfterOnly_q2jbi_250",Te="_confidenceIntentOnly_q2jbi_255",ke="_confidenceUnknown_q2jbi_260",qe="_timelineExpanded_q2jbi_266",we="_empty_q2jbi_338",Ie="_loading_q2jbi_347",Ee="_error_q2jbi_356",t={page:W,header:Y,backLink:z,headerTitle:J,headerTime:Q,statusActive:X,statusCompleted:Z,section:ee,sectionTitle:te,summaryGrid:ne,summaryCard:ae,summaryCardActive:ie,summaryIcon:se,summaryCount:ce,summaryLabel:re,contextGrid:oe,contextRow:le,contextKey:de,contextValue:me,timelineFilter:ge,filterLabel:he,filterSelect:ue,timeline:ye,timelineEntry:xe,timelineTime:fe,timelineIcon:pe,timelineBody:_e,timelineSummary:je,timelineTarget:be,timelineConfidence:ve,confidenceExact:Se,confidencePartial:Ce,confidenceAfterOnly:Ne,confidenceIntentOnly:Te,confidenceUnknown:ke,timelineExpanded:qe,empty:we,loading:Ie,error:Ee},$=[{key:"script",icon:"📝",labelKey:"changelog.category.script"},{key:"instance",icon:"🧱",labelKey:"changelog.category.instance"},{key:"property",icon:"🎨",labelKey:"changelog.category.property"},{key:"lighting",icon:"🌅",labelKey:"changelog.category.lighting"},{key:"terrain",icon:"⛰️",labelKey:"changelog.category.terrain"},{key:"asset",icon:"📦",labelKey:"changelog.category.asset"}];function A(n){if(!n)return"--:--";const s=new Date(n);return`${String(s.getHours()).padStart(2,"0")}:${String(s.getMinutes()).padStart(2,"0")}`}function Le(n){if(!n)return"--:--:--";const s=new Date(n);return`${String(s.getHours()).padStart(2,"0")}:${String(s.getMinutes()).padStart(2,"0")}:${String(s.getSeconds()).padStart(2,"0")}`}function Oe(n,s){if(!n||!s)return"";const l=new Date(s).getTime()-new Date(n).getTime();return l<0?"":`${Math.round(l/6e4)}min`}function Ae(n){switch(n){case"exact":return t.confidenceExact;case"partial":return t.confidencePartial;case"after-only":return t.confidenceAfterOnly;case"intent-only":return t.confidenceIntentOnly;default:return t.confidenceUnknown}}function Ke(n,s){switch(s){case"exact":return n("changelog.detail.confidence.exact","Exact");case"partial":return n("changelog.detail.confidence.partial","Partial");case"after-only":return n("changelog.detail.confidence.afterOnly","After only");case"intent-only":return n("changelog.detail.confidence.intentOnly","Intent only");default:return n("changelog.detail.confidence.unknown","Unknown")}}function Re(n,s){switch(s){case"exact":return n("changelog.detail.confidence.exact.tooltip","Both the before and after state were confirmed for this change.");case"partial":return n("changelog.detail.confidence.partial.tooltip","Only part of the before and after state could be confirmed for this change.");case"after-only":return n("changelog.detail.confidence.afterOnly.tooltip","Only the resulting state after the change could be confirmed.");case"intent-only":return n("changelog.detail.confidence.intentOnly.tooltip","Only the requested action was recorded, not the resulting state.");default:return n("changelog.detail.confidence.unknown.tooltip","This change could not be confidently classified from the available data.")}}function Ge(){var x,f,p,_,j,b,v,S,C,N,T,g,k,h;const{t:n}=M(),{id:s}=B(),l=V(),a=P(s),[d,u]=r.useState("all"),[q,w]=r.useState(null),y=r.useMemo(()=>[...d==="all"?a.changes:a.changes.filter(c=>c.category===d)].reverse(),[a.changes,d]);if(a.loading)return e.jsx("div",{className:t.loading,children:n("common.loading","Loading...")});if(a.error)return e.jsxs("div",{className:t.error,children:[a.error,e.jsx("br",{}),e.jsxs("span",{className:t.backLink,onClick:()=>l("/changelog"),children:["←"," ",n("changelog.detail.backToList","Back to list")]})]});const I=Oe(a.startTime,a.endTime),E=a.endTime?`${A(a.startTime)} → ${A(a.endTime)} (${I})`:`${A(a.startTime)} → ${n("changelog.card.inProgress","in progress")}`,L=!!((x=a.contextSummary)!=null&&x.intent||(f=a.contextSummary)!=null&&f.testScenario||(p=a.contextSummary)!=null&&p.expectedBehavior||(_=a.contextSummary)!=null&&_.observedBehavior),O=!!((j=a.verificationSummary)!=null&&j.label||(b=a.verificationSummary)!=null&&b.status||(v=a.verificationSummary)!=null&&v.testTimestamp);return e.jsxs("div",{className:t.page,children:[e.jsxs("div",{className:t.header,children:[e.jsxs("span",{className:t.backLink,onClick:()=>l("/changelog"),children:["←"," ",n("sidebar.changelog","Changelog")]}),e.jsx("span",{className:t.headerTitle,children:"|"}),e.jsx("span",{className:t.headerTime,children:E}),e.jsx(m,{text:a.status==="active"?n("changelog.card.active.tooltip","This session is still receiving new game changes."):n("changelog.card.completed.tooltip","This session has ended and no more changes are expected."),children:e.jsx("span",{className:a.status==="active"?t.statusActive:t.statusCompleted,children:a.status==="active"?n("changelog.card.active","Active"):n("changelog.card.completed","Completed")})})]}),e.jsxs("div",{className:t.section,children:[e.jsx("div",{className:t.sectionTitle,children:e.jsx(m,{text:n("changelog.detail.changeSummary.tooltip","Counts of extracted game changes grouped by category for this session."),children:e.jsx("span",{children:n("changelog.detail.changeSummary","Change Summary")})})}),e.jsx("div",{className:t.summaryGrid,children:$.map(i=>{const c=a.changeSummary;let o;switch(i.key){case"script":o=c.scriptsModified+c.scriptsCreated;break;case"instance":o=c.instancesCreated+c.instancesDeleted+c.instancesMoved;break;case"property":o=c.propertiesChanged;break;case"lighting":o=c.lightingChanged?1:0;break;case"terrain":o=c.terrainChanged?1:0;break;case"asset":o=c.assetsInserted;break;default:o=0}const K=d===i.key;return e.jsxs("div",{className:`${t.summaryCard} ${K?t.summaryCardActive:""}`,onClick:()=>u(K?"all":i.key),children:[e.jsx("span",{className:t.summaryIcon,children:i.icon}),e.jsx("div",{className:t.summaryCount,children:o}),e.jsx("div",{className:t.summaryLabel,children:n(i.labelKey,i.key)})]},i.key)})})]}),L&&e.jsxs("div",{className:t.section,children:[e.jsx("div",{className:t.sectionTitle,children:e.jsx(m,{text:n("changelog.detail.context.tooltip","Structured execution context captured for this changelog session."),children:e.jsx("span",{children:n("changelog.detail.context.title","Context Summary")})})}),e.jsxs("div",{className:t.contextGrid,children:[((S=a.contextSummary)==null?void 0:S.intent)&&e.jsxs("div",{className:t.contextRow,children:[e.jsx("span",{className:t.contextKey,children:n("changelog.card.sessionIntent","Session intent")}),e.jsx("span",{className:t.contextValue,children:a.contextSummary.intent})]}),((C=a.contextSummary)==null?void 0:C.testScenario)&&e.jsxs("div",{className:t.contextRow,children:[e.jsx("span",{className:t.contextKey,children:n("playtest.context.why","Why this test ran")}),e.jsx("span",{className:t.contextValue,children:a.contextSummary.testScenario})]}),((N=a.contextSummary)==null?void 0:N.expectedBehavior)&&e.jsxs("div",{className:t.contextRow,children:[e.jsx("span",{className:t.contextKey,children:n("playtest.context.expected","Expected")}),e.jsx("span",{className:t.contextValue,children:a.contextSummary.expectedBehavior})]}),((T=a.contextSummary)==null?void 0:T.observedBehavior)&&e.jsxs("div",{className:t.contextRow,children:[e.jsx("span",{className:t.contextKey,children:n("playtest.context.observed","Observed")}),e.jsx("span",{className:t.contextValue,children:a.contextSummary.observedBehavior})]})]})]}),O&&e.jsxs("div",{className:t.section,children:[e.jsx("div",{className:t.sectionTitle,children:e.jsx(m,{text:n("changelog.detail.verification.tooltip","Verification signals linked to this changelog session."),children:e.jsx("span",{children:n("changelog.detail.verification.title","Verification")})})}),e.jsxs("div",{className:t.contextGrid,children:[((g=a.verificationSummary)==null?void 0:g.label)&&e.jsxs("div",{className:t.contextRow,children:[e.jsx("span",{className:t.contextKey,children:n("changelog.detail.verification.label","Result")}),e.jsx("span",{className:t.contextValue,children:a.verificationSummary.label})]}),((k=a.verificationSummary)==null?void 0:k.status)&&e.jsxs("div",{className:t.contextRow,children:[e.jsx("span",{className:t.contextKey,children:n("changelog.detail.verification.status","Status")}),e.jsx("span",{className:t.contextValue,children:a.verificationSummary.status})]}),((h=a.verificationSummary)==null?void 0:h.testTimestamp)&&e.jsxs("div",{className:t.contextRow,children:[e.jsx("span",{className:t.contextKey,children:n("changelog.detail.verification.timestamp","Recorded at")}),e.jsx("span",{className:t.contextValue,children:a.verificationSummary.testTimestamp})]})]})]}),e.jsxs("div",{className:t.section,children:[e.jsx("div",{className:t.sectionTitle,children:e.jsx(m,{text:n("changelog.detail.changeTimeline.tooltip","Chronological list of extracted game changes for this session."),children:e.jsx("span",{children:n("changelog.detail.changeTimeline","Change Timeline")})})}),e.jsxs("div",{className:t.timelineFilter,children:[e.jsx("span",{className:t.filterLabel,children:e.jsx(G,{label:`${n("changelog.detail.filterCategory","Category")}:`,tooltip:n("changelog.detail.filterCategory.tooltip","Filter the timeline to a single change category.")})}),e.jsxs("select",{className:t.filterSelect,value:d,onChange:i=>u(i.target.value),children:[e.jsx("option",{value:"all",children:n("tools.filter.all","All")}),$.map(i=>e.jsxs("option",{value:i.key,children:[i.icon," ",n(i.labelKey,i.key)]},i.key))]})]}),y.length===0?e.jsx("div",{className:t.empty,children:n("changelog.detail.noChanges","No changes in this category")}):e.jsx("div",{className:t.timeline,children:y.map((i,c)=>{const o=q===c;return e.jsxs("div",{children:[e.jsxs("div",{className:t.timelineEntry,onClick:()=>w(o?null:c),children:[e.jsx("span",{className:t.timelineTime,children:Le(i.timestamp)}),e.jsx("span",{className:t.timelineIcon,children:H(i.category)}),e.jsxs("div",{className:t.timelineBody,children:[e.jsxs("div",{className:t.timelineSummary,children:[i.summary,e.jsx(m,{text:Re(n,i.confidence),children:e.jsx("span",{className:`${t.timelineConfidence} ${Ae(i.confidence)}`,children:Ke(n,i.confidence)})})]}),e.jsx("div",{className:t.timelineTarget,children:i.target})]})]}),o&&e.jsx("div",{className:t.timelineExpanded,children:e.jsx(D,{change:i})})]},c)})})]})]})}export{Ge as Component};