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:
@@ -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}
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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";
|
||||
|
||||
Reference in New Issue
Block a user