Split the bar panel into one QML file per stage
Panel.qml keeps the state, the CLI calls and the stage switch (889 lines, was 2360); each stage is its own file taking the panel as a required property. The omarchy installer copies every QML/JS file and clears stale ones. tests/static.sh filters qmllint on [syntax], not the word error. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
+27
-1
@@ -174,4 +174,30 @@ test.
|
|||||||
| session repair | `linux/lib/repair.sh`; `cm-json.py` `cmd_scan_sessions`, `cmd_repair_session` | — |
|
| session repair | `linux/lib/repair.sh`; `cm-json.py` `cmd_scan_sessions`, `cmd_repair_session` | — |
|
||||||
| key storage | `linux/cm-vault.sh` | `lib/vault.ps1` |
|
| key storage | `linux/cm-vault.sh` | `lib/vault.ps1` |
|
||||||
| the bar icon and state | `omarchy/smoido.claude-mode/BarWidget.qml` | — |
|
| the bar icon and state | `omarchy/smoido.claude-mode/BarWidget.qml` | — |
|
||||||
| the panel | `omarchy/smoido.claude-mode/Panel.qml` | — |
|
| what the panel does | `Panel.qml` (state, actions, the processes it runs) | — |
|
||||||
|
| how one panel stage looks | its own file, below | — |
|
||||||
|
|
||||||
|
### The bar widget's files
|
||||||
|
|
||||||
|
All in `omarchy/smoido.claude-mode/`. The panel owns every piece of state and
|
||||||
|
every action; each stage file only draws, from the `panel` it is handed.
|
||||||
|
|
||||||
|
| file | what is in it |
|
||||||
|
|---|---|
|
||||||
|
| `BarWidget.qml` | the icon, the state it watches (`health.json`, `state.json`, the model cache), the session scan, the provider lookup |
|
||||||
|
| `Panel.qml` | the panel's state and actions, the CLI processes they run, the hero and the footer, and the stages in stacking order |
|
||||||
|
| `ModeList.qml` | the modes to switch to, each unfolding its presets |
|
||||||
|
| `BlockedStage.qml` | a refused switch, with the fix |
|
||||||
|
| `ServerStage.qml` | a server provider's address and key |
|
||||||
|
| `PresetStage.qml` | the preset editor |
|
||||||
|
| `TierStage.qml` | picking one tier's model |
|
||||||
|
| `NewPresetStage.qml`, `RenameStage.qml`, `DeleteStage.qml` | a preset's lifecycle |
|
||||||
|
| `SessionsStage.qml` | the running-sessions question before a switch |
|
||||||
|
| `BrokenSessions.qml`, `RepairStage.qml` | sessions a switch broke, and repairing one |
|
||||||
|
| `ModelMap.qml` | the active tier-to-model map |
|
||||||
|
| `PillButton.qml`, `BrandIcon.qml` | the panel's button, and a provider's mark |
|
||||||
|
| `Modes.js` | fallback presentation for anthropic and an older `health.json` |
|
||||||
|
|
||||||
|
A stage cannot reach into another file by id, so where an action has to touch a
|
||||||
|
stage's field — clearing the model search, focusing a name field — `Panel.qml`
|
||||||
|
emits a signal and the stage's `Connections` block does it.
|
||||||
|
|||||||
+9
-3
@@ -36,11 +36,17 @@ fi
|
|||||||
|
|
||||||
# --- payload ---------------------------------------------------------------
|
# --- payload ---------------------------------------------------------------
|
||||||
mkdir -p "$DEST_DIR"
|
mkdir -p "$DEST_DIR"
|
||||||
for f in manifest.json BarWidget.qml Panel.qml BrandIcon.qml Modes.js; do
|
for f in manifest.json BarWidget.qml Panel.qml Modes.js; do
|
||||||
[ -f "$SRC/$f" ] || { fail "missing from payload: $f"; exit 1; }
|
[ -f "$SRC/$f" ] || { fail "missing from payload: $f"; exit 1; }
|
||||||
install -m 0644 "$SRC/$f" "$DEST_DIR/$f"
|
|
||||||
done
|
done
|
||||||
green 'plugin files copied'
|
# Every QML and JS file, not a fixed list: the panel is split into one file per
|
||||||
|
# stage. The old ones are cleared first, so a file removed from the source does
|
||||||
|
# not linger in the plugin directory, where QML would still find it by name.
|
||||||
|
rm -f "$DEST_DIR"/*.qml "$DEST_DIR"/*.js
|
||||||
|
for f in "$SRC"/manifest.json "$SRC"/*.qml "$SRC"/*.js; do
|
||||||
|
install -m 0644 "$f" "$DEST_DIR/$(basename "$f")"
|
||||||
|
done
|
||||||
|
green "plugin files copied ($(ls "$SRC"/*.qml | wc -l | tr -d ' ') QML files)"
|
||||||
|
|
||||||
# --- bar layout ------------------------------------------------------------
|
# --- bar layout ------------------------------------------------------------
|
||||||
# shell.json is the user's own file and may carry unrelated customisation, so
|
# shell.json is the user's own file and may carry unrelated customisation, so
|
||||||
|
|||||||
@@ -0,0 +1,92 @@
|
|||||||
|
import QtQuick
|
||||||
|
import qs.Commons
|
||||||
|
import qs.Ui
|
||||||
|
|
||||||
|
// BlockedStage.qml - Preflight refused. Name the missing thing and offer the one action that fixes it, rather than failing the click silently.
|
||||||
|
//
|
||||||
|
// One stage of the claude-mode panel. Panel.qml hands itself in as `panel`
|
||||||
|
// and owns all the state and actions; this file only draws them.
|
||||||
|
Column {
|
||||||
|
id: stage
|
||||||
|
required property var panel
|
||||||
|
|
||||||
|
width: parent.width
|
||||||
|
visible: stage.panel.stage === "blocked" && stage.panel.blocker !== null
|
||||||
|
spacing: Style.space(7)
|
||||||
|
|
||||||
|
Text {
|
||||||
|
width: parent.width
|
||||||
|
text: stage.panel.blocker ? String(stage.panel.blocker.title) : ""
|
||||||
|
color: stage.panel.urgentColor
|
||||||
|
wrapMode: Text.WordWrap
|
||||||
|
font.family: stage.panel.uiFont
|
||||||
|
font.pixelSize: Style.space(13)
|
||||||
|
font.bold: true
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
width: parent.width
|
||||||
|
text: stage.panel.blocker ? String(stage.panel.blocker.detail) : ""
|
||||||
|
color: Color.popups.text
|
||||||
|
wrapMode: Text.WordWrap
|
||||||
|
lineHeight: 1.2
|
||||||
|
font.family: stage.panel.uiFont
|
||||||
|
font.pixelSize: Style.space(11)
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
width: parent.width
|
||||||
|
visible: stage.panel.blocker && String(stage.panel.blocker.remedy) !== ""
|
||||||
|
text: stage.panel.blocker ? "$ " + String(stage.panel.blocker.remedy) : ""
|
||||||
|
color: Color.muted
|
||||||
|
wrapMode: Text.WrapAnywhere
|
||||||
|
font.family: stage.panel.uiFont
|
||||||
|
font.pixelSize: Style.space(10)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Flow, not Row: these can total more than the card is wide, and a
|
||||||
|
// Row lays them straight past its right edge instead of wrapping.
|
||||||
|
Flow {
|
||||||
|
width: parent.width
|
||||||
|
spacing: Style.space(7)
|
||||||
|
|
||||||
|
PillButton { fontFamily: stage.panel.uiFont;
|
||||||
|
label: "Set up " + stage.panel.pTitle(stage.panel.pendingMode) + "…"
|
||||||
|
primary: true
|
||||||
|
visible: stage.panel.blocker && String(stage.panel.blocker.remedyKind) === "setup"
|
||||||
|
onTriggered: stage.panel.runSetup()
|
||||||
|
}
|
||||||
|
PillButton { fontFamily: stage.panel.uiFont;
|
||||||
|
label: "Store the key…"
|
||||||
|
primary: true
|
||||||
|
visible: stage.panel.blocker && String(stage.panel.blocker.remedyKind) === "set-key"
|
||||||
|
onTriggered: stage.panel.runRemedy()
|
||||||
|
}
|
||||||
|
// A preset with every tier empty: the fix is one click away here,
|
||||||
|
// not in a terminal.
|
||||||
|
PillButton { fontFamily: stage.panel.uiFont;
|
||||||
|
label: "Edit preset…"
|
||||||
|
primary: true
|
||||||
|
visible: stage.panel.blocker && String(stage.panel.blocker.remedyKind) === "edit-preset"
|
||||||
|
onTriggered: {
|
||||||
|
var p = stage.panel.pendingPreset
|
||||||
|
stage.panel.resetFlow()
|
||||||
|
stage.panel.openPresetEditor(p)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
PillButton { fontFamily: stage.panel.uiFont;
|
||||||
|
label: "Server settings…"
|
||||||
|
primary: stage.panel.blocker && ["start-server", "set-url", "needs-key"].indexOf(String(stage.panel.blocker.remedyKind)) >= 0
|
||||||
|
visible: stage.panel.serverEditable(stage.panel.pendingMode)
|
||||||
|
&& !(stage.panel.blocker && String(stage.panel.blocker.remedyKind) === "setup")
|
||||||
|
onTriggered: stage.panel.openServerSettings(stage.panel.pendingPreset)
|
||||||
|
}
|
||||||
|
PillButton { fontFamily: stage.panel.uiFont;
|
||||||
|
label: stage.panel.blocker && ["start-server", "setup"].indexOf(String(stage.panel.blocker.remedyKind)) >= 0
|
||||||
|
? "Check again" : "Try again"
|
||||||
|
primary: stage.panel.blocker && String(stage.panel.blocker.remedyKind) === "start-server"
|
||||||
|
onTriggered: stage.panel.retryPending()
|
||||||
|
}
|
||||||
|
PillButton { fontFamily: stage.panel.uiFont; label: "Cancel"; onTriggered: stage.panel.resetFlow() }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,218 @@
|
|||||||
|
import QtQuick
|
||||||
|
import qs.Commons
|
||||||
|
import qs.Ui
|
||||||
|
|
||||||
|
// BrokenSessions.qml - Sessions that a switch cut short. Shown in the list, because the point is to be noticed without going looking.
|
||||||
|
//
|
||||||
|
// One stage of the claude-mode panel. Panel.qml hands itself in as `panel`
|
||||||
|
// and owns all the state and actions; this file only draws them.
|
||||||
|
Column {
|
||||||
|
id: stage
|
||||||
|
required property var panel
|
||||||
|
|
||||||
|
width: parent.width
|
||||||
|
visible: stage.panel.stage === "list" && (stage.panel.broken.length > 0 || stage.panel.ignored.length > 0)
|
||||||
|
spacing: Style.space(4)
|
||||||
|
|
||||||
|
PanelSeparator { width: parent.width }
|
||||||
|
|
||||||
|
Text {
|
||||||
|
width: parent.width
|
||||||
|
visible: stage.panel.broken.length > 0
|
||||||
|
text: stage.panel.broken.length === 1
|
||||||
|
? "1 session cannot be resumed"
|
||||||
|
: stage.panel.broken.length + " sessions cannot be resumed"
|
||||||
|
color: stage.panel.urgentColor
|
||||||
|
wrapMode: Text.WordWrap
|
||||||
|
font.family: stage.panel.uiFont
|
||||||
|
font.pixelSize: Style.space(11)
|
||||||
|
font.bold: true
|
||||||
|
}
|
||||||
|
|
||||||
|
Repeater {
|
||||||
|
model: stage.panel.stage === "list" ? stage.panel.broken : []
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id: brokenRow
|
||||||
|
required property var modelData
|
||||||
|
width: stage.width
|
||||||
|
height: Style.space(26)
|
||||||
|
|
||||||
|
Column {
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.right: ignoreBtn.left
|
||||||
|
anchors.rightMargin: Style.space(6)
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
spacing: 0
|
||||||
|
|
||||||
|
Text {
|
||||||
|
width: parent.width
|
||||||
|
text: String(brokenRow.modelData.sessionId).substring(0, 8)
|
||||||
|
+ " · " + stage.panel.projectLabel(brokenRow.modelData.project)
|
||||||
|
color: Color.popups.text
|
||||||
|
elide: Text.ElideMiddle
|
||||||
|
font.family: stage.panel.uiFont
|
||||||
|
font.pixelSize: Style.space(10)
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
width: parent.width
|
||||||
|
text: brokenRow.modelData.dropLines + " turn lines after the last good message"
|
||||||
|
color: Color.muted
|
||||||
|
elide: Text.ElideRight
|
||||||
|
font.family: stage.panel.uiFont
|
||||||
|
font.pixelSize: Style.space(9)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
PillButton { fontFamily: stage.panel.uiFont;
|
||||||
|
id: ignoreBtn
|
||||||
|
anchors.right: repairBtn.left
|
||||||
|
anchors.rightMargin: Style.space(5)
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
label: "Ignore"
|
||||||
|
onTriggered: stage.panel.setIgnored(brokenRow.modelData, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
PillButton { fontFamily: stage.panel.uiFont;
|
||||||
|
id: repairBtn
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
label: "Repair"
|
||||||
|
onTriggered: stage.panel.askRepair(brokenRow.modelData)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Hidden: dismissed here, or broken but untouched for longer than
|
||||||
|
// the age limit. Collapsed, because the point of hiding them was that
|
||||||
|
// they stop taking up attention - but never gone without a trace.
|
||||||
|
Item {
|
||||||
|
width: parent.width
|
||||||
|
height: Style.space(20)
|
||||||
|
visible: stage.panel.ignored.length > 0
|
||||||
|
|
||||||
|
Text {
|
||||||
|
id: hiddenLabel
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
text: "hidden (" + stage.panel.ignored.length + ")"
|
||||||
|
color: hiddenArea.containsMouse ? Color.popups.text : Color.muted
|
||||||
|
font.family: stage.panel.uiFont
|
||||||
|
font.pixelSize: Style.space(10)
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
anchors.left: hiddenLabel.right
|
||||||
|
anchors.leftMargin: Style.space(4)
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
text: String.fromCodePoint(stage.panel.hiddenExpanded ? 0xF0140 : 0xF0142)
|
||||||
|
color: hiddenLabel.color
|
||||||
|
font.family: stage.panel.uiFont
|
||||||
|
font.pixelSize: Style.space(11)
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: hiddenArea
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onClicked: stage.panel.hiddenExpanded = !stage.panel.hiddenExpanded
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Column {
|
||||||
|
width: parent.width
|
||||||
|
visible: stage.panel.hiddenExpanded && stage.panel.ignored.length > 0
|
||||||
|
spacing: Style.space(3)
|
||||||
|
|
||||||
|
Text {
|
||||||
|
visible: stage.panel.dismissedList.length > 0
|
||||||
|
text: "Ignored (" + stage.panel.dismissedList.length + ")"
|
||||||
|
color: Color.popups.text
|
||||||
|
font.family: stage.panel.uiFont
|
||||||
|
font.pixelSize: Style.space(10)
|
||||||
|
font.bold: true
|
||||||
|
}
|
||||||
|
|
||||||
|
Repeater {
|
||||||
|
model: stage.panel.hiddenExpanded ? stage.panel.dismissedList.slice(0, stage.panel.hiddenCap) : []
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id: dismissedRow
|
||||||
|
required property var modelData
|
||||||
|
width: stage.width
|
||||||
|
height: Style.space(26)
|
||||||
|
|
||||||
|
Text {
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.right: restoreBtn.left
|
||||||
|
anchors.rightMargin: Style.space(6)
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
text: String(dismissedRow.modelData.sessionId).substring(0, 8)
|
||||||
|
+ " · " + stage.panel.projectLabel(dismissedRow.modelData.project)
|
||||||
|
color: Color.muted
|
||||||
|
elide: Text.ElideMiddle
|
||||||
|
font.family: stage.panel.uiFont
|
||||||
|
font.pixelSize: Style.space(10)
|
||||||
|
}
|
||||||
|
|
||||||
|
PillButton { fontFamily: stage.panel.uiFont;
|
||||||
|
id: restoreBtn
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
label: "Restore"
|
||||||
|
onTriggered: stage.panel.setIgnored(dismissedRow.modelData, false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
visible: stage.panel.dismissedList.length > stage.panel.hiddenCap
|
||||||
|
text: "… and " + (stage.panel.dismissedList.length - stage.panel.hiddenCap)
|
||||||
|
+ " more · claude-mode repair-session --ignored"
|
||||||
|
color: Color.muted
|
||||||
|
font.family: stage.panel.uiFont
|
||||||
|
font.pixelSize: Style.space(9)
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
visible: stage.panel.staleList.length > 0
|
||||||
|
text: "Older than " + stage.panel.ageDays + " days (" + stage.panel.staleList.length + ")"
|
||||||
|
color: Color.popups.text
|
||||||
|
font.family: stage.panel.uiFont
|
||||||
|
font.pixelSize: Style.space(10)
|
||||||
|
font.bold: true
|
||||||
|
}
|
||||||
|
|
||||||
|
Repeater {
|
||||||
|
model: stage.panel.hiddenExpanded ? stage.panel.staleList.slice(0, stage.panel.hiddenCap) : []
|
||||||
|
|
||||||
|
Text {
|
||||||
|
required property var modelData
|
||||||
|
width: stage.width
|
||||||
|
text: String(modelData.sessionId).substring(0, 8)
|
||||||
|
+ " · " + Math.floor((Date.now() / 1000 - Number(modelData.mtime)) / 86400) + "d"
|
||||||
|
+ " · " + stage.panel.projectLabel(modelData.project)
|
||||||
|
color: Color.muted
|
||||||
|
elide: Text.ElideMiddle
|
||||||
|
font.family: stage.panel.uiFont
|
||||||
|
font.pixelSize: Style.space(10)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
width: parent.width
|
||||||
|
visible: stage.panel.staleList.length > 0
|
||||||
|
text: (stage.panel.staleList.length > stage.panel.hiddenCap
|
||||||
|
? "… and " + (stage.panel.staleList.length - stage.panel.hiddenCap) + " more. " : "")
|
||||||
|
+ "Not counted because nothing has touched them since. "
|
||||||
|
+ "claude-mode repair-session --all --max-age 0 lists them."
|
||||||
|
color: Color.muted
|
||||||
|
opacity: 0.85
|
||||||
|
wrapMode: Text.WordWrap
|
||||||
|
font.family: stage.panel.uiFont
|
||||||
|
font.pixelSize: Style.space(9)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
import QtQuick
|
||||||
|
import qs.Commons
|
||||||
|
import qs.Ui
|
||||||
|
|
||||||
|
// DeleteStage.qml - Delete: confirmed, and says so when it leaves a provider empty.
|
||||||
|
//
|
||||||
|
// One stage of the claude-mode panel. Panel.qml hands itself in as `panel`
|
||||||
|
// and owns all the state and actions; this file only draws them.
|
||||||
|
Column {
|
||||||
|
id: stage
|
||||||
|
required property var panel
|
||||||
|
|
||||||
|
width: parent.width
|
||||||
|
visible: stage.panel.stage === "presetDelete"
|
||||||
|
spacing: Style.space(7)
|
||||||
|
|
||||||
|
readonly property bool lastOne: stage.panel.stage === "presetDelete"
|
||||||
|
&& stage.panel.presetsOf(stage.panel.editProvider).length <= 1
|
||||||
|
|
||||||
|
Text {
|
||||||
|
width: parent.width
|
||||||
|
text: "Delete '" + stage.panel.editPreset + "'?"
|
||||||
|
color: Color.popups.text
|
||||||
|
elide: Text.ElideRight
|
||||||
|
font.family: stage.panel.uiFont
|
||||||
|
font.pixelSize: Style.space(13)
|
||||||
|
font.bold: true
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
width: parent.width
|
||||||
|
text: "The preset file is removed, and there is no undo from here. Its key stays in the vault."
|
||||||
|
color: Color.muted
|
||||||
|
wrapMode: Text.WordWrap
|
||||||
|
font.family: stage.panel.uiFont
|
||||||
|
font.pixelSize: Style.space(10)
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
width: parent.width
|
||||||
|
visible: parent.lastOne
|
||||||
|
text: "It is the only " + stage.panel.pTitle(stage.panel.editProvider) + " preset, so "
|
||||||
|
+ stage.panel.pTitle(stage.panel.editProvider) + " will have nothing to switch to until you create another."
|
||||||
|
color: stage.panel.urgentColor
|
||||||
|
wrapMode: Text.WordWrap
|
||||||
|
font.family: stage.panel.uiFont
|
||||||
|
font.pixelSize: Style.space(10)
|
||||||
|
}
|
||||||
|
|
||||||
|
Flow {
|
||||||
|
width: parent.width
|
||||||
|
spacing: Style.space(7)
|
||||||
|
|
||||||
|
PillButton { fontFamily: stage.panel.uiFont; label: "Delete it"; primary: true; onTriggered: stage.panel.deletePreset() }
|
||||||
|
PillButton { fontFamily: stage.panel.uiFont; label: "Back"; onTriggered: { stage.panel.lastError = ""; stage.panel.stage = "preset" } }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,276 @@
|
|||||||
|
import QtQuick
|
||||||
|
import qs.Commons
|
||||||
|
import qs.Ui
|
||||||
|
import "Modes.js" as Modes
|
||||||
|
|
||||||
|
// ModeList.qml - the modes to switch to, each unfolding its presets.
|
||||||
|
//
|
||||||
|
// One stage of the claude-mode panel. Panel.qml hands itself in as `panel`
|
||||||
|
// and owns all the state and actions; this file only draws them.
|
||||||
|
Column {
|
||||||
|
id: stage
|
||||||
|
required property var panel
|
||||||
|
|
||||||
|
width: parent.width
|
||||||
|
visible: stage.panel.stage === "list"
|
||||||
|
spacing: Style.space(10)
|
||||||
|
|
||||||
|
PanelSectionHeader {
|
||||||
|
text: "SWITCH TO"
|
||||||
|
visible: stage.panel.stage === "list"
|
||||||
|
foreground: Color.popups.text
|
||||||
|
fontFamily: stage.panel.uiFont
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- The modes, current one marked rather than hidden. The CLI omits
|
||||||
|
// the active mode because a list you arrow through should not offer a
|
||||||
|
// no-op; here the list is also the status display, so it stays.
|
||||||
|
Column {
|
||||||
|
width: parent.width
|
||||||
|
visible: stage.panel.stage === "list"
|
||||||
|
spacing: Style.space(1)
|
||||||
|
|
||||||
|
Repeater {
|
||||||
|
model: stage.panel.stage === "list" ? stage.panel.modeOrder : []
|
||||||
|
|
||||||
|
Column {
|
||||||
|
id: modeEntry
|
||||||
|
required property var modelData
|
||||||
|
width: stage.width
|
||||||
|
|
||||||
|
readonly property string thisMode: String(modelData)
|
||||||
|
readonly property bool isCurrent: thisMode === stage.panel.mode
|
||||||
|
readonly property bool isExpanded: stage.panel.expandedMode === thisMode
|
||||||
|
readonly property var presetList: Modes.presetsFor(stage.panel.health, thisMode, stage.panel.defaultPresets)
|
||||||
|
|
||||||
|
Item {
|
||||||
|
width: parent.width
|
||||||
|
height: Style.space(30)
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.leftMargin: -Style.space(6)
|
||||||
|
anchors.rightMargin: -Style.space(6)
|
||||||
|
radius: Style.space(4)
|
||||||
|
color: modeEntry.isCurrent
|
||||||
|
? Qt.rgba(Color.accent.r, Color.accent.g, Color.accent.b, 0.12)
|
||||||
|
: (modeHover.containsMouse
|
||||||
|
? Qt.rgba(Color.popups.text.r, Color.popups.text.g, Color.popups.text.b, 0.07)
|
||||||
|
: "transparent")
|
||||||
|
}
|
||||||
|
|
||||||
|
BrandIcon {
|
||||||
|
id: modeGlyph
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.leftMargin: Style.space(3)
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
pathData: stage.panel.pLogo(modeEntry.thisMode)
|
||||||
|
opticalScale: stage.panel.pLogoScale(modeEntry.thisMode)
|
||||||
|
color: modeEntry.isCurrent ? Color.accent : Color.popups.text
|
||||||
|
iconSize: Style.space(16)
|
||||||
|
}
|
||||||
|
|
||||||
|
Column {
|
||||||
|
anchors.left: modeGlyph.right
|
||||||
|
anchors.leftMargin: Style.space(8)
|
||||||
|
anchors.right: modeMark.left
|
||||||
|
anchors.rightMargin: Style.space(6)
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
spacing: 0
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: stage.panel.pTitle(modeEntry.thisMode)
|
||||||
|
color: Color.popups.text
|
||||||
|
font.family: stage.panel.uiFont
|
||||||
|
font.pixelSize: Style.font.body
|
||||||
|
font.bold: modeEntry.isCurrent
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
width: parent.width
|
||||||
|
text: stage.panel.pBlurb(modeEntry.thisMode)
|
||||||
|
color: Color.muted
|
||||||
|
elide: Text.ElideRight
|
||||||
|
font.family: stage.panel.uiFont
|
||||||
|
font.pixelSize: Style.space(10)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Checkmark on the active mode; a chevron on a gateway row that
|
||||||
|
// has a preset list waiting behind it.
|
||||||
|
Text {
|
||||||
|
id: modeMark
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
text: modeEntry.isCurrent
|
||||||
|
? String.fromCodePoint(0xF012C)
|
||||||
|
: (Modes.needsPreset(modeEntry.thisMode)
|
||||||
|
? String.fromCodePoint(modeEntry.isExpanded ? 0xF0140 : 0xF0142)
|
||||||
|
: "")
|
||||||
|
color: modeEntry.isCurrent ? Color.accent : Color.muted
|
||||||
|
font.family: stage.panel.uiFont
|
||||||
|
font.pixelSize: Style.font.body
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: modeHover
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
enabled: !stage.panel.busy
|
||||||
|
onClicked: stage.panel.activate(modeEntry.thisMode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Presets for this provider, unfolded in place. Shown even
|
||||||
|
// with none left, so "New preset…" is still there to click.
|
||||||
|
Column {
|
||||||
|
width: parent.width
|
||||||
|
visible: modeEntry.isExpanded
|
||||||
|
spacing: Style.space(1)
|
||||||
|
|
||||||
|
Repeater {
|
||||||
|
model: modeEntry.isExpanded ? modeEntry.presetList : []
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id: presetRow
|
||||||
|
required property var modelData
|
||||||
|
width: modeEntry.width
|
||||||
|
height: Style.space(26)
|
||||||
|
|
||||||
|
readonly property bool isActive: modeEntry.isCurrent && String(modelData.name) === stage.panel.preset
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.leftMargin: Style.space(20)
|
||||||
|
anchors.rightMargin: -Style.space(6)
|
||||||
|
radius: Style.space(4)
|
||||||
|
color: presetHover.containsMouse
|
||||||
|
? Qt.rgba(Color.accent.r, Color.accent.g, Color.accent.b, 0.14)
|
||||||
|
: "transparent"
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
id: presetName
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.leftMargin: Style.space(30)
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
text: presetRow.modelData.name
|
||||||
|
color: presetRow.isActive ? Color.accent : Color.popups.text
|
||||||
|
font.family: stage.panel.uiFont
|
||||||
|
font.pixelSize: Style.space(12)
|
||||||
|
font.bold: presetRow.isActive
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only worth saying when there is a choice to tell apart.
|
||||||
|
Text {
|
||||||
|
id: defaultTag
|
||||||
|
visible: modeEntry.presetList.length > 1
|
||||||
|
&& String(presetRow.modelData.name) === stage.panel.defaultPresets[modeEntry.thisMode]
|
||||||
|
anchors.left: presetName.right
|
||||||
|
anchors.leftMargin: Style.space(6)
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
text: "default"
|
||||||
|
color: Color.muted
|
||||||
|
font.family: stage.panel.uiFont
|
||||||
|
font.pixelSize: Style.space(9)
|
||||||
|
}
|
||||||
|
|
||||||
|
// The opus mapping is the one that tells you what you are
|
||||||
|
// about to be talking to; the rest is in the tooltip.
|
||||||
|
Text {
|
||||||
|
anchors.left: defaultTag.visible ? defaultTag.right : presetName.right
|
||||||
|
anchors.leftMargin: Style.space(8)
|
||||||
|
anchors.right: gearButton.visible ? gearButton.left : parent.right
|
||||||
|
anchors.rightMargin: Style.space(4)
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
horizontalAlignment: Text.AlignRight
|
||||||
|
text: presetRow.modelData.models && presetRow.modelData.models.opus
|
||||||
|
? String(presetRow.modelData.models.opus)
|
||||||
|
: ""
|
||||||
|
color: Color.muted
|
||||||
|
elide: Text.ElideLeft
|
||||||
|
font.family: stage.panel.uiFont
|
||||||
|
font.pixelSize: Style.space(10)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clicking the row switches; the gear opens that preset in the
|
||||||
|
// editor instead. Per row rather than per provider, because
|
||||||
|
// two presets of one provider can map tiers differently - and
|
||||||
|
// two LM Studio presets can sit on two different machines.
|
||||||
|
MouseArea {
|
||||||
|
id: presetHover
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.rightMargin: gearButton.visible ? gearButton.width : 0
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
enabled: !stage.panel.busy
|
||||||
|
onClicked: stage.panel.switchTo(modeEntry.thisMode, String(presetRow.modelData.name))
|
||||||
|
}
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id: gearButton
|
||||||
|
visible: true
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
width: Style.space(22)
|
||||||
|
height: parent.height
|
||||||
|
|
||||||
|
Text {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
text: String.fromCodePoint(0xF0493)
|
||||||
|
color: gearArea.containsMouse ? Color.accent : Color.muted
|
||||||
|
font.family: stage.panel.uiFont
|
||||||
|
font.pixelSize: Style.space(12)
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: gearArea
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
enabled: !stage.panel.busy
|
||||||
|
onClicked: stage.panel.openPresetEditor(String(presetRow.modelData.name))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Item {
|
||||||
|
width: modeEntry.width
|
||||||
|
height: Style.space(24)
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.leftMargin: Style.space(20)
|
||||||
|
anchors.rightMargin: -Style.space(6)
|
||||||
|
radius: Style.space(4)
|
||||||
|
color: newPresetArea.containsMouse
|
||||||
|
? Qt.rgba(Color.accent.r, Color.accent.g, Color.accent.b, 0.14)
|
||||||
|
: "transparent"
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.leftMargin: Style.space(30)
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
text: "New " + stage.panel.pTitle(modeEntry.thisMode) + " preset…"
|
||||||
|
color: newPresetArea.containsMouse ? Color.accent : Color.muted
|
||||||
|
font.family: stage.panel.uiFont
|
||||||
|
font.pixelSize: Style.space(11)
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: newPresetArea
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
enabled: !stage.panel.busy
|
||||||
|
onClicked: stage.panel.openNewPreset(modeEntry.thisMode, null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
import QtQuick
|
||||||
|
import qs.Commons
|
||||||
|
import qs.Ui
|
||||||
|
|
||||||
|
// ModelMap.qml - the active preset's tier-to-model map, behind a gateway.
|
||||||
|
//
|
||||||
|
// One stage of the claude-mode panel. Panel.qml hands itself in as `panel`
|
||||||
|
// and owns all the state and actions; this file only draws them.
|
||||||
|
Column {
|
||||||
|
id: stage
|
||||||
|
required property var panel
|
||||||
|
|
||||||
|
width: parent.width
|
||||||
|
visible: stage.panel.modelRows.length > 0 && stage.panel.stage === "list"
|
||||||
|
spacing: Style.space(10)
|
||||||
|
|
||||||
|
PanelSeparator { width: parent.width; visible: stage.panel.modelRows.length > 0 && stage.panel.stage === "list" }
|
||||||
|
|
||||||
|
Column {
|
||||||
|
width: parent.width
|
||||||
|
spacing: Style.space(1)
|
||||||
|
visible: stage.panel.modelRows.length > 0 && stage.panel.stage === "list"
|
||||||
|
|
||||||
|
PanelSectionHeader {
|
||||||
|
text: "MODEL MAP"
|
||||||
|
foreground: Color.popups.text
|
||||||
|
fontFamily: stage.panel.uiFont
|
||||||
|
}
|
||||||
|
|
||||||
|
Repeater {
|
||||||
|
model: stage.panel.modelRows
|
||||||
|
|
||||||
|
Item {
|
||||||
|
required property var modelData
|
||||||
|
width: stage.width
|
||||||
|
height: Style.space(19)
|
||||||
|
|
||||||
|
Text {
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
text: parent.modelData.tier
|
||||||
|
color: Color.muted
|
||||||
|
font.family: stage.panel.uiFont
|
||||||
|
font.pixelSize: Style.space(11)
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.leftMargin: Style.space(54)
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
horizontalAlignment: Text.AlignRight
|
||||||
|
text: parent.modelData.id
|
||||||
|
color: Color.popups.text
|
||||||
|
elide: Text.ElideLeft
|
||||||
|
font.family: stage.panel.uiFont
|
||||||
|
font.pixelSize: Style.space(11)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,115 @@
|
|||||||
|
import QtQuick
|
||||||
|
import qs.Commons
|
||||||
|
import qs.Ui
|
||||||
|
|
||||||
|
// NewPresetStage.qml - New preset: a name, and what to start from.
|
||||||
|
//
|
||||||
|
// One stage of the claude-mode panel. Panel.qml hands itself in as `panel`
|
||||||
|
// and owns all the state and actions; this file only draws them.
|
||||||
|
Column {
|
||||||
|
id: stage
|
||||||
|
required property var panel
|
||||||
|
|
||||||
|
width: parent.width
|
||||||
|
visible: stage.panel.stage === "presetNew"
|
||||||
|
spacing: Style.space(7)
|
||||||
|
|
||||||
|
Text {
|
||||||
|
width: parent.width
|
||||||
|
text: "New " + stage.panel.pTitle(stage.panel.newProvider) + " preset"
|
||||||
|
color: Color.popups.text
|
||||||
|
elide: Text.ElideRight
|
||||||
|
font.family: stage.panel.uiFont
|
||||||
|
font.pixelSize: Style.space(13)
|
||||||
|
font.bold: true
|
||||||
|
}
|
||||||
|
|
||||||
|
TextField {
|
||||||
|
id: newNameField
|
||||||
|
width: parent.width
|
||||||
|
text: stage.panel.newName
|
||||||
|
placeholderText: "name"
|
||||||
|
foreground: Color.popups.text
|
||||||
|
font.family: stage.panel.uiFont
|
||||||
|
font.pixelSize: Style.space(11)
|
||||||
|
onTextChanged: stage.panel.newName = text
|
||||||
|
onAccepted: stage.panel.createPreset()
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
width: parent.width
|
||||||
|
readonly property string problem: stage.panel.stage === "presetNew" ? stage.panel.nameProblem(stage.panel.newName, "") : ""
|
||||||
|
visible: problem !== ""
|
||||||
|
text: problem
|
||||||
|
color: stage.panel.urgentColor
|
||||||
|
wrapMode: Text.WordWrap
|
||||||
|
font.family: stage.panel.uiFont
|
||||||
|
font.pixelSize: Style.space(10)
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: "Start from"
|
||||||
|
color: Color.muted
|
||||||
|
font.family: stage.panel.uiFont
|
||||||
|
font.pixelSize: Style.space(10)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only this provider's presets: a copy keeps the provider, and a
|
||||||
|
// preset cannot change provider afterwards.
|
||||||
|
Flow {
|
||||||
|
width: parent.width
|
||||||
|
spacing: Style.space(6)
|
||||||
|
|
||||||
|
Repeater {
|
||||||
|
model: stage.panel.stage === "presetNew" ? stage.panel.presetsOf(stage.panel.newProvider) : []
|
||||||
|
|
||||||
|
PillButton { fontFamily: stage.panel.uiFont;
|
||||||
|
required property var modelData
|
||||||
|
label: "copy of " + String(modelData.name)
|
||||||
|
primary: stage.panel.newFrom === String(modelData.name)
|
||||||
|
onTriggered: stage.panel.newFrom = String(modelData.name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
PillButton { fontFamily: stage.panel.uiFont;
|
||||||
|
label: "blank"
|
||||||
|
primary: stage.panel.newFrom === ""
|
||||||
|
onTriggered: stage.panel.newFrom = ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
id: newFromHint
|
||||||
|
width: parent.width
|
||||||
|
text: stage.panel.newFrom === ""
|
||||||
|
? "Every tier starts empty; the editor opens next to fill them in."
|
||||||
|
: "Same server, key and models as '" + stage.panel.newFrom + "'. The editor opens next to change them."
|
||||||
|
color: Color.muted
|
||||||
|
wrapMode: Text.WordWrap
|
||||||
|
font.family: stage.panel.uiFont
|
||||||
|
font.pixelSize: Style.space(10)
|
||||||
|
}
|
||||||
|
|
||||||
|
Flow {
|
||||||
|
width: parent.width
|
||||||
|
spacing: Style.space(7)
|
||||||
|
|
||||||
|
PillButton { fontFamily: stage.panel.uiFont; label: "Create"; primary: true; onTriggered: stage.panel.createPreset() }
|
||||||
|
PillButton { fontFamily: stage.panel.uiFont;
|
||||||
|
label: "Cancel"
|
||||||
|
onTriggered: {
|
||||||
|
stage.panel.lastError = ""
|
||||||
|
if (stage.panel.editPreset !== "") stage.panel.stage = "preset"
|
||||||
|
else stage.panel.resetFlow()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Connections {
|
||||||
|
target: stage.panel
|
||||||
|
function onNewPresetOpened() {
|
||||||
|
newNameField.text = stage.panel.newName
|
||||||
|
Qt.callLater(function () { newNameField.forceActiveFocus(); newNameField.selectAll() })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,44 @@
|
|||||||
|
import QtQuick
|
||||||
|
import qs.Commons
|
||||||
|
|
||||||
|
// PillButton.qml - the panel's button: a bordered pill, filled when primary.
|
||||||
|
// `enabled: false` greys it out.
|
||||||
|
Rectangle {
|
||||||
|
id: pb
|
||||||
|
property string label: ""
|
||||||
|
property bool primary: false
|
||||||
|
// The bar's font; this file cannot see the panel, so it is handed in.
|
||||||
|
property string fontFamily: Style.font.family
|
||||||
|
signal triggered()
|
||||||
|
|
||||||
|
implicitWidth: pbText.implicitWidth + Style.space(20)
|
||||||
|
implicitHeight: Style.space(25)
|
||||||
|
radius: Style.space(4)
|
||||||
|
// `enabled: false` greys it out; the MouseArea inherits the flag.
|
||||||
|
opacity: enabled ? 1.0 : 0.4
|
||||||
|
|
||||||
|
readonly property color tint: primary ? Color.accent : Color.popups.text
|
||||||
|
color: pbArea.containsMouse
|
||||||
|
? Qt.rgba(tint.r, tint.g, tint.b, primary ? 0.30 : 0.14)
|
||||||
|
: Qt.rgba(tint.r, tint.g, tint.b, primary ? 0.16 : 0.00)
|
||||||
|
border.width: 1
|
||||||
|
border.color: Qt.rgba(tint.r, tint.g, tint.b, primary ? 0.75 : 0.30)
|
||||||
|
|
||||||
|
Text {
|
||||||
|
id: pbText
|
||||||
|
anchors.centerIn: parent
|
||||||
|
text: pb.label
|
||||||
|
color: Color.popups.text
|
||||||
|
font.family: pb.fontFamily
|
||||||
|
font.pixelSize: Style.space(11)
|
||||||
|
font.bold: pb.primary
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: pbArea
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onClicked: pb.triggered()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,162 @@
|
|||||||
|
import QtQuick
|
||||||
|
import qs.Commons
|
||||||
|
import qs.Ui
|
||||||
|
|
||||||
|
// PresetStage.qml - One preset: its tier map, each row opening a picker.
|
||||||
|
//
|
||||||
|
// One stage of the claude-mode panel. Panel.qml hands itself in as `panel`
|
||||||
|
// and owns all the state and actions; this file only draws them.
|
||||||
|
Column {
|
||||||
|
id: stage
|
||||||
|
required property var panel
|
||||||
|
|
||||||
|
width: parent.width
|
||||||
|
visible: stage.panel.stage === "preset" && stage.panel.editPreset !== ""
|
||||||
|
spacing: Style.space(6)
|
||||||
|
|
||||||
|
Text {
|
||||||
|
width: parent.width
|
||||||
|
text: "'" + stage.panel.editPreset + "' · " + stage.panel.pTitle(stage.panel.editProvider)
|
||||||
|
color: Color.popups.text
|
||||||
|
elide: Text.ElideRight
|
||||||
|
font.family: stage.panel.uiFont
|
||||||
|
font.pixelSize: Style.space(13)
|
||||||
|
font.bold: true
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
width: parent.width
|
||||||
|
visible: text !== ""
|
||||||
|
text: stage.panel.editEntry && stage.panel.editEntry.description ? String(stage.panel.editEntry.description) : ""
|
||||||
|
color: Color.muted
|
||||||
|
wrapMode: Text.WordWrap
|
||||||
|
font.family: stage.panel.uiFont
|
||||||
|
font.pixelSize: Style.space(10)
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
width: parent.width
|
||||||
|
visible: stage.panel.editIsActive
|
||||||
|
text: "In use now. A change is applied straight away and reaches sessions started after it."
|
||||||
|
color: Color.accent
|
||||||
|
wrapMode: Text.WordWrap
|
||||||
|
font.family: stage.panel.uiFont
|
||||||
|
font.pixelSize: Style.space(10)
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
width: parent.width
|
||||||
|
visible: stage.panel.editIsDefault
|
||||||
|
text: "claude-mode " + stage.panel.editProvider + " picks this one when no preset is named"
|
||||||
|
+ (stage.panel.editIsChosenDefault ? "." : " (the built-in choice).")
|
||||||
|
color: Color.muted
|
||||||
|
wrapMode: Text.WordWrap
|
||||||
|
font.family: stage.panel.uiFont
|
||||||
|
font.pixelSize: Style.space(10)
|
||||||
|
}
|
||||||
|
|
||||||
|
Column {
|
||||||
|
width: parent.width
|
||||||
|
spacing: Style.space(1)
|
||||||
|
|
||||||
|
Repeater {
|
||||||
|
model: stage.panel.stage === "preset" ? stage.panel.tierNames : []
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id: tierRow
|
||||||
|
required property var modelData
|
||||||
|
readonly property string tier: String(modelData)
|
||||||
|
readonly property string current: stage.panel.editCurrent(tier)
|
||||||
|
width: stage.width
|
||||||
|
height: Style.space(26)
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.leftMargin: -Style.space(6)
|
||||||
|
anchors.rightMargin: -Style.space(6)
|
||||||
|
radius: Style.space(4)
|
||||||
|
color: tierArea.containsMouse
|
||||||
|
? Qt.rgba(Color.accent.r, Color.accent.g, Color.accent.b, 0.14)
|
||||||
|
: "transparent"
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
text: tierRow.tier
|
||||||
|
color: Color.muted
|
||||||
|
font.family: stage.panel.uiFont
|
||||||
|
font.pixelSize: Style.space(11)
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.leftMargin: Style.space(54)
|
||||||
|
anchors.right: tierChevron.left
|
||||||
|
anchors.rightMargin: Style.space(6)
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
horizontalAlignment: Text.AlignRight
|
||||||
|
text: tierRow.current !== "" ? tierRow.current : "not set"
|
||||||
|
color: tierRow.current !== "" ? Color.popups.text : Color.muted
|
||||||
|
elide: Text.ElideLeft
|
||||||
|
font.family: stage.panel.uiFont
|
||||||
|
font.pixelSize: Style.space(11)
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
id: tierChevron
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
text: String.fromCodePoint(0xF0142)
|
||||||
|
color: tierArea.containsMouse ? Color.accent : Color.muted
|
||||||
|
font.family: stage.panel.uiFont
|
||||||
|
font.pixelSize: Style.font.body
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: tierArea
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
enabled: !stage.panel.busy
|
||||||
|
onClicked: stage.panel.openTierEditor(tierRow.tier)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Flow, not Row: these can total more than the card is wide, and a
|
||||||
|
// Row lays them straight past its right edge instead of wrapping.
|
||||||
|
Flow {
|
||||||
|
width: parent.width
|
||||||
|
spacing: Style.space(7)
|
||||||
|
|
||||||
|
PillButton { fontFamily: stage.panel.uiFont;
|
||||||
|
label: "Server settings…"
|
||||||
|
visible: stage.panel.serverEditable(stage.panel.editProvider)
|
||||||
|
onTriggered: stage.panel.openServerSettings(stage.panel.editPreset, "preset")
|
||||||
|
}
|
||||||
|
PillButton { fontFamily: stage.panel.uiFont; label: "Make default"; visible: !stage.panel.editIsDefault; onTriggered: stage.panel.setDefault(true) }
|
||||||
|
PillButton { fontFamily: stage.panel.uiFont; label: "Clear default"; visible: stage.panel.editIsChosenDefault; onTriggered: stage.panel.setDefault(false) }
|
||||||
|
PillButton { fontFamily: stage.panel.uiFont; label: "Duplicate…"; onTriggered: stage.panel.openNewPreset(stage.panel.editProvider, stage.panel.editPreset) }
|
||||||
|
PillButton { fontFamily: stage.panel.uiFont; label: "Rename…"; onTriggered: stage.panel.openRename() }
|
||||||
|
// The CLI refuses to delete the preset in use. A button that always
|
||||||
|
// fails would be worse than one that says why it is off.
|
||||||
|
PillButton { fontFamily: stage.panel.uiFont;
|
||||||
|
label: "Delete…"
|
||||||
|
enabled: !stage.panel.editIsActive
|
||||||
|
onTriggered: { stage.panel.lastError = ""; stage.panel.stage = "presetDelete" }
|
||||||
|
}
|
||||||
|
PillButton { fontFamily: stage.panel.uiFont; label: "Done"; primary: true; onTriggered: stage.panel.resetFlow() }
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
width: parent.width
|
||||||
|
visible: stage.panel.editIsActive
|
||||||
|
text: "In use, so it cannot be deleted. Switch to another preset first."
|
||||||
|
color: Color.muted
|
||||||
|
wrapMode: Text.WordWrap
|
||||||
|
font.family: stage.panel.uiFont
|
||||||
|
font.pixelSize: Style.space(9)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
import QtQuick
|
||||||
|
import qs.Commons
|
||||||
|
import qs.Ui
|
||||||
|
|
||||||
|
// RenameStage.qml - Rename: the file moves, and state.json follows if it is in use.
|
||||||
|
//
|
||||||
|
// One stage of the claude-mode panel. Panel.qml hands itself in as `panel`
|
||||||
|
// and owns all the state and actions; this file only draws them.
|
||||||
|
Column {
|
||||||
|
id: stage
|
||||||
|
required property var panel
|
||||||
|
|
||||||
|
width: parent.width
|
||||||
|
visible: stage.panel.stage === "presetRename"
|
||||||
|
spacing: Style.space(7)
|
||||||
|
|
||||||
|
Text {
|
||||||
|
width: parent.width
|
||||||
|
text: "Rename '" + stage.panel.editPreset + "'"
|
||||||
|
color: Color.popups.text
|
||||||
|
elide: Text.ElideRight
|
||||||
|
font.family: stage.panel.uiFont
|
||||||
|
font.pixelSize: Style.space(13)
|
||||||
|
font.bold: true
|
||||||
|
}
|
||||||
|
|
||||||
|
TextField {
|
||||||
|
id: renameField
|
||||||
|
width: parent.width
|
||||||
|
text: stage.panel.renameTo
|
||||||
|
placeholderText: "new name"
|
||||||
|
foreground: Color.popups.text
|
||||||
|
font.family: stage.panel.uiFont
|
||||||
|
font.pixelSize: Style.space(11)
|
||||||
|
onTextChanged: stage.panel.renameTo = text
|
||||||
|
onAccepted: stage.panel.renamePreset()
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
width: parent.width
|
||||||
|
readonly property string problem: stage.panel.stage === "presetRename" ? stage.panel.nameProblem(stage.panel.renameTo, stage.panel.editPreset) : ""
|
||||||
|
visible: problem !== ""
|
||||||
|
text: problem
|
||||||
|
color: stage.panel.urgentColor
|
||||||
|
wrapMode: Text.WordWrap
|
||||||
|
font.family: stage.panel.uiFont
|
||||||
|
font.pixelSize: Style.space(10)
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
width: parent.width
|
||||||
|
visible: stage.panel.editIsActive
|
||||||
|
text: "It is the preset in use. The switch follows the new name, and running sessions keep working."
|
||||||
|
color: Color.muted
|
||||||
|
wrapMode: Text.WordWrap
|
||||||
|
font.family: stage.panel.uiFont
|
||||||
|
font.pixelSize: Style.space(10)
|
||||||
|
}
|
||||||
|
|
||||||
|
Flow {
|
||||||
|
width: parent.width
|
||||||
|
spacing: Style.space(7)
|
||||||
|
|
||||||
|
PillButton { fontFamily: stage.panel.uiFont; label: "Rename"; primary: true; onTriggered: stage.panel.renamePreset() }
|
||||||
|
PillButton { fontFamily: stage.panel.uiFont; label: "Back"; onTriggered: { stage.panel.lastError = ""; stage.panel.stage = "preset" } }
|
||||||
|
}
|
||||||
|
|
||||||
|
Connections {
|
||||||
|
target: stage.panel
|
||||||
|
function onRenameOpened() {
|
||||||
|
renameField.text = stage.panel.renameTo
|
||||||
|
Qt.callLater(function () { renameField.forceActiveFocus(); renameField.selectAll() })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
import QtQuick
|
||||||
|
import qs.Commons
|
||||||
|
import qs.Ui
|
||||||
|
|
||||||
|
// RepairStage.qml - Confirming one repair.
|
||||||
|
//
|
||||||
|
// One stage of the claude-mode panel. Panel.qml hands itself in as `panel`
|
||||||
|
// and owns all the state and actions; this file only draws them.
|
||||||
|
Column {
|
||||||
|
id: stage
|
||||||
|
required property var panel
|
||||||
|
|
||||||
|
width: parent.width
|
||||||
|
visible: stage.panel.stage === "repair" && stage.panel.repairTarget !== null
|
||||||
|
spacing: Style.space(7)
|
||||||
|
|
||||||
|
Text {
|
||||||
|
width: parent.width
|
||||||
|
text: "Repair " + (stage.panel.repairTarget ? String(stage.panel.repairTarget.sessionId).substring(0, 8) : "")
|
||||||
|
color: Color.popups.text
|
||||||
|
font.family: stage.panel.uiFont
|
||||||
|
font.pixelSize: Style.space(13)
|
||||||
|
font.bold: true
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
width: parent.width
|
||||||
|
text: {
|
||||||
|
if (!stage.panel.repairTarget) return ""
|
||||||
|
var p = (stage.panel.repairTarget.providers || []).join(", ")
|
||||||
|
return "This session took " + (p !== "" ? p + "'s" : "another provider's")
|
||||||
|
+ " output while the mode was switched under it, and Anthropic will not "
|
||||||
|
+ "resume it. Rolling it back to its last good message drops "
|
||||||
|
+ stage.panel.repairTarget.dropLines + " lines."
|
||||||
|
}
|
||||||
|
color: Color.muted
|
||||||
|
wrapMode: Text.WordWrap
|
||||||
|
lineHeight: 1.2
|
||||||
|
font.family: stage.panel.uiFont
|
||||||
|
font.pixelSize: Style.space(10)
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
width: parent.width
|
||||||
|
text: "The original is backed up, the dropped turns are saved as Markdown, "
|
||||||
|
+ "and handed back to the session so it still knows what it did."
|
||||||
|
color: Color.muted
|
||||||
|
opacity: 0.85
|
||||||
|
wrapMode: Text.WordWrap
|
||||||
|
lineHeight: 1.2
|
||||||
|
font.family: stage.panel.uiFont
|
||||||
|
font.pixelSize: Style.space(10)
|
||||||
|
}
|
||||||
|
|
||||||
|
Flow {
|
||||||
|
width: parent.width
|
||||||
|
spacing: Style.space(7)
|
||||||
|
|
||||||
|
PillButton { fontFamily: stage.panel.uiFont; label: "Repair it"; primary: true; onTriggered: stage.panel.doRepair() }
|
||||||
|
PillButton { fontFamily: stage.panel.uiFont; label: "Cancel"; onTriggered: stage.panel.resetFlow() }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,132 @@
|
|||||||
|
import QtQuick
|
||||||
|
import qs.Commons
|
||||||
|
import qs.Ui
|
||||||
|
|
||||||
|
// ServerStage.qml - Where a server provider is, and whether it needs a key.
|
||||||
|
//
|
||||||
|
// One stage of the claude-mode panel. Panel.qml hands itself in as `panel`
|
||||||
|
// and owns all the state and actions; this file only draws them.
|
||||||
|
Column {
|
||||||
|
id: stage
|
||||||
|
required property var panel
|
||||||
|
|
||||||
|
width: parent.width
|
||||||
|
visible: stage.panel.stage === "server"
|
||||||
|
spacing: Style.space(8)
|
||||||
|
|
||||||
|
Text {
|
||||||
|
width: parent.width
|
||||||
|
text: "Server for '" + stage.panel.serverPreset + "'"
|
||||||
|
color: Color.popups.text
|
||||||
|
elide: Text.ElideRight
|
||||||
|
font.family: stage.panel.uiFont
|
||||||
|
font.pixelSize: Style.space(13)
|
||||||
|
font.bold: true
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
width: parent.width
|
||||||
|
text: stage.panel.serverHint
|
||||||
|
color: Color.muted
|
||||||
|
wrapMode: Text.WordWrap
|
||||||
|
lineHeight: 1.2
|
||||||
|
font.family: stage.panel.uiFont
|
||||||
|
font.pixelSize: Style.space(10)
|
||||||
|
}
|
||||||
|
|
||||||
|
TextField {
|
||||||
|
id: urlField
|
||||||
|
width: parent.width
|
||||||
|
text: stage.panel.serverUrl
|
||||||
|
placeholderText: stage.panel.serverDefault !== "" ? stage.panel.serverDefault : "https://…"
|
||||||
|
foreground: Color.popups.text
|
||||||
|
font.family: stage.panel.uiFont
|
||||||
|
font.pixelSize: Style.space(11)
|
||||||
|
onTextChanged: stage.panel.serverUrl = text
|
||||||
|
onAccepted: stage.panel.saveServerSettings()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Flow, not Row: these can total more than the card is wide, and a
|
||||||
|
// Row lays them straight past its right edge instead of wrapping.
|
||||||
|
Flow {
|
||||||
|
width: parent.width
|
||||||
|
spacing: Style.space(7)
|
||||||
|
|
||||||
|
// A custom endpoint has no usual address to go back to.
|
||||||
|
PillButton { fontFamily: stage.panel.uiFont;
|
||||||
|
label: "Use local default"
|
||||||
|
visible: stage.panel.serverDefault !== ""
|
||||||
|
onTriggered: { stage.panel.serverUrl = stage.panel.serverDefault; urlField.text = stage.panel.serverDefault }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
PanelSeparator { width: parent.width }
|
||||||
|
|
||||||
|
Row {
|
||||||
|
width: parent.width
|
||||||
|
spacing: Style.space(9)
|
||||||
|
|
||||||
|
ToggleSwitch {
|
||||||
|
id: authToggle
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
checked: stage.panel.serverNeedsKey
|
||||||
|
foreground: Color.popups.text
|
||||||
|
accent: Color.accent
|
||||||
|
onToggled: stage.panel.serverNeedsKey = !stage.panel.serverNeedsKey
|
||||||
|
}
|
||||||
|
|
||||||
|
Column {
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
width: parent.width - authToggle.width - parent.spacing
|
||||||
|
spacing: Style.space(1)
|
||||||
|
|
||||||
|
Text {
|
||||||
|
width: parent.width
|
||||||
|
text: "Server requires an API key"
|
||||||
|
color: Color.popups.text
|
||||||
|
elide: Text.ElideRight
|
||||||
|
font.family: stage.panel.uiFont
|
||||||
|
font.pixelSize: Style.space(11)
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
width: parent.width
|
||||||
|
text: stage.panel.serverNeedsKey
|
||||||
|
? "Kept in the vault as '" + stage.panel.serverKeyRef + "', never in settings.json."
|
||||||
|
: "Sends " + stage.panel.pTitle(stage.panel.serverProvider) + "'s placeholder token, which is not a secret."
|
||||||
|
color: Color.muted
|
||||||
|
wrapMode: Text.WordWrap
|
||||||
|
font.family: stage.panel.uiFont
|
||||||
|
font.pixelSize: Style.space(10)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Flow, not Row: these can total more than the card is wide, and a
|
||||||
|
// Row lays them straight past its right edge instead of wrapping.
|
||||||
|
Flow {
|
||||||
|
width: parent.width
|
||||||
|
spacing: Style.space(7)
|
||||||
|
visible: stage.panel.serverNeedsKey
|
||||||
|
|
||||||
|
PillButton { fontFamily: stage.panel.uiFont; label: "Store the key…"; onTriggered: stage.panel.storeServerKey() }
|
||||||
|
}
|
||||||
|
|
||||||
|
PanelSeparator { width: parent.width }
|
||||||
|
|
||||||
|
// Flow, not Row: these can total more than the card is wide, and a
|
||||||
|
// Row lays them straight past its right edge instead of wrapping.
|
||||||
|
Flow {
|
||||||
|
width: parent.width
|
||||||
|
spacing: Style.space(7)
|
||||||
|
|
||||||
|
PillButton { fontFamily: stage.panel.uiFont; label: "Save"; primary: true; onTriggered: stage.panel.saveServerSettings() }
|
||||||
|
PillButton { fontFamily: stage.panel.uiFont;
|
||||||
|
label: stage.panel.serverReturn !== "" ? "Back" : "Cancel"
|
||||||
|
onTriggered: {
|
||||||
|
if (stage.panel.serverReturn !== "") stage.panel.stage = stage.panel.serverReturn
|
||||||
|
else stage.panel.resetFlow()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,126 @@
|
|||||||
|
import QtQuick
|
||||||
|
import qs.Commons
|
||||||
|
import qs.Ui
|
||||||
|
|
||||||
|
// SessionsStage.qml - Sessions are running. They will keep the old provider until they are restarted, and one mid-request can lose that turn outright, so this is a decision rather than a notification.
|
||||||
|
//
|
||||||
|
// One stage of the claude-mode panel. Panel.qml hands itself in as `panel`
|
||||||
|
// and owns all the state and actions; this file only draws them.
|
||||||
|
Column {
|
||||||
|
id: stage
|
||||||
|
required property var panel
|
||||||
|
|
||||||
|
width: parent.width
|
||||||
|
visible: stage.panel.stage === "confirm" && stage.panel.sessionInfo !== null
|
||||||
|
spacing: Style.space(7)
|
||||||
|
|
||||||
|
readonly property int total: stage.panel.sessionInfo ? Number(stage.panel.sessionInfo.count) : 0
|
||||||
|
readonly property int busyCount: stage.panel.sessionInfo ? Number(stage.panel.sessionInfo.busy) : 0
|
||||||
|
|
||||||
|
Text {
|
||||||
|
width: parent.width
|
||||||
|
text: parent.total === 1
|
||||||
|
? "1 Claude session is running"
|
||||||
|
: parent.total + " Claude sessions are running"
|
||||||
|
color: Color.popups.text
|
||||||
|
wrapMode: Text.WordWrap
|
||||||
|
font.family: stage.panel.uiFont
|
||||||
|
font.pixelSize: Style.space(13)
|
||||||
|
font.bold: true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Not "they will keep using the old provider". Their endpoint and model
|
||||||
|
// ids are fixed at startup, but the key is re-fetched on a timer and
|
||||||
|
// will resolve to the new mode, which the old endpoint refuses - so
|
||||||
|
// they fail rather than carry on.
|
||||||
|
Text {
|
||||||
|
width: parent.width
|
||||||
|
text: "They keep pointing at " + stage.panel.pTitle(stage.panel.mode) + ", but their key is "
|
||||||
|
+ "re-fetched on a timer and will switch under them. Worse, if one takes a "
|
||||||
|
+ "reply from the new provider first, that provider's message-id format "
|
||||||
|
+ "goes into its transcript and Anthropic will refuse to resume it at all."
|
||||||
|
color: Color.muted
|
||||||
|
wrapMode: Text.WordWrap
|
||||||
|
lineHeight: 1.2
|
||||||
|
font.family: stage.panel.uiFont
|
||||||
|
font.pixelSize: Style.space(10)
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
width: parent.width
|
||||||
|
visible: parent.busyCount > 0
|
||||||
|
text: parent.busyCount === 1
|
||||||
|
? "One is working right now and will break mid-turn."
|
||||||
|
: parent.busyCount + " are working right now and will break mid-turn."
|
||||||
|
color: stage.panel.urgentColor
|
||||||
|
wrapMode: Text.WordWrap
|
||||||
|
font.family: stage.panel.uiFont
|
||||||
|
font.pixelSize: Style.space(10)
|
||||||
|
}
|
||||||
|
|
||||||
|
Column {
|
||||||
|
width: parent.width
|
||||||
|
spacing: Style.space(1)
|
||||||
|
|
||||||
|
Repeater {
|
||||||
|
model: stage.panel.sessionInfo ? stage.panel.sessionInfo.sessions : []
|
||||||
|
|
||||||
|
Item {
|
||||||
|
required property var modelData
|
||||||
|
width: stage.width
|
||||||
|
height: Style.space(17)
|
||||||
|
|
||||||
|
Text {
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
text: parent.modelData.tty
|
||||||
|
color: Color.muted
|
||||||
|
font.family: stage.panel.uiFont
|
||||||
|
font.pixelSize: Style.space(10)
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.leftMargin: Style.space(52)
|
||||||
|
anchors.right: busyTag.left
|
||||||
|
anchors.rightMargin: Style.space(6)
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
text: parent.modelData.cwd
|
||||||
|
color: Color.popups.text
|
||||||
|
elide: Text.ElideLeft
|
||||||
|
font.family: stage.panel.uiFont
|
||||||
|
font.pixelSize: Style.space(10)
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
id: busyTag
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
visible: parent.modelData.busy === true
|
||||||
|
text: "working"
|
||||||
|
color: stage.panel.urgentColor
|
||||||
|
font.family: stage.panel.uiFont
|
||||||
|
font.pixelSize: Style.space(10)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Flow, not Row: these can total more than the card is wide, and a
|
||||||
|
// Row lays them straight past its right edge instead of wrapping.
|
||||||
|
Flow {
|
||||||
|
width: parent.width
|
||||||
|
spacing: Style.space(7)
|
||||||
|
|
||||||
|
// Restart is the only option that ends with every session on the mode
|
||||||
|
// the bar is now claiming, so it leads and it is the primary.
|
||||||
|
PillButton { fontFamily: stage.panel.uiFont;
|
||||||
|
label: "Switch and restart"
|
||||||
|
primary: true
|
||||||
|
onTriggered: stage.panel.applySwitch("restart")
|
||||||
|
}
|
||||||
|
PillButton { fontFamily: stage.panel.uiFont; label: "Switch and close"; onTriggered: stage.panel.applySwitch("stop") }
|
||||||
|
PillButton { fontFamily: stage.panel.uiFont; label: "Switch only"; onTriggered: stage.panel.applySwitch("none") }
|
||||||
|
PillButton { fontFamily: stage.panel.uiFont; label: "Cancel"; onTriggered: stage.panel.resetFlow() }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,223 @@
|
|||||||
|
import QtQuick
|
||||||
|
import qs.Commons
|
||||||
|
import qs.Ui
|
||||||
|
|
||||||
|
// TierStage.qml - One tier: pick from the cached catalogue, or type any id.
|
||||||
|
//
|
||||||
|
// One stage of the claude-mode panel. Panel.qml hands itself in as `panel`
|
||||||
|
// and owns all the state and actions; this file only draws them.
|
||||||
|
Column {
|
||||||
|
id: stage
|
||||||
|
required property var panel
|
||||||
|
|
||||||
|
width: parent.width
|
||||||
|
visible: stage.panel.stage === "presetTier"
|
||||||
|
spacing: Style.space(7)
|
||||||
|
|
||||||
|
Text {
|
||||||
|
width: parent.width
|
||||||
|
text: stage.panel.editTier + " · '" + stage.panel.editPreset + "'"
|
||||||
|
color: Color.popups.text
|
||||||
|
elide: Text.ElideRight
|
||||||
|
font.family: stage.panel.uiFont
|
||||||
|
font.pixelSize: Style.space(13)
|
||||||
|
font.bold: true
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
width: parent.width
|
||||||
|
text: "now: " + (stage.panel.editCurrent(stage.panel.editTier) || "not set")
|
||||||
|
color: Color.muted
|
||||||
|
elide: Text.ElideLeft
|
||||||
|
font.family: stage.panel.uiFont
|
||||||
|
font.pixelSize: Style.space(10)
|
||||||
|
}
|
||||||
|
|
||||||
|
// One field that both filters the catalogue and takes any id typed by
|
||||||
|
// hand, with the matches listed inline beneath it - rather than the
|
||||||
|
// shell's SearchableDropdown, whose list is a second popup layered on
|
||||||
|
// this card. Inline, the card's height is the only thing to manage,
|
||||||
|
// and the list can never be clipped by the card's edge.
|
||||||
|
TextField {
|
||||||
|
id: modelField
|
||||||
|
width: parent.width
|
||||||
|
text: stage.panel.editModel
|
||||||
|
placeholderText: stage.panel.modelOptions.length > 0
|
||||||
|
? "Search " + stage.panel.modelOptions.length + " models, or type any id"
|
||||||
|
: "Model id"
|
||||||
|
foreground: Color.popups.text
|
||||||
|
font.family: stage.panel.uiFont
|
||||||
|
font.pixelSize: Style.space(11)
|
||||||
|
onTextChanged: {
|
||||||
|
stage.panel.editModel = text
|
||||||
|
modelList.currentIndex = -1
|
||||||
|
}
|
||||||
|
// Enter takes the highlighted match if the arrows chose one, and
|
||||||
|
// saves what is in the field otherwise.
|
||||||
|
onAccepted: {
|
||||||
|
if (modelList.currentIndex >= 0 && modelList.currentIndex < stage.panel.modelMatches.length)
|
||||||
|
stage.panel.pickModel(String(stage.panel.modelMatches[modelList.currentIndex].value))
|
||||||
|
else
|
||||||
|
stage.panel.saveTier()
|
||||||
|
}
|
||||||
|
Keys.onDownPressed: function (event) {
|
||||||
|
if (modelList.count > 0) {
|
||||||
|
modelList.currentIndex = Math.min(modelList.currentIndex + 1, modelList.count - 1)
|
||||||
|
modelList.positionViewAtIndex(modelList.currentIndex, ListView.Contain)
|
||||||
|
}
|
||||||
|
event.accepted = true
|
||||||
|
}
|
||||||
|
Keys.onUpPressed: function (event) {
|
||||||
|
if (modelList.currentIndex >= 0) {
|
||||||
|
modelList.currentIndex = modelList.currentIndex - 1
|
||||||
|
if (modelList.currentIndex >= 0) modelList.positionViewAtIndex(modelList.currentIndex, ListView.Contain)
|
||||||
|
}
|
||||||
|
event.accepted = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scrolls inside a box of at most six rows: the card clamps to its
|
||||||
|
// content rather than scrolling, so an unbounded list would push the
|
||||||
|
// buttons off the bottom edge.
|
||||||
|
Rectangle {
|
||||||
|
id: modelListFrame
|
||||||
|
readonly property int rowHeight: Style.space(30)
|
||||||
|
width: parent.width
|
||||||
|
visible: stage.panel.modelOptions.length > 0
|
||||||
|
height: rowHeight * Math.max(1, Math.min(modelList.count, 6)) + 2
|
||||||
|
radius: Style.space(4)
|
||||||
|
color: "transparent"
|
||||||
|
border.width: 1
|
||||||
|
border.color: Qt.rgba(Color.popups.text.r, Color.popups.text.g, Color.popups.text.b, 0.14)
|
||||||
|
|
||||||
|
ListView {
|
||||||
|
id: modelList
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: 1
|
||||||
|
clip: true
|
||||||
|
boundsBehavior: Flickable.StopAtBounds
|
||||||
|
currentIndex: -1
|
||||||
|
model: stage.panel.stage === "presetTier" ? stage.panel.modelMatches : []
|
||||||
|
|
||||||
|
delegate: Item {
|
||||||
|
id: matchRow
|
||||||
|
required property var modelData
|
||||||
|
required property int index
|
||||||
|
readonly property bool chosen: String(modelData.value) === stage.panel.editModel.trim()
|
||||||
|
width: modelList.width
|
||||||
|
height: modelListFrame.rowHeight
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
anchors.fill: parent
|
||||||
|
color: matchRow.index === modelList.currentIndex || matchArea.containsMouse
|
||||||
|
? Qt.rgba(Color.accent.r, Color.accent.g, Color.accent.b, 0.16)
|
||||||
|
: (matchRow.chosen ? Qt.rgba(Color.accent.r, Color.accent.g, Color.accent.b, 0.10) : "transparent")
|
||||||
|
}
|
||||||
|
|
||||||
|
Column {
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.leftMargin: Style.space(8)
|
||||||
|
anchors.rightMargin: Style.space(8)
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
spacing: 0
|
||||||
|
|
||||||
|
Text {
|
||||||
|
width: parent.width
|
||||||
|
text: String(matchRow.modelData.label)
|
||||||
|
color: matchRow.chosen ? Color.accent : Color.popups.text
|
||||||
|
elide: Text.ElideMiddle
|
||||||
|
font.family: stage.panel.uiFont
|
||||||
|
font.pixelSize: Style.space(11)
|
||||||
|
font.bold: matchRow.chosen
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
width: parent.width
|
||||||
|
visible: text !== ""
|
||||||
|
text: String(matchRow.modelData.description || "")
|
||||||
|
color: Color.muted
|
||||||
|
elide: Text.ElideRight
|
||||||
|
font.family: stage.panel.uiFont
|
||||||
|
font.pixelSize: Style.space(9)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: matchArea
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onClicked: stage.panel.pickModel(String(matchRow.modelData.value))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
visible: modelList.count === 0
|
||||||
|
text: "No match. Save uses the id as typed."
|
||||||
|
color: Color.muted
|
||||||
|
font.family: stage.panel.uiFont
|
||||||
|
font.pixelSize: Style.space(10)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
width: parent.width
|
||||||
|
visible: stage.panel.modelOptions.length === 0
|
||||||
|
text: stage.panel.catalogueNode && stage.panel.catalogueNode.ok === false
|
||||||
|
? "The last fetch failed and no list is cached. Type an id, or try fetching again."
|
||||||
|
: "No model list cached for " + stage.panel.pTitle(stage.panel.editProvider) + " yet. Type an id, or fetch the list."
|
||||||
|
color: Color.muted
|
||||||
|
wrapMode: Text.WordWrap
|
||||||
|
font.family: stage.panel.uiFont
|
||||||
|
font.pixelSize: Style.space(10)
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
width: parent.width
|
||||||
|
visible: stage.panel.catalogueNode !== null && stage.panel.modelOptions.length > 0
|
||||||
|
text: stage.panel.catalogueNode
|
||||||
|
? stage.panel.modelOptions.length + " models cached, fetched " + stage.panel.ageLabel(stage.panel.catalogueNode.fetchedAt)
|
||||||
|
+ (stage.panel.catalogueNode.ok === false ? " · the last refresh failed" : "")
|
||||||
|
: ""
|
||||||
|
color: Color.muted
|
||||||
|
opacity: 0.85
|
||||||
|
elide: Text.ElideRight
|
||||||
|
font.family: stage.panel.uiFont
|
||||||
|
font.pixelSize: Style.space(9)
|
||||||
|
}
|
||||||
|
|
||||||
|
Flow {
|
||||||
|
width: parent.width
|
||||||
|
spacing: Style.space(7)
|
||||||
|
|
||||||
|
PillButton { fontFamily: stage.panel.uiFont; label: "Save"; primary: true; onTriggered: stage.panel.saveTier() }
|
||||||
|
PillButton { fontFamily: stage.panel.uiFont;
|
||||||
|
label: stage.panel.modelOptions.length > 0 ? "Refresh list" : "Fetch models"
|
||||||
|
onTriggered: stage.panel.fetchModels()
|
||||||
|
}
|
||||||
|
PillButton { fontFamily: stage.panel.uiFont;
|
||||||
|
label: "Back"
|
||||||
|
onTriggered: { stage.panel.lastError = ""; stage.panel.stage = "preset" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// The panel cannot reach this field by id, so when a tier is opened or a
|
||||||
|
// model picked it says so, and the field is reset or filled here. Typing
|
||||||
|
// breaks the field's binding, so without the reset a value left from another
|
||||||
|
// tier would survive into this one.
|
||||||
|
Connections {
|
||||||
|
target: stage.panel
|
||||||
|
function onTierEditorOpened() {
|
||||||
|
modelField.text = ""
|
||||||
|
modelList.currentIndex = -1
|
||||||
|
Qt.callLater(function () { modelField.forceActiveFocus() })
|
||||||
|
}
|
||||||
|
function onModelPicked(modelId) {
|
||||||
|
modelField.text = modelId
|
||||||
|
modelField.forceActiveFocus()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+3
-1
@@ -160,10 +160,12 @@ QMLLINT="$(command -v qmllint || true)"
|
|||||||
if [ -n "$QMLLINT" ]; then
|
if [ -n "$QMLLINT" ]; then
|
||||||
# The shell's own modules (qs.*, Quickshell) do not resolve outside it, so
|
# The shell's own modules (qs.*, Quickshell) do not resolve outside it, so
|
||||||
# only real syntax errors count; unresolved-type warnings are expected.
|
# only real syntax errors count; unresolved-type warnings are expected.
|
||||||
|
# qmllint reports a syntax error as a *warning* tagged [syntax] - matching
|
||||||
|
# on the word "error" instead caught every file that mentions lastError.
|
||||||
qml_syntax() {
|
qml_syntax() {
|
||||||
local f out
|
local f out
|
||||||
for f in omarchy/smoido.claude-mode/*.qml; do
|
for f in omarchy/smoido.claude-mode/*.qml; do
|
||||||
out="$("$QMLLINT" "$f" 2>&1 | grep -iE 'error|expected token|syntax|unexpected|duplicate' || true)"
|
out="$("$QMLLINT" "$f" 2>&1 | grep -F '[syntax]' || true)"
|
||||||
[ -z "$out" ] || { echo "$f"; echo "$out"; return 1; }
|
[ -z "$out" ] || { echo "$f"; echo "$out"; return 1; }
|
||||||
done
|
done
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user