feat: add task management CLI and improve activity curation

- Add task store with dependency tracking and CLI (packages/server/src/tasks/)
- Extract principal param logic to shared util for server/app reuse
- Improve activity curator: collapse timeline, dedupe tool calls, simpler output
- Add MessageInput ref for focus control after agent cancel
- Include recent activity in wait timeout messages

🤖 Generated with [Claude Code](https://claude.com/claude-code)
This commit is contained in:
Mohamed Boudra
2026-01-08 14:56:10 +07:00
parent 80886592ac
commit d233115ab9
17 changed files with 1779 additions and 130 deletions
@@ -26,6 +26,7 @@ import {
MessageInput,
type MessagePayload,
type ImageAttachment,
type MessageInputRef,
} from "./message-input";
import type { UseWebSocketReturn } from "@/hooks/use-websocket";
import { Theme } from "@/styles/theme";
@@ -132,6 +133,7 @@ export function AgentInputArea({
const agentIdRef = useRef(agentId);
const sendAgentMessageRef = useRef(sendAgentMessage);
const onSubmitMessageRef = useRef(onSubmitMessage);
const messageInputRef = useRef<MessageInputRef>(null);
// Expose addImages function to parent for drag-and-drop support
const addImages = useCallback((images: ImageAttachment[]) => {
@@ -338,6 +340,7 @@ export function AgentInputArea({
}
setIsCancellingAgent(true);
cancelAgentRun(agentIdRef.current);
messageInputRef.current?.focus();
}
function handleEditQueuedMessage(id: string) {
@@ -470,6 +473,7 @@ export function AgentInputArea({
{/* MessageInput handles everything: text, dictation, attachments, all buttons */}
<MessageInput
ref={messageInputRef}
value={userInput}
onChangeText={setUserInput}
onSubmit={handleSubmit}
+53 -31
View File
@@ -9,7 +9,14 @@ import {
Platform,
BackHandler,
} from "react-native";
import { useState, useRef, useCallback, useEffect } from "react";
import {
useState,
useRef,
useCallback,
useEffect,
useImperativeHandle,
forwardRef,
} from "react";
import { StyleSheet, useUnistyles } from "react-native-unistyles";
import { Mic, ArrowUp, Paperclip, X, Square } from "lucide-react-native";
import Animated, {
@@ -58,6 +65,10 @@ export interface MessageInputProps {
onQueue?: (payload: MessagePayload) => void;
}
export interface MessageInputRef {
focus: () => void;
}
const MIN_INPUT_HEIGHT = 30;
const MAX_INPUT_HEIGHT = 160;
const IS_WEB = Platform.OS === "web";
@@ -78,35 +89,45 @@ type TextAreaHandle = {
} & Record<string, unknown>;
};
export function MessageInput({
value,
onChangeText,
onSubmit,
isSubmitDisabled = false,
isSubmitLoading = false,
images = [],
onPickImages,
onRemoveImage,
ws,
sendAgentAudio,
placeholder = "Message...",
autoFocus = false,
disabled = false,
leftContent,
rightContent,
isAgentRunning = false,
onQueue,
}: MessageInputProps) {
const { theme } = useUnistyles();
const [inputHeight, setInputHeight] = useState(MIN_INPUT_HEIGHT);
const textInputRef = useRef<
TextInput | (TextInput & { getNativeRef?: () => unknown }) | null
>(null);
const inputHeightRef = useRef(MIN_INPUT_HEIGHT);
const baselineInputHeightRef = useRef<number | null>(null);
const overlayTransition = useSharedValue(0);
const sendAfterTranscriptRef = useRef(false);
const valueRef = useRef(value);
export const MessageInput = forwardRef<MessageInputRef, MessageInputProps>(
function MessageInput(
{
value,
onChangeText,
onSubmit,
isSubmitDisabled = false,
isSubmitLoading = false,
images = [],
onPickImages,
onRemoveImage,
ws,
sendAgentAudio,
placeholder = "Message...",
autoFocus = false,
disabled = false,
leftContent,
rightContent,
isAgentRunning = false,
onQueue,
},
ref
) {
const { theme } = useUnistyles();
const [inputHeight, setInputHeight] = useState(MIN_INPUT_HEIGHT);
const textInputRef = useRef<
TextInput | (TextInput & { getNativeRef?: () => unknown }) | null
>(null);
useImperativeHandle(ref, () => ({
focus: () => {
textInputRef.current?.focus();
},
}));
const inputHeightRef = useRef(MIN_INPUT_HEIGHT);
const baselineInputHeightRef = useRef<number | null>(null);
const overlayTransition = useSharedValue(0);
const sendAfterTranscriptRef = useRef(false);
const valueRef = useRef(value);
useEffect(() => {
valueRef.current = value;
@@ -566,7 +587,8 @@ export function MessageInput({
</Animated.View>
</View>
);
}
}
);
const styles = StyleSheet.create(((theme: any) => ({
container: {
+7 -36
View File
@@ -754,39 +754,10 @@ export function extractKeyValuePairs(result: unknown): KeyValuePair[] {
}
// ---- Principal Parameter Extraction ----
const PrincipalParamSchema = z.union([
z.object({ file_path: z.string() }).transform((d) => ({ type: "path" as const, value: d.file_path })),
z.object({ filePath: z.string() }).transform((d) => ({ type: "path" as const, value: d.filePath })),
z.object({ path: z.string() }).transform((d) => ({ type: "path" as const, value: d.path })),
z.object({ command: z.string() }).transform((d) => ({ type: "text" as const, value: d.command })),
z.object({ pattern: z.string() }).transform((d) => ({ type: "text" as const, value: d.pattern })),
z.object({ query: z.string() }).transform((d) => ({ type: "text" as const, value: d.query })),
z.object({ url: z.string() }).transform((d) => ({ type: "text" as const, value: d.url })),
]);
export function stripCwdPrefix(filePath: string, cwd?: string): string {
if (!cwd || !filePath) return filePath;
const normalizedCwd = cwd.replace(/\\/g, "/").replace(/\/+$/, "");
const normalizedPath = filePath.replace(/\\/g, "/");
const prefix = `${normalizedCwd}/`;
if (normalizedPath.startsWith(prefix)) {
return normalizedPath.slice(prefix.length);
}
if (normalizedPath === normalizedCwd) {
return ".";
}
return filePath;
}
export function extractPrincipalParam(args: unknown, cwd?: string): string | undefined {
const parsed = PrincipalParamSchema.safeParse(args);
if (!parsed.success) {
return undefined;
}
const { type, value } = parsed.data;
return type === "path" ? stripCwdPrefix(value, cwd) : value;
}
// Re-export from server to avoid drift
export {
extractPrincipalParam,
stripCwdPrefix,
extractTodos,
type TodoItem,
} from "@paseo/server/utils/tool-call-parsers";