mirror of
https://github.com/All-Hands-AI/OpenHands.git
synced 2026-04-29 03:00:45 -04:00
Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 31341cc7f1 | |||
| aaa2dbe45e |
+7
@@ -16,6 +16,7 @@ const BrowserTab = lazy(() => import("#/routes/browser-tab"));
|
||||
const JupyterTab = lazy(() => import("#/routes/jupyter-tab"));
|
||||
const ServedTab = lazy(() => import("#/routes/served-tab"));
|
||||
const VSCodeTab = lazy(() => import("#/routes/vscode-tab"));
|
||||
const TasksTab = lazy(() => import("#/routes/tasks-tab"));
|
||||
|
||||
export function ConversationTabContent() {
|
||||
const selectedTab = useSelector(
|
||||
@@ -34,6 +35,7 @@ export function ConversationTabContent() {
|
||||
const isServedActive = selectedTab === "served";
|
||||
const isVSCodeActive = selectedTab === "vscode";
|
||||
const isTerminalActive = selectedTab === "terminal";
|
||||
const isTasksActive = selectedTab === "tasks";
|
||||
|
||||
// Define tab configurations
|
||||
const tabs = [
|
||||
@@ -55,6 +57,7 @@ export function ConversationTabContent() {
|
||||
component: Terminal,
|
||||
isActive: isTerminalActive,
|
||||
},
|
||||
{ key: "tasks", component: TasksTab, isActive: isTasksActive },
|
||||
];
|
||||
|
||||
const conversationTabTitle = useMemo(() => {
|
||||
@@ -76,6 +79,9 @@ export function ConversationTabContent() {
|
||||
if (isTerminalActive) {
|
||||
return t(I18nKey.COMMON$TERMINAL);
|
||||
}
|
||||
if (isTasksActive) {
|
||||
return t(I18nKey.COMMON$TASKS);
|
||||
}
|
||||
return "";
|
||||
}, [
|
||||
isEditorActive,
|
||||
@@ -84,6 +90,7 @@ export function ConversationTabContent() {
|
||||
isServedActive,
|
||||
isVSCodeActive,
|
||||
isTerminalActive,
|
||||
isTasksActive,
|
||||
]);
|
||||
|
||||
if (shouldShownAgentLoading) {
|
||||
|
||||
+3
@@ -5,12 +5,14 @@ type ConversationTabNavProps = {
|
||||
icon: ComponentType<{ className: string }>;
|
||||
onClick(): void;
|
||||
isActive?: boolean;
|
||||
"data-testid"?: string;
|
||||
};
|
||||
|
||||
export function ConversationTabNav({
|
||||
icon: Icon,
|
||||
onClick,
|
||||
isActive,
|
||||
"data-testid": dataTestId,
|
||||
}: ConversationTabNavProps) {
|
||||
return (
|
||||
<button
|
||||
@@ -18,6 +20,7 @@ export function ConversationTabNav({
|
||||
onClick={() => {
|
||||
onClick();
|
||||
}}
|
||||
data-testid={dataTestId}
|
||||
className={cn(
|
||||
"p-1 rounded-md cursor-pointer",
|
||||
"text-[#9299AA] bg-[#0D0F11]",
|
||||
|
||||
@@ -8,6 +8,7 @@ import GlobeIcon from "#/icons/globe.svg?react";
|
||||
import ServerIcon from "#/icons/server.svg?react";
|
||||
import GitChanges from "#/icons/git_changes.svg?react";
|
||||
import VSCodeIcon from "#/icons/vscode.svg?react";
|
||||
import ListIcon from "#/icons/list.svg?react";
|
||||
import { cn } from "#/utils/utils";
|
||||
import { ConversationTabNav } from "./conversation-tab-nav";
|
||||
import { ChatActionTooltip } from "../../chat/chat-action-tooltip";
|
||||
@@ -130,8 +131,19 @@ export function ConversationTabs() {
|
||||
tooltipContent: t(I18nKey.COMMON$BROWSER),
|
||||
tooltipAriaLabel: t(I18nKey.COMMON$BROWSER),
|
||||
},
|
||||
{
|
||||
isActive: isTabActive("tasks"),
|
||||
icon: ListIcon,
|
||||
onClick: () => onTabSelected("tasks"),
|
||||
tooltipContent: t(I18nKey.COMMON$TASKS),
|
||||
tooltipAriaLabel: t(I18nKey.COMMON$TASKS),
|
||||
},
|
||||
];
|
||||
|
||||
// Debug logging to help troubleshoot tab visibility
|
||||
console.log("ConversationTabs: Rendering", tabs.length, "tabs");
|
||||
console.log("ConversationTabs: Tasks tab config:", tabs[tabs.length - 1]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
@@ -153,6 +165,7 @@ export function ConversationTabs() {
|
||||
icon={icon}
|
||||
onClick={onClick}
|
||||
isActive={isActive}
|
||||
data-testid={index === tabs.length - 1 ? "tasks-tab" : undefined}
|
||||
/>
|
||||
</ChatActionTooltip>
|
||||
),
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import ConversationService from "#/api/conversation-service/conversation-service.api";
|
||||
import { useConversationId } from "#/hooks/use-conversation-id";
|
||||
import { useRuntimeIsReady } from "#/hooks/use-runtime-is-ready";
|
||||
|
||||
interface Task {
|
||||
id: string;
|
||||
title: string;
|
||||
status: "todo" | "in_progress" | "done";
|
||||
notes?: string;
|
||||
}
|
||||
|
||||
const parseTasksFromMarkdown = (content: string): Task[] => {
|
||||
const tasks: Task[] = [];
|
||||
const lines = content.split("\n");
|
||||
|
||||
let currentTask: Partial<Task> | null = null;
|
||||
|
||||
for (const line of lines) {
|
||||
// Match task lines like: "1. ✅ Add 'tasks' to ConversationTab type in conversation-slice.tsx"
|
||||
const taskMatch = line.match(/^(\d+)\.\s*([✅🔄⏳])\s*(.+)$/u);
|
||||
if (taskMatch) {
|
||||
// Save previous task if exists
|
||||
if (
|
||||
currentTask &&
|
||||
currentTask.id &&
|
||||
currentTask.title &&
|
||||
currentTask.status
|
||||
) {
|
||||
tasks.push(currentTask as Task);
|
||||
}
|
||||
|
||||
const [, id, statusIcon, title] = taskMatch;
|
||||
let status: Task["status"] = "todo";
|
||||
|
||||
// Determine status from emoji
|
||||
if (statusIcon === "✅") {
|
||||
status = "done";
|
||||
} else if (statusIcon === "🔄") {
|
||||
status = "in_progress";
|
||||
} else if (statusIcon === "⏳") {
|
||||
status = "todo";
|
||||
}
|
||||
|
||||
currentTask = {
|
||||
id,
|
||||
title: title.trim(),
|
||||
status,
|
||||
notes: "",
|
||||
};
|
||||
} else if (currentTask && line.trim() && !line.startsWith("#")) {
|
||||
// This is likely a notes line for the current task
|
||||
if (currentTask.notes) {
|
||||
currentTask.notes += ` ${line.trim()}`;
|
||||
} else {
|
||||
currentTask.notes = line.trim();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Don't forget the last task
|
||||
if (
|
||||
currentTask &&
|
||||
currentTask.id &&
|
||||
currentTask.title &&
|
||||
currentTask.status
|
||||
) {
|
||||
tasks.push(currentTask as Task);
|
||||
}
|
||||
|
||||
return tasks;
|
||||
};
|
||||
|
||||
export const useGetTasks = () => {
|
||||
const { conversationId } = useConversationId();
|
||||
const runtimeIsReady = useRuntimeIsReady();
|
||||
|
||||
return useQuery({
|
||||
queryKey: ["tasks", conversationId],
|
||||
queryFn: async () => {
|
||||
try {
|
||||
const content = await ConversationService.getFile(
|
||||
conversationId,
|
||||
"TASKS.md",
|
||||
);
|
||||
return parseTasksFromMarkdown(content);
|
||||
} catch (error) {
|
||||
// If TASKS.md doesn't exist, return empty array
|
||||
return [];
|
||||
}
|
||||
},
|
||||
retry: false,
|
||||
staleTime: 1000 * 30, // 30 seconds
|
||||
gcTime: 1000 * 60 * 5, // 5 minutes
|
||||
enabled: runtimeIsReady && !!conversationId,
|
||||
meta: {
|
||||
disableToast: true,
|
||||
},
|
||||
});
|
||||
};
|
||||
@@ -890,6 +890,7 @@ export enum I18nKey {
|
||||
COMMON$START_CONVERSATION = "COMMON$START_CONVERSATION",
|
||||
COMMON$STOP_SERVER = "COMMON$STOP_SERVER",
|
||||
COMMON$TERMINAL = "COMMON$TERMINAL",
|
||||
COMMON$TASKS = "COMMON$TASKS",
|
||||
COMMON$UNKNOWN = "COMMON$UNKNOWN",
|
||||
COMMON$USER_SETTINGS = "COMMON$USER_SETTINGS",
|
||||
COMMON$VIEW = "COMMON$VIEW",
|
||||
@@ -910,4 +911,6 @@ export enum I18nKey {
|
||||
COMMON$STOP_RUNTIME = "COMMON$STOP_RUNTIME",
|
||||
COMMON$START_RUNTIME = "COMMON$START_RUNTIME",
|
||||
COMMON$JUPYTER_EMPTY_MESSAGE = "COMMON$JUPYTER_EMPTY_MESSAGE",
|
||||
COMMON$ERROR_LOADING_TASKS = "COMMON$ERROR_LOADING_TASKS",
|
||||
COMMON$NO_TASKS_AVAILABLE = "COMMON$NO_TASKS_AVAILABLE",
|
||||
}
|
||||
|
||||
@@ -14239,6 +14239,22 @@
|
||||
"de": "Terminal",
|
||||
"uk": "Термінал"
|
||||
},
|
||||
"COMMON$TASKS": {
|
||||
"en": "Task List",
|
||||
"ja": "タスクリスト",
|
||||
"zh-CN": "任务列表",
|
||||
"zh-TW": "任務列表",
|
||||
"ko-KR": "작업 목록",
|
||||
"no": "Oppgaveliste",
|
||||
"it": "Elenco attività",
|
||||
"pt": "Lista de tarefas",
|
||||
"es": "Lista de tareas",
|
||||
"ar": "قائمة المهام",
|
||||
"fr": "Liste des tâches",
|
||||
"tr": "Görev Listesi",
|
||||
"de": "Aufgabenliste",
|
||||
"uk": "Список завдань"
|
||||
},
|
||||
"COMMON$UNKNOWN": {
|
||||
"en": "Unknown",
|
||||
"ja": "不明",
|
||||
@@ -14558,5 +14574,37 @@
|
||||
"tr": "Jupyter defteriniz boş. Gösterilecek hücre yok.",
|
||||
"de": "Ihr Jupyter-Notebook ist leer. Keine Zellen zum Anzeigen.",
|
||||
"uk": "Ваш Jupyter-ноутбук порожній. Немає клітинок для відображення."
|
||||
},
|
||||
"COMMON$ERROR_LOADING_TASKS": {
|
||||
"en": "Error loading tasks",
|
||||
"ja": "タスクの読み込み中にエラーが発生しました",
|
||||
"zh-CN": "加载任务时出错",
|
||||
"zh-TW": "載入任務時發生錯誤",
|
||||
"ko-KR": "작업을 불러오는 중 오류가 발생했습니다",
|
||||
"no": "Feil ved lasting av oppgaver",
|
||||
"it": "Errore nel caricamento delle attività",
|
||||
"pt": "Erro ao carregar tarefas",
|
||||
"es": "Error al cargar tareas",
|
||||
"ar": "خطأ في تحميل المهام",
|
||||
"fr": "Erreur lors du chargement des tâches",
|
||||
"tr": "Görevler yüklenirken hata oluştu",
|
||||
"de": "Fehler beim Laden der Aufgaben",
|
||||
"uk": "Помилка завантаження завдань"
|
||||
},
|
||||
"COMMON$NO_TASKS_AVAILABLE": {
|
||||
"en": "No tasks available",
|
||||
"ja": "利用可能なタスクがありません",
|
||||
"zh-CN": "没有可用的任务",
|
||||
"zh-TW": "沒有可用的任務",
|
||||
"ko-KR": "사용 가능한 작업이 없습니다",
|
||||
"no": "Ingen oppgaver tilgjengelig",
|
||||
"it": "Nessuna attività disponibile",
|
||||
"pt": "Nenhuma tarefa disponível",
|
||||
"es": "No hay tareas disponibles",
|
||||
"ar": "لا توجد مهام متاحة",
|
||||
"fr": "Aucune tâche disponible",
|
||||
"tr": "Mevcut görev yok",
|
||||
"de": "Keine Aufgaben verfügbar",
|
||||
"uk": "Немає доступних завдань"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useSelector } from "react-redux";
|
||||
import { TaskListSection } from "#/components/features/chat/task-tracking/task-list-section";
|
||||
import { useGetTasks } from "#/hooks/query/use-get-tasks";
|
||||
import { I18nKey } from "#/i18n/declaration";
|
||||
import { RootState } from "#/store";
|
||||
import { RUNTIME_INACTIVE_STATES } from "#/types/agent-state";
|
||||
import { RandomTip } from "#/components/features/tips/random-tip";
|
||||
|
||||
function StatusMessage({ children }: React.PropsWithChildren) {
|
||||
return (
|
||||
<div className="w-full h-full flex flex-col items-center text-center justify-center text-2xl text-tertiary-light">
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function TasksTab() {
|
||||
const { t } = useTranslation();
|
||||
const {
|
||||
data: tasks,
|
||||
isSuccess,
|
||||
isError,
|
||||
error,
|
||||
isLoading: loadingTasks,
|
||||
} = useGetTasks();
|
||||
|
||||
const [statusMessage, setStatusMessage] = React.useState<string[] | null>(
|
||||
null,
|
||||
);
|
||||
|
||||
const { curAgentState } = useSelector((state: RootState) => state.agent);
|
||||
const runtimeIsActive = !RUNTIME_INACTIVE_STATES.includes(curAgentState);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!runtimeIsActive) {
|
||||
setStatusMessage([I18nKey.DIFF_VIEWER$WAITING_FOR_RUNTIME]);
|
||||
} else if (error) {
|
||||
setStatusMessage([I18nKey.COMMON$ERROR_LOADING_TASKS]);
|
||||
} else if (loadingTasks) {
|
||||
setStatusMessage([I18nKey.HOME$LOADING]);
|
||||
} else {
|
||||
setStatusMessage(null);
|
||||
}
|
||||
}, [runtimeIsActive, loadingTasks, error, setStatusMessage]);
|
||||
|
||||
return (
|
||||
<main className="h-full overflow-y-scroll p-4 md:pr-1.5 gap-3 flex flex-col items-center custom-scrollbar-always">
|
||||
{!isSuccess || !tasks || tasks.length === 0 ? (
|
||||
<div className="relative flex h-full w-full items-center">
|
||||
<div className="absolute inset-x-0 top-1/2 -translate-y-1/2">
|
||||
{statusMessage && (
|
||||
<StatusMessage>
|
||||
{statusMessage.map((msg) => (
|
||||
<span key={msg}>{t(msg)}</span>
|
||||
))}
|
||||
</StatusMessage>
|
||||
)}
|
||||
{!statusMessage && isSuccess && tasks && tasks.length === 0 && (
|
||||
<StatusMessage>
|
||||
<span>{t(I18nKey.COMMON$NO_TASKS_AVAILABLE)}</span>
|
||||
</StatusMessage>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="absolute inset-x-0 bottom-0">
|
||||
{!isError && tasks?.length === 0 && (
|
||||
<div className="max-w-2xl mb-4 text-m bg-tertiary rounded-xl p-4 text-left mx-auto">
|
||||
<RandomTip />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="w-full max-w-4xl">
|
||||
<TaskListSection taskList={tasks} />
|
||||
</div>
|
||||
)}
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
export default TasksTab;
|
||||
@@ -6,7 +6,8 @@ export type ConversationTab =
|
||||
| "jupyter"
|
||||
| "served"
|
||||
| "vscode"
|
||||
| "terminal";
|
||||
| "terminal"
|
||||
| "tasks";
|
||||
|
||||
export interface IMessageToSend {
|
||||
text: string;
|
||||
|
||||
Reference in New Issue
Block a user