From 63b0cad5cff3111165ae3acf83dcbcc582c61357 Mon Sep 17 00:00:00 2001
From: sanil-23
Date: Thu, 4 Jun 2026 20:51:35 +0530
Subject: [PATCH 01/10] feat(task-board): surface autonomous runs as live task
sessions
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Autonomous task-board runs now materialise as top-level "task session"
conversation threads (labels: ["tasks"]) so they appear in Conversations →
Tasks like a manually-run todo, stream live via the web-channel bridge
(client_id="system" + TurnStateMirror), and finalise with a chat_done
terminal event so the session stops "processing". Cards link to their
session (sessionThreadId) and surface a "View work" action that opens the
exact session.
- task_session module: create_session_thread + append_final
- dispatcher: stamp sessionThreadId, stream via spawn_progress_bridge,
emit chat_done/chat_error; cancellable via an ACTIVE_RUNS registry +
cancel_session wired into channel_web_cancel (so chat Cancel works on
task threads), with race-safe write-back
- todos: set_session_thread RPC/ops; todos_add carries source_metadata
- approve flow: idempotent (dedup by source external_id) + hide
already-picked-up items from the task-sources inbox
- nav: View work opens the exact thread on the Tasks tab; Settings →
Task Sources back returns to the opener; Intelligence tab is URL-backed;
task-session threads excluded from the per-thread agent board list
- i18n: conversations.taskKanban.viewWork across all locales
Co-Authored-By: Claude
---
.../intelligence/IntelligenceTasksTab.tsx | 82 +++++-
.../settings/hooks/useSettingsNavigation.ts | 2 +
app/src/lib/i18n/ar.ts | 1 +
app/src/lib/i18n/bn.ts | 1 +
app/src/lib/i18n/de.ts | 1 +
app/src/lib/i18n/en.ts | 1 +
app/src/lib/i18n/es.ts | 1 +
app/src/lib/i18n/fr.ts | 1 +
app/src/lib/i18n/hi.ts | 1 +
app/src/lib/i18n/id.ts | 1 +
app/src/lib/i18n/it.ts | 1 +
app/src/lib/i18n/ko.ts | 1 +
app/src/lib/i18n/pl.ts | 1 +
app/src/lib/i18n/pt.ts | 1 +
app/src/lib/i18n/ru.ts | 1 +
app/src/lib/i18n/zh-CN.ts | 1 +
app/src/pages/Conversations.tsx | 31 ++-
app/src/pages/Intelligence.tsx | 24 +-
.../components/TaskKanbanBoard.tsx | 27 +-
.../pages/conversations/utils/threadFilter.ts | 2 +-
app/src/services/api/todosApi.ts | 19 ++
app/src/types/turnState.ts | 4 +
src/openhuman/agent/mod.rs | 1 +
src/openhuman/agent/task_board.rs | 10 +
src/openhuman/agent/task_dispatcher.rs | 250 ++++++++++++++++-
src/openhuman/agent/task_session.rs | 251 ++++++++++++++++++
src/openhuman/channels/providers/web.rs | 14 +-
.../threads/turn_state/mirror_tests.rs | 1 +
src/openhuman/todos/ops.rs | 49 ++++
src/openhuman/todos/schemas.rs | 55 +++-
30 files changed, 809 insertions(+), 27 deletions(-)
create mode 100644 src/openhuman/agent/task_session.rs
diff --git a/app/src/components/intelligence/IntelligenceTasksTab.tsx b/app/src/components/intelligence/IntelligenceTasksTab.tsx
index 9122fb6260..aa392f6922 100644
--- a/app/src/components/intelligence/IntelligenceTasksTab.tsx
+++ b/app/src/components/intelligence/IntelligenceTasksTab.tsx
@@ -28,6 +28,7 @@ import { useNavigate } from 'react-router-dom';
import { useT } from '../../lib/i18n/I18nContext';
import { TaskKanbanBoard } from '../../pages/conversations/components/TaskKanbanBoard';
+import { isTaskThread } from '../../pages/conversations/utils/threadFilter';
import { threadApi } from '../../services/api/threadApi';
import {
TASK_SOURCES_THREAD_ID,
@@ -366,7 +367,11 @@ export default function IntelligenceTasksTab() {
card.id,
'in_progress'
);
- if (mountedRef.current) setPersonalBoard(startedBoard);
+ // Link the card to its session thread so the board offers "View session".
+ const linkedBoard = await todosApi
+ .setSessionThread(USER_TASKS_THREAD_ID, card.id, thread.id)
+ .catch(() => startedBoard);
+ if (mountedRef.current) setPersonalBoard(linkedBoard);
dispatch(setSelectedThread(thread.id));
dispatch(setToolTimelineForThread({ threadId: thread.id, entries: [] }));
@@ -399,12 +404,36 @@ export default function IntelligenceTasksTab() {
const now = new Date().toISOString();
setActionError(null);
try {
+ const sourceExternalId = readSourceMetadata(sourceCard.sourceMetadata).externalId;
+ // Idempotent approve: if this source item was already promoted to
+ // user-tasks (picked up / running), don't add a second card — just
+ // retire the inbox card. Stops the duplicate when an edited item is
+ // re-offered for approval.
+ const alreadyPromoted = sourceExternalId
+ ? personalBoard?.cards.some(
+ c => readSourceMetadata(c.sourceMetadata).externalId === sourceExternalId
+ )
+ : false;
+ if (alreadyPromoted) {
+ const sourceSaved = await todosApi.updateStatus(
+ TASK_SOURCES_THREAD_ID,
+ sourceCard.id,
+ 'done'
+ );
+ if (mountedRef.current) {
+ setTaskSourcesBoard(sourceSaved);
+ setRefiningCard(null);
+ }
+ return;
+ }
const added = await todosApi.add({
threadId: USER_TASKS_THREAD_ID,
content: draft.title,
status: 'todo',
objective: draft.objective,
notes: draft.notes,
+ // Stamp the source link so the inbox can detect it's now picked up.
+ sourceMetadata: sourceCard.sourceMetadata,
});
const created =
added.cards.find(card => card.title === draft.title && card.updatedAt >= now) ??
@@ -444,7 +473,7 @@ export default function IntelligenceTasksTab() {
if (mountedRef.current) setActionError(t('intelligence.tasks.sourcePlan.createFailed'));
}
},
- [t]
+ [t, personalBoard]
);
// ── derived agent board list (read-only) ─────────────────────────────
@@ -452,16 +481,36 @@ export default function IntelligenceTasksTab() {
const threadMap = new Map(threads.map(th => [th.id, th]));
const allThreadIds = new Set([...Object.keys(liveBoards), ...Object.keys(persistedBoards)]);
+ // Hide task-source items already picked up (promoted to the user-tasks board)
+ // so an already-running task isn't re-offered for approval in the inbox.
+ const pickedUpExternalIds = new Set(
+ (personalBoard?.cards ?? [])
+ .map(c => readSourceMetadata(c.sourceMetadata).externalId)
+ .filter((id): id is string => Boolean(id))
+ );
+ const visibleTaskSourcesBoard: TaskBoard | null = taskSourcesBoard
+ ? {
+ ...taskSourcesBoard,
+ cards: taskSourcesBoard.cards.filter(
+ c => !pickedUpExternalIds.has(readSourceMetadata(c.sourceMetadata).externalId ?? '')
+ ),
+ }
+ : null;
+
const boardEntries: ThreadTaskBoard[] = [];
for (const threadId of allThreadIds) {
if (threadId === USER_TASKS_THREAD_ID) continue; // personal board rendered separately
if (threadId === TASK_SOURCES_THREAD_ID) continue; // task sources rendered separately
+ const thread = threadMap.get(threadId);
+ // Skip task SESSION threads (autonomous `task-*` runs + manual task-labelled
+ // threads): their cards already appear on the user-tasks / task-sources
+ // boards and the session is reachable via the card's "View work" — rendering
+ // their live board here just duplicates those cards as redundant tables.
+ if (threadId.startsWith('task-') || (thread && isTaskThread(thread))) continue;
const liveBoard = liveBoards[threadId];
const persistedBoard = persistedBoards[threadId];
const board = liveBoard ?? persistedBoard;
if (!board || board.cards.length === 0) continue;
-
- const thread = threadMap.get(threadId);
const title =
thread?.title && thread.title.trim().length > 0
? thread.title
@@ -513,6 +562,21 @@ export default function IntelligenceTasksTab() {
onUpdateCard={handleUpdatePersonal}
onDeleteCard={handleDeletePersonal}
onWorkTask={handleWorkPersonal}
+ onViewSession={card => {
+ if (!card.sessionThreadId) return;
+ const tid = card.sessionThreadId;
+ // Open the exact session — mirror the manual "Work" path's
+ // thread-open sequence so /chat lands on this thread, not just
+ // the Conversations page.
+ dispatch(setSelectedThread(tid));
+ dispatch(setActiveThread(tid));
+ void dispatch(loadThreads());
+ void dispatch(loadThreadMessages(tid));
+ // Pass the thread as an explicit open-intent so Conversations'
+ // mount-resume honors it (its default resume only considers
+ // General-tab threads and would otherwise drop this task session).
+ navigate('/chat', { state: { openThreadId: tid } });
+ }}
workingCardId={workingCardId}
/>
) : (
@@ -532,7 +596,7 @@ export default function IntelligenceTasksTab() {
{taskSourcesBoard && (
@@ -711,6 +775,7 @@ function TaskSourceTaskList({
onWorkOnTask: (card: TaskBoardCard) => void;
}) {
const { t } = useT();
+ const navigate = useNavigate();
const sortedCards = useMemo(
() => [...board.cards].sort((a, b) => a.order - b.order),
[board.cards]
@@ -727,11 +792,12 @@ function TaskSourceTaskList({
{t('intelligence.tasks.sourceList.subtitle')}
- navigate('/settings/task-sources')}
className="text-xs font-medium text-ocean-600 hover:text-ocean-700 dark:text-ocean-300 dark:hover:text-ocean-200">
{t('conversations.taskKanban.sources.manage')}
-
+
{sortedCards.length === 0 ? (
diff --git a/app/src/components/settings/hooks/useSettingsNavigation.ts b/app/src/components/settings/hooks/useSettingsNavigation.ts
index 72a6206aeb..16b7ff0836 100644
--- a/app/src/components/settings/hooks/useSettingsNavigation.ts
+++ b/app/src/components/settings/hooks/useSettingsNavigation.ts
@@ -45,6 +45,7 @@ export type SettingsRoute =
| 'webhooks-triggers'
| 'composio-triggers'
| 'composio-routing'
+ | 'task-sources'
| 'mcp-server'
| 'dev-workflow'
| 'sandbox-settings'
@@ -115,6 +116,7 @@ export const useSettingsNavigation = (): SettingsNavigationHook => {
if (path.includes('/settings/webhooks-triggers')) return 'webhooks-triggers';
if (path.includes('/settings/composio-triggers')) return 'composio-triggers';
if (path.includes('/settings/composio-routing')) return 'composio-routing';
+ if (path.includes('/settings/task-sources')) return 'task-sources';
if (path.includes('/settings/intelligence')) return 'intelligence';
if (path.includes('/settings/crypto')) return 'crypto';
if (path.includes('/settings/recovery-phrase')) return 'recovery-phrase';
diff --git a/app/src/lib/i18n/ar.ts b/app/src/lib/i18n/ar.ts
index 8e8ed9feef..5e6eee5c3d 100644
--- a/app/src/lib/i18n/ar.ts
+++ b/app/src/lib/i18n/ar.ts
@@ -2472,6 +2472,7 @@ const messages: TranslationMap = {
'conversations.taskKanban.saveChanges': 'التغييرات',
'conversations.taskKanban.deleteCard': 'Delete',
'conversations.taskKanban.workTask': 'العمل على المهمة',
+ 'conversations.taskKanban.viewWork': 'عرض العمل',
'conversations.taskKanban.startingTask': 'جارٍ البدء…',
'conversations.taskKanban.updateFailed': 'ولم يتمكن من استكمال المهمة؛ ولم يتم توفير التغييرات.',
'conversations.taskKanban.sourcesButton': 'المصادر',
diff --git a/app/src/lib/i18n/bn.ts b/app/src/lib/i18n/bn.ts
index bfd8673a6f..3f523aa2aa 100644
--- a/app/src/lib/i18n/bn.ts
+++ b/app/src/lib/i18n/bn.ts
@@ -2520,6 +2520,7 @@ const messages: TranslationMap = {
'conversations.taskKanban.saveChanges': 'পরিবর্তন সংরক্ষণ করা হবে',
'conversations.taskKanban.deleteCard': 'Delete',
'conversations.taskKanban.workTask': 'কাজ শুরু করুন',
+ 'conversations.taskKanban.viewWork': 'কাজ দেখুন',
'conversations.taskKanban.startingTask': 'শুরু হচ্ছে...',
'conversations.taskKanban.updateFailed': 'কাজটি সংরক্ষণ করা যায়নি।',
'conversations.taskKanban.sourcesButton': 'উৎস',
diff --git a/app/src/lib/i18n/de.ts b/app/src/lib/i18n/de.ts
index b705b83586..b5f1b1a69e 100644
--- a/app/src/lib/i18n/de.ts
+++ b/app/src/lib/i18n/de.ts
@@ -2584,6 +2584,7 @@ const messages: TranslationMap = {
'conversations.taskKanban.saveChanges': 'Änderungen speichern',
'conversations.taskKanban.deleteCard': 'Löschen',
'conversations.taskKanban.workTask': 'Aufgabe bearbeiten',
+ 'conversations.taskKanban.viewWork': 'Arbeit anzeigen',
'conversations.taskKanban.startingTask': 'Startet…',
'conversations.taskKanban.updateFailed':
'Aufgabe konnte nicht aktualisiert werden; Änderungen wurden nicht gespeichert.',
diff --git a/app/src/lib/i18n/en.ts b/app/src/lib/i18n/en.ts
index ec033e1532..a6f1aab4f0 100644
--- a/app/src/lib/i18n/en.ts
+++ b/app/src/lib/i18n/en.ts
@@ -2902,6 +2902,7 @@ const en: TranslationMap = {
'conversations.taskKanban.saveChanges': 'Save changes',
'conversations.taskKanban.deleteCard': 'Delete',
'conversations.taskKanban.workTask': 'Work task',
+ 'conversations.taskKanban.viewWork': 'View work',
'conversations.taskKanban.startingTask': 'Starting…',
'conversations.taskKanban.updateFailed': 'Could not update task; changes were not saved.',
'conversations.taskKanban.sourcesButton': 'Sources',
diff --git a/app/src/lib/i18n/es.ts b/app/src/lib/i18n/es.ts
index 96ee3d78a9..112962c0c9 100644
--- a/app/src/lib/i18n/es.ts
+++ b/app/src/lib/i18n/es.ts
@@ -2567,6 +2567,7 @@ const messages: TranslationMap = {
'conversations.taskKanban.saveChanges': 'Guardar cambios',
'conversations.taskKanban.deleteCard': 'Borrar',
'conversations.taskKanban.workTask': 'Trabajar tarea',
+ 'conversations.taskKanban.viewWork': 'Ver trabajo',
'conversations.taskKanban.startingTask': 'Iniciando…',
'conversations.taskKanban.updateFailed':
'No se pudo actualizar la tarea; los cambios no se guardaron.',
diff --git a/app/src/lib/i18n/fr.ts b/app/src/lib/i18n/fr.ts
index 8ac8f1c518..4c772c5fbd 100644
--- a/app/src/lib/i18n/fr.ts
+++ b/app/src/lib/i18n/fr.ts
@@ -2576,6 +2576,7 @@ const messages: TranslationMap = {
'conversations.taskKanban.saveChanges': 'Enregistrer les modifications',
'conversations.taskKanban.deleteCard': 'Supprimer',
'conversations.taskKanban.workTask': 'Travailler la tâche',
+ 'conversations.taskKanban.viewWork': 'Voir le travail',
'conversations.taskKanban.startingTask': 'Démarrage…',
'conversations.taskKanban.updateFailed':
"Impossible de mettre à jour la tâche; les modifications n'ont pas été enregistrées.",
diff --git a/app/src/lib/i18n/hi.ts b/app/src/lib/i18n/hi.ts
index ca1eb50bbf..709eead9df 100644
--- a/app/src/lib/i18n/hi.ts
+++ b/app/src/lib/i18n/hi.ts
@@ -2524,6 +2524,7 @@ const messages: TranslationMap = {
'conversations.taskKanban.saveChanges': 'परिवर्तन सहेजें',
'conversations.taskKanban.deleteCard': 'Delete',
'conversations.taskKanban.workTask': 'कार्य शुरू करें',
+ 'conversations.taskKanban.viewWork': 'कार्य देखें',
'conversations.taskKanban.startingTask': 'शुरू हो रहा है…',
'conversations.taskKanban.updateFailed': 'कार्य अद्यतन नहीं कर सका; परिवर्तन बचाया नहीं गया।',
'conversations.taskKanban.sourcesButton': 'स्रोत',
diff --git a/app/src/lib/i18n/id.ts b/app/src/lib/i18n/id.ts
index 5504c1c487..52fba35017 100644
--- a/app/src/lib/i18n/id.ts
+++ b/app/src/lib/i18n/id.ts
@@ -2526,6 +2526,7 @@ const messages: TranslationMap = {
'conversations.taskKanban.saveChanges': 'Simpan perubahan',
'conversations.taskKanban.deleteCard': 'Hapus',
'conversations.taskKanban.workTask': 'Kerjakan tugas',
+ 'conversations.taskKanban.viewWork': 'Lihat pekerjaan',
'conversations.taskKanban.startingTask': 'Memulai…',
'conversations.taskKanban.updateFailed': 'Tak bisa memutakhirkan tugas; perubahan tak disimpan.',
'conversations.taskKanban.sourcesButton': 'Sumber',
diff --git a/app/src/lib/i18n/it.ts b/app/src/lib/i18n/it.ts
index 0fd113f594..c53c138d61 100644
--- a/app/src/lib/i18n/it.ts
+++ b/app/src/lib/i18n/it.ts
@@ -2557,6 +2557,7 @@ const messages: TranslationMap = {
'conversations.taskKanban.saveChanges': 'Salva modifiche',
'conversations.taskKanban.deleteCard': 'Elimina',
'conversations.taskKanban.workTask': "Lavora sull'attività",
+ 'conversations.taskKanban.viewWork': 'Visualizza lavoro',
'conversations.taskKanban.startingTask': 'Avvio…',
'conversations.taskKanban.updateFailed':
"Impossibile aggiornare l'attività; le modifiche non sono state salvate.",
diff --git a/app/src/lib/i18n/ko.ts b/app/src/lib/i18n/ko.ts
index 059104d37e..b00b190c7a 100644
--- a/app/src/lib/i18n/ko.ts
+++ b/app/src/lib/i18n/ko.ts
@@ -2498,6 +2498,7 @@ const messages: TranslationMap = {
'conversations.taskKanban.saveChanges': '변경 사항 저장',
'conversations.taskKanban.deleteCard': '삭제',
'conversations.taskKanban.workTask': '작업 시작',
+ 'conversations.taskKanban.viewWork': '작업 보기',
'conversations.taskKanban.startingTask': '시작 중…',
'conversations.taskKanban.updateFailed':
'작업을 업데이트할 수 없어 변경 사항이 저장되지 않았습니다.',
diff --git a/app/src/lib/i18n/pl.ts b/app/src/lib/i18n/pl.ts
index c613cc7a50..85896f07d5 100644
--- a/app/src/lib/i18n/pl.ts
+++ b/app/src/lib/i18n/pl.ts
@@ -2552,6 +2552,7 @@ const messages: TranslationMap = {
'conversations.taskKanban.saveChanges': 'Zapisz zmiany',
'conversations.taskKanban.deleteCard': 'Usuń',
'conversations.taskKanban.workTask': 'Pracuj nad zadaniem',
+ 'conversations.taskKanban.viewWork': 'Zobacz pracę',
'conversations.taskKanban.startingTask': 'Uruchamianie…',
'conversations.taskKanban.updateFailed':
'Nie udało się zaktualizować zadania; zmian nie zapisano.',
diff --git a/app/src/lib/i18n/pt.ts b/app/src/lib/i18n/pt.ts
index 214e3adabf..9cb5a9aa59 100644
--- a/app/src/lib/i18n/pt.ts
+++ b/app/src/lib/i18n/pt.ts
@@ -2565,6 +2565,7 @@ const messages: TranslationMap = {
'conversations.taskKanban.saveChanges': 'Salvar alterações',
'conversations.taskKanban.deleteCard': 'Excluir',
'conversations.taskKanban.workTask': 'Trabalhar na tarefa',
+ 'conversations.taskKanban.viewWork': 'Ver trabalho',
'conversations.taskKanban.startingTask': 'Iniciando…',
'conversations.taskKanban.updateFailed':
'Não foi possível atualizar a tarefa; as alterações não foram salvas.',
diff --git a/app/src/lib/i18n/ru.ts b/app/src/lib/i18n/ru.ts
index 9795ee47e5..0d3c041b38 100644
--- a/app/src/lib/i18n/ru.ts
+++ b/app/src/lib/i18n/ru.ts
@@ -2539,6 +2539,7 @@ const messages: TranslationMap = {
'conversations.taskKanban.saveChanges': 'Сохранить изменения',
'conversations.taskKanban.deleteCard': 'Удалить',
'conversations.taskKanban.workTask': 'Работать над задачей',
+ 'conversations.taskKanban.viewWork': 'Показать работу',
'conversations.taskKanban.startingTask': 'Запуск…',
'conversations.taskKanban.updateFailed': 'Не удалось обновить задачу; изменения не сохранились.',
'conversations.taskKanban.sourcesButton': 'Источники',
diff --git a/app/src/lib/i18n/zh-CN.ts b/app/src/lib/i18n/zh-CN.ts
index 272f7cecaa..714b73acf6 100644
--- a/app/src/lib/i18n/zh-CN.ts
+++ b/app/src/lib/i18n/zh-CN.ts
@@ -2397,6 +2397,7 @@ const messages: TranslationMap = {
'conversations.taskKanban.saveChanges': '保存更改',
'conversations.taskKanban.deleteCard': '删除',
'conversations.taskKanban.workTask': '处理任务',
+ 'conversations.taskKanban.viewWork': '查看工作',
'conversations.taskKanban.startingTask': '正在启动…',
'conversations.taskKanban.updateFailed': '无法更新任务;更改未保存。',
'conversations.taskKanban.sourcesButton': '来源',
diff --git a/app/src/pages/Conversations.tsx b/app/src/pages/Conversations.tsx
index bc3b3cae46..0e3ea6d77a 100644
--- a/app/src/pages/Conversations.tsx
+++ b/app/src/pages/Conversations.tsx
@@ -186,6 +186,7 @@ const Conversations = ({
const { t } = useT();
const dispatch = useAppDispatch();
const navigate = useNavigate();
+ const location = useLocation();
const { threads, selectedThreadId, messages, isLoadingMessages, messagesError, activeThreadId } =
useAppSelector(state => state.thread);
@@ -370,6 +371,29 @@ const Conversations = ({
// Match the sidebar's default General filter here so initial/resume
// selection can't auto-pick a thread hidden by the selected tab.
const visibleThreads = data.threads.filter(t => isThreadVisibleInTab(t, GENERAL_TAB_VALUE));
+ // An explicit "open this session" intent (e.g. View work from the Agent
+ // Tasks board) wins over passive resume — and bypasses the General-tab
+ // visibility filter so a task-labelled session thread can actually be
+ // opened (the resume default below only considers General threads).
+ const openThreadId = (location.state as { openThreadId?: string } | null)?.openThreadId;
+ const openThread = openThreadId
+ ? data.threads.find(t => t.id === openThreadId)
+ : undefined;
+ if (openThread) {
+ // Switch the sidebar tab to the bucket that contains the opened
+ // thread (e.g. Tasks for a task session) so it's visible/selected in
+ // the list instead of hidden behind the default General tab.
+ setSelectedLabel(
+ isThreadVisibleInTab(openThread, TASKS_TAB_VALUE)
+ ? TASKS_TAB_VALUE
+ : isThreadVisibleInTab(openThread, SUBCONSCIOUS_TAB_VALUE)
+ ? SUBCONSCIOUS_TAB_VALUE
+ : GENERAL_TAB_VALUE
+ );
+ dispatch(setSelectedThread(openThread.id));
+ void dispatch(loadThreadMessages(openThread.id));
+ return;
+ }
if (visibleThreads.length > 0) {
// Prefer the thread the user was last viewing (persisted across
// reloads via redux-persist on the `thread` slice). Only fall
@@ -423,7 +447,6 @@ const Conversations = ({
});
}, [dispatch]);
- const location = useLocation();
const { containerRef: messagesContainerRef, endRef: messagesEndRef } = useStickToBottom(
messages,
selectedThreadId,
@@ -1588,6 +1611,12 @@ const Conversations = ({
t,
});
}}
+ onViewSession={card => {
+ if (!card.sessionThreadId) return;
+ dispatch(setSelectedThread(card.sessionThreadId));
+ dispatch(setActiveThread(card.sessionThreadId));
+ void dispatch(loadThreadMessages(card.sessionThreadId));
+ }}
/>
)}
{visibleMessages.map(msg => (
diff --git a/app/src/pages/Intelligence.tsx b/app/src/pages/Intelligence.tsx
index e3ef7ee23a..0dc75fc901 100644
--- a/app/src/pages/Intelligence.tsx
+++ b/app/src/pages/Intelligence.tsx
@@ -1,4 +1,5 @@
import { useCallback, useEffect, useState } from 'react';
+import { useSearchParams } from 'react-router-dom';
import { ConfirmationModal } from '../components/intelligence/ConfirmationModal';
import IntelligenceSubconsciousTab from '../components/intelligence/IntelligenceSubconsciousTab';
@@ -25,7 +26,28 @@ type IntelligenceTab = 'memory' | 'subconscious' | 'tasks' | 'workflows' | 'coun
export default function Intelligence() {
const { t } = useT();
- const [activeTab, setActiveTab] = useState('memory');
+ // Tab is URL-backed (`/intelligence?tab=…`) so navigating away — e.g. to
+ // Settings → Task Sources from the Agent Tasks tab — and coming back via
+ // browser-back restores the same tab instead of resetting to Memory.
+ // `replace` so switching tabs doesn't stack history entries.
+ const [searchParams, setSearchParams] = useSearchParams();
+ const tabParam = searchParams.get('tab');
+ const activeTab: IntelligenceTab =
+ tabParam && ['memory', 'subconscious', 'tasks', 'workflows', 'council'].includes(tabParam)
+ ? (tabParam as IntelligenceTab)
+ : 'memory';
+ const setActiveTab = useCallback(
+ (tab: IntelligenceTab) => {
+ setSearchParams(
+ prev => {
+ prev.set('tab', tab);
+ return prev;
+ },
+ { replace: true }
+ );
+ },
+ [setSearchParams]
+ );
// The legacy header pills (system-status + Ingesting/Queued chips) were
// sourced from `useConsciousItems` + `useMemoryIngestionStatus`. They are
diff --git a/app/src/pages/conversations/components/TaskKanbanBoard.tsx b/app/src/pages/conversations/components/TaskKanbanBoard.tsx
index f095bb59e7..28243d210b 100644
--- a/app/src/pages/conversations/components/TaskKanbanBoard.tsx
+++ b/app/src/pages/conversations/components/TaskKanbanBoard.tsx
@@ -1,4 +1,5 @@
import { useCallback, useEffect, useMemo, useState } from 'react';
+import { useNavigate } from 'react-router-dom';
import {
LuArrowLeft,
LuArrowRight,
@@ -97,6 +98,9 @@ interface TaskKanbanBoardProps {
onDecidePlan?: (card: TaskBoardCard, approve: boolean) => void;
/** Start work on a card from a higher-level task board. */
onWorkTask?: (card: TaskBoardCard) => void;
+ /** Jump to the card's agent session in Conversations. Shown on any card that
+ * carries a `sessionThreadId` (a run is live or has happened). */
+ onViewSession?: (card: TaskBoardCard) => void;
workingCardId?: string | null;
}
@@ -110,6 +114,7 @@ export function TaskKanbanBoard({
onDeleteCard,
onDecidePlan,
onWorkTask,
+ onViewSession,
workingCardId = null,
}: TaskKanbanBoardProps) {
const { t } = useT();
@@ -195,6 +200,7 @@ export function TaskKanbanBoard({
hasBriefActions={Boolean(onUpdateCard || onDeleteCard)}
onDecidePlan={onDecidePlan}
onWorkTask={onWorkTask}
+ onViewSession={onViewSession}
working={workingCardId === card.id}
onOpenBrief={() => setSelectedCardId(card.id)}
/>
@@ -224,6 +230,7 @@ function TaskBoardArticle({
hasBriefActions,
onDecidePlan,
onWorkTask,
+ onViewSession,
working,
onOpenBrief,
}: {
@@ -234,6 +241,7 @@ function TaskBoardArticle({
hasBriefActions: boolean;
onDecidePlan?: (card: TaskBoardCard, approve: boolean) => void;
onWorkTask?: (card: TaskBoardCard) => void;
+ onViewSession?: (card: TaskBoardCard) => void;
working: boolean;
onOpenBrief: () => void;
}) {
@@ -246,7 +254,16 @@ function TaskBoardArticle({
{card.title}
- {card.status === 'awaiting_approval' && onDecidePlan ? (
+ {card.sessionThreadId && onViewSession ? (
+
+ ) : card.status === 'awaiting_approval' && onDecidePlan ? (
-
navigate('/settings/task-sources')}
className="text-[11px] font-medium text-ocean-600 hover:text-ocean-700 dark:text-ocean-300 dark:hover:text-ocean-200">
{t('conversations.taskKanban.sources.manage')}
-
+