Skip to content

Commit 0f1cc1e

Browse files
committed
refactor: purify AISnapshotStore as data-only layer
Move _undoApplied (UI button state) to AIChatPanel. Eliminate _initialSnapshotCreated (redundant with getSnapshotCount() > 0) and _lastSnapshotAfter (redundant with _snapshots[last]). Reorder createInitialSnapshot before recordFileBeforeEdit so back-fill populates _snapshots[0] directly. Add Phoenix MCP usage hints to CLAUDE.md.
1 parent 98da381 commit 0f1cc1e

4 files changed

Lines changed: 41 additions & 59 deletions

File tree

CLAUDE.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,3 +12,15 @@
1212
- Always use curly braces for `if`/`else`/`for`/`while`.
1313
- No trailing whitespace.
1414
- Use `const` and `let` instead of `var`.
15+
16+
## Phoenix MCP (Desktop App Testing)
17+
18+
Use `exec_js` to run JS in the Phoenix browser runtime. jQuery `$()` is global. `brackets.test.*` exposes internal modules (DocumentManager, CommandManager, ProjectManager, FileSystem, EditorManager). Always `return` a value from `exec_js` to see results. Prefer reusing an already-running Phoenix instance (`get_phoenix_status`) over launching a new one.
19+
20+
**Open AI sidebar tab:** `document.querySelectorAll('span').forEach(s => { if (s.textContent.trim() === 'AI' && s.childNodes.length === 1) s.parentElement.click(); });`
21+
22+
**Send AI chat message:** `$('.ai-chat-textarea').val('prompt'); $('.ai-chat-textarea').trigger('input'); $('.ai-send-btn').click();`
23+
24+
**Click AI chat buttons:** `$('.ai-edit-restore-btn:contains("Undo")').click();`
25+
26+
**Check logs:** `get_browser_console_logs` with `filter` regex (e.g. `"AI UI"`, `"error"`) and `tail` — includes both browser console and Node.js (PhNode) logs. Use `get_terminal_logs` for Electron process output (only available if Phoenix was launched via `start_phoenix`).

