diff --git a/packages/adapters/claude-local/src/ui/parse-stdout.ts b/packages/adapters/claude-local/src/ui/parse-stdout.ts index 737700d..0ff6613 100644 --- a/packages/adapters/claude-local/src/ui/parse-stdout.ts +++ b/packages/adapters/claude-local/src/ui/parse-stdout.ts @@ -63,6 +63,9 @@ export function parseClaudeStdoutLine(line: string, ts: string): TranscriptEntry if (blockType === "text") { const text = typeof block.text === "string" ? block.text : ""; if (text) entries.push({ kind: "assistant", ts, text }); + } else if (blockType === "thinking") { + const text = typeof block.thinking === "string" ? block.thinking : ""; + if (text) entries.push({ kind: "thinking", ts, text }); } else if (blockType === "tool_use") { entries.push({ kind: "tool_call", @@ -83,7 +86,10 @@ export function parseClaudeStdoutLine(line: string, ts: string): TranscriptEntry const block = asRecord(blockRaw); if (!block) continue; const blockType = typeof block.type === "string" ? block.type : ""; - if (blockType === "tool_result") { + if (blockType === "text") { + const text = typeof block.text === "string" ? block.text : ""; + if (text) entries.push({ kind: "user", ts, text }); + } else if (blockType === "tool_result") { const toolUseId = typeof block.tool_use_id === "string" ? block.tool_use_id : ""; const isError = block.is_error === true; let text = ""; @@ -101,7 +107,7 @@ export function parseClaudeStdoutLine(line: string, ts: string): TranscriptEntry } } if (entries.length > 0) return entries; - // fall through to stdout for user messages without tool_result blocks + // fall through to stdout for user messages without recognized blocks } if (type === "result") { diff --git a/ui/src/components/GoalProperties.tsx b/ui/src/components/GoalProperties.tsx index ca48588..be6ee7f 100644 --- a/ui/src/components/GoalProperties.tsx +++ b/ui/src/components/GoalProperties.tsx @@ -1,10 +1,22 @@ +import { useState } from "react"; +import { Link } from "react-router-dom"; +import { useQuery } from "@tanstack/react-query"; import type { Goal } from "@paperclip/shared"; +import { GOAL_STATUSES, GOAL_LEVELS } from "@paperclip/shared"; +import { agentsApi } from "../api/agents"; +import { goalsApi } from "../api/goals"; +import { useCompany } from "../context/CompanyContext"; +import { queryKeys } from "../lib/queryKeys"; import { StatusBadge } from "./StatusBadge"; import { formatDate } from "../lib/utils"; import { Separator } from "@/components/ui/separator"; +import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; +import { Button } from "@/components/ui/button"; +import { cn } from "../lib/utils"; interface GoalPropertiesProps { goal: Goal; + onUpdate?: (data: Record) => void; } function PropertyRow({ label, children }: { label: string; children: React.ReactNode }) { @@ -16,24 +28,124 @@ function PropertyRow({ label, children }: { label: string; children: React.React ); } -export function GoalProperties({ goal }: GoalPropertiesProps) { +function label(s: string): string { + return s.replace(/_/g, " ").replace(/\b\w/g, (c) => c.toUpperCase()); +} + +function PickerButton({ + current, + options, + onChange, + children, +}: { + current: string; + options: readonly string[]; + onChange: (value: string) => void; + children: React.ReactNode; +}) { + const [open, setOpen] = useState(false); + return ( + + + + + + {options.map((opt) => ( + + ))} + + + ); +} + +export function GoalProperties({ goal, onUpdate }: GoalPropertiesProps) { + const { selectedCompanyId } = useCompany(); + + const { data: agents } = useQuery({ + queryKey: queryKeys.agents.list(selectedCompanyId!), + queryFn: () => agentsApi.list(selectedCompanyId!), + enabled: !!selectedCompanyId, + }); + + const { data: allGoals } = useQuery({ + queryKey: queryKeys.goals.list(selectedCompanyId!), + queryFn: () => goalsApi.list(selectedCompanyId!), + enabled: !!selectedCompanyId, + }); + + const ownerAgent = goal.ownerAgentId + ? agents?.find((a) => a.id === goal.ownerAgentId) + : null; + + const parentGoal = goal.parentId + ? allGoals?.find((g) => g.id === goal.parentId) + : null; + return (
- + {onUpdate ? ( + onUpdate({ status })} + > + + + ) : ( + + )} + - {goal.level} + {onUpdate ? ( + onUpdate({ level })} + > + {goal.level} + + ) : ( + {goal.level} + )} - {goal.ownerAgentId && ( - - {goal.ownerAgentId.slice(0, 8)} - - )} + + + {ownerAgent ? ( + + {ownerAgent.name} + + ) : ( + None + )} + + {goal.parentId && ( - {goal.parentId.slice(0, 8)} + + {parentGoal?.title ?? goal.parentId.slice(0, 8)} + )}
diff --git a/ui/src/components/InlineEditor.tsx b/ui/src/components/InlineEditor.tsx index f1fc157..cbfd6b1 100644 --- a/ui/src/components/InlineEditor.tsx +++ b/ui/src/components/InlineEditor.tsx @@ -1,4 +1,4 @@ -import { useState, useRef, useEffect } from "react"; +import { useState, useRef, useEffect, useCallback } from "react"; import { cn } from "../lib/utils"; interface InlineEditorProps { @@ -10,6 +10,9 @@ interface InlineEditorProps { multiline?: boolean; } +/** Shared padding so display and edit modes occupy the exact same box. */ +const pad = "px-1 -mx-1"; + export function InlineEditor({ value, onSave, @@ -26,12 +29,21 @@ export function InlineEditor({ setDraft(value); }, [value]); + const autoSize = useCallback((el: HTMLTextAreaElement | null) => { + if (!el) return; + el.style.height = "auto"; + el.style.height = `${el.scrollHeight}px`; + }, []); + useEffect(() => { if (editing && inputRef.current) { inputRef.current.focus(); inputRef.current.select(); + if (multiline && inputRef.current instanceof HTMLTextAreaElement) { + autoSize(inputRef.current); + } } - }, [editing]); + }, [editing, multiline, autoSize]); function commit() { const trimmed = draft.trim(); @@ -58,26 +70,49 @@ export function InlineEditor({ const sharedProps = { ref: inputRef as any, value: draft, - onChange: (e: React.ChangeEvent) => - setDraft(e.target.value), + onChange: (e: React.ChangeEvent) => { + setDraft(e.target.value); + if (multiline && e.target instanceof HTMLTextAreaElement) { + autoSize(e.target); + } + }, onBlur: commit, onKeyDown: handleKeyDown, - className: cn( - "bg-transparent border border-border rounded px-2 py-1 w-full outline-none focus:ring-1 focus:ring-ring", - className - ), }; if (multiline) { - return