feat: type hints green [bracketed] notation, workspace cleanup, unique naming

- Type inference hints now show value-first with bracketed type in comment
  green: "0x7ff718570000 [ptr64]", "6, 16 [int32_t×2]"
- Raise hint threshold to strong-only (score >= 75%)
- Remove Bool inference, widen Int16 range to ±16384
- Workspace: remove dead WorkspaceProxy, fix null deref, debounce search,
  cache icons, add pinning support
- Unique naming: UnnamedClass0/UnnamedEnum1 with global counter
- Footer buttons: +10h +100h +1000h replacing +1024
- MCP: project lifecycle API, snapshot provider fix
This commit is contained in:
IChooseYou
2026-03-09 10:39:22 -06:00
parent a21e5a07a8
commit 483f87cfbd
20 changed files with 310 additions and 2069 deletions

View File

@@ -261,14 +261,17 @@ void composeLeaf(ComposeState& state, const NodeTree& tree,
? prov.readBytes(absAddr, sz) : QByteArray(sz, '\0');
auto suggestions = inferTypes(
reinterpret_cast<const uint8_t*>(b.constData()), sz);
if (!suggestions.isEmpty() && suggestions[0].strength >= 2) {
if (!suggestions.isEmpty() && suggestions[0].strength >= 3) {
lm.typeHintStart = lineText.size() + 2; // after " " gap
lm.typeHintKinds = suggestions[0].kinds;
lm.typeHint = formatHint(suggestions[0]);
QString typeName = formatHint(suggestions[0]);
QString preview = formatPreview(
reinterpret_cast<const uint8_t*>(b.constData()), sz, suggestions[0]);
// Value-first with bracketed type: "0x7ff718570000 [ptr64]"
if (!preview.isEmpty())
lm.typeHint += QStringLiteral(" ") + preview;
lm.typeHint = preview + QStringLiteral(" [") + typeName + QStringLiteral("]");
else
lm.typeHint = QStringLiteral("[") + typeName + QStringLiteral("]");
lineText += QStringLiteral(" ") + lm.typeHint;
}
}

View File

@@ -190,9 +190,10 @@ RcxEditor* RcxController::addSplitEditor(QWidget* parent) {
// Eagerly pre-warm the type popup so first click isn't slow (~350ms cold start).
if (!m_cachedPopup) {
QTimer::singleShot(0, this, [this, editor]() {
if (!m_cachedPopup && !m_editors.isEmpty())
ensurePopup(editor);
QPointer<RcxEditor> safeEditor = editor;
QTimer::singleShot(0, this, [this, safeEditor]() {
if (!m_cachedPopup && !m_editors.isEmpty() && safeEditor)
ensurePopup(safeEditor);
});
}
return editor;
@@ -200,7 +201,7 @@ RcxEditor* RcxController::addSplitEditor(QWidget* parent) {
void RcxController::removeSplitEditor(RcxEditor* editor) {
m_editors.removeOne(editor);
// Caller (MainWindow) owns the parent QTabWidget and handles widget destruction.
editor->disconnect(this);
}
void RcxController::connectEditor(RcxEditor* editor) {

View File

@@ -570,13 +570,13 @@ static constexpr int kCommandRowLine = 0;
static constexpr int kFirstDataLine = 1;
static constexpr uint64_t kFooterIdBit = 0x8000000000000000ULL;
static constexpr uint64_t kArrayElemBit = 0x4000000000000000ULL; // marks array element selection
static constexpr uint64_t kArrayElemShift = 48; // bits 48-61 hold element index
static constexpr uint64_t kArrayElemMask = 0x3FFF000000000000ULL; // 14 bits → max 16383 elements
static constexpr uint64_t kArrayElemShift = 42; // bits 42-61 hold element index
static constexpr uint64_t kArrayElemMask = 0x3FFFFC0000000000ULL; // 20 bits → max 1048575 elements
// Encode an array element selection ID: nodeId | kArrayElemBit | (elemIdx << 48)
// Encode an array element selection ID: nodeId | kArrayElemBit | (elemIdx << 42)
inline uint64_t makeArrayElemSelId(uint64_t nodeId, int elemIdx) {
Q_ASSERT(elemIdx >= 0);
return nodeId | kArrayElemBit | ((uint64_t)(elemIdx & 0x3FFF) << kArrayElemShift);
return nodeId | kArrayElemBit | ((uint64_t)(elemIdx & 0xFFFFF) << kArrayElemShift);
}
inline int arrayElemIdxFromSelId(uint64_t selId) {
return (int)((selId & kArrayElemMask) >> kArrayElemShift);
@@ -584,11 +584,11 @@ inline int arrayElemIdxFromSelId(uint64_t selId) {
// Member selection encoding (enum/bitfield members) — mirrors array element pattern
static constexpr uint64_t kMemberBit = 0x2000000000000000ULL;
static constexpr uint64_t kMemberSubShift = 48;
static constexpr uint64_t kMemberSubMask = 0x3FFF000000000000ULL;
static constexpr uint64_t kMemberSubShift = 42;
static constexpr uint64_t kMemberSubMask = 0x3FFFFC0000000000ULL;
inline uint64_t makeMemberSelId(uint64_t nodeId, int subLine) {
return nodeId | kMemberBit | ((uint64_t)(subLine & 0x3FFF) << kMemberSubShift);
return nodeId | kMemberBit | ((uint64_t)(subLine & 0xFFFFF) << kMemberSubShift);
}
inline int memberSubFromSelId(uint64_t selId) {
return (int)((selId & kMemberSubMask) >> kMemberSubShift);

View File

@@ -762,7 +762,7 @@ void RcxEditor::applyTheme(const Theme& theme) {
m_sci->SendScintilla(QsciScintillaBase::SCI_INDICSETFORE,
IND_LOCAL_OFF, theme.textFaint);
m_sci->SendScintilla(QsciScintillaBase::SCI_INDICSETFORE,
IND_TYPE_HINT, theme.textFaint);
IND_TYPE_HINT, theme.indHintGreen);
m_sci->SendScintilla(QsciScintillaBase::SCI_INDICSETFORE,
IND_FIND, theme.borderFocused);
@@ -905,7 +905,9 @@ void RcxEditor::applyDocument(const ComposeResult& result) {
const auto& lm = result.meta[i];
if (lm.heatLevel > 0 || isFuncPtr(lm.nodeKind) ||
lm.nodeKind == NodeKind::Pointer32 ||
lm.nodeKind == NodeKind::Pointer64)
lm.nodeKind == NodeKind::Pointer64 ||
lm.lineKind == LineKind::Footer ||
lm.typeHintStart >= 0)
lineTexts[i] = getLineText(m_sci, i);
}
applyHeatmapHighlight(result.meta, lineTexts);
@@ -915,7 +917,7 @@ void RcxEditor::applyDocument(const ComposeResult& result) {
// Footer buttons — pill styling
for (int i = 0; i < result.meta.size(); i++) {
if (result.meta[i].lineKind != LineKind::Footer) continue;
QString ft = getLineText(m_sci, i);
const QString& ft = lineTexts[i];
// Struct footer: +10h +100h +1000h Trim (search longest first)
int p1000 = ft.indexOf(QStringLiteral("+1000h"));
if (p1000 >= 0)
@@ -935,6 +937,15 @@ void RcxEditor::applyDocument(const ComposeResult& result) {
fillIndicatorCols(IND_CMD_PILL, i, trimStart, trimStart + 4);
}
// Apply type inference hint coloring (green, same as comment annotations)
for (int i = 0; i < result.meta.size(); i++) {
const auto& lm = result.meta[i];
if (lm.typeHintStart < 0) continue;
const QString& ft = lineTexts[i];
if (lm.typeHintStart < ft.size())
fillIndicatorCols(IND_TYPE_HINT, i, lm.typeHintStart, ft.size());
}
// Reset hint line - applySelectionOverlay will repaint indicators
m_hintLine = -1;
@@ -2562,6 +2573,8 @@ bool RcxEditor::beginInlineEdit(EditTarget target, int line, int col) {
ColumnSpan cs = commentSpanFor(*lm, 9999, lm->effectiveTypeW, lm->effectiveNameW);
m_editState.commentCol = cs.valid ? cs.start : -1;
m_editState.lastValidationOk = true; // original value is always valid
} else if (target == EditTarget::BaseAddress) {
m_editState.commentCol = norm.end + 2; // command row has no column layout
} else {
m_editState.commentCol = -1;
}
@@ -2575,7 +2588,8 @@ bool RcxEditor::beginInlineEdit(EditTarget target, int line, int col) {
// For value editing: extend line with trailing spaces for the edit comment area
// (comment padding is no longer baked into every line to avoid unnecessary scroll width)
if (target == EditTarget::Value && m_editState.commentCol >= 0) {
if ((target == EditTarget::Value || target == EditTarget::BaseAddress)
&& m_editState.commentCol >= 0) {
int commentStart = norm.end + 2;
int neededLen = commentStart + kColComment;
int currentLen = (int)lineText.size();
@@ -2624,6 +2638,8 @@ bool RcxEditor::beginInlineEdit(EditTarget target, int line, int col) {
// Show initial edit hint in comment column
if (target == EditTarget::Value)
setEditComment(QStringLiteral("Enter=Save Esc=Cancel"));
else if (target == EditTarget::BaseAddress)
setEditComment(QStringLiteral("e.g. <mod.exe> + 0xFF | [0x1000 + 0x10] | 7ff6`1234ABCD"));
// Note: Type, ArrayElementType, PointerTarget are handled by TypeSelectorPopup
// and exit early above (never reach here).

View File

@@ -661,8 +661,10 @@ QString validateValue(NodeKind kind, const QString& text) {
QString digits = hasHexPrefix ? s.mid(2) : s;
if (hasHexPrefix || isHexKind) {
// Hex mode: only 0-9, a-f, A-F
// Hex mode: only 0-9, a-f, A-F (spaces allowed for multi-byte hex kinds)
bool isMultiByteHex = (kind >= NodeKind::Hex16 && kind <= NodeKind::Hex64);
for (QChar c : digits) {
if (c == ' ' && isMultiByteHex) continue;
if (!c.isDigit() && !(c >= 'a' && c <= 'f') && !(c >= 'A' && c <= 'F'))
return QStringLiteral("invalid hex '%1'").arg(c);
}

View File

@@ -1188,6 +1188,16 @@ static int structTypeSize(const QString& typeName, const BuildContext& ctx) {
return 0;
}
// Compute total array elements from multi-dimensional sizes, capped to prevent overflow.
static int clampedArrayElements(const QVector<int>& dims, int maxElements = 1000000) {
int64_t total = 1;
for (int dim : dims) {
total *= (dim > 0 ? dim : 1);
if (total > maxElements) return maxElements;
}
return (int)total;
}
static void buildFields(BuildContext& ctx, uint64_t parentId, int baseOffset,
const QVector<ParsedField>& fields) {
int computedOffset = 0;
@@ -1276,8 +1286,7 @@ static void buildFields(BuildContext& ctx, uint64_t parentId, int baseOffset,
// Array of pointers: PVOID arr[N]
if (!field.arraySizes.isEmpty()) {
int totalElements = 1;
for (int dim : field.arraySizes) totalElements *= (dim > 0 ? dim : 1);
int totalElements = clampedArrayElements(field.arraySizes);
Node n;
n.kind = NodeKind::Array;
@@ -1315,8 +1324,7 @@ static void buildFields(BuildContext& ctx, uint64_t parentId, int baseOffset,
int elemSize = 4;
NodeKind elemKind = NodeKind::UInt32;
if (!field.arraySizes.isEmpty()) {
int totalElements = 1;
for (int dim : field.arraySizes) totalElements *= (dim > 0 ? dim : 1);
int totalElements = clampedArrayElements(field.arraySizes);
Node n;
n.kind = NodeKind::Array;
n.name = field.name;
@@ -1420,8 +1428,7 @@ static void buildFields(BuildContext& ctx, uint64_t parentId, int baseOffset,
ctx.tree.addNode(n); computedOffset = fieldOffset + 64; continue;
}
int totalElements = 1;
for (int dim : field.arraySizes) totalElements *= (dim > 0 ? dim : 1);
int totalElements = clampedArrayElements(field.arraySizes);
Node n;
n.kind = NodeKind::Array;
@@ -1440,8 +1447,7 @@ static void buildFields(BuildContext& ctx, uint64_t parentId, int baseOffset,
int elemSize = structTypeSize(field.typeName, ctx);
if (!field.arraySizes.isEmpty()) {
int totalElements = 1;
for (int dim : field.arraySizes) totalElements *= (dim > 0 ? dim : 1);
int totalElements = clampedArrayElements(field.arraySizes);
Node n;
n.kind = NodeKind::Array;

View File

@@ -604,6 +604,25 @@ MainWindow::MainWindow(QWidget* parent) : QMainWindow(parent) {
createWorkspaceDock();
createScannerDock();
// Hidden sentinel dock — never visible, only used to force Qt to create a
// QTabBar when the first document dock is added (Qt only creates tab bars
// via tabifyDockWidget). Immediately hidden after tabification so it takes
// zero layout space. An event filter on the QTabBar keeps it visible.
{
m_sentinelDock = new QDockWidget(this);
m_sentinelDock->setObjectName(QStringLiteral("_sentinel"));
m_sentinelDock->setFeatures(QDockWidget::NoDockWidgetFeatures);
auto* sw = new QWidget(m_sentinelDock);
sw->setFixedSize(0, 0);
m_sentinelDock->setWidget(sw);
auto* stb = new QWidget(m_sentinelDock);
stb->setFixedHeight(0);
m_sentinelDock->setTitleBarWidget(stb);
addDockWidget(Qt::TopDockWidgetArea, m_sentinelDock);
m_sentinelDock->hide(); // hidden = zero layout space
}
createMenus();
createStatusBar();
@@ -1644,6 +1663,16 @@ QDockWidget* MainWindow::createTab(RcxDocument* doc) {
else
addDockWidget(Qt::TopDockWidgetArea, dock);
// Bootstrap: tabify the hidden sentinel with the first doc dock so Qt
// creates a QTabBar. Then hide sentinel (zero layout space). The event
// filter in eventFilter() keeps the tab bar visible even at count==1.
if (m_sentinelDock && m_docDocks.isEmpty()) {
m_sentinelDock->show();
tabifyDockWidget(dock, m_sentinelDock);
m_sentinelDock->hide();
dock->raise();
}
m_docDocks.append(dock);
m_tabs[dock] = { doc, ctrl, splitter, {}, 0 };
m_activeDocDock = dock;
@@ -1698,7 +1727,7 @@ QDockWidget* MainWindow::createTab(RcxDocument* doc) {
m_activeDocDock = m_docDocks.isEmpty() ? nullptr : m_docDocks.last();
rebuildAllDocs();
rebuildWorkspaceModel();
if (m_tabs.isEmpty())
if (m_tabs.isEmpty() && !m_closingAll)
project_new();
});
@@ -1780,6 +1809,10 @@ QDockWidget* MainWindow::createTab(RcxDocument* doc) {
updateWindowTitle();
});
});
// Notify MCP clients of tree changes
connect(doc, &RcxDocument::documentChanged, this, [this]() {
if (m_mcp) m_mcp->notifyTreeChanged();
});
connect(&doc->undoStack, &QUndoStack::indexChanged,
this, [this, dockGuard](int) {
if (!dockGuard) return;
@@ -1875,6 +1908,9 @@ void MainWindow::setupDockTabBars() {
.arg(theme.background.name(), theme.border.name(), theme.hover.name()));
}
// Force tab bar visible (event filter keeps it alive, belt-and-suspenders)
tabBar->show();
// Install tab buttons for any tab that doesn't have them yet
for (int i = 0; i < tabBar->count(); ++i) {
auto* existing = qobject_cast<DockTabButtons*>(
@@ -2010,6 +2046,25 @@ void MainWindow::setupDockTabBars() {
}
bool MainWindow::eventFilter(QObject* obj, QEvent* event) {
// Keep dock tab bars visible even when Qt wants to hide them (count==1).
// Qt's QMainWindowLayout calls setVisible(false) on the QTabBar when only
// one dock remains in a tab group. We catch the resulting Hide event and
// immediately re-show the tab bar, provided at least one doc dock is docked.
if (event->type() == QEvent::Hide && !m_tabBarShowGuard) {
if (auto* tabBar = qobject_cast<QTabBar*>(obj)) {
if (tabBar->parent() == this && tabBar->count() >= 1) {
bool hasDockedDoc = false;
for (auto* d : m_docDocks)
if (!d->isFloating() && d->isVisible()) { hasDockedDoc = true; break; }
if (hasDockedDoc) {
m_tabBarShowGuard = true;
tabBar->show();
m_tabBarShowGuard = false;
return true;
}
}
}
}
if (event->type() == QEvent::MouseButtonPress) {
auto* me = static_cast<QMouseEvent*>(event);
if (me->button() == Qt::MiddleButton) {
@@ -3107,8 +3162,10 @@ void MainWindow::importReclassXml() {
auto* doc = new RcxDocument(this);
doc->tree = std::move(tree);
closeAllDocDocks();
createTab(doc);
{ ClosingGuard guard(m_closingAll);
closeAllDocDocks();
createTab(doc);
}
rebuildWorkspaceModel();
setAppStatus(QStringLiteral("Imported %1 classes from %2")
.arg(classCount).arg(QFileInfo(filePath).fileName()));
@@ -3156,8 +3213,10 @@ void MainWindow::importFromSource() {
auto* doc = new RcxDocument(this);
doc->tree = std::move(tree);
closeAllDocDocks();
createTab(doc);
{ ClosingGuard guard(m_closingAll);
closeAllDocDocks();
createTab(doc);
}
rebuildWorkspaceModel();
if (!m_docDocks.isEmpty()) {
splitDockWidget(m_workspaceDock, m_docDocks.first(), Qt::Horizontal);
@@ -3210,8 +3269,10 @@ void MainWindow::importPdb() {
auto* doc = new rcx::RcxDocument(this);
doc->tree = std::move(tree);
closeAllDocDocks();
createTab(doc);
{ ClosingGuard guard(m_closingAll);
closeAllDocDocks();
createTab(doc);
}
rebuildWorkspaceModel();
if (!m_docDocks.isEmpty()) {
splitDockWidget(m_workspaceDock, m_docDocks.first(), Qt::Horizontal);
@@ -3408,8 +3469,11 @@ QDockWidget* MainWindow::project_open(const QString& path) {
}
auto* doc = new RcxDocument(this);
doc->tree = std::move(tree);
closeAllDocDocks();
auto* dock = createTab(doc);
QDockWidget* dock;
{ ClosingGuard guard(m_closingAll);
closeAllDocDocks();
dock = createTab(doc);
}
rebuildWorkspaceModel();
if (!m_docDocks.isEmpty()) {
splitDockWidget(m_workspaceDock, m_docDocks.first(), Qt::Horizontal);
@@ -3433,9 +3497,11 @@ QDockWidget* MainWindow::project_open(const QString& path) {
}
// Close all existing tabs so the project replaces the current state
closeAllDocDocks();
auto* dock = createTab(doc);
QDockWidget* dock;
{ ClosingGuard guard(m_closingAll);
closeAllDocDocks();
dock = createTab(doc);
}
rebuildWorkspaceModel();
if (!m_docDocks.isEmpty()) {
splitDockWidget(m_workspaceDock, m_docDocks.first(), Qt::Horizontal);
@@ -3750,6 +3816,16 @@ void MainWindow::createWorkspaceDock() {
actConvert = menu.addAction("Convert to Class");
}
// Pin/Unpin
bool allPinned = true;
for (const auto& item : items)
if (!m_pinnedIds.contains(item.structId)) { allPinned = false; break; }
auto* actPin = menu.addAction(
QIcon(QStringLiteral(":/vsicons/pin.svg")),
allPinned ? QStringLiteral("Unpin") : QStringLiteral("Pin"));
menu.addSeparator();
// Delete: works for single or multi
QString delLabel = items.size() == 1
? QStringLiteral("Delete")
@@ -3941,6 +4017,17 @@ void MainWindow::createWorkspaceDock() {
tab.doc->undoStack.push(new rcx::RcxCommand(tab.ctrl,
rcx::cmd::ChangeClassKeyword{item.structId, item.keyword, newKw}));
rebuildWorkspaceModel();
} else if (chosen && chosen == actPin) {
for (const auto& item : items) {
if (allPinned)
m_pinnedIds.remove(item.structId);
else
m_pinnedIds.insert(item.structId);
}
// Full rebuild to reorder pinned items to top
m_workspaceModel->removeRows(0, m_workspaceModel->rowCount());
rebuildWorkspaceModelNow();
}
});
@@ -4244,9 +4331,9 @@ void MainWindow::rebuildWorkspaceModelNow() {
QString name = rootName(tab.doc->tree, tab.ctrl->viewRootId());
tabs.append({ &tab.doc->tree, name, static_cast<void*>(it.key()) });
}
rcx::syncProjectExplorer(m_workspaceModel, tabs);
rcx::syncProjectExplorer(m_workspaceModel, tabs, m_pinnedIds);
// Mark items that are currently viewed in a tab
// Mark items that are currently viewed in a tab + pinned state
QSet<uint64_t> viewedIds;
for (auto it = m_tabs.begin(); it != m_tabs.end(); ++it)
viewedIds.insert(it->ctrl->viewRootId());
@@ -4255,6 +4342,7 @@ void MainWindow::rebuildWorkspaceModelNow() {
if (!item) continue;
uint64_t id = item->data(Qt::UserRole + 1).toULongLong();
item->setData(viewedIds.contains(id), Qt::UserRole + 3);
item->setData(m_pinnedIds.contains(id), Qt::UserRole + 4);
}
if (m_dockTitleLabel) {

View File

@@ -120,7 +120,15 @@ private:
QMap<QDockWidget*, TabState> m_tabs;
QVector<QDockWidget*> m_docDocks; // ordered list for tabByIndex
QDockWidget* m_activeDocDock = nullptr; // tracks active document dock
QDockWidget* m_sentinelDock = nullptr; // hidden dock to bootstrap tab bar creation
QVector<RcxDocument*> m_allDocs; // all open docs, shared with controllers
bool m_closingAll = false; // guards spurious project_new during batch close
bool m_tabBarShowGuard = false; // prevents recursion in event filter re-show
struct ClosingGuard {
bool& flag;
ClosingGuard(bool& f) : flag(f) { flag = true; }
~ClosingGuard() { flag = false; }
};
void rebuildAllDocs();
void createMenus();
@@ -165,6 +173,7 @@ private:
QLabel* m_dockTitleLabel = nullptr;
QToolButton* m_dockCloseBtn = nullptr;
DockGripWidget* m_dockGrip = nullptr;
QSet<uint64_t> m_pinnedIds;
void createWorkspaceDock();
void rebuildWorkspaceModel(); // debounced — safe to call frequently
void rebuildWorkspaceModelNow(); // immediate rebuild

View File

@@ -10,13 +10,24 @@
namespace rcx {
static constexpr int kMaxReadBuffer = 10 * 1024 * 1024; // 10 MB
// ════════════════════════════════════════════════════════════════════
// Construction / lifecycle
// ════════════════════════════════════════════════════════════════════
McpBridge::McpBridge(MainWindow* mainWindow, QObject* parent)
: QObject(parent), m_mainWindow(mainWindow)
{}
{
m_notifyTimer = new QTimer(this);
m_notifyTimer->setSingleShot(true);
m_notifyTimer->setInterval(100);
connect(m_notifyTimer, &QTimer::timeout, this, [this]() {
if (m_client && m_initialized)
sendNotification("notifications/resources/updated",
QJsonObject{{"uri", "project://tree"}});
});
}
McpBridge::~McpBridge() {
stop();
@@ -84,15 +95,24 @@ void McpBridge::onNewConnection() {
void McpBridge::onReadyRead() {
m_readBuffer.append(m_client->readAll());
// Newline-delimited JSON framing
if (m_readBuffer.size() > kMaxReadBuffer) {
qWarning() << "[MCP] Read buffer exceeded 10MB, disconnecting client";
m_client->disconnectFromServer();
return;
}
// Newline-delimited JSON framing (cursor approach avoids quadratic shifting)
int consumed = 0;
while (true) {
int idx = m_readBuffer.indexOf('\n');
int idx = m_readBuffer.indexOf('\n', consumed);
if (idx < 0) break;
QByteArray line = m_readBuffer.left(idx).trimmed();
m_readBuffer.remove(0, idx + 1);
QByteArray line = m_readBuffer.mid(consumed, idx - consumed).trimmed();
consumed = idx + 1;
if (!line.isEmpty())
processLine(line);
}
if (consumed > 0)
m_readBuffer.remove(0, consumed);
}
void McpBridge::onDisconnected() {
@@ -153,6 +173,7 @@ QJsonObject McpBridge::makeTextResult(const QString& text, bool isError) {
// ════════════════════════════════════════════════════════════════════
void McpBridge::processLine(const QByteArray& line) {
try {
qDebug() << "[MCP] <<" << line.trimmed().left(200);
auto doc = QJsonDocument::fromJson(line);
if (!doc.isObject()) {
@@ -172,12 +193,10 @@ void McpBridge::processLine(const QByteArray& line) {
if (method == "initialize") {
m_mainWindow->setMcpStatus(QStringLiteral("MCP: client connected"));
QCoreApplication::processEvents();
sendJson(handleInitialize(id, req.value("params").toObject()));
m_mainWindow->clearMcpStatus();
} else if (method == "tools/list") {
m_mainWindow->setMcpStatus(QStringLiteral("MCP: tools/list"));
QCoreApplication::processEvents();
sendJson(handleToolsList(id));
m_mainWindow->clearMcpStatus();
} else if (method == "tools/call") {
@@ -185,6 +204,14 @@ void McpBridge::processLine(const QByteArray& line) {
} else {
sendJson(errReply(id, -32601, "Method not found: " + method));
}
} catch (const std::exception& e) {
qWarning() << "[MCP] Exception:" << e.what();
sendJson(errReply(QJsonValue(), -32603,
QStringLiteral("Internal error: %1").arg(e.what())));
} catch (...) {
qWarning() << "[MCP] Unknown exception";
sendJson(errReply(QJsonValue(), -32603, "Internal error"));
}
}
// ════════════════════════════════════════════════════════════════════
@@ -476,7 +503,7 @@ QJsonObject McpBridge::handleToolsCall(const QJsonValue& id, const QJsonObject&
// Show tool activity in status bar (with shimmer)
m_mainWindow->setMcpStatus(QStringLiteral("MCP: %1").arg(toolName));
QCoreApplication::processEvents(); // paint immediately
QCoreApplication::processEvents(QEventLoop::ExcludeUserInputEvents);
QJsonObject result;
if (toolName == "project.state") result = toolProjectState(args);
@@ -501,11 +528,15 @@ QJsonObject McpBridge::handleToolsCall(const QJsonValue& id, const QJsonObject&
// ════════════════════════════════════════════════════════════════════
QString McpBridge::resolvePlaceholder(const QString& ref,
const QHash<QString, uint64_t>& placeholderMap) {
const QHash<QString, uint64_t>& placeholderMap,
bool* ok) {
if (ok) *ok = true;
if (ref.startsWith('$')) {
auto it = placeholderMap.find(ref);
if (it != placeholderMap.end())
return QString::number(it.value());
if (ok) *ok = false;
return ref; // unresolved placeholder
}
return ref; // not a placeholder — return as-is
}
@@ -514,26 +545,36 @@ QString McpBridge::resolvePlaceholder(const QString& ref,
// Smart tab resolution
// ════════════════════════════════════════════════════════════════════
MainWindow::TabState* McpBridge::resolveTab(const QJsonObject& args) {
MainWindow::TabState* McpBridge::resolveTab(const QJsonObject& args, int* resolvedIndex) {
if (resolvedIndex) *resolvedIndex = -1;
// 1) Explicit tab index from args
if (args.contains("tabIndex")) {
int idx = args.value("tabIndex").toInt();
auto* t = m_mainWindow->tabByIndex(idx);
if (t) return t;
if (t) { if (resolvedIndex) *resolvedIndex = idx; return t; }
}
// 2) Active sub-window (user clicked on it)
auto* t = m_mainWindow->activeTab();
if (t) return t;
if (t) {
if (resolvedIndex) {
for (int i = 0; i < m_mainWindow->tabCount(); i++) {
if (m_mainWindow->tabByIndex(i) == t) { *resolvedIndex = i; break; }
}
}
return t;
}
// 3) Fall back to first available tab
if (m_mainWindow->tabCount() > 0) {
t = m_mainWindow->tabByIndex(0);
if (t) return t;
if (t) { if (resolvedIndex) *resolvedIndex = 0; return t; }
}
// 4) No tabs at all — auto-create a project
m_mainWindow->project_new();
if (resolvedIndex) *resolvedIndex = 0;
return m_mainWindow->tabByIndex(0);
}
@@ -725,8 +766,11 @@ QJsonObject McpBridge::toolTreeApply(const QJsonObject& args) {
QStringList skippedOps;
for (int i = 0; i < ops.size(); i++) {
// Safety valve: keep paint events flowing for large batches
if (i % 100 == 0 && ops.size() > 200)
if (i % 100 == 0 && ops.size() > 200) {
m_mainWindow->setMcpStatus(
QStringLiteral("MCP: tree.apply %1/%2").arg(i).arg(ops.size()));
QCoreApplication::processEvents(QEventLoop::ExcludeUserInputEvents, 5);
}
QJsonObject op = ops[i].toObject();
QString opType = op.value("op").toString();
@@ -736,15 +780,29 @@ QJsonObject McpBridge::toolTreeApply(const QJsonObject& args) {
n.id = placeholders.value(QStringLiteral("$%1").arg(i), tree.reserveId());
n.kind = kindFromString(op.value("kind").toString("Hex64"));
n.name = op.value("name").toString();
QString pid = resolvePlaceholder(op.value("parentId").toString("0"), placeholders);
bool pidOk;
QString pid = resolvePlaceholder(op.value("parentId").toString("0"), placeholders, &pidOk);
if (!pidOk) {
skippedOps.append(QStringLiteral("op[%1]: unresolved placeholder for parentId").arg(i));
continue;
}
n.parentId = pid.toULongLong();
if (n.parentId != 0 && tree.indexOfId(n.parentId) < 0) {
skippedOps.append(QStringLiteral("op[%1]: parentId '%2' not found").arg(i).arg(pid));
continue;
}
n.offset = op.value("offset").toInt(0);
n.structTypeName = op.value("structTypeName").toString();
n.classKeyword = op.value("classKeyword").toString();
n.strLen = op.value("strLen").toInt(64);
n.strLen = qBound(1, op.value("strLen").toInt(64), 1000000);
n.elementKind = kindFromString(op.value("elementKind").toString("UInt8"));
n.arrayLen = op.value("arrayLen").toInt(1);
QString refStr = resolvePlaceholder(op.value("refId").toString("0"), placeholders);
n.arrayLen = qBound(1, op.value("arrayLen").toInt(1), 1000000);
bool refOk;
QString refStr = resolvePlaceholder(op.value("refId").toString("0"), placeholders, &refOk);
if (!refOk) {
skippedOps.append(QStringLiteral("op[%1]: unresolved placeholder for refId").arg(i));
continue;
}
n.refId = refStr.toULongLong();
// Auto-place: offset -1 means "after last sibling"
@@ -870,7 +928,7 @@ QJsonObject McpBridge::toolTreeApply(const QJsonObject& args) {
int idx = tree.indexOfId(nid.toULongLong());
if (idx >= 0) {
NodeKind newElemKind = kindFromString(op.value("elementKind").toString());
int newLen = op.value("arrayLen").toInt(1);
int newLen = qBound(1, op.value("arrayLen").toInt(1), 1000000);
doc->undoStack.push(new RcxCommand(ctrl,
cmd::ChangeArrayMeta{tree.nodes[idx].id,
tree.nodes[idx].elementKind, newElemKind,
@@ -1383,8 +1441,7 @@ QJsonObject McpBridge::toolProcessInfo(const QJsonObject& args) {
void McpBridge::notifyTreeChanged() {
if (!m_client || !m_initialized) return;
sendNotification("notifications/resources/updated",
QJsonObject{{"uri", "project://tree"}});
m_notifyTimer->start(); // debounce 100ms
}
void McpBridge::notifyDataChanged() {

View File

@@ -7,6 +7,7 @@
#include <QJsonArray>
#include <QJsonDocument>
#include <QByteArray>
#include <QTimer>
namespace rcx {
@@ -34,6 +35,7 @@ private:
QByteArray m_readBuffer;
bool m_initialized = false;
bool m_slowMode = false;
QTimer* m_notifyTimer = nullptr;
// JSON-RPC plumbing
void onNewConnection();
@@ -65,10 +67,11 @@ private:
// Helpers
QJsonObject makeTextResult(const QString& text, bool isError = false);
QString resolvePlaceholder(const QString& ref,
const QHash<QString, uint64_t>& placeholderMap);
const QHash<QString, uint64_t>& placeholderMap,
bool* ok = nullptr);
// Smart tab resolution: tabIndex arg → activeTab → first tab → auto-create
MainWindow::TabState* resolveTab(const QJsonObject& args);
MainWindow::TabState* resolveTab(const QJsonObject& args, int* resolvedIndex = nullptr);
};
} // namespace rcx

View File

@@ -53,6 +53,7 @@ public:
bool isReadable(uint64_t addr, int len) const override {
if (len <= 0) return (len == 0);
uint64_t end = addr + static_cast<uint64_t>(len);
if (end < addr) return false; // overflow
for (uint64_t p = addr & kPageMask; p < end; p += kPageSize) {
if (!m_pages.contains(p)) return false;
}

View File

@@ -702,7 +702,7 @@ void ScannerPanel::onCellEdited(int row, int col) {
m_statusLabel->setText(QStringLiteral("Wrote %1 byte%2 to 0x%3")
.arg(bytes.size())
.arg(bytes.size() == 1 ? "" : "s")
.arg(addr, 0, 16, QLatin1Char('0')).toUpper());
.arg(QString::number(addr, 16).toUpper()));
// Re-read and update cache
m_resultTable->blockSignals(true);
int readSize = (m_lastScanMode == 1) ? valueSize() : 16;

View File

@@ -34,15 +34,13 @@ QVector<TypeSuggestion> inferTypes(
const InferHints& hints = {},
int maxResults = 3);
// Format top suggestion as short display string (e.g. "ptr64 strong", "float×2 moderate")
// Format top suggestion as short type label (e.g. "ptr64", "int32_t×2")
inline QString formatHint(const TypeSuggestion& s) {
if (s.kinds.isEmpty()) return {};
const char* name = kindMeta(s.kinds[0])->typeName;
QString base = (s.kinds.size() == 1)
return (s.kinds.size() == 1)
? QString::fromLatin1(name)
: QStringLiteral("%1\u00D7%2").arg(QString::fromLatin1(name)).arg(s.kinds.size());
const char* conf = s.strength >= 3 ? " strong" : " moderate";
return base + QLatin1String(conf);
}
// ── Implementation (header-only) ──
@@ -258,7 +256,7 @@ inline FeatureResult countInt16Features(uint16_t val,
int passed = 0, checked = 2;
int16_t sv = (int16_t)val;
passed += (val != 0) ? 1 : 0;
passed += (sv >= -4096 && sv <= 4096) ? 1 : 0;
passed += (sv >= -16384 && sv <= 16384) ? 1 : 0;
if (h.sampleCount > 0 && minP && maxP) {
checked += 2;
@@ -373,7 +371,6 @@ inline void tryWhole1(const uint8_t* data, QVector<Candidate>& out) {
uint8_t v = data[0];
int score = (v == 0 || v == 1) ? 50 : 25;
addCandidate(out, NodeKind::UInt8, score);
if (v <= 1) addCandidate(out, NodeKind::Bool, 60);
}
// ── Try uniform splits ──

View File

@@ -95,7 +95,8 @@ inline QStandardItem* makeTypeItem(const Node* node, const NodeTree* tree,
// Full rebuild — used by benchmarks and first build.
inline void buildProjectExplorer(QStandardItemModel* model,
const QVector<TabInfo>& tabs) {
const QVector<TabInfo>& tabs,
const QSet<uint64_t>& pinnedIds = {}) {
model->clear();
model->setHorizontalHeaderLabels({QStringLiteral("Name")});
@@ -113,18 +114,32 @@ inline void buildProjectExplorer(QStandardItemModel* model,
}
}
for (const auto& e : types)
// Pinned items at the very top, then structs, then enums
QVector<Entry> pinned;
QVector<Entry> unpinnedTypes, unpinnedEnums;
for (const auto& e : types) {
if (pinnedIds.contains(e.node->id)) pinned.append(e);
else unpinnedTypes.append(e);
}
for (const auto& e : enums) {
if (pinnedIds.contains(e.node->id)) pinned.append(e);
else unpinnedEnums.append(e);
}
for (const auto& e : pinned)
model->appendRow(makeTypeItem(e.node, e.tree, e.subPtr));
for (const auto& e : enums)
for (const auto& e : unpinnedTypes)
model->appendRow(makeTypeItem(e.node, e.tree, e.subPtr));
for (const auto& e : unpinnedEnums)
model->appendRow(makeTypeItem(e.node, e.tree, e.subPtr));
}
// Incremental sync — preserves tree expansion/scroll state.
inline void syncProjectExplorer(QStandardItemModel* model,
const QVector<TabInfo>& tabs) {
const QVector<TabInfo>& tabs,
const QSet<uint64_t>& pinnedIds = {}) {
// First call — full build
if (model->rowCount() == 0 && !tabs.isEmpty()) {
buildProjectExplorer(model, tabs);
buildProjectExplorer(model, tabs, pinnedIds);
return;
}
@@ -276,27 +291,39 @@ public:
QString name = (dashPos > 1) ? fullText.left(dashPos - 1) : fullText;
QString count = (dashPos > 1) ? fullText.mid(dashPos + 2).trimmed() : QString();
bool pinned = index.data(Qt::UserRole + 4).toBool();
// Reserve right side for pin icon + count pill
int rightEdge = textRect.right();
if (!count.isEmpty()) {
int cw = opt.fontMetrics.horizontalAdvance(count) + 10;
int ch = opt.fontMetrics.height();
int cy = textRect.y() + (textRect.height() - ch) / 2;
QRect pill(textRect.right() - cw, cy, cw, ch);
// Draw name clipped before pill
if (pill.left() > textRect.left() + 4) {
QRect nameRect = textRect;
nameRect.setRight(pill.left() - 4);
QString elided = opt.fontMetrics.elidedText(name, Qt::ElideRight, nameRect.width());
painter->setPen(m_text);
painter->drawText(nameRect, Qt::AlignLeft | Qt::AlignVCenter, elided);
}
QRect pill(rightEdge - cw, cy, cw, ch);
rightEdge = pill.left() - 2;
painter->setPen(Qt::NoPen);
painter->setBrush(m_badgeBg);
painter->drawRect(pill);
painter->setPen(m_textMuted);
painter->drawText(pill, Qt::AlignCenter, count);
} else {
}
if (pinned) {
static const QIcon pinIcon(":/vsicons/pin.svg");
int isz = opt.fontMetrics.height() - 2;
int iy = textRect.y() + (textRect.height() - isz) / 2;
QRect pinRect(rightEdge - isz, iy, isz, isz);
pinIcon.paint(painter, pinRect);
rightEdge = pinRect.left() - 2;
}
// Draw name clipped before right-side elements
if (rightEdge > textRect.left() + 4) {
QRect nameRect = textRect;
nameRect.setRight(rightEdge);
QString elided = opt.fontMetrics.elidedText(name, Qt::ElideRight, nameRect.width());
painter->setPen(m_text);
painter->drawText(textRect, Qt::AlignLeft | Qt::AlignVCenter, name);
painter->drawText(nameRect, Qt::AlignLeft | Qt::AlignVCenter, elided);
}
} else {
// Child: "TypeName fieldName"