synabun 2026.4.21 → 2026.4.25-beta.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 (59) hide show
  1. package/CHANGELOG.md +161 -881
  2. package/README.md +1 -1
  3. package/hooks/claude-code/prompt-submit.mjs +39 -18
  4. package/hooks/claude-code/session-start.mjs +10 -3
  5. package/hooks/claude-code/shared.mjs +39 -2
  6. package/hooks/claude-code/stop.mjs +33 -6
  7. package/mcp-server/src/index.ts +9 -1
  8. package/mcp-server/src/services/neural-interface.ts +42 -14
  9. package/mcp-server/src/services/profiles.ts +8 -7
  10. package/mcp-server/src/tools/browser-advanced.ts +12 -10
  11. package/mcp-server/src/tools/browser-interact.ts +39 -38
  12. package/mcp-server/src/tools/browser-navigate.ts +12 -4
  13. package/mcp-server/src/tools/loop.ts +7 -3
  14. package/mcp-server/src/tools/profile.ts +19 -1
  15. package/neural-interface/lib/loop-logger.js +103 -0
  16. package/neural-interface/lib/mcp-installer.js +109 -0
  17. package/neural-interface/lib/plugin-installer.js +264 -0
  18. package/neural-interface/package.json +1 -1
  19. package/neural-interface/public/shared/api.js +66 -17
  20. package/neural-interface/public/shared/cdx/cdx-icons.js +5 -4
  21. package/neural-interface/public/shared/cdx/cdx-panel.js +44 -14
  22. package/neural-interface/public/shared/cdx/cdx-render.js +170 -43
  23. package/neural-interface/public/shared/cdx/cdx-styles.js +23 -10
  24. package/neural-interface/public/shared/cdx/cdx-tabs.js +384 -70
  25. package/neural-interface/public/shared/constants.js +3 -2
  26. package/neural-interface/public/shared/html-shell.js +32 -11
  27. package/neural-interface/public/shared/ocp/ocp-panel.js +45 -28
  28. package/neural-interface/public/shared/ocp/ocp-render.js +50 -43
  29. package/neural-interface/public/shared/ocp/ocp-tabs.js +693 -82
  30. package/neural-interface/public/shared/styles.css +635 -0
  31. package/neural-interface/public/shared/ui-automation-studio.js +38 -12
  32. package/neural-interface/public/shared/ui-claude-panel.js +216 -21
  33. package/neural-interface/public/shared/ui-file-explorer.js +38 -18
  34. package/neural-interface/public/shared/ui-menubar.js +34 -10
  35. package/neural-interface/public/shared/ui-navbar.js +8 -6
  36. package/neural-interface/public/shared/ui-schedules-studio.js +1464 -0
  37. package/neural-interface/public/shared/ui-settings.js +274 -50
  38. package/neural-interface/public/shared/ui-sidepanel-layout.js +18 -7
  39. package/neural-interface/public/shared/ui-sidepanel-tray.js +287 -0
  40. package/neural-interface/public/shared/ui-sync.js +9 -5
  41. package/neural-interface/public/shared/ui-terminal.js +76 -27
  42. package/neural-interface/public/shared/ui-tool-updates.js +49 -15
  43. package/neural-interface/public/shared/ui-tutorial-steps.js +4 -2
  44. package/neural-interface/public/shared/ui-tutorial.js +24 -18
  45. package/neural-interface/public/shared/ui-whiteboard.js +79 -31
  46. package/neural-interface/public/shared/vterm-buffer.js +9 -8
  47. package/neural-interface/public/variant/2d/main.js +9 -7
  48. package/neural-interface/public/variant/3d/main.js +10 -8
  49. package/neural-interface/server.js +1281 -252
  50. package/package.json +1 -1
  51. package/skills/leonardo/SKILL.md +5 -1
  52. package/skills/leonardo/modules/image-prompter.md +5 -1
  53. package/skills/leonardo/modules/video-prompter.md +5 -1
  54. package/skills/synabun/SKILL.md +44 -26
  55. package/skills/synabun/modules/audit.md +40 -36
  56. package/skills/synabun/modules/changelog.md +22 -18
  57. package/skills/synabun/modules/memorize.md +19 -15
  58. package/skills/synabun/modules/schedule.md +285 -0
  59. package/CLAUDE.md +0 -217
@@ -9,7 +9,7 @@ import { isClaudePanelOpen, toggleClaudePanel } from '../ui-claude-panel.js';
9
9
  import { reserveRightPanelLayout, clearRightPanelLayout } from '../ui-sidepanel-layout.js';
10
10
  import { notify, NOTIF_TYPE } from '../ui-notifications.js';
