feat(core,cli,server,web): add goal mode — loop Tasks on one Session until an objective completes (#66)

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
rank-YuandClaude Fable 5 authored and GitHub committed 2026-07-27 23:43:24 +08:00
1 parent e1141ca010
commit 46463bee26
61 files changed
+3411 -188

No files matched your search

+11 -2
View File
@@ -54,6 +54,7 @@ import { MessageStream } from "./message-stream";
import type { StreamRenderContext } from "./message-stream";
import { ChatInput } from "./chat-input";
import { DraftView } from "./draft-view";
import { GoalStatusBanner } from "./goal-banner";
import { handoffMessage, modelSwitchMessage } from "./agent-mentions";
import { sameModelRef } from "../models/model-grouping";
import { providerInfo } from "@prismshadow/penguin-core/model-catalog";
@@ -486,7 +487,7 @@ export function ChatPage() {
);
const onSend = useCallback(
async (input: TaskInputPart[]): Promise<boolean> => {
async (input: TaskInputPart[], goal: { budget: number } | null): Promise<boolean> => {
if (!selected) return false;
try {
// An explicitly picked per-turn thinking level rides on each task; "" (untouched)
@@ -494,6 +495,7 @@ export function ChatPage() {
// keep taking effect mid-session until the user pins a level.
const res = await api.postTask(selected.sessionId, {
input,
...(goal ? { goal } : {}),
...(turnThinkingLevel
? { thinkingLevel: turnThinkingLevel as TaskCreateRequest["thinkingLevel"] }
: {}),
@@ -1004,7 +1006,14 @@ export function ChatPage() {
)}
</div>
<div className="shrink-0 border-t border-gray-200 bg-white px-3 pb-[calc(0.75rem+env(safe-area-inset-bottom))] pt-3 md:pb-3 dark:border-gray-800 dark:bg-gray-950">
<div className="mx-auto max-w-3xl">{input}</div>
<div className="mx-auto max-w-3xl">
{/* Goal banner docked above the composer: an in-flight goal's progress
(restored on load while still active), or the terminal state reached
during this page's lifetime. The stop button is the composer's
regular stop (one abort ends the whole goal loop). */}
{stream.goal && <GoalStatusBanner goal={stream.goal} />}
{input}
</div>
</div>
</>
)