Skip to content

feat: Human-in-the-Loop (HITL) approval UI - #104

Merged
ginccc merged 15 commits into
mainfrom
feat/hitl-ui
Jul 6, 2026
Merged

feat: Human-in-the-Loop (HITL) approval UI#104
ginccc merged 15 commits into
mainfrom
feat/hitl-ui

Conversation

@ginccc

@ginccc ginccc commented Jul 3, 2026

Copy link
Copy Markdown
Member

Summary

Adds the Human-in-the-Loop (HITL) approval experience to EDDI-Manager across both the 1:1 conversation surface and the multi-agent group-discussion surface, plus the configuration UI to enable it. Cross-checked against the EDDI backend's feat/hitl-framework HITL contract.

What's included

Runtime — approval flow

  • 1:1 conversations: an approval banner on AWAITING_HUMAN conversations (approve/reject with optional note, cancel), and an AWAITING_HUMAN state + filter on the conversations list.
  • Group discussions: approve/reject a paused phase and resume the discussion live over the approve/stream SSE endpoint; cancel; per-task approvals for TASK granularity; a live countdown / timeout-policy banner; the task board's "Awaiting Approval" column; and handling for the awaiting_approval, hitl_resume, cancelled, error/group_error, and member_pause_skipped SSE events.
  • Cross-group Approvals queue (/manage/approvals): a single inbox of every pending approval (1:1 + group) via the backend's GET /groups/pending-approvals, with search, quick actions, and per-surface routing.

Configuration

  • Agent editor: a HITL section — timeout policy, ISO-8601 approval timeout (client-validated, with a seeded default for finite policies), and the designer-supplied pauseReason.
  • Group wizard: per-phase approval points (materialised from the exact preset phases so runtime behaviour is preserved), timeout policy/duration, and TASK granularity + on-task-rejection for TASK_FORCE; plus a read-only HITL summary in the group config panel.

i18n: all new strings across the 11 supported locales.

Backend contract

Verified against EDDI feat/hitl-framework (endpoints, DTOs, SSE event names/payloads, config models, validation). Adopted the new cross-group GET /groups/pending-approvals inbox and mirrored the new agent-level pauseReason field; verdict handling is now case-insensitive on the backend (the uppercase APPROVED/REJECTED sent here is fine).

Testing

  • tsc -b (also fixed the typecheck script, which was previously a no-op on the solution-style tsconfig), eslint, and vite build all clean.
  • 3,427 unit tests pass (234 files), including new coverage for the HITL API hooks, the approval banner (incl. the live countdown/overdue timer), the config editors, ISO-8601 duration validation, the group-stream HITL SSE state transitions, and the AWAITING_APPROVAL banner render path.
  • The implementation went through several rounds of adversarial multi-agent code review against the live backend before this PR was opened.

Review response

All 9 CodeRabbit + 2 Copilot actionable findings addressed and resolved (see thread replies for details):

  • Fixed: missing taskBoard.awaitingApproval i18n key (all 11 locales), silently-swallowed cross-group approvals fetch errors (now surfaced as a non-blocking warning), stale timeoutDraft resync on policy toggle, nullable PendingApprovalSummary fields, and the stale CONTRIBUTING.md typecheck doc.
  • Also applied from the nitpick pass: centralised ISO-8601 duration formatting (banner + config panel), DRY'd the two SSE POST call sites, seeded a default timeout on finite-policy switch in the group wizard (parity with the agent editor), an accessible name on the refresh button, and stricter HITL field typing on GroupConversation.
  • Left open: the .ds-sync/ findings (5 threads) — that tooling is unrelated to HITL and tracked for the separate extraction PR noted below; the duplicate-display-name task-matching finding — a backend data constraint (the SSE payload has no stable per-task agent id), not fixable frontend-only.

Note on scope

The first commit (14439b16) also bundles ~9.5k lines of unrelated .ds-sync design-system tooling. As discussed, that can be extracted into its own PR — it is not part of the HITL feature and has no runtime impact.

Summary by CodeRabbit

  • New Features

    • Added a new Approvals area for reviewing pending human-in-the-loop items, including conversation and group approval queues.
    • Added Human-in-the-Loop controls across conversations, groups, tasks, and chat, with approve/reject/cancel actions and pause banners.
    • Introduced group-level approval configuration, including selectable approval phases, timeout settings, and tool-call approval gating.
    • Added support for an “Awaiting Approval” task state and related navigation, labels, and translations.
  • Bug Fixes

    • Improved paused-conversation handling so the UI stays in sync after approvals, cancellations, and resumed streams.

ginccc added 2 commits July 2, 2026 20:54
- New hitl.ts API module (types + functions for both surfaces)
- AWAITING_HUMAN state on conversations, CANCELLED state on groups
- HITL pause bookmark fields on conversation/group models
- hitlConfig on Agent and AgentGroupConfiguration
- SSE events: awaiting_approval, hitl_resume, cancelled
- use-hitl.ts hooks (queries with polling + mutations)
- ApprovalBanner component (timeout countdown, per-task approvals)
- Approvals queue page (/manage/approvals) with search + actions
- Sidebar + router integration
- conversation-detail: approval banner for AWAITING_HUMAN
- conversations: AWAITING_HUMAN filter and state rendering
- discussion-transcript: approval banner for AWAITING_APPROVAL
- group-detail: CANCELLED state + cancel button
- task-board: AWAITING_APPROVAL column
- MSW handlers for all HITL endpoints
- i18n: 27 hitl.* keys across all 11 locales
…ckend contract

Makes the HITL UI functional end-to-end and configurable, and adapts it to the
current EDDI feat/hitl-framework contract.

Group approval flow
- Wire the group ApprovalBanner to a real approve/reject via the approve/stream
  SSE endpoint (approveAndStream) so the discussion resumes live; fix the cancel
  path (gcId), and drive success/error toasts off the actual resume outcome
  rather than optimistically.
- Handle the error/group_error and member_pause_skipped SSE events; on pause,
  switch to the persisted conversation (with a live fallback to avoid a skeleton
  flash) so the banner shows the countdown, timeout policy and awaiting tasks.

HITL config editors
- Agent editor: HitlConfig section (timeout policy + ISO-8601 approval timeout
  with client validation and a seeded default for finite policies, plus the new
  pauseReason field).
- Group wizard: per-phase approval points (materialised from the exact preset
  phases), timeout policy/duration, and TASK granularity + on-rejection for
  TASK_FORCE; read-only HITL summary in the group config panel.

Backend contract adaptation
- Use the new cross-group GET /groups/pending-approvals inbox (single request)
  instead of a per-group fan-out; mirror agent-level pauseReason.

Correctness & polish
- Central, backend-accurate ISO-8601 duration parsing (fraction on seconds only);
  correct react-query invalidations; task-board awaiting-approval column; a11y
  (aria-pressed, labelled note); i18n across all 11 locales.
- Fix the vacuous typecheck script (tsc --noEmit -> tsc -b).

Tests: hitl-config, hitl-labels, approval-banner, use-hitl, config section, and
group-stream HITL SSE transitions; MSW handlers for all HITL endpoints.
@coderabbitai

coderabbitai Bot commented Jul 3, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Changes

DS-sync build/validate/capture pipeline

Layer / File(s) Summary
Shared utilities, config validation, hashing
.ds-sync/lib/common.mjs, .ds-sync/lib/sync-hashes.mjs, .ds-sync/lib/detect.mjs
Adds cross-platform path/string helpers, config-key validation, deterministic hashing recipes for styles/render/source keys, and shape detection.
Bundling and CSS/font asset pipeline
.ds-sync/lib/bundle.mjs, .ds-sync/lib/css.mjs, .ds-sync/lib/css-fallback.mjs
Adds esbuild IIFE bundling with React shims and export evidence, plus token/font copying and stylesheet assembly with Storybook CSS fallback.
Type/props extraction and doc ingestion
.ds-sync/lib/dts.mjs, .ds-sync/lib/docs.mjs
Adds ts-morph based .d.ts prop extraction and doc discovery/ingestion/guideline emission.
Preview generation and story import resolution
.ds-sync/lib/previews.mjs, .ds-sync/lib/preview-gen-storybook.mjs, .ds-sync/lib/story-imports.mjs
Adds preview file ownership/build, Storybook preview wrapper generation, and esbuild plugins redirecting story imports to the shipped bundle.
Source shape adapters
.ds-sync/lib/source-kit.mjs, .ds-sync/lib/source-storybook.mjs
Adds plain package resolution and Storybook config discovery/story pairing/decorator bundling adapters.
Per-component output emission
.ds-sync/lib/emit.mjs
Adds React vendoring, preview HTML, .jsx/.d.ts/.prompt.md emission, review page, README, and build meta writers.
package-build.mjs orchestration CLI
.ds-sync/package-build.mjs, .ds-sync/package.json
Adds the full build CLI wiring config validation, shape detection, bundling, CSS/token handling, component filtering, docs, previews, and emit outputs.
Preview rebuild and remote diff
.ds-sync/lib/preview-rebuild.mjs, .ds-sync/lib/remote-diff.mjs
Adds scoped preview rebuild with manifest restamping and remote sidecar diff/upload computation.
Validation, capture, and compare tooling
.ds-sync/package-validate.mjs, .ds-sync/package-capture.mjs, .ds-sync/storybook/compare.mjs, .ds-sync/storybook/http-serve.mjs, .ds-sync/storybook/probe.mjs
Adds integrity/render validation, screenshot capture/grading harnesses, static file serving, and provider detection.
Resync driver orchestration
.ds-sync/resync.mjs
Adds the multi-stage build→diff→validate→capture driver with canary logic and verdict output.
SKILL documentation
.ds-sync/storybook/SKILL.md
Adds workflow documentation covering setup, self-heal loop, grading, campaign orchestration, and upload procedure.