11
11
  import {
12
- STOR, EFFORT_LEVELS, MAX_TABS, MAX_THREAD_SNAPSHOTS, MAX_THREAD_SNAPSHOT_CHARS,
12
+ STOR, EFFORT_LEVELS, MAX_THREAD_SNAPSHOTS, MAX_THREAD_SNAPSHOT_CHARS,
13
13
  CODEX_GREETING_MARKER_START, CODEX_GREETING_MARKER_END, CODEX_PLAN_MODE_PREFIXES,
14
14
  STATUS_TONE, PANEL_OWNER, windowId, STALL_WARN_MS, STALL_KILL_MS,
15
15
  OPENAI_ICON, ICON_SPARK, ICON_TERMINAL, ICON_TOOL, ICON_FILES, ICON_PLUS,
@@ -73,7 +73,12 @@ function scrollEnd() { _onScrollEnd?.(); }
73
73
  function autosizeInput() { _onAutosizeInput?.(); }
74
74
  function emit(...args) { _onEmit?.(...args); }
75
75
 
76
- function sendSocket(msg) { return wsSendSocket(msg, _ws); }
76
+ function sendSocket(msg) {
77
+ const packet = msg && _boundTab?.id && !msg.sessionId
78
+ ? { sessionId: _boundTab.id, ...msg }
79
+ : msg;
80
+ return wsSendSocket(packet, _ws);
81
+ }
77
82
 
78
83
  // ── connectTab / disconnectTab wrappers (build callbacks for cdx-ws) ──
79
84
 
@@ -111,8 +116,14 @@ function buildWsCallbacks(tab) {
111
116
  onClose(tab, ws, { intentional }) {
112
117
  _connected = false;
113
118
  _bootstrapped = false;
119
+ _pendingReattach = !intentional;
114
120
  _ws = null;
115
- if (_boundTab) { _boundTab.connected = false; _boundTab.bootstrapped = false; _boundTab.ws = null; }
121
+ if (_boundTab) {
122
+ _boundTab.connected = false;
123
+ _boundTab.bootstrapped = false;
124
+ _boundTab.pendingReattach = _pendingReattach;
125
+ _boundTab.ws = null;
126
+ }
116
127
  syncInputEnabled();
117
128
  syncSessionControls();
118
129
  if (!intentional && !tab.closed) {
@@ -124,6 +135,8 @@ function buildWsCallbacks(tab) {
124
135
  },
125
136
 
126
137
  onReattachResult(msg) {
138
+ _pendingReattach = false;
139
+ if (_boundTab) _boundTab.pendingReattach = false;
127
140
  if (msg.ok) {
128
141
  _threadId = msg.threadId || _threadId;
129
142
  if (_boundTab) _boundTab.threadId = _threadId;
@@ -244,7 +257,14 @@ function buildWsCallbacks(tab) {
244
257
  onInterruptAck(msg) {
245
258
  _running = false;
246
259
  _activeTurnId = null;
247
- if (_boundTab) { _boundTab.running = false; _boundTab.activeTurnId = null; }
260
+ if (_boundTab) {
261
+ _boundTab.running = false;
262
+ _boundTab.activeTurnId = null;
263
+ if (_boundTab._abortRetry) {
264
+ clearTimeout(_boundTab._abortRetry);
265
+ _boundTab._abortRetry = null;
266
+ }
267
+ }
248
268
  clearInterruptTimer();
249
269
  stopStallTimer();
250
270
  setStatus('Interrupted', 'muted');
@@ -265,7 +285,7 @@ function buildWsCallbacks(tab) {
265
285
  if (window.__cxpModelListResolve) {
266
286
  window.__cxpModelListResolve(models);
267
287
  } else {
268
- populateModelDropdown(models.length ? models : CODEX_DEFAULT_MODELS);
288
+ populateModelDropdown(mergeCodexModelList(models));
269
289
  }
270
290
  },
271
291
 
@@ -355,6 +375,9 @@ let _planContent = '';
355
375
  let _editedPlanContent = '';
356
376
  let _showPostPlanActions = false;
357
377
  let _postPlanHeader = 'PLAN COMPLETE';
378
+ let _planTurnActive = false;
379
+ let _planApprovalPending = false;
380
+ let _lastPlanTurnId = '';
358
381
  let _showPostCompactionPrompt = false;
359
382
  let _postCompactionPending = false;
360
383
  let _stallTimer = null;
@@ -746,6 +769,12 @@ export function flushThreadSnapshotSave(tab = _boundTab) {
746
769
  writeThreadSnapshot(tab);
747
770
  }
748
771
 
772
+ export function flushAllThreadSnapshots() {
773
+ for (const tab of _tabs) {
774
+ try { flushThreadSnapshotSave(tab); } catch {}
775
+ }
776
+ }
777
+
749
778
  function normalizeTokenUsageBreakdown(value) {
750
779
  if (!value || typeof value !== 'object') return null;
751
780
  return {
@@ -822,18 +851,65 @@ let _hljs = null;
822
851
 
823
852
 
824
853
  // ── Plan state functions (lines 886-938) ──
854
+ function isPlanModePromptText(text) {
855
+ const value = String(text || '').trim();
856
+ if (!value) return false;
857
+ return /^\[PLAN MODE\b/i.test(value)
858
+ || CODEX_PLAN_MODE_PREFIXES.some((prefix) => value.startsWith(prefix));
859
+ }
860
+
861
+ function syncPlanHandoffFlags(tab = _boundTab) {
862
+ if (!tab || !isActiveTab(tab)) return;
863
+ _planTurnActive = !!tab.planTurnActive;
864
+ _planApprovalPending = !!tab.planApprovalPending;
865
+ _lastPlanTurnId = tab.lastPlanTurnId || '';
866
+ }
867
+
868
+ function setPlanTurnActive(tab = _boundTab, value, turnId = '') {
869
+ if (!tab) return;
870
+ tab.planTurnActive = !!value;
871
+ if (turnId !== undefined) tab.lastPlanTurnId = turnId || '';
872
+ syncPlanHandoffFlags(tab);
873
+ }
874
+
875
+ function setPlanApprovalPending(tab = _boundTab, value) {
876
+ if (!tab) return;
877
+ tab.planApprovalPending = !!value;
878
+ syncPlanHandoffFlags(tab);
879
+ }
880
+
881
+ function shouldHoldForPlanApproval(tab = activeTab()) {
882
+ return !!(tab?.showPostPlanActions || tab?.planApprovalPending);
883
+ }
884
+
885
+ function captureCompletedPlanItem(tab = _boundTab, item = null) {
886
+ if (!tab || !tab.planTurnActive || !item) return '';
887
+ const itemType = item.type || '';
888
+ if (itemType !== 'plan' && itemType !== 'agentMessage') return '';
889
+ const state = item.id ? tab.items?.get?.(item.id) : null;
890
+ const text = String(state?.buffer || item.text || '').trim();
891
+ if (text.length <= 80) return '';
892
+ return capturePlanContent(tab, text);
893
+ }
894
+
825
895
  export function clearPlanHandoffState(tab = _boundTab, { keepFilePath = false } = {}) {
826
896
  if (!tab) return;
827
897
  tab.planContent = '';
828
898
  tab.editedPlanContent = '';
829
899
  tab.showPostPlanActions = false;
830
900
  tab.postPlanHeader = 'PLAN COMPLETE';
901
+ tab.planTurnActive = false;
902
+ tab.planApprovalPending = false;
903
+ tab.lastPlanTurnId = '';
831
904
  if (!keepFilePath) tab.planFilePath = '';
832
905
  if (isActiveTab(tab)) {
833
906
  _planContent = '';
834
907
  _editedPlanContent = '';
835
908
  _showPostPlanActions = false;
836
909
  _postPlanHeader = 'PLAN COMPLETE';
910
+ _planTurnActive = false;
911
+ _planApprovalPending = false;
912
+ _lastPlanTurnId = '';
837
913
  if (!keepFilePath) _planFilePath = '';
838
914
  }
839
915
  removePostPlanCards(tab.messagesEl);
@@ -914,6 +990,39 @@ export function capturePlanContent(tab = _boundTab, explicitText = '') {
914
990
  return next;
915
991
  }
916
992
 
993
+ export async function ensurePlanFile(tab = _boundTab) {
994
+ if (!tab) return '';
995
+ if (tab.planFilePath) return tab.planFilePath;
996
+ if (tab._planFilePromise) return tab._planFilePromise;
997
+ const planText = capturePlanContent(tab);
998
+ if (!planText) return '';
999
+
1000
+ tab._planFilePromise = fetch('/api/create-plan', {
1001
+ method: 'POST',
1002
+ headers: { 'Content-Type': 'application/json' },
1003
+ body: JSON.stringify({ content: planText, cwd: tab.project || _project || '' }),
1004
+ })
1005
+ .then(async (res) => {
1006
+ const result = await res.json().catch(() => ({}));
1007
+ if (!res.ok || !result?.ok || !result.path) {
1008
+ throw new Error(result?.error || 'create-plan failed');
1009
+ }
1010
+ tab.planFilePath = result.path;
1011
+ if (isActiveTab(tab)) _planFilePath = result.path;
1012
+ saveTabs();
1013
+ return result.path;
1014
+ })
1015
+ .catch((err) => {
1016
+ console.warn('[codex-panel] create-plan failed:', err);
1017
+ return '';
1018
+ })
1019
+ .finally(() => {
1020
+ tab._planFilePromise = null;
1021
+ });
1022
+
1023
+ return tab._planFilePromise;
1024
+ }
1025
+
917
1026
  // ── Formatting & helpers (lines 1148-1421) ──
918
1027
  export function formatStatus(status) {
919
1028
  if (!status) return 'pending';
@@ -966,6 +1075,7 @@ function formatContextTrackerState(meta) {
966
1075
  }
967
1076
 
968
1077
  const CODEX_MODEL_PRICING = {
1078
+ 'gpt-5.5': { input: 5.00, cachedInput: 5.00, output: 30.00 },
969
1079
  'gpt-5.4': { input: 2.50, cachedInput: 0.25, output: 15.00 },
970
1080
  'gpt-5.4-mini': { input: 0.75, cachedInput: 0.075, output: 4.50 },
971
1081
  };
@@ -1000,10 +1110,15 @@ function itemHeadline(item) {
1000
1110
  : 'Preparing file changes',
1001
1111
  };
1002
1112
  case 'reasoning':
1003
- return {
1004
- title: 'Reasoning',
1005
- detail: cleanPreview(Array.isArray(item.summary) ? item.summary.join(' ') : 'Working through the next step'),
1006
- };
1113
+ {
1114
+ const summaryText = Array.isArray(item.summary)
1115
+ ? item.summary.map((part) => typeof part === 'string' ? part : (part?.text || part?.summaryText || '')).filter(Boolean).join(' ')
1116
+ : (item.summaryText || item.summary_text || item.text || '');
1117
+ return {
1118
+ title: 'Reasoning summary',
1119
+ detail: cleanPreview(summaryText || 'Working through the next step'),
1120
+ };
1121
+ }
1007
1122
  case 'plan':
1008
1123
  return {
1009
1124
  title: 'Updating plan',
@@ -1269,6 +1384,9 @@ export function snapshotBoundState() {
1269
1384
  editedPlanContent: _editedPlanContent,
1270
1385
  showPostPlanActions: _showPostPlanActions,
1271
1386
  postPlanHeader: _postPlanHeader,
1387
+ planTurnActive: _planTurnActive,
1388
+ planApprovalPending: _planApprovalPending,
1389
+ lastPlanTurnId: _lastPlanTurnId,
1272
1390
  showPostCompactionPrompt: _showPostCompactionPrompt,
1273
1391
  postCompactionPending: _postCompactionPending,
1274
1392
  blockingServerRequests: _blockingServerRequests,
@@ -1319,6 +1437,9 @@ export function restoreBoundState(state) {
1319
1437
  _editedPlanContent = state.editedPlanContent || '';
1320
1438
  _showPostPlanActions = !!state.showPostPlanActions;
1321
1439
  _postPlanHeader = state.postPlanHeader || 'PLAN COMPLETE';
1440
+ _planTurnActive = !!state.planTurnActive;
1441
+ _planApprovalPending = !!state.planApprovalPending;
1442
+ _lastPlanTurnId = state.lastPlanTurnId || '';
1322
1443
  _showPostCompactionPrompt = !!state.showPostCompactionPrompt;
1323
1444
  _postCompactionPending = !!state.postCompactionPending;
1324
1445
  _blockingServerRequests = state.blockingServerRequests || new Map();
@@ -1368,6 +1489,9 @@ export function commitBoundState() {
1368
1489
  _boundTab.editedPlanContent = _editedPlanContent;
1369
1490
  _boundTab.showPostPlanActions = _showPostPlanActions;
1370
1491
  _boundTab.postPlanHeader = _postPlanHeader;
1492
+ _boundTab.planTurnActive = _planTurnActive;
1493
+ _boundTab.planApprovalPending = _planApprovalPending;
1494
+ _boundTab.lastPlanTurnId = _lastPlanTurnId;
1371
1495
  _boundTab.showPostCompactionPrompt = _showPostCompactionPrompt;
1372
1496
  _boundTab.postCompactionPending = _postCompactionPending;
1373
1497
  _boundTab.blockingServerRequests = _blockingServerRequests;
@@ -1417,6 +1541,9 @@ export function bindTabState(tab) {
1417
1541
  _editedPlanContent = tab?.editedPlanContent || '';
1418
1542
  _showPostPlanActions = !!tab?.showPostPlanActions;
1419
1543
  _postPlanHeader = tab?.postPlanHeader || 'PLAN COMPLETE';
1544
+ _planTurnActive = !!tab?.planTurnActive;
1545
+ _planApprovalPending = !!tab?.planApprovalPending;
1546
+ _lastPlanTurnId = tab?.lastPlanTurnId || '';
1420
1547
  _showPostCompactionPrompt = !!tab?.showPostCompactionPrompt;
1421
1548
  _postCompactionPending = !!tab?.postCompactionPending;
1422
1549
  _blockingServerRequests = tab?.blockingServerRequests || new Map();
@@ -1484,6 +1611,7 @@ function hasMeaningfulTrayState(tab) {
1484
1611
  const draft = normalizeSessionLabel(tab.draft || '');
1485
1612
  return !!(
1486
1613
  tab.threadId
1614
+ || hasCustomSessionLabel(tab)
1487
1615
  || tab.running
1488
1616
  || tab.startingThread
1489
1617
  || draft
@@ -1493,6 +1621,76 @@ function hasMeaningfulTrayState(tab) {
1493
1621
  );
1494
1622
  }
1495
1623
 
1624
+ function hasCustomSessionLabel(tab) {
1625
+ const label = normalizeSessionLabel(tab?.pendingSessionLabel || tab?.sessionLabel || '');
1626
+ if (!label) return false;
1627
+ return !['new session', 'saved session', 'untitled session', 'codex'].includes(label.toLowerCase());
1628
+ }
1629
+
1630
+ function hasMeaningfulTabState(tab) {
1631
+ if (!tab) return false;
1632
+ return !!(
1633
+ hasMeaningfulTrayState(tab)
1634
+ || tab.planContent
1635
+ || tab.editedPlanContent
1636
+ || tab.showPostPlanActions
1637
+ || tab.planApprovalPending
1638
+ || tab.postCompactionPending
1639
+ || tab.items?.size
1640
+ || tab.activeItems?.size
1641
+ || tab.blockingServerRequests?.size
1642
+ );
1643
+ }
1644
+
1645
+ function isDiscardableBlankTab(tab) {
1646
+ return !!tab && !hasMeaningfulTabState(tab);
1647
+ }
1648
+
1649
+ function disposeDiscardedTab(tab) {
1650
+ if (!tab) return;
1651
+ flushThreadSnapshotSave(tab);
1652
+ disconnectTab(tab);
1653
+ tab.closed = true;
1654
+ if (tab.snapshotTimer) {
1655
+ clearTimeout(tab.snapshotTimer);
1656
+ tab.snapshotTimer = null;
1657
+ }
1658
+ tab.messagesEl?.remove();
1659
+ tab.pillEl?.remove();
1660
+ }
1661
+
1662
+ function pruneDiscardableTabs({ keepTab = activeTab(), persist = false } = {}) {
1663
+ // Blank tabs are provisional composer state; inactive blanks have no tray affordance.
1664
+ let pruned = false;
1665
+ for (let idx = _tabs.length - 1; idx >= 0; idx -= 1) {
1666
+ const tab = _tabs[idx];
1667
+ if (tab === keepTab || !isDiscardableBlankTab(tab)) continue;
1668
+ disposeDiscardedTab(tab);
1669
+ _tabs.splice(idx, 1);
1670
+ if (_activeTabIdx > idx) _activeTabIdx -= 1;
1671
+ else if (_activeTabIdx >= _tabs.length) _activeTabIdx = _tabs.length - 1;
1672
+ pruned = true;
1673
+ }
1674
+ if (_activeTabIdx < 0 && _tabs.length) _activeTabIdx = 0;
1675
+ if (pruned) {
1676
+ renderPills();
1677
+ if (persist) saveTabs();
1678
+ }
1679
+ return pruned;
1680
+ }
1681
+
1682
+ function isBlankTabRequest(saved = {}) {
1683
+ return !saved.threadId
1684
+ && !normalizeSessionLabel(saved.title || '')
1685
+ && !normalizeSessionLabel(saved.draft || '')
1686
+ && !saved.planContent
1687
+ && !saved.editedPlanContent
1688
+ && !saved.showPostPlanActions
1689
+ && !saved.planApprovalPending
1690
+ && !saved.showPostCompactionPrompt
1691
+ && !saved.postCompactionPending;
1692
+ }
1693
+
1496
1694
  function createTrayPill(tab) {
1497
1695
  const tray = document.getElementById('term-minimized-tray');
1498
1696
  if (!tray) return null;
@@ -1588,7 +1786,10 @@ export function switchTab(idx) {
1588
1786
  }
1589
1787
 
1590
1788
  export function createTab(saved = {}, { autoSwitch = true } = {}) {
1591
- if (_tabs.length >= MAX_TABS) return null;
1789
+ if (autoSwitch && isBlankTabRequest(saved) && isDiscardableBlankTab(activeTab())) {
1790
+ return activeTab();
1791
+ }
1792
+ pruneDiscardableTabs({ keepTab: activeTab(), persist: false });
1592
1793
  const inheritedProject = saved.project ?? activeTab()?.project ?? (storage.getItem(STOR.project) || '');
1593
1794
  const threadId = saved.threadId || null;
1594
1795
  const title = normalizeSessionLabel(saved.title || '');
@@ -1653,6 +1854,9 @@ export function createTab(saved = {}, { autoSwitch = true } = {}) {
1653
1854
  editedPlanContent: saved.editedPlanContent || '',
1654
1855
  showPostPlanActions: !!saved.showPostPlanActions,
1655
1856
  postPlanHeader: saved.postPlanHeader || 'PLAN COMPLETE',
1857
+ planTurnActive: !!saved.planTurnActive,
1858
+ planApprovalPending: !!saved.planApprovalPending,
1859
+ lastPlanTurnId: saved.lastPlanTurnId || '',
1656
1860
  pendingSessionLabel: saved.pendingSessionLabel || null, // user-named before thread exists; sent via thread_rename once ready
1657
1861
  showPostCompactionPrompt: !!saved.showPostCompactionPrompt,
1658
1862
  postCompactionPending: !!saved.postCompactionPending,
@@ -1736,6 +1940,7 @@ export function closeTab(idx) {
1736
1940
  export function saveTabs() {
1737
1941
  try {
1738
1942
  commitBoundState();
1943
+ pruneDiscardableTabs({ keepTab: activeTab(), persist: false });
1739
1944
  const payload = JSON.stringify({
1740
1945
  tabs: _tabs.map((tab) => {
1741
1946
  const persistedTokenUsage = normalizeThreadTokenUsage(tab.threadTokenUsage);
@@ -1760,6 +1965,9 @@ export function saveTabs() {
1760
1965
  editedPlanContent: tab.editedPlanContent || '',
1761
1966
  showPostPlanActions: !!tab.showPostPlanActions,
1762
1967
  postPlanHeader: tab.postPlanHeader || 'PLAN COMPLETE',
1968
+ planTurnActive: !!tab.planTurnActive,
1969
+ planApprovalPending: !!tab.planApprovalPending,
1970
+ lastPlanTurnId: tab.lastPlanTurnId || '',
1763
1971
  showPostCompactionPrompt: !!tab.showPostCompactionPrompt,
1764
1972
  postCompactionPending: !!tab.postCompactionPending,
1765
1973
  };
@@ -1779,6 +1987,8 @@ export function restoreTabs() {
1779
1987
  if (Array.isArray(data?.tabs) && data.tabs.length) {
1780
1988
  for (const saved of data.tabs) createTab(saved, { autoSwitch: false });
1781
1989
  _activeTabIdx = Math.min(Math.max(Number(data.activeIdx) || 0, 0), _tabs.length - 1);
1990
+ pruneDiscardableTabs({ keepTab: activeTab(), persist: false });
1991
+ if (!_tabs.length) throw new Error('No restorable Codex tabs');
1782
1992
  updateActiveTabView();
1783
1993
  saveTabs();
1784
1994
  return;
@@ -3062,14 +3272,16 @@ export function syncInputEnabled() {
3062
3272
  const blockedByRequest = hasBlockingServerRequests();
3063
3273
  if (input) input.disabled = !_connected || !_bootstrapped || _startingThread || blockedByRequest;
3064
3274
  if (send) {
3065
- // Stop button shows when any tab in the panel is running, so the user can
3066
- // abort the whole panel regardless of which tab is focused.
3067
- const isStopMode = _running || _tabs.some((t) => t?.running);
3275
+ const isStopMode = !!(tab?.running || _running);
3068
3276
  send.classList.toggle('cxp-send-stopping', !!isStopMode);
3069
3277
  send.title = isStopMode ? 'Stop' : 'Send';
3070
3278
  send.disabled = isStopMode ? false : (!_connected || !_bootstrapped || _running || _startingThread || blockedByRequest || (!hasText && !hasImages && !hasPaths));
3071
3279
  }
3072
- if (fresh) fresh.disabled = _tabs.length >= MAX_TABS;
3280
+ if (fresh) {
3281
+ pruneDiscardableTabs({ keepTab: tab, persist: true });
3282
+ fresh.disabled = false;
3283
+ fresh.title = 'New Codex tab';
3284
+ }
3073
3285
  if (close) close.disabled = false;
3074
3286
  if (compact) {
3075
3287
  compact.disabled = !_connected || !_bootstrapped || !_threadId || _running || _startingThread || _compacting;
@@ -3108,7 +3320,9 @@ export function dispatchPrompt(prompt, { tab = activeTab(), images = null, paths
3108
3320
  finalPrompt += `${finalPrompt ? '\n\n' : ''}Referenced files:\n${pathList.map((path) => `- ${path}`).join('\n')}`;
3109
3321
  }
3110
3322
 
3323
+ const explicitPlanPrompt = isPlanModePromptText(finalPrompt);
3111
3324
  const usePlanModePrefix = forcePlanModePrefix == null ? !!tab?.planMode : !!forcePlanModePrefix;
3325
+ const sentAsPlanMode = !!usePlanModePrefix || explicitPlanPrompt;
3112
3326
  if (usePlanModePrefix) {
3113
3327
  finalPrompt = `${CODEX_PLAN_MODE_PREFIXES[0]}\n\n${finalPrompt}`;
3114
3328
  }
@@ -3117,6 +3331,8 @@ export function dispatchPrompt(prompt, { tab = activeTab(), images = null, paths
3117
3331
  type: 'query',
3118
3332
  requestId: crypto.randomUUID(),
3119
3333
  prompt: finalPrompt,
3334
+ planMode: sentAsPlanMode,
3335
+ autoAccept: !!tab?.autoAccept,
3120
3336
  threadId: _threadId || null,
3121
3337
  fresh: _freshThread || !_threadId,
3122
3338
  cwd: _project || null,
@@ -3133,6 +3349,16 @@ export function dispatchPrompt(prompt, { tab = activeTab(), images = null, paths
3133
3349
  setStatus('Codex is not connected', 'error');
3134
3350
  return false;
3135
3351
  }
3352
+ if (tab) {
3353
+ tab.planTurnActive = sentAsPlanMode;
3354
+ tab.planApprovalPending = false;
3355
+ tab.lastPlanTurnId = msg.requestId;
3356
+ if (isActiveTab(tab)) {
3357
+ _planTurnActive = sentAsPlanMode;
3358
+ _planApprovalPending = false;
3359
+ _lastPlanTurnId = msg.requestId;
3360
+ }
3361
+ }
3136
3362
  clearPostCompactionState(tab, { updateTranscript: !_freshThread });
3137
3363
  if (_freshThread) clearTranscript('Starting fresh Codex thread…');
3138
3364
  _freshThread = false;
@@ -3194,29 +3420,47 @@ export function handleNotify(method, params) {
3194
3420
  break;
3195
3421
  case 'turn/completed':
3196
3422
  if (_compacting) setCompactingUI(false);
3197
- setRunning(false);
3198
- if (params.turn?.error?.message) {
3199
- appendSystem(params.turn.error.message, 'error');
3200
- setStatus(params.turn.error.message, 'error');
3201
- notifyCodexTurnOutcome(NOTIF_TYPE.ERROR, _boundTab, params.turn?.id || _activeTurnId);
3202
- } else if (_boundTab?.planMode) {
3203
- const capturedPlan = capturePlanContent(_boundTab);
3204
- if (capturedPlan) {
3205
- _boundTab.showPostPlanActions = true;
3206
- _boundTab.postPlanHeader = 'PLAN COMPLETE';
3207
- _showPostPlanActions = true;
3208
- _postPlanHeader = 'PLAN COMPLETE';
3209
- renderPostPlanActions(_boundTab);
3210
- saveTabs();
3423
+ {
3424
+ const tab = _boundTab;
3425
+ const wasPlanTurn = !!(tab?.planTurnActive || _planTurnActive || tab?.planMode);
3426
+ setRunning(false);
3427
+ if (params.turn?.error?.message) {
3428
+ setPlanTurnActive(_boundTab, false, '');
3429
+ setPlanApprovalPending(_boundTab, false);
3430
+ appendSystem(params.turn.error.message, 'error');
3431
+ setStatus(params.turn.error.message, 'error');
3432
+ notifyCodexTurnOutcome(NOTIF_TYPE.ERROR, _boundTab, params.turn?.id || _activeTurnId);
3433
+ } else if (wasPlanTurn && tab) {
3434
+ const capturedPlan = capturePlanContent(tab);
3435
+ if (capturedPlan) {
3436
+ tab.planMode = false;
3437
+ tab.showPostPlanActions = true;
3438
+ tab.postPlanHeader = 'PLAN COMPLETE';
3439
+ tab.planApprovalPending = true;
3440
+ tab.planTurnActive = false;
3441
+ _showPostPlanActions = true;
3442
+ _postPlanHeader = 'PLAN COMPLETE';
3443
+ _planApprovalPending = true;
3444
+ _planTurnActive = false;
3445
+ _lastPlanTurnId = '';
3446
+ tab.lastPlanTurnId = '';
3447
+ ensurePlanFile(tab);
3448
+ renderPostPlanActions(tab);
3449
+ syncToolbarState();
3450
+ saveTabs();
3451
+ } else {
3452
+ setPlanTurnActive(tab, false, '');
3453
+ setPlanApprovalPending(tab, false);
3454
+ }
3455
+ notifyCodexTurnOutcome(NOTIF_TYPE.DONE, tab, params.turn?.id || _activeTurnId);
3456
+ } else {
3457
+ notifyCodexTurnOutcome(NOTIF_TYPE.DONE, _boundTab, params.turn?.id || _activeTurnId);
3211
3458
  }
3212
- notifyCodexTurnOutcome(NOTIF_TYPE.DONE, _boundTab, params.turn?.id || _activeTurnId);
3213
- } else {
3214
- notifyCodexTurnOutcome(NOTIF_TYPE.DONE, _boundTab, params.turn?.id || _activeTurnId);
3215
- }
3216
- if (_boundTab?.postCompactionPending && !_boundTab?.showPostPlanActions) {
3217
- flushPostCompactionPrompt(_boundTab);
3459
+ if (_boundTab?.postCompactionPending && !_boundTab?.showPostPlanActions) {
3460
+ flushPostCompactionPrompt(_boundTab);
3461
+ }
3462
+ if (!shouldHoldForPlanApproval(_boundTab)) advanceQueue();
3218
3463
  }
3219
- advanceQueue();
3220
3464
  break;
3221
3465
  case 'thread/compacted':
3222
3466
  setCompactingUI(false);
@@ -3266,25 +3510,30 @@ export function handleNotify(method, params) {
3266
3510
  case 'item/autoApprovalReview/completed':
3267
3511
  setGuardianReviewState(params.targetItemId, params.review);
3268
3512
  break;
3269
- case 'item/completed':
3513
+ case 'item/completed': {
3514
+ const completedItem = params.item?.type === 'reasoning' && params.item.status == null
3515
+ ? { ...params.item, status: 'complete' }
3516
+ : params.item;
3270
3517
  // Inject pending images into userMessage if not consumed at item/started
3271
- if (params.item?.type === 'userMessage' && _boundTab?._pendingSendImages?.length) {
3518
+ if (completedItem?.type === 'userMessage' && _boundTab?._pendingSendImages?.length) {
3272
3519
  const imgItems = _boundTab._pendingSendImages.map((img) => ({
3273
3520
  type: 'localImage',
3274
3521
  name: img.name || 'image',
3275
3522
  dataUrl: img.dataUrl,
3276
3523
  }));
3277
- params.item.content = [...imgItems, ...(params.item.content || [])];
3524
+ completedItem.content = [...imgItems, ...(completedItem.content || [])];
3278
3525
  _boundTab._pendingSendImages = null;
3279
3526
  }
3280
- updateItemFromData(params.item);
3281
- trackActiveItemCompletion(params.item);
3282
- if (params.item?.type === 'contextCompaction') {
3527
+ updateItemFromData(completedItem);
3528
+ trackActiveItemCompletion(completedItem);
3529
+ captureCompletedPlanItem(_boundTab, completedItem);
3530
+ if (completedItem?.type === 'contextCompaction') {
3283
3531
  markPostCompactionPrompt(_boundTab, { defer: _running });
3284
3532
  }
3285
3533
  // Re-show thinking — Codex is processing between items
3286
3534
  if (_running) showThinking();
3287
3535
  break;
3536
+ }
3288
3537
  case 'item/agentMessage/delta':
3289
3538
  appendAgentDelta(params.itemId, params.delta);
3290
3539
  repositionThinking();
@@ -3294,14 +3543,19 @@ export function handleNotify(method, params) {
3294
3543
  repositionThinking();
3295
3544
  break;
3296
3545
  case 'item/reasoning/textDelta':
3546
+ hideThinking();
3297
3547
  appendReasoningDelta(params.itemId, params.delta, false);
3298
- repositionThinking();
3548
+ syncWorkStatus();
3299
3549
  break;
3300
3550
  case 'item/reasoning/summaryTextDelta':
3551
+ hideThinking();
3301
3552
  appendReasoningDelta(params.itemId, params.delta, true, params.summaryIndex);
3553
+ syncWorkStatus();
3302
3554
  break;
3303
3555
  case 'item/reasoning/summaryPartAdded':
3556
+ hideThinking();
3304
3557
  appendReasoningSummaryPart(params.itemId, params.summaryIndex);
3558
+ syncWorkStatus();
3305
3559
  break;
3306
3560
  case 'item/commandExecution/outputDelta':
3307
3561
  appendOutputDelta(params.itemId, params.delta, 'commandExecution');
@@ -3451,7 +3705,7 @@ export function syncToolbarState() {
3451
3705
  effortBtn.title = `Effort level: ${effort}`;
3452
3706
  effortBtn.classList.toggle('active', effort !== 'off');
3453
3707
  const dots = effortBtn.querySelectorAll('.cxp-effort-dots i');
3454
- const litCount = effort === 'off' ? 0 : effort === 'low' ? 1 : effort === 'medium' ? 2 : 3;
3708
+ const litCount = Math.max(0, EFFORT_LEVELS.indexOf(effort));
3455
3709
  dots.forEach((d, i) => d.classList.toggle('lit', i < litCount));
3456
3710
  }
3457
3711
  if (planBtn) {
@@ -3472,24 +3726,42 @@ export function syncToolbarState() {
3472
3726
  }
3473
3727
  }
3474
3728
 
3475
- const CODEX_DEFAULT_MODELS = ['gpt-5.4', 'gpt-5.4-mini', 'gpt-5.3-codex'];
3729
+ const CODEX_DEFAULT_MODELS = ['gpt-5.5', 'gpt-5.4', 'gpt-5.4-mini', 'gpt-5.3-codex'];
3476
3730
  let _modelListCache = null;
3477
3731
  let _modelListRequest = null;
3478
3732
 
3733
+ function getCodexModelName(model) {
3734
+ const name = typeof model === 'string' ? model : (model?.id || model?.model || model?.name || '');
3735
+ return String(name || '').trim();
3736
+ }
3737
+
3738
+ function mergeCodexModelList(models) {
3739
+ const merged = [];
3740
+ const seen = new Set();
3741
+ for (const model of [...CODEX_DEFAULT_MODELS, ...(Array.isArray(models) ? models : [])]) {
3742
+ const name = getCodexModelName(model);
3743
+ const key = name.toLowerCase();
3744
+ if (!name || seen.has(key)) continue;
3745
+ seen.add(key);
3746
+ merged.push(name);
3747
+ }
3748
+ return merged;
3749
+ }
3750
+
3479
3751
  export function requestModelList() {
3480
3752
  if (_modelListCache) return Promise.resolve(_modelListCache);
3481
3753
  if (_modelListRequest) return _modelListRequest;
3482
3754
  _modelListRequest = new Promise((resolve) => {
3483
3755
  const requestId = crypto.randomUUID();
3484
3756
  if (!sendSocket({ type: 'model_list', requestId })) {
3485
- _modelListCache = CODEX_DEFAULT_MODELS;
3757
+ _modelListCache = mergeCodexModelList([]);
3486
3758
  populateModelDropdown(_modelListCache);
3487
3759
  resolve(_modelListCache);
3488
3760
  _modelListRequest = null;
3489
3761
  return;
3490
3762
  }
3491
3763
  window.__cxpModelListResolve = (models) => {
3492
- _modelListCache = models?.length ? models : CODEX_DEFAULT_MODELS;
3764
+ _modelListCache = mergeCodexModelList(models);
3493
3765
  populateModelDropdown(_modelListCache);
3494
3766
  resolve(_modelListCache);
3495
3767
  _modelListRequest = null;
@@ -3511,7 +3783,7 @@ export function populateModelDropdown(models) {
3511
3783
  if (!menu) return;
3512
3784
  menu.innerHTML = '';
3513
3785
  for (const m of models) {
3514
- const name = typeof m === 'string' ? m : (m?.id || m?.model || m?.name || '');
3786
+ const name = getCodexModelName(m);
3515
3787
  if (!name) continue;
3516
3788
  const item = document.createElement('div');
3517
3789
  item.className = 'cxp-dd-item';
@@ -3576,22 +3848,35 @@ export function stopStallTimer() {
3576
3848
  _stallTimer = null;
3577
3849
  }
3578
3850
 
3579
- export function interruptTurn() {
3580
- if (!_running) return;
3851
+ export function interruptTab(tab = activeTab()) {
3852
+ if (!tab?.running) return false;
3581
3853
 
3582
- // Reset UI immediately — don't wait for server round-trip
3583
- setRunning(false);
3584
- setStatus('Stopped', 'info');
3585
- appendSystem('Stopped.', 'muted');
3854
+ return withTab(tab, () => {
3855
+ const threadId = _threadId || tab.threadId || null;
3856
+
3857
+ // Reset UI immediately; the server interrupt/kill is best-effort cleanup.
3858
+ setRunning(false);
3859
+ setStatus('Stopped', 'info');
3860
+ appendSystem('Stopped.', 'muted');
3861
+
3862
+ clearInterruptTimer();
3863
+ if (tab._abortRetry) {
3864
+ clearTimeout(tab._abortRetry);
3865
+ tab._abortRetry = null;
3866
+ }
3867
+ sendSocket({ type: 'interrupt', sessionId: tab.id, threadId });
3868
+ tab._abortRetry = setTimeout(() => {
3869
+ tab._abortRetry = null;
3870
+ const ws = tab.ws;
3871
+ if (ws?.readyState !== 1) return;
3872
+ try { ws.send(JSON.stringify({ type: 'force_kill', sessionId: tab.id, threadId })); } catch {}
3873
+ }, 2500);
3874
+ return true;
3875
+ });
3876
+ }
3586
3877
 
3587
- clearInterruptTimer();
3588
- sendSocket({ type: 'interrupt', threadId: _threadId || null });
3589
- _interruptTimer = setTimeout(() => {
3590
- _interruptTimer = null;
3591
- if (_interruptRetried) return;
3592
- _interruptRetried = true;
3593
- sendSocket({ type: 'force_kill', threadId: _threadId || null });
3594
- }, 2500);
3878
+ export function interruptTurn() {
3879
+ return interruptTab(activeTab());
3595
3880
  }
3596
3881
 
3597
3882
  export function interruptAllTabs() {
@@ -3602,12 +3887,12 @@ export function interruptAllTabs() {
3602
3887
  const ws = tab.ws;
3603
3888
  const threadId = tab.threadId || null;
3604
3889
  if (ws && ws.readyState === 1) {
3605
- try { ws.send(JSON.stringify({ type: 'interrupt', threadId })); } catch {}
3890
+ try { ws.send(JSON.stringify({ type: 'interrupt', sessionId: tab.id, threadId })); } catch {}
3606
3891
  if (tab._abortRetry) clearTimeout(tab._abortRetry);
3607
3892
  tab._abortRetry = setTimeout(() => {
3608
3893
  tab._abortRetry = null;
3609
3894
  try {
3610
- if (ws.readyState === 1) ws.send(JSON.stringify({ type: 'force_kill', threadId }));
3895
+ if (ws.readyState === 1) ws.send(JSON.stringify({ type: 'force_kill', sessionId: tab.id, threadId }));
3611
3896
  } catch {}
3612
3897
  }, 2000);
3613
3898
  }
@@ -3778,6 +4063,8 @@ function formatStatusSnapshotMarkdown(snapshot, tab = activeTab()) {
3778
4063
  `- Thread: \`${runtime.activeThreadId || tab?.threadId || 'none'}\`${runtime.activeTurnId ? ` (active turn \`${runtime.activeTurnId}\`)` : ''}`,
3779
4064
  `- Model: \`${tab?.model || effective.model || 'default'}\``,
3780
4065
  `- Reasoning effort: \`${tab?.effort && tab.effort !== 'off' ? tab.effort : (effective.model_reasoning_effort || 'default')}\``,
4066
+ `- Reasoning summary: \`${effective.model_reasoning_summary || 'default'}\``,
4067
+ `- Response verbosity: \`${effective.model_verbosity || 'default'}\``,
3781
4068
  `- Personality: \`${effective.personality || 'pragmatic'}\``,
3782
4069
  `- Fast mode: \`${effective.service_tier || 'default'}\``,
3783
4070
  `- Approval policy: \`${effective.approval_policy || 'never'}\``,
@@ -3911,11 +4198,18 @@ function setModelForTab(tab, model) {
3911
4198
  }
3912
4199
 
3913
4200
  function setEffortForTab(tab, effort) {
3914
- if (!tab || !effort) return;
3915
- tab.effort = effort;
3916
- storage.setItem(STOR.effort, effort);
4201
+ if (!tab) return false;
4202
+ const normalized = String(effort || '').trim().toLowerCase();
4203
+ const nextEffort = normalized === 'default' || normalized === 'none' ? 'off' : normalized;
4204
+ if (!EFFORT_LEVELS.includes(nextEffort)) {
4205
+ appendSystem(`Unknown reasoning effort: ${effort}. Use off, minimal, low, medium, high, or xhigh.`, 'error');
4206
+ return false;
4207
+ }
4208
+ tab.effort = nextEffort;
4209
+ storage.setItem(STOR.effort, nextEffort);
3917
4210
  syncToolbarState();
3918
4211
  saveTabs();
4212
+ return true;
3919
4213
  }
3920
4214
 
3921
4215
  async function handleLocalSlashCommand(payload = currentComposerPayload()) {
@@ -4093,8 +4387,7 @@ async function handleLocalSlashCommand(payload = currentComposerPayload()) {
4093
4387
  cycleEffort();
4094
4388
  break;
4095
4389
  }
4096
- setEffortForTab(tab, args);
4097
- appendMarkdown(`Reasoning effort set to \`${args}\`.`);
4390
+ if (setEffortForTab(tab, args)) appendMarkdown(`Reasoning effort set to \`${tab.effort || 'off'}\`.`);
4098
4391
  break;
4099
4392
  case 'mcp':
4100
4393
  consumeComposer(input, tab, { clearAttachments: false });
@@ -4242,6 +4535,10 @@ export function sendPrompt() {
4242
4535
  consumeComposer(input, tab);
4243
4536
  return;
4244
4537
  }
4538
+ if (shouldHoldForPlanApproval(tab)) {
4539
+ appendSystem('Choose Continue with implementation, Compact context, or Edit plan before sending another message.', 'working');
4540
+ return;
4541
+ }
4245
4542
  if (tab?.planMode) {
4246
4543
  preparePlanModeTurn(tab);
4247
4544
  }
@@ -4382,7 +4679,7 @@ export function clearQueue() {
4382
4679
 
4383
4680
  export function advanceQueue() {
4384
4681
  const tab = activeTab();
4385
- if (!tab || _running || tab.queuePaused || !tab.queue.length || hasBlockingServerRequests()) return;
4682
+ if (!tab || _running || tab.queuePaused || !tab.queue.length || hasBlockingServerRequests() || shouldHoldForPlanApproval(tab)) return;
4386
4683
  const next = tab.queue.shift();
4387
4684
  if (!next) return;
4388
4685
  if (tab.planMode) {
@@ -4903,7 +5200,7 @@ export async function openSettingsPanel() {
4903
5200
  // Get dynamic model list (fall back to hardcoded defaults)
4904
5201
  let models;
4905
5202
  try { models = await requestModelList(); } catch { models = null; }
4906
- const modelList = models?.length ? models : CODEX_DEFAULT_MODELS;
5203
+ const modelList = mergeCodexModelList(models);
4907
5204
 
4908
5205
  configEl.innerHTML = '';
4909
5206
  const fieldDefs = [
@@ -4938,6 +5235,19 @@ export async function openSettingsPanel() {
4938
5235
  { value: 'high', label: 'High' },
4939
5236
  { value: 'xhigh', label: 'Extra High' },
4940
5237
  ]},
5238
+ { key: 'model_reasoning_summary', label: 'Reasoning Summary', type: 'select', options: [
5239
+ { value: '', label: '(default)' },
5240
+ { value: 'auto', label: 'Auto' },
5241
+ { value: 'concise', label: 'Concise' },
5242
+ { value: 'detailed', label: 'Detailed' },
5243
+ { value: 'none', label: 'None' },
5244
+ ]},
5245
+ { key: 'model_verbosity', label: 'Response Verbosity', type: 'select', options: [
5246
+ { value: '', label: '(default)' },
5247
+ { value: 'low', label: 'Low' },
5248
+ { value: 'medium', label: 'Medium' },
5249
+ { value: 'high', label: 'High' },
5250
+ ]},
4941
5251
  { key: 'model_context_window', label: 'Context Window', type: 'number', placeholder: 'tokens' },
4942
5252
  { key: 'model_auto_compact_token_limit', label: 'Auto-Compact', type: 'number', placeholder: 'token limit' },
4943
5253
  { key: 'web_search', label: 'Web Search', type: 'checkbox' },
@@ -5063,6 +5373,7 @@ export function initContextBridge() {
5063
5373
  isActiveTab,
5064
5374
  dispatchPrompt,
5065
5375
  capturePlanContent,
5376
+ ensurePlanFile,
5066
5377
  saveTabs,
5067
5378
  hideEmpty,
5068
5379
  showEmpty,
@@ -5101,6 +5412,9 @@ export function initContextBridge() {
5101
5412
  onBlockingServerRequestAnswered: (requestId) => clearBlockingServerRequest(requestId),
5102
5413
  setPostPlanHeader: (v) => { _postPlanHeader = v; },
5103
5414
  setShowPostPlanActions: (v) => { _showPostPlanActions = v; },
5415
+ setPlanTurnActive: (v) => { _planTurnActive = !!v; },
5416
+ setPlanApprovalPending: (v) => { _planApprovalPending = !!v; },
5417
+ setLastPlanTurnId: (v) => { _lastPlanTurnId = v || ''; },
5104
5418
  setShowPostCompactionPrompt: (v) => { _showPostCompactionPrompt = !!v; },
5105
5419
  setPostCompactionPending: (v) => { _postCompactionPending = !!v; },
5106
5420
  setPlanFilePath: (v) => { _planFilePath = v; },