mirror of
https://gitee.com/mateos/mateclaw.git
synced 2026-09-15 03:55:09 +08:00
fix(goal): retain acceptance access for paused and terminal goals
This commit is contained in:
parent
dec4c509c4
commit
54e7b9b9e0
@ -82,6 +82,15 @@ public class GoalController {
|
||||
return R.ok(goalService.toResponse(goalService.findActiveByConversation(conversationId)));
|
||||
}
|
||||
|
||||
@Operation(summary = "Read this conversation's goal history, including paused and terminal goals")
|
||||
@GetMapping("/by-conversation/{conversationId}/history")
|
||||
public R<List<GoalResponse>> history(@PathVariable String conversationId,
|
||||
@RequestParam(required = false) Long beforeId,
|
||||
@RequestParam(defaultValue = "20") int limit, Authentication auth) {
|
||||
requireOwner(conversationId, currentUsername(auth));
|
||||
return R.ok(goalService.toResponseList(goalService.listByConversation(conversationId, beforeId, limit)));
|
||||
}
|
||||
|
||||
@Operation(summary = "Get goal detail by id")
|
||||
@GetMapping("/{id}")
|
||||
public R<GoalResponse> get(@PathVariable Long id, Authentication auth) {
|
||||
|
||||
@ -41,6 +41,9 @@ public interface GoalService {
|
||||
/** Paged list filtered by status / owner. */
|
||||
List<GoalEntity> list(String status, String username, int limit);
|
||||
|
||||
/** Conversation-scoped history, newest id first, with an exclusive cursor. */
|
||||
List<GoalEntity> listByConversation(String conversationId, Long beforeId, int limit);
|
||||
|
||||
/** Sparse update. Throws if any terminal-state goal is targeted. */
|
||||
GoalEntity update(Long id, GoalUpdateRequest req, String username);
|
||||
|
||||
|
||||
@ -214,6 +214,16 @@ public class GoalServiceImpl implements GoalService {
|
||||
.last("LIMIT 1"));
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<GoalEntity> listByConversation(String conversationId, Long beforeId, int limit) {
|
||||
if (conversationId == null || conversationId.isBlank()) return List.of();
|
||||
return goalMapper.selectList(new LambdaQueryWrapper<GoalEntity>()
|
||||
.eq(GoalEntity::getConversationId, conversationId)
|
||||
.lt(beforeId != null, GoalEntity::getId, beforeId)
|
||||
.orderByDesc(GoalEntity::getId)
|
||||
.last("LIMIT " + Math.max(1, Math.min(50, limit))));
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<GoalEntity> list(String status, String username, int limit) {
|
||||
LambdaQueryWrapper<GoalEntity> w = new LambdaQueryWrapper<GoalEntity>()
|
||||
|
||||
@ -4,6 +4,8 @@ Open a conversation with an existing Goal, click the Goals button in its header,
|
||||
|
||||
User configuration, independent managed versions, binding checks and the shared completion gate are connected. Every current requirement needs a matching valid binding before a selected goal can complete under its existing completion rules. Automatic evaluation, explicit completeGoal and retries share that gate. Unselected goals retain existing behavior.
|
||||
|
||||
The conversation Goals panel includes paused and terminal goals, loading 20 at a time with an option to load older records. Paused goals still allow requirement edits, publication and checks; terminal goals only expose existing requirements and content. Closing the panel, switching conversations or leaving the page clears its contents, and a failed refresh clears the old list. Reading history does not resume execution.
|
||||
|
||||
## Managed version API
|
||||
|
||||
Prefix: `/api/v1/goals/{goalId}/json-acceptance`. An enabled account with conversation-owner or administrator permission is required. Preserve IDs, revisions and generations as strings in clients.
|
||||
|
||||
@ -4,6 +4,8 @@
|
||||
|
||||
当前已接通用户配置、独立受管版本、绑定检查及共享完成检查。选中模式后,所有当前要求必须具有匹配的有效绑定,才可在既有完成规则满足时完成;自动评估、显式 completeGoal 和重试均使用同一完成检查。未选中的 Goal 保持既有行为。
|
||||
|
||||
当前会话的“目标”面板也保留暂停、已完成等历史目标,每次读取20项,可加载更早记录。暂停目标仍可修订要求、发布和检查;终态目标只能查看要求与已有正文。关闭面板、切换会话或离开页面后清除面板内容;刷新失败时清除旧列表。读取历史不会恢复目标运行。
|
||||
|
||||
## 受管版本接口
|
||||
|
||||
接口前缀 `/api/v1/goals/{goalId}/json-acceptance`,需要启用账户及对话所有者或管理员权限。ID、revision 和 generation 在响应中使用字符串,客户端应原样保留。
|
||||
|
||||
@ -71,6 +71,29 @@ class GoalJsonAcceptanceIntegrationTest {
|
||||
return new GoalJsonAcceptanceService.ConfigureRequest(revision, "report", List.of(fields));
|
||||
}
|
||||
|
||||
@Test void conversationHistoryPreservesPausedAndCompletedGoalsWithExclusivePaging() {
|
||||
GoalEntity paused = goal(false);
|
||||
acceptance.configure(paused.getId(), "r", request(0, "summary"), alice);
|
||||
goals.pause(paused.getId(), alice);
|
||||
GoalCreateRequest next = new GoalCreateRequest();
|
||||
next.setConversationId(paused.getConversationId()); next.setWorkspaceId(1L); next.setAgentId(1L);
|
||||
next.setTitle("Next report"); next.setDescription("History fixture"); next.setPersistentExecution(false);
|
||||
GoalEntity completed = goals.create(next, alice);
|
||||
goals.markCompleted(completed.getId(), null);
|
||||
GoalEntity deleted = goals.create(next, alice);
|
||||
jdbc.update("UPDATE mate_agent_goal SET deleted=1 WHERE id=?", deleted.getId());
|
||||
goal(false); // A newer goal in another conversation must not enter this page.
|
||||
var first = goals.listByConversation(paused.getConversationId(), null, 1);
|
||||
assertEquals(List.of(completed.getId()), first.stream().map(GoalEntity::getId).toList());
|
||||
assertEquals(GoalStatus.COMPLETED, first.getFirst().getStatus());
|
||||
var second = goals.listByConversation(paused.getConversationId(), completed.getId(), 1);
|
||||
assertEquals(List.of(paused.getId()), second.stream().map(GoalEntity::getId).toList());
|
||||
assertEquals(GoalStatus.PAUSED, second.getFirst().getStatus());
|
||||
assertTrue(second.getFirst().isJsonAcceptanceRequired());
|
||||
assertTrue(goals.listByConversation(paused.getConversationId(), paused.getId(), 20).isEmpty());
|
||||
assertNull(goals.findActiveByConversation(paused.getConversationId()), "History must not revive an inactive goal");
|
||||
}
|
||||
|
||||
@Test void ownerCanPersistAndReviseRequirementsWithoutAcceptingAStaleEdit() {
|
||||
GoalEntity goal = goal(false);
|
||||
assertFalse(acceptance.get(goal.getId(), alice).required());
|
||||
|
||||
@ -2,6 +2,8 @@ package vip.mate.goal.controller;
|
||||
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import java.util.List;
|
||||
import static org.mockito.ArgumentMatchers.anyInt;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
@ -162,6 +164,25 @@ class GoalControllerTest {
|
||||
|
||||
// ==================== find / get ====================
|
||||
|
||||
@Test
|
||||
void historyRequiresConversationOwnershipBeforeReadingAnyRows() {
|
||||
when(conversationService.isConversationOwner("other", "alice")).thenReturn(false);
|
||||
assertEquals(403, assertThrows(MateClawException.class,
|
||||
() -> controller.history("other", null, 20, auth)).getCode());
|
||||
verify(goalService, never()).listByConversation(anyString(), any(), anyInt());
|
||||
}
|
||||
|
||||
@Test
|
||||
void historyKeepsTheExclusiveLongCursorAndMapsEveryStatus() {
|
||||
when(conversationService.isConversationOwner("conv-1", "alice")).thenReturn(true);
|
||||
long cursor = 9223372036854775801L;
|
||||
var rows = List.of(goal(2L, "conv-1", GoalStatus.COMPLETED), goal(1L, "conv-1", GoalStatus.PAUSED));
|
||||
var responses = List.of(resp(2L, GoalStatus.COMPLETED), resp(1L, GoalStatus.PAUSED));
|
||||
when(goalService.listByConversation("conv-1", cursor, 20)).thenReturn(rows);
|
||||
when(goalService.toResponseList(rows)).thenReturn(responses);
|
||||
assertEquals(responses, controller.history("conv-1", cursor, 20, auth).getData());
|
||||
}
|
||||
|
||||
@Test
|
||||
void findActive_returnsNull_whenNoActiveGoal() {
|
||||
when(conversationService.isConversationOwner("conv-1", "alice")).thenReturn(true);
|
||||
|
||||
@ -1844,6 +1844,9 @@ export const goalApi = {
|
||||
findActive: (conversationId: string) =>
|
||||
http.get<Goal | null>(`/goals/by-conversation/${encId(conversationId)}`),
|
||||
|
||||
history: (conversationId: string, beforeId?: string) =>
|
||||
http.get<Goal[]>(`/goals/by-conversation/${encId(conversationId)}/history`, { params: { beforeId, limit: 20 } }),
|
||||
|
||||
get: (id: string) => http.get<Goal>(`/goals/${id}`),
|
||||
|
||||
events: (id: string, limit = 100) =>
|
||||
|
||||
@ -13,15 +13,18 @@
|
||||
</button>
|
||||
|
||||
<div class="gp-head">
|
||||
<h2 class="gp-head__title">{{ t('plans.activeGoals') }}</h2>
|
||||
<h2 class="gp-head__title">{{ title || t('plans.activeGoals') }}</h2>
|
||||
</div>
|
||||
|
||||
<div v-if="loading" class="pd-loading">{{ t('common.loading') }}</div>
|
||||
<button v-if="showRefresh" type="button" :disabled="loading" @click="$emit('refresh')">{{ t('common.refresh') }}</button>
|
||||
<p v-if="error" role="alert">{{ error }}</p>
|
||||
<div v-if="loading && !goals.length" class="pd-loading">{{ t('common.loading') }}</div>
|
||||
<el-empty v-else-if="!goals.length" :description="t('plans.noGoals')" />
|
||||
|
||||
<div v-else class="gp-list">
|
||||
<div v-for="goal in goals" :key="goal.id" class="gp-goal">
|
||||
<div class="gp-goal__title">{{ cleanGoal(goal.title) }}</div>
|
||||
<p v-if="showRefresh">{{ t('goalJsonAcceptance.historyStatus.' + goal.status) }}</p>
|
||||
<p v-if="showDesc(goal)" class="gp-goal__desc">{{ cleanGoal(goal.description) }}</p>
|
||||
|
||||
<div class="gp-goal__score" v-if="goal.completionScore != null">
|
||||
@ -46,6 +49,7 @@
|
||||
<ExecutionEvidenceList v-if="goal.conversationId" :conversation-id="goal.conversationId" :goal-id="goal.id" />
|
||||
</div>
|
||||
</div>
|
||||
<button v-if="hasMore" type="button" :disabled="loading" @click="$emit('load-more')">{{ t('goalJsonAcceptance.historyLoadMore') }}</button>
|
||||
</aside>
|
||||
</div>
|
||||
</Transition>
|
||||
@ -63,9 +67,13 @@ const props = defineProps<{
|
||||
open: boolean
|
||||
goals: Goal[]
|
||||
loading: boolean
|
||||
title?: string
|
||||
error?: string
|
||||
hasMore?: boolean
|
||||
showRefresh?: boolean
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{ close: [] }>()
|
||||
const emit = defineEmits<{ close: []; refresh: []; 'load-more': [] }>()
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
|
||||
69
mateclaw-ui/src/components/goal/ConversationGoalsControl.vue
Normal file
69
mateclaw-ui/src/components/goal/ConversationGoalsControl.vue
Normal file
@ -0,0 +1,69 @@
|
||||
<template>
|
||||
<button class="conversation-goals-button" type="button" :title="t('plans.goals')"
|
||||
:aria-label="t('plans.goals')" :aria-expanded="open" @click="show">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><circle cx="12" cy="12" r="10"/><circle cx="12" cy="12" r="6"/><circle cx="12" cy="12" r="2"/></svg>
|
||||
</button>
|
||||
<GoalsPanel v-if="open" :open="open" :goals="goals" :loading="loading"
|
||||
:title="t('plans.goals')" :error="error" :has-more="hasMore" :show-refresh="true"
|
||||
@close="close" @refresh="load(false)" @load-more="load(true)" />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, watch, onBeforeUnmount, onDeactivated } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { goalApi, type Goal } from '@/api'
|
||||
import GoalsPanel from '@/components/agents/GoalsPanel.vue'
|
||||
|
||||
const props = defineProps<{ conversationId: string }>()
|
||||
const { t } = useI18n()
|
||||
const open = ref(false)
|
||||
const loading = ref(false)
|
||||
const goals = ref<Goal[]>([])
|
||||
const error = ref('')
|
||||
const hasMore = ref(false)
|
||||
let request = 0
|
||||
|
||||
function close() {
|
||||
request++
|
||||
open.value = false
|
||||
loading.value = false
|
||||
goals.value = []
|
||||
error.value = ''
|
||||
hasMore.value = false
|
||||
}
|
||||
async function show() {
|
||||
open.value = true
|
||||
await load(false)
|
||||
}
|
||||
async function load(append: boolean) {
|
||||
if (loading.value || !props.conversationId) return
|
||||
const cursor = append ? goals.value.at(-1)?.id : undefined
|
||||
const token = ++request
|
||||
const conversation = props.conversationId
|
||||
loading.value = true
|
||||
error.value = ''
|
||||
if (!append) goals.value = []
|
||||
try {
|
||||
const response: any = await goalApi.history(conversation, cursor)
|
||||
if (token !== request || conversation !== props.conversationId || !open.value) return
|
||||
const page: Goal[] = response?.data ?? []
|
||||
goals.value = append ? [...goals.value, ...page] : page
|
||||
hasMore.value = page.length === 20
|
||||
} catch {
|
||||
if (token !== request || conversation !== props.conversationId || !open.value) return
|
||||
goals.value = []
|
||||
hasMore.value = false
|
||||
error.value = t('goalJsonAcceptance.historyLoadFailed')
|
||||
} finally {
|
||||
if (token === request) loading.value = false
|
||||
}
|
||||
}
|
||||
watch(() => props.conversationId, close)
|
||||
onBeforeUnmount(close)
|
||||
onDeactivated(close)
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.conversation-goals-button { width:30px; height:30px; border:1px solid var(--mc-border); background:var(--mc-panel-raised); border-radius:10px; cursor:pointer; display:flex; align-items:center; justify-content:center; color:var(--mc-text-secondary); }
|
||||
.conversation-goals-button:hover { border-color:var(--mc-danger); color:var(--mc-danger); }
|
||||
</style>
|
||||
@ -0,0 +1,67 @@
|
||||
import { createApp, h, nextTick, reactive, ref, KeepAlive } from 'vue'
|
||||
import { createI18n } from 'vue-i18n'
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import ConversationGoalsControl from '../ConversationGoalsControl.vue'
|
||||
import { goalApi } from '@/api'
|
||||
import en from '@/i18n/locales/en-US'
|
||||
|
||||
vi.mock('@/api', () => ({ goalApi: { history: vi.fn() } }))
|
||||
vi.mock('@/components/agents/GoalsPanel.vue', () => ({ default: {
|
||||
props: ['goals', 'loading', 'error', 'hasMore'], emits: ['close', 'refresh', 'load-more'],
|
||||
template: '<section role="dialog"><p v-for="goal in goals" :key="goal.id">{{goal.id}} {{goal.status}}</p><span role="alert">{{error}}</span><button @click="$emit(\'close\')">Close</button><button @click="$emit(\'refresh\')">Refresh</button><button v-if="hasMore" @click="$emit(\'load-more\')">Older</button></section>',
|
||||
} }))
|
||||
const apps: ReturnType<typeof createApp>[] = []
|
||||
async function flush() { await Promise.resolve(); await Promise.resolve(); await nextTick() }
|
||||
function mount() {
|
||||
const props = reactive({ conversationId: 'owned-conversation' })
|
||||
const active = ref(true)
|
||||
const host = document.createElement('div'); document.body.append(host)
|
||||
const app = createApp({ render: () => h(KeepAlive, null, () => active.value ? h(ConversationGoalsControl, props) : h('div')) })
|
||||
app.use(createI18n({ legacy: false, locale: 'en', messages: { en } })); app.mount(host); apps.push(app)
|
||||
return { host, props, active }
|
||||
}
|
||||
async function click(host: HTMLElement, text: string) {
|
||||
Array.from(host.querySelectorAll('button')).find(b => b.textContent === text)!.click(); await flush()
|
||||
}
|
||||
afterEach(() => { apps.splice(0).forEach(a => a.unmount()); document.body.innerHTML = ''; vi.resetAllMocks() })
|
||||
|
||||
describe('conversation goal history', () => {
|
||||
it('loads only on demand and preserves paused/terminal goals and opaque cursors', async () => {
|
||||
const page = Array.from({ length: 20 }, (_, i) => ({ id: String(9223372036854775800n - BigInt(i)), status: i ? 'paused' : 'completed' }))
|
||||
vi.mocked(goalApi.history).mockResolvedValueOnce({ data: page } as any).mockResolvedValueOnce({ data: [] } as any)
|
||||
const { host } = mount(); expect(goalApi.history).not.toHaveBeenCalled()
|
||||
host.querySelector('button')!.click(); await flush()
|
||||
expect(host.textContent).toContain('completed'); expect(host.textContent).toContain('paused')
|
||||
await click(host, 'Older')
|
||||
expect(goalApi.history).toHaveBeenLastCalledWith('owned-conversation', page.at(-1)!.id)
|
||||
expect(host.textContent).not.toContain('Older')
|
||||
})
|
||||
it('discards delayed history when changing conversation or closing the panel', async () => {
|
||||
let resolve!: (value: any) => void
|
||||
vi.mocked(goalApi.history).mockImplementation(() => new Promise(r => { resolve = r }))
|
||||
const { host, props } = mount(); host.querySelector('button')!.click(); await flush()
|
||||
props.conversationId = 'new-conversation'; await flush()
|
||||
resolve({ data: [{ id: 'old', status: 'paused' }] }); await flush()
|
||||
expect(host.querySelector('[role=dialog]')).toBeNull(); expect(host.textContent).not.toContain('old')
|
||||
host.querySelector('button')!.click(); await flush(); await click(host, 'Close')
|
||||
resolve({ data: [{ id: 'late', status: 'completed' }] }); await flush()
|
||||
expect(host.querySelector('[role=dialog]')).toBeNull()
|
||||
})
|
||||
it('discards an in-flight page when a cached route is deactivated', async () => {
|
||||
let resolve!: (value: any) => void
|
||||
vi.mocked(goalApi.history).mockImplementation(() => new Promise(r => { resolve = r }))
|
||||
const { host, active } = mount(); host.querySelector('button')!.click(); await flush()
|
||||
active.value = false; await flush()
|
||||
resolve({ data: [{ id: 'hidden-route-goal', status: 'paused' }] }); await flush()
|
||||
active.value = true; await flush()
|
||||
expect(host.querySelector('[role=dialog]')).toBeNull()
|
||||
expect(host.textContent).not.toContain('hidden-route-goal')
|
||||
})
|
||||
it('clears the visible page after a refresh loses access', async () => {
|
||||
vi.mocked(goalApi.history).mockResolvedValueOnce({ data: [{ id: 'private-goal', status: 'paused' }] } as any)
|
||||
.mockRejectedValueOnce({ code: 403 })
|
||||
const { host } = mount(); host.querySelector('button')!.click(); await flush()
|
||||
expect(host.textContent).toContain('private-goal'); await click(host, 'Refresh')
|
||||
expect(host.textContent).not.toContain('private-goal'); expect(host.querySelector('[role=alert]')!.textContent).toContain('checking your access')
|
||||
})
|
||||
})
|
||||
@ -38,6 +38,9 @@ export default {
|
||||
}
|
||||
},
|
||||
goalJsonAcceptance: {
|
||||
historyLoadFailed: 'Could not load this conversation’s goals. Reload after checking your access.',
|
||||
historyLoadMore: 'Load older goals',
|
||||
historyStatus: { active: 'Active', paused: 'Paused', completed: 'Completed', exhausted: 'Budget exhausted', abandoned: 'Abandoned' },
|
||||
"title": "Managed JSON acceptance",
|
||||
"scope": "Require the listed top-level fields in a platform-managed JSON object, with values other than null (false, zero and empty strings are allowed). Text claims and ordinary file checks cannot satisfy this requirement.",
|
||||
"refresh": "Reload requirements",
|
||||
|
||||
@ -38,6 +38,9 @@ export default {
|
||||
}
|
||||
},
|
||||
goalJsonAcceptance: {
|
||||
historyLoadFailed: '无法读取此会话的目标。请确认访问权限后重新读取。',
|
||||
historyLoadMore: '加载更早的目标',
|
||||
historyStatus: { active: '进行中', paused: '已暂停', completed: '已完成', exhausted: '预算耗尽', abandoned: '已放弃' },
|
||||
"title": "受管 JSON 验收",
|
||||
"scope": "要求平台受管 JSON 对象存在指定顶层字段且值不为 null(允许 false、0 和空字符串)。文本声明和普通文件诊断不能满足这项要求。",
|
||||
"refresh": "重新读取要求",
|
||||
|
||||
@ -76,11 +76,8 @@
|
||||
<div v-else class="no-agent-hint">{{ $t('chat.selectAgent') }}</div>
|
||||
</div>
|
||||
<div class="chat-header-right">
|
||||
<button v-if="currentConversationGoal" class="header-btn" type="button"
|
||||
:title="$t('plans.goals')" :aria-label="$t('plans.goals')"
|
||||
:aria-expanded="conversationGoalOpen" @click="conversationGoalOpen = true">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><circle cx="12" cy="12" r="10"/><circle cx="12" cy="12" r="6"/><circle cx="12" cy="12" r="2"/></svg>
|
||||
</button>
|
||||
<ConversationGoalsControl v-if="currentConversationId && !isEphemeralConversation(currentConversationId)"
|
||||
:key="currentConversationId" :conversation-id="currentConversationId" />
|
||||
<!-- Model selector — Issue #81 v2 R3: always pass full providers + show-all-states
|
||||
so unhealthy rows render as dimmed entries with status chips and a Fix
|
||||
button instead of disappearing entirely. -->
|
||||
@ -116,10 +113,6 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<GoalsPanel v-if="currentConversationGoal && conversationGoalOpen" :key="currentConversationGoal.id"
|
||||
:open="conversationGoalOpen" :goals="[currentConversationGoal]" :loading="false"
|
||||
@close="conversationGoalOpen = false" />
|
||||
|
||||
<TeamWorkerBanner
|
||||
v-if="workerRunContext"
|
||||
:run-id="workerRunContext.runId"
|
||||
@ -360,7 +353,7 @@ import { buildViewerModelProviders } from '@/utils/viewerModelProviders'
|
||||
import GoalSetInlinePrompt from '@/components/goal/GoalSetInlinePrompt.vue'
|
||||
import GoalSystemLine from '@/components/goal/GoalSystemLine.vue'
|
||||
|
||||
const GoalsPanel = defineAsyncComponent(() => import('@/components/agents/GoalsPanel.vue'))
|
||||
const ConversationGoalsControl = defineAsyncComponent(() => import('@/components/goal/ConversationGoalsControl.vue'))
|
||||
|
||||
// ============ Talk Mode ============
|
||||
const showTalkMode = ref(false)
|
||||
@ -1403,11 +1396,6 @@ watch([selectedAgentId, currentConversationId], () => {
|
||||
// avatar ring listens on goalStore.activeGoalByConv[cid]; without this
|
||||
// fetch the ring would only appear after an SSE event mutated the store.
|
||||
const goalStore = useGoalStore()
|
||||
const conversationGoalOpen = ref(false)
|
||||
const currentConversationGoal = computed(() => currentConversationId.value
|
||||
? goalStore.activeGoal(currentConversationId.value) : null)
|
||||
watch(() => currentConversationGoal.value?.id, () => { conversationGoalOpen.value = false })
|
||||
watch(currentConversationId, () => { conversationGoalOpen.value = false })
|
||||
const workspaceStore = useWorkspaceStore()
|
||||
const currentWorkspaceId = computed(() => workspaceStore.currentWorkspaceId ?? '1')
|
||||
const canConfigureModels = computed(() => workspaceStore.isGlobalAdmin)
|
||||
|
||||
Loading…
Reference in New Issue
Block a user