Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
f780e36
Add debug log support
Steve-Mcl Feb 11, 2026
593586d
Refactor context option properties and enhance debug log context actions
Steve-Mcl Feb 12, 2026
1a85ac6
Add support for feature flags in expert assistant context
Steve-Mcl Feb 17, 2026
c62b07e
Merge branch 'main' into 6655-expert-debuglog-context
Steve-Mcl Feb 17, 2026
6333326
Remove unused PackageResourceCard component
Steve-Mcl Feb 18, 2026
79d3aad
Merge branch '6655-expert-debuglog-context' of https://github.com/Flo…
Steve-Mcl Feb 18, 2026
8fad707
Merge branch 'main' into 6655-expert-debuglog-context
ppawlowski Feb 18, 2026
f07e143
use mutations to reset debug log array
Steve-Mcl Feb 19, 2026
bacbc41
Merge branch '6655-expert-debuglog-context' of https://github.com/Flo…
Steve-Mcl Feb 19, 2026
9a0949d
Merge branch 'main' into 6655-expert-debuglog-context
Steve-Mcl Feb 19, 2026
3569720
Merge branch 'main' into 6655-expert-debuglog-context
Steve-Mcl Feb 26, 2026
a24fc73
fix syntax error from merge conflict resolution
Steve-Mcl Feb 26, 2026
c94518c
Merge branch 'main' of github.com:FlowFuse/flowfuse into 6655-expert-…
Feb 26, 2026
f93555c
Merge branch 'main' of github.com:FlowFuse/flowfuse into 6655-expert-…
Feb 26, 2026
1ea2fa0
Merge branch 'main' into 6655-expert-debuglog-context
cstns Mar 3, 2026
38572f6
Merge branch 'main' into 6655-expert-debuglog-context
Steve-Mcl Mar 4, 2026
f467099
reset assistant state if instance/device changes state
Steve-Mcl Mar 4, 2026
e7781ab
Ensure feature checks for palette management and debug log match know…
Steve-Mcl Mar 4, 2026
35c09a1
Fix debug log chip tip
Steve-Mcl Mar 4, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions frontend/src/components/expert/Expert.vue
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,22 @@ export default {
'product/expert/addWelcomeMessageIfNeeded'
)
}
},
'instance.meta.state': {
handler (newState) {
if (this.isEditorContext && newState !== 'running') {
this.reset() // reset assistant state
}
},
deep: true
},
'device.status': {
handler (newState) {
if (this.isEditorContext && newState !== 'running') {
this.reset() // reset assistant state
}
},
deep: true
}
},
mounted () {
Expand Down Expand Up @@ -276,6 +292,7 @@ export default {
'setAbortController',
'resetSessionTimer'
]),
...mapActions('product/assistant', ['reset']),