src/core-ai/AIChatPanel.js

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ define(function (require, exports, module) {
4242
const _previousContentMap = {}; // filePath → previous content before edit, for undo support
4343
let _currentEdits = []; // edits in current response, for summary card
4444
let _firstEditInResponse = true; // tracks first edit per response for initial PUC
45+
let _undoApplied = false; // whether undo/restore has been clicked on any card
4546
// --- AI event trace logging (compact, non-flooding) ---
4647
let _traceTextChunks = 0;
4748
let _traceToolStreamCounts = {}; // toolId → count
@@ -292,6 +293,7 @@ define(function (require, exports, module) {
292293
_hasReceivedContent = false;
293294
_isStreaming = false;
294295
_firstEditInResponse = true;
296+
_undoApplied = false;
295297
SnapshotStore.reset();
296298
Object.keys(_previousContentMap).forEach(function (key) {
297299
delete _previousContentMap[key];
@@ -493,15 +495,16 @@ define(function (require, exports, module) {
493495
linesRemoved: oldLines
494496
});
495497

496-
// Capture pre-edit content into pending snapshot and back-fill
498+
// Capture pre-edit content for snapshot tracking
497499
const previousContent = _previousContentMap[edit.file];
498500
const isNewFile = (edit.oldText === null && (previousContent === undefined || previousContent === ""));
499-
SnapshotStore.recordFileBeforeEdit(edit.file, previousContent, isNewFile);
500501

501-
// On first edit per response, insert initial PUC if needed
502+
// On first edit per response, insert initial PUC if needed.
503+
// Create initial snapshot *before* recordFileBeforeEdit so it pushes
504+
// an empty {} that recordFileBeforeEdit will back-fill directly.
502505
if (_firstEditInResponse) {
503506
_firstEditInResponse = false;
504-
if (!SnapshotStore.isInitialSnapshotCreated()) {
507+
if (SnapshotStore.getSnapshotCount() === 0) {
505508
const initialIndex = SnapshotStore.createInitialSnapshot();
506509
// Insert initial restore point PUC before the current tool indicator
507510
const $puc = $(
@@ -525,6 +528,9 @@ define(function (require, exports, module) {
525528
}
526529
}
527530

531+
// Record pre-edit content into pending snapshot and back-fill
532+
SnapshotStore.recordFileBeforeEdit(edit.file, previousContent, isNewFile);
533+
528534
// Find the oldest Edit/Write tool indicator for this file that doesn't
529535
// already have edit actions. This is more robust than matching by toolId
530536
// because the SDK with includePartialMessages may re-emit tool_use blocks
@@ -601,6 +607,7 @@ define(function (require, exports, module) {
601607
function _appendEditSummary() {
602608
// Finalize snapshot and get the after-snapshot index
603609
const afterIndex = SnapshotStore.finalizeResponse();
610+
_undoApplied = false;
604611

605612
// Aggregate per-file stats
606613
const fileStats = {};
@@ -630,7 +637,7 @@ define(function (require, exports, module) {
630637
.attr("title", "Restore files to this point");
631638

632639
// Determine button label: "Undo" if not undone, else "Restore to this point"
633-
const isUndo = !SnapshotStore.isUndoApplied();
640+
const isUndo = !_undoApplied;
634641
const label = isUndo ? "Undo" : "Restore to this point";
635642
const title = isUndo ? "Undo changes from this response" : "Restore files to this point";
636643

@@ -687,7 +694,7 @@ define(function (require, exports, module) {
687694
$msgs.find(".ai-restore-highlighted").removeClass("ai-restore-highlighted");
688695

689696
// Once any "Restore to this point" is clicked, undo is no longer applicable
690-
SnapshotStore.setUndoApplied(true);
697+
_undoApplied = true;
691698

692699
// Reset all buttons to "Restore to this point"
693700
$msgs.find('.ai-edit-restore-btn').each(function () {
@@ -718,7 +725,7 @@ define(function (require, exports, module) {
718725
*/
719726
function _onUndoClick(afterIndex) {
720727
const $msgs = _$msgs();
721-
SnapshotStore.setUndoApplied(true);
728+
_undoApplied = true;
722729
const targetIndex = afterIndex - 1;
723730

724731
// Reset all buttons to "Restore to this point"

src/core-ai/AISnapshotStore.js

Lines changed: 3 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -33,10 +33,7 @@ define(function (require, exports, module) {
3333
// --- Private state ---
3434
const _contentStore = {}; // hash → content string (content-addressable dedup)
3535
let _snapshots = []; // flat: _snapshots[i] = { filePath: hash|null }
36-
let _lastSnapshotAfter = {}; // cumulative state after last completed response
3736
let _pendingBeforeSnap = {}; // built during current response: filePath → hash|null
38-
let _initialSnapshotCreated = false; // has the initial (pre-AI) snapshot been pushed?
39-
let _undoApplied = false;
4037

4138
// --- Path utility ---
4239

@@ -226,10 +223,6 @@ define(function (require, exports, module) {
226223
snap[filePath] = hash;
227224
}
228225
});
229-
// Also back-fill _lastSnapshotAfter
230-
if (_lastSnapshotAfter[filePath] === undefined) {
231-
_lastSnapshotAfter[filePath] = hash;
232-
}
233226
}
234227
}
235228

@@ -239,19 +232,10 @@ define(function (require, exports, module) {
239232
* @return {number} the snapshot index (always 0)
240233
*/
241234
function createInitialSnapshot() {
242-
const snap = Object.assign({}, _lastSnapshotAfter);
243-
_snapshots.push(snap);
244-
_initialSnapshotCreated = true;
235+
_snapshots.push({});
245236
return 0;
246237
}
247238

248-
/**
249-
* @return {boolean} whether the initial snapshot has been created this session
250-
*/
251-
function isInitialSnapshotCreated() {
252-
return _initialSnapshotCreated;
253-
}
254-
255239
/**
256240
* Finalize snapshot state when a response completes.
257241
* Builds an "after" snapshot from current document content for edited files,
@@ -261,8 +245,8 @@ define(function (require, exports, module) {
261245
function finalizeResponse() {
262246
let afterIndex = -1;
263247
if (Object.keys(_pendingBeforeSnap).length > 0) {
264-
// Build "after" snapshot = current _lastSnapshotAfter + current content of edited files
265-
const afterSnap = Object.assign({}, _lastSnapshotAfter);
248+
// Build "after" snapshot = last snapshot + current content of edited files
249+
const afterSnap = Object.assign({}, _snapshots[_snapshots.length - 1]);
266250
Object.keys(_pendingBeforeSnap).forEach(function (fp) {
267251
const vfsPath = realToVfsPath(fp);
268252
const openDoc = DocumentManager.getOpenDocumentForPath(vfsPath);
@@ -271,11 +255,9 @@ define(function (require, exports, module) {
271255
}
272256
});
273257
_snapshots.push(afterSnap);
274-
_lastSnapshotAfter = afterSnap;
275258
afterIndex = _snapshots.length - 1;
276259
}
277260
_pendingBeforeSnap = {};
278-
_undoApplied = false;
279261
return afterIndex;
280262
}
281263

@@ -294,20 +276,6 @@ define(function (require, exports, module) {
294276
});
295277
}
296278

297-
/**
298-
* @return {boolean} whether undo has been applied (latest summary clicked)
299-
*/
300-
function isUndoApplied() {
301-
return _undoApplied;
302-
}
303-
304-
/**
305-
* @param {boolean} val
306-
*/
307-
function setUndoApplied(val) {
308-
_undoApplied = val;
309-
}
310-
311279
/**
312280
* @return {number} number of snapshots
313281
*/
@@ -321,22 +289,16 @@ define(function (require, exports, module) {
321289
function reset() {
322290
Object.keys(_contentStore).forEach(function (k) { delete _contentStore[k]; });
323291
_snapshots = [];
324-
_lastSnapshotAfter = {};
325292
_pendingBeforeSnap = {};
326-
_initialSnapshotCreated = false;
327-
_undoApplied = false;
328293
}
329294

330295
exports.realToVfsPath = realToVfsPath;
331296
exports.saveDocToDisk = saveDocToDisk;
332297
exports.storeContent = storeContent;
333298
exports.recordFileBeforeEdit = recordFileBeforeEdit;
334299
exports.createInitialSnapshot = createInitialSnapshot;
335-
exports.isInitialSnapshotCreated = isInitialSnapshotCreated;
336300
exports.finalizeResponse = finalizeResponse;
337301
exports.restoreToSnapshot = restoreToSnapshot;
338-
exports.isUndoApplied = isUndoApplied;
339-
exports.setUndoApplied = setUndoApplied;
340302
exports.getSnapshotCount = getSnapshotCount;
341303
exports.reset = reset;
342304
});

src/core-ai/editApplyVerification.md

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,12 @@ _snapshots[2] = after R2 edits
2222

2323
## State Variables
2424

25-
- `_snapshots[]`: flat array of `{ filePath: hash|null }` snapshots
26-
- `_lastSnapshotAfter`: cumulative state after last completed response
27-
- `_pendingBeforeSnap`: per-file pre-edit tracking during current response
28-
- `_initialSnapshotCreated`: whether snapshot 0 has been pushed
29-
- `_undoApplied`: whether undo/restore has been clicked on any card
25+
### AISnapshotStore (pure data layer)
26+
- `_snapshots[]`: flat array of `{ filePath: hash|null }` snapshots. `getSnapshotCount() > 0` replaces the old `_initialSnapshotCreated` flag.
27+
- `_pendingBeforeSnap`: per-file pre-edit tracking during current response (dedup guard for first-edit-per-file + file list for `finalizeResponse`)
28+
29+
### AIChatPanel (UI state)
30+
- `_undoApplied`: whether undo/restore has been clicked on any card (UI control for button labels)
3031

3132
## DOM Layout Example
3233

@@ -49,18 +50,18 @@ _snapshots[2] = after R2 edits
4950
### AISnapshotStore
5051

5152
- `recordFileBeforeEdit(filePath, previousContent, isNewFile)`: tracks pre-edit state, back-fills all existing snapshots
52-
- `createInitialSnapshot()`: pushes snapshot 0 from `_lastSnapshotAfter`, returns index 0
53-
- `isInitialSnapshotCreated()`: returns whether snapshot 0 exists
54-
- `finalizeResponse()`: builds after-snapshot from current doc content, pushes it, resets `_undoApplied`, returns index (or -1)
53+
- `createInitialSnapshot()`: pushes empty `{}` as snapshot 0, returns index 0. Must be called *before* `recordFileBeforeEdit` so the back-fill populates it.
54+
- `getSnapshotCount()`: returns `_snapshots.length` (replaces `isInitialSnapshotCreated()`)
55+
- `finalizeResponse()`: builds after-snapshot from `_snapshots[last]` + current doc content, pushes it, returns index (or -1)
5556
- `restoreToSnapshot(index, callback)`: applies `_snapshots[index]` to files, calls `callback(errorCount)`
56-
- `isUndoApplied()` / `setUndoApplied(val)`: getter/setter for undo state
5757
- `reset()`: clears all state for new session
5858

5959
### AIChatPanel
6060

6161
- `_$msgs()`: live DOM query helper — returns `$(".ai-chat-messages")` to avoid stale cached `$messages` reference (see Implementation Notes)
62-
- `_onToolEdit()`: on first edit per response, inserts initial PUC if not yet created. Diff toggle only (no per-edit undo).
63-
- `_appendEditSummary()`: calls `finalizeResponse()`, creates summary card with "Undo" or "Restore to this point" button
62+
- `_undoApplied`: local module state — reset to `false` in `_appendEditSummary()` (after `finalizeResponse()`) and `_newSession()`; set to `true` in `_onRestoreClick()` and `_onUndoClick()`
63+
- `_onToolEdit()`: on first edit per response, creates initial snapshot (if none) *then* records pre-edit state. Inserts initial PUC. Diff toggle only (no per-edit undo).
64+
- `_appendEditSummary()`: calls `finalizeResponse()`, resets `_undoApplied`, creates summary card with "Undo" or "Restore to this point" button
6465
- `_onUndoClick(afterIndex)`: sets `_undoApplied`, resets all buttons to "Restore to this point", restores to `afterIndex - 1`, highlights target element as "Restored", scrolls to it
6566
- `_onRestoreClick(snapshotIndex)`: sets `_undoApplied`, resets all buttons to "Restore to this point", restores to the given snapshot, marks clicked element as "Restored"
6667
- `_setStreaming(streaming)`: disables/enables all restore buttons during AI streaming

0 commit comments

Comments
 (0)