diff --git a/seed/tasks/convert-to-mp3/TASK.md b/seed/tasks/convert-to-mp3/TASK.md index 4293fe6d..87443309 100644 --- a/seed/tasks/convert-to-mp3/TASK.md +++ b/seed/tasks/convert-to-mp3/TASK.md @@ -26,6 +26,10 @@ inputs: file_path: type: string description: Path to an audio file or directory to convert. + delete_source: + type: boolean + description: Delete source files after successful conversion. + default: false args: [file_path] --- diff --git a/seed/tasks/convert-to-mp3/run.sh b/seed/tasks/convert-to-mp3/run.sh index 1b3732d4..86080f21 100755 --- a/seed/tasks/convert-to-mp3/run.sh +++ b/seed/tasks/convert-to-mp3/run.sh @@ -4,6 +4,7 @@ set -euo pipefail # ── Task: Convert To MP3 ── EXTENSIONS="flac|wav|ogg|wma|aac|m4a|opus|aiff|aif|ape|wv|alac|dsf|dff" +DELETE_SOURCE="${INPUT_DELETE_SOURCE:-false}" process_file() { local input="$1" @@ -25,8 +26,12 @@ process_file() { fi echo "Converting: $input → $output" - if ffmpeg -i "$input" -codec:a libmp3lame -b:a 320k -map_metadata 0 -id3v2_version 3 -y "$output" 2>/dev/null; then + if ffmpeg -nostdin -i "$input" -codec:a libmp3lame -b:a 320k -map_metadata 0 -id3v2_version 3 -y "$output" 2>/dev/null; then echo " Done" + if [[ "$DELETE_SOURCE" == "true" ]]; then + rm "$input" + echo " Deleted source" + fi else echo " Failed" >&2 return 1 diff --git a/src/workspaces/officerdev/src/apps/FileBrowser/FileBrowserApp/components/TaskRunnerModal.tsx b/src/workspaces/officerdev/src/apps/FileBrowser/FileBrowserApp/components/TaskRunnerModal.tsx index 2fa7b97e..e9acc720 100644 --- a/src/workspaces/officerdev/src/apps/FileBrowser/FileBrowserApp/components/TaskRunnerModal.tsx +++ b/src/workspaces/officerdev/src/apps/FileBrowser/FileBrowserApp/components/TaskRunnerModal.tsx @@ -1,5 +1,5 @@ import { useState, useEffect, useRef } from 'react'; -import { X, Play, Square, CircleCheck, CircleX } from 'lucide-react'; +import { X, Play, Square, CircleCheck, CircleX, Copy, Check } from 'lucide-react'; import { Dialog, DialogOverlay, DialogPortal } from '@/components/ui/dialog'; import * as DialogPrimitive from '@radix-ui/react-dialog'; import { cardStyle } from '@/components/Card'; @@ -7,6 +7,7 @@ import type { TaskInfo, ChatMessage } from '../../../Chat'; import { usePiChat, MessageBubble, StreamingBubble, ModelSelector } from '../../../Chat'; import { useSettings } from 'state/useSettings'; import { useUserVisibleModels } from 'state/useModels'; +import { useClient } from 'hooks/useClient'; import type { TaskSummary } from '../../useTasks'; import { useTaskRunner } from './useTaskRunner'; @@ -191,17 +192,104 @@ const PiMonoInner = ({ defaultInput, cwd, initialModel, taskInfo, sandboxed }: P ); }; +// ── Task input definitions ── + +type TaskInputDef = { + type: string; + description?: string; + default?: string; +}; + +type TaskInputFormProps = { + inputDefs: Record; + values: Record; + onChange: (key: string, value: string) => void; + autoFilledKeys: Set; +}; + +const TaskInputForm = ({ inputDefs, values, onChange, autoFilledKeys }: TaskInputFormProps) => { + const configurableInputs = Object.entries(inputDefs).filter(([key]) => !autoFilledKeys.has(key)); + if (configurableInputs.length === 0) return null; + + return ( +
+ {configurableInputs.map(([key, def]) => { + if (def.type === 'boolean') { + const isTrue = values[key] === 'true'; + return ( +
+
+ {def.description ?? key} +
+
+ + +
+
+ ); + } + + // Default: text input for string/number + return ( +
+ + onChange(key, e.target.value)} + className="px-3 py-1.5 text-sm rounded-lg border border-duck-dark/15 dark:border-foreground/15 bg-background focus:outline-none focus:ring-1 focus:ring-duck-teal" + /> +
+ ); + })} +
+ ); +}; + // ── Script-mode runner ── type ScriptRunnerProps = { taskDirName: string; - inputs: Record; + autoInputs: Record; cwd?: string; }; -const ScriptRunner = ({ taskDirName, inputs, cwd }: ScriptRunnerProps) => { +const ScriptRunner = ({ taskDirName, autoInputs, cwd }: ScriptRunnerProps) => { const runner = useTaskRunner(); + const client = useClient(); const bottomRef = useRef(null); + const [inputDefs, setInputDefs] = useState | null>(null); + const [formValues, setFormValues] = useState>({}); + + // Fetch task detail to get input definitions + useEffect(() => { + client.get<{ inputs?: Record }>(`/tasks/${taskDirName}`).then((task) => { + const defs = task.inputs ?? {}; + setInputDefs(defs); + // Initialize form values from defaults + const defaults: Record = {}; + for (const [key, def] of Object.entries(defs)) { + if (def.default !== undefined) defaults[key] = def.default; + } + setFormValues(defaults); + }); + }, [taskDirName]); + + const autoFilledKeys = new Set(Object.keys(autoInputs)); + + const handleInputChange = (key: string, value: string) => { + setFormValues((prev) => ({ ...prev, [key]: value })); + }; // Auto-scroll useEffect(() => { @@ -217,21 +305,40 @@ const ScriptRunner = ({ taskDirName, inputs, cwd }: ScriptRunnerProps) => { prevPhaseRef.current = runner.phase; }, [runner.phase]); + const [copied, setCopied] = useState(false); + const handleCopy = () => { + const text = runner.output.map((line) => line.text).join(''); + navigator.clipboard.writeText(text); + setCopied(true); + setTimeout(() => setCopied(false), 2000); + }; + const handleRun = () => { - runner.run(taskDirName, inputs, cwd); + const allInputs = { ...formValues, ...autoInputs }; + runner.run(taskDirName, allInputs, cwd); }; if (runner.phase === 'ready') { return ( -
- +
+ {inputDefs && ( + + )} +
+ +
); } @@ -239,22 +346,33 @@ const ScriptRunner = ({ taskDirName, inputs, cwd }: ScriptRunnerProps) => { return (
-
-          {runner.output.map((line, i) => (
-            
-              {line.text}
-            
-          ))}
-        
+
+ +
+            
+              {runner.output.map((line, i) => (
+                
+                  {line.text}
+                
+              ))}
+            
+          
+
@@ -308,9 +426,9 @@ export const TaskRunnerModal = ({ open, onOpenChange, task, entryName, entryFull : `Execute the task "${task.name}" (${task.dirName})`); const taskInfo: TaskInfo = { taskName: task.name, taskDirName: task.dirName, entryName: entryName ?? '', entryType: entryType ?? 'file' }; - // Script mode inputs — for now, map the entry path to file_path - const scriptInputs: Record = {}; - if (entryFullPath) scriptInputs.file_path = entryFullPath; + // Script mode: auto-filled inputs from context (e.g. file_path from file browser) + const autoInputs: Record = {}; + if (entryFullPath) autoInputs.file_path = entryFullPath; return ( @@ -342,7 +460,7 @@ export const TaskRunnerModal = ({ open, onOpenChange, task, entryName, entryFull ) : (