Estimated code review effort: 5 (Critical) | ~150 minutes

Human-in-the-loop approval workflow

Layer / File(s) Summary
HITL API contracts and types
src/lib/api/hitl.ts, src/lib/api/agents.ts, src/lib/api/conversations.ts, src/lib/api/groups.ts
Adds HITL DTOs/endpoints and extends Agent, ConversationState, Group config/conversation, and SSE event types with HITL fields.
HITL config/label helpers and tool-approval validation
src/lib/hitl-config.ts, src/lib/hitl-labels.ts, src/lib/hitl-tool-approvals.ts, tests
Adds phase/timeout-duration helpers, translation label helpers, and tool-approval pattern/config validation.
Group approval streaming and pending-approval hooks
src/hooks/use-group-discussion-stream.ts, src/hooks/use-hitl.ts, tests
Adds SSE approve/resume/cancel handling and React Query hooks for pending approvals, resume, and cancel.
ApprovalBanner shared component
src/components/hitl/approval-banner.tsx, tests
Renders pause metadata, countdown, task-level and tool-call approvals, and decision/cancel actions.
Agent-level and task-level tool approval gating UI
src/components/editors/agent-config-sections.tsx, src/components/editors/tool-approvals-editor.tsx, src/components/editors/llm/task-tool-approvals-section.tsx, src/components/editors/llm/types.ts, src/components/editors/llm-editor.tsx, src/pages/agent-detail.tsx, tests
Adds HitlConfigSection and ToolApprovalsEditor, wired into agent details and per-task overrides.
Group wizard HITL configuration
src/pages/group-wizard.tsx, tests
Adds HITL enablement/approval-phase/timeout/granularity controls and review-step badge.
Group config panel HITL summary and inline editor
src/components/groups/group-config-panel.tsx, src/components/groups/group-hitl-editor.tsx, tests
Adds a Human Approval summary/edit toggle and inline HITL editor for existing groups.
Discussion transcript and task board integration
src/components/groups/discussion-transcript.tsx, src/components/groups/task-board.tsx, tests
Adds approval banner rendering/callbacks and an awaiting-approval column/status.
Group detail page approve/cancel wiring
src/pages/group-detail.tsx
Adds cancel mutation, deferred approve toasts, stream-fallback rendering, and config-drift recovery banner.
Approvals inbox page and navigation
src/pages/approvals.tsx, src/app.tsx, src/components/layout/sidebar.tsx, tests
Adds a merged pending-approvals queue page routed at /manage/approvals with a sidebar entry.
Conversation, audit, channel, and schedule HITL surfaces
src/pages/conversation-detail.tsx, src/pages/conversations.tsx, src/pages/audit.tsx, src/pages/channel-detail.tsx, src/pages/schedules.tsx
Adds AWAITING_HUMAN handling, HITL decision summaries, channel config fields, and schedule badges/gating.
Chat pause UX
src/hooks/use-chat.ts, src/components/chat/chat-panel.tsx, tests
Adds pause state from 409/SSE signals, a pause banner, and disabled inputs while awaiting approval.
Localization and mocks
src/i18n/locales/*.json, src/test/mocks/handlers.ts
Adds HITL/approvals translation keys across all locales and MSW handlers for pending-approvals/decision endpoints.

Estimated code review effort: 5 (Critical) | ~150 minutes

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant ApprovalBanner
  participant useGroupDiscussionStream
  participant GroupsAPI as "groups.ts (streamGroupApproval)"
  participant Backend

  User->>ApprovalBanner: click Approve/Reject
  ApprovalBanner->>useGroupDiscussionStream: onDecide(verdict, note, taskApprovals/toolDecisions)
  useGroupDiscussionStream->>GroupsAPI: approveAndStream(groupId, gcId, request)
  GroupsAPI->>Backend: POST /approve/stream
  Backend-->>GroupsAPI: SSE hitl_resume, ... , group_complete
  GroupsAPI-->>useGroupDiscussionStream: yield GroupSSEEvent
  useGroupDiscussionStream-->>ApprovalBanner: streamState (hitlResume, COMPLETED)
  ApprovalBanner-->>User: toast success / updated transcript
Loading
sequenceDiagram
  participant Chat as ChatPanel
  participant Hook as useSendMessage
  participant Backend

  Chat->>Hook: send message
  Hook->>Backend: POST /agents/:conversationId
  Backend-->>Hook: 409 (awaiting human approval)
  Hook->>Hook: remove optimistic message, setPaused(true, reason)
  Hook-->>Chat: isPaused=true, pauseReason
  Chat-->>Chat: render pause banner, disable input
Loading

Possibly related PRs

  • labsai/EDDI-Manager#100: Both PRs modify src/components/groups/task-board.tsx and related group discussion/task streaming UI, adding overlapping status/column handling.

Suggested labels: enhancement, feature, needs-review

Suggested reviewers: (based on repository ownership of .ds-sync/ tooling and src/components/hitl/ frontend code)

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: adding the HITL approval UI and related workflow.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/hitl-ui

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 9

🧹 Nitpick comments (12)
src/pages/approvals.tsx (1)

151-157: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Icon-only refresh button lacks an accessible name.

Screen readers announce nothing meaningful for this control.

♿ Proposed fix
           <button
             onClick={handleRefresh}
             className="inline-flex items-center gap-1.5 rounded-lg border border-input bg-background px-3 py-2 text-sm font-medium text-foreground hover:bg-muted transition-colors"
             data-testid="refresh-approvals"
+            aria-label={t("common.refresh", "Refresh")}
           >
🤖 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/pages/approvals.tsx` around lines 151 - 157, The refresh button rendered
in approvals.tsx via the handleRefresh button currently has only an icon, so it
needs an accessible name for screen readers. Add an appropriate text alternative
to the button itself in the approvals page component (for example on the button
using RefreshCw), and make sure the control is still visually icon-only while
exposing a clear label to assistive tech.
src/lib/api/groups.ts (2)

214-222: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Consider typing hitlTimeoutPolicy/hitlApprovalTimeout more strictly.

These are declared as plain string, but group-wizard.tsx treats timeout policy as a constrained set of values (requiresApprovalTimeout(state.hitl.timeoutPolicy)) and validates approvalTimeout as an ISO-8601 duration. Reusing the union type from hitl.ts here (rather than string) would preserve type safety for consumers reading GroupConversation.hitlTimeoutPolicy.

🤖 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/lib/api/groups.ts` around lines 214 - 222, The HITL pause fields in
GroupConversation are too loosely typed, especially hitlTimeoutPolicy and
hitlApprovalTimeout. Update the GroupConversation shape in groups.ts to reuse
the stricter HITL types from hitl.ts so consumers like group-wizard.tsx can rely
on the same constrained timeout policy values and ISO-8601 duration typing. Keep
the existing field names, but replace the plain string annotations with the
shared union/type definitions used by the HITL state model.

504-551: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Duplicate fetch/header scaffolding between streamGroupDiscussion and streamGroupApproval.

Both functions build an identical Content-Type/auth-header fetch call before delegating to readGroupSSE. Consider extracting a small postSSE(url, body, signal) helper to avoid drift between the two call sites.

♻️ Sketch of a shared helper
+async function postSSE(path: string, body: unknown, signal?: AbortSignal): Promise<Response> {
+  return fetch(`${api.getBaseUrl()}${path}`, {
+    method: "POST",
+    headers: { "Content-Type": "application/json", ...api.getAuthHeader() },
+    body: JSON.stringify(body),
+    signal,
+  });
+}
+
 export async function* streamGroupDiscussion(...): AsyncGenerator<GroupSSEEvent> {
-  const response = await fetch(`${api.getBaseUrl()}/groups/${groupId}/conversations/stream`, {...});
+  const response = await postSSE(`/groups/${groupId}/conversations/stream`, { question, userId: userId || "manager-user" }, signal);
   yield* readGroupSSE(response);
 }

 export async function* streamGroupApproval(...): AsyncGenerator<GroupSSEEvent> {
-  const response = await fetch(`${api.getBaseUrl()}/groups/${groupId}/conversations/${gcId}/approve/stream`, {...});
+  const response = await postSSE(`/groups/${groupId}/conversations/${gcId}/approve/stream`, request, signal);
   yield* readGroupSSE(response);
 }
🤖 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/lib/api/groups.ts` around lines 504 - 551, The fetch-and-SSE setup is
duplicated in streamGroupDiscussion and streamGroupApproval, which can drift
over time. Extract the shared POST logic into a small helper such as postSSE
that takes the URL, request body, and optional signal, sets the Content-Type and
auth headers via api.getAuthHeader(), performs fetch, and returns the response
for readGroupSSE. Update both streamGroupDiscussion and streamGroupApproval to
call the helper so the request scaffolding lives in one place.
src/hooks/use-group-discussion-stream.ts (1)

41-65: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Minor type duplication and looser typing for HITL fields.

hitlPause's shape and the awaiting_approval payload cast at Line 479-484 duplicate the same literal type; likewise hitlResume.verdict is typed string rather than the HitlVerdict union already used elsewhere (e.g. DiscussionTranscriptProps.onApprove). Extracting/reusing shared types would tighten consistency across the HITL surface.

Also applies to: 477-524

🤖 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/hooks/use-group-discussion-stream.ts` around lines 41 - 65, The HITL
state types in use-group-discussion-stream are duplicated and too loose, so
tighten them by extracting shared types for the pause payload and using the
existing HitlVerdict union for hitlResume instead of string. Update the relevant
types around hitlPause, hitlResume, and the awaiting_approval event handling to
reuse these shared symbols consistently, and align any related casts or prop
contracts such as DiscussionTranscriptProps.onApprove so the HITL surface stays
type-safe and source-of-truth driven.
.ds-sync/resync.mjs (1)

67-76: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Duplicate of remote-diff.mjs's argument-parsing block.

Same "unrecognized argument" loop as lib/remote-diff.mjs lines 51-59. Worth consolidating into a shared helper (see companion comment on that file) rather than keeping two copies in sync by hand.

🤖 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 @.ds-sync/resync.mjs around lines 67 - 76, The argument-parsing block in
resync.mjs duplicates the same unrecognized-argument loop used in
remote-diff.mjs, so consolidate this logic into the shared helper used by both
scripts instead of keeping a second copy in sync. Refactor the processing around
argv, flag, and VALUE_FLAGS in this file to call that shared parser/validator,
and keep the --no-render-check and value-flag handling behavior unchanged.
.ds-sync/lib/remote-diff.mjs (1)

51-59: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Argument-parsing boilerplate duplicated with resync.mjs.

This "unrecognized argument" flag-parsing block is near-identical to the one in resync.mjs (lines 67-76). Consider extracting a shared helper into lib/common.mjs (which already centralizes cross-script utilities like validateConfig) so future flag additions/fixes only need to happen once.

🤖 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 @.ds-sync/lib/remote-diff.mjs around lines 51 - 59, The argument-parsing
block in remote-diff.mjs duplicates the same unrecognized-argument logic used in
resync.mjs, so factor it into a shared helper in lib/common.mjs alongside
utilities like validateConfig. Update remote-diff.mjs to call that shared helper
for parsing/validation, and have resync.mjs use the same helper so future flag
changes only need one implementation.
src/components/hitl/approval-banner.tsx (1)

42-60: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Countdown/duration units are hardcoded, not localized.

formatMs/formatDuration emit raw "d", "h", "m", "s" suffixes directly, unlike every other user-facing string in this component which goes through t(...). This is the one piece of text that will render in English regardless of locale (Arabic, Japanese, etc.), even though the rest of the banner (including the adjacent "Paused" timestamp via Intl.DateTimeFormat) is properly localized.

♻️ Suggested approach
-const parts: [number, string][] = [[d, "d"], [h, "h"], [m, "m"], [s, "s"]];
+const parts: [number, string][] = [
+  [d, t("hitl.unit.day", "d")],
+  [h, t("hitl.unit.hour", "h")],
+  [m, t("hitl.unit.minute", "m")],
+  [s, t("hitl.unit.second", "s")],
+];

Note formatMs/formatDuration currently don't receive t; they'd need to accept it as a parameter or be called from within the component with access to useTranslation().

🤖 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/components/hitl/approval-banner.tsx` around lines 42 - 60, The duration
formatting in formatMs and formatDuration uses hardcoded unit suffixes, so
update these helpers to use the component’s translation function instead of raw
"d"/"h"/"m"/"s" text. Pass t from ApprovalBanner into formatDuration (and
formatMs if needed), then build the displayed string from localized unit labels
so all countdown text follows the active locale like the rest of the banner.
src/components/hitl/__tests__/approval-banner.test.tsx (1)

1-89: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

No coverage for the countdown/overdue rendering path.

The suite covers decision submission and per-task logic well, but getTimeRemaining/formatMs/the "Overdue" vs "Remaining" chip branch (component lines 106-199) has no test at all despite being new, timer-driven logic central to this PR's "timeout and countdown handling" objective. Consider adding a test with vi.useFakeTimers() asserting both the "Remaining: …" and "Overdue" states.

🤖 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/components/hitl/__tests__/approval-banner.test.tsx` around lines 1 - 89,
Add test coverage for the new countdown/overdue render path in ApprovalBanner,
since the suite currently only validates decisions and task granularity. In
src/components/hitl/__tests__/approval-banner.test.tsx, use vi.useFakeTimers()
around renderWithProviders(<ApprovalBanner ... />) to exercise the timer-driven
logic in ApprovalBanner and assert the chip switches from a “Remaining: …” state
(via getTimeRemaining/formatMs) to “Overdue” after advancing time. Include
checks that target the same rendered UI branch used by the component’s countdown
display so the new timeout handling is covered.
src/pages/group-detail.tsx (1)

311-335: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

New cancel <button> is nested inside the row's outer <button>.

The row at line 264 is itself a <button>; the delete button (pre-existing) and now this new cancel button are both rendered as children of it. Nested interactive elements are invalid per the HTML content model and can confuse assistive-tech role/announcement (a screen reader may announce ambiguous or duplicated button semantics), even though stopPropagation() keeps click behavior working for mouse users.

♻️ Suggested approach — make the row a non-button container with an explicit click handler, so action buttons are true siblings
-            <button
+            <div
+              role="button"
+              tabIndex={0}
               key={conv.id}
               onClick={() => handleSelectConversation(conv.id)}
+              onKeyDown={(e) => {
+                if (e.key === "Enter" || e.key === " ") handleSelectConversation(conv.id);
+              }}
               className={cn(...)}
               ...
             >
               ...
-            </button>
+            </div>
🤖 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/pages/group-detail.tsx` around lines 311 - 335, The row item in group
detail is using a button as the outer container while also rendering the delete
and cancel action buttons inside it, creating invalid nested interactive
elements. Update the row wrapper in group-detail.tsx so the clickable row is a
non-button container with an explicit click handler, and keep the action
controls in the same row as sibling buttons. Preserve the existing behavior in
handleDeleteConversation and handleCancelDiscussion, but ensure the new cancel
button and the existing trash button are no longer descendants of the row’s main
clickable control.
src/components/groups/__tests__/discussion-transcript.test.tsx (1)

183-189: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

No new test exercises the AWAITING_APPROVAL / ApprovalBanner path.

Both changed spots only add hitlPause/hitlResume/cancelInfo: null to existing mock objects for unrelated (IN_PROGRESS/FAILED) scenarios. Given this PR wires a whole new ApprovalBanner render branch, onApprove/onCancelDiscussion callbacks, and tasksAwaitingApproval derivation into this component, a test rendering DiscussionTranscript with streamState.state === "AWAITING_APPROVAL" (and/or a persisted conversation in that state) asserting the banner appears and onApprove/onCancelDiscussion fire correctly would meaningfully cover a new critical path.

Also applies to: 220-226

🤖 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/components/groups/__tests__/discussion-transcript.test.tsx` around lines
183 - 189, Add a test for the new AWAITING_APPROVAL flow in
DiscussionTranscript, since the current mock updates only cover unrelated
states. Extend src/components/groups/__tests__/discussion-transcript.test.tsx
with a case where streamState.state is AWAITING_APPROVAL (or the conversation is
persisted in that state), then assert ApprovalBanner renders and that the
onApprove and onCancelDiscussion handlers are invoked correctly. Use the
DiscussionTranscript component and the new tasksAwaitingApproval/ApprovalBanner
path as the main symbols to target.
src/pages/group-wizard.tsx (1)

852-864: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider auto-seeding a valid approval timeout on policy switch, like the agent-level config.

agent-config-sections.tsx's HitlConfigSection seeds approvalTimeout: "PT15M" when switching to a finite policy so the config never lands in an obviously-invalid state. Here, switching to a finite timeoutPolicy leaves approvalTimeout untouched (confirmed by the wizard test expecting Next to become disabled), forcing the user to manually type a duration before proceeding. Mirroring the agent-level seeding behavior would reduce friction and keep both HITL config UIs consistent.

🤖 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/pages/group-wizard.tsx` around lines 852 - 864, The HITL timeout policy
selector in group-wizard should seed a valid approval timeout when switching
from WAIT_INDEFINITELY to a finite policy, matching HitlConfigSection in
agent-config-sections.tsx. Update the onChange handling around patchHitl so that
choosing AUTO_APPROVE, AUTO_REJECT, or ABORT also sets approvalTimeout to a
sensible default (for example PT15M) when it is currently unset or indefinite,
keeping the wizard state valid and consistent with the agent-level config.
src/components/groups/group-config-panel.tsx (1)

207-215: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Use a shared formatter for approvalTimeout.
approvalTimeout is rendered as raw ISO-8601 text here, while the approval banner shows a formatted duration. Move that formatting into a shared HITL helper and reuse it in both places.

🤖 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/components/groups/group-config-panel.tsx` around lines 207 - 215, The
approval timeout is being rendered inconsistently in group-config-panel.tsx: the
`approvalTimeout` value is shown as raw ISO-8601 text here while the approval
banner uses a formatted duration. Extract the timeout formatting into a shared
HITL helper (for example alongside `timeoutPolicyLabel` or the existing approval
banner formatting logic) and update both this `InfoRow` in `group-config-panel`
and the banner to call that helper so they display the same formatted duration.
🤖 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 @.ds-sync/lib/bundle.mjs:
- Around line 133-144: The alias resolution in tsconfig-paths currently returns
as soon as existsSync finds a match, which can incorrectly accept directories
and block the /index.* fallback. Update the onResolve logic in bundle.mjs to
verify the candidate at stem + ext is a file before returning it, using
statSync(...).isFile() in the tsconfig-paths resolver so directory aliases like
`@/components` continue on to the index candidates.
- Around line 269-270: The evidence pass in bundle.mjs is incorrectly assuming
the first metafile output is the entry JS bundle, which can pick a CSS output
and leave exports empty. Update the logic around the metafile outputs lookup to
select the output associated with the entry point instead of using
Object.values(...)[0], then keep the __dsMainNs filter and export collection in
the same flow.

In @.ds-sync/package-validate.mjs:
- Around line 374-396: The .d.ts validation walk in the package-validate logic
is too brittle: it calls walkDts on join(OUT, 'components') without checking
that directory exists, so tokens-only syncs can throw and get mislabeled by the
catch. Update the walkDts invocation to follow the same existsSync guard pattern
used elsewhere, and in the TypeScript parse loop replace the internal
sf.parseDiagnostics access with a supported syntactic-diagnostics API on the ts
SourceFile/createSourceFile flow so the check remains stable across TypeScript
versions.
- Around line 413-434: The render-check setup in package-validate.mjs is failing
early when OUT/components does not exist, because collect() calls readdirSync on
that path before the guarded render-check flow starts. Add an existsSync check
for the components directory or move the whole setup into the existing try block
so tokens-only syncs skip render-check cleanly. Keep the fix centered around
collect(), serveDir(OUT), and mkdirSync(shotDir, ...) so missing components do
not abort validation.

In @.ds-sync/storybook/compare.mjs:
- Around line 437-444: The catch block in compare.mjs assumes the rejection
reason has a message property, which can crash if goto throws a null/undefined
value or a non-Error. Update the catch handling around dsPage.goto so the
non-timeout branch safely derives the error text from the caught value itself,
guarding access before using split; keep the existing timeout check and preserve
pageErr assignment logic in compare.mjs.

In `@package.json`:
- Line 11: The CONTRIBUTING.md CI-checks table is stale for the Type Check step
and still says tsc --noEmit, but the package.json typecheck script now uses tsc
-b. Update the documentation entry for the Type Check/CI-checks row to match the
current npm run typecheck behavior so contributors can debug failures against
the correct command.

In `@src/components/editors/agent-config-sections.tsx`:
- Around line 1098-1104: The timeout input draft in the HITL editor is only
synced from hitl.approvalTimeout, so it can stay stale when the finite
timeoutPolicy is toggled off and back on. Update the timeoutDraft state in
agent-config-sections.tsx so it also resyncs when
requiresApprovalTimeout(hitl.timeoutPolicy) changes, not just when
hitl.approvalTimeout changes, and make sure the policy-switch handling around
the HITL timeout field uses the same source of truth. Keep the draft reset
behavior aligned with the existing useEffect and the timeout field rendering
logic so the value shown after re-enabling a finite policy matches the persisted
approvalTimeout.

In `@src/hooks/use-group-discussion-stream.ts`:
- Around line 265-269: The task attribution in use-group-discussion-stream
should not rely on payload.displayName alone because duplicate display names can
map progress to the wrong speaker. Update the agentTask lookup in the group
discussion stream logic to match on a unique speaker identifier from the payload
and task plan, or add a disambiguating field alongside displayName, so the
matcher in the task attribution flow is unique and stable.

In `@src/pages/approvals.tsx`:
- Around line 32-33: The approvals page is only using `usePendingApprovals()`
for loading/error handling, so failures from `useAllGroupPendingApprovals()` are
ignored. Update `approvals.tsx` to destructure and combine the
`isLoading`/`isError` state from both hooks (`usePendingApprovals` and
`useAllGroupPendingApprovals`) and drive the page-level branches from the merged
state, while still rendering both `regular` and `groupPendings` data when
available. Ensure the main render logic in the approvals page reflects missing
group approvals as an error state instead of silently showing only regular
approvals.

---

Nitpick comments:
In @.ds-sync/lib/remote-diff.mjs:
- Around line 51-59: The argument-parsing block in remote-diff.mjs duplicates
the same unrecognized-argument logic used in resync.mjs, so factor it into a
shared helper in lib/common.mjs alongside utilities like validateConfig. Update
remote-diff.mjs to call that shared helper for parsing/validation, and have
resync.mjs use the same helper so future flag changes only need one
implementation.

In @.ds-sync/resync.mjs:
- Around line 67-76: The argument-parsing block in resync.mjs duplicates the
same unrecognized-argument loop used in remote-diff.mjs, so consolidate this
logic into the shared helper used by both scripts instead of keeping a second
copy in sync. Refactor the processing around argv, flag, and VALUE_FLAGS in this
file to call that shared parser/validator, and keep the --no-render-check and
value-flag handling behavior unchanged.

In `@src/components/groups/__tests__/discussion-transcript.test.tsx`:
- Around line 183-189: Add a test for the new AWAITING_APPROVAL flow in
DiscussionTranscript, since the current mock updates only cover unrelated
states. Extend src/components/groups/__tests__/discussion-transcript.test.tsx
with a case where streamState.state is AWAITING_APPROVAL (or the conversation is
persisted in that state), then assert ApprovalBanner renders and that the
onApprove and onCancelDiscussion handlers are invoked correctly. Use the
DiscussionTranscript component and the new tasksAwaitingApproval/ApprovalBanner
path as the main symbols to target.

In `@src/components/groups/group-config-panel.tsx`:
- Around line 207-215: The approval timeout is being rendered inconsistently in
group-config-panel.tsx: the `approvalTimeout` value is shown as raw ISO-8601
text here while the approval banner uses a formatted duration. Extract the
timeout formatting into a shared HITL helper (for example alongside
`timeoutPolicyLabel` or the existing approval banner formatting logic) and
update both this `InfoRow` in `group-config-panel` and the banner to call that
helper so they display the same formatted duration.

In `@src/components/hitl/__tests__/approval-banner.test.tsx`:
- Around line 1-89: Add test coverage for the new countdown/overdue render path
in ApprovalBanner, since the suite currently only validates decisions and task
granularity. In src/components/hitl/__tests__/approval-banner.test.tsx, use
vi.useFakeTimers() around renderWithProviders(<ApprovalBanner ... />) to
exercise the timer-driven logic in ApprovalBanner and assert the chip switches
from a “Remaining: …” state (via getTimeRemaining/formatMs) to “Overdue” after
advancing time. Include checks that target the same rendered UI branch used by
the component’s countdown display so the new timeout handling is covered.

In `@src/components/hitl/approval-banner.tsx`:
- Around line 42-60: The duration formatting in formatMs and formatDuration uses
hardcoded unit suffixes, so update these helpers to use the component’s
translation function instead of raw "d"/"h"/"m"/"s" text. Pass t from
ApprovalBanner into formatDuration (and formatMs if needed), then build the
displayed string from localized unit labels so all countdown text follows the
active locale like the rest of the banner.

In `@src/hooks/use-group-discussion-stream.ts`:
- Around line 41-65: The HITL state types in use-group-discussion-stream are
duplicated and too loose, so tighten them by extracting shared types for the
pause payload and using the existing HitlVerdict union for hitlResume instead of
string. Update the relevant types around hitlPause, hitlResume, and the
awaiting_approval event handling to reuse these shared symbols consistently, and
align any related casts or prop contracts such as
DiscussionTranscriptProps.onApprove so the HITL surface stays type-safe and
source-of-truth driven.

In `@src/lib/api/groups.ts`:
- Around line 214-222: The HITL pause fields in GroupConversation are too
loosely typed, especially hitlTimeoutPolicy and hitlApprovalTimeout. Update the
GroupConversation shape in groups.ts to reuse the stricter HITL types from
hitl.ts so consumers like group-wizard.tsx can rely on the same constrained
timeout policy values and ISO-8601 duration typing. Keep the existing field
names, but replace the plain string annotations with the shared union/type
definitions used by the HITL state model.
- Around line 504-551: The fetch-and-SSE setup is duplicated in
streamGroupDiscussion and streamGroupApproval, which can drift over time.
Extract the shared POST logic into a small helper such as postSSE that takes the
URL, request body, and optional signal, sets the Content-Type and auth headers
via api.getAuthHeader(), performs fetch, and returns the response for
readGroupSSE. Update both streamGroupDiscussion and streamGroupApproval to call
the helper so the request scaffolding lives in one place.

In `@src/pages/approvals.tsx`:
- Around line 151-157: The refresh button rendered in approvals.tsx via the
handleRefresh button currently has only an icon, so it needs an accessible name
for screen readers. Add an appropriate text alternative to the button itself in
the approvals page component (for example on the button using RefreshCw), and
make sure the control is still visually icon-only while exposing a clear label
to assistive tech.

In `@src/pages/group-detail.tsx`:
- Around line 311-335: The row item in group detail is using a button as the
outer container while also rendering the delete and cancel action buttons inside
it, creating invalid nested interactive elements. Update the row wrapper in
group-detail.tsx so the clickable row is a non-button container with an explicit
click handler, and keep the action controls in the same row as sibling buttons.
Preserve the existing behavior in handleDeleteConversation and
handleCancelDiscussion, but ensure the new cancel button and the existing trash
button are no longer descendants of the row’s main clickable control.

In `@src/pages/group-wizard.tsx`:
- Around line 852-864: The HITL timeout policy selector in group-wizard should
seed a valid approval timeout when switching from WAIT_INDEFINITELY to a finite
policy, matching HitlConfigSection in agent-config-sections.tsx. Update the
onChange handling around patchHitl so that choosing AUTO_APPROVE, AUTO_REJECT,
or ABORT also sets approvalTimeout to a sensible default (for example PT15M)
when it is currently unset or indefinite, keeping the wizard state valid and
consistent with the agent-level config.
🪄 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: defaults

Review profile: CHILL

Plan: Pro

Run ID: 29352b15-3f82-4c9c-b8a6-42f290635365

📥 Commits

Reviewing files that changed from the base of the PR and between 8c4fd86 and 7cb08e8.

⛔ Files ignored due to path filters (1)
  • .ds-sync/package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (68)
  • .ds-sync/lib/bundle.mjs
  • .ds-sync/lib/common.mjs
  • .ds-sync/lib/css-fallback.mjs
  • .ds-sync/lib/css.mjs
  • .ds-sync/lib/detect.mjs
  • .ds-sync/lib/docs.mjs
  • .ds-sync/lib/dts.mjs
  • .ds-sync/lib/emit.mjs
  • .ds-sync/lib/preview-gen-storybook.mjs
  • .ds-sync/lib/preview-rebuild.mjs
  • .ds-sync/lib/previews.mjs
  • .ds-sync/lib/remote-diff.mjs
  • .ds-sync/lib/source-kit.mjs
  • .ds-sync/lib/source-storybook.mjs
  • .ds-sync/lib/story-imports.mjs
  • .ds-sync/lib/sync-hashes.mjs
  • .ds-sync/package-build.mjs
  • .ds-sync/package-capture.mjs
  • .ds-sync/package-validate.mjs
  • .ds-sync/package.json
  • .ds-sync/resync.mjs
  • .ds-sync/storybook/SKILL.md
  • .ds-sync/storybook/compare.mjs
  • .ds-sync/storybook/http-serve.mjs
  • .ds-sync/storybook/probe.mjs
  • .gitignore
  • package.json
  • src/app.tsx
  • src/components/editors/__tests__/hitl-config-section.test.tsx
  • src/components/editors/agent-config-sections.tsx
  • src/components/groups/__tests__/discussion-transcript.test.tsx
  • src/components/groups/__tests__/task-board.test.tsx
  • src/components/groups/discussion-transcript.tsx
  • src/components/groups/group-config-panel.tsx
  • src/components/groups/task-board.tsx
  • src/components/hitl/__tests__/approval-banner.test.tsx
  • src/components/hitl/approval-banner.tsx
  • src/components/layout/sidebar.tsx
  • src/hooks/__tests__/use-group-discussion-stream.test.ts
  • src/hooks/__tests__/use-hitl.test.tsx
  • src/hooks/use-group-discussion-stream.ts
  • src/hooks/use-hitl.ts
  • src/i18n/locales/ar.json
  • src/i18n/locales/de.json
  • src/i18n/locales/en.json
  • src/i18n/locales/es.json
  • src/i18n/locales/fr.json
  • src/i18n/locales/hi.json
  • src/i18n/locales/ja.json
  • src/i18n/locales/ko.json
  • src/i18n/locales/pt.json
  • src/i18n/locales/th.json
  • src/i18n/locales/zh.json
  • src/lib/__tests__/hitl-config.test.ts
  • src/lib/api/agents.ts
  • src/lib/api/conversations.ts
  • src/lib/api/groups.ts
  • src/lib/api/hitl.ts
  • src/lib/hitl-config.ts
  • src/lib/hitl-labels.ts
  • src/pages/__tests__/group-wizard.test.tsx
  • src/pages/agent-detail.tsx
  • src/pages/approvals.tsx
  • src/pages/conversation-detail.tsx
  • src/pages/conversations.tsx
  • src/pages/group-detail.tsx
  • src/pages/group-wizard.tsx
  • src/test/mocks/handlers.ts

Comment thread .ds-sync/lib/bundle.mjs
Comment thread .ds-sync/lib/bundle.mjs
Comment thread .ds-sync/package-validate.mjs
Comment thread .ds-sync/package-validate.mjs
Comment thread .ds-sync/storybook/compare.mjs
Comment thread package.json
Comment thread src/components/editors/agent-config-sections.tsx
Comment thread src/hooks/use-group-discussion-stream.ts
Comment thread src/pages/approvals.tsx Outdated

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a complete Human-in-the-Loop (HITL) approval experience to EDDI-Manager, covering 1:1 conversations, group discussions (including approve/resume via SSE), and a unified pending-approvals inbox page, plus agent/group configuration UI and supporting API/hooks/mocks.

Changes:

  • Introduces HITL runtime UI: approval banners, new conversation state/filter, group pause/resume/cancel handling, and a cross-surface approvals queue (/manage/approvals).
  • Adds HITL configuration: agent-level timeout + pauseReason, and group-level approval points / policies / granularity behavior.
  • Expands supporting infrastructure: API bindings + hooks, MSW handlers, tests, i18n updates, and includes .ds-sync tooling plus a typecheck script fix.

Reviewed changes

Copilot reviewed 67 out of 69 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
src/test/mocks/handlers.ts Adds MSW coverage for HITL pending approvals, approval status, resume/cancel, and group approve/stream SSE.
src/pages/group-detail.tsx Wires group HITL approve/resume + cancel into the group detail streaming + transcript experience.
src/pages/conversations.tsx Adds AWAITING_HUMAN UI state icon and filter option in the conversations list.
src/pages/conversation-detail.tsx Renders HITL ApprovalBanner for 1:1 conversations and hooks up resume/cancel mutations.
src/pages/approvals.tsx New unified “Pending Approvals” inbox page (1:1 + cross-group).
src/pages/agent-detail.tsx Adds the HITL configuration section into the agent editor page.
src/pages/tests/group-wizard.test.tsx Adds group wizard tests for HITL gating, timeout validation, and granularity controls.
src/lib/hitl-labels.ts Centralizes localized labels for HITL enums (timeout policy, granularity, rejection).
src/lib/hitl-config.ts Adds preset phase materialization and ISO-8601 duration validation utilities for HITL config.
src/lib/api/hitl.ts Introduces HITL API DTOs and endpoint bindings (pending approvals, resume/cancel, group cancel, etc.).
src/lib/api/groups.ts Extends group conversation state/types and adds streamGroupApproval (approve/stream) support.
src/lib/api/conversations.ts Extends 1:1 conversation state/type to include AWAITING_HUMAN and HITL bookmark fields.
src/lib/api/agents.ts Adds agent-level hitlConfig typing to agent model.
src/lib/tests/hitl-config.test.ts Unit tests for phase materialization and ISO duration validation helpers.
src/i18n/locales/en.json Adds HITL/approvals strings and common strings used by the new UI.
src/i18n/locales/de.json Propagates HITL/approvals strings to German locale.
src/i18n/locales/fr.json Propagates HITL/approvals strings to French locale.
src/i18n/locales/es.json Propagates HITL/approvals strings to Spanish locale.
src/i18n/locales/ar.json Propagates HITL/approvals strings to Arabic locale.
src/i18n/locales/zh.json Propagates HITL/approvals strings to Chinese locale.
src/i18n/locales/th.json Propagates HITL/approvals strings to Thai locale.
src/i18n/locales/ja.json Propagates HITL/approvals strings to Japanese locale.
src/i18n/locales/ko.json Propagates HITL/approvals strings to Korean locale.
src/i18n/locales/pt.json Propagates HITL/approvals strings to Portuguese locale.
src/i18n/locales/hi.json Propagates HITL/approvals strings to Hindi locale.
src/hooks/use-hitl.ts Adds React Query hooks for pending approvals + resume/cancel (regular + group cancel).
src/hooks/use-group-discussion-stream.ts Adds HITL pause/resume/cancel SSE state handling and approve/stream support.
src/hooks/tests/use-hitl.test.tsx Adds tests for HITL hooks (pending approvals, resume, validation error surfacing).
src/hooks/tests/use-group-discussion-stream.test.ts Adds tests for HITL SSE transitions (awaiting_approval, hitl_resume, cancelled, error).
src/components/layout/sidebar.tsx Adds navigation entry for the new approvals page.
src/components/hitl/tests/approval-banner.test.tsx Adds unit tests for ApprovalBanner decisions, notes, and per-task approvals.
src/components/groups/task-board.tsx Adds an “Awaiting Approval” column and status bucketing for HITL TASK pauses.
src/components/groups/group-config-panel.tsx Adds read-only HITL summary block in group config panel.
src/components/groups/discussion-transcript.tsx Renders group HITL ApprovalBanner and supports per-task approvals + cancel actions.
src/components/groups/tests/task-board.test.tsx Adds coverage for awaiting-approval status outranking “completed”.
src/components/groups/tests/discussion-transcript.test.tsx Updates fixtures to include new HITL fields in stream state.
src/components/editors/agent-config-sections.tsx Implements the agent HITL config editor section (policy + timeout + pauseReason).
src/components/editors/tests/hitl-config-section.test.tsx Adds unit tests for the agent HITL config section behavior.
src/app.tsx Registers the /manage/approvals route.
package.json Fixes typecheck script to run project build checks (tsc -b).
.gitignore Ignores generated ds-bundle/ output from .ds-sync tooling.
.ds-sync/storybook/probe.mjs Adds storybook provider-chain probing helper for design-system sync tooling.
.ds-sync/storybook/http-serve.mjs Adds a minimal static file server for .ds-sync storybook utilities.
.ds-sync/package.json Adds .ds-sync tool dependencies (esbuild, ts-morph, etc.).
.ds-sync/lib/sync-hashes.mjs Adds hashing/recipe logic used by design-system sync pipeline.
.ds-sync/lib/source-kit.mjs Adds non-storybook package adapter/resolution logic for design-system sync.
.ds-sync/lib/previews.mjs Adds preview generation and compilation logic for design-system sync.
.ds-sync/lib/preview-gen-storybook.mjs Adds storybook preview wrapper generator for design-system sync.
.ds-sync/lib/detect.mjs Adds storybook/package shape detection for design-system sync.
.ds-sync/lib/css.mjs Adds CSS/token/font extraction and styles.css writer for design-system sync.
.ds-sync/lib/css-fallback.mjs Adds storybook-based CSS and font fallbacks for design-system sync.
.ds-sync/lib/common.mjs Adds shared helpers/config validation/error remedies for design-system sync.
Files not reviewed (1)
  • .ds-sync/package-lock.json: Generated file

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/components/groups/task-board.tsx
Comment thread src/lib/api/hitl.ts
ginccc added 3 commits July 3, 2026 11:27
- Add the missing taskBoard.awaitingApproval i18n key (was falling back to
  English in every locale) across all 11 locales.
- Surface a non-blocking warning when the cross-group approvals inbox fails to
  load instead of silently showing only 1:1 approvals; give the refresh button an
  accessible name.
- Resync the agent approval-timeout draft when the field is re-shown after a
  policy toggle; seed a default timeout on finite-policy switch in the group
  wizard too (consistent with the agent editor).
- Allow null on nullable PendingApprovalSummary fields; tighten GroupConversation
  HITL field types to the shared unions.
- Centralise ISO-8601 duration formatting (formatIsoDuration/formatDurationMs) and
  reuse it in the banner and group config panel; DRY the two SSE POST calls.
- Update CONTRIBUTING.md (typecheck is now tsc -b).

Tests: countdown/overdue timer, the AWAITING_APPROVAL banner + decision
callbacks, and the updated wizard finite-policy seeding.
The backend emits approve/stream resume rejections (409 concurrent
decision, 400 invalid taskApprovals/note) as "group_error", never a
bare "error" — a raw "error" name collides with the browser
EventSource transport-error event (EDDI #36).

- Remove the dead "error" SSE union member and switch case, and correct
  the now-false comments to reflect that group_error carries these
  rejections.
- Retarget the rejection test to emit the real "group_error" event.
- Classify config-drift aborts via errorKind so the UI can guide the
  user to fix the config and re-approve instead of treating the pause
  as terminal.
The previous commit made errorKind a required field on GroupStreamState,
but the DiscussionTranscript fixtures that construct that type were in a
separate uncommitted change — so the committed tree failed `tsc -b` in CI
(TS2741) even though the local working tree passed. Add errorKind to both
fixtures to restore the typecheck.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
src/pages/group-wizard.tsx (1)

186-196: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Reselecting a template leaves stale approvalPhases referencing a different style's phase names.

selectTemplate updates style/maxRounds but doesn't reset approvalPhases (unlike the style-card handler at Line 676 and the max-rounds handler at Lines 715-724, which both prune/reset phase selections on style/rounds changes). Since the step navigator lets users jump back to the template step from any later step, a user can enable HITL, pick approval phases for template A, go back, and pick template B (different style). isHitlConfigValid only checks approvalPhases.length, not name validity, so it still passes — but none of the stale names will match template B's phases, and applyApprovalPhases silently gates nothing, producing a group where HITL looks enabled but never pauses.

🐛 Proposed fix
   function selectTemplate(tmpl: GroupTemplate) {
     update({
       name: tmpl.name,
       description: tmpl.description,
       style: tmpl.style,
       maxRounds: tmpl.maxRounds,
       members: tmpl.roles.map((r, i) => createEmptySlot(i, r.displayName, r.role)),
       moderator: tmpl.moderatorSuggested ? createModeratorSlot() : null,
+      approvalPhases: [],
     });
     setCurrentStep(1);
   }
🤖 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/pages/group-wizard.tsx` around lines 186 - 196, selectTemplate currently
updates the template’s name/style/maxRounds/members but leaves approvalPhases
untouched, so stale phase names can survive when switching templates. Update the
selectTemplate handler to reset or prune approvalPhases based on the newly
selected template’s style, matching the cleanup behavior used in the style-card
and max-rounds handlers. Use the existing selectTemplate function, the
approvalPhases state, and the same phase-mapping logic already used elsewhere in
group-wizard.tsx to ensure HITL selections stay consistent after reselecting a
template.
src/components/groups/__tests__/discussion-transcript.test.tsx (1)

213-215: 🎯 Functional Correctness | 🔴 Critical | ⚡ Quick win

Missing errorKind breaks the build (TS2741).

Both mockStreamState and mockStreamStateWithError fail type-checking per CI: errorKind is required on GroupStreamState but is not set on either mock, causing tsc -b to fail the build.

🐛 Proposed fix
       tasksInProgress: new Set(),
       tasksCompleted: new Set(),
+      errorKind: null,
       hitlPause: null,
       hitlResume: null,
       cancelInfo: null,
     };

Apply the same addition to mockStreamStateWithError at line 250.

Also applies to: 250-252

🤖 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/components/groups/__tests__/discussion-transcript.test.tsx` around lines
213 - 215, Both `mockStreamState` and `mockStreamStateWithError` are missing the
required `errorKind` field from `GroupStreamState`, which is causing the
TypeScript build failure. Add `errorKind` to each mock object in
`discussion-transcript.test.tsx`, matching the existing mock shape and any
error-specific value already used by the test data so both assignments satisfy
the type.

Source: Pipeline failures

🤖 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.

Outside diff comments:
In `@src/components/groups/__tests__/discussion-transcript.test.tsx`:
- Around line 213-215: Both `mockStreamState` and `mockStreamStateWithError` are
missing the required `errorKind` field from `GroupStreamState`, which is causing
the TypeScript build failure. Add `errorKind` to each mock object in
`discussion-transcript.test.tsx`, matching the existing mock shape and any
error-specific value already used by the test data so both assignments satisfy
the type.

In `@src/pages/group-wizard.tsx`:
- Around line 186-196: selectTemplate currently updates the template’s
name/style/maxRounds/members but leaves approvalPhases untouched, so stale phase
names can survive when switching templates. Update the selectTemplate handler to
reset or prune approvalPhases based on the newly selected template’s style,
matching the cleanup behavior used in the style-card and max-rounds handlers.
Use the existing selectTemplate function, the approvalPhases state, and the same
phase-mapping logic already used elsewhere in group-wizard.tsx to ensure HITL
selections stay consistent after reselecting a template.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 49f42def-3906-4338-b597-a1842c833b19

📥 Commits

Reviewing files that changed from the base of the PR and between 7cb08e8 and 0ac78ae.

📒 Files selected for processing (25)
  • CONTRIBUTING.md
  • src/components/editors/agent-config-sections.tsx
  • src/components/groups/__tests__/discussion-transcript.test.tsx
  • src/components/groups/group-config-panel.tsx
  • src/components/hitl/__tests__/approval-banner.test.tsx
  • src/components/hitl/approval-banner.tsx
  • src/hooks/__tests__/use-group-discussion-stream.test.ts
  • src/hooks/use-group-discussion-stream.ts
  • src/i18n/locales/ar.json
  • src/i18n/locales/de.json
  • src/i18n/locales/en.json
  • src/i18n/locales/es.json
  • src/i18n/locales/fr.json
  • src/i18n/locales/hi.json
  • src/i18n/locales/ja.json
  • src/i18n/locales/ko.json
  • src/i18n/locales/pt.json
  • src/i18n/locales/th.json
  • src/i18n/locales/zh.json
  • src/lib/api/groups.ts
  • src/lib/api/hitl.ts
  • src/lib/hitl-config.ts
  • src/pages/__tests__/group-wizard.test.tsx
  • src/pages/approvals.tsx
  • src/pages/group-wizard.tsx
✅ Files skipped from review due to trivial changes (5)
  • src/i18n/locales/en.json
  • src/i18n/locales/zh.json
  • src/i18n/locales/de.json
  • src/i18n/locales/ar.json
  • src/i18n/locales/es.json
🚧 Files skipped from review as they are similar to previous changes (14)
  • src/pages/tests/group-wizard.test.tsx
  • src/hooks/tests/use-group-discussion-stream.test.ts
  • src/components/groups/group-config-panel.tsx
  • src/lib/api/hitl.ts
  • src/pages/approvals.tsx
  • src/i18n/locales/hi.json
  • src/components/editors/agent-config-sections.tsx
  • src/components/hitl/approval-banner.tsx
  • src/i18n/locales/fr.json
  • src/i18n/locales/ko.json
  • src/i18n/locales/ja.json
  • src/i18n/locales/th.json
  • src/hooks/use-group-discussion-stream.ts
  • src/i18n/locales/pt.json

ginccc added 8 commits July 6, 2026 20:16
Reselecting a template updated style/maxRounds but left approvalPhases
referencing the previous style's phase names. Since users can jump back
to the template step, this produced a group where HITL looked enabled
but never paused (stale names matched no phases). Mirror the reset
already done by the style-card and max-rounds handlers.
…gating, add validator mirror

Extend the HITL API contract for the tool-call approval surface: per-callId
toolDecisions on HitlDecision, pauseType/toolNames on PendingApprovalSummary,
and the structured approval-status pauseDetails types (TOOL_CALL/RULE) with
their field-limit constants. Add hitl-tool-approvals.ts, a client-side mirror
of the backend ToolApprovalPatterns/HitlConfigValidation save-time validators
so a bad tool-approvals config is caught inline instead of bouncing off a
400. Add localized labels for the new onNoProgress/rejection-policy enums.
Add ToolApprovalsEditor — the config-driven tool-call approval gate UI
(requireApproval/exempt patterns, pause limits, timeout override, group-turn
behavior) — wired into the agent-level HITL config and, via
task-tool-approvals-section, as a per-LLM-task full-replace override. Live
validation against the client-side ToolApprovalPatterns/HitlConfigValidation
mirror surfaces save-time errors inline, and warns when an agent-level
AUTO_APPROVE would silently demote a tool pause to WAIT_INDEFINITELY. Fixes
a stale-draft glitch where toggling the timeout policy off and back on could
leave an invalid unsaved approval-timeout value displayed.
Add GroupHitlEditor — an inline editor for a group's HITL settings
(timeout policy/duration, per-phase requiresApproval, and, for TASK_FORCE
groups, granularity/on-rejection), wired into the group config panel and
detail page. Preset-style groups store phases:null; saving materializes the
phase list the same way the create wizard does. Blocks save when approval
is enabled but no phase is selected, since that would silently produce a
group that looks gated but never pauses. Keys the panel on groupId so
switching groups remounts the editor instead of reusing stale in-progress
edits from the previous group.
…ails loading state

Extend ApprovalBanner for a TOOL_CALL pause: per-call approve/reject toggles
(unset = inherit the top-level verdict), an optional argument-amendment
editor with client-side JSON/size/truncated-arg validation before submit,
and surfacing of executedUngatedCalls / outcomeUnknown (crash-recovery)
warnings. Rejecting the batch is always all-or-nothing, since a per-call
APPROVED mixed with a top-level REJECTED is contradictory and the backend
400s it. Add a pauseDetailsPending flag that disables Approve (not
Reject/Cancel) while the structured pause-status fetch is still in flight,
so a reviewer can't blind-approve a gated tool call before its details
(RULE vs TOOL_CALL) are known.
…eam/load paths

Detect an AWAITING_HUMAN commit on the non-streaming send, streaming done,
and conversation-load paths, disabling the input and quick-reply pills and
showing the paused banner with the backend's pauseReason. Previously the
quick-reply pills stayed clickable while paused (the one send affordance the
disabled-input guard missed) and a conversation loaded from history always
showed the generic default banner text instead of its real reason, since the
load path discarded the snapshot's hitlPauseReason. Also drop the trailing
empty streaming placeholder message on a 409 (paused) rejection so no
perpetual typing indicator lingers under the pause banner.
…chedule pages

Merge regular (1:1) and cross-group pending approvals into one inbox queue,
badging TOOL_CALL pauses (routed to Review for detailed per-call inspection,
never a blind inline quick-approve) versus RULE pauses (inline
Approve/Reject). Wait for both the regular and cross-group queries before
leaving the loading state, so an empty regular list can't flash "No pending
approvals" before slower-loading group items arrive. Surface decidedBy/
automated (system:timeout, system:retention) on the audit page, wire the
per-channel HITL approval-routing fields on channel-detail, and the group
approval-timeout field on schedules.
Propagate the HITL feature's en.json keys (tool-approval editor labels,
per-tool-call approval strings, group Human Approval editor, channel/audit/
schedule wiring) to de, es, fr, pt, ja, ko, zh, ar, hi, th, per the
project's mandatory i18n rule.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 85 out of 87 changed files in this pull request and generated 1 comment.

Files not reviewed (1)
  • .ds-sync/package-lock.json: Generated file

Comment thread src/hooks/use-hitl.ts

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 7

🧹 Nitpick comments (4)
src/components/groups/group-hitl-editor.tsx (1)

74-93: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Stale timeoutDraft persists after switching away from a finite policy.

Switching timeoutPolicy to e.g. AUTO_REJECT seeds timeoutDraft to "PT15M" (lines 161-166), but switching back to WAIT_INDEFINITELY/AUTO_APPROVE never clears it. On save, line 81 still sends that leftover approvalTimeout even though it's no longer needed/shown.

♻️ Proposed fix
-              onChange={(e) => {
+              onChange={(e) => {
                 const policy = e.target.value as GroupHitlConfig["timeoutPolicy"];
                 patchHitl({ timeoutPolicy: policy });
                 if (
                   requiresApprovalTimeout(policy) &&
                   !(timeoutDraft && isValidIsoDuration(timeoutDraft))
                 ) {
                   setTimeoutDraft("PT15M");
+                } else if (!requiresApprovalTimeout(policy)) {
+                  setTimeoutDraft("");
                 }
               }}

Also applies to: 155-176

🤖 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/components/groups/group-hitl-editor.tsx` around lines 74 - 93, The save
flow in group-hitl-editor’s save function is still using a stale timeoutDraft
value after switching away from a finite timeout policy. Update the
timeoutPolicy handling in the component’s state logic so that when the policy
changes to WAIT_INDEFINITELY or AUTO_APPROVE, timeoutDraft is cleared or
ignored, and ensure save only includes approvalTimeout when the selected policy
actually requires it. Use the existing timeoutPolicy setter logic and the save()
path that builds GroupHitlConfig to keep the draft and persisted config in sync.
src/components/editors/agent-config-sections.tsx (1)

1149-1149: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Use the shared MAX_PAUSE_REASON_LENGTH constant instead of a hardcoded 500.

tool-approvals-editor.tsx already imports MAX_PAUSE_REASON_LENGTH from @/lib/api/hitl for the identical constraint. Hardcoding 500 here duplicates that value and can silently drift if the backend limit changes.

♻️ Proposed fix
+import { MAX_PAUSE_REASON_LENGTH } from "`@/lib/api/hitl`";
...
-                maxLength={500}
+                maxLength={MAX_PAUSE_REASON_LENGTH}
🤖 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/components/editors/agent-config-sections.tsx` at line 1149, Replace the
hardcoded maxLength value in the pause reason/editor input within
agent-config-sections.tsx with the shared MAX_PAUSE_REASON_LENGTH constant,
matching the pattern already used in tool-approvals-editor.tsx. Locate the
relevant input by the maxLength={500} prop and update it to reference
MAX_PAUSE_REASON_LENGTH from `@/lib/api/hitl` so both editors stay aligned with
the same backend limit.
src/pages/group-detail.tsx (1)

323-336: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Cancel button disables list-wide during any single cancel.

disabled={cancelDiscussionMutation.isPending} is driven by one shared mutation instance, so cancelling conversation A also disables the cancel button on every other unrelated conversation row until it settles. Minor UX nit given how infrequent/fast cancels are.

🤖 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/pages/group-detail.tsx` around lines 323 - 336, The cancel action in
group-detail’s conversation row is using a shared pending flag, which disables
every cancel button at once. Update the button logic around the conversation
item render so each row only disables its own cancel control based on that row’s
in-flight state, using the existing handleCancelDiscussion and
cancelDiscussionMutation setup as the reference points. Keep the mutation scoped
to the clicked conversation so unrelated rows remain interactive while one
cancel is pending.
src/components/hitl/approval-banner.tsx (1)

592-602: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Use the amended-arguments limit for maxLength. MAX_TOOL_CALL_NOTE_LENGTH * 40 lets the textarea accept more input than AMENDED_ARGS_MAX_BYTES allows at submit time, so the rejection only appears later.

🤖 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/components/hitl/approval-banner.tsx` around lines 592 - 602, The amend
textarea in approval-banner.tsx is using a length cap that exceeds the actual
amended-arguments limit, so align its maxLength with the same limit enforced at
submit time. Update the textarea in the showAmend block to use the
amended-arguments constant already used by the validation logic, keeping the
behavior consistent with onAmendChange and the tool-call amend flow.
🤖 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 `@src/components/editors/tool-approvals-editor.tsx`:
- Around line 136-171: The numeric cap inputs in ToolApprovalsEditor are
committing on every keystroke, unlike the other fields in this component, which
can trigger stale-version mutation races. Update the `maxPausesPerTurn` and
`maxAutoApprovalsPerTurn` inputs to follow the same local draft state plus
`onBlur` commit pattern used by `requireApproval`, `exempt`, `pauseReason`,
`pendingMessage`, and `approvalTimeout`. Keep the input value bound to local
state while typing, then call the existing `onChange`/`patchHitl` flow only when
the field blurs so `updateAgent.mutate` is invoked once per edit instead of on
every keypress.

In `@src/hooks/use-chat.ts`:
- Around line 422-443: The 409 handling in use-chat’s onError only removes the
empty agent streaming placeholder, so the optimistic user message remains even
though the request was rejected. Update the 409 branch to also remove the last
user turn from store.setState when the send was not consumed, while preserving
the existing paused state updates. Keep the fix localized to onError in use-chat
and make sure the transcript state is rolled back consistently before calling
setPaused, setProcessing, and setQuickReplies.
- Line 75: The chat pause state is not being cleared when resuming or cancelling
a conversation, so `useChatStore` can keep stale `isPaused`/`pauseReason`
values. Update `useResumeConversation` and `useCancelConversation` to reset the
store in their success path by calling the relevant `useChatStore` setter (for
example the `setPaused` action) or by refetching the active conversation after
the mutation completes. Make sure the fix is applied in the hooks that manage
resume/cancel flow so `ChatPanel` reflects the current state immediately.

In `@src/i18n/locales/hi.json`:
- Line 997: The locale entry for allSigned is still in English, so update the
hi.json translation to match the surrounding localized keys. Use the same i18n
JSON structure and replace the allSigned value with the proper Hindi
translation, keeping the key consistent with nearby sibling entries.

In `@src/i18n/locales/ja.json`:
- Line 997: The new allSigned entry in the Japanese locale is still in English;
update the string in the ja.json translation map alongside the nearby decidedBy
and automated keys so it matches the rest of the Japanese localization. Use the
existing locale entries in that object to locate and replace the untranslated
value.

In `@src/i18n/locales/th.json`:
- Around line 997-999: The Thai locale is missing a translation for the
audit.allSigned key, so update the th.json entry that currently uses English to
a proper Thai string. Make the change alongside the matching audit keys in the
locale JSON group, using the existing sibling keys like decidedBy and automated
in th.json as the location reference, and ensure the translated value stays
consistent with the corresponding key in en.json and the other locale files.

In `@src/i18n/locales/zh.json`:
- Around line 997-999: The `audit.allSigned` entry in the Chinese locale is
still untranslated, so update the `allSigned` value in
`src/i18n/locales/zh.json` to the proper Chinese text to match the surrounding
`decidedBy` and `automated` keys. Verify the `allSigned` key is consistently
localized alongside the other `audit` strings in the locale JSON files,
following the same translation pattern used in `src/i18n/locales/*.json`.

---

Nitpick comments:
In `@src/components/editors/agent-config-sections.tsx`:
- Line 1149: Replace the hardcoded maxLength value in the pause reason/editor
input within agent-config-sections.tsx with the shared MAX_PAUSE_REASON_LENGTH
constant, matching the pattern already used in tool-approvals-editor.tsx. Locate
the relevant input by the maxLength={500} prop and update it to reference
MAX_PAUSE_REASON_LENGTH from `@/lib/api/hitl` so both editors stay aligned with
the same backend limit.

In `@src/components/groups/group-hitl-editor.tsx`:
- Around line 74-93: The save flow in group-hitl-editor’s save function is still
using a stale timeoutDraft value after switching away from a finite timeout
policy. Update the timeoutPolicy handling in the component’s state logic so that
when the policy changes to WAIT_INDEFINITELY or AUTO_APPROVE, timeoutDraft is
cleared or ignored, and ensure save only includes approvalTimeout when the
selected policy actually requires it. Use the existing timeoutPolicy setter
logic and the save() path that builds GroupHitlConfig to keep the draft and
persisted config in sync.

In `@src/components/hitl/approval-banner.tsx`:
- Around line 592-602: The amend textarea in approval-banner.tsx is using a
length cap that exceeds the actual amended-arguments limit, so align its
maxLength with the same limit enforced at submit time. Update the textarea in
the showAmend block to use the amended-arguments constant already used by the
validation logic, keeping the behavior consistent with onAmendChange and the
tool-call amend flow.

In `@src/pages/group-detail.tsx`:
- Around line 323-336: The cancel action in group-detail’s conversation row is
using a shared pending flag, which disables every cancel button at once. Update
the button logic around the conversation item render so each row only disables
its own cancel control based on that row’s in-flight state, using the existing
handleCancelDiscussion and cancelDiscussionMutation setup as the reference
points. Keep the mutation scoped to the clicked conversation so unrelated rows
remain interactive while one cancel is pending.
🪄 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: defaults

Review profile: CHILL

Plan: Pro

Run ID: 6e600ad2-cefc-4495-9499-828858540427

📥 Commits

Reviewing files that changed from the base of the PR and between 0ac78ae and cb57d92.

📒 Files selected for processing (41)
  • src/components/chat/chat-panel.tsx
  • src/components/editors/__tests__/hitl-config-section.test.tsx
  • src/components/editors/__tests__/tool-approvals-editor.test.tsx
  • src/components/editors/agent-config-sections.tsx
  • src/components/editors/llm-editor.tsx
  • src/components/editors/llm/task-tool-approvals-section.tsx
  • src/components/editors/llm/types.ts
  • src/components/editors/tool-approvals-editor.tsx
  • src/components/groups/__tests__/discussion-transcript.test.tsx
  • src/components/groups/__tests__/group-hitl-editor.test.tsx
  • src/components/groups/group-config-panel.tsx
  • src/components/groups/group-hitl-editor.tsx
  • src/components/hitl/__tests__/approval-banner.test.tsx
  • src/components/hitl/approval-banner.tsx
  • src/hooks/__tests__/use-chat-load-pause.test.tsx
  • src/hooks/use-chat.ts
  • src/hooks/use-hitl.ts
  • src/i18n/locales/ar.json
  • src/i18n/locales/de.json
  • src/i18n/locales/en.json
  • src/i18n/locales/es.json
  • src/i18n/locales/fr.json
  • src/i18n/locales/hi.json
  • src/i18n/locales/ja.json
  • src/i18n/locales/ko.json
  • src/i18n/locales/pt.json
  • src/i18n/locales/th.json
  • src/i18n/locales/zh.json
  • src/lib/__tests__/hitl-tool-approvals.test.ts
  • src/lib/api/hitl.ts
  • src/lib/hitl-labels.ts
  • src/lib/hitl-tool-approvals.ts
  • src/pages/__tests__/approvals.test.tsx
  • src/pages/__tests__/chat-pause.test.tsx
  • src/pages/approvals.tsx
  • src/pages/audit.tsx
  • src/pages/channel-detail.tsx
  • src/pages/conversation-detail.tsx
  • src/pages/group-detail.tsx
  • src/pages/group-wizard.tsx
  • src/pages/schedules.tsx
✅ Files skipped from review due to trivial changes (4)
  • src/i18n/locales/ar.json
  • src/i18n/locales/es.json
  • src/i18n/locales/de.json
  • src/i18n/locales/pt.json
🚧 Files skipped from review as they are similar to previous changes (7)
  • src/components/groups/tests/discussion-transcript.test.tsx
  • src/lib/hitl-labels.ts
  • src/pages/conversation-detail.tsx
  • src/pages/approvals.tsx
  • src/pages/group-wizard.tsx
  • src/i18n/locales/fr.json
  • src/i18n/locales/ko.json

Comment thread src/components/editors/tool-approvals-editor.tsx
Comment thread src/hooks/use-chat.ts
Comment thread src/hooks/use-chat.ts
Comment thread src/i18n/locales/hi.json
Comment thread src/i18n/locales/ja.json
Comment thread src/i18n/locales/th.json
Comment thread src/i18n/locales/zh.json
ginccc added 2 commits July 7, 2026 00:57
# Conflicts:
#	.gitignore
#	src/components/chat/chat-panel.tsx
#	src/hooks/use-chat.ts
- tool-approvals editor: commit the numeric caps (maxPausesPerTurn /
  maxAutoApprovalsPerTurn) on blur via local draft state, matching every
  sibling field. Committing on each keystroke fired multiple agent
  mutations with the same stale version, racing the optimistic-concurrency
  check and silently dropping edits.
- chat 409: remove the optimistic user message (not just the empty
  placeholder) when a send is rejected as paused — the backend never
  consumed it, so leaving it looked sent-and-received.
- chat pause: clear the app-wide chat store's pause banner when the open
  conversation is resumed/cancelled from the detail page or inbox; the
  persistent chat drawer/panel doesn't reload on its own.
- group HITL editor: don't persist a leftover approvalTimeout after
  switching back to a non-finite timeout policy.
- use MAX_PAUSE_REASON_LENGTH (not a hardcoded 500) for the agent-level
  pause-reason cap; cap the amend textarea at AMENDED_ARGS_MAX_BYTES.
- document that the cross-group inbox `truncated` flag is a client-side
  heuristic (the backend returns a plain list with no truncation signal).

Adds regression tests for the numeric-cap blur commit, the 409
message removal, and the resume-clears-pause behavior.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
src/hooks/__tests__/use-hitl.test.tsx (1)

71-103: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add cancel-path coverage too. useCancelConversation also calls clearChatPauseIfCurrent, so mirror these same/different conversation cases there as well.

🤖 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/hooks/__tests__/use-hitl.test.tsx` around lines 71 - 103, The new
coverage only exercises `useResumeConversation`, but `useCancelConversation`
uses the same `clearChatPauseIfCurrent` behavior and needs the same protection.
Add mirrored tests alongside the existing ones for `useCancelConversation`,
covering both the matching conversation case where `useChatStore.isPaused`
should be cleared and the different conversation case where the pause banner
must remain unchanged. Use the existing `useChatStore`, `makeWrapper`, and
`mutateAsync` test pattern to keep the scenarios aligned.
🤖 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.

Nitpick comments:
In `@src/hooks/__tests__/use-hitl.test.tsx`:
- Around line 71-103: The new coverage only exercises `useResumeConversation`,
but `useCancelConversation` uses the same `clearChatPauseIfCurrent` behavior and
needs the same protection. Add mirrored tests alongside the existing ones for
`useCancelConversation`, covering both the matching conversation case where
`useChatStore.isPaused` should be cleared and the different conversation case
where the pause banner must remain unchanged. Use the existing `useChatStore`,
`makeWrapper`, and `mutateAsync` test pattern to keep the scenarios aligned.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 82ec2577-8e56-4c13-bc5a-3cebfab5f80a

📥 Commits

Reviewing files that changed from the base of the PR and between cb57d92 and 900d266.

📒 Files selected for processing (24)
  • src/components/chat/chat-panel.tsx
  • src/components/editors/__tests__/tool-approvals-editor.test.tsx
  • src/components/editors/agent-config-sections.tsx
  • src/components/editors/llm-editor.tsx
  • src/components/editors/llm/types.ts
  • src/components/editors/tool-approvals-editor.tsx
  • src/components/groups/group-hitl-editor.tsx
  • src/components/hitl/approval-banner.tsx
  • src/hooks/__tests__/use-chat.test.tsx
  • src/hooks/__tests__/use-hitl.test.tsx
  • src/hooks/use-chat.ts
  • src/hooks/use-hitl.ts
  • src/i18n/locales/ar.json
  • src/i18n/locales/de.json
  • src/i18n/locales/en.json
  • src/i18n/locales/es.json
  • src/i18n/locales/fr.json
  • src/i18n/locales/hi.json
  • src/i18n/locales/ja.json
  • src/i18n/locales/ko.json
  • src/i18n/locales/pt.json
  • src/i18n/locales/th.json
  • src/i18n/locales/zh.json
  • src/test/mocks/handlers.ts
✅ Files skipped from review due to trivial changes (6)
  • src/i18n/locales/ko.json
  • src/i18n/locales/th.json
  • src/i18n/locales/de.json
  • src/i18n/locales/hi.json
  • src/i18n/locales/pt.json
  • src/i18n/locales/zh.json
🚧 Files skipped from review as they are similar to previous changes (15)
  • src/components/editors/llm-editor.tsx
  • src/components/editors/llm/types.ts
  • src/components/editors/tests/tool-approvals-editor.test.tsx
  • src/components/chat/chat-panel.tsx
  • src/test/mocks/handlers.ts
  • src/hooks/use-hitl.ts
  • src/components/groups/group-hitl-editor.tsx
  • src/i18n/locales/es.json
  • src/hooks/use-chat.ts
  • src/components/editors/tool-approvals-editor.tsx
  • src/components/editors/agent-config-sections.tsx
  • src/i18n/locales/en.json
  • src/i18n/locales/ar.json
  • src/components/hitl/approval-banner.tsx
  • src/i18n/locales/fr.json

@ginccc
ginccc merged commit 4e4bbdd into main Jul 6, 2026
4 checks passed
@ginccc
ginccc deleted the feat/hitl-ui branch July 6, 2026 23:37
This was referenced Jul 21, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants