summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorsoryu <soryu@soryu.co>2026-01-26 19:15:03 +0000
committersoryu <soryu@soryu.co>2026-01-26 19:15:03 +0000
commit2ee09745c12f0762ab99fadb00cdc0c363cb027b (patch)
tree7a634661956989c1b10e2ad50969a8d9d9a828b2
parentcb4f2fc40dbabb40de948512eee74c7e46264665 (diff)
downloadsoryu-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.tsx181
-rw-r--r--makima/frontend/src/components/mesh/TaskDetail.tsx13
-rw-r--r--makima/frontend/src/lib/api.ts142
-rw-r--r--makima/frontend/tsconfig.tsbuildinfo2
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