summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorsoryu <soryu@soryu.co>2026-01-22 13:16:01 +0000
committersoryu <soryu@soryu.co>2026-01-22 13:16:01 +0000
commitc5cd9fe0515f024a6f442e6b7eca614a38aa6deb (patch)
tree3945da69ce346798b413d7063485aebfd5be3d11
parent1b1b737006f9505b2a188a669c5a37671658ce3f (diff)
downloadsoryu-makima/task-task-5dde682c-5dde682c.tar.gz
soryu-makima/task-task-5dde682c-5dde682c.zip
Add dismiss functionality for completed standalone tasksmakima/task-task-5dde682c-5dde682c
## Changes ### Backend - Add 'hidden' field to Task model (models.rs) - Add database migration for hidden column (20250122000000_add_task_hidden.sql) - Update task listing queries to include hidden field and filter out hidden tasks - Update update_task_for_owner to handle hidden field ### Frontend - Add hidden field to TaskSummary interface (api.ts) - Add dismissTask API function (api.ts) - Add hideTask function to useTasks hook - Add Dismiss button to TaskList for completed standalone tasks - Wire up onDismiss handler in mesh.tsx route ## Behavior - Completed standalone tasks (tasks without a contract) show a "Dismiss" button - Dismissing a task sets hidden=true and removes it from the task list - Hidden tasks are filtered out by default in all task listing queries Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
-rw-r--r--makima/frontend/package-lock.json14
-rw-r--r--makima/frontend/src/components/mesh/TaskList.tsx38
-rw-r--r--makima/frontend/src/hooks/useTasks.ts17
-rw-r--r--makima/frontend/src/lib/api.ts16
-rw-r--r--makima/frontend/src/routes/mesh.tsx10
-rw-r--r--makima/migrations/20250122000000_add_task_hidden.sql5
-rw-r--r--makima/src/db/models.rs12
-rw-r--r--makima/src/db/repository.rs25
8 files changed, 114 insertions, 23 deletions
diff --git a/makima/frontend/package-lock.json b/makima/frontend/package-lock.json
index 88297c2..cc842b1 100644
--- a/makima/frontend/package-lock.json
+++ b/makima/frontend/package-lock.json
@@ -54,6 +54,7 @@
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.5.tgz",
"integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==",
"dev": true,
+ "peer": true,
"dependencies": {
"@babel/code-frame": "^7.27.1",
"@babel/generator": "^7.28.5",
@@ -960,6 +961,7 @@
"resolved": "https://registry.npmjs.org/@react-router/dev/-/dev-7.11.0.tgz",
"integrity": "sha512-g1ou5Zw3r4mCU0L+EXH4vRtAiyt8qz1JOvL1k+PW4rZ4+71h5nBy/fLgD7cg5BnzQZmjRO1PzCgpF5BIrlKYxQ==",
"dev": true,
+ "peer": true,
"dependencies": {
"@babel/core": "^7.27.7",
"@babel/generator": "^7.27.5",
@@ -1857,6 +1859,7 @@
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.7.tgz",
"integrity": "sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg==",
"devOptional": true,
+ "peer": true,
"dependencies": {
"csstype": "^3.2.2"
}
@@ -1973,6 +1976,7 @@
"url": "https://github.com/sponsors/ai"
}
],
+ "peer": true,
"dependencies": {
"baseline-browser-mapping": "^2.9.0",
"caniuse-lite": "^1.0.30001759",
@@ -2793,6 +2797,7 @@
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"dev": true,
+ "peer": true,
"engines": {
"node": ">=12"
},
@@ -2864,6 +2869,7 @@
"version": "19.2.3",
"resolved": "https://registry.npmjs.org/react/-/react-19.2.3.tgz",
"integrity": "sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==",
+ "peer": true,
"engines": {
"node": ">=0.10.0"
}
@@ -2872,6 +2878,7 @@
"version": "19.2.3",
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.3.tgz",
"integrity": "sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg==",
+ "peer": true,
"dependencies": {
"scheduler": "^0.27.0"
},
@@ -2889,6 +2896,7 @@
"version": "9.2.0",
"resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz",
"integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==",
+ "peer": true,
"dependencies": {
"@types/use-sync-external-store": "^0.0.6",
"use-sync-external-store": "^1.4.0"
@@ -2920,6 +2928,7 @@
"version": "7.11.0",
"resolved": "https://registry.npmjs.org/react-router/-/react-router-7.11.0.tgz",
"integrity": "sha512-uI4JkMmjbWCZc01WVP2cH7ZfSzH91JAZUDd7/nIprDgWxBV1TkkmLToFh7EbMTcMak8URFRa2YoBL/W8GWnCTQ==",
+ "peer": true,
"dependencies": {
"cookie": "^1.0.1",
"set-cookie-parser": "^2.6.0"
@@ -2979,7 +2988,8 @@
"node_modules/redux": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz",
- "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w=="
+ "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==",
+ "peer": true
},
"node_modules/redux-thunk": {
"version": "3.1.0",
@@ -3116,6 +3126,7 @@
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"dev": true,
+ "peer": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
@@ -3207,6 +3218,7 @@
"resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz",
"integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==",
"dev": true,
+ "peer": true,
"dependencies": {
"esbuild": "^0.25.0",
"fdir": "^6.4.4",
diff --git a/makima/frontend/src/components/mesh/TaskList.tsx b/makima/frontend/src/components/mesh/TaskList.tsx
index 80077b6..a38fe07 100644
--- a/makima/frontend/src/components/mesh/TaskList.tsx
+++ b/makima/frontend/src/components/mesh/TaskList.tsx
@@ -6,6 +6,7 @@ interface TaskListProps {
loading: boolean;
onSelect: (id: string) => void;
onDelete: (id: string) => void;
+ onDismiss: (id: string) => void;
onCreate: () => void;
}
@@ -88,6 +89,7 @@ export function TaskList({
loading,
onSelect,
onDelete,
+ onDismiss,
onCreate,
}: TaskListProps) {
// Filter state - default to 'active' to show only active contracts
@@ -291,17 +293,31 @@ export function TaskList({
</div>
</button>
{/* Supervisor tasks cannot be deleted directly - they are deleted with the contract */}
- {!task.isSupervisor && (
- <button
- onClick={(e) => {
- e.stopPropagation();
- onDelete(task.id);
- }}
- className="px-2 py-1 font-mono text-[10px] text-red-400 hover:bg-red-400/10 border border-red-400/30 hover:border-red-400/50 transition-colors uppercase"
- >
- Delete
- </button>
- )}
+ <div className="flex gap-2">
+ {/* Show dismiss button for completed standalone tasks (tasks without a contract) */}
+ {!task.contractId && (task.status === "done" || task.status === "failed" || task.status === "merged") && (
+ <button
+ onClick={(e) => {
+ e.stopPropagation();
+ onDismiss(task.id);
+ }}
+ className="px-2 py-1 font-mono text-[10px] text-[#8b949e] hover:bg-[rgba(117,170,252,0.1)] border border-[rgba(117,170,252,0.25)] hover:border-[#3f6fb3] transition-colors uppercase"
+ >
+ Dismiss
+ </button>
+ )}
+ {!task.isSupervisor && (
+ <button
+ onClick={(e) => {
+ e.stopPropagation();
+ onDelete(task.id);
+ }}
+ className="px-2 py-1 font-mono text-[10px] text-red-400 hover:bg-red-400/10 border border-red-400/30 hover:border-red-400/50 transition-colors uppercase"
+ >
+ Delete
+ </button>
+ )}
+ </div>
</div>
</div>
))}
diff --git a/makima/frontend/src/hooks/useTasks.ts b/makima/frontend/src/hooks/useTasks.ts
index 6e6c992..4667c4c 100644
--- a/makima/frontend/src/hooks/useTasks.ts
+++ b/makima/frontend/src/hooks/useTasks.ts
@@ -5,6 +5,7 @@ import {
createTask,
updateTask,
deleteTask,
+ dismissTask,
VersionConflictError,
type TaskSummary,
type TaskWithSubtasks,
@@ -110,6 +111,21 @@ export function useTasks() {
[fetchTasks]
);
+ const hideTask = useCallback(
+ async (id: string): Promise<boolean> => {
+ setError(null);
+ try {
+ await dismissTask(id);
+ await fetchTasks(); // Refresh list
+ return true;
+ } catch (e) {
+ setError(e instanceof Error ? e.message : "Failed to dismiss task");
+ return false;
+ }
+ },
+ [fetchTasks]
+ );
+
// Initial fetch
useEffect(() => {
fetchTasks();
@@ -126,5 +142,6 @@ export function useTasks() {
saveTask,
editTask,
removeTask,
+ hideTask,
};
}
diff --git a/makima/frontend/src/lib/api.ts b/makima/frontend/src/lib/api.ts
index 86ff06c..b42a19f 100644
--- a/makima/frontend/src/lib/api.ts
+++ b/makima/frontend/src/lib/api.ts
@@ -543,6 +543,8 @@ export interface TaskSummary {
subtaskCount: number;
/** Whether this is a supervisor task (contract orchestrator) */
isSupervisor: boolean;
+ /** Whether this task is hidden from the UI (user dismissed it) */
+ hidden: boolean;
version: number;
createdAt: string;
updatedAt: string;
@@ -639,6 +641,8 @@ export interface UpdateTaskRequest {
targetRepoPath?: string;
/** Action on completion: "none", "branch", "merge", "pr" */
completionAction?: CompletionAction;
+ /** Whether this task is hidden from the UI (user dismissed it) */
+ hidden?: boolean;
version?: number;
}
@@ -2631,3 +2635,15 @@ export function getSupervisorStatus(
canResume,
};
}
+
+// =============================================================================
+// Task Dismiss (Hide) Functions
+// =============================================================================
+
+/**
+ * Dismiss (hide) a completed standalone task from the UI.
+ * This marks the task as hidden so it won't appear in the task list.
+ */
+export async function dismissTask(taskId: string): Promise<Task> {
+ return updateTask(taskId, { hidden: true });
+}
diff --git a/makima/frontend/src/routes/mesh.tsx b/makima/frontend/src/routes/mesh.tsx
index 453bdff..d3ca84e 100644
--- a/makima/frontend/src/routes/mesh.tsx
+++ b/makima/frontend/src/routes/mesh.tsx
@@ -81,7 +81,7 @@ export default function MeshPage() {
const { id } = useParams<{ id: string }>();
const navigate = useNavigate();
const { isAuthenticated, isAuthConfigured, isLoading: authLoading } = useAuth();
- const { tasks, loading, error, conflict, clearConflict, fetchTask, fetchTasks, editTask, removeTask, saveTask } = useTasks();
+ const { tasks, loading, error, conflict, clearConflict, fetchTask, fetchTasks, editTask, removeTask, hideTask, saveTask } = useTasks();
const { pendingQuestions, submitAnswer } = useSupervisorQuestions();
// Memoize pending question IDs for efficient lookup
@@ -373,6 +373,13 @@ export default function MeshPage() {
[removeTask, id, taskDetail, navigate]
);
+ const handleDismiss = useCallback(
+ async (taskId: string) => {
+ await hideTask(taskId);
+ },
+ [hideTask]
+ );
+
const handleStart = useCallback(
async (taskId: string) => {
try {
@@ -830,6 +837,7 @@ export default function MeshPage() {
loading={loading || creating}
onSelect={handleSelectTask}
onDelete={handleDelete}
+ onDismiss={handleDismiss}
onCreate={handleCreate}
/>
</div>
diff --git a/makima/migrations/20250122000000_add_task_hidden.sql b/makima/migrations/20250122000000_add_task_hidden.sql
new file mode 100644
index 0000000..42eed52
--- /dev/null
+++ b/makima/migrations/20250122000000_add_task_hidden.sql
@@ -0,0 +1,5 @@
+-- Add hidden column to tasks table for dismissing completed standalone tasks
+ALTER TABLE tasks ADD COLUMN hidden BOOLEAN NOT NULL DEFAULT FALSE;
+
+-- Create index for filtering hidden tasks efficiently
+CREATE INDEX IF NOT EXISTS idx_tasks_hidden ON tasks(hidden) WHERE hidden = false;
diff --git a/makima/src/db/models.rs b/makima/src/db/models.rs
index bf95a3a..6ede268 100644
--- a/makima/src/db/models.rs
+++ b/makima/src/db/models.rs
@@ -525,6 +525,12 @@ pub struct Task {
/// Used to track the origin of "what if" explorations.
#[serde(skip_serializing_if = "Option::is_none")]
pub branched_from_task_id: Option<Uuid>,
+
+ // UI visibility
+ /// Whether this task is hidden from the UI (user dismissed it).
+ /// Standalone completed tasks can be dismissed by the user.
+ #[serde(default)]
+ pub hidden: bool,
}
impl Task {
@@ -564,6 +570,9 @@ pub struct TaskSummary {
/// True for contract supervisor tasks
#[serde(default)]
pub is_supervisor: bool,
+ /// Whether this task is hidden from the UI (user dismissed it)
+ #[serde(default)]
+ pub hidden: bool,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
}
@@ -586,6 +595,7 @@ impl From<Task> for TaskSummary {
subtask_count: 0, // Would need separate query
version: task.version,
is_supervisor: task.is_supervisor,
+ hidden: task.hidden,
created_at: task.created_at,
updated_at: task.updated_at,
}
@@ -670,6 +680,8 @@ pub struct UpdateTaskRequest {
/// Explicitly clear daemon_id (set to NULL)
#[serde(default)]
pub clear_daemon_id: bool,
+ /// Whether this task is hidden from the UI (user dismissed it)
+ pub hidden: Option<bool>,
/// Version for optimistic locking
pub version: Option<i32>,
}
diff --git a/makima/src/db/repository.rs b/makima/src/db/repository.rs
index 7387735..84afc8d 100644
--- a/makima/src/db/repository.rs
+++ b/makima/src/db/repository.rs
@@ -733,6 +733,7 @@ pub async fn get_task(pool: &PgPool, id: Uuid) -> Result<Option<Task>, sqlx::Err
}
/// List all top-level tasks (no parent), ordered by created_at DESC.
+/// Hidden tasks are excluded by default.
pub async fn list_tasks(pool: &PgPool) -> Result<Vec<TaskSummary>, sqlx::Error> {
sqlx::query_as::<_, TaskSummary>(
r#"
@@ -742,10 +743,10 @@ pub async fn list_tasks(pool: &PgPool) -> Result<Vec<TaskSummary>, sqlx::Error>
t.parent_task_id, t.depth, t.name, t.status, t.priority,
t.progress_summary,
(SELECT COUNT(*) FROM tasks WHERE parent_task_id = t.id) as subtask_count,
- t.version, t.is_supervisor, t.created_at, t.updated_at
+ t.version, t.is_supervisor, COALESCE(t.hidden, false) as hidden, t.created_at, t.updated_at
FROM tasks t
LEFT JOIN contracts c ON t.contract_id = c.id
- WHERE t.parent_task_id IS NULL
+ WHERE t.parent_task_id IS NULL AND COALESCE(t.hidden, false) = false
ORDER BY t.priority DESC, t.created_at DESC
"#,
)
@@ -763,7 +764,7 @@ pub async fn list_subtasks(pool: &PgPool, parent_id: Uuid) -> Result<Vec<TaskSum
t.parent_task_id, t.depth, t.name, t.status, t.priority,
t.progress_summary,
(SELECT COUNT(*) FROM tasks WHERE parent_task_id = t.id) as subtask_count,
- t.version, t.is_supervisor, t.created_at, t.updated_at
+ t.version, t.is_supervisor, COALESCE(t.hidden, false) as hidden, t.created_at, t.updated_at
FROM tasks t
LEFT JOIN contracts c ON t.contract_id = c.id
WHERE t.parent_task_id = $1
@@ -1129,6 +1130,7 @@ pub async fn get_task_for_owner(
}
/// List all top-level tasks (no parent) for an owner, ordered by created_at DESC.
+/// Hidden tasks are excluded by default.
pub async fn list_tasks_for_owner(
pool: &PgPool,
owner_id: Uuid,
@@ -1141,10 +1143,10 @@ pub async fn list_tasks_for_owner(
t.parent_task_id, t.depth, t.name, t.status, t.priority,
t.progress_summary,
(SELECT COUNT(*) FROM tasks WHERE parent_task_id = t.id) as subtask_count,
- t.version, t.is_supervisor, t.created_at, t.updated_at
+ t.version, t.is_supervisor, COALESCE(t.hidden, false) as hidden, t.created_at, t.updated_at
FROM tasks t
LEFT JOIN contracts c ON t.contract_id = c.id
- WHERE t.owner_id = $1 AND t.parent_task_id IS NULL
+ WHERE t.owner_id = $1 AND t.parent_task_id IS NULL AND COALESCE(t.hidden, false) = false
ORDER BY t.priority DESC, t.created_at DESC
"#,
)
@@ -1167,7 +1169,7 @@ pub async fn list_subtasks_for_owner(
t.parent_task_id, t.depth, t.name, t.status, t.priority,
t.progress_summary,
(SELECT COUNT(*) FROM tasks WHERE parent_task_id = t.id) as subtask_count,
- t.version, t.is_supervisor, t.created_at, t.updated_at
+ t.version, t.is_supervisor, COALESCE(t.hidden, false) as hidden, t.created_at, t.updated_at
FROM tasks t
LEFT JOIN contracts c ON t.contract_id = c.id
WHERE t.owner_id = $1 AND t.parent_task_id = $2
@@ -1217,6 +1219,7 @@ pub async fn update_task_for_owner(
let repository_url = req.repository_url.or(existing.repository_url);
let target_repo_path = req.target_repo_path.or(existing.target_repo_path);
let completion_action = req.completion_action.or(existing.completion_action);
+ let hidden = req.hidden.unwrap_or(existing.hidden);
let daemon_id = if req.clear_daemon_id {
None
} else {
@@ -1232,8 +1235,8 @@ pub async fn update_task_for_owner(
progress_summary = $8, last_output = $9, error_message = $10,
merge_mode = $11, pr_url = $12, daemon_id = $13,
target_repo_path = $14, completion_action = $15, repository_url = $16,
- updated_at = NOW()
- WHERE id = $1 AND owner_id = $2 AND version = $17
+ hidden = $17, updated_at = NOW()
+ WHERE id = $1 AND owner_id = $2 AND version = $18
RETURNING *
"#,
)
@@ -1253,6 +1256,7 @@ pub async fn update_task_for_owner(
.bind(&target_repo_path)
.bind(&completion_action)
.bind(&repository_url)
+ .bind(hidden)
.bind(req.version.unwrap())
.fetch_optional(pool)
.await?
@@ -1264,7 +1268,7 @@ pub async fn update_task_for_owner(
progress_summary = $8, last_output = $9, error_message = $10,
merge_mode = $11, pr_url = $12, daemon_id = $13,
target_repo_path = $14, completion_action = $15, repository_url = $16,
- updated_at = NOW()
+ hidden = $17, updated_at = NOW()
WHERE id = $1 AND owner_id = $2
RETURNING *
"#,
@@ -1285,6 +1289,7 @@ pub async fn update_task_for_owner(
.bind(&target_repo_path)
.bind(&completion_action)
.bind(&repository_url)
+ .bind(hidden)
.fetch_optional(pool)
.await?
};
@@ -2685,7 +2690,7 @@ pub async fn list_tasks_in_contract(
t.parent_task_id, t.depth, t.name, t.status, t.priority,
t.progress_summary,
(SELECT COUNT(*) FROM tasks WHERE parent_task_id = t.id) as subtask_count,
- t.version, t.is_supervisor, t.created_at, t.updated_at
+ t.version, t.is_supervisor, COALESCE(t.hidden, false) as hidden, t.created_at, t.updated_at
FROM tasks t
LEFT JOIN contracts c ON t.contract_id = c.id
WHERE t.contract_id = $1 AND t.owner_id = $2