Add task source UI and agent launch flow#3279
Conversation
📝 WalkthroughWalkthroughAdds a task-sources board and refinement-to-personal flow in the Intelligence Tasks UI, introduces TaskSource controls and Notion DB browsing, extends tests and i18n, defines TASK_SOURCES_THREAD_ID, and enhances providers with TaskKind/TaskContainer plus a local gh/REST GitHub fetch mode and related tests. ChangesTasks UI: task sources, refinement, and work execution
GitHub provider: local search via gh/REST and task-sources backend
Sequence Diagram(s)sequenceDiagram
participant User
participant Kanban as TaskKanbanBoard
participant Tab as IntelligenceTasksTab
participant Threads as threadApi
participant Todos as todosApi
participant Chat as chatService
participant Router as navigate('/chat')
User->>Kanban: Click "Work task" / approve source plan
Kanban->>Tab: onWorkTask(card) / open refinement dialog
Tab->>Threads: createNewThread(label, profileId)
Threads-->>Tab: threadId
Tab->>Threads: updateTitle(threadId, derivedTitle)
Tab->>Threads: appendMessage(threadId, constructedPrompt)
Tab->>Todos: updateStatus(cardId, in_progress) / add/edit personal task
Tab->>Router: navigate('/chat')
Tab->>Chat: chatSend(threadId, constructedPrompt)
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related issues
Possibly related PRs
Suggested reviewers
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. Comment |
There was a problem hiding this comment.
Actionable comments posted: 6
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/openhuman/memory_sync/composio/providers/github/provider.rs (1)
416-468:⚠️ Potential issue | 🟠 Major | ⚡ Quick winKeep the Composio-backed search path as the primary fetch.
Line 430 now bypasses
ctx.execute(ACTION_SEARCH_ISSUES, ...)entirely and always depends on localgh/ env-token auth. That regresses users who connected GitHub only through the app, and it skips the provider-backedfetch_taskspath already exercised intests/memory_sync_sources_raw_coverage_e2e.rs:488-552. Try the provider action first, then fall back to localgh/REST only when the connection is absent or unavailable.🔧 Suggested direction
- let data = fetch_github_tasks_local(&query, max, &filter.extra).await?; + let mut args = json!({ + "q": query, + "sort": "updated", + "order": "desc", + "per_page": max.min(100) as u32, + "page": 1, + }); + merge_extra(&mut args, &filter.extra); + + let data = match ctx.execute(ACTION_SEARCH_ISSUES, Some(args.clone())).await { + Ok(resp) if resp.successful => resp.data, + Ok(resp) => { + tracing::debug!( + error = ?resp.error, + "[task_sources:github] provider search unavailable, falling back to local" + ); + fetch_github_tasks_local(&query, max, &filter.extra).await? + } + Err(err) => { + tracing::debug!( + error = %err, + "[task_sources:github] provider search failed, falling back to local" + ); + fetch_github_tasks_local(&query, max, &filter.extra).await? + } + };🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/openhuman/memory_sync/composio/providers/github/provider.rs` around lines 416 - 468, fetch_tasks currently always calls fetch_github_tasks_local which forces local gh/env-token auth and skips the provider-backed search; change fetch_tasks to first attempt the provider action via ctx.execute(ACTION_SEARCH_ISSUES, ...) and use its returned Value when successful, and only if that call fails (or the connection/action is absent/unavailable) fall back to calling fetch_github_tasks_local(query, max, &filter.extra); ensure error paths from ctx.execute are handled analogous to the existing gh/rest fallback so you still try gh_search_issues/rest_search_issues when provider search isn't available.
🧹 Nitpick comments (1)
app/src/pages/conversations/components/TaskKanbanBoard.tsx (1)
219-635: 🏗️ Heavy liftSplit the new source/task UI out of
TaskKanbanBoard.tsx.This file is already far beyond the repo's size target, and this PR adds multiple new UI units plus parsing helpers here. Extracting
TaskBoardArticle,TaskSourceControls,SourceBrief, and the source-metadata helpers into sibling modules would keep the board component focused and make the new flows easier to test in isolation. As per coding guidelines,app/src/**/*.{ts,tsx}: File size: prefer ≤ ~500 lines; split growing modules.Also applies to: 896-960
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@app/src/pages/conversations/components/TaskKanbanBoard.tsx` around lines 219 - 635, The file is too large; split out the new source/task UI and helpers by extracting TaskBoardArticle, TaskSourceControls, SourceBrief (if present or planned), and the metadata helpers (readSourceMetadata, readString, readNumber, providerLabel, sourceBadgeLabel, formatUrgency, formatFetchNotice) into their own sibling modules (e.g., TaskBoardArticle.tsx, TaskSourceControls.tsx, SourceBrief.tsx, taskSourceUtils.ts) and export/import them from TaskKanbanBoard; ensure props and types are preserved, move any related useT/useState/useEffect imports into the new files, update imports where these symbols are used in TaskKanbanBoard, and add tests or stories targeting the extracted components to keep behavior identical.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@app/src/components/intelligence/IntelligenceTasksTab.tsx`:
- Around line 404-414: The refinement dialog is being closed
(setRefiningCard(null)) immediately after setPersonalBoard succeeds, before the
call to todosApi.updateStatus(TASK_SOURCES_THREAD_ID, sourceCard.id, 'done')
completes; move the dialog-close logic so it runs only after the updateStatus
call succeeds and after setTaskSourcesBoard(sourceSaved) (use mountedRef.current
checks as currently done), and on update failure leave the dialog open and/or
revert the personal-board change (undo setPersonalBoard(saved) or surface an
error) so you don't create duplicate tasks; update the block around
setPersonalBoard, setRefiningCard, todosApi.updateStatus, and
setTaskSourcesBoard accordingly.
- Around line 709-753: The Work on task button is still active for cards marked
done, allowing duplicates; update the button's disabled logic to also check the
local done flag (set disabled={disabled || done}) and ensure the onClick handler
(onWorkOnTask(card)) is not callable when done is true; locate the done variable
and the button element in IntelligenceTasksTab (the button using onWorkOnTask
and disabled) and apply this change so already-queued source cards cannot be
acted on.
In `@app/src/pages/conversations/components/TaskKanbanBoard.tsx`:
- Around line 19-32: This file re-declares the reserved thread id literal
'task-sources' as TASK_SOURCES_THREAD_ID instead of importing the canonical
constant exported by todosApi; remove the local const TASK_SOURCES_THREAD_ID and
import TASK_SOURCES_THREAD_ID from the module that now exports it (use the
existing import block from '../../../utils/tauriCommands' or add the import from
the module that provides todosApi's export), then update any references in
TaskKanbanBoard.tsx to use the imported TASK_SOURCES_THREAD_ID symbol.
- Around line 268-279: The "Work task" button currently shows for any card where
card.status !== 'done'; change that to only show for explicit startable statuses
to avoid duplicate/invalid launches. In TaskKanbanBoard, replace the condition
around the onWorkTask button with a check against an allowlist (e.g.
isStartableStatus(card.status) or ['todo','ready'] includes card.status) or add
a small helper function isStartableStatus(status) and use it in the render
condition; keep the onClick handler (onWorkTask(card)), disabled/working props
and labels unchanged. This ensures the button only appears for tasks that can
actually be started and prevents starting tasks in in_progress, blocked, or
rejected states.
In `@src/openhuman/memory_sync/composio/providers/github/provider.rs`:
- Around line 672-694: normalize_github_repo_filter currently misses inputs that
start with "github.com/" (no scheme) and doesn't strip URL query/fragment or
trailing path components before collapsing to owner/repo, causing inputs like
"github.com/owner/repo" or "https://github.com/owner/repo?tab=issues" to be
normalized incorrectly; update normalize_github_repo_filter to first strip any
leading "github.com/" prefix in addition to https/http/git@ schemes, remove URL
query strings and fragments (anything after '?' or '#'), trim trailing slashes
and ".git", then split on '/' and return format!("{owner}/{repo}") only when
both are non-empty (otherwise return trimmed input), ensuring references to
normalize_github_repo_filter, owner, repo, and cleaned variables in the existing
function are used to locate the change.
---
Outside diff comments:
In `@src/openhuman/memory_sync/composio/providers/github/provider.rs`:
- Around line 416-468: fetch_tasks currently always calls
fetch_github_tasks_local which forces local gh/env-token auth and skips the
provider-backed search; change fetch_tasks to first attempt the provider action
via ctx.execute(ACTION_SEARCH_ISSUES, ...) and use its returned Value when
successful, and only if that call fails (or the connection/action is
absent/unavailable) fall back to calling fetch_github_tasks_local(query, max,
&filter.extra); ensure error paths from ctx.execute are handled analogous to the
existing gh/rest fallback so you still try gh_search_issues/rest_search_issues
when provider search isn't available.
---
Nitpick comments:
In `@app/src/pages/conversations/components/TaskKanbanBoard.tsx`:
- Around line 219-635: The file is too large; split out the new source/task UI
and helpers by extracting TaskBoardArticle, TaskSourceControls, SourceBrief (if
present or planned), and the metadata helpers (readSourceMetadata, readString,
readNumber, providerLabel, sourceBadgeLabel, formatUrgency, formatFetchNotice)
into their own sibling modules (e.g., TaskBoardArticle.tsx,
TaskSourceControls.tsx, SourceBrief.tsx, taskSourceUtils.ts) and export/import
them from TaskKanbanBoard; ensure props and types are preserved, move any
related useT/useState/useEffect imports into the new files, update imports where
these symbols are used in TaskKanbanBoard, and add tests or stories targeting
the extracted components to keep behavior identical.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 7312c124-7d7e-440a-acb7-39f01f84a910
📒 Files selected for processing (21)
app/src/components/intelligence/IntelligenceTasksTab.tsxapp/src/components/intelligence/__tests__/IntelligenceTasksTab.test.tsxapp/src/lib/i18n/ar.tsapp/src/lib/i18n/bn.tsapp/src/lib/i18n/de.tsapp/src/lib/i18n/en.tsapp/src/lib/i18n/es.tsapp/src/lib/i18n/fr.tsapp/src/lib/i18n/hi.tsapp/src/lib/i18n/id.tsapp/src/lib/i18n/it.tsapp/src/lib/i18n/ko.tsapp/src/lib/i18n/pl.tsapp/src/lib/i18n/pt.tsapp/src/lib/i18n/ru.tsapp/src/lib/i18n/zh-CN.tsapp/src/pages/conversations/components/TaskKanbanBoard.test.tsxapp/src/pages/conversations/components/TaskKanbanBoard.tsxapp/src/services/api/todosApi.tssrc/openhuman/memory_sync/composio/providers/github/provider.rssrc/openhuman/memory_sync/composio/providers/github/tests.rs
| const source = readSourceMetadata(card.sourceMetadata); | ||
| const done = card.status === 'done'; | ||
| return ( | ||
| <li key={card.id} className="p-3"> | ||
| <div className="flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between"> | ||
| <div className="min-w-0 space-y-1"> | ||
| <div className="flex flex-wrap items-center gap-1.5"> | ||
| <span className="rounded-md bg-sky-50 px-1.5 py-0.5 text-[10px] font-medium text-sky-700 dark:bg-sky-500/10 dark:text-sky-200"> | ||
| {taskSourceLabel(card, t)} | ||
| </span> | ||
| {done && ( | ||
| <span className="inline-flex items-center gap-1 rounded-md bg-sage-50 px-1.5 py-0.5 text-[10px] font-medium text-sage-700 dark:bg-sage-500/10 dark:text-sage-200"> | ||
| <LuCheck className="h-3 w-3" /> | ||
| {t('intelligence.tasks.sourceList.queued')} | ||
| </span> | ||
| )} | ||
| </div> | ||
| <p className="break-words text-sm font-medium leading-snug text-stone-800 dark:text-neutral-100"> | ||
| {card.title} | ||
| </p> | ||
| {(card.objective || card.notes) && ( | ||
| <p className="line-clamp-2 break-words text-xs leading-snug text-stone-500 dark:text-neutral-400"> | ||
| {card.objective || card.notes} | ||
| </p> | ||
| )} | ||
| </div> | ||
| <div className="flex flex-none items-center gap-2"> | ||
| {source.url && ( | ||
| <a | ||
| href={source.url} | ||
| target="_blank" | ||
| rel="noreferrer" | ||
| title={t('conversations.taskKanban.source.openExternal')} | ||
| className="flex h-8 w-8 items-center justify-center rounded-md border border-stone-200 text-stone-500 hover:bg-stone-50 dark:border-neutral-800 dark:text-neutral-300 dark:hover:bg-neutral-800"> | ||
| <LuExternalLink className="h-4 w-4" /> | ||
| </a> | ||
| )} | ||
| <button | ||
| type="button" | ||
| disabled={disabled} | ||
| onClick={() => onWorkOnTask(card)} | ||
| className="inline-flex items-center gap-1.5 rounded-md bg-ocean-600 px-3 py-1.5 text-xs font-medium text-white hover:bg-ocean-700 disabled:opacity-40"> | ||
| <LuSparkles className="h-3.5 w-3.5" /> | ||
| {t('intelligence.tasks.sourceList.workOnTask')} | ||
| </button> |
There was a problem hiding this comment.
Disable the action for already-queued source cards.
A done source card only gets a different badge, but the Work on task button stays enabled. That lets the same source item go back through the approval flow and create duplicate personal tasks.
Suggested fix
- <button
+ <button
type="button"
- disabled={disabled}
+ disabled={disabled || done}
onClick={() => onWorkOnTask(card)}
className="inline-flex items-center gap-1.5 rounded-md bg-ocean-600 px-3 py-1.5 text-xs font-medium text-white hover:bg-ocean-700 disabled:opacity-40">📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const source = readSourceMetadata(card.sourceMetadata); | |
| const done = card.status === 'done'; | |
| return ( | |
| <li key={card.id} className="p-3"> | |
| <div className="flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between"> | |
| <div className="min-w-0 space-y-1"> | |
| <div className="flex flex-wrap items-center gap-1.5"> | |
| <span className="rounded-md bg-sky-50 px-1.5 py-0.5 text-[10px] font-medium text-sky-700 dark:bg-sky-500/10 dark:text-sky-200"> | |
| {taskSourceLabel(card, t)} | |
| </span> | |
| {done && ( | |
| <span className="inline-flex items-center gap-1 rounded-md bg-sage-50 px-1.5 py-0.5 text-[10px] font-medium text-sage-700 dark:bg-sage-500/10 dark:text-sage-200"> | |
| <LuCheck className="h-3 w-3" /> | |
| {t('intelligence.tasks.sourceList.queued')} | |
| </span> | |
| )} | |
| </div> | |
| <p className="break-words text-sm font-medium leading-snug text-stone-800 dark:text-neutral-100"> | |
| {card.title} | |
| </p> | |
| {(card.objective || card.notes) && ( | |
| <p className="line-clamp-2 break-words text-xs leading-snug text-stone-500 dark:text-neutral-400"> | |
| {card.objective || card.notes} | |
| </p> | |
| )} | |
| </div> | |
| <div className="flex flex-none items-center gap-2"> | |
| {source.url && ( | |
| <a | |
| href={source.url} | |
| target="_blank" | |
| rel="noreferrer" | |
| title={t('conversations.taskKanban.source.openExternal')} | |
| className="flex h-8 w-8 items-center justify-center rounded-md border border-stone-200 text-stone-500 hover:bg-stone-50 dark:border-neutral-800 dark:text-neutral-300 dark:hover:bg-neutral-800"> | |
| <LuExternalLink className="h-4 w-4" /> | |
| </a> | |
| )} | |
| <button | |
| type="button" | |
| disabled={disabled} | |
| onClick={() => onWorkOnTask(card)} | |
| className="inline-flex items-center gap-1.5 rounded-md bg-ocean-600 px-3 py-1.5 text-xs font-medium text-white hover:bg-ocean-700 disabled:opacity-40"> | |
| <LuSparkles className="h-3.5 w-3.5" /> | |
| {t('intelligence.tasks.sourceList.workOnTask')} | |
| </button> | |
| const source = readSourceMetadata(card.sourceMetadata); | |
| const done = card.status === 'done'; | |
| return ( | |
| <li key={card.id} className="p-3"> | |
| <div className="flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between"> | |
| <div className="min-w-0 space-y-1"> | |
| <div className="flex flex-wrap items-center gap-1.5"> | |
| <span className="rounded-md bg-sky-50 px-1.5 py-0.5 text-[10px] font-medium text-sky-700 dark:bg-sky-500/10 dark:text-sky-200"> | |
| {taskSourceLabel(card, t)} | |
| </span> | |
| {done && ( | |
| <span className="inline-flex items-center gap-1 rounded-md bg-sage-50 px-1.5 py-0.5 text-[10px] font-medium text-sage-700 dark:bg-sage-500/10 dark:text-sage-200"> | |
| <LuCheck className="h-3 w-3" /> | |
| {t('intelligence.tasks.sourceList.queued')} | |
| </span> | |
| )} | |
| </div> | |
| <p className="break-words text-sm font-medium leading-snug text-stone-800 dark:text-neutral-100"> | |
| {card.title} | |
| </p> | |
| {(card.objective || card.notes) && ( | |
| <p className="line-clamp-2 break-words text-xs leading-snug text-stone-500 dark:text-neutral-400"> | |
| {card.objective || card.notes} | |
| </p> | |
| )} | |
| </div> | |
| <div className="flex flex-none items-center gap-2"> | |
| {source.url && ( | |
| <a | |
| href={source.url} | |
| target="_blank" | |
| rel="noreferrer" | |
| title={t('conversations.taskKanban.source.openExternal')} | |
| className="flex h-8 w-8 items-center justify-center rounded-md border border-stone-200 text-stone-500 hover:bg-stone-50 dark:border-neutral-800 dark:text-neutral-300 dark:hover:bg-neutral-800"> | |
| <LuExternalLink className="h-4 w-4" /> | |
| </a> | |
| )} | |
| <button | |
| type="button" | |
| disabled={disabled || done} | |
| onClick={() => onWorkOnTask(card)} | |
| className="inline-flex items-center gap-1.5 rounded-md bg-ocean-600 px-3 py-1.5 text-xs font-medium text-white hover:bg-ocean-700 disabled:opacity-40"> | |
| <LuSparkles className="h-3.5 w-3.5" /> | |
| {t('intelligence.tasks.sourceList.workOnTask')} | |
| </button> |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@app/src/components/intelligence/IntelligenceTasksTab.tsx` around lines 709 -
753, The Work on task button is still active for cards marked done, allowing
duplicates; update the button's disabled logic to also check the local done flag
(set disabled={disabled || done}) and ensure the onClick handler
(onWorkOnTask(card)) is not callable when done is true; locate the done variable
and the button element in IntelligenceTasksTab (the button using onWorkOnTask
and disabled) and apply this change so already-queued source cards cannot be
acted on.
| import { | ||
| type FetchOutcome, | ||
| isTauri, | ||
| openhumanTaskSourcesFetch, | ||
| openhumanTaskSourcesList, | ||
| openhumanTaskSourcesStatus, | ||
| openhumanTaskSourcesUpdate, | ||
| type TaskSource, | ||
| type TaskSourcesStatus, | ||
| } from '../../../utils/tauriCommands'; | ||
|
|
||
| type ColumnDef = { status: TaskBoardCardStatus; labelKey: string }; | ||
|
|
||
| const TASK_SOURCES_THREAD_ID = 'task-sources'; |
There was a problem hiding this comment.
🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win
Import the reserved thread id instead of re-declaring it.
todosApi.ts now exports TASK_SOURCES_THREAD_ID, but this file still hard-codes the same literal locally. That makes the task-sources routing contract drift-prone if the sentinel ever changes in one place only.
♻️ Proposed fix
+import { TASK_SOURCES_THREAD_ID } from '../../../services/api/todosApi';
import {
type FetchOutcome,
isTauri,
openhumanTaskSourcesFetch,
openhumanTaskSourcesList,
openhumanTaskSourcesStatus,
openhumanTaskSourcesUpdate,
type TaskSource,
type TaskSourcesStatus,
} from '../../../utils/tauriCommands';
-
-const TASK_SOURCES_THREAD_ID = 'task-sources';📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| import { | |
| type FetchOutcome, | |
| isTauri, | |
| openhumanTaskSourcesFetch, | |
| openhumanTaskSourcesList, | |
| openhumanTaskSourcesStatus, | |
| openhumanTaskSourcesUpdate, | |
| type TaskSource, | |
| type TaskSourcesStatus, | |
| } from '../../../utils/tauriCommands'; | |
| type ColumnDef = { status: TaskBoardCardStatus; labelKey: string }; | |
| const TASK_SOURCES_THREAD_ID = 'task-sources'; | |
| import { TASK_SOURCES_THREAD_ID } from '../../../services/api/todosApi'; | |
| import { | |
| type FetchOutcome, | |
| isTauri, | |
| openhumanTaskSourcesFetch, | |
| openhumanTaskSourcesList, | |
| openhumanTaskSourcesStatus, | |
| openhumanTaskSourcesUpdate, | |
| type TaskSource, | |
| type TaskSourcesStatus, | |
| } from '../../../utils/tauriCommands'; | |
| type ColumnDef = { status: TaskBoardCardStatus; labelKey: string }; |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@app/src/pages/conversations/components/TaskKanbanBoard.tsx` around lines 19 -
32, This file re-declares the reserved thread id literal 'task-sources' as
TASK_SOURCES_THREAD_ID instead of importing the canonical constant exported by
todosApi; remove the local const TASK_SOURCES_THREAD_ID and import
TASK_SOURCES_THREAD_ID from the module that now exports it (use the existing
import block from '../../../utils/tauriCommands' or add the import from the
module that provides todosApi's export), then update any references in
TaskKanbanBoard.tsx to use the imported TASK_SOURCES_THREAD_ID symbol.
| ) : onWorkTask && card.status !== 'done' ? ( | ||
| <button | ||
| type="button" | ||
| title={t('conversations.taskKanban.workTask')} | ||
| disabled={disabled || working} | ||
| onClick={() => onWorkTask(card)} | ||
| className="inline-flex flex-shrink-0 items-center gap-1 rounded-md bg-ocean-600 px-1.5 py-0.5 text-[10px] font-medium text-white transition-colors hover:bg-ocean-700 disabled:opacity-40"> | ||
| <LuPlay className="h-3 w-3" /> | ||
| {working | ||
| ? t('conversations.taskKanban.startingTask') | ||
| : t('conversations.taskKanban.workTask')} | ||
| </button> |
There was a problem hiding this comment.
Restrict Work task to startable statuses only.
This action creates a new work thread and starts chat execution, so card.status !== 'done' still exposes it on in_progress, blocked, and rejected cards. That makes duplicate or invalid launches easy after a task has already started or failed.
🐛 Proposed fix
+ const canStartWork = card.status === 'todo' || card.status === 'ready';
+
return (
<article className="rounded-lg border border-stone-200 dark:border-neutral-800 bg-white dark:bg-neutral-900 px-2.5 py-2 shadow-sm">
@@
- ) : onWorkTask && card.status !== 'done' ? (
+ ) : onWorkTask && canStartWork ? (
<button
type="button"
title={t('conversations.taskKanban.workTask')}🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@app/src/pages/conversations/components/TaskKanbanBoard.tsx` around lines 268
- 279, The "Work task" button currently shows for any card where card.status !==
'done'; change that to only show for explicit startable statuses to avoid
duplicate/invalid launches. In TaskKanbanBoard, replace the condition around the
onWorkTask button with a check against an allowlist (e.g.
isStartableStatus(card.status) or ['todo','ready'] includes card.status) or add
a small helper function isStartableStatus(status) and use it in the render
condition; keep the onClick handler (onWorkTask(card)), disabled/working props
and labels unchanged. This ensures the button only appears for tasks that can
actually be started and prevents starting tasks in in_progress, blocked, or
rejected states.
| pub(super) fn normalize_github_repo_filter(raw: &str) -> String { | ||
| let trimmed = raw.trim(); | ||
| let without_scheme = trimmed | ||
| .strip_prefix("https://github.com/") | ||
| .or_else(|| trimmed.strip_prefix("http://github.com/")) | ||
| .or_else(|| trimmed.strip_prefix("git@github.com:")) | ||
| .unwrap_or(trimmed); | ||
| let cleaned = without_scheme | ||
| .trim_start_matches('/') | ||
| .trim_end_matches('/') | ||
| .trim_end_matches(".git"); | ||
| let mut parts = cleaned.split('/').filter(|part| !part.is_empty()); | ||
| match (parts.next(), parts.next()) { | ||
| (Some(owner), Some(repo)) => { | ||
| let repo = repo.trim_end_matches(".git"); | ||
| if owner.is_empty() || repo.is_empty() { | ||
| trimmed.to_string() | ||
| } else { | ||
| format!("{owner}/{repo}") | ||
| } | ||
| } | ||
| _ => trimmed.to_string(), | ||
| } |
There was a problem hiding this comment.
Handle bare github.com/... and URL suffixes before collapsing to owner/repo.
A common pasted filter like github.com/tinyhumansai/openhuman currently normalizes to github.com/tinyhumansai, and https://github.com/tinyhumansai/openhuman?tab=issues keeps the ?tab=... in the repo segment. Since build_fetch_query() feeds this straight into repo:{...}, those inputs silently return no tasks.
🧹 Proposed fix
pub(super) fn normalize_github_repo_filter(raw: &str) -> String {
let trimmed = raw.trim();
let without_scheme = trimmed
.strip_prefix("https://github.com/")
.or_else(|| trimmed.strip_prefix("http://github.com/"))
+ .or_else(|| trimmed.strip_prefix("github.com/"))
+ .or_else(|| trimmed.strip_prefix("www.github.com/"))
.or_else(|| trimmed.strip_prefix("git@github.com:"))
.unwrap_or(trimmed);
- let cleaned = without_scheme
+ let without_suffix = without_scheme
+ .split(['?', '#'])
+ .next()
+ .unwrap_or(without_scheme);
+ let cleaned = without_suffix
.trim_start_matches('/')
.trim_end_matches('/')
.trim_end_matches(".git");📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| pub(super) fn normalize_github_repo_filter(raw: &str) -> String { | |
| let trimmed = raw.trim(); | |
| let without_scheme = trimmed | |
| .strip_prefix("https://github.com/") | |
| .or_else(|| trimmed.strip_prefix("http://github.com/")) | |
| .or_else(|| trimmed.strip_prefix("git@github.com:")) | |
| .unwrap_or(trimmed); | |
| let cleaned = without_scheme | |
| .trim_start_matches('/') | |
| .trim_end_matches('/') | |
| .trim_end_matches(".git"); | |
| let mut parts = cleaned.split('/').filter(|part| !part.is_empty()); | |
| match (parts.next(), parts.next()) { | |
| (Some(owner), Some(repo)) => { | |
| let repo = repo.trim_end_matches(".git"); | |
| if owner.is_empty() || repo.is_empty() { | |
| trimmed.to_string() | |
| } else { | |
| format!("{owner}/{repo}") | |
| } | |
| } | |
| _ => trimmed.to_string(), | |
| } | |
| pub(super) fn normalize_github_repo_filter(raw: &str) -> String { | |
| let trimmed = raw.trim(); | |
| let without_scheme = trimmed | |
| .strip_prefix("https://github.com/") | |
| .or_else(|| trimmed.strip_prefix("http://github.com/")) | |
| .or_else(|| trimmed.strip_prefix("github.com/")) | |
| .or_else(|| trimmed.strip_prefix("www.github.com/")) | |
| .or_else(|| trimmed.strip_prefix("git@github.com:")) | |
| .unwrap_or(trimmed); | |
| let without_suffix = without_scheme | |
| .split(['?', '#']) | |
| .next() | |
| .unwrap_or(without_scheme); | |
| let cleaned = without_suffix | |
| .trim_start_matches('/') | |
| .trim_end_matches('/') | |
| .trim_end_matches(".git"); | |
| let mut parts = cleaned.split('/').filter(|part| !part.is_empty()); | |
| match (parts.next(), parts.next()) { | |
| (Some(owner), Some(repo)) => { | |
| let repo = repo.trim_end_matches(".git"); | |
| if owner.is_empty() || repo.is_empty() { | |
| trimmed.to_string() | |
| } else { | |
| format!("{owner}/{repo}") | |
| } | |
| } | |
| _ => trimmed.to_string(), | |
| } | |
| } |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/openhuman/memory_sync/composio/providers/github/provider.rs` around lines
672 - 694, normalize_github_repo_filter currently misses inputs that start with
"github.com/" (no scheme) and doesn't strip URL query/fragment or trailing path
components before collapsing to owner/repo, causing inputs like
"github.com/owner/repo" or "https://github.com/owner/repo?tab=issues" to be
normalized incorrectly; update normalize_github_repo_filter to first strip any
leading "github.com/" prefix in addition to https/http/git@ schemes, remove URL
query strings and fragments (anything after '?' or '#'), trim trailing slashes
and ".git", then split on '/' and return format!("{owner}/{repo}") only when
both are non-empty (otherwise return trimmed input), ensuring references to
normalize_github_repo_filter, owner, repo, and cleaned variables in the existing
function are used to locate the change.
graycyrus
left a comment
There was a problem hiding this comment.
Review — Task source UI + agent launch flow
Really like the UX here — refine a source task into an approved brief, then "Work task" launches it into a labeled, observable chat thread instead of copy/paste. That's a genuinely nice flow, the Rust is clean and idiomatic (token read from env and never logged, sane timeouts, github_search_arg_pairs correctly skips nulls/empties), and the i18n parity is there. Thanks for the careful work.
Requesting changes on two things — one architectural, one CI — plus a couple of smaller notes.
1. The GitHub fetch is described as a "fallback" but the code replaces Composio unconditionally
The PR summary says "GitHub task-source fetching fallback through local gh/REST when Composio account connection is unavailable" — but fetch_tasks no longer touches Composio at all; it always goes through fetch_github_tasks_local (gh CLI → REST with a GH_TOKEN/GITHUB_TOKEN env token). In a shipped desktop build a user typically has no gh on PATH and no GITHUB_TOKEN env — they connected GitHub via Composio OAuth. So this risks fixing local dev while regressing real users' task-source preview. Could we either (a) make it a true fallback (try the Composio/ctx path first, fall back to local only when unavailable), or (b) if local-only is intentional, spell out in the PR how end users without gh/env-token are expected to authenticate? See the inline note. (Only preview/fetch changed — sync/ingest still uses Composio, which is good.)
2. Rust diff-coverage gate is failing
The ~190 new Rust lines (fetch_github_tasks_local, gh_search_issues, rest_search_issues, resolve_github_login*, github_env_token, expand_me_in_github_search_args) are uncovered except for github_search_arg_pairs. Subprocess/network are hard to unit-test, but several of these are pure and easily testable — covering them should clear the gate without mocking a subprocess (inline notes).
Smaller notes
- No linked issue for a 2.2k-line / 21-file feature — worth filing one (and it'd satisfy the failing PR Submission Checklist).
- Possible overlap with #3270 (active-run steering/queue): both mutate thread/task state when launching a run. If they land in the same window, please sanity-check the
in_progress/ queue transitions don't collide. - Reviewability: the Rust GitHub-provider change is independently reviewable from the UI — splitting it out would make both easier to land, but not required.
The feature itself is solid; it's the Composio-vs-local question + coverage that need resolving before merge.
| )); | ||
| } | ||
|
|
||
| let data = fetch_github_tasks_local(&query, max, &filter.extra).await?; |
There was a problem hiding this comment.
[blocking — architecture] This replaces the Composio path entirely, it isn't a fallback. The old code routed through ctx.execute(ACTION_SEARCH_ISSUES, …) (the user's Composio-connected GitHub account); now fetch_tasks always calls fetch_github_tasks_local, which needs gh on PATH or a GH_TOKEN/GITHUB_TOKEN env var. A packaged desktop user generally has neither — they authed GitHub via Composio OAuth — so their task-source preview would stop working. Either try the ctx/Composio path first and fall back to local only when it's genuinely unavailable (matching the PR description), or document how end users without gh/env-token authenticate. Today ctx is unused in fetch_tasks, which is the tell.
| } | ||
| } | ||
|
|
||
| async fn fetch_github_tasks_local(query: &str, max: usize, extra: &Value) -> Result<Value, String> { |
There was a problem hiding this comment.
[blocking — coverage] This function plus gh_search_issues / rest_search_issues / resolve_github_login* are the bulk of the uncovered new lines tripping the Rust Core Coverage gate. The subprocess/network bodies are hard to unit-test, but the control flow is testable: e.g. factor the gh failed → REST fallback decision so you can assert the error-aggregation message (gh: …; REST: …) without spawning anything, and assert the tracing::debug! fallback path is taken. Even covering the orchestration + error formatting here lifts the diff-cover meaningfully.
| .map_err(|e| format!("parse GitHub API response: {e}")) | ||
| } | ||
|
|
||
| fn github_env_token() -> Option<String> { |
There was a problem hiding this comment.
Low-hanging coverage: github_env_token() and expand_me_in_github_search_args() are pure (modulo the network call inside the latter). Quick unit tests would help the gate: for github_env_token, cover GH_TOKEN set / fallback to GITHUB_TOKEN / whitespace-trimmed / empty-→None; for expand_me_in_github_search_args, the no-@me early return is a pure no-op you can assert leaves q untouched. Pair with a couple more github_search_arg_pairs cases (null-skip, number, bool, non-object error).
…ion DB picker, PR/issue intent, skip closed; localize task-flow copy Resolves the CHANGES_REQUESTED + CodeRabbit notes and adds related task-source improvements. GitHub fetch (graycyrus #1 + #2): - fetch is no longer an unconditional local replacement. New per-source `fetch_mode` (FilterSpec::Github) → `GithubFetchMode { Auto, Composio, Local }`, default Auto = try the connected Composio account first, fall back to local `gh`/REST only when unavailable. `Composio`/`Local` force a path. Shipped Composio users no longer regress; local/dev still works. - restored the Composio path as `fetch_github_tasks_composio`; `fetch_tasks` branches on the mode and normalizes once. - added unit tests for the pure helpers (mode default/serde, env-token precedence, arg-pair edge cases) to cover the changed Rust lines. Skip done work (GitHub): `normalize_github_issue` drops items with `state == "closed"` (covers merged PRs AND closed issues); `build_fetch_query` biases to `is:open` when no explicit state. + tests. PR-vs-issue intent: `TaskKind` on NormalizedTask; GitHub normalizer tags issue/PR; enrich frames the objective ("Review pull request:" / "Resolve issue:") + prompt; route stamps `source_metadata.kind`. + tests. Notion database picker: `task_sources_list_databases` RPC + `ComposioProvider::list_databases` (NOTION_SEARCH_NOTION_PAGE) + a "Browse databases" dropdown in the source form + i18n (14 locales). + tests. CodeRabbit FE fixes (IntelligenceTasksTab): - localize the new task-flow copy (seeded work-task message + refinement-form defaults) via useT() with real translations across 14 locales. - keep the refinement dialog open until BOTH the personal-board save and the source-card updateStatus('done') succeed (prevents duplicate-on-retry). Verified: cargo check + github/notion/task_sources tests, pnpm typecheck, i18n:check parity — all green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (1)
src/openhuman/memory_sync/composio/providers/notion/provider.rs (1)
511-516: 💤 Low valueConsider downgrading response-shape logging to debug! once the integration stabilizes.
The
info!-level logging of raw response shape details (keys, is_array) is valuable for validating the new Composio API integration. Once thelist_databasesbehavior is confirmed in production, consider downgrading this todebug!ortrace!level to reduce log volume, consistent with the guideline to use debug/trace for development-oriented diagnostics.As per coding guidelines: "use
log/tracingatdebugortracelevel for development-oriented logs at entry/exit points, branch decisions, external calls, retries/timeouts, state transitions, and error handling paths."🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/openhuman/memory_sync/composio/providers/notion/provider.rs` around lines 511 - 516, The current tracing::info! call in the list_databases handling logs raw response shape (resp.successful, resp.data.is_array(), resp.data.as_object() keys) at info level; change this to a lower-verbosity level (e.g., tracing::debug! or tracing::trace!) so the response-shape diagnostics for Composio’s list_databases are only emitted during development/diagnostics. Locate the logging in provider.rs around the list_databases response handling (the block referencing resp.successful and resp.data) and replace the info! invocation with debug! (or trace!) while keeping the same structured fields and message.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@app/src/components/settings/panels/TaskSourcesPanel.tsx`:
- Around line 99-103: The effect that calls setDatabases([]) inside useEffect
when provider changes should be removed and the state reset moved into the
provider change handler; locate the provider onChange handler (the function that
updates provider selection) and add a synchronous setDatabases([]) there so the
database picker is cleared at the moment the provider is changed, ensuring you
update the same state variable used by useEffect (setDatabases) and remove the
now-unnecessary useEffect block that referenced provider.
In `@app/src/lib/i18n/ar.ts`:
- Line 2618: Replace the incorrect Arabic phrase "هذا المحادثة" with the correct
form "هذه المحادثة" in the closing instruction string found in
app/src/lib/i18n/ar.ts (the string that currently reads 'ابدأ بإعادة صياغة خطة
التنفيذ الملموسة بإيجاز، ثم نفّذها. أبقِ التقدّم مرئيًا في هذا المحادثة وحدّث
لوحة المهام عند تغيّر حالة العمل.'); update that string literal so the sentence
reads with "هذه المحادثة".
---
Nitpick comments:
In `@src/openhuman/memory_sync/composio/providers/notion/provider.rs`:
- Around line 511-516: The current tracing::info! call in the list_databases
handling logs raw response shape (resp.successful, resp.data.is_array(),
resp.data.as_object() keys) at info level; change this to a lower-verbosity
level (e.g., tracing::debug! or tracing::trace!) so the response-shape
diagnostics for Composio’s list_databases are only emitted during
development/diagnostics. Locate the logging in provider.rs around the
list_databases response handling (the block referencing resp.successful and
resp.data) and replace the info! invocation with debug! (or trace!) while
keeping the same structured fields and message.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 6f0c9057-14e1-4a5c-957f-273e7ff01dc9
📒 Files selected for processing (36)
app/src/components/intelligence/IntelligenceTasksTab.tsxapp/src/components/settings/panels/TaskSourcesPanel.tsxapp/src/lib/i18n/ar.tsapp/src/lib/i18n/bn.tsapp/src/lib/i18n/de.tsapp/src/lib/i18n/en.tsapp/src/lib/i18n/es.tsapp/src/lib/i18n/fr.tsapp/src/lib/i18n/hi.tsapp/src/lib/i18n/id.tsapp/src/lib/i18n/it.tsapp/src/lib/i18n/ko.tsapp/src/lib/i18n/pl.tsapp/src/lib/i18n/pt.tsapp/src/lib/i18n/ru.tsapp/src/lib/i18n/zh-CN.tsapp/src/utils/tauriCommands/taskSources.tssrc/openhuman/memory_sync/composio/providers/clickup/provider.rssrc/openhuman/memory_sync/composio/providers/github/provider.rssrc/openhuman/memory_sync/composio/providers/github/tests.rssrc/openhuman/memory_sync/composio/providers/linear/provider.rssrc/openhuman/memory_sync/composio/providers/mod.rssrc/openhuman/memory_sync/composio/providers/notion/provider.rssrc/openhuman/memory_sync/composio/providers/notion/tests.rssrc/openhuman/memory_sync/composio/providers/traits.rssrc/openhuman/memory_sync/composio/providers/types.rssrc/openhuman/task_sources/enrich.rssrc/openhuman/task_sources/filter.rssrc/openhuman/task_sources/mod.rssrc/openhuman/task_sources/ops.rssrc/openhuman/task_sources/periodic.rssrc/openhuman/task_sources/pipeline_tests.rssrc/openhuman/task_sources/route.rssrc/openhuman/task_sources/schemas.rssrc/openhuman/task_sources/store_tests.rssrc/openhuman/task_sources/types.rs
✅ Files skipped from review due to trivial changes (9)
- src/openhuman/task_sources/pipeline_tests.rs
- src/openhuman/task_sources/periodic.rs
- app/src/lib/i18n/en.ts
- app/src/lib/i18n/ru.ts
- app/src/lib/i18n/hi.ts
- app/src/lib/i18n/pt.ts
- app/src/lib/i18n/ko.ts
- app/src/lib/i18n/es.ts
- app/src/lib/i18n/id.ts
🚧 Files skipped from review as they are similar to previous changes (4)
- app/src/lib/i18n/zh-CN.ts
- app/src/lib/i18n/bn.ts
- app/src/lib/i18n/fr.ts
- app/src/components/intelligence/IntelligenceTasksTab.tsx
| // Clear any loaded database picker when the provider changes — the list is | ||
| // provider-specific (today only Notion exposes one). | ||
| useEffect(() => { | ||
| setDatabases([]); | ||
| }, [provider]); |
There was a problem hiding this comment.
Avoid synchronous setState inside useEffect.
This effect's sole purpose is to reset state on provider change, which the codebase's react-hooks/set-state-in-effect lint rule disallows. Clear the picker in the provider onChange handler (Line 299) instead.
♻️ Proposed refactor
- // Notion database picker: populated on demand via `browseDatabases`.
- const [databases, setDatabases] = useState<TaskContainer[]>([]);
-
- // Clear any loaded database picker when the provider changes — the list is
- // provider-specific (today only Notion exposes one).
- useEffect(() => {
- setDatabases([]);
- }, [provider]);
+ // Notion database picker: populated on demand via `browseDatabases`.
+ const [databases, setDatabases] = useState<TaskContainer[]>([]);Then reset the list where the provider actually changes:
value={provider}
- onChange={e => setProvider(e.target.value as TaskSourceProvider)}>
+ onChange={e => {
+ setProvider(e.target.value as TaskSourceProvider);
+ // Picker list is provider-specific (today only Notion exposes one).
+ setDatabases([]);
+ }}>Based on learnings, do not perform synchronous setState calls directly inside useEffect bodies; the react-hooks/set-state-in-effect rule disallows resetting state at the top of an effect.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@app/src/components/settings/panels/TaskSourcesPanel.tsx` around lines 99 -
103, The effect that calls setDatabases([]) inside useEffect when provider
changes should be removed and the state reset moved into the provider change
handler; locate the provider onChange handler (the function that updates
provider selection) and add a synchronous setDatabases([]) there so the database
picker is cleared at the moment the provider is changed, ensuring you update the
same state variable used by useEffect (setDatabases) and remove the
now-unnecessary useEffect block that referenced provider.
| 'intelligence.workTask.externalIdLine': '- المعرّف الخارجي: {externalId}', | ||
| 'intelligence.workTask.urlLine': '- الرابط: {url}', | ||
| 'intelligence.workTask.closingInstruction': | ||
| 'ابدأ بإعادة صياغة خطة التنفيذ الملموسة بإيجاز، ثم نفّذها. أبقِ التقدّم مرئيًا في هذا المحادثة وحدّث لوحة المهام عند تغيّر حالة العمل.', |
There was a problem hiding this comment.
Fix Arabic grammar in the closing instruction string.
هذا المحادثة is grammatically incorrect in Arabic; it should be هذه المحادثة.
✏️ Suggested fix
- 'intelligence.workTask.closingInstruction':
- 'ابدأ بإعادة صياغة خطة التنفيذ الملموسة بإيجاز، ثم نفّذها. أبقِ التقدّم مرئيًا في هذا المحادثة وحدّث لوحة المهام عند تغيّر حالة العمل.',
+ 'intelligence.workTask.closingInstruction':
+ 'ابدأ بإعادة صياغة خطة التنفيذ الملموسة بإيجاز، ثم نفّذها. أبقِ التقدّم مرئيًا في هذه المحادثة وحدّث لوحة المهام عند تغيّر حالة العمل.',📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| 'ابدأ بإعادة صياغة خطة التنفيذ الملموسة بإيجاز، ثم نفّذها. أبقِ التقدّم مرئيًا في هذا المحادثة وحدّث لوحة المهام عند تغيّر حالة العمل.', | |
| 'ابدأ بإعادة صياغة خطة التنفيذ الملموسة بإيجاز، ثم نفّذها. أبقِ التقدّم مرئيًا في هذه المحادثة وحدّث لوحة المهام عند تغيّر حالة العمل.', |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@app/src/lib/i18n/ar.ts` at line 2618, Replace the incorrect Arabic phrase
"هذا المحادثة" with the correct form "هذه المحادثة" in the closing instruction
string found in app/src/lib/i18n/ar.ts (the string that currently reads 'ابدأ
بإعادة صياغة خطة التنفيذ الملموسة بإيجاز، ثم نفّذها. أبقِ التقدّم مرئيًا في هذا
المحادثة وحدّث لوحة المهام عند تغيّر حالة العمل.'); update that string literal
so the sentence reads with "هذه المحادثة".
Summary
Work taskaction that creates anagent-tasklabeled thread, seeds it with the task brief, marks the task in progress, and starts chat execution.gh/REST paths when Composio account connection is unavailable.Problem
Solution
Work tasklaunch path that creates a new labeled thread, appends the generated task prompt, sets inference state, navigates to chat, and callschatSendwith the active agent profile and locale.Submission Checklist
diff-cover) meet the gate enforced by.github/workflows/pr-ci.yml. CI will enforce changed-lines diff coverage; local full diff coverage was not run because targeted validations were used.## Related— N/A: no new coverage-matrix feature ID.docs/RELEASE-MANUAL-SMOKE.md) — N/A: not a release-cut smoke surface.Closes #NNNin the## Relatedsection — N/A: no issue was provided for this branch.Impact
Related
AI Authored PR Metadata (required for Codex/Linear PRs)
Linear Issue
Commit & Branch
task-sources-kanban-ui203ba93d4afa0411e5df5c8824b7614bacfff755Validation Run
pnpm --filter openhuman-app format:checkvia pre-pushpnpm format:checkpnpm typecheckvia pre-pushpnpm compile, pluspnpm --dir app exec tsc --noEmit --pretty falsepnpm debug unit src/components/intelligence/__tests__/IntelligenceTasksTab.test.tsx --verbose;pnpm debug unit src/lib/i18n/__tests__/coverage.test.ts --verbosecargo fmt --manifest-path ../Cargo.toml --all --checkvia pre-push format check;pnpm rust:checkvia pre-pushcargo fmt --manifest-path src-tauri/Cargo.toml --all --check;cargo check --manifest-path src-tauri/Cargo.tomlvia pre-pushValidation Blocked
command:N/Aerror:N/Aimpact:N/ABehavior Changes
Work taskaction appear in the Intelligence task board.Parity Contract
chatSendlifecycle.Duplicate / Superseded PR Handling
Summary by CodeRabbit
New Features
Tests
Localization