async handleSendMessage (query) {
if (!query.trim()) return
Expand Down
10 changes: 8 additions & 2 deletions frontend/src/components/expert/ExpertChatInput.vue
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,8 @@
<div class="left">
<context-selector v-if="!isOperatorAgent" />
<div class="context-items-container" @wheel="horizontalScrolling">
<include-context-item v-for="(context, index) in selectedContext" :key="index" :contextItem="context" />
<include-context-item v-for="(context, index) in selectedContextFiltered" :key="index" :contextItem="context" />
<include-debug-context-button v-if="hasDebugLogsSelected && !isOperatorAgent" />
<include-selection-button v-if="hasUserSelection && !isOperatorAgent" />
</div>
</div>
Expand Down Expand Up @@ -75,6 +76,7 @@ import ResizeBar from '../ResizeBar.vue'
import CapabilitiesSelector from './components/CapabilitiesSelector.vue'
import ContextSelector from './components/ContextSelector.vue'
import IncludeContextItem from './components/IncludeContextItem.vue'
import IncludeDebugContextButton from './components/IncludeDebugContextButton.vue'
import IncludeSelectionButton from './components/IncludeSelectionButton.vue'

export default {
Expand All @@ -83,6 +85,7 @@ export default {
CapabilitiesSelector,
ContextSelector,
IncludeContextItem,
IncludeDebugContextButton,
IncludeSelectionButton,
ResizeBar
},
Expand Down Expand Up @@ -131,7 +134,7 @@ export default {
}
},
computed: {
...mapGetters('product/assistant', ['hasUserSelection', 'getSelectedContext']),
...mapGetters('product/assistant', ['getSelectedContext', 'hasDebugLogsSelected', 'hasUserSelection']),
isInputDisabled () {
if (this.isSessionExpired) return true
if (this.isGenerating) return true
Expand All @@ -155,6 +158,9 @@ export default {
return []
}
return this.getSelectedContext
},
selectedContextFiltered () {
return this.selectedContext.filter(c => c.showAsChip !== false)
}
},
mounted () {
Expand Down
32 changes: 18 additions & 14 deletions frontend/src/components/expert/components/ContextSelector.vue
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,10 @@
:hide-chevron="true"
:icon-only="true"
return-model
label-key="name"
label-key="label"
placeholder="Context"
open-above
:min-options-width="220"
:options-min-width="250"
align-right
@option-selected="selectItem"
>
Expand Down Expand Up @@ -76,19 +76,23 @@ export default {
pluralize,
...mapActions('product/assistant', ['setSelectedContext']),
selectItem (option) {
const cleanOption = (option) => ({
value: option.value,
name: option.name,
title: option.title,
icon: option.icon
})
const cleanedOption = cleanOption(option)
const currentSelection = this.selectedContext || []
const exists = currentSelection.some(c => c.value === cleanedOption.value)
if (exists) {
return // already selected, do nothing
if (option.onSelectAction) {
this.$store.dispatch(`product/assistant/${option.onSelectAction}`)
} else {
const cleanOption = (option) => ({
value: option.value,
name: option.name,
label: option.label,
icon: option.icon
})
const cleanedOption = cleanOption(option)
const currentSelection = this.selectedContext || []
const exists = currentSelection.some(c => c.value === cleanedOption.value)
if (exists) {
return // already selected, do nothing
}
this.setSelectedContext([...currentSelection, cleanedOption].filter(Boolean))
}
this.setSelectedContext([...currentSelection, cleanedOption].filter(Boolean))
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
<template>
<ContextChip
class="flow-selection-button"
:modelValue="modelValue"
@toggle="toggleSelection"
>
<template #text>
<span>Debug</span>
<span class="counter italic" :title="selectionTitle">( {{ selectedCounter }} {{ pluralize('log', selectedCounter) }} )</span>
</template>
</ContextChip>
</template>

<script>
import { mapActions, mapState } from 'vuex'

import { pluralize } from '../../../composables/String.js'

import ContextChip from './ContextChip/index.vue'

export default {
name: 'IncludeDebugContextButton',
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because this component is a wrapper over the default chip, my feeling is that it's a chip too, not a button

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
name: 'IncludeDebugContextButton',
name: 'DebugContextChip',

components: {
ContextChip
},
props: {
modelValue: {
Copy link
Contributor

@cstns cstns Mar 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

passing modelValues is not needed in the case of context chips, their rendering logic is already determined in the parent they're used in with the v-if directives. This prop is also not passed when the component is used.

model values are useful when the parent component need to watch over the data passed to child components and it mutates, which we're not doing

Not a blocker, I will revisit the context chips as part of the refactoring task.

type: Boolean,
required: false,
default: true
}
},
emits: ['update:modelValue'],
computed: {
...mapState('product/assistant', ['debugLog']),
selectedCounter () {
return this.debugLog.length
},
selectionTitle () {
const map = {}
this.debugLog.forEach(n => {
map[n.type] = (map[n.type] ?? 0) + 1
})

const tipBuilder = (logEntry, index) => {
logEntry = logEntry || {}
const level = logEntry.level || ''
const nonDebugLevel = level !== 'debug' ? level : ''
const topic = logEntry.metadata?.topic || ''
const name = logEntry.source?.name || logEntry.source?.id || logEntry.metadata?.path || ''
const format = logEntry.metadata?.format || ''
const type = logEntry.source?.type || ''
const property = logEntry.metadata?.property || ''
const strBuilder = []
if (name) strBuilder.push(`node: ${name}`)
if (topic) strBuilder.push(`topic: ${topic}`)
if (nonDebugLevel) {
if (type) {
strBuilder.push(`${type} : (${nonDebugLevel})`)
}
}
if (property) {
if (format) {
strBuilder.push(`property: ${property} ${format}`)
} else {
strBuilder.push(`property: ${property}`)
}
}
return `${index + 1}: ${strBuilder.join(', ')}`
}

const list = this.debugLog.map(tipBuilder).join('\n')

return `Selected Logs: \n${list}`
}
},
methods: {
...mapActions('product/assistant', ['resetDebugLogContext']),
pluralize,
toggleSelection () {
this.$emit('update:modelValue', !this.modelValue)
this.resetDebugLogContext()
}
}
}
</script>

<style lang="scss">
.flow-selection-button {
.text {
.counter {
color: $ff-grey-500;
margin-left: 4px;
font-size: $ff-funit-xs;
}
}
}
</style>
6 changes: 1 addition & 5 deletions frontend/src/store/modules/context/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -60,16 +60,12 @@ const getters = {
if (rootState.product.assistant.selectedContext?.some(e => e.value === 'palette')) {
palette = rootGetters['product/assistant/paletteContribOnly']
}
let debugLog = null
if (rootState.product.assistant.selectedContext?.some(e => e.value === 'debug')) {
debugLog = rootGetters['product/assistant/debugLog']
}

return {
assistantVersion: rootState.product.assistant.version,
assistantFeatures: rootState.product.assistant.assistantFeatures,
palette,
debugLog,
debugLog: rootGetters['product/assistant/debugLog'],
userId: rootState.account?.user?.id || null,
teamId: rootState.account?.team?.id || null,
teamSlug: rootState.account?.team?.slug || null,
Expand Down
Loading
Loading