diff --git a/frontend/src/app/workspace/chats/[thread_id]/layout.tsx b/frontend/src/app/workspace/chats/[thread_id]/layout.tsx
index 71c2b5e..48effe4 100644
--- a/frontend/src/app/workspace/chats/[thread_id]/layout.tsx
+++ b/frontend/src/app/workspace/chats/[thread_id]/layout.tsx
@@ -28,7 +28,7 @@ export default function ChatLayout({
color={
resolvedTheme === "dark" ? "#60A5FA" : "oklch(0 0.0098 87.47)"
}
- maxOpacity={resolvedTheme === "dark" ? 0.04 : 0.03}
+ maxOpacity={resolvedTheme === "dark" ? 0.04 : 0.025}
flickerChance={0.1}
/>
@@ -149,19 +153,34 @@ export default function ChatPage() {
: "max-w-(--container-width-md)",
)}
>
-
-
-
+ {isNewThread && (
+
+
+
+ )}
+
+ setTodoListCollapsed(!todoListCollapsed)
+ }
+ />
+
+ ) : null
+ }
onContextChange={(context) =>
setSettings("context", context)
}
diff --git a/frontend/src/components/ai-elements/prompt-input.tsx b/frontend/src/components/ai-elements/prompt-input.tsx
index d18b3a6..254ce11 100644
--- a/frontend/src/components/ai-elements/prompt-input.tsx
+++ b/frontend/src/components/ai-elements/prompt-input.tsx
@@ -789,7 +789,7 @@ export const PromptInput = ({
ref={formRef}
{...props}
>
- {children}
+ {children}
>
);
diff --git a/frontend/src/components/workspace/artifacts/context.tsx b/frontend/src/components/workspace/artifacts/context.tsx
index 1058ae8..a7621f4 100644
--- a/frontend/src/components/workspace/artifacts/context.tsx
+++ b/frontend/src/components/workspace/artifacts/context.tsx
@@ -7,13 +7,13 @@ export interface ArtifactsContextType {
setArtifacts: (artifacts: string[]) => void;
selectedArtifact: string | null;
+ autoSelect: boolean;
+ select: (artifact: string, autoSelect?: boolean) => void;
+ deselect: () => void;
open: boolean;
autoOpen: boolean;
setOpen: (open: boolean) => void;
- deselect: () => void;
-
- select: (artifact: string) => void;
}
const ArtifactsContext = createContext(
@@ -27,17 +27,22 @@ interface ArtifactsProviderProps {
export function ArtifactsProvider({ children }: ArtifactsProviderProps) {
const [artifacts, setArtifacts] = useState([]);
const [selectedArtifact, setSelectedArtifact] = useState(null);
+ const [autoSelect, setAutoSelect] = useState(true);
const [open, setOpen] = useState(false);
const [autoOpen, setAutoOpen] = useState(true);
const { setOpen: setSidebarOpen } = useSidebar();
- const select = (artifact: string) => {
+ const select = (artifact: string, autoSelect = false) => {
setSelectedArtifact(artifact);
setSidebarOpen(false);
+ if (!autoSelect) {
+ setAutoSelect(false);
+ }
};
const deselect = () => {
setSelectedArtifact(null);
+ setAutoSelect(true);
};
const value: ArtifactsContextType = {
@@ -46,9 +51,11 @@ export function ArtifactsProvider({ children }: ArtifactsProviderProps) {
open,
autoOpen,
+ autoSelect,
setOpen: (isOpen: boolean) => {
if (!isOpen && autoOpen) {
setAutoOpen(false);
+ setAutoSelect(false);
}
setOpen(isOpen);
},
diff --git a/frontend/src/components/workspace/input-box.tsx b/frontend/src/components/workspace/input-box.tsx
index ca9269a..280698e 100644
--- a/frontend/src/components/workspace/input-box.tsx
+++ b/frontend/src/components/workspace/input-box.tsx
@@ -1,7 +1,7 @@
"use client";
import type { ChatStatus } from "ai";
-import { CheckIcon, LightbulbIcon, LightbulbOffIcon } from "lucide-react";
+import { CheckIcon, LightbulbIcon, ListTodoIcon } from "lucide-react";
import { useCallback, useMemo, useState, type ComponentProps } from "react";
import {
@@ -35,6 +35,7 @@ export function InputBox({
autoFocus,
status = "ready",
context,
+ extraHeader,
onContextChange,
onSubmit,
onStop,
@@ -43,6 +44,7 @@ export function InputBox({
assistantId?: string | null;
status?: ChatStatus;
context: Omit;
+ extraHeader?: React.ReactNode;
onContextChange?: (context: Omit) => void;
onSubmit?: (message: PromptInputMessage) => void;
onStop?: () => void;
@@ -72,6 +74,12 @@ export function InputBox({
thinking_enabled: !context.thinking_enabled,
});
}, [onContextChange, context]);
+ const handlePlanModeToggle = useCallback(() => {
+ onContextChange?.({
+ ...context,
+ is_plan_mode: !context.is_plan_mode,
+ });
+ }, [onContextChange, context]);
const handleSubmit = useCallback(
async (message: PromptInputMessage) => {
if (status === "streaming") {
@@ -89,7 +97,6 @@ export function InputBox({
+ {extraHeader && (
+
+ )}
-
+
) : (
-
+
)}
)}
+
+ {t.inputBox.planMode}
+
+ {t.inputBox.clickToDisablePlanMode}
+
+
+ ) : (
+
+
{t.inputBox.planMode}
+
+ {t.inputBox.clickToEnablePlanMode}
+
+
+ )
+ }
+ >
+ {selectedModel?.supports_thinking && (
+
+ <>
+ {context.is_plan_mode ? (
+
+ ) : (
+
+ )}
+
+ {t.inputBox.planMode}
+
+ >
+
+ )}
+
) : (
-
+
),
)}
{lastToolCallStep && (
-
+
)}
@@ -173,15 +178,20 @@ function ToolCall({
name,
args,
result,
+ isLast = false,
+ isLoading = false,
}: {
id?: string;
messageId?: string;
name: string;
args: Record;
result?: string | Record;
+ isLast?: boolean;
+ isLoading?: boolean;
}) {
const { t } = useI18n();
- const { setOpen, autoOpen, selectedArtifact, select } = useArtifacts();
+ const { setOpen, autoOpen, autoSelect, selectedArtifact, select } =
+ useArtifacts();
if (name === "web_search") {
let label: React.ReactNode = t.toolCalls.searchForRelatedInfo;
if (typeof args.query === "string") {
@@ -265,7 +275,7 @@ function ToolCall({
description = t.toolCalls.writeFile;
}
const path: string | undefined = (args as { path: string })?.path;
- if (autoOpen && path) {
+ if (isLoading && isLast && autoOpen && autoSelect && path) {
setTimeout(() => {
const url = new URL(
`write-file:${path}?message_id=${messageId}&tool_call_id=${id}`,
@@ -273,7 +283,7 @@ function ToolCall({
if (selectedArtifact === url) {
return;
}
- select(url);
+ select(url, true);
setOpen(true);
}, 100);
}
@@ -320,27 +330,7 @@ function ToolCall({
)}
);
- } else if (name === "present_files") {
- return (
-
-
- {Array.isArray((args as { filepaths: string[] }).filepaths) &&
- (args as { filepaths: string[] }).filepaths.map(
- (filepath: string) => (
-
- {filepath}
-
- ),
- )}
-
-
- );
- }
- if (name === "ask_clarification") {
+ } else if (name === "ask_clarification") {
return (
);
+ } else if (name === "write_todos") {
+ return (
+
+ );
} else {
const description: string | undefined = (args as { description: string })
?.description;
diff --git a/frontend/src/components/workspace/messages/message-list.tsx b/frontend/src/components/workspace/messages/message-list.tsx
index 97c11fe..09788e6 100644
--- a/frontend/src/components/workspace/messages/message-list.tsx
+++ b/frontend/src/components/workspace/messages/message-list.tsx
@@ -23,10 +23,12 @@ export function MessageList({
className,
threadId,
thread,
+ paddingBottom = 160,
}: {
className?: string;
threadId: string;
thread: UseStream;
+ paddingBottom?: number;
}) {
if (thread.isThreadLoading) {
return ;
@@ -70,7 +72,7 @@ export function MessageList({
);
})}
{thread.isLoading && }
-
+
);
diff --git a/frontend/src/components/workspace/todo-list.tsx b/frontend/src/components/workspace/todo-list.tsx
new file mode 100644
index 0000000..a1e31e7
--- /dev/null
+++ b/frontend/src/components/workspace/todo-list.tsx
@@ -0,0 +1,68 @@
+import { ChevronUpIcon } from "lucide-react";
+
+import type { Todo } from "@/core/todos";
+import { cn } from "@/lib/utils";
+
+import {
+ QueueItem,
+ QueueItemContent,
+ QueueItemIndicator,
+ QueueList,
+} from "../ai-elements/queue";
+
+export function TodoList({
+ className,
+ todos,
+ collapsed = false,
+ onToggle,
+}: {
+ className?: string;
+ todos: Todo[];
+ collapsed?: boolean;
+ onToggle?: () => void;
+}) {
+ return (
+
+
{
+ onToggle?.();
+ }}
+ >
+ To-dos
+
+
+
+
+
+
+ {todos.map((todo, i) => (
+
+
+
+
+ {todo.content}
+
+
+
+ ))}
+
+
+
+ );
+}
diff --git a/frontend/src/core/i18n/locales/en-US.ts b/frontend/src/core/i18n/locales/en-US.ts
index 2f87746..5e0ffe9 100644
--- a/frontend/src/core/i18n/locales/en-US.ts
+++ b/frontend/src/core/i18n/locales/en-US.ts
@@ -41,6 +41,11 @@ export const enUS: Translations = {
thinkingDisabled: "Thinking is disabled",
clickToDisableThinking: "Click to disable thinking",
clickToEnableThinking: "Click to enable thinking",
+ planMode: "Plan mode",
+ planModeEnabled: "Plan mode is enabled",
+ planModeDisabled: "Plan mode is disabled",
+ clickToDisablePlanMode: "Click to disable plan mode",
+ clickToEnablePlanMode: "Click to enable plan mode",
searchModels: "Search models...",
},
@@ -87,6 +92,7 @@ export const enUS: Translations = {
listFolder: "List folder",
readFile: "Read file",
writeFile: "Write file",
+ writeTodos: "Update to-do list",
},
// Settings
diff --git a/frontend/src/core/i18n/locales/types.ts b/frontend/src/core/i18n/locales/types.ts
index 5586983..6279c27 100644
--- a/frontend/src/core/i18n/locales/types.ts
+++ b/frontend/src/core/i18n/locales/types.ts
@@ -38,6 +38,11 @@ export interface Translations {
thinkingDisabled: string;
clickToDisableThinking: string;
clickToEnableThinking: string;
+ planMode: string;
+ planModeEnabled: string;
+ planModeDisabled: string;
+ clickToDisablePlanMode: string;
+ clickToEnablePlanMode: string;
searchModels: string;
};
@@ -84,6 +89,7 @@ export interface Translations {
listFolder: string;
readFile: string;
writeFile: string;
+ writeTodos: string;
};
// Settings
diff --git a/frontend/src/core/i18n/locales/zh-CN.ts b/frontend/src/core/i18n/locales/zh-CN.ts
index cea42c8..12afc98 100644
--- a/frontend/src/core/i18n/locales/zh-CN.ts
+++ b/frontend/src/core/i18n/locales/zh-CN.ts
@@ -41,6 +41,11 @@ export const zhCN: Translations = {
thinkingDisabled: "思考功能已禁用",
clickToDisableThinking: "点击禁用思考功能",
clickToEnableThinking: "点击启用思考功能",
+ planMode: "To-do 模式",
+ planModeEnabled: "To-do 模式已启用",
+ planModeDisabled: "To-do 模式已禁用",
+ clickToDisablePlanMode: "点击禁用 To-do 模式",
+ clickToEnablePlanMode: "点击启用 To-do 模式",
searchModels: "搜索模型...",
},
@@ -87,6 +92,7 @@ export const zhCN: Translations = {
listFolder: "列出文件夹",
readFile: "读取文件",
writeFile: "写入文件",
+ writeTodos: "更新 To-do 列表",
},
// Settings
diff --git a/frontend/src/core/messages/utils.ts b/frontend/src/core/messages/utils.ts
index faa137e..75ecbff 100644
--- a/frontend/src/core/messages/utils.ts
+++ b/frontend/src/core/messages/utils.ts
@@ -85,29 +85,6 @@ export function groupMessages(
}
}
- // if (!isLoading) {
- // const lastGroup: MessageGroup | undefined = groups[groups.length - 1];
- // if (
- // lastGroup?.type === "assistant:processing" &&
- // lastGroup.messages.length > 0
- // ) {
- // const lastMessage = lastGroup.messages[lastGroup.messages.length - 1]!;
- // const reasoningContent = extractReasoningContentFromMessage(lastMessage);
- // const content = extractContentFromMessage(lastMessage);
- // if (reasoningContent && !content) {
- // lastGroup.messages.pop();
- // if (lastGroup.messages.length === 0) {
- // groups.pop();
- // }
- // groups.push({
- // id: lastMessage.id,
- // type: "assistant",
- // messages: [lastMessage],
- // });
- // }
- // }
- // }
-
const resultsOfGroups: T[] = [];
for (const group of groups) {
const resultOfGroup = mapper(group);
diff --git a/frontend/src/core/settings/local.ts b/frontend/src/core/settings/local.ts
index 41312c8..c493d09 100644
--- a/frontend/src/core/settings/local.ts
+++ b/frontend/src/core/settings/local.ts
@@ -4,6 +4,7 @@ export const DEFAULT_LOCAL_SETTINGS: LocalSettings = {
context: {
model_name: "deepseek-v3.2",
thinking_enabled: true,
+ is_plan_mode: true,
},
layout: {
sidebar_collapsed: false,
@@ -27,10 +28,18 @@ export function getLocalSettings(): LocalSettings {
try {
if (json) {
const settings = JSON.parse(json);
- return {
+ const mergedSettings = {
...DEFAULT_LOCAL_SETTINGS,
- ...settings,
+ context: {
+ ...DEFAULT_LOCAL_SETTINGS.context,
+ ...settings.context,
+ },
+ layout: {
+ ...DEFAULT_LOCAL_SETTINGS.layout,
+ ...settings.layout,
+ },
};
+ return mergedSettings;
}
} catch {}
return DEFAULT_LOCAL_SETTINGS;
diff --git a/frontend/src/core/threads/types.ts b/frontend/src/core/threads/types.ts
index cb6d68f..5d4bc43 100644
--- a/frontend/src/core/threads/types.ts
+++ b/frontend/src/core/threads/types.ts
@@ -1,10 +1,13 @@
import { type BaseMessage } from "@langchain/core/messages";
import type { Thread } from "@langchain/langgraph-sdk";
+import type { Todo } from "../todos";
+
export interface AgentThreadState extends Record {
title: string;
messages: BaseMessage[];
artifacts: string[];
+ todos?: Todo[];
}
export interface AgentThread extends Thread {}
@@ -13,4 +16,5 @@ export interface AgentThreadContext extends Record {
thread_id: string;
model_name: string;
thinking_enabled: boolean;
+ is_plan_mode: boolean;
}
diff --git a/frontend/src/core/todos/index.ts b/frontend/src/core/todos/index.ts
new file mode 100644
index 0000000..eea524d
--- /dev/null
+++ b/frontend/src/core/todos/index.ts
@@ -0,0 +1 @@
+export * from "./types";
diff --git a/frontend/src/core/todos/types.ts b/frontend/src/core/todos/types.ts
new file mode 100644
index 0000000..a515b2a
--- /dev/null
+++ b/frontend/src/core/todos/types.ts
@@ -0,0 +1,4 @@
+export interface Todo {
+ content?: string;
+ status?: "pending" | "in_progress" | "completed";
+}