diff options
| author | soryu <soryu@soryu.co> | 2026-01-26 19:15:03 +0000 |
|---|---|---|
| committer | soryu <soryu@soryu.co> | 2026-01-26 19:15:03 +0000 |
| commit | 2ee09745c12f0762ab99fadb00cdc0c363cb027b (patch) | |
| tree | 7a634661956989c1b10e2ad50969a8d9d9a828b2 | |
| parent | cb4f2fc40dbabb40de948512eee74c7e46264665 (diff) | |
| download | soryu-makima/task-task-6dc740fd-6dc740fd.tar.gz soryu-makima/task-task-6dc740fd-6dc740fd.zip | |
[WIP] Heartbeat checkpoint - 2026-01-26 19:15:03 UTCmakima/task-task-6dc740fd-6dc740fd
| -rw-r--r-- | makima/frontend/src/components/mesh/GitActionsPanel.tsx | 181 | ||||
| -rw-r--r-- | makima/frontend/src/components/mesh/TaskDetail.tsx | 13 | ||||
| -rw-r--r-- | makima/frontend/src/lib/api.ts | 142 | ||||
| -rw-r--r-- | makima/frontend/tsconfig.tsbuildinfo | 2 |
4 files changed, 301 insertions, 37 deletions
diff --git a/makima/frontend/src/components/mesh/GitActionsPanel.tsx b/makima/frontend/src/components/mesh/GitActionsPanel.tsx new file mode 100644 index 0000000..be2e06d --- /dev/null +++ b/makima/frontend/src/components/mesh/GitActionsPanel.tsx @@ -0,0 +1,181 @@ +import { useState, useCallback } from "react"; +import { exportTaskPatch, pushTaskBranch, createTaskPR, type ExportPatchResponse } from "../../lib/api"; + +interface GitActionsPanelProps { + taskId: string; + isLocalOnly: boolean; + taskStatus: string; +} + +interface ToastMessage { + type: "success" | "error" | "info"; + message: string; +} + +export function GitActionsPanel({ + taskId, + isLocalOnly, + taskStatus, +}: GitActionsPanelProps) { + const [isExporting, setIsExporting] = useState(false); + const [isPushing, setIsPushing] = useState(false); + const [isCreatingPR, setIsCreatingPR] = useState(false); + const [toast, setToast] = useState<ToastMessage | null>(null); + const [exportedPatch, setExportedPatch] = useState<ExportPatchResponse | null>(null); + + // Only show for completed tasks + if (taskStatus !== "done") return null; + + const showToast = (type: ToastMessage["type"], message: string) => { + setToast({ type, message }); + setTimeout(() => setToast(null), 5000); + }; + + const handleExportPatch = useCallback(async () => { + setIsExporting(true); + try { + const result = await exportTaskPatch(taskId); + setExportedPatch(result); + showToast("success", `Patch generated: ${result.fileName}`); + } catch (e) { + showToast("error", e instanceof Error ? e.message : "Failed to generate patch"); + } finally { + setIsExporting(false); + } + }, [taskId]); + + const handlePushBranch = useCallback(async () => { + if (isLocalOnly) { + showToast("error", "Push disabled in local-only mode"); + return; + } + setIsPushing(true); + try { + const result = await pushTaskBranch(taskId); + showToast("success", `Branch pushed: ${result.branchName}`); + } catch (e) { + showToast("error", e instanceof Error ? e.message : "Failed to push branch"); + } finally { + setIsPushing(false); + } + }, [taskId, isLocalOnly]); + + const handleCreatePR = useCallback(async () => { + if (isLocalOnly) { + showToast("error", "PR creation disabled in local-only mode"); + return; + } + setIsCreatingPR(true); + try { + const result = await createTaskPR(taskId); + if (result.prUrl) { + showToast("success", `PR created: ${result.prUrl}`); + // Open PR in new tab + window.open(result.prUrl, "_blank", "noopener,noreferrer"); + } else { + showToast("success", "PR creation initiated"); + } + } catch (e) { + showToast("error", e instanceof Error ? e.message : "Failed to create PR"); + } finally { + setIsCreatingPR(false); + } + }, [taskId, isLocalOnly]); + + return ( + <div className="space-y-3"> + {/* Section Header */} + <div className="font-mono text-xs text-[#9bc3ff] tracking-wide uppercase"> + Git Actions + </div> + + {/* Local-only mode alert */} + {isLocalOnly && ( + <div className="bg-yellow-400/10 border border-yellow-400/30 px-3 py-2 font-mono text-xs text-yellow-400 flex items-center gap-2"> + <span className="w-1.5 h-1.5 bg-yellow-400 rounded-full" /> + Contract is in local-only mode. Push/PR actions disabled. + </div> + )} + + {/* Toast notification */} + {toast && ( + <div + className={`px-3 py-2 font-mono text-xs border ${ + toast.type === "success" + ? "bg-green-400/10 border-green-400/30 text-green-400" + : toast.type === "error" + ? "bg-red-400/10 border-red-400/30 text-red-400" + : "bg-blue-400/10 border-blue-400/30 text-blue-400" + }`} + > + {toast.message} + </div> + )} + + {/* Action buttons */} + <div className="flex flex-wrap gap-2"> + <button + onClick={handleExportPatch} + disabled={isExporting} + className="px-3 py-1.5 font-mono text-xs text-[#75aafc] border border-[rgba(117,170,252,0.25)] hover:border-[#3f6fb3] hover:bg-[rgba(117,170,252,0.05)] transition-colors disabled:opacity-50 disabled:cursor-not-allowed" + > + {isExporting ? "Generating..." : "Generate Patch"} + </button> + + <button + onClick={handlePushBranch} + disabled={isPushing || isLocalOnly} + className={`px-3 py-1.5 font-mono text-xs border transition-colors disabled:opacity-50 disabled:cursor-not-allowed ${ + isLocalOnly + ? "text-[#555] border-[rgba(117,170,252,0.15)] cursor-not-allowed" + : "text-cyan-400 border-cyan-400/30 hover:border-cyan-400/50 hover:bg-cyan-400/10" + }`} + title={isLocalOnly ? "Disabled in local-only mode" : "Push changes to remote branch"} + > + {isPushing ? "Pushing..." : "Push Remote Branch"} + </button> + + <button + onClick={handleCreatePR} + disabled={isCreatingPR || isLocalOnly} + className={`px-3 py-1.5 font-mono text-xs border transition-colors disabled:opacity-50 disabled:cursor-not-allowed ${ + isLocalOnly + ? "text-[#555] border-[rgba(117,170,252,0.15)] cursor-not-allowed" + : "text-green-400 border-green-400/30 hover:border-green-400/50 hover:bg-green-400/10" + }`} + title={isLocalOnly ? "Disabled in local-only mode" : "Create pull request"} + > + {isCreatingPR ? "Creating PR..." : "Create PR"} + </button> + </div> + + {/* Export result info */} + {exportedPatch && ( + <div className="bg-[rgba(0,0,0,0.2)] border border-[rgba(117,170,252,0.15)] p-3 space-y-2"> + <div className="font-mono text-xs text-[#555]">Exported Patch</div> + <div className="font-mono text-xs text-[#75aafc] break-all"> + <span className="text-[#555]">File:</span> {exportedPatch.fileName} + </div> + {exportedPatch.filePath && ( + <div className="font-mono text-xs text-[#75aafc] break-all"> + <span className="text-[#555]">Path:</span> {exportedPatch.filePath} + </div> + )} + {exportedPatch.patchSize && ( + <div className="font-mono text-xs text-[#75aafc]"> + <span className="text-[#555]">Size:</span> {formatBytes(exportedPatch.patchSize)} + </div> + )} + </div> + )} + </div> + ); +} + +function formatBytes(bytes: number): string { + if (bytes === 0) return "0 B"; + const k = 1024; + const sizes = ["B", "KB", "MB", "GB"]; + const i = Math.floor(Math.log(bytes) / Math.log(k)); + return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i]; +} diff --git a/makima/frontend/src/components/mesh/TaskDetail.tsx b/makima/frontend/src/components/mesh/TaskDetail.tsx index 8936d28..2822b6d 100644 --- a/makima/frontend/src/components/mesh/TaskDetail.tsx +++ b/makima/frontend/src/components/mesh/TaskDetail.tsx @@ -7,6 +7,7 @@ import { PRPreview } from "./PRPreview"; import { InlineSubtaskEditor } from "./InlineSubtaskEditor"; import { DirectoryInput } from "./DirectoryInput"; import { BranchTaskModal } from "./BranchTaskModal"; +import { GitActionsPanel } from "./GitActionsPanel"; interface TaskDetailProps { task: TaskWithSubtasks; @@ -36,6 +37,8 @@ interface TaskDetailProps { fetchSubtasks?: (taskId: string) => Promise<TaskSummary[]>; /** For supervisor tasks: all tasks in the contract (excluding the supervisor itself) */ contractTasks?: TaskSummary[]; + /** Whether the contract is in local-only mode (no push/PR) */ + isLocalOnly?: boolean; } function formatDate(dateStr: string): string { @@ -119,6 +122,7 @@ export function TaskDetail({ onAutoMerge, fetchSubtasks, contractTasks, + isLocalOnly = false, }: TaskDetailProps) { const [isEditing, setIsEditing] = useState(false); const [editName, setEditName] = useState(task.name); @@ -879,6 +883,15 @@ export function TaskDetail({ )} </div> )} + + {/* Git Actions Panel for manual git operations */} + {task.status === "done" && ( + <GitActionsPanel + taskId={task.id} + isLocalOnly={isLocalOnly} + taskStatus={task.status} + /> + )} </div> {/* Overlay Diff Modal */} diff --git a/makima/frontend/src/lib/api.ts b/makima/frontend/src/lib/api.ts index 64ce591..4a7486f 100644 --- a/makima/frontend/src/lib/api.ts +++ b/makima/frontend/src/lib/api.ts @@ -811,6 +811,112 @@ export async function retryCompletionAction( return res.json(); } +// ============================================================================= +// Git Actions for Tasks (Manual) +// ============================================================================= + +/** Response from export patch */ +export interface ExportPatchResponse { + success: boolean; + taskId: string; + fileName: string; + filePath?: string; + patchSize?: number; + message: string; +} + +/** + * Export a task's changes as a patch file. + * The patch will be saved to the contract's patch directory. + */ +export async function exportTaskPatch( + taskId: string, + fileName?: string +): Promise<ExportPatchResponse> { + const body: Record<string, unknown> = {}; + if (fileName) { + body.fileName = fileName; + } + const res = await authFetch(`${API_BASE}/api/v1/mesh/tasks/${taskId}/export-patch`, { + method: "POST", + body: JSON.stringify(body), + }); + if (!res.ok) { + const errorText = await res.text(); + throw new Error(`Failed to export patch: ${errorText || res.statusText}`); + } + return res.json(); +} + +/** Response from push branch */ +export interface PushBranchResponse { + success: boolean; + taskId: string; + branchName: string; + remote?: string; + message: string; +} + +/** + * Push a task's changes to a remote branch. + * Creates a branch if it doesn't exist and pushes the commits. + */ +export async function pushTaskBranch( + taskId: string, + branchName?: string +): Promise<PushBranchResponse> { + const body: Record<string, unknown> = {}; + if (branchName) { + body.branchName = branchName; + } + const res = await authFetch(`${API_BASE}/api/v1/mesh/tasks/${taskId}/push-branch`, { + method: "POST", + body: JSON.stringify(body), + }); + if (!res.ok) { + const errorText = await res.text(); + throw new Error(`Failed to push branch: ${errorText || res.statusText}`); + } + return res.json(); +} + +/** Response from create PR */ +export interface CreatePRResponse { + success: boolean; + taskId: string; + prUrl?: string; + prNumber?: number; + branchName?: string; + message: string; +} + +/** + * Create a pull request for a task's changes. + * First pushes the branch if needed, then creates the PR. + */ +export async function createTaskPR( + taskId: string, + title?: string, + body?: string +): Promise<CreatePRResponse> { + const reqBody: Record<string, unknown> = {}; + if (title) { + reqBody.title = title; + } + if (body) { + reqBody.body = body; + } + const res = await authFetch(`${API_BASE}/api/v1/mesh/tasks/${taskId}/create-pr`, { + method: "POST", + body: JSON.stringify(reqBody), + }); + if (!res.ok) { + const errorText = await res.text(); + throw new Error(`Failed to create PR: ${errorText || res.statusText}`); + } + return res.json(); +} + /** A suggested directory from a connected daemon */ export interface DaemonDirectory { /** Path to the directory */ @@ -2045,42 +2151,6 @@ export async function getTemplate(id: string): Promise<FileTemplate> { } // ============================================================================= -// Contract Type Templates (Workflow Definitions) -// ============================================================================= - -/** A contract type template defining a workflow */ -export interface ContractTypeTemplate { - /** Unique identifier (e.g., 'simple', 'specification', 'feature-development') */ - id: string; - /** Display name */ - name: string; - /** What this contract type is for */ - description: string; - /** Ordered list of phases in the workflow */ - phases: string[]; - /** Starting phase */ - defaultPhase: string; - /** True for built-in types ('simple', 'specification') */ - isBuiltin: boolean; -} - -export interface ListContractTypesResponse { - contractTypes: ContractTypeTemplate[]; -} - -/** - * List all available contract type templates. - * Returns built-in types (simple, specification) and any custom types. - */ -export async function listContractTypes(): Promise<ListContractTypesResponse> { - const res = await authFetch(`${API_BASE}/api/v1/contract-types`); - if (!res.ok) { - throw new Error(`Failed to list contract types: ${res.statusText}`); - } - return res.json(); -} - -// ============================================================================= // Supervisor Question Types and Functions // ============================================================================= diff --git a/makima/frontend/tsconfig.tsbuildinfo b/makima/frontend/tsconfig.tsbuildinfo index 034501a..eff7718 100644 --- a/makima/frontend/tsconfig.tsbuildinfo +++ b/makima/frontend/tsconfig.tsbuildinfo @@ -1 +1 @@ -{"root":["./src/main.tsx","./src/vite-env.d.ts","./src/components/gridoverlay.tsx","./src/components/japanesehovertext.tsx","./src/components/logo.tsx","./src/components/masthead.tsx","./src/components/navstrip.tsx","./src/components/phaseconfirmationnotification.tsx","./src/components/protectedroute.tsx","./src/components/rewritelink.tsx","./src/components/simplemarkdown.tsx","./src/components/supervisorquestionnotification.tsx","./src/components/charts/chartrenderer.tsx","./src/components/contracts/autopilotpanel.tsx","./src/components/contracts/contractcliinput.tsx","./src/components/contracts/contractcontextmenu.tsx","./src/components/contracts/contractdetail.tsx","./src/components/contracts/contractlist.tsx","./src/components/contracts/phasebadge.tsx","./src/components/contracts/phaseconfirmationmodal.tsx","./src/components/contracts/phasedeliverablespanel.tsx","./src/components/contracts/phasehint.tsx","./src/components/contracts/phaseprogressbar.tsx","./src/components/contracts/quickactionbuttons.tsx","./src/components/contracts/repositorypanel.tsx","./src/components/contracts/taskderivationpreview.tsx","./src/components/files/bodyrenderer.tsx","./src/components/files/cliinput.tsx","./src/components/files/conflictnotification.tsx","./src/components/files/elementcontextmenu.tsx","./src/components/files/filedetail.tsx","./src/components/files/filelist.tsx","./src/components/files/reposyncindicator.tsx","./src/components/files/updatenotification.tsx","./src/components/files/versionhistorydropdown.tsx","./src/components/history/checkpointcard.tsx","./src/components/history/checkpointlist.tsx","./src/components/history/conversationmessage.tsx","./src/components/history/conversationview.tsx","./src/components/history/historyfilters.tsx","./src/components/history/resumecontrols.tsx","./src/components/history/timelineeventcard.tsx","./src/components/history/timelinelist.tsx","./src/components/history/index.ts","./src/components/listen/contractpickermodal.tsx","./src/components/listen/controlpanel.tsx","./src/components/listen/speakerpanel.tsx","./src/components/listen/transcriptanalysispanel.tsx","./src/components/listen/transcriptpanel.tsx","./src/components/mesh/branchtaskmodal.tsx","./src/components/mesh/contractcompletequestion.tsx","./src/components/mesh/directoryinput.tsx","./src/components/mesh/inlinesubtaskeditor.tsx","./src/components/mesh/mergeconflictresolver.tsx","./src/components/mesh/overlaydiffviewer.tsx","./src/components/mesh/prpreview.tsx","./src/components/mesh/subtasktree.tsx","./src/components/mesh/taskdetail.tsx","./src/components/mesh/tasklist.tsx","./src/components/mesh/taskoutput.tsx","./src/components/mesh/tasktree.tsx","./src/components/mesh/unifiedmeshchatinput.tsx","./src/components/workflow/phasecolumn.tsx","./src/components/workflow/workflowboard.tsx","./src/components/workflow/workflowcontractcard.tsx","./src/contexts/authcontext.tsx","./src/contexts/supervisorquestionscontext.tsx","./src/hooks/usecontracts.ts","./src/hooks/usefilesubscription.ts","./src/hooks/usefiles.ts","./src/hooks/usemeshchathistory.ts","./src/hooks/usemicrophone.ts","./src/hooks/usetasksubscription.ts","./src/hooks/usetasks.ts","./src/hooks/usetextscramble.ts","./src/hooks/useversionhistory.ts","./src/hooks/usewebsocket.ts","./src/lib/api.ts","./src/lib/listenapi.ts","./src/lib/markdown.ts","./src/lib/supabase.ts","./src/routes/_index.tsx","./src/routes/contract-file.tsx","./src/routes/contracts.tsx","./src/routes/files.tsx","./src/routes/history.tsx","./src/routes/listen.tsx","./src/routes/login.tsx","./src/routes/mesh.tsx","./src/routes/settings.tsx","./src/routes/workflow.tsx","./src/types/messages.ts"],"version":"5.9.3"}
\ No newline at end of file +{"root":["./src/main.tsx","./src/vite-env.d.ts","./src/components/gridoverlay.tsx","./src/components/japanesehovertext.tsx","./src/components/logo.tsx","./src/components/masthead.tsx","./src/components/navstrip.tsx","./src/components/phaseconfirmationnotification.tsx","./src/components/protectedroute.tsx","./src/components/rewritelink.tsx","./src/components/simplemarkdown.tsx","./src/components/supervisorquestionnotification.tsx","./src/components/charts/chartrenderer.tsx","./src/components/contracts/autopilotpanel.tsx","./src/components/contracts/contractcliinput.tsx","./src/components/contracts/contractcontextmenu.tsx","./src/components/contracts/contractdetail.tsx","./src/components/contracts/contractlist.tsx","./src/components/contracts/phasebadge.tsx","./src/components/contracts/phaseconfirmationmodal.tsx","./src/components/contracts/phasedeliverablespanel.tsx","./src/components/contracts/phasehint.tsx","./src/components/contracts/phaseprogressbar.tsx","./src/components/contracts/quickactionbuttons.tsx","./src/components/contracts/repositorypanel.tsx","./src/components/contracts/taskderivationpreview.tsx","./src/components/files/bodyrenderer.tsx","./src/components/files/cliinput.tsx","./src/components/files/conflictnotification.tsx","./src/components/files/elementcontextmenu.tsx","./src/components/files/filedetail.tsx","./src/components/files/filelist.tsx","./src/components/files/reposyncindicator.tsx","./src/components/files/updatenotification.tsx","./src/components/files/versionhistorydropdown.tsx","./src/components/history/checkpointcard.tsx","./src/components/history/checkpointlist.tsx","./src/components/history/conversationmessage.tsx","./src/components/history/conversationview.tsx","./src/components/history/historyfilters.tsx","./src/components/history/resumecontrols.tsx","./src/components/history/timelineeventcard.tsx","./src/components/history/timelinelist.tsx","./src/components/history/index.ts","./src/components/listen/contractpickermodal.tsx","./src/components/listen/controlpanel.tsx","./src/components/listen/speakerpanel.tsx","./src/components/listen/transcriptanalysispanel.tsx","./src/components/listen/transcriptpanel.tsx","./src/components/mesh/branchtaskmodal.tsx","./src/components/mesh/contractcompletequestion.tsx","./src/components/mesh/directoryinput.tsx","./src/components/mesh/gitactionspanel.tsx","./src/components/mesh/inlinesubtaskeditor.tsx","./src/components/mesh/mergeconflictresolver.tsx","./src/components/mesh/overlaydiffviewer.tsx","./src/components/mesh/prpreview.tsx","./src/components/mesh/subtasktree.tsx","./src/components/mesh/taskdetail.tsx","./src/components/mesh/tasklist.tsx","./src/components/mesh/taskoutput.tsx","./src/components/mesh/tasktree.tsx","./src/components/mesh/unifiedmeshchatinput.tsx","./src/components/templates/templateeditor.tsx","./src/components/workflow/phasecolumn.tsx","./src/components/workflow/workflowboard.tsx","./src/components/workflow/workflowcontractcard.tsx","./src/contexts/authcontext.tsx","./src/contexts/supervisorquestionscontext.tsx","./src/hooks/usecontracts.ts","./src/hooks/usefilesubscription.ts","./src/hooks/usefiles.ts","./src/hooks/usemeshchathistory.ts","./src/hooks/usemicrophone.ts","./src/hooks/usetasksubscription.ts","./src/hooks/usetasks.ts","./src/hooks/usetextscramble.ts","./src/hooks/useversionhistory.ts","./src/hooks/usewebsocket.ts","./src/lib/api.ts","./src/lib/listenapi.ts","./src/lib/markdown.ts","./src/lib/supabase.ts","./src/routes/_index.tsx","./src/routes/contract-file.tsx","./src/routes/contracts.tsx","./src/routes/files.tsx","./src/routes/history.tsx","./src/routes/listen.tsx","./src/routes/login.tsx","./src/routes/mesh.tsx","./src/routes/settings.tsx","./src/routes/templates.tsx","./src/routes/workflow.tsx","./src/types/messages.ts","./src/types/templates.ts"],"errors":true,"version":"5.9.3"}
\ No newline at end of file |
