add slash command autocomplete: popup, model/backend completions, keyboard nav
This commit is contained in:
parent
3a33c4e42a
commit
64afb587e3
257
gui/main.qml
257
gui/main.qml
|
|
@ -62,12 +62,196 @@ ApplicationWindow {
|
|||
Shortcut {
|
||||
sequence: "Escape"
|
||||
onActivated: {
|
||||
if (ollie.activeState !== "idle") {
|
||||
if (completionPopup.visible) {
|
||||
completionPopup.close()
|
||||
} else if (ollie.activeState !== "idle") {
|
||||
ollie.interrupt()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- Slash command definitions ---
|
||||
readonly property var slashCommands: [
|
||||
{ name: "model", desc: "Set model" },
|
||||
{ name: "backend", desc: "Set backend" },
|
||||
{ name: "agent", desc: "Switch agent" },
|
||||
{ name: "stop", desc: "Stop current run" },
|
||||
{ name: "kill", desc: "Kill session" },
|
||||
{ name: "new", desc: "New session" },
|
||||
{ name: "compact", desc: "Compact context" },
|
||||
{ name: "help", desc: "Show help" }
|
||||
]
|
||||
|
||||
// --- Completion popup ---
|
||||
Popup {
|
||||
id: completionPopup
|
||||
x: 80
|
||||
y: parent.height - 280
|
||||
width: Math.min(360, parent.width - 160)
|
||||
height: Math.min(200, visibleMatches * 28 + 32)
|
||||
modal: false
|
||||
focus: true
|
||||
closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutsideParent
|
||||
|
||||
background: Rectangle {
|
||||
radius: 6
|
||||
color: theme.isDark ? "#1e1e1e" : "#ffffff"
|
||||
border.color: theme.midColor
|
||||
border.width: 1
|
||||
elevation: 4
|
||||
}
|
||||
|
||||
ListView {
|
||||
id: completionList
|
||||
anchors.fill: parent
|
||||
anchors.margins: 4
|
||||
model: completionModel
|
||||
currentIndex: -1
|
||||
clip: true
|
||||
|
||||
delegate: ItemDelegate {
|
||||
width: completionList.width
|
||||
height: 28
|
||||
highlighted: index === completionList.currentIndex
|
||||
|
||||
contentItem: RowLayout {
|
||||
spacing: 8
|
||||
|
||||
Label {
|
||||
text: model.name
|
||||
font.bold: true
|
||||
font.pointSize: theme.fontSize > 0 ? theme.fontSize * 0.9 : 11
|
||||
color: theme.textColor
|
||||
}
|
||||
|
||||
Item { Layout.fillWidth: true }
|
||||
|
||||
Label {
|
||||
text: model.desc || ""
|
||||
opacity: 0.5
|
||||
font.pointSize: theme.fontSize > 0 ? theme.fontSize * 0.8 : 10
|
||||
color: theme.textColor
|
||||
}
|
||||
}
|
||||
|
||||
onClicked: {
|
||||
completionList.currentIndex = index
|
||||
acceptCompletion()
|
||||
}
|
||||
}
|
||||
|
||||
Keys.onPressed: function(event) {
|
||||
if (event.key === Qt.Key_Down) {
|
||||
if (completionList.currentIndex < completionList.count - 1) {
|
||||
completionList.currentIndex++
|
||||
event.accepted = true
|
||||
}
|
||||
} else if (event.key === Qt.Key_Up) {
|
||||
if (completionList.currentIndex > 0) {
|
||||
completionList.currentIndex--
|
||||
event.accepted = true
|
||||
}
|
||||
} else if (event.key === Qt.Key_Enter || event.key === Qt.Key_Return) {
|
||||
if (completionList.currentIndex >= 0) {
|
||||
acceptCompletion()
|
||||
event.accepted = true
|
||||
}
|
||||
} else if (event.key === Qt.Key_Escape) {
|
||||
completionPopup.close()
|
||||
event.accepted = true
|
||||
}
|
||||
}
|
||||
|
||||
Keys.onTabPressed: {
|
||||
if (completionList.currentIndex >= 0) {
|
||||
acceptCompletion()
|
||||
event.accepted = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ListModel {
|
||||
id: completionModel
|
||||
}
|
||||
|
||||
function open(prefix) {
|
||||
updateCompletions(prefix)
|
||||
visible = true
|
||||
promptInput.forceActiveFocus()
|
||||
}
|
||||
|
||||
function close() {
|
||||
visible = false
|
||||
completionModel.clear()
|
||||
completionList.currentIndex = -1
|
||||
}
|
||||
|
||||
function updateCompletions(prefix) {
|
||||
completionModel.clear()
|
||||
var parts = prefix.split(" ")
|
||||
var cmd = parts[0].toLowerCase()
|
||||
var arg = parts.length > 1 ? parts.slice(1).join(" ") : ""
|
||||
|
||||
// If there's an argument part after a known command, show argument completions
|
||||
if (arg !== "") {
|
||||
if (cmd === "model") {
|
||||
var models = ollie.getAvailableModels(ollie.availableBackends[currentBackendIndex] || "")
|
||||
for (var i = 0; i < models.length; ++i) {
|
||||
if (models[i].toLowerCase().indexOf(arg.toLowerCase()) >= 0) {
|
||||
completionModel.append({ name: models[i], desc: "", isArg: true })
|
||||
}
|
||||
}
|
||||
} else if (cmd === "backend") {
|
||||
var bks = ollie.availableBackends
|
||||
for (var j = 0; j < bks.length; ++j) {
|
||||
if (bks[j].toLowerCase().indexOf(arg.toLowerCase()) >= 0) {
|
||||
completionModel.append({ name: bks[j], desc: "", isArg: true })
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Show command completions filtered by prefix
|
||||
for (var k = 0; k < root.slashCommands.length; ++k) {
|
||||
var sc = root.slashCommands[k]
|
||||
if (sc.name.indexOf(cmd) >= 0) {
|
||||
completionModel.append({ name: sc.name, desc: sc.desc, isArg: false })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (completionModel.count > 0) {
|
||||
completionList.currentIndex = 0
|
||||
visible = true
|
||||
} else {
|
||||
visible = false
|
||||
}
|
||||
}
|
||||
|
||||
function acceptCompletion() {
|
||||
if (completionList.currentIndex < 0 || completionList.currentIndex >= completionModel.count) return
|
||||
var item = completionModel.get(completionList.currentIndex)
|
||||
var oldText = promptInput.text
|
||||
var slashPos = oldText.lastIndexOf("/")
|
||||
if (slashPos < 0) return
|
||||
|
||||
var beforeSlash = oldText.substring(0, slashPos)
|
||||
var afterSlash = oldText.substring(slashPos + 1)
|
||||
var parts = afterSlash.split(" ")
|
||||
|
||||
if (item.isArg) {
|
||||
// Replace the argument part
|
||||
parts[parts.length - 1] = item.name
|
||||
promptInput.text = beforeSlash + "/" + parts.join(" ")
|
||||
} else {
|
||||
// Replace command with command + space
|
||||
promptInput.text = beforeSlash + "/" + item.name + " "
|
||||
}
|
||||
close()
|
||||
}
|
||||
|
||||
property int visibleMatches: completionModel.count
|
||||
}
|
||||
|
||||
// Deferred scroll timer — fires once after model changes settle
|
||||
Timer {
|
||||
id: scrollTimer
|
||||
|
|
@ -744,6 +928,77 @@ ApplicationWindow {
|
|||
placeholderText: ollie.activeSessionId ? "Message..." : "No session"
|
||||
enabled: ollie.activeSessionId !== ""
|
||||
wrapMode: TextEdit.Wrap
|
||||
|
||||
property string _lastText: ""
|
||||
property int _lastCursorPos: 0
|
||||
|
||||
onTextChanged: {
|
||||
var oldText = _lastText
|
||||
var newText = text
|
||||
var cursorPos = cursorPosition
|
||||
|
||||
// Detect slash prefix (text after last "/" up to cursor)
|
||||
if (newText.indexOf("/") >= 0) {
|
||||
var slashPos = newText.lastIndexOf("/")
|
||||
if (slashPos < cursorPos) {
|
||||
var prefix = newText.substring(slashPos + 1, cursorPos)
|
||||
if (prefix !== oldText.substring(oldText.lastIndexOf("/"), cursorPos) ||
|
||||
oldText !== _lastText) {
|
||||
completionPopup.open(prefix)
|
||||
}
|
||||
} else {
|
||||
completionPopup.close()
|
||||
}
|
||||
} else {
|
||||
completionPopup.close()
|
||||
}
|
||||
|
||||
_lastText = newText
|
||||
_lastCursorPos = cursorPos
|
||||
}
|
||||
|
||||
onCursorPositionChanged: {
|
||||
var newText = text
|
||||
if (newText.indexOf("/") >= 0) {
|
||||
var slashPos = newText.lastIndexOf("/")
|
||||
if (slashPos < cursorPosition) {
|
||||
var prefix = newText.substring(slashPos + 1, cursorPosition)
|
||||
completionPopup.open(prefix)
|
||||
} else {
|
||||
completionPopup.close()
|
||||
}
|
||||
} else {
|
||||
completionPopup.close()
|
||||
}
|
||||
}
|
||||
|
||||
Keys.onPressed: function(event) {
|
||||
if (completionPopup.visible) {
|
||||
if (event.key === Qt.Key_Tab) {
|
||||
completionPopup.acceptCompletion()
|
||||
event.accepted = true
|
||||
} else if (event.key === Qt.Key_Enter || event.key === Qt.Key_Return) {
|
||||
if (completionPopup.completionList.currentIndex >= 0) {
|
||||
completionPopup.acceptCompletion()
|
||||
event.accepted = true
|
||||
}
|
||||
} else if (event.key === Qt.Key_Escape) {
|
||||
completionPopup.close()
|
||||
event.accepted = true
|
||||
} else if (event.key === Qt.Key_Down) {
|
||||
if (completionPopup.completionList.currentIndex < completionPopup.completionList.count - 1) {
|
||||
completionPopup.completionList.currentIndex++
|
||||
event.accepted = true
|
||||
}
|
||||
} else if (event.key === Qt.Key_Up) {
|
||||
if (completionPopup.completionList.currentIndex > 0) {
|
||||
completionPopup.completionList.currentIndex--
|
||||
event.accepted = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Keys.onReturnPressed: function(event) {
|
||||
if (event.modifiers & Qt.ShiftModifier) {
|
||||
event.accepted = false
|
||||
|
|
|
|||
Reference in New Issue