Skip to content

Commit 30fe8db

Browse files
authored
Merge pull request #77 from bluemoonfoundry/syntax-attention-enhancements
Syntax attention enhancements
2 parents 5860224 + eafeb95 commit 30fe8db

17 files changed

+2370
-665
lines changed

App.tsx

Lines changed: 48 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,8 @@ import SceneComposer from './components/SceneComposer';
2222
import ImageMapComposer from './components/ImageMapComposer';
2323
import ScreenLayoutComposer from './components/ScreenLayoutComposer';
2424
import MarkdownPreviewView from './components/MarkdownPreviewView';
25-
import PunchlistManager from './components/PunchlistManager';
25+
import DiagnosticsPanel from './components/DiagnosticsPanel';
26+
import { useDiagnostics, migratePunchlistToTasks } from './hooks/useDiagnostics';
2627
import TabContextMenu from './components/TabContextMenu';
2728
import Sash from './components/Sash';
2829
import StatusBar from './components/StatusBar';
@@ -38,7 +39,7 @@ import type {
3839
Block, BlockGroup, Link, Position, FileSystemTreeNode, EditorTab,
3940
ToastMessage, IdeSettings, Theme, ProjectImage, RenpyAudio,
4041
ClipboardState, ImageMetadata, AudioMetadata, LabelNode, Character,
41-
AppSettings, ProjectSettings, StickyNote, SceneComposition, SceneSprite, ImageMapComposition, ScreenLayoutComposition, PunchlistMetadata, MouseGestureSettings,
42+
AppSettings, ProjectSettings, StickyNote, SceneComposition, SceneSprite, ImageMapComposition, ScreenLayoutComposition, PunchlistMetadata, DiagnosticsTask, MouseGestureSettings,
4243
ProjectLoadResult, ScannedImageAsset, ScannedAudioAsset, SerializedSprite, SerializedSceneComposition, UserSnippet
4344
} from './types';
4445
import * as monaco from 'monaco-editor/esm/vs/editor/editor.api';
@@ -383,8 +384,10 @@ const App: React.FC = () => {
383384
// Screen Layout Composer State
384385
const [screenLayoutCompositions, setScreenLayoutCompositions] = useImmer<Record<string, ScreenLayoutComposition>>({});
385386

386-
// Punchlist State
387+
// Punchlist State (kept for migration — not written on save)
387388
const [punchlistMetadata, setPunchlistMetadata] = useImmer<Record<string, PunchlistMetadata>>({});
389+
// Diagnostics Tasks State
390+
const [diagnosticsTasks, setDiagnosticsTasks] = useImmer<DiagnosticsTask[]>([]);
388391

389392
const [dirtyBlockIds, setDirtyBlockIds] = useState<Set<string>>(new Set());
390393
const [dirtyEditors, setDirtyEditors] = useState<Set<string>>(new Set()); // Blocks modified in editor but not synced to block state yet
@@ -458,6 +461,7 @@ const App: React.FC = () => {
458461

459462
// --- Analysis ---
460463
const analysisResult = useRenpyAnalysis(blocks, 0); // 0 is a trigger for force re-analysis if needed
464+
const diagnosticsResult = useDiagnostics(blocks, analysisResult, images, imageMetadata, audios, audioMetadata);
461465

462466
// --- Refs ---
463467
const editorInstances = useRef<Map<string, monaco.editor.IStandaloneCodeEditor>>(new Map());
@@ -1226,7 +1230,7 @@ const App: React.FC = () => {
12261230
}, [blocks, analysisResult, handleTidyUp]);
12271231

12281232
// --- Tab Management Helpers ---
1229-
const handleOpenStaticTab = useCallback((type: 'canvas' | 'route-canvas' | 'punchlist' | 'ai-generator' | 'stats') => {
1233+
const handleOpenStaticTab = useCallback((type: 'canvas' | 'route-canvas' | 'diagnostics' | 'ai-generator' | 'stats') => {
12301234
const id = type;
12311235
// If already open in primary, activate it there
12321236
if (openTabs.find(t => t.id === id)) {
@@ -1355,6 +1359,14 @@ const App: React.FC = () => {
13551359
setStickyNotes(projectData.settings.stickyNotes || []);
13561360
setCharacterProfiles(projectData.settings.characterProfiles || {});
13571361
setPunchlistMetadata(projectData.settings.punchlistMetadata || {});
1362+
// Diagnostics tasks — migrate from old punchlist metadata if needed
1363+
if (projectData.settings.diagnosticsTasks) {
1364+
setDiagnosticsTasks(projectData.settings.diagnosticsTasks);
1365+
} else if (projectData.settings.punchlistMetadata) {
1366+
setDiagnosticsTasks(migratePunchlistToTasks(projectData.settings.punchlistMetadata));
1367+
} else {
1368+
setDiagnosticsTasks([]);
1369+
}
13581370

13591371
// Load Scene Compositions
13601372
// Helper to link saved paths back to loaded image objects
@@ -1521,7 +1533,7 @@ const App: React.FC = () => {
15211533
if (tab.type === 'markdown' && tab.filePath) {
15221534
return true; // File existence checked on tab render
15231535
}
1524-
return tab.type === 'canvas' || tab.type === 'route-canvas' || tab.type === 'punchlist' || tab.type === 'ai-generator' || tab.type === 'stats';
1536+
return tab.type === 'canvas' || tab.type === 'route-canvas' || tab.type === 'punchlist' || tab.type === 'diagnostics' || tab.type === 'ai-generator' || tab.type === 'stats';
15251537
});
15261538

15271539
const rehydratedTabs = validTabs.map(tab => {
@@ -1531,6 +1543,10 @@ const App: React.FC = () => {
15311543
return { ...tab, id: matchingBlock.id, blockId: matchingBlock.id };
15321544
}
15331545
}
1546+
// Migrate old punchlist tab to diagnostics
1547+
if (tab.type === 'punchlist' || tab.id === 'punchlist') {
1548+
return { ...tab, type: 'diagnostics' as const, id: 'diagnostics' };
1549+
}
15341550
// Migrate old single scene tab
15351551
if (tab.type === 'scene-composer' && !tab.sceneId) {
15361552
return { ...tab, sceneId: 'scene-default' };
@@ -1552,7 +1568,7 @@ const App: React.FC = () => {
15521568
if (tab.type === 'audio' && tab.filePath) return audioMap.has(tab.filePath);
15531569
if (tab.type === 'character' && tab.characterTag) return tempAnalysis.characters.has(tab.characterTag);
15541570
if (tab.type === 'markdown' && tab.filePath) return true;
1555-
return tab.type === 'canvas' || tab.type === 'route-canvas' || tab.type === 'punchlist' || tab.type === 'ai-generator' || tab.type === 'stats' || tab.type === 'scene-composer';
1571+
return tab.type === 'canvas' || tab.type === 'route-canvas' || tab.type === 'punchlist' || tab.type === 'diagnostics' || tab.type === 'ai-generator' || tab.type === 'stats' || tab.type === 'scene-composer';
15561572
});
15571573
setSplitLayout(validSecondary.length > 0 ? savedSplitLayout : 'none');
15581574
setSplitPrimarySize(projectData.settings.splitPrimarySize ?? 600);
@@ -1933,6 +1949,7 @@ const App: React.FC = () => {
19331949
stickyNotes: Array.from(stickyNotes),
19341950
characterProfiles,
19351951
punchlistMetadata,
1952+
diagnosticsTasks,
19361953
sceneCompositions: serializableScenes as unknown as Record<string, SceneComposition>,
19371954
sceneNames,
19381955
imagemapCompositions: serializableImagemaps,
@@ -1947,7 +1964,7 @@ const App: React.FC = () => {
19471964
console.error("Failed to save IDE settings:", e);
19481965
addToast('Failed to save workspace settings', 'error');
19491966
}
1950-
}, [projectRootPath, projectSettings, openTabs, activeTabId, splitLayout, splitPrimarySize, secondaryOpenTabs, secondaryActiveTabId, stickyNotes, characterProfiles, addToast, sceneCompositions, sceneNames, imagemapCompositions, screenLayoutCompositions, imageScanDirectories, audioScanDirectories, punchlistMetadata]);
1967+
}, [projectRootPath, projectSettings, openTabs, activeTabId, splitLayout, splitPrimarySize, secondaryOpenTabs, secondaryActiveTabId, stickyNotes, characterProfiles, addToast, sceneCompositions, sceneNames, imagemapCompositions, screenLayoutCompositions, imageScanDirectories, audioScanDirectories, punchlistMetadata, diagnosticsTasks]);
19511968

19521969

19531970
const handleSaveAll = useCallback(async () => {
@@ -2796,7 +2813,7 @@ const App: React.FC = () => {
27962813
if (data.command === 'save-all') handleSaveAll();
27972814
if (data.command === 'run-project' && projectRootPath) window.electronAPI?.runGame(appSettings.renpyPath, projectRootPath);
27982815
if (data.command === 'stop-project') window.electronAPI?.stopGame();
2799-
if (data.command === 'open-static-tab' && data.type) handleOpenStaticTab(data.type as 'canvas' | 'route-canvas' | 'punchlist' | 'ai-generator');
2816+
if (data.command === 'open-static-tab' && data.type) handleOpenStaticTab(data.type as 'canvas' | 'route-canvas' | 'diagnostics' | 'ai-generator');
28002817
if (data.command === 'toggle-search') handleToggleSearch();
28012818
if (data.command === 'open-settings') setSettingsModalOpen(true);
28022819
if (data.command === 'open-shortcuts') setShortcutsModalOpen(true);
@@ -2902,7 +2919,7 @@ const App: React.FC = () => {
29022919
const getTabLabel = (tab: EditorTab): React.ReactNode => {
29032920
if (tab.id === 'canvas') return 'Story Canvas';
29042921
if (tab.id === 'route-canvas') return 'Route Canvas';
2905-
if (tab.id === 'punchlist') return 'Punchlist';
2922+
if (tab.id === 'diagnostics' || tab.id === 'punchlist') return 'Diagnostics';
29062923
if (tab.id === 'stats') return 'Stats';
29072924
if (tab.type === 'ai-generator') return 'AI Generator';
29082925
if (tab.type === 'scene-composer') return sceneNames[tab.sceneId!] || 'Scene';
@@ -2939,12 +2956,12 @@ const App: React.FC = () => {
29392956
mouseGestures={appSettings.mouseGestures}
29402957
/>;
29412958
}
2942-
if (tab.type === 'punchlist') {
2943-
return <PunchlistManager
2944-
blocks={blocks} stickyNotes={stickyNotes} analysisResult={analysisResult}
2945-
projectImages={images} imageMetadata={imageMetadata} projectAudios={audios} audioMetadata={audioMetadata}
2946-
punchlistMetadata={punchlistMetadata}
2947-
onUpdateMetadata={(id, data) => { setPunchlistMetadata(draft => { if (data === undefined) { delete draft[id]; } else { draft[id] = { ...draft[id], ...data }; } }); setHasUnsavedSettings(true); }}
2959+
if (tab.type === 'diagnostics' || tab.type === 'punchlist') {
2960+
return <DiagnosticsPanel
2961+
diagnostics={diagnosticsResult}
2962+
blocks={blocks} stickyNotes={stickyNotes}
2963+
tasks={diagnosticsTasks}
2964+
onUpdateTasks={(updated) => { setDiagnosticsTasks(updated); setHasUnsavedSettings(true); }}
29482965
onOpenBlock={handleOpenEditor} onHighlightBlock={(id) => handleCenterOnBlock(id)}
29492966
/>;
29502967
}
@@ -2955,7 +2972,15 @@ const App: React.FC = () => {
29552972
/>;
29562973
}
29572974
if (tab.id === 'stats') {
2958-
return <StatsView blocks={blocks} analysisResult={analysisResult} routeAnalysisResult={routeAnalysisResult} />;
2975+
return <StatsView
2976+
blocks={blocks}
2977+
analysisResult={analysisResult}
2978+
routeAnalysisResult={routeAnalysisResult}
2979+
imageCount={images.size}
2980+
audioCount={audios.size}
2981+
diagnosticsErrorCount={diagnosticsResult.errorCount}
2982+
onOpenDiagnostics={() => handleOpenStaticTab('diagnostics')}
2983+
/>;
29592984
}
29602985
if (tab.type === 'editor' && tab.blockId) {
29612986
const block = blocks.find(b => b.id === tab.blockId);
@@ -3080,6 +3105,11 @@ const App: React.FC = () => {
30803105
onContextMenu={(e) => handleTabContextMenu(e, tab.id, paneId)}
30813106
>
30823107
<span className="truncate flex-grow">{getTabLabel(tab)}</span>
3108+
{(tab.id === 'diagnostics' || tab.id === 'punchlist') && diagnosticsResult.errorCount > 0 && (
3109+
<span className="ml-1.5 px-1.5 py-0.5 text-[10px] font-bold bg-red-500 text-white rounded-full min-w-[18px] text-center flex-none">
3110+
{diagnosticsResult.errorCount}
3111+
</span>
3112+
)}
30833113
{tab.id !== 'canvas' && (
30843114
<button onClick={(e) => handleCloseTab(tab.id, paneId, e)} aria-label="Close tab" className="ml-2 opacity-0 group-hover:opacity-100 hover:text-red-500 rounded-full p-0.5">
30853115
<svg xmlns="http://www.w3.org/2000/svg" className="h-3 w-3" viewBox="0 0 20 20" fill="currentColor"><path fillRule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" clipRule="evenodd" /></svg>
@@ -3161,7 +3191,8 @@ const App: React.FC = () => {
31613191
requestOpenFolder={handleOpenProjectFolder}
31623192
handleSave={handleSaveAll}
31633193
onOpenSettings={() => setSettingsModalOpen(true)}
3164-
onOpenStaticTab={handleOpenStaticTab as (type: 'canvas' | 'route-canvas' | 'stats') => void}
3194+
onOpenStaticTab={handleOpenStaticTab as (type: 'canvas' | 'route-canvas' | 'stats' | 'diagnostics') => void}
3195+
diagnosticsErrorCount={diagnosticsResult.errorCount}
31653196
onAddStickyNote={() => addStickyNote()}
31663197
isGameRunning={isGameRunning}
31673198
onRunGame={() => window.electronAPI?.runGame(appSettings.renpyPath, projectRootPath!)}

CLAUDE.md

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ Key rules: `react-hooks/rules-of-hooks` (error), `react-hooks/exhaustive-deps` (
6969
- **Character, Variable, ImageAsset, AudioAsset, Screen, Scene**: Story element types
7070
- **UserSnippet**: User-defined code snippet (id, title, prefix, description, code, optional monacoBody for placeholder support)
7171
- **ProjectLoadResult, ScanDirectoryResult**: Typed IPC return shapes (replacing prior `any` usage)
72-
- **SerializedSprite, SerializedSceneComposition**: JSON-safe versions of scene composer types
72+
- **SerializedSprite, SerializedSceneComposition**: JSON-safe versions of scene composer types. `SceneComposition` carries an optional `resolution?: { width: number; height: number }` field (defaults to 1920×1080 when absent) persisted in `project.ide.json`.
7373
- **ImageMapComposition**: Container for a clickable imagemap — ground image, optional hover overlay, and an array of hotspots. Persisted in `ProjectSettings.imagemapCompositions` (keyed by id) and saved to `project.ide.json`.
7474
- **ImageMapHotspot**: A single clickable region with `x`, `y`, `width`, `height`, an `ImageMapActionType` (`'jump' | 'call'`), and a target label
7575
- **SerializedImageMapComposition**: JSON-safe version of an imagemap composition
@@ -131,6 +131,7 @@ API keys are stored encrypted via Electron's `safeStorage` at `userData/api-keys
131131
- **UI rendering**: Functional components with hooks only, no class components
132132
- **Modals/overlays**: Rendered via `createPortal()`; all modals use `useModalAccessibility` hook for focus trap, Escape key close, and focus restore
133133
- **Styling**: Tailwind CSS utility classes; dark mode via `class` strategy
134+
- **Copy-to-clipboard**: Always use `components/CopyButton.tsx`. Props: `text` (string to copy), `label` (default `"Copy to Clipboard"`), `size` (`'xs'` for code-preview headers / list rows, `'sm'` default, `'md'` for primary action buttons). Idle state: clipboard icon + label. After click: green bg, checkmark, "Copied!" for 2 s. Never write per-component clipboard state or `alert()` feedback.
134135
- **Path alias**: `@/*` maps to project root in imports (tsconfig)
135136
- **Block = file**: Each `.rpy` file maps 1:1 to a Block on the canvas; the first label becomes the block title
136137
- **Accessibility**: Icon-only buttons must have `aria-label`; modals must have `role="dialog"`, `aria-modal`, and `aria-labelledby`
@@ -181,6 +182,15 @@ Users can create custom code snippets (persisted in `AppSettings.userSnippets`):
181182

182183
The app integrates AI APIs (Google Gemini via `@google/genai`, with optional OpenAI and Anthropic support via dynamic imports) for generating story content. API keys are encrypted at rest using Electron's `safeStorage`. The generator UI lives in `components/AIGenerator.tsx`.
183184

185+
## Scene Composer
186+
187+
`components/SceneComposer.tsx` provides a visual layout editor for positioning backgrounds and sprites:
188+
- Drag-and-drop images from the Image Assets panel onto the stage; drag sprites to reposition
189+
- Supports per-sprite zoom, flip, rotate, alpha, and blur controls; layer reordering via drag in the layer list
190+
- **Configurable canvas resolution**: toolbar dropdown offers presets (1920×1080, 1280×720, 1024×768, 800×600) plus a "Custom…" option revealing W×H number inputs. Resolution persists in `SceneComposition.resolution` and is saved to `project.ide.json`. Defaults to 1920×1080 when absent (backwards compatible).
191+
- Generates Ren'Py `scene`/`show` code in the Code Preview panel; exports a composited PNG via canvas
192+
- Keyboard: Delete/Backspace removes selected sprite; Arrow keys nudge (Shift = 5× step); Escape clears selection
193+
184194
## ImageMap Composer
185195

186196
`components/ImageMapComposer.tsx` provides a visual editor for creating Ren'Py `imagebutton`/`imagemap` screens with clickable hotspot regions:

0 commit comments

Comments
 (0)