2026-07-12 09:51:09 -04:00
import qs
import qs . services
import qs . modules . common
import qs . modules . common . widgets
import qs . modules . common . functions
2026-07-30 15:17:55 -04:00
import qs . modules . souveraine . subconscious
2026-07-12 09:51:09 -04:00
import qs . modules . ii . sidebarLeft . aiChat
2026-08-11 15:43:30 -04:00
import qs . modules . souveraine . island
shell: land the owned message delegate and the speech fixes
Applied 0009 then 0008 (0008's re-synthesize control calls a function 0009
adds). Casey applied and reloaded; verified against a running shell rather
than from a clean load.
0009 Speech.qml
stop() could not stop. playProc ran `sh -c "mpv ... || ffplay ..."` — a
compound command, so sh does not exec-replace itself and SIGTERM killed the
wrapper while the player kept sounding as an orphan. Reproduced directly.
That is the back-to-back TTS slam: every stop left audio playing and the
next speak started a second player over it. Shell dropped; the pid held is
now the pid making noise. ffplay fallback deleted rather than repaired —
needing a fallback is what forced the wrapper that broke the kill.
resynthesize() added, bypassing the cache: the button existed for "that came
out wrong" and, being keyed on the same text, always replayed the identical
file. synthesizing/playing split out of one `speaking` boolean.
0008 AiChat.qml
One line: delegate AiMessage -> AgentMessage. The vendor block named 8 tools;
the registry holds 19. The 11 it could not see were exactly the interiority
surface — outfit, nickname, subagent, atmosphere, reach, consult, itinerary,
todo, schedule, halt, intrusive.
regenerate and edit dropped deliberately, per Casey: text regeneration is not
possible (Ai.regenerate() already returned advice) and there is no in-place
edit (the vendor wrote to a local array the server never sees). delete is
armed and states that it hides locally only. Do not restore them.
Queue entries removed here, per the directory's own discipline.
2026-08-12 14:50:57 -04:00
import qs . modules . souveraine . agent
2026-07-12 09:51:09 -04:00
import QtQuick
import QtQuick . Controls
import QtQuick . Layouts
import Qt5Compat . GraphicalEffects
import Quickshell
import Quickshell . Io
Item {
id: root
property real padding: 4
property var inputField: messageInputField
property string commandPrefix: "/"
property var suggestionQuery: ""
property var suggestionList: [ ]
2026-07-30 09:18:56 -04:00
// Turn clock formatting. A local 9B can take minutes on a cold prompt, so
// the readout has to stay legible from milliseconds to tens of minutes.
function formatTurnDuration ( ms ) {
if ( ms < 1000 )
return ` $ { ms } ms ` ;
const seconds = ms / 1000 ;
if ( seconds < 60 )
return ` $ { seconds . toFixed ( 1 ) } s ` ;
const minutes = Math . floor ( seconds / 60 ) ;
const rest = Math . floor ( seconds % 60 ) ;
return ` $ { minutes } m $ { rest < 10 ? "0" : "" } $ { rest } s ` ;
}
function formatClockTime ( msEpoch ) {
if ( ! msEpoch )
return "—" ;
return new Date ( msEpoch ) . toLocaleTimeString ( Qt . locale ( ) , "HH:mm:ss" ) ;
}
2026-07-12 09:51:09 -04:00
onFocusChanged: focus = > {
if ( focus ) {
root . inputField . forceActiveFocus ( ) ;
}
}
Keys.onPressed: event = > {
messageInputField . forceActiveFocus ( ) ;
if ( event . modifiers === Qt . NoModifier ) {
if ( event . key === Qt . Key_PageUp ) {
messageListView . contentY = Math . max ( 0 , messageListView . contentY - messageListView . height / 2 ) ;
event . accepted = true ;
} else if ( event . key === Qt . Key_PageDown ) {
messageListView . contentY = Math . min ( messageListView . contentHeight - messageListView . height / 2 , messageListView . contentY + messageListView . height / 2 ) ;
event . accepted = true ;
}
}
if ( ( event . modifiers & Qt . ControlModifier ) && ( event . modifiers & Qt . ShiftModifier ) && event . key === Qt . Key_O ) {
Ai . clearMessages ( ) ;
}
}
property var allCommands: [
{
name: "attach" ,
description: Translation . tr ( "Attach a file. Only works with Gemini." ) ,
execute: args = > {
Ai . attachFile ( args . join ( " " ) . trim ( ) ) ;
}
} ,
{
name: "model" ,
description: Translation . tr ( "Choose model" ) ,
execute: args = > {
Ai . setModel ( args [ 0 ] ) ;
}
} ,
2026-07-12 16:31:55 -04:00
{
name: "agent" ,
description: Translation . tr ( "Choose Souveraine agent" ) ,
execute: args = > {
Ai . setModel ( args [ 0 ] ) ;
}
} ,
{
name: "resume" ,
description: Translation . tr ( "Resume the selected agent's latest conversation" ) ,
execute: ( ) = > {
Ai . resumeConversation ( ) ;
}
} ,
2026-07-12 09:51:09 -04:00
{
name: "tool" ,
description: Translation . tr ( "Set the tool to use for the model." ) ,
execute: args = > {
// console.log(args)
if ( args . length == 0 || args [ 0 ] == "get" ) {
Ai . addMessage ( Translation . tr ( "Usage: %1tool TOOL_NAME" ) . arg ( root . commandPrefix ) , Ai . interfaceRole ) ;
} else {
const tool = args [ 0 ] ;
const switched = Ai . setTool ( tool ) ;
if ( switched ) {
Ai . addMessage ( Translation . tr ( "Tool set to: %1" ) . arg ( tool ) , Ai . interfaceRole ) ;
}
}
}
} ,
{
name: "prompt" ,
description: Translation . tr ( "Set the system prompt for the model." ) ,
execute: args = > {
if ( args . length === 0 || args [ 0 ] === "get" ) {
Ai . printPrompt ( ) ;
return ;
}
Ai . loadPrompt ( args . join ( " " ) . trim ( ) ) ;
}
} ,
{
name: "key" ,
description: Translation . tr ( "Set API key" ) ,
execute: args = > {
if ( args [ 0 ] == "get" ) {
Ai . printApiKey ( ) ;
} else {
Ai . setApiKey ( args [ 0 ] ) ;
}
}
} ,
{
name: "save" ,
description: Translation . tr ( "Save chat" ) ,
execute: args = > {
const joinedArgs = args . join ( " " ) ;
if ( joinedArgs . trim ( ) . length == 0 ) {
Ai . addMessage ( Translation . tr ( "Usage: %1save CHAT_NAME" ) . arg ( root . commandPrefix ) , Ai . interfaceRole ) ;
return ;
}
Ai . saveChat ( joinedArgs ) ;
}
} ,
{
name: "load" ,
description: Translation . tr ( "Load chat" ) ,
execute: args = > {
const joinedArgs = args . join ( " " ) ;
if ( joinedArgs . trim ( ) . length == 0 ) {
Ai . addMessage ( Translation . tr ( "Usage: %1load CHAT_NAME" ) . arg ( root . commandPrefix ) , Ai . interfaceRole ) ;
return ;
}
Ai . loadChat ( joinedArgs ) ;
}
} ,
{
2026-08-12 19:50:32 -04:00
name: "new" ,
description: Translation . tr ( "Start a new conversation" ) ,
2026-07-12 09:51:09 -04:00
execute: ( ) = > {
Ai . clearMessages ( ) ;
}
} ,
{
name: "temp" ,
description: Translation . tr ( "Set temperature (randomness) of the model. Values range between 0 to 2 for Gemini, 0 to 1 for other models. Default is 0.5." ) ,
execute: args = > {
// console.log(args)
if ( args . length == 0 || args [ 0 ] == "get" ) {
Ai . printTemperature ( ) ;
} else {
const temp = parseFloat ( args [ 0 ] ) ;
Ai . setTemperature ( temp ) ;
}
}
} ,
{
name: "test" ,
description: Translation . tr ( "Markdown test" ) ,
execute: ( ) = > {
Ai . addMessage ( `
< think >
A longer think block to test revealing animation
OwO wem ipsum dowo sit amet , consekituwet awipiscing ewit , sed do eiuwsmod tempow inwididunt ut wabowe et dowo mawa . Ut enim ad minim weniam , quis nostwud exeucitation uwuwamcow bowowis nisi ut awiquip ex ea commowo consequat . Duuis aute iwuwe dowo in wepwependewit in wowuptate velit esse ciwwum dowo eu fugiat nuwa pawiatuw . Excepteuw sint occaecat cupidatat non pwowoident , sunt in cuwpa qui officia desewunt mowit anim id est wabowum . Meouw ! > w <
Mowe uwu wem ipsum !
< / t h i n k >
# # ✏ ️ Markdown test
# # # Formatting
- * Italic * , \ ` Monospace \ ` , * * Bold * * , [ Link ] ( https: //example.com)
- Arch lincox icon < img src = "${Quickshell.shellPath(" assets / icons / arch - symbolic . svg ")}" height = "${Appearance.font.pixelSize.small}" / >
# # # Table
Quickshell vs AGS / Astal
| | Quickshell | AGS / Astal |
| -- -- -- -- -- -- -- -- -- -- -- -- -- | -- -- -- -- -- -- -- -- -- | -- -- -- -- -- -- -- -- -- - |
| UI Toolkit | Qt | Gtk3 / Gtk4 |
| Language | QML | Js / Ts / Lua |
| Reactivity | Implied | Needs declaration |
| Widget placement | Mildly difficult | More intuitive |
| Bluetooth & Wifi support | ❌ | ✅ |
| No - delay keybinds | ✅ | ❌ |
| Development | New APIs | New syntax |
# # # Code block
Just a hello world . . .
\ ` \ ` \ ` cpp
# include < bits / stdc ++ . h >
// This is intentionally very long to test scrolling
const std: : string GREETING = \ "UwU\" ;
int main ( int argc , char * argv [ ] ) {
std: : cout << GREETING ;
}
\ ` \ ` \ `
# # # LaTeX
Inline w / dollar signs: $ \ \ frac { 1 } { 2 } = \ \ frac { 2 } { 4 } $
Inline w / double dollar signs: $ $ \ \ int_0 ^ \ \ infty e ^ { - x ^ 2 } dx = \ \ frac { \ \ sqrt { \ \ pi } } { 2 } $ $
Inline w / backslash and square brackets \ \ [ \ \ int_0 ^ \ \ infty \ \ frac { 1 } { x ^ 2 } dx = \ \ infty \ \ ]
Inline w / backslash and round brackets \ \ ( e ^ { i \ \ pi } + 1 = 0 \ \ )
` , Ai . interfaceRole ) ;
}
} ,
]
shell: dock reorder, fullscreen detection fix, idle-power, sessiond, misc shell work
- Dock drag-to-reorder for pinned apps (insertion gap, quick-slide vs dwell)
- Fullscreen detection: scan all windows via HyprlandData.windowList
- IdleCoordinator, GlobalStates, Session.qml updates
- Deploy script, qmldir, settings, wallpaper, visualizer fixes
- sessiond server, memory module updates
Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-22 22:18:20 -04:00
// Latest visible assistant reply — what gets read aloud. Skips
// lock-redacted content (LockContentPolicy swaps it for a marker).
2026-07-21 13:28:47 -04:00
function lastAssistantText ( ) {
for ( let i = Ai . messageIDs . length - 1 ; i >= 0 ; i -- ) {
const m = Ai . messageByID [ Ai . messageIDs [ i ] ] ;
if ( m && m . role === "assistant" && ( m . visibleToUser ? ? true )
&& m . content && m . content . length > 0
&& ! m . content . startsWith ( "[content hidden" ) ) {
shell: dock reorder, fullscreen detection fix, idle-power, sessiond, misc shell work
- Dock drag-to-reorder for pinned apps (insertion gap, quick-slide vs dwell)
- Fullscreen detection: scan all windows via HyprlandData.windowList
- IdleCoordinator, GlobalStates, Session.qml updates
- Deploy script, qmldir, settings, wallpaper, visualizer fixes
- sessiond server, memory module updates
Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-22 22:18:20 -04:00
return StringUtils . ttsClean ( m . content ) ;
2026-07-21 13:28:47 -04:00
}
}
return "" ;
}
2026-07-12 09:51:09 -04:00
function handleInput ( inputText ) {
if ( inputText . startsWith ( root . commandPrefix ) ) {
// Handle special commands
const command = inputText . split ( " " ) [ 0 ] . substring ( 1 ) ;
const args = inputText . split ( " " ) . slice ( 1 ) ;
const commandObj = root . allCommands . find ( cmd = > cmd . name === ` $ { command } ` ) ;
if ( commandObj ) {
commandObj . execute ( args ) ;
} else {
Ai . addMessage ( Translation . tr ( "Unknown command: " ) + command , Ai . interfaceRole ) ;
}
} else {
Ai . sendUserMessage ( inputText ) ;
}
// Always scroll to bottom when user sends a message
messageListView . positionViewAtEnd ( ) ;
}
Process {
id: decodeImageAndAttachProc
property string imageDecodePath: Directories . cliphistDecode
property string imageDecodeFileName: "image"
property string imageDecodeFilePath: ` $ { imageDecodePath } / $ { imageDecodeFileName } `
function handleEntry ( entry: string ) {
imageDecodeFileName = parseInt ( entry . match ( /^(\d+)\t/ ) [ 1 ] ) ;
decodeImageAndAttachProc . exec ( [ "bash" , "-c" , ` [ - f $ { imageDecodeFilePath } ] || echo '${StringUtils.shellSingleQuoteEscape(entry)}' | $ { Cliphist . cliphistBinary } decode > '${imageDecodeFilePath}' ` ] ) ;
}
onExited: ( exitCode , exitStatus ) = > {
if ( exitCode === 0 ) {
Ai . attachFile ( imageDecodeFilePath ) ;
} else {
console . error ( "[AiChat] Failed to decode image in clipboard content" ) ;
}
}
}
component StatusItem: MouseArea {
id: statusItem
property string icon
property string statusText
property string description
hoverEnabled: true
implicitHeight: statusItemRowLayout . implicitHeight
implicitWidth: statusItemRowLayout . implicitWidth
RowLayout {
id: statusItemRowLayout
spacing: 0
MaterialSymbol {
text: statusItem . icon
iconSize: Appearance . font . pixelSize . huge
color: Appearance . colors . colSubtext
}
StyledText {
font.pixelSize: Appearance . font . pixelSize . small
text: statusItem . statusText
color: Appearance . colors . colSubtext
animateChange: true
}
}
StyledToolTip {
text: statusItem . description
extraVisibleCondition: false
alternativeVisibleCondition: statusItem . containsMouse
}
}
component StatusSeparator: Rectangle {
implicitWidth: 4
implicitHeight: 4
radius: implicitWidth / 2
color: Appearance . colors . colOutlineVariant
}
ColumnLayout {
id: columnLayout
anchors {
fill: parent
margins: root . padding
}
spacing: root . padding
2026-08-11 15:43:30 -04:00
AgentSessionPanel {
Layout.fillWidth: true
}
2026-07-12 09:51:09 -04:00
Item {
// Messages
Layout.fillWidth: true
Layout.fillHeight: true
layer.enabled: true
layer.effect: OpacityMask {
maskSource: Rectangle {
width: swipeView . width
height: swipeView . height
radius: Appearance . rounding . small
}
}
StyledRectangularShadow {
z: 1
target: statusBg
opacity: messageListView . atYBeginning ? 0 : 1
visible: opacity > 0
Behavior on opacity {
animation: Appearance . animation . elementMoveFast . numberAnimation . createObject ( this )
}
}
Rectangle {
id: statusBg
z: 2
anchors {
horizontalCenter: parent . horizontalCenter
top: parent . top
topMargin: 4
}
implicitWidth: statusRowLayout . implicitWidth + 10 * 2
implicitHeight: Math . max ( statusRowLayout . implicitHeight , 38 )
radius: Appearance . rounding . normal - root . padding
color: messageListView . atYBeginning ? Appearance.colors.colLayer2 : Appearance . colors . colLayer2Base
Behavior on color {
animation: Appearance . animation . elementMoveFast . colorAnimation . createObject ( this )
}
RowLayout {
id: statusRowLayout
anchors.centerIn: parent
spacing: 10
StatusItem {
icon: Ai . currentModelHasApiKey ? "key" : "key_off"
statusText: ""
description: Ai . currentModelHasApiKey ? Translation . tr ( "API key is set\nChange with /key YOUR_API_KEY" ) : Translation . tr ( "No API key\nSet it with /key YOUR_API_KEY" )
}
StatusSeparator { }
StatusItem {
icon: "device_thermostat"
statusText: Ai . temperature . toFixed ( 1 )
description: Translation . tr ( "Temperature\nChange with /temp VALUE" )
}
StatusSeparator {
2026-08-12 19:50:32 -04:00
visible: Ai . context . known || Ai . tokenCount . total > 0
2026-07-12 09:51:09 -04:00
}
StatusItem {
2026-08-12 19:50:32 -04:00
// Context occupancy. Until 2026-08-12 this pill showed
// `event.tokens`, which was actually the model's
// ceiling — a constant 250000 presented as usage. Now
// it shows both numbers and the ratio between them,
// which is the only reading that means anything.
visible: Ai . context . known || Ai . tokenCount . total > 0
2026-08-12 20:30:34 -04:00
// Tiers mirror ConsciousnessEngine::on_response exactly:
// 80 / 90 / 95 are the three compaction-warning tiers.
// They are advisory — the engine never forces compaction.
icon: Ai . context . percent >= 90 ? "error"
: Ai . context . percent >= 80 ? "warning"
2026-08-12 19:50:32 -04:00
: "token"
statusText: Ai . context . known
? ` $ { Math . round ( Ai . context . used / 1000 ) } k / $ { Math . round ( Ai . context . limit / 1000 ) } k · $ { Ai . context . percent } % `
: ( Ai . tokenCount . total > 0 ? ` $ { Ai . tokenCount . total } ` : "—" )
description: Ai . context . known
2026-08-12 20:30:34 -04:00
? Translation . tr ( "Context: %1 of %2 tokens (%3%)\nCeiling is this agent's configured context window.\nAdvisory warnings at 80/90/95% — compaction is never forced." )
2026-08-12 19:50:32 -04:00
. arg ( Ai . context . used . toLocaleString ( ) )
. arg ( Ai . context . limit . toLocaleString ( ) )
. arg ( Ai . context . percent )
: Translation . tr ( "Context occupancy unknown until the next turn reports it." )
2026-07-30 09:18:56 -04:00
}
StatusSeparator {
visible: Souveraine . turnStartedAt > 0
}
StatusItem {
visible: Souveraine . turnStartedAt > 0
icon: Souveraine . turnActive ? "hourglass_top" : "timer"
statusText: root . formatTurnDuration ( Souveraine . turnElapsedMs )
description: Souveraine . turnActive
? Translation . tr ( "Waiting on the agent\nSent at %1" ) . arg ( root . formatClockTime ( Souveraine . turnStartedAt ) )
: Translation . tr ( "Last turn took %1\nSent at %2" ) . arg ( root . formatTurnDuration ( Souveraine . turnElapsedMs ) ) . arg ( root . formatClockTime ( Souveraine . turnStartedAt ) )
2026-07-12 09:51:09 -04:00
}
2026-08-17 09:16:11 -04:00
StatusSeparator {
visible: Ai . queuedInterjections > 0
}
StatusItem {
visible: Ai . queuedInterjections > 0
icon: "pan_tool"
statusText: Translation . tr ( "%1 queued" ) . arg ( Ai . queuedInterjections )
description: Translation . tr ( "Notes handed to the turn in flight.\nShe reads them at the next round boundary — a long tool round finishes first.\nQueued means sent, not yet read." )
}
2026-07-12 09:51:09 -04:00
}
}
ScrollEdgeFade {
z: 1
target: messageListView
vertical: true
}
StyledListView { // Message list
id: messageListView
z: 0
anchors.fill: parent
spacing: 10
popin: false
topMargin: statusBg . implicitHeight + statusBg . anchors . topMargin * 2
touchpadScrollFactor: Config . options . interactions . scrolling . touchpadScrollFactor * 1.4
mouseScrollFactor: Config . options . interactions . scrolling . mouseScrollFactor * 1.4
property int lastResponseLength: 0
// onContentHeightChanged: {
// if (atYEnd)
// Qt.callLater(positionViewAtEnd);
// }
// onCountChanged: {
// // Auto-scroll when new messages are added
// if (atYEnd)
// Qt.callLater(positionViewAtEnd);
// }
add: null // Prevent function calls from being janky
model: ScriptModel {
values: Ai . messageIDs . filter ( id = > {
const message = Ai . messageByID [ id ] ;
return message ? . visibleToUser ? ? true ;
} )
}
shell: land the owned message delegate and the speech fixes
Applied 0009 then 0008 (0008's re-synthesize control calls a function 0009
adds). Casey applied and reloaded; verified against a running shell rather
than from a clean load.
0009 Speech.qml
stop() could not stop. playProc ran `sh -c "mpv ... || ffplay ..."` — a
compound command, so sh does not exec-replace itself and SIGTERM killed the
wrapper while the player kept sounding as an orphan. Reproduced directly.
That is the back-to-back TTS slam: every stop left audio playing and the
next speak started a second player over it. Shell dropped; the pid held is
now the pid making noise. ffplay fallback deleted rather than repaired —
needing a fallback is what forced the wrapper that broke the kill.
resynthesize() added, bypassing the cache: the button existed for "that came
out wrong" and, being keyed on the same text, always replayed the identical
file. synthesizing/playing split out of one `speaking` boolean.
0008 AiChat.qml
One line: delegate AiMessage -> AgentMessage. The vendor block named 8 tools;
the registry holds 19. The 11 it could not see were exactly the interiority
surface — outfit, nickname, subagent, atmosphere, reach, consult, itinerary,
todo, schedule, halt, intrusive.
regenerate and edit dropped deliberately, per Casey: text regeneration is not
possible (Ai.regenerate() already returned advice) and there is no in-place
edit (the vendor wrote to a local array the server never sees). delete is
armed and states that it hides locally only. Do not restore them.
Queue entries removed here, per the directory's own discipline.
2026-08-12 14:50:57 -04:00
delegate: AgentMessage {
2026-07-12 09:51:09 -04:00
required property var modelData
required property int index
messageIndex: index
messageData: {
Ai . messageByID [ modelData ] ;
}
messageInputField: root . inputField
}
}
PagePlaceholder {
z: 2
shown: Ai . messageIDs . length === 0
icon: "neurology"
title: Translation . tr ( "Large language models" )
description: Translation . tr ( "Type /key to get started with online models\nCtrl+O to expand sidebar\nCtrl+P to pin sidebar\nCtrl+D to detach sidebar" )
shape: MaterialShape . Shape . PixelCircle
}
ScrollToBottomButton {
z: 3
target: messageListView
}
}
DescriptionBox {
text: root . suggestionList [ suggestions . selectedIndex ] ? . description ? ? ""
showArrows: root . suggestionList . length > 1
}
FlowButtonGroup { // Suggestions
id: suggestions
visible: root . suggestionList . length > 0 && messageInputField . text . length > 0
property int selectedIndex: 0
Layout.fillWidth: true
spacing: 5
Repeater {
id: suggestionRepeater
model: {
suggestions . selectedIndex = 0 ;
return root . suggestionList . slice ( 0 , 10 ) ;
}
delegate: ApiCommandButton {
id: commandButton
colBackground: suggestions . selectedIndex === index ? Appearance.colors.colSecondaryContainerHover : Appearance . colors . colSecondaryContainer
bounce: false
contentItem: StyledText {
font.pixelSize: Appearance . font . pixelSize . small
color: Appearance . m3colors . m3onSurface
horizontalAlignment: Text . AlignHCenter
text: modelData . displayName ? ? modelData . name
}
onHoveredChanged: {
if ( commandButton . hovered ) {
suggestions . selectedIndex = index ;
}
}
onClicked: {
suggestions . acceptSuggestion ( modelData . name ) ;
}
}
}
function acceptSuggestion ( word ) {
const words = messageInputField . text . trim ( ) . split ( /\s+/ ) ;
if ( words . length > 0 ) {
words [ words . length - 1 ] = word ;
} else {
words . push ( word ) ;
}
const updatedText = words . join ( " " ) + " " ;
messageInputField . text = updatedText ;
messageInputField . cursorPosition = messageInputField . text . length ;
messageInputField . forceActiveFocus ( ) ;
}
function acceptSelectedWord ( ) {
if ( suggestions . selectedIndex >= 0 && suggestions . selectedIndex < suggestionRepeater . count ) {
const word = root . suggestionList [ suggestions . selectedIndex ] . name ;
suggestions . acceptSuggestion ( word ) ;
}
}
}
2026-07-31 12:51:17 -04:00
SubconsciousTicker {
// Tier 1 declares the intent; mounting Tier 2 is the host's job.
onRequestOpenPanel: root . subconsciousPanelOpen = true
}
2026-07-30 15:17:55 -04:00
2026-07-12 09:51:09 -04:00
Rectangle { // Input area
id: inputWrapper
property real spacing: 5
Layout.fillWidth: true
radius: Appearance . rounding . normal - root . padding
color: Appearance . colors . colLayer2
implicitHeight: Math . max ( inputFieldRowLayout . implicitHeight + inputFieldRowLayout . anchors . topMargin + commandButtonsRow . implicitHeight + commandButtonsRow . anchors . bottomMargin + spacing , 45 ) + ( attachedFileIndicator . implicitHeight + spacing + attachedFileIndicator . anchors . topMargin )
clip: true
Behavior on implicitHeight {
animation: Appearance . animation . elementMove . numberAnimation . createObject ( this )
}
AttachedFileIndicator {
id: attachedFileIndicator
anchors {
top: parent . top
left: parent . left
right: parent . right
margins: visible ? 5 : 0
}
filePath: Ai . pendingFilePath
onRemove: Ai . attachFile ( "" )
}
RowLayout { // Input field and send button
id: inputFieldRowLayout
anchors {
bottom: commandButtonsRow . top
left: parent . left
right: parent . right
bottomMargin: 5
}
spacing: 0
ScrollView {
id: inputScrollView
Layout.fillWidth: true
Layout.preferredHeight: Math . min ( root . height * 3 / 5 , messageInputField . height )
clip: true
ScrollBar.vertical.policy: ScrollBar . AsNeeded
StyledTextArea { // The actual TextArea (inside ScrollView to enable scrolling)
id: messageInputField
anchors.fill: parent
wrapMode: TextArea . Wrap
padding: 10
color: activeFocus ? Appearance.m3colors.m3onSurface : Appearance . m3colors . m3onSurfaceVariant
placeholderText: Translation . tr ( 'Message the model... "%1" for commands' ) . arg ( root . commandPrefix )
background: null
2026-08-11 18:24:24 -04:00
// Input-method focus is evidence that typing is
// possible, not a reliable request to show the OSK.
// ViewTop preserves focus across a keyboard dismiss,
// so a second tap may not produce another focus edge.
// Drive both paths: focus raises it the first time;
// every deliberate tap raises it again after a user
// dismissal. The user-close path remains authoritative
// and no hold is taken merely because chat is open.
onActiveFocusChanged: {
if ( activeFocus )
GlobalStates . oskOpen = true ;
}
TapHandler {
onTapped: {
messageInputField . forceActiveFocus ( ) ;
GlobalStates . oskOpen = true ;
}
}
2026-07-12 09:51:09 -04:00
onTextChanged: {
// Handle suggestions
if ( messageInputField . text . length === 0 ) {
root . suggestionQuery = "" ;
root . suggestionList = [ ] ;
return ;
2026-07-12 16:31:55 -04:00
} else if ( messageInputField . text . startsWith ( ` $ { root . commandPrefix } model ` ) || messageInputField . text . startsWith ( ` $ { root . commandPrefix } agent ` ) ) {
2026-07-12 09:51:09 -04:00
root . suggestionQuery = messageInputField . text . split ( " " ) [ 1 ] ? ? "" ;
const modelResults = Fuzzy . go ( root . suggestionQuery , Ai . modelList . map ( model = > {
return {
name: Fuzzy . prepare ( model ) ,
obj: model
} ;
} ) , {
all: true ,
key: "name"
} ) ;
root . suggestionList = modelResults . map ( model = > {
return {
2026-07-12 16:31:55 -04:00
name: ` $ { messageInputField . text . trim ( ) . split ( " " ) . length == 1 ? ( messageInputField . text . startsWith ( ` $ { root . commandPrefix } agent ` ) ? ( root . commandPrefix + "agent " ) : ( root . commandPrefix + "model " ) ) : "" } $ { model . target } ` ,
2026-07-12 09:51:09 -04:00
displayName: ` $ { Ai . models [ model . target ] . name } ` ,
description: ` $ { Ai . models [ model . target ] . description } `
} ;
} ) ;
} else if ( messageInputField . text . startsWith ( ` $ { root . commandPrefix } prompt ` ) ) {
root . suggestionQuery = messageInputField . text . split ( " " ) [ 1 ] ? ? "" ;
const promptFileResults = Fuzzy . go ( root . suggestionQuery , Ai . promptFiles . map ( file = > {
return {
name: Fuzzy . prepare ( file ) ,
obj: file
} ;
} ) , {
all: true ,
key: "name"
} ) ;
root . suggestionList = promptFileResults . map ( file = > {
return {
name: ` $ { messageInputField . text . trim ( ) . split ( " " ) . length == 1 ? ( root . commandPrefix + "prompt " ) : "" } $ { file . target } ` ,
displayName: ` $ { FileUtils . trimFileExt ( FileUtils . fileNameForPath ( file . target ) ) } ` ,
description: Translation . tr ( "Load prompt from %1" ) . arg ( file . target )
} ;
} ) ;
} else if ( messageInputField . text . startsWith ( ` $ { root . commandPrefix } save ` ) ) {
root . suggestionQuery = messageInputField . text . split ( " " ) [ 1 ] ? ? "" ;
const promptFileResults = Fuzzy . go ( root . suggestionQuery , Ai . savedChats . map ( file = > {
return {
name: Fuzzy . prepare ( file ) ,
obj: file
} ;
} ) , {
all: true ,
key: "name"
} ) ;
root . suggestionList = promptFileResults . map ( file = > {
const chatName = FileUtils . trimFileExt ( FileUtils . fileNameForPath ( file . target ) ) . trim ( ) ;
return {
name: ` $ { messageInputField . text . trim ( ) . split ( " " ) . length == 1 ? ( root . commandPrefix + "save " ) : "" } $ { chatName } ` ,
displayName: ` $ { chatName } ` ,
description: Translation . tr ( "Save chat to %1" ) . arg ( chatName )
} ;
} ) ;
} else if ( messageInputField . text . startsWith ( ` $ { root . commandPrefix } load ` ) ) {
root . suggestionQuery = messageInputField . text . split ( " " ) [ 1 ] ? ? "" ;
const promptFileResults = Fuzzy . go ( root . suggestionQuery , Ai . savedChats . map ( file = > {
return {
name: Fuzzy . prepare ( file ) ,
obj: file
} ;
} ) , {
all: true ,
key: "name"
} ) ;
root . suggestionList = promptFileResults . map ( file = > {
const chatName = FileUtils . trimFileExt ( FileUtils . fileNameForPath ( file . target ) ) . trim ( ) ;
return {
name: ` $ { messageInputField . text . trim ( ) . split ( " " ) . length == 1 ? ( root . commandPrefix + "load " ) : "" } $ { chatName } ` ,
displayName: ` $ { chatName } ` ,
description: Translation . tr ( ` Load chat from % 1 ` ) . arg ( file . target )
} ;
} ) ;
} else if ( messageInputField . text . startsWith ( ` $ { root . commandPrefix } tool ` ) ) {
root . suggestionQuery = messageInputField . text . split ( " " ) [ 1 ] ? ? "" ;
const toolResults = Fuzzy . go ( root . suggestionQuery , Ai . availableTools . map ( tool = > {
return {
name: Fuzzy . prepare ( tool ) ,
obj: tool
} ;
} ) , {
all: true ,
key: "name"
} ) ;
root . suggestionList = toolResults . map ( tool = > {
const toolName = tool . target ;
return {
name: ` $ { messageInputField . text . trim ( ) . split ( " " ) . length == 1 ? ( root . commandPrefix + "tool " ) : "" } $ { tool . target } ` ,
displayName: toolName ,
description: Ai . toolDescriptions [ toolName ]
} ;
} ) ;
} else if ( messageInputField . text . startsWith ( root . commandPrefix ) ) {
root . suggestionQuery = messageInputField . text ;
root . suggestionList = root . allCommands . filter ( cmd = > cmd . name . startsWith ( messageInputField . text . substring ( 1 ) ) ) . map ( cmd = > {
return {
name: ` $ { root . commandPrefix } $ { cmd . name } ` ,
description: ` $ { cmd . description } `
} ;
} ) ;
}
}
function accept ( ) {
root . handleInput ( text ) ;
text = "" ;
}
Keys.onPressed: event = > {
if ( event . key === Qt . Key_Tab ) {
suggestions . acceptSelectedWord ( ) ;
event . accepted = true ;
} else if ( event . key === Qt . Key_Up && suggestions . visible ) {
suggestions . selectedIndex = Math . max ( 0 , suggestions . selectedIndex - 1 ) ;
event . accepted = true ;
} else if ( event . key === Qt . Key_Down && suggestions . visible ) {
suggestions . selectedIndex = Math . min ( root . suggestionList . length - 1 , suggestions . selectedIndex + 1 ) ;
event . accepted = true ;
} else if ( ( event . key === Qt . Key_Enter || event . key === Qt . Key_Return ) ) {
if ( event . modifiers & Qt . ShiftModifier ) {
// Insert newline
messageInputField . insert ( messageInputField . cursorPosition , "\n" ) ;
event . accepted = true ;
} else {
// Accept text
const inputText = messageInputField . text ;
messageInputField . clear ( ) ;
root . handleInput ( inputText ) ;
event . accepted = true ;
}
} else if ( ( event . modifiers & Qt . ControlModifier ) && event . key === Qt . Key_V ) {
// Intercept Ctrl+V to handle image/file pasting
if ( event . modifiers & Qt . ShiftModifier ) {
// Let Shift+Ctrl+V = plain paste
messageInputField . text += Quickshell . clipboardText ;
event . accepted = true ;
return ;
}
// Try image paste first
const currentClipboardEntry = Cliphist . entries [ 0 ] ;
const cleanCliphistEntry = StringUtils . cleanCliphistEntry ( currentClipboardEntry ) ;
if ( /^\d+\t\[\[.*binary data.*\d+x\d+.*\]\]$/ . test ( currentClipboardEntry ) ) {
// First entry = currently copied entry = image?
decodeImageAndAttachProc . handleEntry ( currentClipboardEntry ) ;
event . accepted = true ;
return ;
} else if ( cleanCliphistEntry . startsWith ( "file://" ) ) {
// First entry = currently copied entry = image?
const fileName = decodeURIComponent ( cleanCliphistEntry ) ;
Ai . attachFile ( fileName ) ;
event . accepted = true ;
return ;
}
event . accepted = false ; // No image, let text pasting proceed
} else if ( event . key === Qt . Key_Escape ) {
2026-08-17 09:16:11 -04:00
// Esc detaches a file first — that is the
// older, narrower meaning and it keeps it.
2026-07-12 09:51:09 -04:00
if ( Ai . pendingFilePath . length > 0 ) {
Ai . attachFile ( "" ) ;
event . accepted = true ;
2026-08-17 09:16:11 -04:00
} else if ( Souveraine . turnActive ) {
// Nothing attached and a turn is running:
// Esc is the raised hand. Cancellation is
// read at a round boundary, not mid-tool
// (`src/server/turn.rs:248`), so partial
// work survives.
Souveraine . cancelTurn ( ) ;
event . accepted = true ;
2026-07-12 09:51:09 -04:00
} else {
event . accepted = false ;
}
}
}
}
}
RippleButton { // Send button
id: sendButton
Layout.alignment: Qt . AlignBottom
Layout.rightMargin: 5
implicitWidth: 40
implicitHeight: 40
buttonRadius: Appearance . rounding . small
enabled: messageInputField . text . length > 0
toggled: enabled
MouseArea {
anchors.fill: parent
cursorShape: sendButton . enabled ? Qt.PointingHandCursor : Qt . ArrowCursor
onClicked: {
const inputText = messageInputField . text ;
root . handleInput ( inputText ) ;
messageInputField . clear ( ) ;
}
}
contentItem: MaterialSymbol {
anchors.centerIn: parent
horizontalAlignment: Text . AlignHCenter
iconSize: 22
color: sendButton . enabled ? Appearance.m3colors.m3onPrimary : Appearance . colors . colOnLayer2Disabled
text: "arrow_upward"
}
}
}
RowLayout { // Controls
id: commandButtonsRow
anchors.left: parent . left
anchors.right: parent . right
anchors.bottom: parent . bottom
anchors.bottomMargin: 5
anchors.leftMargin: 10
anchors.rightMargin: 5
spacing: 4
property var commandsShown: [
{
name: "" ,
sendDirectly: false ,
dontAddSpace: true
} ,
{
2026-08-12 19:50:32 -04:00
name: "new" ,
2026-07-12 09:51:09 -04:00
sendDirectly: true
} ,
]
ApiInputBoxIndicator {
// Model indicator. Souveraine's Ai service has no
// hardcoded models — getModel() is undefined until the
// server's /v1/models lands, so guard every access.
icon: "api"
text: Ai . getModel ( ) ? . name ? ? Translation . tr ( "No model" )
tooltipText: Translation . tr ( "Current model: %1\nSet it with %2model MODEL" ) . arg ( Ai . getModel ( ) ? . name ? ? Translation . tr ( "none" ) ) . arg ( root ? . commandPrefix ? ? "/" )
}
ApiInputBoxIndicator {
// Tool indicator
icon: "service_toolbox"
text: ( Ai . currentTool ? ? "" ) . charAt ( 0 ) . toUpperCase ( ) + ( Ai . currentTool ? ? "" ) . slice ( 1 )
tooltipText: Translation . tr ( "Current tool: %1\nSet it with %2tool TOOL" ) . arg ( Ai . currentTool ? ? "" ) . arg ( root ? . commandPrefix ? ? "/" )
}
2026-07-31 12:51:17 -04:00
ApiInputBoxIndicator {
// Tier-2 subconscious log. The ticker's long-press is the
// other way in, but the ticker only exists while a pass is
// actually running — without this the event log would be
// reachable only during the few seconds it is already on
// screen, which is to say not reachable.
icon: "psychology"
text: Ai . subconsciousEvents . length > 0 ? String ( Ai . subconsciousEvents . length ) : ""
tooltipText: Translation . tr ( "Subconscious event log" )
MouseArea {
anchors.fill: parent
// hoverEnabled false so hover still falls through to the
// tooltip's own MouseArea underneath; we only want clicks.
hoverEnabled: false
cursorShape: Qt . PointingHandCursor
onClicked: root . subconsciousPanelOpen = ! root . subconsciousPanelOpen
}
}
2026-07-12 09:51:09 -04:00
Item {
Layout.fillWidth: true
}
ButtonGroup {
// Command buttons
padding: 0
Repeater {
// Command buttons
model: commandButtonsRow . commandsShown
delegate: ApiCommandButton {
property string commandRepresentation: ` $ { root . commandPrefix } $ { modelData . name } `
buttonText: commandRepresentation
downAction: ( ) = > {
if ( modelData . sendDirectly ) {
root . handleInput ( commandRepresentation ) ;
} else {
messageInputField . text = commandRepresentation + ( modelData . dontAddSpace ? "" : " " ) ;
messageInputField . cursorPosition = messageInputField . text . length ;
messageInputField . forceActiveFocus ( ) ;
}
2026-08-12 19:50:32 -04:00
if ( modelData . name === "new" ) {
2026-07-12 09:51:09 -04:00
messageInputField . text = "" ;
}
}
}
}
}
}
}
}
2026-07-31 12:51:17 -04:00
// ── Tier-2 subconscious event log ────────────────────────────────────
// SubconsciousEventPanel is content, not a window: by design it carries no
// scrim, no close button and no placement — its own header says so, and
// names the gap ("no souveraine host mounts this yet"). Somebody has to be
// the host, and until now nobody was: the panel was written, deployed, and
// unreachable. That is exactly the failure SHELL-SURFACES.md exists to
// stop — a surface that reaches the tree but never the glass, silently,
// because nothing errors when nothing instantiates.
//
// The AI chat is the right host: the Tier-1 ticker already lives here and
// Tier 2 is the same stream after the fact. Chrome is ours; the panel stays
// placeable anywhere else later, viewtop included.
property bool subconsciousPanelOpen: false
Item {
id: subconsciousPanelHost
anchors.fill: parent
z: 100
visible: opacity > 0
opacity: root . subconsciousPanelOpen ? 1 : 0
Behavior on opacity {
animation: Appearance . animation . elementMoveFast . numberAnimation . createObject ( this )
}
Rectangle { // Scrim — tap outside the panel to dismiss
anchors.fill: parent
radius: Appearance . rounding . small
color: Appearance . colors . colScrim
MouseArea {
anchors.fill: parent
preventStealing: true
propagateComposedEvents: false
onClicked: root . subconsciousPanelOpen = false
}
}
// The panel ships no close affordance, so the host draws one. It sits
// above the panel rather than over it: the panel's own header already
// occupies its top-right with the event count.
RowLayout {
id: subconsciousChrome
anchors {
top: parent . top
left: parent . left
right: parent . right
margins: 8
}
height: 32
Item { Layout.fillWidth: true }
Item {
// Finger-sized hit area; the glyph alone is far too small a
// target on the phone.
Layout.preferredWidth: 32
Layout.preferredHeight: 32
MaterialSymbol {
anchors.centerIn: parent
text: "close"
iconSize: Appearance . font . pixelSize . larger
color: Appearance . colors . colSubtext
}
MouseArea {
anchors.fill: parent
cursorShape: Qt . PointingHandCursor
onClicked: root . subconsciousPanelOpen = false
}
}
}
SubconsciousEventPanel {
anchors {
top: subconsciousChrome . bottom
left: parent . left
right: parent . right
bottom: parent . bottom
leftMargin: 8
rightMargin: 8
bottomMargin: 8
topMargin: 4
}
}
}
2026-07-12 09:51:09 -04:00
}