From 9e0e5cb0f4a4fc2c0fb2dbd4b6eff299ac5a04c3 Mon Sep 17 00:00:00 2001 From: Levi Neely <141506390+lneely@users.noreply.github.com> Date: Thu, 16 Jul 2026 19:42:21 +0200 Subject: [PATCH] Improve KDE GUI file and command handling --- Makefile | 18 ++++--- dolphin/ollie-session-here | 18 +++++-- gui/chatblockmodel.cpp | 103 +++++++++++++++++++++++++++++++++++-- gui/chatblockmodel.h | 5 +- gui/main-kf5.qml | 7 +-- gui/main.qml | 7 +-- 6 files changed, 135 insertions(+), 23 deletions(-) diff --git a/Makefile b/Makefile index cf50730..2d49737 100644 --- a/Makefile +++ b/Makefile @@ -26,6 +26,8 @@ kf5: install: build @echo "=== Installing ollie-kde (KF6) ===" + # Remove stale copies from old install path + rm -f /usr/local/bin/ollie-ask /usr/local/bin/ollie-session-here # Binaries install -m755 $(BUILD_DIR)/ollie-tray /usr/bin/ install -m755 $(BUILD_DIR)/ollie-gui /usr/bin/ @@ -58,8 +60,8 @@ install: build mkdir -p ~/.local/share/kio/servicemenus rm -f ~/.local/share/kio/servicemenus/ollie-actions.desktop install -m755 dolphin/ollie-actions.desktop ~/.local/share/kio/servicemenus/ - install -m755 dolphin/ollie-ask /usr/local/bin/ - install -m755 dolphin/ollie-session-here /usr/local/bin/ + install -m755 dolphin/ollie-ask /usr/bin/ + install -m755 dolphin/ollie-session-here /usr/bin/ # Finalize ldconfig kquitapp6 krunner 2>/dev/null; true @@ -78,7 +80,7 @@ uninstall: rm -rf /usr/lib64/qt6/qml/org/ollie kpackagetool6 -g -t Plasma/Applet -r org.ollie.plasmoid 2>/dev/null; true rm -f ~/.local/share/kio/servicemenus/ollie-actions.desktop - rm -f /usr/local/bin/ollie-ask /usr/local/bin/ollie-session-here + rm -f /usr/bin/ollie-ask /usr/bin/ollie-session-here ldconfig kquitapp6 krunner 2>/dev/null; true @echo "=== Uninstall complete ===" @@ -96,8 +98,8 @@ dolphin-install: mkdir -p ~/.local/share/kio/servicemenus rm -f ~/.local/share/kio/servicemenus/ollie-actions.desktop install -m755 dolphin/ollie-actions.desktop ~/.local/share/kio/servicemenus/ - install -m755 dolphin/ollie-ask /usr/local/bin/ - install -m755 dolphin/ollie-session-here /usr/local/bin/ + install -m755 dolphin/ollie-ask /usr/bin/ + install -m755 dolphin/ollie-session-here /usr/bin/ @echo "Dolphin menus installed. Restart Dolphin or re-open context menu." plasmoid-install: @@ -156,8 +158,8 @@ kf5-install: mkdir -p ~/.local/share/kio/servicemenus rm -f ~/.local/share/kio/servicemenus/ollie-actions.desktop install -m755 dolphin/ollie-actions-kf5.desktop ~/.local/share/kio/servicemenus/ollie-actions.desktop - install -m755 dolphin/ollie-ask /usr/local/bin/ - install -m755 dolphin/ollie-session-here /usr/local/bin/ + install -m755 dolphin/ollie-ask /usr/bin/ + install -m755 dolphin/ollie-session-here /usr/bin/ # Finalize ldconfig kquitapp5 krunner 2>/dev/null; true @@ -173,7 +175,7 @@ kf5-uninstall: rm -rf /usr/lib/x86_64-linux-gnu/qt5/qml/org/ollie kpackagetool5 -t Plasma/Applet -r org.ollie.plasmoid 2>/dev/null; true rm -f ~/.local/share/kio/servicemenus/ollie-actions.desktop - rm -f /usr/local/bin/ollie-ask /usr/local/bin/ollie-session-here + rm -f /usr/bin/ollie-ask /usr/bin/ollie-session-here ldconfig kquitapp5 krunner 2>/dev/null; true @echo "=== Uninstall complete (KF5) ===" diff --git a/dolphin/ollie-session-here b/dolphin/ollie-session-here index 3c1d423..5813eb1 100755 --- a/dolphin/ollie-session-here +++ b/dolphin/ollie-session-here @@ -1,11 +1,21 @@ #!/bin/sh # ollie-session-here: start a session in the given directory +# Works from both Dolphin (passes %f) and terminal (uses $PWD). TARGET="$1" [ -f "$TARGET" ] && TARGET="$(dirname "$TARGET")" -[ -z "$TARGET" ] && TARGET="$HOME" +[ -z "$TARGET" ] && TARGET="$PWD" +TARGET="$(cd "$TARGET" 2>/dev/null && pwd)" || TARGET="$PWD" -dbus-send --session --dest=org.ollie.SessionManager --type=method_call \ +RESULT=$(dbus-send --session --dest=org.ollie.SessionManager --type=method_call --print-reply \ /org/ollie/SessionManager org.ollie.SessionManager.CreateSession \ - string:"$TARGET" string:"" string:"" string:"default" + string:"$TARGET" string:"" string:"" string:"default" 2>&1) -notify-send -t 3000 "Ollie" "Session started in $TARGET" +SESSION_ID=$(echo "$RESULT" | grep 'string "' | head -1 | sed 's/.*string "\(.*\)"/\1/') + +if [ -t 1 ]; then + # Terminal: print session ID for further use + echo "$SESSION_ID" +else + # Dolphin/non-interactive: desktop notification + notify-send -t 3000 "Ollie" "Session started in $TARGET" +fi diff --git a/gui/chatblockmodel.cpp b/gui/chatblockmodel.cpp index 1ea211b..68372bb 100644 --- a/gui/chatblockmodel.cpp +++ b/gui/chatblockmodel.cpp @@ -9,6 +9,9 @@ #include #include #include +#include +#include +#include #include #include #include @@ -469,7 +472,97 @@ void ChatBlockModel::copyToClipboard(const QString &text) const QGuiApplication::clipboard()->setText(text); } -void ChatBlockModel::executeInTerminal(const QString &code, const QString &language) const +QString ChatBlockModel::linkifyFilePaths(const QString &markdown) const +{ + // Agent output normally uses absolute paths, optionally followed by :line:column. + // Restrict matching to absolute paths so ordinary prose and relative URLs remain intact. + static const QRegularExpression pathRe( + R"((?\[\]\(\),;!?\:]+)(?::(\d+))?(?::(\d+))?\1)"); + + // Preserve complete Markdown links; rewriting a path inside their label would + // produce nested links that Qt renders as literal Markdown. + static const QRegularExpression markdownLinkRe(R"(\[[^\]]*\]\([^\)]*\))"); + QList> linkRanges; + auto links = markdownLinkRe.globalMatch(markdown); + while (links.hasNext()) { + const QRegularExpressionMatch link = links.next(); + linkRanges.append({link.capturedStart(), link.capturedEnd()}); + } + + QString result; + result.reserve(markdown.size() + 64); + int lastEnd = 0; + auto matches = pathRe.globalMatch(markdown); + while (matches.hasNext()) { + const QRegularExpressionMatch match = matches.next(); + const QString path = match.captured(2); + const int start = match.capturedStart(); + + bool insideMarkdownLink = false; + for (const auto &range : linkRanges) { + if (start >= range.first && start < range.second) { + insideMarkdownLink = true; + break; + } + } + if (insideMarkdownLink) + continue; + + result += markdown.mid(lastEnd, start - lastEnd); + QUrl url = QUrl::fromLocalFile(path); + QStringList query; + if (!match.captured(3).isEmpty()) + query << QStringLiteral("line=") + match.captured(3); + if (!match.captured(4).isEmpty()) + query << QStringLiteral("column=") + match.captured(4); + if (!query.isEmpty()) + url.setQuery(query.join(QLatin1Char('&'))); + + QString label = match.captured(0); + if (!match.captured(1).isEmpty()) + label = label.mid(1, label.size() - 2); + result += QStringLiteral("[") + label + QStringLiteral("](") + + url.toString(QUrl::FullyEncoded) + QStringLiteral(")"); + lastEnd = match.capturedEnd(); + } + result += markdown.mid(lastEnd); + return result; +} + +void ChatBlockModel::openLink(const QString &link) const +{ + const QUrl url(link); + if (!url.isLocalFile()) { + QDesktopServices::openUrl(url); + return; + } + + const QString path = url.toLocalFile(); + int line = 0; + int column = 0; + const QStringList queryItems = url.query().split(QLatin1Char('&'), Qt::SkipEmptyParts); + for (const QString &item : queryItems) { + if (item.startsWith(QLatin1String("line="))) + line = item.mid(5).toInt(); + else if (item.startsWith(QLatin1String("column="))) + column = item.mid(7).toInt(); + } + + const QString kate = QStandardPaths::findExecutable(QStringLiteral("kate")); + if (!kate.isEmpty() && line > 0) { + QStringList args; + args << QStringLiteral("--line") << QString::number(line); + if (column > 0) + args << QStringLiteral("--column") << QString::number(column); + args << path; + if (QProcess::startDetached(kate, args)) + return; + } + QDesktopServices::openUrl(QUrl::fromLocalFile(path)); +} + +bool ChatBlockModel::executeInTerminal(const QString &code, const QString &language, + const QString &workingDirectory) const { // Determine interpreter/shebang QString shebang = QStringLiteral("#!/bin/bash"); @@ -507,7 +600,7 @@ void ChatBlockModel::executeInTerminal(const QString &code, const QString &langu tmp.setFileTemplate(QStringLiteral("/tmp/ollie-exec-XXXXXX") + ext); tmp.setAutoRemove(false); // keep it so terminal can read it if (!tmp.open()) - return; + return false; QString script = shebang + QStringLiteral("\n") + code + QStringLiteral("\n"); tmp.write(script.toUtf8()); @@ -536,7 +629,9 @@ void ChatBlockModel::executeInTerminal(const QString &code, const QString &langu args << QStringLiteral("-e") << scriptPath; } - QProcess::startDetached(exe, args); - return; + const QString cwd = QDir(workingDirectory).exists() ? workingDirectory : QDir::homePath(); + if (QProcess::startDetached(exe, args, cwd)) + return true; } + return false; } diff --git a/gui/chatblockmodel.h b/gui/chatblockmodel.h index b1e8fb7..73ba5ff 100644 --- a/gui/chatblockmodel.h +++ b/gui/chatblockmodel.h @@ -63,7 +63,10 @@ public: Q_INVOKABLE QString highlightedHtml(const QString &code, const QString &language) const; Q_INVOKABLE QString prettyJson(const QString &text) const; Q_INVOKABLE void copyToClipboard(const QString &text) const; - Q_INVOKABLE void executeInTerminal(const QString &code, const QString &language) const; + Q_INVOKABLE QString linkifyFilePaths(const QString &markdown) const; + Q_INVOKABLE void openLink(const QString &link) const; + Q_INVOKABLE bool executeInTerminal(const QString &code, const QString &language, + const QString &workingDirectory = QString()) const; Q_INVOKABLE bool loadEarlier(); // returns true if more blocks were loaded signals: diff --git a/gui/main-kf5.qml b/gui/main-kf5.qml index 2ecfe26..ce61a41 100644 --- a/gui/main-kf5.qml +++ b/gui/main-kf5.qml @@ -1081,14 +1081,14 @@ ApplicationWindow { id: proseSubDelegate Text { width: parent ? parent.width : implicitWidth - text: subBlock.content || "" + text: chatModel.linkifyFilePaths(subBlock.content || "") wrapMode: Text.Wrap textFormat: Text.MarkdownText color: palette.text font.pointSize: root.font.pointSize font.family: root.font.family lineHeight: 1.3 - onLinkActivated: Qt.openUrlExternally(link) + onLinkActivated: chatModel.openLink(link) } } @@ -1186,7 +1186,8 @@ ApplicationWindow { "py", "perl", "ruby", "rb", "lua", "node", "javascript", "js", "php"].indexOf(lang) >= 0 } - onClicked: chatModel.executeInTerminal(subBlock.content, subBlock.language) + onClicked: chatModel.executeInTerminal(subBlock.content, subBlock.language, + dbus.activeCwd) ToolTip.text: "Execute in terminal" ToolTip.visible: hovered } diff --git a/gui/main.qml b/gui/main.qml index 2e0c34b..b6c7981 100644 --- a/gui/main.qml +++ b/gui/main.qml @@ -1086,7 +1086,7 @@ ApplicationWindow { id: proseSubDelegate Text { width: parent ? parent.width : implicitWidth - text: subBlock.content || "" + text: chatModel.linkifyFilePaths(subBlock.content || "") wrapMode: Text.Wrap textFormat: Text.MarkdownText color: palette.text @@ -1094,7 +1094,7 @@ ApplicationWindow { font.family: root.font.family lineHeight: 1.3 onLinkActivated: function(link) { - Qt.openUrlExternally(link) + chatModel.openLink(link) } } } @@ -1193,7 +1193,8 @@ ApplicationWindow { "py", "perl", "ruby", "rb", "lua", "node", "javascript", "js", "php"].indexOf(lang) >= 0 } - onClicked: chatModel.executeInTerminal(subBlock.content, subBlock.language) + onClicked: chatModel.executeInTerminal(subBlock.content, subBlock.language, + dbus.activeCwd) ToolTip.text: "Execute in terminal" ToolTip.visible: hovered }