davinci-resolve-mcp 2.46.0 → 2.47.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +37 -0
- package/README.md +1 -1
- package/docs/SKILL.md +6 -0
- package/docs/guides/control-panel.md +25 -4
- package/install.py +1 -1
- package/package.json +1 -1
- package/scripts/regen_panel_screenshots.py +1 -0
- package/src/analysis_dashboard.py +432 -0
- package/src/granular/common.py +1 -1
- package/src/server.py +1 -1
- package/src/utils/edit_engine.py +9 -1
- package/src/utils/project_cleanup.py +73 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,43 @@
|
|
|
2
2
|
|
|
3
3
|
Release history for the DaVinci Resolve MCP Server. The latest release is summarized in the root README; older entries live here to keep the README focused.
|
|
4
4
|
|
|
5
|
+
## What's New in v2.47.0
|
|
6
|
+
|
|
7
|
+
Edit-engine plan browser in the control panel (Media → Edit Plans) — the
|
|
8
|
+
panel UX pass for the v2.45.0 edit engine. Chat-first: the panel surfaces
|
|
9
|
+
plans and evidence; execution stays in chat behind the confirm-token gate.
|
|
10
|
+
|
|
11
|
+
- **Added** a plan browser at `#analysis/review/plans`: every saved
|
|
12
|
+
`edit_engine` plan with kind, summary, save time, and an `executed` chip;
|
|
13
|
+
fingerprint-corrupt plans surface as warning rows instead of being hidden.
|
|
14
|
+
- **Added** plan detail views per kind: selects decisions render with shot
|
|
15
|
+
thumbnails, rank, duration, rationale, and a deep link to the shot page;
|
|
16
|
+
tighten plans list each dead-air lift with its transcript-gap evidence and
|
|
17
|
+
skipped items; swap plans show the current item plus numbered alternates
|
|
18
|
+
with similarity scores. Executed plans show their execution readback.
|
|
19
|
+
- **Added** a copyable per-kind execute chat prompt on each plan (the panel
|
|
20
|
+
never executes; swaps include an `alternate_index` placeholder).
|
|
21
|
+
- **Added** `/api/edit_plans` + `/api/edit_plans/<plan_id>` panel endpoints
|
|
22
|
+
(DB/file only — no Resolve round-trips; decisions are enriched server-side
|
|
23
|
+
with `resolve_clip_id` and a `thumb_frame_index` for the existing frames
|
|
24
|
+
route).
|
|
25
|
+
- **Changed** `edit_engine.list_plans` gained `include_corrupt` (default off;
|
|
26
|
+
the MCP action shape is unchanged).
|
|
27
|
+
|
|
28
|
+
## What's New in v2.46.1
|
|
29
|
+
|
|
30
|
+
Test and hygiene hardening; no public tool surface changes.
|
|
31
|
+
|
|
32
|
+
- **Fixed** the three `InventoryCacheReuseTests` failures that appeared
|
|
33
|
+
whenever a live Resolve instance was running: the reuse/build-path tests
|
|
34
|
+
now stub the read-only Resolve probe, so the suite is green with Resolve
|
|
35
|
+
open or closed.
|
|
36
|
+
- **Added** `src/utils/project_cleanup.delete_project_safely`: a retrying
|
|
37
|
+
delete helper for disposable Resolve projects (switch away from the target,
|
|
38
|
+
retry `DeleteProject` once after a pause, report the leftover by name on
|
|
39
|
+
persistent failure). The live edit-engine validation harness now uses it
|
|
40
|
+
during cleanup, so disposable pilot projects stop lingering in the library.
|
|
41
|
+
|
|
5
42
|
## What's New in v2.46.0
|
|
6
43
|
|
|
7
44
|
Community PR bundle: five contributed fixes and features (#62–#66), live-validated
|
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# DaVinci Resolve MCP Server
|
|
2
2
|
|
|
3
|
-
[](https://github.com/samuelgursky/davinci-resolve-mcp/releases)
|
|
4
4
|
[](https://www.npmjs.com/package/davinci-resolve-mcp)
|
|
5
5
|
[](docs/reference/api-coverage.md)
|
|
6
6
|
[-blue.svg)](#server-modes)
|
package/docs/SKILL.md
CHANGED
|
@@ -706,6 +706,12 @@ The engine needs the analysis substrate: analyzed clips in the DB (run
|
|
|
706
706
|
`db_ingest` on older roots), transcripts for tighten, and visual embeddings
|
|
707
707
|
(`build_embeddings(kinds=['visual'])`) for swap.
|
|
708
708
|
|
|
709
|
+
Plans are reviewable in the control panel (Media → Edit Plans, v2.47.0+):
|
|
710
|
+
decisions/lifts/alternates with thumbnails and rationale, deep links to shot
|
|
711
|
+
pages, and a copyable per-kind execute prompt. The panel never executes —
|
|
712
|
+
when the user says they reviewed a plan there, execution still comes back
|
|
713
|
+
through chat with the confirm-token gate.
|
|
714
|
+
|
|
709
715
|
**`timeline_versioning`** — Version-on-mutate, archive, rollback, brain-edit history (C6).
|
|
710
716
|
|
|
711
717
|
Every destructive timeline op (compound, captions, ripple delete, gap close,
|
|
@@ -26,10 +26,10 @@ pidfile and `resolve_control(action="close_control_panel")` stops it.
|
|
|
26
26
|
|
|
27
27
|
## Navigation and deep links
|
|
28
28
|
|
|
29
|
-
Top-level sections: **Overview**, **Media** (Inventory / Review / History
|
|
30
|
-
**AI Console**, **Setup** (Resolve / MCP / Storage / Tools /
|
|
31
|
-
History), **Docs**, and **Preferences** (Analysis / Caps + Safety /
|
|
32
|
-
and Markers / Paths and Workflow / MCP Updates). The project selector on the
|
|
29
|
+
Top-level sections: **Overview**, **Media** (Inventory / Review / History /
|
|
30
|
+
Edit Plans), **AI Console**, **Setup** (Resolve / MCP / Storage / Tools /
|
|
31
|
+
Media Pool History), **Docs**, and **Preferences** (Analysis / Caps + Safety /
|
|
32
|
+
Metadata and Markers / Paths and Workflow / MCP Updates). The project selector on the
|
|
33
33
|
right scopes the panel to an analysis context; **View All Projects** opens a
|
|
34
34
|
read-only browser over the Resolve project database with confirm-gated
|
|
35
35
|
loading.
|
|
@@ -42,6 +42,7 @@ into chat:
|
|
|
42
42
|
#analysis/media #analysis/review
|
|
43
43
|
#analysis/review/history #analysis/review/clip/<clip_id>
|
|
44
44
|
#analysis/review/clip/<clip_id>/shot/<n> #analysis/review/clip/<clip_id>/transcript
|
|
45
|
+
#analysis/review/plans #analysis/review/plans/<plan_id>
|
|
45
46
|
#diagnostics/mcp #preferences/caps
|
|
46
47
|
```
|
|
47
48
|
|
|
@@ -131,6 +132,26 @@ timeline before risky work, and select any timeline to inspect its version
|
|
|
131
132
|
chain and the edits between versions. Every destructive timeline edit made
|
|
132
133
|
through the MCP archives a version here automatically.
|
|
133
134
|
|
|
135
|
+
### Media → Edit Plans
|
|
136
|
+
|
|
137
|
+

|
|
138
|
+
|
|
139
|
+
Dry-run plans saved by the edit engine (`edit_engine` actions
|
|
140
|
+
`plan_selects` / `plan_tighten` / `plan_swap`). The list shows each plan's
|
|
141
|
+
kind, summary, save time, and an `executed` chip once it has run; plans that
|
|
142
|
+
fail their fingerprint check appear as a warning row instead of being hidden.
|
|
143
|
+
Opening a plan shows its full evidence: selects decisions render with shot
|
|
144
|
+
thumbnails, rank, duration, and rationale (with a deep link to each shot
|
|
145
|
+
page); tighten plans list every dead-air lift with its transcript-gap
|
|
146
|
+
evidence; swap plans show the current item and each numbered alternate with
|
|
147
|
+
its similarity score.
|
|
148
|
+
|
|
149
|
+
The panel never executes a plan. Each detail view carries a copyable chat
|
|
150
|
+
prompt (per kind, with the `alternate_index` placeholder for swaps) — paste
|
|
151
|
+
it into your MCP chat session, which holds the confirm-token gate and the
|
|
152
|
+
versioned execution path. Executed plans keep their execution readback
|
|
153
|
+
inline.
|
|
154
|
+
|
|
134
155
|
### AI Console
|
|
135
156
|
|
|
136
157
|

|
package/install.py
CHANGED
|
@@ -35,7 +35,7 @@ from src.utils.update_check import (
|
|
|
35
35
|
|
|
36
36
|
# ─── Version ──────────────────────────────────────────────────────────────────
|
|
37
37
|
|
|
38
|
-
VERSION = "2.
|
|
38
|
+
VERSION = "2.47.0"
|
|
39
39
|
# Only hard floor: mcp[cli] requires Python 3.10+. There is no upper bound —
|
|
40
40
|
# Resolve's scripting bridge loads into newer interpreters on recent builds
|
|
41
41
|
# (Python 3.14 verified against Resolve Studio 20.3.2). Older Resolve builds
|
package/package.json
CHANGED
|
@@ -57,6 +57,7 @@ def main() -> int:
|
|
|
57
57
|
("#preferences/analysis", "08-preferences-analysis.png", 3000),
|
|
58
58
|
("#aiconsole", "09-ai-console.png", 3000),
|
|
59
59
|
("#analysis/review/history", "10-history.png", 3500),
|
|
60
|
+
("#analysis/review/plans", "11-edit-plans.png", 3500),
|
|
60
61
|
]
|
|
61
62
|
with sync_playwright() as pw:
|
|
62
63
|
browser = pw.chromium.launch()
|
|
@@ -2379,6 +2379,84 @@ HTML = r"""<!doctype html>
|
|
|
2379
2379
|
}
|
|
2380
2380
|
.history-rollback-btn:hover { background: var(--accent-warning); color: var(--bg-base); }
|
|
2381
2381
|
|
|
2382
|
+
/* ─── Edit-engine plan browser ──────────────────────────────────── */
|
|
2383
|
+
.plans-toolbar {
|
|
2384
|
+
display: flex;
|
|
2385
|
+
align-items: center;
|
|
2386
|
+
justify-content: space-between;
|
|
2387
|
+
gap: var(--space-3);
|
|
2388
|
+
margin-bottom: var(--space-3);
|
|
2389
|
+
}
|
|
2390
|
+
.plans-toolbar .section-meta { margin-left: var(--space-2); }
|
|
2391
|
+
.plans-list { display: grid; gap: var(--space-2); }
|
|
2392
|
+
.plan-row {
|
|
2393
|
+
display: flex;
|
|
2394
|
+
align-items: center;
|
|
2395
|
+
gap: var(--space-3);
|
|
2396
|
+
padding: var(--space-2) var(--space-3);
|
|
2397
|
+
border: 1px solid var(--border-default);
|
|
2398
|
+
border-radius: var(--radius-md);
|
|
2399
|
+
background: var(--lab-panel-elevated, var(--bg-base));
|
|
2400
|
+
cursor: pointer;
|
|
2401
|
+
}
|
|
2402
|
+
.plan-row:hover { background: var(--bg-muted); }
|
|
2403
|
+
.plan-row.is-corrupt {
|
|
2404
|
+
cursor: default;
|
|
2405
|
+
border-color: var(--accent-warning);
|
|
2406
|
+
background: var(--accent-warning-muted);
|
|
2407
|
+
}
|
|
2408
|
+
.plan-row .summary { flex: 1; min-width: 0; }
|
|
2409
|
+
.plan-row .saved-at { color: var(--text-muted); font-size: var(--text-xs); white-space: nowrap; }
|
|
2410
|
+
.plan-chip {
|
|
2411
|
+
display: inline-block;
|
|
2412
|
+
padding: 1px var(--space-2);
|
|
2413
|
+
border-radius: var(--radius-sm);
|
|
2414
|
+
font-size: var(--text-xs);
|
|
2415
|
+
font-weight: 600;
|
|
2416
|
+
text-transform: uppercase;
|
|
2417
|
+
letter-spacing: 0.04em;
|
|
2418
|
+
border: 1px solid var(--border-default);
|
|
2419
|
+
color: var(--text-secondary);
|
|
2420
|
+
white-space: nowrap;
|
|
2421
|
+
}
|
|
2422
|
+
.plan-chip.kind-selects { color: var(--accent-success); border-color: var(--accent-success); }
|
|
2423
|
+
.plan-chip.kind-tighten { color: var(--accent-warning); border-color: var(--accent-warning); }
|
|
2424
|
+
.plan-chip.kind-swap { color: var(--accent-info, var(--text-secondary)); border-color: currentColor; }
|
|
2425
|
+
.plan-chip.executed { color: var(--text-muted); text-transform: none; letter-spacing: 0; }
|
|
2426
|
+
.plan-chip.corrupt { color: var(--accent-warning); border-color: var(--accent-warning); }
|
|
2427
|
+
.plan-detail-header {
|
|
2428
|
+
display: flex;
|
|
2429
|
+
align-items: center;
|
|
2430
|
+
flex-wrap: wrap;
|
|
2431
|
+
gap: var(--space-2);
|
|
2432
|
+
margin-bottom: var(--space-3);
|
|
2433
|
+
}
|
|
2434
|
+
.plan-detail-header .timeline-name { font-weight: 600; font-size: var(--text-md); }
|
|
2435
|
+
.plan-detail-body { display: grid; gap: var(--space-3); }
|
|
2436
|
+
.plan-section {
|
|
2437
|
+
padding: var(--space-3);
|
|
2438
|
+
border: 1px solid var(--border-default);
|
|
2439
|
+
border-radius: var(--radius-md);
|
|
2440
|
+
background: var(--lab-panel-elevated, var(--bg-base));
|
|
2441
|
+
}
|
|
2442
|
+
.plan-section > strong { display: block; margin-bottom: var(--space-2); }
|
|
2443
|
+
.plan-decision-card {
|
|
2444
|
+
display: flex;
|
|
2445
|
+
gap: var(--space-3);
|
|
2446
|
+
padding: var(--space-2) 0;
|
|
2447
|
+
border-bottom: 1px solid var(--border-default);
|
|
2448
|
+
}
|
|
2449
|
+
.plan-decision-card:last-child { border-bottom: none; }
|
|
2450
|
+
.plan-decision-card .review-thumb { width: 120px; height: 68px; object-fit: cover; border-radius: var(--radius-sm); flex: none; }
|
|
2451
|
+
.plan-decision-card .review-thumb.placeholder {
|
|
2452
|
+
display: flex; align-items: center; justify-content: center;
|
|
2453
|
+
background: var(--bg-muted); color: var(--text-muted); font-size: var(--text-xs);
|
|
2454
|
+
}
|
|
2455
|
+
.plan-decision-card .body { flex: 1; min-width: 0; display: grid; gap: 2px; align-content: start; }
|
|
2456
|
+
.plan-decision-card .title-row { display: flex; align-items: center; gap: var(--space-2); flex-wrap: wrap; }
|
|
2457
|
+
.plan-decision-card .rationale { color: var(--text-secondary); font-size: var(--text-xs); }
|
|
2458
|
+
.plan-decision-card .meta { color: var(--text-muted); font-size: var(--text-xs); }
|
|
2459
|
+
|
|
2382
2460
|
/* ─── Analysis caps preferences widget ──────────────────────────── */
|
|
2383
2461
|
.caps-section {
|
|
2384
2462
|
margin-top: var(--space-4);
|
|
@@ -3847,6 +3925,7 @@ HTML = r"""<!doctype html>
|
|
|
3847
3925
|
<button class="nav-dropdown-item" data-panel-target="analysis" data-subpage-target="media" role="menuitem"><span class="nav-dropdown-icon" aria-hidden="true"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="5" width="18" height="14" rx="2"></rect><path d="m7 15 3-3 2 2 4-5 1 2"></path></svg></span>Inventory</button>
|
|
3848
3926
|
<button class="nav-dropdown-item" data-panel-target="analysis" data-subpage-target="review" role="menuitem"><span class="nav-dropdown-icon" aria-hidden="true"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="18" height="14" rx="2"></rect><circle cx="9" cy="10" r="2"></circle><path d="m21 17-5-5-9 9"></path></svg></span>Review</button>
|
|
3849
3927
|
<button class="nav-dropdown-item" id="navHistoryItem" data-panel-target="analysis" data-subpage-target="review" data-review-view="history" role="menuitem"><span class="nav-dropdown-icon" aria-hidden="true"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8"></path><path d="M3 3v5h5"></path><path d="M12 7v5l4 2"></path></svg></span>History</button>
|
|
3928
|
+
<button class="nav-dropdown-item" id="navPlansItem" data-panel-target="analysis" data-subpage-target="review" data-review-view="plans" role="menuitem"><span class="nav-dropdown-icon" aria-hidden="true"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="8" y="2" width="8" height="4" rx="1"></rect><path d="M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2"></path><path d="M9 12h6"></path><path d="M9 16h6"></path></svg></span>Edit Plans</button>
|
|
3850
3929
|
</div>
|
|
3851
3930
|
</div>
|
|
3852
3931
|
<button class="control-tab" data-panel-target="aiconsole">AI Console</button>
|
|
@@ -4192,6 +4271,20 @@ HTML = r"""<!doctype html>
|
|
|
4192
4271
|
</section>
|
|
4193
4272
|
</div>
|
|
4194
4273
|
</div>
|
|
4274
|
+
<div id="reviewPlansView" style="display:none">
|
|
4275
|
+
<div class="plans-toolbar">
|
|
4276
|
+
<div>
|
|
4277
|
+
<strong>Edit plans</strong>
|
|
4278
|
+
<span class="section-meta">Dry-run plans saved by the edit engine — review here, execute from chat.</span>
|
|
4279
|
+
</div>
|
|
4280
|
+
<button id="plansRefreshBtn" class="secondary" type="button">Refresh</button>
|
|
4281
|
+
</div>
|
|
4282
|
+
<div id="plansListBody" class="plans-list"><div class="empty">Loading…</div></div>
|
|
4283
|
+
</div>
|
|
4284
|
+
<div id="reviewPlanView" style="display:none">
|
|
4285
|
+
<div id="planDetailHeader" class="plan-detail-header"></div>
|
|
4286
|
+
<div id="planDetailBody" class="plan-detail-body"></div>
|
|
4287
|
+
</div>
|
|
4195
4288
|
</section>
|
|
4196
4289
|
</main>
|
|
4197
4290
|
|
|
@@ -5160,6 +5253,10 @@ HTML = r"""<!doctype html>
|
|
|
5160
5253
|
if (r.view === 'combined' && Array.isArray(r.combinedClipIds) && r.combinedClipIds.length) {
|
|
5161
5254
|
return `/combined/${r.combinedClipIds.map(encodeURIComponent).join(',')}`;
|
|
5162
5255
|
}
|
|
5256
|
+
if (r.view === 'plan' && state.plans?.currentPlanId) {
|
|
5257
|
+
return `/plans/${encodeURIComponent(state.plans.currentPlanId)}`;
|
|
5258
|
+
}
|
|
5259
|
+
if (r.view === 'plans') return '/plans';
|
|
5163
5260
|
return '';
|
|
5164
5261
|
}
|
|
5165
5262
|
|
|
@@ -5191,6 +5288,15 @@ HTML = r"""<!doctype html>
|
|
|
5191
5288
|
reviewSetView('history');
|
|
5192
5289
|
refreshHistoryTimelines().catch(alertError);
|
|
5193
5290
|
}
|
|
5291
|
+
// Deep links: #analysis/review/plans[/<plan_id>]
|
|
5292
|
+
if (panelName === 'analysis' && subpage === 'review' && route[2] === 'plans') {
|
|
5293
|
+
if (route[3]) {
|
|
5294
|
+
openEditPlan(decodeURIComponent(route[3]), { pushHash: false }).catch(alertError);
|
|
5295
|
+
} else {
|
|
5296
|
+
reviewSetView('plans', { pushHash: false });
|
|
5297
|
+
refreshEditPlans().catch(alertError);
|
|
5298
|
+
}
|
|
5299
|
+
}
|
|
5194
5300
|
// Deep links: #analysis/review/combined/<id1,id2,...>
|
|
5195
5301
|
if (panelName === 'analysis' && subpage === 'review' && route[2] === 'combined' && route[3]) {
|
|
5196
5302
|
const ids = decodeURIComponent(route[3]).split(',').filter(Boolean);
|
|
@@ -7210,6 +7316,229 @@ HTML = r"""<!doctype html>
|
|
|
7210
7316
|
await refreshHistoryTimelines();
|
|
7211
7317
|
}
|
|
7212
7318
|
|
|
7319
|
+
// ─── Edit-engine plan browser (chat-first: the panel surfaces plans
|
|
7320
|
+
// and evidence; execution happens via copyable chat prompts only) ─
|
|
7321
|
+
state.plans = state.plans || { list: null, currentPlanId: null, payload: null };
|
|
7322
|
+
|
|
7323
|
+
const PLAN_KIND_LABELS = { selects: 'Selects', tighten: 'Tighten', swap: 'Swap' };
|
|
7324
|
+
|
|
7325
|
+
function planExecutePrompt(plan) {
|
|
7326
|
+
const id = plan.plan_id;
|
|
7327
|
+
if (plan.kind === 'selects') {
|
|
7328
|
+
return `Execute edit plan ${id}: call edit_engine(action="execute_selects", params={"plan_id": "${id}"}). It creates a NEW selects timeline; nothing existing is touched. Show me the readback when done.`;
|
|
7329
|
+
}
|
|
7330
|
+
if (plan.kind === 'tighten') {
|
|
7331
|
+
return `Execute edit plan ${id}: call edit_engine(action="execute_tighten", params={"plan_id": "${id}"}). It assembles a tightened VARIANT timeline from the keep ranges; the original timeline is never mutated. Show me the readback when done.`;
|
|
7332
|
+
}
|
|
7333
|
+
if (plan.kind === 'swap') {
|
|
7334
|
+
return `Execute edit plan ${id} with alternate <N>: call edit_engine(action="execute_swap", params={"plan_id": "${id}", "alternate_index": <N>}) — replace <N> with the number of the alternate I picked below (0-based). The timeline is version-archived before the swap. Show me the readback when done.`;
|
|
7335
|
+
}
|
|
7336
|
+
return `Load edit plan ${id} with edit_engine(action="get_plan", params={"plan_id": "${id}"}) and walk me through it.`;
|
|
7337
|
+
}
|
|
7338
|
+
|
|
7339
|
+
function planThumb(row, altText) {
|
|
7340
|
+
const clipId = row.resolve_clip_id;
|
|
7341
|
+
const frameIndex = row.thumb_frame_index;
|
|
7342
|
+
if (clipId && frameIndex != null) {
|
|
7343
|
+
return `<img class="review-thumb" loading="lazy" src="/api/clips/${encodeURIComponent(clipId)}/frames/${frameIndex}" alt="${escapeHtml(altText)}" onerror="this.replaceWith(Object.assign(document.createElement('div'),{className:'review-thumb placeholder',textContent:'no frame'}))">`;
|
|
7344
|
+
}
|
|
7345
|
+
return '<div class="review-thumb placeholder">no frame</div>';
|
|
7346
|
+
}
|
|
7347
|
+
|
|
7348
|
+
async function refreshEditPlans() {
|
|
7349
|
+
const body = $('plansListBody');
|
|
7350
|
+
if (body) body.innerHTML = '<div class="empty">Loading…</div>';
|
|
7351
|
+
const data = await api('/api/edit_plans').catch(err => ({ success: false, error: String(err) }));
|
|
7352
|
+
if (!data || !data.success) {
|
|
7353
|
+
if (body) body.innerHTML = `<div class="empty">Error: ${escapeHtml(data?.error || 'unknown')}</div>`;
|
|
7354
|
+
return;
|
|
7355
|
+
}
|
|
7356
|
+
state.plans.list = data.plans || [];
|
|
7357
|
+
renderPlansList();
|
|
7358
|
+
}
|
|
7359
|
+
|
|
7360
|
+
function renderPlansList() {
|
|
7361
|
+
const body = $('plansListBody');
|
|
7362
|
+
if (!body) return;
|
|
7363
|
+
const plans = state.plans.list || [];
|
|
7364
|
+
if (!plans.length) {
|
|
7365
|
+
body.innerHTML = '<div class="empty">No edit plans yet. Ask your chat session to plan one, e.g. “Plan a selects reel from this bin” (edit_engine action="plan_selects").</div>';
|
|
7366
|
+
return;
|
|
7367
|
+
}
|
|
7368
|
+
body.innerHTML = plans.map(p => {
|
|
7369
|
+
if (p.corrupt) {
|
|
7370
|
+
return `<div class="plan-row is-corrupt">
|
|
7371
|
+
<span class="plan-chip corrupt">corrupt</span>
|
|
7372
|
+
<span class="summary">Plan ${escapeHtml(p.plan_id || '?')} failed its fingerprint check — it was edited on disk or truncated. It cannot be executed; re-plan to replace it.</span>
|
|
7373
|
+
</div>`;
|
|
7374
|
+
}
|
|
7375
|
+
const executed = p.executed_at
|
|
7376
|
+
? `<span class="plan-chip executed" title="${escapeHtml(p.executed_at)}">executed</span>` : '';
|
|
7377
|
+
return `<div class="plan-row" data-plan-id="${escapeHtml(p.plan_id)}" tabindex="0" role="button" aria-label="Open plan ${escapeHtml(p.plan_id)}">
|
|
7378
|
+
<span class="plan-chip kind-${escapeHtml(p.kind || '')}">${escapeHtml(PLAN_KIND_LABELS[p.kind] || p.kind || '?')}</span>
|
|
7379
|
+
<span class="summary" title="${escapeHtml(p.summary || '')}">${escapeHtml(p.summary || p.plan_id)}</span>
|
|
7380
|
+
${executed}
|
|
7381
|
+
<span class="saved-at">${escapeHtml(formatVersionDate(p.saved_at))}</span>
|
|
7382
|
+
</div>`;
|
|
7383
|
+
}).join('');
|
|
7384
|
+
body.querySelectorAll('.plan-row[data-plan-id]').forEach(row => {
|
|
7385
|
+
const open = () => openEditPlan(row.dataset.planId).catch(alertError);
|
|
7386
|
+
row.addEventListener('click', open);
|
|
7387
|
+
row.addEventListener('keydown', e => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); open(); } });
|
|
7388
|
+
});
|
|
7389
|
+
}
|
|
7390
|
+
|
|
7391
|
+
async function openEditPlan(planId, opts = {}) {
|
|
7392
|
+
state.plans.currentPlanId = planId;
|
|
7393
|
+
reviewSetView('plan', opts);
|
|
7394
|
+
const body = $('planDetailBody');
|
|
7395
|
+
const header = $('planDetailHeader');
|
|
7396
|
+
if (body) body.innerHTML = '<div class="empty">Loading…</div>';
|
|
7397
|
+
if (header) header.innerHTML = '';
|
|
7398
|
+
const data = await api(`/api/edit_plans/${encodeURIComponent(planId)}`)
|
|
7399
|
+
.catch(err => ({ success: false, error: String(err) }));
|
|
7400
|
+
if (!data || !data.success) {
|
|
7401
|
+
if (body) body.innerHTML = `<div class="empty">Error: ${escapeHtml(data?.error || 'unknown')}</div>`;
|
|
7402
|
+
return;
|
|
7403
|
+
}
|
|
7404
|
+
state.plans.payload = data;
|
|
7405
|
+
renderPlanDetail(data);
|
|
7406
|
+
}
|
|
7407
|
+
|
|
7408
|
+
function renderPlanDetail(data) {
|
|
7409
|
+
const header = $('planDetailHeader');
|
|
7410
|
+
const body = $('planDetailBody');
|
|
7411
|
+
if (!header || !body) return;
|
|
7412
|
+
if (data.corrupt) {
|
|
7413
|
+
header.innerHTML = `<span class="plan-chip corrupt">corrupt</span><span class="timeline-name">Plan ${escapeHtml(data.plan_id || '?')}</span>`;
|
|
7414
|
+
body.innerHTML = '<div class="empty">This plan failed its fingerprint check — it was edited on disk or truncated. It cannot be executed; re-plan to replace it.</div>';
|
|
7415
|
+
return;
|
|
7416
|
+
}
|
|
7417
|
+
const plan = data.plan || {};
|
|
7418
|
+
const executed = plan.executed_at
|
|
7419
|
+
? `<span class="plan-chip executed" title="${escapeHtml(plan.executed_at)}">executed ${escapeHtml(formatVersionDate(plan.executed_at))}</span>` : '';
|
|
7420
|
+
header.innerHTML = `
|
|
7421
|
+
<span class="plan-chip kind-${escapeHtml(plan.kind || '')}">${escapeHtml(PLAN_KIND_LABELS[plan.kind] || plan.kind || '?')}</span>
|
|
7422
|
+
<span class="timeline-name">${escapeHtml(plan.timeline_name || plan.plan_id || '')}</span>
|
|
7423
|
+
${executed}
|
|
7424
|
+
<span class="saved-at" style="color:var(--text-muted);font-size:var(--text-xs)">saved ${escapeHtml(formatVersionDate(plan.saved_at))} · plan ${escapeHtml(plan.plan_id || '')}</span>`;
|
|
7425
|
+
|
|
7426
|
+
const sections = [];
|
|
7427
|
+
if (plan.summary) {
|
|
7428
|
+
sections.push(`<div class="plan-section"><strong>Summary</strong><div>${escapeHtml(plan.summary)}</div></div>`);
|
|
7429
|
+
}
|
|
7430
|
+
// Chat-first execute affordance: the panel NEVER executes. Executed
|
|
7431
|
+
// plans keep the card (re-execution of selects is harmless and swap
|
|
7432
|
+
// may target another alternate) — the chip above signals state.
|
|
7433
|
+
sections.push(`<div class="plan-section"><strong>Execute (from chat)</strong>${chatPromptCard(
|
|
7434
|
+
plan.kind === 'swap'
|
|
7435
|
+
? 'Pick an alternate below, then paste this in your chat session (fill in <N>):'
|
|
7436
|
+
: 'Paste this in your chat session to execute the plan:',
|
|
7437
|
+
planExecutePrompt(plan))}</div>`);
|
|
7438
|
+
if (plan.execution_summary) {
|
|
7439
|
+
sections.push(`<div class="plan-section"><strong>Execution readback</strong><pre style="margin:0;white-space:pre-wrap;font-size:var(--text-xs)">${escapeHtml(JSON.stringify(plan.execution_summary, null, 2))}</pre></div>`);
|
|
7440
|
+
}
|
|
7441
|
+
if (plan.settings) {
|
|
7442
|
+
const settings = Object.entries(plan.settings)
|
|
7443
|
+
.map(([k, v]) => `${escapeHtml(k)}=${escapeHtml(v == null ? '—' : String(v))}`).join(' · ');
|
|
7444
|
+
sections.push(`<div class="plan-section"><strong>Settings</strong><div style="font-size:var(--text-xs);color:var(--text-secondary)">${settings}</div></div>`);
|
|
7445
|
+
}
|
|
7446
|
+
if (plan.kind === 'selects') {
|
|
7447
|
+
sections.push(renderSelectsDecisions(plan));
|
|
7448
|
+
} else if (plan.kind === 'tighten') {
|
|
7449
|
+
sections.push(renderTightenLifts(plan));
|
|
7450
|
+
} else if (plan.kind === 'swap') {
|
|
7451
|
+
sections.push(renderSwapAlternates(plan));
|
|
7452
|
+
}
|
|
7453
|
+
body.innerHTML = sections.join('');
|
|
7454
|
+
body.querySelectorAll('[data-open-shot-clip]').forEach(btn => {
|
|
7455
|
+
btn.addEventListener('click', () => {
|
|
7456
|
+
const clipId = btn.dataset.openShotClip;
|
|
7457
|
+
const shotIndex = Number(btn.dataset.openShotIndex);
|
|
7458
|
+
openClipDetail(clipId)
|
|
7459
|
+
.then(() => Number.isFinite(shotIndex) ? openShotDetail(shotIndex) : null)
|
|
7460
|
+
.catch(alertError);
|
|
7461
|
+
});
|
|
7462
|
+
});
|
|
7463
|
+
}
|
|
7464
|
+
|
|
7465
|
+
function renderSelectsDecisions(plan) {
|
|
7466
|
+
const decisions = plan.decisions || [];
|
|
7467
|
+
const rankLabel = { 3: 'high', 2: 'medium', 1: 'low' };
|
|
7468
|
+
const cards = decisions.map((d, i) => {
|
|
7469
|
+
const range = Array.isArray(d.source_frame_range) ? `frames ${d.source_frame_range[0]}–${d.source_frame_range[1]}` : '';
|
|
7470
|
+
const shotLink = d.resolve_clip_id != null && d.shot_index != null
|
|
7471
|
+
? `<button class="secondary" style="font-size:var(--text-xs);padding:1px 8px" data-open-shot-clip="${escapeHtml(d.resolve_clip_id)}" data-open-shot-index="${d.shot_index}">Open shot page</button>` : '';
|
|
7472
|
+
return `<div class="plan-decision-card">
|
|
7473
|
+
${planThumb(d, `decision ${i + 1}`)}
|
|
7474
|
+
<div class="body">
|
|
7475
|
+
<div class="title-row">
|
|
7476
|
+
<strong>${i + 1}. ${escapeHtml(d.clip_name || d.clip_uuid || '?')} · shot ${d.shot_index ?? '?'}</strong>
|
|
7477
|
+
<span class="plan-chip">${escapeHtml(rankLabel[d.rank] || `rank ${d.rank}`)}</span>
|
|
7478
|
+
<span class="meta">${(d.duration_seconds ?? '?')}s${range ? ` · ${range}` : ''}</span>
|
|
7479
|
+
${shotLink}
|
|
7480
|
+
</div>
|
|
7481
|
+
${d.description ? `<div class="meta">${escapeHtml(d.description)}</div>` : ''}
|
|
7482
|
+
<div class="rationale">${escapeHtml(d.rationale || '')}</div>
|
|
7483
|
+
</div>
|
|
7484
|
+
</div>`;
|
|
7485
|
+
});
|
|
7486
|
+
return `<div class="plan-section"><strong>Decisions (${decisions.length} shots, ~${plan.estimated_duration_seconds ?? '?'}s)</strong>${cards.join('') || '<div class="empty">No decisions.</div>'}</div>`;
|
|
7487
|
+
}
|
|
7488
|
+
|
|
7489
|
+
function renderTightenLifts(plan) {
|
|
7490
|
+
const lifts = plan.lifts || [];
|
|
7491
|
+
const rows = lifts.map(l => {
|
|
7492
|
+
const gap = l.evidence?.source_gap_seconds;
|
|
7493
|
+
return `<tr>
|
|
7494
|
+
<td class="edit-type">${escapeHtml(l.kind || 'lift')}</td>
|
|
7495
|
+
<td>${escapeHtml(l.item_name || '—')}</td>
|
|
7496
|
+
<td>${l.timeline_start_frame}–${l.timeline_end_frame}</td>
|
|
7497
|
+
<td>${l.duration_seconds ?? '—'}s</td>
|
|
7498
|
+
<td>${escapeHtml(l.rationale || '')}${gap ? ` <span style="color:var(--text-muted)">(gap ${gap[0]}–${gap[1]}s)</span>` : ''}</td>
|
|
7499
|
+
</tr>`;
|
|
7500
|
+
}).join('');
|
|
7501
|
+
const skipped = (plan.skipped || []).map(s =>
|
|
7502
|
+
`<div class="plan-decision-card"><div class="body"><div class="meta">skipped ${escapeHtml(s.item || '?')}: ${escapeHtml(s.reason || '')}</div></div></div>`).join('');
|
|
7503
|
+
return `<div class="plan-section">
|
|
7504
|
+
<strong>Lifts (${lifts.length} · ${plan.keep_ranges ? plan.keep_ranges.length : 0} keep ranges)</strong>
|
|
7505
|
+
<table class="history-edits-table">
|
|
7506
|
+
<thead><tr><th>Kind</th><th>Item</th><th>Timeline frames</th><th>Removes</th><th>Rationale</th></tr></thead>
|
|
7507
|
+
<tbody>${rows || '<tr><td colspan="5">No lifts.</td></tr>'}</tbody>
|
|
7508
|
+
</table>
|
|
7509
|
+
${skipped}
|
|
7510
|
+
</div>`;
|
|
7511
|
+
}
|
|
7512
|
+
|
|
7513
|
+
function renderSwapAlternates(plan) {
|
|
7514
|
+
const alternates = plan.alternates || [];
|
|
7515
|
+
const item = plan.item || {};
|
|
7516
|
+
const current = `<div class="plan-decision-card"><div class="body">
|
|
7517
|
+
<div class="title-row"><strong>Current: ${escapeHtml(item.item_name || 'item')}</strong>
|
|
7518
|
+
<span class="meta">slot ${item.timeline_start_frame}–${item.timeline_end_frame} · track ${item.track_index ?? 1}</span></div>
|
|
7519
|
+
${item.current_description ? `<div class="meta">${escapeHtml(item.current_description)}</div>` : ''}
|
|
7520
|
+
</div></div>`;
|
|
7521
|
+
const cards = alternates.map((a, i) => {
|
|
7522
|
+
const range = Array.isArray(a.source_frame_range) ? `frames ${a.source_frame_range[0]}–${a.source_frame_range[1]}` : '';
|
|
7523
|
+
const shotLink = a.resolve_clip_id != null && a.shot_index != null
|
|
7524
|
+
? `<button class="secondary" style="font-size:var(--text-xs);padding:1px 8px" data-open-shot-clip="${escapeHtml(a.resolve_clip_id)}" data-open-shot-index="${a.shot_index}">Open shot page</button>` : '';
|
|
7525
|
+
return `<div class="plan-decision-card">
|
|
7526
|
+
${planThumb(a, `alternate ${i}`)}
|
|
7527
|
+
<div class="body">
|
|
7528
|
+
<div class="title-row">
|
|
7529
|
+
<strong>alternate_index ${i} · ${escapeHtml(a.clip_name || '?')} · shot ${a.shot_index ?? '?'}</strong>
|
|
7530
|
+
<span class="plan-chip">score ${a.score ?? '?'}</span>
|
|
7531
|
+
<span class="meta">${range}</span>
|
|
7532
|
+
${shotLink}
|
|
7533
|
+
</div>
|
|
7534
|
+
${a.description ? `<div class="meta">${escapeHtml(a.description)}</div>` : ''}
|
|
7535
|
+
<div class="rationale">${escapeHtml(a.rationale || '')}</div>
|
|
7536
|
+
</div>
|
|
7537
|
+
</div>`;
|
|
7538
|
+
});
|
|
7539
|
+
return `<div class="plan-section"><strong>Alternates (${alternates.length})</strong>${current}${cards.join('') || '<div class="empty">No alternates.</div>'}</div>`;
|
|
7540
|
+
}
|
|
7541
|
+
|
|
7213
7542
|
// ─── Run scoping controls ──────────────────────────────────────────
|
|
7214
7543
|
state.runScope = state.runScope || { current: null, recent: [] };
|
|
7215
7544
|
|
|
@@ -8088,6 +8417,10 @@ HTML = r"""<!doctype html>
|
|
|
8088
8417
|
if (combinedEl) combinedEl.style.display = view === 'combined' ? '' : 'none';
|
|
8089
8418
|
const historyEl = $('reviewHistoryView');
|
|
8090
8419
|
if (historyEl) historyEl.style.display = view === 'history' ? '' : 'none';
|
|
8420
|
+
const plansEl = $('reviewPlansView');
|
|
8421
|
+
if (plansEl) plansEl.style.display = view === 'plans' ? '' : 'none';
|
|
8422
|
+
const planEl = $('reviewPlanView');
|
|
8423
|
+
if (planEl) planEl.style.display = view === 'plan' ? '' : 'none';
|
|
8091
8424
|
const back = $('reviewBackBtn');
|
|
8092
8425
|
if (back) back.style.display = view === 'bin' ? 'none' : '';
|
|
8093
8426
|
const meta = $('reviewMeta');
|
|
@@ -8098,6 +8431,8 @@ HTML = r"""<!doctype html>
|
|
|
8098
8431
|
else if (view === 'transcript') meta.textContent = `Transcript · ${state.review.currentClipData?.card?.clip_name || 'clip'}`;
|
|
8099
8432
|
else if (view === 'combined') meta.textContent = `Combined review · ${state.review.combinedData?.clip_count || '?'} clips`;
|
|
8100
8433
|
else if (view === 'history') meta.textContent = 'Timeline history · archived versions and brain edits per timeline';
|
|
8434
|
+
else if (view === 'plans') meta.textContent = 'Edit plans · dry-run plans saved by the edit engine';
|
|
8435
|
+
else if (view === 'plan') meta.textContent = `Plan ${state.plans?.currentPlanId || ''} · review here, execute from chat`;
|
|
8101
8436
|
}
|
|
8102
8437
|
if (opts.writePanelState !== false) {
|
|
8103
8438
|
writePanelStateAsync({
|
|
@@ -10470,6 +10805,12 @@ HTML = r"""<!doctype html>
|
|
|
10470
10805
|
if (window.location.hash !== '#analysis/review/history') {
|
|
10471
10806
|
window.history.replaceState(null, '', '#analysis/review/history');
|
|
10472
10807
|
}
|
|
10808
|
+
} else if (control.dataset.reviewView === 'plans') {
|
|
10809
|
+
reviewSetView('plans');
|
|
10810
|
+
refreshEditPlans().catch(alertError);
|
|
10811
|
+
if (window.location.hash !== '#analysis/review/plans') {
|
|
10812
|
+
window.history.replaceState(null, '', '#analysis/review/plans');
|
|
10813
|
+
}
|
|
10473
10814
|
}
|
|
10474
10815
|
});
|
|
10475
10816
|
});
|
|
@@ -10588,6 +10929,10 @@ HTML = r"""<!doctype html>
|
|
|
10588
10929
|
if (historyRefreshEl) {
|
|
10589
10930
|
historyRefreshEl.onclick = () => refreshHistoryTimelines().catch(alertError);
|
|
10590
10931
|
}
|
|
10932
|
+
const plansRefreshEl = $('plansRefreshBtn');
|
|
10933
|
+
if (plansRefreshEl) {
|
|
10934
|
+
plansRefreshEl.onclick = () => refreshEditPlans().catch(alertError);
|
|
10935
|
+
}
|
|
10591
10936
|
const historyArchiveBtnEl = $('historyArchiveCurrentBtn');
|
|
10592
10937
|
if (historyArchiveBtnEl) {
|
|
10593
10938
|
historyArchiveBtnEl.onclick = () => {
|
|
@@ -10707,6 +11052,13 @@ HTML = r"""<!doctype html>
|
|
|
10707
11052
|
reviewSetView('bin');
|
|
10708
11053
|
} else if (state.review.view === 'history') {
|
|
10709
11054
|
reviewSetView('bin');
|
|
11055
|
+
} else if (state.review.view === 'plan') {
|
|
11056
|
+
state.plans.currentPlanId = null;
|
|
11057
|
+
state.plans.payload = null;
|
|
11058
|
+
reviewSetView('plans');
|
|
11059
|
+
refreshEditPlans().catch(alertError);
|
|
11060
|
+
} else if (state.review.view === 'plans') {
|
|
11061
|
+
reviewSetView('bin');
|
|
10710
11062
|
}
|
|
10711
11063
|
};
|
|
10712
11064
|
$('reviewBinGrid').addEventListener('click', event => {
|
|
@@ -13361,6 +13713,71 @@ def get_timeline_history_payload(
|
|
|
13361
13713
|
}
|
|
13362
13714
|
|
|
13363
13715
|
|
|
13716
|
+
def list_edit_plans_payload(project_root: str) -> Dict[str, Any]:
|
|
13717
|
+
"""Edit-engine plan list for the panel browser (DB/file only, no Resolve).
|
|
13718
|
+
|
|
13719
|
+
Fingerprint-corrupt plans surface as {"plan_id", "corrupt": True} warning
|
|
13720
|
+
rows rather than being silently hidden.
|
|
13721
|
+
"""
|
|
13722
|
+
try:
|
|
13723
|
+
from src.utils import edit_engine as _edit_engine
|
|
13724
|
+
return _edit_engine.list_plans(project_root, limit=50, include_corrupt=True)
|
|
13725
|
+
except Exception as exc: # noqa: BLE001 — panel reads fail soft
|
|
13726
|
+
return {"success": False, "error": f"{type(exc).__name__}: {exc}", "plans": []}
|
|
13727
|
+
|
|
13728
|
+
|
|
13729
|
+
def get_edit_plan_payload(project_root: str, plan_id: str) -> Dict[str, Any]:
|
|
13730
|
+
"""Full plan detail for the panel, enriched for rendering: selects
|
|
13731
|
+
decisions and swap alternates gain a `thumb_frame_index` (the shot's first
|
|
13732
|
+
sampled frame, for the existing /api/clips/<id>/frames/<idx> route) and a
|
|
13733
|
+
`resolve_clip_id` fallback mapped from clip_uuid. Enrichment is best-effort
|
|
13734
|
+
— the plan still renders without thumbnails when the DB is unavailable.
|
|
13735
|
+
"""
|
|
13736
|
+
try:
|
|
13737
|
+
from src.utils import edit_engine as _edit_engine
|
|
13738
|
+
plan = _edit_engine.load_plan(project_root, plan_id)
|
|
13739
|
+
except Exception as exc: # noqa: BLE001
|
|
13740
|
+
return {"success": False, "error": f"{type(exc).__name__}: {exc}"}
|
|
13741
|
+
if plan is None:
|
|
13742
|
+
return {"success": False, "error": f"Plan {plan_id} not found"}
|
|
13743
|
+
if plan.get("_corrupt"):
|
|
13744
|
+
return {"success": True, "plan_id": plan_id, "corrupt": True}
|
|
13745
|
+
plan = json.loads(json.dumps(plan, default=str)) # detach a plain copy
|
|
13746
|
+
try:
|
|
13747
|
+
conn = _timeline_brain_db.connect(project_root)
|
|
13748
|
+
clip_id_cache: Dict[str, Any] = {}
|
|
13749
|
+
|
|
13750
|
+
def _enrich(row: Dict[str, Any]) -> None:
|
|
13751
|
+
clip_uuid = str(row.get("clip_uuid") or "")
|
|
13752
|
+
if not row.get("resolve_clip_id") and clip_uuid:
|
|
13753
|
+
if clip_uuid not in clip_id_cache:
|
|
13754
|
+
hit = conn.execute(
|
|
13755
|
+
"SELECT resolve_clip_id FROM clips WHERE clip_uuid = ?",
|
|
13756
|
+
(clip_uuid,),
|
|
13757
|
+
).fetchone()
|
|
13758
|
+
clip_id_cache[clip_uuid] = hit["resolve_clip_id"] if hit else None
|
|
13759
|
+
if clip_id_cache[clip_uuid]:
|
|
13760
|
+
row["resolve_clip_id"] = clip_id_cache[clip_uuid]
|
|
13761
|
+
shot_uuid = row.get("shot_uuid")
|
|
13762
|
+
if shot_uuid and row.get("thumb_frame_index") is None:
|
|
13763
|
+
hit = conn.execute(
|
|
13764
|
+
"SELECT MIN(frame_index) AS frame_index FROM frames WHERE shot_uuid = ?",
|
|
13765
|
+
(str(shot_uuid),),
|
|
13766
|
+
).fetchone()
|
|
13767
|
+
if hit and hit["frame_index"] is not None:
|
|
13768
|
+
row["thumb_frame_index"] = int(hit["frame_index"])
|
|
13769
|
+
|
|
13770
|
+
for decision in plan.get("decisions") or []:
|
|
13771
|
+
if isinstance(decision, dict):
|
|
13772
|
+
_enrich(decision)
|
|
13773
|
+
for alternate in plan.get("alternates") or []:
|
|
13774
|
+
if isinstance(alternate, dict):
|
|
13775
|
+
_enrich(alternate)
|
|
13776
|
+
except Exception: # noqa: BLE001 — thumbnails are progressive enhancement
|
|
13777
|
+
pass
|
|
13778
|
+
return {"success": True, "corrupt": False, "plan": plan}
|
|
13779
|
+
|
|
13780
|
+
|
|
13364
13781
|
def proxy_timeline_versioning_action(body: Dict[str, Any]) -> Dict[str, Any]:
|
|
13365
13782
|
"""Bridge dashboard → MCP server timeline_versioning tool.
|
|
13366
13783
|
|
|
@@ -14473,6 +14890,21 @@ class Handler(BaseHTTPRequestHandler):
|
|
|
14473
14890
|
return
|
|
14474
14891
|
self._json(get_timeline_history_payload(self.state.project_root, timeline_name))
|
|
14475
14892
|
return
|
|
14893
|
+
# ─── Edit-engine plan browser (DB/file only — no Resolve) ───────
|
|
14894
|
+
if path == "/api/edit_plans":
|
|
14895
|
+
self._json(list_edit_plans_payload(self.state.project_root))
|
|
14896
|
+
return
|
|
14897
|
+
if path.startswith("/api/edit_plans/"):
|
|
14898
|
+
plan_id = unquote(path[len("/api/edit_plans/"):])
|
|
14899
|
+
if not plan_id:
|
|
14900
|
+
self._json({"success": False, "error": "plan_id required"}, HTTPStatus.BAD_REQUEST)
|
|
14901
|
+
return
|
|
14902
|
+
payload = get_edit_plan_payload(self.state.project_root, plan_id)
|
|
14903
|
+
if not payload.get("success") and "not found" in str(payload.get("error", "")):
|
|
14904
|
+
self._json(payload, HTTPStatus.NOT_FOUND)
|
|
14905
|
+
return
|
|
14906
|
+
self._json(payload)
|
|
14907
|
+
return
|
|
14476
14908
|
if path == "/api/brain_edits/registry":
|
|
14477
14909
|
self._json({"success": True, **_brain_edits.read_brain_edits_registry(self.state.project_root)})
|
|
14478
14910
|
return
|
package/src/granular/common.py
CHANGED
|
@@ -80,7 +80,7 @@ if not logging.getLogger().handlers:
|
|
|
80
80
|
handlers=[logging.StreamHandler()],
|
|
81
81
|
)
|
|
82
82
|
|
|
83
|
-
VERSION = "2.
|
|
83
|
+
VERSION = "2.47.0"
|
|
84
84
|
logger = logging.getLogger("davinci-resolve-mcp")
|
|
85
85
|
logger.info(f"Starting DaVinci Resolve MCP Server v{VERSION}")
|
|
86
86
|
logger.info(f"Detected platform: {get_platform()}")
|
package/src/server.py
CHANGED
package/src/utils/edit_engine.py
CHANGED
|
@@ -81,7 +81,13 @@ def load_plan(project_root: str, plan_id: str) -> Optional[Dict[str, Any]]:
|
|
|
81
81
|
return plan
|
|
82
82
|
|
|
83
83
|
|
|
84
|
-
def list_plans(project_root: str, *, limit: int = 20) -> Dict[str, Any]:
|
|
84
|
+
def list_plans(project_root: str, *, limit: int = 20, include_corrupt: bool = False) -> Dict[str, Any]:
|
|
85
|
+
"""List saved plans, newest first.
|
|
86
|
+
|
|
87
|
+
`include_corrupt=True` (panel browser) surfaces fingerprint-mismatched
|
|
88
|
+
plans as ``{"plan_id", "corrupt": True}`` warning rows instead of hiding
|
|
89
|
+
them; the default keeps the MCP-action shape unchanged.
|
|
90
|
+
"""
|
|
85
91
|
directory = _plan_dir(project_root)
|
|
86
92
|
rows: List[Dict[str, Any]] = []
|
|
87
93
|
if os.path.isdir(directory):
|
|
@@ -90,6 +96,8 @@ def list_plans(project_root: str, *, limit: int = 20) -> Dict[str, Any]:
|
|
|
90
96
|
continue
|
|
91
97
|
plan = load_plan(project_root, name[:-5])
|
|
92
98
|
if not plan or plan.get("_corrupt"):
|
|
99
|
+
if include_corrupt:
|
|
100
|
+
rows.append({"plan_id": name[:-5], "corrupt": True})
|
|
93
101
|
continue
|
|
94
102
|
rows.append({
|
|
95
103
|
"plan_id": plan.get("plan_id"),
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"""Best-effort deletion of disposable Resolve projects.
|
|
2
|
+
|
|
3
|
+
DeleteProject is flaky on some Resolve builds: it silently returns False when
|
|
4
|
+
the target is (or was very recently) the current project, and occasionally on
|
|
5
|
+
the first attempt even when it isn't. Disposable test projects then linger in
|
|
6
|
+
the project library. This helper centralizes the mitigation so every
|
|
7
|
+
disposable-project flow gets it for free:
|
|
8
|
+
|
|
9
|
+
1. make sure the target is not the current project (load a fallback project,
|
|
10
|
+
or close the target if no fallback is available),
|
|
11
|
+
2. retry the delete once after a short pause,
|
|
12
|
+
3. report the leftover by name when it still fails, so callers can surface it
|
|
13
|
+
instead of silently leaking.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
import time
|
|
17
|
+
from typing import Any, Dict, Optional
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def delete_project_safely(
|
|
21
|
+
pm: Any,
|
|
22
|
+
name: str,
|
|
23
|
+
*,
|
|
24
|
+
switch_to: Optional[str] = None,
|
|
25
|
+
retries: int = 1,
|
|
26
|
+
delay_seconds: float = 1.0,
|
|
27
|
+
) -> Dict[str, Any]:
|
|
28
|
+
"""Delete project `name` via project-manager handle `pm`, working around
|
|
29
|
+
DeleteProject flakiness. Returns {success, attempts, leftover, detail}.
|
|
30
|
+
|
|
31
|
+
`switch_to`: project to load first when `name` is current (e.g. the
|
|
32
|
+
project that was open before the disposable one was created). Without it,
|
|
33
|
+
the current project is closed instead.
|
|
34
|
+
"""
|
|
35
|
+
attempts = 0
|
|
36
|
+
detail = ""
|
|
37
|
+
try:
|
|
38
|
+
current = None
|
|
39
|
+
try:
|
|
40
|
+
project = pm.GetCurrentProject()
|
|
41
|
+
current = project.GetName() if project else None
|
|
42
|
+
except Exception:
|
|
43
|
+
current = None
|
|
44
|
+
if current == name:
|
|
45
|
+
switched = False
|
|
46
|
+
if switch_to and switch_to != name:
|
|
47
|
+
try:
|
|
48
|
+
switched = bool(pm.LoadProject(switch_to))
|
|
49
|
+
except Exception:
|
|
50
|
+
switched = False
|
|
51
|
+
if not switched:
|
|
52
|
+
try:
|
|
53
|
+
project = pm.GetCurrentProject()
|
|
54
|
+
if project is not None:
|
|
55
|
+
pm.CloseProject(project)
|
|
56
|
+
except Exception:
|
|
57
|
+
pass
|
|
58
|
+
|
|
59
|
+
last_error = None
|
|
60
|
+
for attempt in range(1 + max(0, int(retries))):
|
|
61
|
+
attempts = attempt + 1
|
|
62
|
+
try:
|
|
63
|
+
if bool(pm.DeleteProject(name)):
|
|
64
|
+
return {"success": True, "attempts": attempts, "leftover": None, "detail": ""}
|
|
65
|
+
last_error = "DeleteProject returned False"
|
|
66
|
+
except Exception as exc:
|
|
67
|
+
last_error = str(exc)
|
|
68
|
+
if attempt < retries:
|
|
69
|
+
time.sleep(max(0.0, delay_seconds))
|
|
70
|
+
detail = last_error or "DeleteProject failed"
|
|
71
|
+
except Exception as exc:
|
|
72
|
+
detail = str(exc)
|
|
73
|
+
return {"success": False, "attempts": attempts, "leftover": name, "detail": detail}
|