2026-03-14 07:13:59 -05:00
|
|
|
import { useState, useEffect, useRef, useCallback, useMemo, type ChangeEvent, type DragEvent } from "react";
|
2026-02-17 12:24:48 -06:00
|
|
|
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
2026-03-23 07:48:50 -05:00
|
|
|
import { pickTextColorForSolidBg } from "@/lib/color-contrast";
|
Add detail pages, property panels, and restyle list pages
New pages: AgentDetail, GoalDetail, IssueDetail, ProjectDetail, Inbox,
MyIssues. New feature components: AgentProperties, GoalProperties,
IssueProperties, ProjectProperties, GoalTree, NewIssueDialog. Add
heartbeats API client. Restyle all list pages (Agents, Issues, Goals,
Projects, Dashboard, Costs, Activity, Org) with EntityRow, FilterBar,
and improved layouts. Add routing for detail views.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 09:57:06 -06:00
|
|
|
import { useDialog } from "../context/DialogContext";
|
|
|
|
|
import { useCompany } from "../context/CompanyContext";
|
2026-03-13 17:12:25 -05:00
|
|
|
import { executionWorkspacesApi } from "../api/execution-workspaces";
|
Add detail pages, property panels, and restyle list pages
New pages: AgentDetail, GoalDetail, IssueDetail, ProjectDetail, Inbox,
MyIssues. New feature components: AgentProperties, GoalProperties,
IssueProperties, ProjectProperties, GoalTree, NewIssueDialog. Add
heartbeats API client. Restyle all list pages (Agents, Issues, Goals,
Projects, Dashboard, Costs, Activity, Org) with EntityRow, FilterBar,
and improved layouts. Add routing for detail views.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 09:57:06 -06:00
|
|
|
import { issuesApi } from "../api/issues";
|
2026-03-17 09:24:28 -05:00
|
|
|
import { instanceSettingsApi } from "../api/instanceSettings";
|
2026-02-17 10:53:20 -06:00
|
|
|
import { projectsApi } from "../api/projects";
|
2026-02-17 12:24:48 -06:00
|
|
|
import { agentsApi } from "../api/agents";
|
feat: implement multi-user access and invite flows (#3784)
## Thinking Path
> - Paperclip is the control plane for autonomous AI companies.
> - V1 needs to stay local-first while also supporting shared,
authenticated deployments.
> - Human operators need real identities, company membership, invite
flows, profile surfaces, and company-scoped access controls.
> - Agents and operators also need the existing issue, inbox, workspace,
approval, and plugin flows to keep working under those authenticated
boundaries.
> - This branch accumulated the multi-user implementation, follow-up QA
fixes, workspace/runtime refinements, invite UX improvements,
release-branch conflict resolution, and review hardening.
> - This pull request consolidates that branch onto the current `master`
branch as a single reviewable PR.
> - The benefit is a complete multi-user implementation path with tests
and docs carried forward without dropping existing branch work.
## What Changed
- Added authenticated human-user access surfaces: auth/session routes,
company user directory, profile settings, company access/member
management, join requests, and invite management.
- Added invite creation, invite landing, onboarding, logo/branding,
invite grants, deduped join requests, and authenticated multi-user E2E
coverage.
- Tightened company-scoped and instance-admin authorization across
board, plugin, adapter, access, issue, and workspace routes.
- Added profile-image URL validation hardening, avatar preservation on
name-only profile updates, and join-request uniqueness migration cleanup
for pending human requests.
- Added an atomic member role/status/grants update path so Company
Access saves no longer leave partially updated permissions.
- Improved issue chat, inbox, assignee identity rendering,
sidebar/account/company navigation, workspace routing, and execution
workspace reuse behavior for multi-user operation.
- Added and updated server/UI tests covering auth, invites, membership,
issue workspace inheritance, plugin authz, inbox/chat behavior, and
multi-user flows.
- Merged current `public-gh/master` into this branch, resolved all
conflicts, and verified no `pnpm-lock.yaml` change is included in this
PR diff.
## Verification
- `pnpm exec vitest run server/src/__tests__/issues-service.test.ts
ui/src/components/IssueChatThread.test.tsx ui/src/pages/Inbox.test.tsx`
- `pnpm run preflight:workspace-links && pnpm exec vitest run
server/src/__tests__/plugin-routes-authz.test.ts`
- `pnpm exec vitest run server/src/__tests__/plugin-routes-authz.test.ts
server/src/__tests__/workspace-runtime-service-authz.test.ts
server/src/__tests__/access-validators.test.ts`
- `pnpm exec vitest run
server/src/__tests__/authz-company-access.test.ts
server/src/__tests__/routines-routes.test.ts
server/src/__tests__/sidebar-preferences-routes.test.ts
server/src/__tests__/approval-routes-idempotency.test.ts
server/src/__tests__/openclaw-invite-prompt-route.test.ts
server/src/__tests__/agent-cross-tenant-authz-routes.test.ts
server/src/__tests__/routines-e2e.test.ts`
- `pnpm exec vitest run server/src/__tests__/auth-routes.test.ts
ui/src/pages/CompanyAccess.test.tsx`
- `pnpm --filter @paperclipai/shared typecheck && pnpm --filter
@paperclipai/db typecheck && pnpm --filter @paperclipai/server
typecheck`
- `pnpm --filter @paperclipai/shared typecheck && pnpm --filter
@paperclipai/server typecheck`
- `pnpm --filter @paperclipai/ui typecheck`
- `pnpm db:generate`
- `npx playwright test --config tests/e2e/playwright.config.ts --list`
- Confirmed branch has no uncommitted changes and is `0` commits behind
`public-gh/master` before PR creation.
- Confirmed no `pnpm-lock.yaml` change is staged or present in the PR
diff.
## Risks
- High review surface area: this PR contains the accumulated multi-user
branch plus follow-up fixes, so reviewers should focus especially on
company-boundary enforcement and authenticated-vs-local deployment
behavior.
- UI behavior changed across invites, inbox, issue chat, access
settings, and sidebar navigation; no browser screenshots are included in
this branch-consolidation PR.
- Plugin install, upgrade, and lifecycle/config mutations now require
instance-admin access, which is intentional but may change expectations
for non-admin board users.
- A join-request dedupe migration rejects duplicate pending human
requests before creating unique indexes; deployments with unusual
historical duplicates should review the migration behavior.
- Company member role/status/grant saves now use a new combined
endpoint; older separate endpoints remain for compatibility.
- Full production build was not run locally in this heartbeat; CI should
cover the full matrix.
## Model Used
- OpenAI Codex coding agent, GPT-5-based model, CLI/tool-use
environment. Exact deployed model identifier and context window were not
exposed by the runtime.
## Checklist
- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] If this change affects the UI, I have included before/after
screenshots
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [x] I will address all Greptile and reviewer comments before
requesting merge
Note on screenshots: this is a branch-consolidation PR for an
already-developed multi-user branch, and no browser screenshots were
captured during this heartbeat.
---------
Co-authored-by: dotta <dotta@example.com>
Co-authored-by: Paperclip <noreply@paperclip.ing>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-17 09:44:19 -05:00
|
|
|
import { accessApi } from "../api/access";
|
2026-03-02 14:20:49 -06:00
|
|
|
import { authApi } from "../api/auth";
|
Add MarkdownEditor component, asset image upload, and rich description editing
Introduce MarkdownEditor built on @mdxeditor/editor with headings,
lists, links, quotes, image upload with drag-and-drop, and themed CSS
integration. Add asset image upload API (routes, service, storage) and
wire image upload into InlineEditor multiline mode, NewIssueDialog,
NewProjectDialog, GoalDetail, IssueDetail, and ProjectDetail
description fields. Tighten prompt template editor styling.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 12:50:45 -06:00
|
|
|
import { assetsApi } from "../api/assets";
|
feat: implement multi-user access and invite flows (#3784)
## Thinking Path
> - Paperclip is the control plane for autonomous AI companies.
> - V1 needs to stay local-first while also supporting shared,
authenticated deployments.
> - Human operators need real identities, company membership, invite
flows, profile surfaces, and company-scoped access controls.
> - Agents and operators also need the existing issue, inbox, workspace,
approval, and plugin flows to keep working under those authenticated
boundaries.
> - This branch accumulated the multi-user implementation, follow-up QA
fixes, workspace/runtime refinements, invite UX improvements,
release-branch conflict resolution, and review hardening.
> - This pull request consolidates that branch onto the current `master`
branch as a single reviewable PR.
> - The benefit is a complete multi-user implementation path with tests
and docs carried forward without dropping existing branch work.
## What Changed
- Added authenticated human-user access surfaces: auth/session routes,
company user directory, profile settings, company access/member
management, join requests, and invite management.
- Added invite creation, invite landing, onboarding, logo/branding,
invite grants, deduped join requests, and authenticated multi-user E2E
coverage.
- Tightened company-scoped and instance-admin authorization across
board, plugin, adapter, access, issue, and workspace routes.
- Added profile-image URL validation hardening, avatar preservation on
name-only profile updates, and join-request uniqueness migration cleanup
for pending human requests.
- Added an atomic member role/status/grants update path so Company
Access saves no longer leave partially updated permissions.
- Improved issue chat, inbox, assignee identity rendering,
sidebar/account/company navigation, workspace routing, and execution
workspace reuse behavior for multi-user operation.
- Added and updated server/UI tests covering auth, invites, membership,
issue workspace inheritance, plugin authz, inbox/chat behavior, and
multi-user flows.
- Merged current `public-gh/master` into this branch, resolved all
conflicts, and verified no `pnpm-lock.yaml` change is included in this
PR diff.
## Verification
- `pnpm exec vitest run server/src/__tests__/issues-service.test.ts
ui/src/components/IssueChatThread.test.tsx ui/src/pages/Inbox.test.tsx`
- `pnpm run preflight:workspace-links && pnpm exec vitest run
server/src/__tests__/plugin-routes-authz.test.ts`
- `pnpm exec vitest run server/src/__tests__/plugin-routes-authz.test.ts
server/src/__tests__/workspace-runtime-service-authz.test.ts
server/src/__tests__/access-validators.test.ts`
- `pnpm exec vitest run
server/src/__tests__/authz-company-access.test.ts
server/src/__tests__/routines-routes.test.ts
server/src/__tests__/sidebar-preferences-routes.test.ts
server/src/__tests__/approval-routes-idempotency.test.ts
server/src/__tests__/openclaw-invite-prompt-route.test.ts
server/src/__tests__/agent-cross-tenant-authz-routes.test.ts
server/src/__tests__/routines-e2e.test.ts`
- `pnpm exec vitest run server/src/__tests__/auth-routes.test.ts
ui/src/pages/CompanyAccess.test.tsx`
- `pnpm --filter @paperclipai/shared typecheck && pnpm --filter
@paperclipai/db typecheck && pnpm --filter @paperclipai/server
typecheck`
- `pnpm --filter @paperclipai/shared typecheck && pnpm --filter
@paperclipai/server typecheck`
- `pnpm --filter @paperclipai/ui typecheck`
- `pnpm db:generate`
- `npx playwright test --config tests/e2e/playwright.config.ts --list`
- Confirmed branch has no uncommitted changes and is `0` commits behind
`public-gh/master` before PR creation.
- Confirmed no `pnpm-lock.yaml` change is staged or present in the PR
diff.
## Risks
- High review surface area: this PR contains the accumulated multi-user
branch plus follow-up fixes, so reviewers should focus especially on
company-boundary enforcement and authenticated-vs-local deployment
behavior.
- UI behavior changed across invites, inbox, issue chat, access
settings, and sidebar navigation; no browser screenshots are included in
this branch-consolidation PR.
- Plugin install, upgrade, and lifecycle/config mutations now require
instance-admin access, which is intentional but may change expectations
for non-admin board users.
- A join-request dedupe migration rejects duplicate pending human
requests before creating unique indexes; deployments with unusual
historical duplicates should review the migration behavior.
- Company member role/status/grant saves now use a new combined
endpoint; older separate endpoints remain for compatibility.
- Full production build was not run locally in this heartbeat; CI should
cover the full matrix.
## Model Used
- OpenAI Codex coding agent, GPT-5-based model, CLI/tool-use
environment. Exact deployed model identifier and context window were not
exposed by the runtime.
## Checklist
- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] If this change affects the UI, I have included before/after
screenshots
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [x] I will address all Greptile and reviewer comments before
requesting merge
Note on screenshots: this is a branch-consolidation PR for an
already-developed multi-user branch, and no browser screenshots were
captured during this heartbeat.
---------
Co-authored-by: dotta <dotta@example.com>
Co-authored-by: Paperclip <noreply@paperclip.ing>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-17 09:44:19 -05:00
|
|
|
import { buildCompanyUserInlineOptions, buildMarkdownMentionOptions } from "../lib/company-members";
|
2026-02-17 12:24:48 -06:00
|
|
|
import { queryKeys } from "../lib/queryKeys";
|
2026-03-02 14:20:49 -06:00
|
|
|
import { useProjectOrder } from "../hooks/useProjectOrder";
|
2026-03-05 11:19:56 -06:00
|
|
|
import { getRecentAssigneeIds, sortAgentsByRecency, trackRecentAssignee } from "../lib/recent-assignees";
|
[codex] Polish issue and operator workflow UI (#4090)
## Thinking Path
> - Paperclip operators spend much of their time in issues, inboxes,
selectors, and rich comment threads.
> - Small interaction problems in those surfaces slow down supervision
of AI-agent work.
> - The branch included related operator quality-of-life fixes for issue
layout, inbox actions, recent selectors, mobile inputs, and chat
rendering stability.
> - These changes are UI-focused and can land independently from
workspace navigation and access-profile work.
> - This pull request groups the operator QoL fixes into one standalone
branch.
> - The benefit is a more stable and efficient board workflow for issue
triage and task editing.
## What Changed
- Widened issue detail content and added a desktop inbox archive action.
- Fixed mobile text-field zoom by keeping touch input font sizes at
16px.
- Prioritized recent picker selections for assignees/projects in issue
and routine flows.
- Showed actionable approvals in the Mine inbox model.
- Fixed issue chat renderer state crashes and hardened tests.
## Verification
- `pnpm install --frozen-lockfile`
- `pnpm exec vitest run ui/src/components/IssueChatThread.test.tsx
ui/src/lib/inbox.test.ts ui/src/lib/recent-selections.test.ts`
- Split integration check: merged last after the other
[PAP-1614](/PAP/issues/PAP-1614) branches with no merge conflicts.
- Confirmed this branch does not include `pnpm-lock.yaml`.
## Risks
- Low to medium risk: mostly UI state, layout, and selection-priority
behavior.
- Visual layout and mobile zoom behavior may need browser/device QA
beyond component tests.
- No database migrations are included.
> For core feature work, check [`ROADMAP.md`](ROADMAP.md) first and
discuss it in `#dev` before opening the PR. Feature PRs that overlap
with planned core work may need to be redirected — check the roadmap
first. See `CONTRIBUTING.md`.
## Model Used
- OpenAI Codex, GPT-5.4 tool-enabled coding model, agentic
code-editing/runtime with local shell and GitHub CLI access; exact
context window and reasoning mode are not exposed by the Paperclip
harness.
## Checklist
- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] If this change affects the UI, I have included before/after
screenshots
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [x] I will address all Greptile and reviewer comments before
requesting merge
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-04-20 06:16:41 -05:00
|
|
|
import { getRecentProjectIds, trackRecentProject } from "../lib/recent-projects";
|
2026-04-06 08:40:38 -05:00
|
|
|
import { buildExecutionPolicy } from "../lib/issue-execution-policy";
|
[codex] Harden execution reliability and heartbeat tooling (#3679)
## Thinking Path
> - Paperclip orchestrates AI agents for zero-human companies
> - Reliable execution depends on heartbeat routing, issue lifecycle
semantics, telemetry, and a fast enough local verification loop to keep
regressions visible
> - The remaining commits on this branch were mostly server/runtime
correctness fixes plus test and documentation follow-ups in that area
> - Those changes are logically separate from the UI-focused
issue-detail and workspace/navigation branches even when they touch
overlapping issue APIs
> - This pull request groups the execution reliability, heartbeat,
telemetry, and tooling changes into one standalone branch
> - The benefit is a focused review of the control-plane correctness
work, including the follow-up fix that restored the implicit
comment-reopen helpers after branch splitting
## What Changed
- Hardened issue/heartbeat execution behavior, including self-review
stage skipping, deferred mention wakes during active execution, stranded
execution recovery, active-run scoping, assignee resolution, and
blocked-to-todo wake resumption
- Reduced noisy polling/logging overhead by trimming issue run payloads,
compacting persisted run logs, silencing high-volume request logs, and
capping heartbeat-run queries in dashboard/inbox surfaces
- Expanded telemetry and status semantics with adapter/model fields on
task completion plus clearer status guidance in docs/onboarding material
- Updated test infrastructure and verification defaults with faster
route-test module isolation, cheaper default `pnpm test`, e2e isolation
from local state, and repo verification follow-ups
- Included docs/release housekeeping from the branch and added a small
follow-up commit restoring the implicit comment-reopen helpers that were
dropped during branch reconstruction
## Verification
- `pnpm vitest run
server/src/__tests__/issue-comment-reopen-routes.test.ts
server/src/__tests__/issue-telemetry-routes.test.ts`
- `pnpm vitest run server/src/__tests__/http-log-policy.test.ts
server/src/__tests__/heartbeat-run-log.test.ts
server/src/__tests__/health.test.ts`
- `server/src/__tests__/activity-service.test.ts`,
`server/src/__tests__/heartbeat-comment-wake-batching.test.ts`, and
`server/src/__tests__/heartbeat-process-recovery.test.ts` were attempted
on this host but the embedded Postgres harness reported
init-script/data-dir problems and skipped or failed to start, so they
are noted as environment-limited
## Risks
- Medium: this branch changes core issue/heartbeat routing and
reopen/wakeup behavior, so regressions would affect agent execution flow
rather than isolated UI polish
- Because it also updates verification infrastructure, reviewers should
pay attention to whether the new tests are asserting the right failure
modes and not just reshaping harness behavior
## Model Used
- OpenAI Codex coding agent (GPT-5-class runtime in Codex CLI; exact
deployed model ID is not exposed in this environment), reasoning
enabled, tool use and local code execution enabled
## Checklist
- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [ ] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [ ] If this change affects the UI, I have included before/after
screenshots
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [x] I will address all Greptile and reviewer comments before
requesting merge
---------
Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-04-14 13:34:52 -05:00
|
|
|
import { useToastActions } from "../context/ToastContext";
|
2026-03-12 16:11:37 -05:00
|
|
|
import {
|
|
|
|
|
assigneeValueFromSelection,
|
|
|
|
|
currentUserAssigneeOption,
|
|
|
|
|
parseAssigneeValue,
|
|
|
|
|
} from "../lib/assignees";
|
Add detail pages, property panels, and restyle list pages
New pages: AgentDetail, GoalDetail, IssueDetail, ProjectDetail, Inbox,
MyIssues. New feature components: AgentProperties, GoalProperties,
IssueProperties, ProjectProperties, GoalTree, NewIssueDialog. Add
heartbeats API client. Restyle all list pages (Agents, Issues, Goals,
Projects, Dashboard, Costs, Activity, Org) with EntityRow, FilterBar,
and improved layouts. Add routing for detail views.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 09:57:06 -06:00
|
|
|
import {
|
|
|
|
|
Dialog,
|
|
|
|
|
DialogContent,
|
|
|
|
|
} from "@/components/ui/dialog";
|
|
|
|
|
import { Button } from "@/components/ui/button";
|
2026-04-04 10:00:39 -05:00
|
|
|
import { ToggleSwitch } from "@/components/ui/toggle-switch";
|
Add detail pages, property panels, and restyle list pages
New pages: AgentDetail, GoalDetail, IssueDetail, ProjectDetail, Inbox,
MyIssues. New feature components: AgentProperties, GoalProperties,
IssueProperties, ProjectProperties, GoalTree, NewIssueDialog. Add
heartbeats API client. Restyle all list pages (Agents, Issues, Goals,
Projects, Dashboard, Costs, Activity, Org) with EntityRow, FilterBar,
and improved layouts. Add routing for detail views.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 09:57:06 -06:00
|
|
|
import {
|
2026-02-17 10:53:20 -06:00
|
|
|
Popover,
|
|
|
|
|
PopoverContent,
|
|
|
|
|
PopoverTrigger,
|
|
|
|
|
} from "@/components/ui/popover";
|
|
|
|
|
import {
|
|
|
|
|
Maximize2,
|
|
|
|
|
Minimize2,
|
|
|
|
|
MoreHorizontal,
|
2026-02-26 10:32:44 -06:00
|
|
|
ChevronRight,
|
|
|
|
|
ChevronDown,
|
2026-02-17 10:53:20 -06:00
|
|
|
CircleDot,
|
|
|
|
|
Minus,
|
|
|
|
|
ArrowUp,
|
|
|
|
|
ArrowDown,
|
|
|
|
|
AlertTriangle,
|
|
|
|
|
Tag,
|
|
|
|
|
Calendar,
|
2026-02-25 21:36:06 -06:00
|
|
|
Paperclip,
|
2026-03-14 07:13:59 -05:00
|
|
|
FileText,
|
2026-03-10 21:06:16 -05:00
|
|
|
Loader2,
|
2026-04-06 10:58:59 -05:00
|
|
|
ListTree,
|
2026-03-14 07:13:59 -05:00
|
|
|
X,
|
2026-04-06 19:14:40 -05:00
|
|
|
Eye,
|
|
|
|
|
ShieldCheck,
|
2026-02-17 10:53:20 -06:00
|
|
|
} from "lucide-react";
|
|
|
|
|
import { cn } from "../lib/utils";
|
2026-03-05 15:52:59 +01:00
|
|
|
import { extractProviderIdWithFallback } from "../lib/model-utils";
|
2026-02-23 19:52:43 -06:00
|
|
|
import { issueStatusText, issueStatusTextDefault, priorityColor, priorityColorDefault } from "../lib/status-colors";
|
2026-03-02 13:31:58 -06:00
|
|
|
import { MarkdownEditor, type MarkdownEditorRef, type MentionOption } from "./MarkdownEditor";
|
2026-02-23 14:41:21 -06:00
|
|
|
import { AgentIcon } from "./AgentIconPicker";
|
2026-02-26 08:53:03 -06:00
|
|
|
import { InlineEntitySelector, type InlineEntityOption } from "./InlineEntitySelector";
|
2026-02-17 10:53:20 -06:00
|
|
|
|
2026-02-17 20:46:12 -06:00
|
|
|
const DRAFT_KEY = "paperclip:issue-draft";
|
|
|
|
|
const DEBOUNCE_MS = 800;
|
|
|
|
|
|
2026-02-26 16:33:48 -06:00
|
|
|
|
2026-02-17 20:46:12 -06:00
|
|
|
interface IssueDraft {
|
|
|
|
|
title: string;
|
|
|
|
|
description: string;
|
|
|
|
|
status: string;
|
|
|
|
|
priority: string;
|
2026-03-12 16:11:37 -05:00
|
|
|
assigneeValue: string;
|
2026-04-06 08:40:38 -05:00
|
|
|
reviewerValue: string;
|
|
|
|
|
approverValue: string;
|
2026-03-12 16:11:37 -05:00
|
|
|
assigneeId?: string;
|
2026-02-17 20:46:12 -06:00
|
|
|
projectId: string;
|
2026-03-13 17:12:25 -05:00
|
|
|
projectWorkspaceId?: string;
|
2026-02-26 10:32:44 -06:00
|
|
|
assigneeModelOverride: string;
|
|
|
|
|
assigneeThinkingEffort: string;
|
2026-02-26 16:33:48 -06:00
|
|
|
assigneeChrome: boolean;
|
2026-03-13 17:12:25 -05:00
|
|
|
executionWorkspaceMode?: string;
|
|
|
|
|
selectedExecutionWorkspaceId?: string;
|
|
|
|
|
useIsolatedExecutionWorkspace?: boolean;
|
2026-02-26 10:32:44 -06:00
|
|
|
}
|
|
|
|
|
|
2026-03-14 07:13:59 -05:00
|
|
|
type StagedIssueFile = {
|
|
|
|
|
id: string;
|
|
|
|
|
file: File;
|
|
|
|
|
kind: "document" | "attachment";
|
|
|
|
|
documentKey?: string;
|
|
|
|
|
title?: string | null;
|
|
|
|
|
};
|
|
|
|
|
|
2026-03-04 16:48:54 -06:00
|
|
|
const ISSUE_OVERRIDE_ADAPTER_TYPES = new Set(["claude_local", "codex_local", "opencode_local"]);
|
2026-03-14 07:13:59 -05:00
|
|
|
const STAGED_FILE_ACCEPT = "image/*,application/pdf,text/plain,text/markdown,application/json,text/csv,text/html,.md,.markdown";
|
2026-02-26 10:32:44 -06:00
|
|
|
|
|
|
|
|
const ISSUE_THINKING_EFFORT_OPTIONS = {
|
|
|
|
|
claude_local: [
|
|
|
|
|
{ value: "", label: "Default" },
|
|
|
|
|
{ value: "low", label: "Low" },
|
|
|
|
|
{ value: "medium", label: "Medium" },
|
|
|
|
|
{ value: "high", label: "High" },
|
|
|
|
|
],
|
|
|
|
|
codex_local: [
|
|
|
|
|
{ value: "", label: "Default" },
|
|
|
|
|
{ value: "minimal", label: "Minimal" },
|
|
|
|
|
{ value: "low", label: "Low" },
|
|
|
|
|
{ value: "medium", label: "Medium" },
|
|
|
|
|
{ value: "high", label: "High" },
|
2026-03-05 18:59:42 -06:00
|
|
|
{ value: "xhigh", label: "X-High" },
|
2026-02-26 10:32:44 -06:00
|
|
|
],
|
2026-03-04 16:48:54 -06:00
|
|
|
opencode_local: [
|
|
|
|
|
{ value: "", label: "Default" },
|
|
|
|
|
{ value: "minimal", label: "Minimal" },
|
|
|
|
|
{ value: "low", label: "Low" },
|
|
|
|
|
{ value: "medium", label: "Medium" },
|
|
|
|
|
{ value: "high", label: "High" },
|
2026-03-05 18:59:42 -06:00
|
|
|
{ value: "xhigh", label: "X-High" },
|
2026-03-04 16:48:54 -06:00
|
|
|
{ value: "max", label: "Max" },
|
|
|
|
|
],
|
2026-02-26 10:32:44 -06:00
|
|
|
} as const;
|
|
|
|
|
|
|
|
|
|
function buildAssigneeAdapterOverrides(input: {
|
|
|
|
|
adapterType: string | null | undefined;
|
|
|
|
|
modelOverride: string;
|
|
|
|
|
thinkingEffortOverride: string;
|
2026-02-26 16:33:48 -06:00
|
|
|
chrome: boolean;
|
2026-02-26 10:32:44 -06:00
|
|
|
}): Record<string, unknown> | null {
|
|
|
|
|
const adapterType = input.adapterType ?? null;
|
|
|
|
|
if (!adapterType || !ISSUE_OVERRIDE_ADAPTER_TYPES.has(adapterType)) {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const adapterConfig: Record<string, unknown> = {};
|
|
|
|
|
if (input.modelOverride) adapterConfig.model = input.modelOverride;
|
|
|
|
|
if (input.thinkingEffortOverride) {
|
|
|
|
|
if (adapterType === "codex_local") {
|
|
|
|
|
adapterConfig.modelReasoningEffort = input.thinkingEffortOverride;
|
2026-03-04 16:48:54 -06:00
|
|
|
} else if (adapterType === "opencode_local") {
|
|
|
|
|
adapterConfig.variant = input.thinkingEffortOverride;
|
2026-02-26 10:32:44 -06:00
|
|
|
} else if (adapterType === "claude_local") {
|
|
|
|
|
adapterConfig.effort = input.thinkingEffortOverride;
|
2026-03-05 15:24:20 +01:00
|
|
|
} else if (adapterType === "opencode_local") {
|
|
|
|
|
adapterConfig.variant = input.thinkingEffortOverride;
|
2026-02-26 10:32:44 -06:00
|
|
|
}
|
|
|
|
|
}
|
2026-02-26 16:33:48 -06:00
|
|
|
if (adapterType === "claude_local" && input.chrome) {
|
|
|
|
|
adapterConfig.chrome = true;
|
|
|
|
|
}
|
2026-02-26 10:32:44 -06:00
|
|
|
|
|
|
|
|
const overrides: Record<string, unknown> = {};
|
|
|
|
|
if (Object.keys(adapterConfig).length > 0) {
|
|
|
|
|
overrides.adapterConfig = adapterConfig;
|
|
|
|
|
}
|
|
|
|
|
return Object.keys(overrides).length > 0 ? overrides : null;
|
2026-02-17 20:46:12 -06:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function loadDraft(): IssueDraft | null {
|
|
|
|
|
try {
|
|
|
|
|
const raw = localStorage.getItem(DRAFT_KEY);
|
|
|
|
|
if (!raw) return null;
|
|
|
|
|
return JSON.parse(raw) as IssueDraft;
|
|
|
|
|
} catch {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function saveDraft(draft: IssueDraft) {
|
|
|
|
|
localStorage.setItem(DRAFT_KEY, JSON.stringify(draft));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function clearDraft() {
|
|
|
|
|
localStorage.removeItem(DRAFT_KEY);
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-14 07:21:21 -05:00
|
|
|
function isTextDocumentFile(file: File) {
|
2026-03-14 07:13:59 -05:00
|
|
|
const name = file.name.toLowerCase();
|
|
|
|
|
return (
|
|
|
|
|
name.endsWith(".md") ||
|
|
|
|
|
name.endsWith(".markdown") ||
|
2026-03-14 07:21:21 -05:00
|
|
|
name.endsWith(".txt") ||
|
|
|
|
|
file.type === "text/markdown" ||
|
|
|
|
|
file.type === "text/plain"
|
2026-03-14 07:13:59 -05:00
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function fileBaseName(filename: string) {
|
|
|
|
|
return filename.replace(/\.[^.]+$/, "");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function slugifyDocumentKey(input: string) {
|
|
|
|
|
const slug = input
|
|
|
|
|
.trim()
|
|
|
|
|
.toLowerCase()
|
|
|
|
|
.replace(/[^a-z0-9]+/g, "-")
|
|
|
|
|
.replace(/^-+|-+$/g, "");
|
|
|
|
|
return slug || "document";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function titleizeFilename(input: string) {
|
|
|
|
|
return input
|
|
|
|
|
.split(/[-_ ]+/g)
|
|
|
|
|
.filter(Boolean)
|
|
|
|
|
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
|
|
|
|
|
.join(" ");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function createUniqueDocumentKey(baseKey: string, stagedFiles: StagedIssueFile[]) {
|
|
|
|
|
const existingKeys = new Set(
|
|
|
|
|
stagedFiles
|
|
|
|
|
.filter((file) => file.kind === "document")
|
|
|
|
|
.map((file) => file.documentKey)
|
|
|
|
|
.filter((key): key is string => Boolean(key)),
|
|
|
|
|
);
|
|
|
|
|
if (!existingKeys.has(baseKey)) return baseKey;
|
|
|
|
|
let suffix = 2;
|
|
|
|
|
while (existingKeys.has(`${baseKey}-${suffix}`)) {
|
|
|
|
|
suffix += 1;
|
|
|
|
|
}
|
|
|
|
|
return `${baseKey}-${suffix}`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function formatFileSize(file: File) {
|
|
|
|
|
if (file.size < 1024) return `${file.size} B`;
|
|
|
|
|
if (file.size < 1024 * 1024) return `${(file.size / 1024).toFixed(1)} KB`;
|
|
|
|
|
return `${(file.size / (1024 * 1024)).toFixed(1)} MB`;
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-17 10:53:20 -06:00
|
|
|
const statuses = [
|
2026-02-23 19:52:43 -06:00
|
|
|
{ value: "backlog", label: "Backlog", color: issueStatusText.backlog ?? issueStatusTextDefault },
|
|
|
|
|
{ value: "todo", label: "Todo", color: issueStatusText.todo ?? issueStatusTextDefault },
|
|
|
|
|
{ value: "in_progress", label: "In Progress", color: issueStatusText.in_progress ?? issueStatusTextDefault },
|
|
|
|
|
{ value: "in_review", label: "In Review", color: issueStatusText.in_review ?? issueStatusTextDefault },
|
|
|
|
|
{ value: "done", label: "Done", color: issueStatusText.done ?? issueStatusTextDefault },
|
2026-02-17 10:53:20 -06:00
|
|
|
];
|
|
|
|
|
|
|
|
|
|
const priorities = [
|
2026-02-23 19:52:43 -06:00
|
|
|
{ value: "critical", label: "Critical", icon: AlertTriangle, color: priorityColor.critical ?? priorityColorDefault },
|
|
|
|
|
{ value: "high", label: "High", icon: ArrowUp, color: priorityColor.high ?? priorityColorDefault },
|
|
|
|
|
{ value: "medium", label: "Medium", icon: Minus, color: priorityColor.medium ?? priorityColorDefault },
|
|
|
|
|
{ value: "low", label: "Low", icon: ArrowDown, color: priorityColor.low ?? priorityColorDefault },
|
2026-02-17 10:53:20 -06:00
|
|
|
];
|
Add detail pages, property panels, and restyle list pages
New pages: AgentDetail, GoalDetail, IssueDetail, ProjectDetail, Inbox,
MyIssues. New feature components: AgentProperties, GoalProperties,
IssueProperties, ProjectProperties, GoalTree, NewIssueDialog. Add
heartbeats API client. Restyle all list pages (Agents, Issues, Goals,
Projects, Dashboard, Costs, Activity, Org) with EntityRow, FilterBar,
and improved layouts. Add routing for detail views.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 09:57:06 -06:00
|
|
|
|
2026-03-13 17:12:25 -05:00
|
|
|
const EXECUTION_WORKSPACE_MODES = [
|
|
|
|
|
{ value: "shared_workspace", label: "Project default" },
|
|
|
|
|
{ value: "isolated_workspace", label: "New isolated workspace" },
|
|
|
|
|
{ value: "reuse_existing", label: "Reuse existing workspace" },
|
|
|
|
|
] as const;
|
|
|
|
|
|
|
|
|
|
function defaultProjectWorkspaceIdForProject(project: { workspaces?: Array<{ id: string; isPrimary: boolean }>; executionWorkspacePolicy?: { defaultProjectWorkspaceId?: string | null } | null } | null | undefined) {
|
|
|
|
|
if (!project) return "";
|
|
|
|
|
return project.executionWorkspacePolicy?.defaultProjectWorkspaceId
|
|
|
|
|
?? project.workspaces?.find((workspace) => workspace.isPrimary)?.id
|
|
|
|
|
?? project.workspaces?.[0]?.id
|
|
|
|
|
?? "";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function defaultExecutionWorkspaceModeForProject(project: { executionWorkspacePolicy?: { enabled?: boolean; defaultMode?: string | null } | null } | null | undefined) {
|
|
|
|
|
const defaultMode = project?.executionWorkspacePolicy?.enabled ? project.executionWorkspacePolicy.defaultMode : null;
|
|
|
|
|
if (
|
|
|
|
|
defaultMode === "isolated_workspace" ||
|
|
|
|
|
defaultMode === "operator_branch" ||
|
|
|
|
|
defaultMode === "adapter_default"
|
|
|
|
|
) {
|
|
|
|
|
return defaultMode === "adapter_default" ? "agent_default" : defaultMode;
|
|
|
|
|
}
|
|
|
|
|
return "shared_workspace";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function issueExecutionWorkspaceModeForExistingWorkspace(mode: string | null | undefined) {
|
|
|
|
|
if (mode === "isolated_workspace" || mode === "operator_branch" || mode === "shared_workspace") {
|
|
|
|
|
return mode;
|
|
|
|
|
}
|
|
|
|
|
if (mode === "adapter_managed" || mode === "cloud_sandbox") {
|
|
|
|
|
return "agent_default";
|
|
|
|
|
}
|
|
|
|
|
return "shared_workspace";
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-17 12:24:48 -06:00
|
|
|
export function NewIssueDialog() {
|
Add detail pages, property panels, and restyle list pages
New pages: AgentDetail, GoalDetail, IssueDetail, ProjectDetail, Inbox,
MyIssues. New feature components: AgentProperties, GoalProperties,
IssueProperties, ProjectProperties, GoalTree, NewIssueDialog. Add
heartbeats API client. Restyle all list pages (Agents, Issues, Goals,
Projects, Dashboard, Costs, Activity, Org) with EntityRow, FilterBar,
and improved layouts. Add routing for detail views.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 09:57:06 -06:00
|
|
|
const { newIssueOpen, newIssueDefaults, closeNewIssue } = useDialog();
|
2026-02-26 16:33:48 -06:00
|
|
|
const { companies, selectedCompanyId, selectedCompany } = useCompany();
|
2026-02-17 12:24:48 -06:00
|
|
|
const queryClient = useQueryClient();
|
[codex] Harden execution reliability and heartbeat tooling (#3679)
## Thinking Path
> - Paperclip orchestrates AI agents for zero-human companies
> - Reliable execution depends on heartbeat routing, issue lifecycle
semantics, telemetry, and a fast enough local verification loop to keep
regressions visible
> - The remaining commits on this branch were mostly server/runtime
correctness fixes plus test and documentation follow-ups in that area
> - Those changes are logically separate from the UI-focused
issue-detail and workspace/navigation branches even when they touch
overlapping issue APIs
> - This pull request groups the execution reliability, heartbeat,
telemetry, and tooling changes into one standalone branch
> - The benefit is a focused review of the control-plane correctness
work, including the follow-up fix that restored the implicit
comment-reopen helpers after branch splitting
## What Changed
- Hardened issue/heartbeat execution behavior, including self-review
stage skipping, deferred mention wakes during active execution, stranded
execution recovery, active-run scoping, assignee resolution, and
blocked-to-todo wake resumption
- Reduced noisy polling/logging overhead by trimming issue run payloads,
compacting persisted run logs, silencing high-volume request logs, and
capping heartbeat-run queries in dashboard/inbox surfaces
- Expanded telemetry and status semantics with adapter/model fields on
task completion plus clearer status guidance in docs/onboarding material
- Updated test infrastructure and verification defaults with faster
route-test module isolation, cheaper default `pnpm test`, e2e isolation
from local state, and repo verification follow-ups
- Included docs/release housekeeping from the branch and added a small
follow-up commit restoring the implicit comment-reopen helpers that were
dropped during branch reconstruction
## Verification
- `pnpm vitest run
server/src/__tests__/issue-comment-reopen-routes.test.ts
server/src/__tests__/issue-telemetry-routes.test.ts`
- `pnpm vitest run server/src/__tests__/http-log-policy.test.ts
server/src/__tests__/heartbeat-run-log.test.ts
server/src/__tests__/health.test.ts`
- `server/src/__tests__/activity-service.test.ts`,
`server/src/__tests__/heartbeat-comment-wake-batching.test.ts`, and
`server/src/__tests__/heartbeat-process-recovery.test.ts` were attempted
on this host but the embedded Postgres harness reported
init-script/data-dir problems and skipped or failed to start, so they
are noted as environment-limited
## Risks
- Medium: this branch changes core issue/heartbeat routing and
reopen/wakeup behavior, so regressions would affect agent execution flow
rather than isolated UI polish
- Because it also updates verification infrastructure, reviewers should
pay attention to whether the new tests are asserting the right failure
modes and not just reshaping harness behavior
## Model Used
- OpenAI Codex coding agent (GPT-5-class runtime in Codex CLI; exact
deployed model ID is not exposed in this environment), reasoning
enabled, tool use and local code execution enabled
## Checklist
- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [ ] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [ ] If this change affects the UI, I have included before/after
screenshots
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [x] I will address all Greptile and reviewer comments before
requesting merge
---------
Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-04-14 13:34:52 -05:00
|
|
|
const { pushToast } = useToastActions();
|
Add detail pages, property panels, and restyle list pages
New pages: AgentDetail, GoalDetail, IssueDetail, ProjectDetail, Inbox,
MyIssues. New feature components: AgentProperties, GoalProperties,
IssueProperties, ProjectProperties, GoalTree, NewIssueDialog. Add
heartbeats API client. Restyle all list pages (Agents, Issues, Goals,
Projects, Dashboard, Costs, Activity, Org) with EntityRow, FilterBar,
and improved layouts. Add routing for detail views.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 09:57:06 -06:00
|
|
|
const [title, setTitle] = useState("");
|
|
|
|
|
const [description, setDescription] = useState("");
|
2026-02-17 10:53:20 -06:00
|
|
|
const [status, setStatus] = useState("todo");
|
|
|
|
|
const [priority, setPriority] = useState("");
|
2026-03-12 16:11:37 -05:00
|
|
|
const [assigneeValue, setAssigneeValue] = useState("");
|
2026-04-06 08:40:38 -05:00
|
|
|
const [reviewerValue, setReviewerValue] = useState("");
|
|
|
|
|
const [approverValue, setApproverValue] = useState("");
|
2026-04-06 19:59:17 -05:00
|
|
|
const [showReviewerRow, setShowReviewerRow] = useState(false);
|
|
|
|
|
const [showApproverRow, setShowApproverRow] = useState(false);
|
|
|
|
|
const [participantMenuOpen, setParticipantMenuOpen] = useState(false);
|
2026-02-17 10:53:20 -06:00
|
|
|
const [projectId, setProjectId] = useState("");
|
2026-03-13 17:12:25 -05:00
|
|
|
const [projectWorkspaceId, setProjectWorkspaceId] = useState("");
|
2026-02-26 10:32:44 -06:00
|
|
|
const [assigneeOptionsOpen, setAssigneeOptionsOpen] = useState(false);
|
|
|
|
|
const [assigneeModelOverride, setAssigneeModelOverride] = useState("");
|
|
|
|
|
const [assigneeThinkingEffort, setAssigneeThinkingEffort] = useState("");
|
2026-02-26 16:33:48 -06:00
|
|
|
const [assigneeChrome, setAssigneeChrome] = useState(false);
|
2026-03-13 17:12:25 -05:00
|
|
|
const [executionWorkspaceMode, setExecutionWorkspaceMode] = useState<string>("shared_workspace");
|
|
|
|
|
const [selectedExecutionWorkspaceId, setSelectedExecutionWorkspaceId] = useState("");
|
2026-02-17 10:53:20 -06:00
|
|
|
const [expanded, setExpanded] = useState(false);
|
2026-02-26 16:33:48 -06:00
|
|
|
const [dialogCompanyId, setDialogCompanyId] = useState<string | null>(null);
|
2026-03-14 07:13:59 -05:00
|
|
|
const [stagedFiles, setStagedFiles] = useState<StagedIssueFile[]>([]);
|
|
|
|
|
const [isFileDragOver, setIsFileDragOver] = useState(false);
|
2026-02-17 20:46:12 -06:00
|
|
|
const draftTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
|
2026-03-10 09:03:31 -05:00
|
|
|
const executionWorkspaceDefaultProjectId = useRef<string | null>(null);
|
Add detail pages, property panels, and restyle list pages
New pages: AgentDetail, GoalDetail, IssueDetail, ProjectDetail, Inbox,
MyIssues. New feature components: AgentProperties, GoalProperties,
IssueProperties, ProjectProperties, GoalTree, NewIssueDialog. Add
heartbeats API client. Restyle all list pages (Agents, Issues, Goals,
Projects, Dashboard, Costs, Activity, Org) with EntityRow, FilterBar,
and improved layouts. Add routing for detail views.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 09:57:06 -06:00
|
|
|
|
2026-02-26 16:33:48 -06:00
|
|
|
const effectiveCompanyId = dialogCompanyId ?? selectedCompanyId;
|
|
|
|
|
const dialogCompany = companies.find((c) => c.id === effectiveCompanyId) ?? selectedCompany;
|
2026-04-06 10:58:59 -05:00
|
|
|
const isSubIssueMode = Boolean(newIssueDefaults.parentId);
|
|
|
|
|
const parentIssueLabel = newIssueDefaults.parentIdentifier
|
|
|
|
|
?? (newIssueDefaults.parentId ? newIssueDefaults.parentId.slice(0, 8) : "");
|
2026-04-06 11:34:11 -05:00
|
|
|
const parentExecutionWorkspaceId = newIssueDefaults.executionWorkspaceId ?? "";
|
|
|
|
|
const parentExecutionWorkspaceLabel = newIssueDefaults.parentExecutionWorkspaceLabel ?? parentExecutionWorkspaceId;
|
2026-02-26 16:33:48 -06:00
|
|
|
|
2026-02-17 10:53:20 -06:00
|
|
|
// Popover states
|
|
|
|
|
const [statusOpen, setStatusOpen] = useState(false);
|
|
|
|
|
const [priorityOpen, setPriorityOpen] = useState(false);
|
|
|
|
|
const [moreOpen, setMoreOpen] = useState(false);
|
2026-02-26 16:33:48 -06:00
|
|
|
const [companyOpen, setCompanyOpen] = useState(false);
|
Add MarkdownEditor component, asset image upload, and rich description editing
Introduce MarkdownEditor built on @mdxeditor/editor with headings,
lists, links, quotes, image upload with drag-and-drop, and themed CSS
integration. Add asset image upload API (routes, service, storage) and
wire image upload into InlineEditor multiline mode, NewIssueDialog,
NewProjectDialog, GoalDetail, IssueDetail, and ProjectDetail
description fields. Tighten prompt template editor styling.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 12:50:45 -06:00
|
|
|
const descriptionEditorRef = useRef<MarkdownEditorRef>(null);
|
2026-03-14 07:13:59 -05:00
|
|
|
const stageFileInputRef = useRef<HTMLInputElement | null>(null);
|
2026-02-26 08:53:03 -06:00
|
|
|
const assigneeSelectorRef = useRef<HTMLButtonElement | null>(null);
|
|
|
|
|
const projectSelectorRef = useRef<HTMLButtonElement | null>(null);
|
2026-02-17 10:53:20 -06:00
|
|
|
|
2026-02-17 12:24:48 -06:00
|
|
|
const { data: agents } = useQuery({
|
2026-02-26 16:33:48 -06:00
|
|
|
queryKey: queryKeys.agents.list(effectiveCompanyId!),
|
|
|
|
|
queryFn: () => agentsApi.list(effectiveCompanyId!),
|
|
|
|
|
enabled: !!effectiveCompanyId && newIssueOpen,
|
2026-02-17 12:24:48 -06:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const { data: projects } = useQuery({
|
2026-02-26 16:33:48 -06:00
|
|
|
queryKey: queryKeys.projects.list(effectiveCompanyId!),
|
|
|
|
|
queryFn: () => projectsApi.list(effectiveCompanyId!),
|
|
|
|
|
enabled: !!effectiveCompanyId && newIssueOpen,
|
2026-02-17 12:24:48 -06:00
|
|
|
});
|
2026-03-13 17:12:25 -05:00
|
|
|
const { data: reusableExecutionWorkspaces } = useQuery({
|
|
|
|
|
queryKey: queryKeys.executionWorkspaces.list(effectiveCompanyId!, {
|
|
|
|
|
projectId,
|
|
|
|
|
projectWorkspaceId: projectWorkspaceId || undefined,
|
|
|
|
|
reuseEligible: true,
|
|
|
|
|
}),
|
|
|
|
|
queryFn: () =>
|
|
|
|
|
executionWorkspacesApi.list(effectiveCompanyId!, {
|
|
|
|
|
projectId,
|
|
|
|
|
projectWorkspaceId: projectWorkspaceId || undefined,
|
|
|
|
|
reuseEligible: true,
|
|
|
|
|
}),
|
2026-03-16 18:37:59 -05:00
|
|
|
enabled: Boolean(effectiveCompanyId) && newIssueOpen && Boolean(projectId),
|
2026-03-13 17:12:25 -05:00
|
|
|
});
|
2026-03-02 14:20:49 -06:00
|
|
|
const { data: session } = useQuery({
|
|
|
|
|
queryKey: queryKeys.auth.session,
|
|
|
|
|
queryFn: () => authApi.getSession(),
|
|
|
|
|
});
|
feat: implement multi-user access and invite flows (#3784)
## Thinking Path
> - Paperclip is the control plane for autonomous AI companies.
> - V1 needs to stay local-first while also supporting shared,
authenticated deployments.
> - Human operators need real identities, company membership, invite
flows, profile surfaces, and company-scoped access controls.
> - Agents and operators also need the existing issue, inbox, workspace,
approval, and plugin flows to keep working under those authenticated
boundaries.
> - This branch accumulated the multi-user implementation, follow-up QA
fixes, workspace/runtime refinements, invite UX improvements,
release-branch conflict resolution, and review hardening.
> - This pull request consolidates that branch onto the current `master`
branch as a single reviewable PR.
> - The benefit is a complete multi-user implementation path with tests
and docs carried forward without dropping existing branch work.
## What Changed
- Added authenticated human-user access surfaces: auth/session routes,
company user directory, profile settings, company access/member
management, join requests, and invite management.
- Added invite creation, invite landing, onboarding, logo/branding,
invite grants, deduped join requests, and authenticated multi-user E2E
coverage.
- Tightened company-scoped and instance-admin authorization across
board, plugin, adapter, access, issue, and workspace routes.
- Added profile-image URL validation hardening, avatar preservation on
name-only profile updates, and join-request uniqueness migration cleanup
for pending human requests.
- Added an atomic member role/status/grants update path so Company
Access saves no longer leave partially updated permissions.
- Improved issue chat, inbox, assignee identity rendering,
sidebar/account/company navigation, workspace routing, and execution
workspace reuse behavior for multi-user operation.
- Added and updated server/UI tests covering auth, invites, membership,
issue workspace inheritance, plugin authz, inbox/chat behavior, and
multi-user flows.
- Merged current `public-gh/master` into this branch, resolved all
conflicts, and verified no `pnpm-lock.yaml` change is included in this
PR diff.
## Verification
- `pnpm exec vitest run server/src/__tests__/issues-service.test.ts
ui/src/components/IssueChatThread.test.tsx ui/src/pages/Inbox.test.tsx`
- `pnpm run preflight:workspace-links && pnpm exec vitest run
server/src/__tests__/plugin-routes-authz.test.ts`
- `pnpm exec vitest run server/src/__tests__/plugin-routes-authz.test.ts
server/src/__tests__/workspace-runtime-service-authz.test.ts
server/src/__tests__/access-validators.test.ts`
- `pnpm exec vitest run
server/src/__tests__/authz-company-access.test.ts
server/src/__tests__/routines-routes.test.ts
server/src/__tests__/sidebar-preferences-routes.test.ts
server/src/__tests__/approval-routes-idempotency.test.ts
server/src/__tests__/openclaw-invite-prompt-route.test.ts
server/src/__tests__/agent-cross-tenant-authz-routes.test.ts
server/src/__tests__/routines-e2e.test.ts`
- `pnpm exec vitest run server/src/__tests__/auth-routes.test.ts
ui/src/pages/CompanyAccess.test.tsx`
- `pnpm --filter @paperclipai/shared typecheck && pnpm --filter
@paperclipai/db typecheck && pnpm --filter @paperclipai/server
typecheck`
- `pnpm --filter @paperclipai/shared typecheck && pnpm --filter
@paperclipai/server typecheck`
- `pnpm --filter @paperclipai/ui typecheck`
- `pnpm db:generate`
- `npx playwright test --config tests/e2e/playwright.config.ts --list`
- Confirmed branch has no uncommitted changes and is `0` commits behind
`public-gh/master` before PR creation.
- Confirmed no `pnpm-lock.yaml` change is staged or present in the PR
diff.
## Risks
- High review surface area: this PR contains the accumulated multi-user
branch plus follow-up fixes, so reviewers should focus especially on
company-boundary enforcement and authenticated-vs-local deployment
behavior.
- UI behavior changed across invites, inbox, issue chat, access
settings, and sidebar navigation; no browser screenshots are included in
this branch-consolidation PR.
- Plugin install, upgrade, and lifecycle/config mutations now require
instance-admin access, which is intentional but may change expectations
for non-admin board users.
- A join-request dedupe migration rejects duplicate pending human
requests before creating unique indexes; deployments with unusual
historical duplicates should review the migration behavior.
- Company member role/status/grant saves now use a new combined
endpoint; older separate endpoints remain for compatibility.
- Full production build was not run locally in this heartbeat; CI should
cover the full matrix.
## Model Used
- OpenAI Codex coding agent, GPT-5-based model, CLI/tool-use
environment. Exact deployed model identifier and context window were not
exposed by the runtime.
## Checklist
- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] If this change affects the UI, I have included before/after
screenshots
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [x] I will address all Greptile and reviewer comments before
requesting merge
Note on screenshots: this is a branch-consolidation PR for an
already-developed multi-user branch, and no browser screenshots were
captured during this heartbeat.
---------
Co-authored-by: dotta <dotta@example.com>
Co-authored-by: Paperclip <noreply@paperclip.ing>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-17 09:44:19 -05:00
|
|
|
const { data: companyMembers } = useQuery({
|
|
|
|
|
queryKey: queryKeys.access.companyUserDirectory(effectiveCompanyId!),
|
|
|
|
|
queryFn: () => accessApi.listUserDirectory(effectiveCompanyId!),
|
|
|
|
|
enabled: Boolean(effectiveCompanyId) && newIssueOpen,
|
|
|
|
|
});
|
2026-03-17 09:24:28 -05:00
|
|
|
const { data: experimentalSettings } = useQuery({
|
|
|
|
|
queryKey: queryKeys.instance.experimentalSettings,
|
|
|
|
|
queryFn: () => instanceSettingsApi.getExperimental(),
|
|
|
|
|
enabled: newIssueOpen,
|
2026-04-02 11:38:57 -05:00
|
|
|
retry: false,
|
2026-03-17 09:24:28 -05:00
|
|
|
});
|
2026-03-02 14:20:49 -06:00
|
|
|
const currentUserId = session?.user?.id ?? session?.session?.userId ?? null;
|
2026-03-14 17:47:53 -05:00
|
|
|
const activeProjects = useMemo(
|
|
|
|
|
() => (projects ?? []).filter((p) => !p.archivedAt),
|
|
|
|
|
[projects],
|
|
|
|
|
);
|
2026-03-02 14:20:49 -06:00
|
|
|
const { orderedProjects } = useProjectOrder({
|
2026-03-14 17:47:53 -05:00
|
|
|
projects: activeProjects,
|
2026-03-02 14:20:49 -06:00
|
|
|
companyId: effectiveCompanyId,
|
|
|
|
|
userId: currentUserId,
|
|
|
|
|
});
|
2026-02-17 10:53:20 -06:00
|
|
|
|
2026-03-12 16:11:37 -05:00
|
|
|
const selectedAssignee = useMemo(() => parseAssigneeValue(assigneeValue), [assigneeValue]);
|
|
|
|
|
const selectedAssigneeAgentId = selectedAssignee.assigneeAgentId;
|
|
|
|
|
const selectedAssigneeUserId = selectedAssignee.assigneeUserId;
|
|
|
|
|
|
|
|
|
|
const assigneeAdapterType = (agents ?? []).find((agent) => agent.id === selectedAssigneeAgentId)?.adapterType ?? null;
|
2026-02-26 10:32:44 -06:00
|
|
|
const supportsAssigneeOverrides = Boolean(
|
|
|
|
|
assigneeAdapterType && ISSUE_OVERRIDE_ADAPTER_TYPES.has(assigneeAdapterType),
|
|
|
|
|
);
|
2026-03-02 13:31:58 -06:00
|
|
|
const mentionOptions = useMemo<MentionOption[]>(() => {
|
feat: implement multi-user access and invite flows (#3784)
## Thinking Path
> - Paperclip is the control plane for autonomous AI companies.
> - V1 needs to stay local-first while also supporting shared,
authenticated deployments.
> - Human operators need real identities, company membership, invite
flows, profile surfaces, and company-scoped access controls.
> - Agents and operators also need the existing issue, inbox, workspace,
approval, and plugin flows to keep working under those authenticated
boundaries.
> - This branch accumulated the multi-user implementation, follow-up QA
fixes, workspace/runtime refinements, invite UX improvements,
release-branch conflict resolution, and review hardening.
> - This pull request consolidates that branch onto the current `master`
branch as a single reviewable PR.
> - The benefit is a complete multi-user implementation path with tests
and docs carried forward without dropping existing branch work.
## What Changed
- Added authenticated human-user access surfaces: auth/session routes,
company user directory, profile settings, company access/member
management, join requests, and invite management.
- Added invite creation, invite landing, onboarding, logo/branding,
invite grants, deduped join requests, and authenticated multi-user E2E
coverage.
- Tightened company-scoped and instance-admin authorization across
board, plugin, adapter, access, issue, and workspace routes.
- Added profile-image URL validation hardening, avatar preservation on
name-only profile updates, and join-request uniqueness migration cleanup
for pending human requests.
- Added an atomic member role/status/grants update path so Company
Access saves no longer leave partially updated permissions.
- Improved issue chat, inbox, assignee identity rendering,
sidebar/account/company navigation, workspace routing, and execution
workspace reuse behavior for multi-user operation.
- Added and updated server/UI tests covering auth, invites, membership,
issue workspace inheritance, plugin authz, inbox/chat behavior, and
multi-user flows.
- Merged current `public-gh/master` into this branch, resolved all
conflicts, and verified no `pnpm-lock.yaml` change is included in this
PR diff.
## Verification
- `pnpm exec vitest run server/src/__tests__/issues-service.test.ts
ui/src/components/IssueChatThread.test.tsx ui/src/pages/Inbox.test.tsx`
- `pnpm run preflight:workspace-links && pnpm exec vitest run
server/src/__tests__/plugin-routes-authz.test.ts`
- `pnpm exec vitest run server/src/__tests__/plugin-routes-authz.test.ts
server/src/__tests__/workspace-runtime-service-authz.test.ts
server/src/__tests__/access-validators.test.ts`
- `pnpm exec vitest run
server/src/__tests__/authz-company-access.test.ts
server/src/__tests__/routines-routes.test.ts
server/src/__tests__/sidebar-preferences-routes.test.ts
server/src/__tests__/approval-routes-idempotency.test.ts
server/src/__tests__/openclaw-invite-prompt-route.test.ts
server/src/__tests__/agent-cross-tenant-authz-routes.test.ts
server/src/__tests__/routines-e2e.test.ts`
- `pnpm exec vitest run server/src/__tests__/auth-routes.test.ts
ui/src/pages/CompanyAccess.test.tsx`
- `pnpm --filter @paperclipai/shared typecheck && pnpm --filter
@paperclipai/db typecheck && pnpm --filter @paperclipai/server
typecheck`
- `pnpm --filter @paperclipai/shared typecheck && pnpm --filter
@paperclipai/server typecheck`
- `pnpm --filter @paperclipai/ui typecheck`
- `pnpm db:generate`
- `npx playwright test --config tests/e2e/playwright.config.ts --list`
- Confirmed branch has no uncommitted changes and is `0` commits behind
`public-gh/master` before PR creation.
- Confirmed no `pnpm-lock.yaml` change is staged or present in the PR
diff.
## Risks
- High review surface area: this PR contains the accumulated multi-user
branch plus follow-up fixes, so reviewers should focus especially on
company-boundary enforcement and authenticated-vs-local deployment
behavior.
- UI behavior changed across invites, inbox, issue chat, access
settings, and sidebar navigation; no browser screenshots are included in
this branch-consolidation PR.
- Plugin install, upgrade, and lifecycle/config mutations now require
instance-admin access, which is intentional but may change expectations
for non-admin board users.
- A join-request dedupe migration rejects duplicate pending human
requests before creating unique indexes; deployments with unusual
historical duplicates should review the migration behavior.
- Company member role/status/grant saves now use a new combined
endpoint; older separate endpoints remain for compatibility.
- Full production build was not run locally in this heartbeat; CI should
cover the full matrix.
## Model Used
- OpenAI Codex coding agent, GPT-5-based model, CLI/tool-use
environment. Exact deployed model identifier and context window were not
exposed by the runtime.
## Checklist
- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] If this change affects the UI, I have included before/after
screenshots
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [x] I will address all Greptile and reviewer comments before
requesting merge
Note on screenshots: this is a branch-consolidation PR for an
already-developed multi-user branch, and no browser screenshots were
captured during this heartbeat.
---------
Co-authored-by: dotta <dotta@example.com>
Co-authored-by: Paperclip <noreply@paperclip.ing>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-17 09:44:19 -05:00
|
|
|
return buildMarkdownMentionOptions({
|
|
|
|
|
agents,
|
|
|
|
|
projects: orderedProjects,
|
|
|
|
|
members: companyMembers?.users,
|
|
|
|
|
});
|
|
|
|
|
}, [agents, companyMembers?.users, orderedProjects]);
|
2026-02-26 10:32:44 -06:00
|
|
|
|
|
|
|
|
const { data: assigneeAdapterModels } = useQuery({
|
2026-03-05 15:24:20 +01:00
|
|
|
queryKey:
|
|
|
|
|
effectiveCompanyId && assigneeAdapterType
|
|
|
|
|
? queryKeys.agents.adapterModels(effectiveCompanyId, assigneeAdapterType)
|
|
|
|
|
: ["agents", "none", "adapter-models", assigneeAdapterType ?? "none"],
|
|
|
|
|
queryFn: () => agentsApi.adapterModels(effectiveCompanyId!, assigneeAdapterType!),
|
|
|
|
|
enabled: Boolean(effectiveCompanyId) && newIssueOpen && supportsAssigneeOverrides,
|
2026-02-26 10:32:44 -06:00
|
|
|
});
|
|
|
|
|
|
2026-02-17 12:24:48 -06:00
|
|
|
const createIssue = useMutation({
|
2026-03-14 07:13:59 -05:00
|
|
|
mutationFn: async ({
|
|
|
|
|
companyId,
|
|
|
|
|
stagedFiles: pendingStagedFiles,
|
|
|
|
|
...data
|
|
|
|
|
}: { companyId: string; stagedFiles: StagedIssueFile[] } & Record<string, unknown>) => {
|
|
|
|
|
const issue = await issuesApi.create(companyId, data);
|
|
|
|
|
const failures: string[] = [];
|
|
|
|
|
|
|
|
|
|
for (const stagedFile of pendingStagedFiles) {
|
|
|
|
|
try {
|
|
|
|
|
if (stagedFile.kind === "document") {
|
|
|
|
|
const body = await stagedFile.file.text();
|
|
|
|
|
await issuesApi.upsertDocument(issue.id, stagedFile.documentKey ?? "document", {
|
|
|
|
|
title: stagedFile.documentKey === "plan" ? null : stagedFile.title ?? null,
|
|
|
|
|
format: "markdown",
|
|
|
|
|
body,
|
|
|
|
|
baseRevisionId: null,
|
|
|
|
|
});
|
|
|
|
|
} else {
|
|
|
|
|
await issuesApi.uploadAttachment(companyId, issue.id, stagedFile.file);
|
|
|
|
|
}
|
|
|
|
|
} catch {
|
|
|
|
|
failures.push(stagedFile.file.name);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return { issue, companyId, failures };
|
|
|
|
|
},
|
|
|
|
|
onSuccess: ({ issue, companyId, failures }) => {
|
|
|
|
|
queryClient.invalidateQueries({ queryKey: queryKeys.issues.list(companyId) });
|
2026-03-26 08:19:16 -05:00
|
|
|
queryClient.invalidateQueries({ queryKey: queryKeys.issues.listMineByMe(companyId) });
|
2026-03-17 16:14:43 -05:00
|
|
|
queryClient.invalidateQueries({ queryKey: queryKeys.issues.listTouchedByMe(companyId) });
|
|
|
|
|
queryClient.invalidateQueries({ queryKey: queryKeys.issues.listUnreadTouchedByMe(companyId) });
|
|
|
|
|
queryClient.invalidateQueries({ queryKey: queryKeys.sidebarBadges(companyId) });
|
UI: approval detail page, agent hiring UX, costs breakdown, sidebar badges, and dashboard improvements
Add ApprovalDetail page with comment thread, revision request/resubmit flow,
and ApprovalPayload component for structured payload display. Extend AgentDetail
with permissions management, config revision history, and duplicate action.
Add agent hire dialog with permission-gated access. Rework Costs page with
per-agent breakdown table and period filtering. Add sidebar badge counts for
pending approvals and inbox items. Enhance Dashboard with live metrics and
sparkline trends. Extend Agents list with pending_approval status and bulk
actions. Update IssueDetail with approval linking. Various component improvements
to MetricCard, InlineEditor, CommentThread, and StatusBadge.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 13:03:08 -06:00
|
|
|
if (draftTimer.current) clearTimeout(draftTimer.current);
|
2026-03-14 07:13:59 -05:00
|
|
|
if (failures.length > 0) {
|
|
|
|
|
const prefix = (companies.find((company) => company.id === companyId)?.issuePrefix ?? "").trim();
|
|
|
|
|
const issueRef = issue.identifier ?? issue.id;
|
|
|
|
|
pushToast({
|
|
|
|
|
title: `Created ${issueRef} with upload warnings`,
|
|
|
|
|
body: `${failures.length} staged ${failures.length === 1 ? "file" : "files"} could not be added.`,
|
|
|
|
|
tone: "warn",
|
|
|
|
|
action: prefix
|
|
|
|
|
? { label: `Open ${issueRef}`, href: `/${prefix}/issues/${issueRef}` }
|
|
|
|
|
: undefined,
|
|
|
|
|
});
|
|
|
|
|
}
|
2026-02-17 20:46:12 -06:00
|
|
|
clearDraft();
|
2026-02-17 12:24:48 -06:00
|
|
|
reset();
|
|
|
|
|
closeNewIssue();
|
|
|
|
|
},
|
|
|
|
|
});
|
2026-02-17 10:53:20 -06:00
|
|
|
|
Add MarkdownEditor component, asset image upload, and rich description editing
Introduce MarkdownEditor built on @mdxeditor/editor with headings,
lists, links, quotes, image upload with drag-and-drop, and themed CSS
integration. Add asset image upload API (routes, service, storage) and
wire image upload into InlineEditor multiline mode, NewIssueDialog,
NewProjectDialog, GoalDetail, IssueDetail, and ProjectDetail
description fields. Tighten prompt template editor styling.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 12:50:45 -06:00
|
|
|
const uploadDescriptionImage = useMutation({
|
|
|
|
|
mutationFn: async (file: File) => {
|
2026-02-26 16:33:48 -06:00
|
|
|
if (!effectiveCompanyId) throw new Error("No company selected");
|
|
|
|
|
return assetsApi.uploadImage(effectiveCompanyId, file, "issues/drafts");
|
Add MarkdownEditor component, asset image upload, and rich description editing
Introduce MarkdownEditor built on @mdxeditor/editor with headings,
lists, links, quotes, image upload with drag-and-drop, and themed CSS
integration. Add asset image upload API (routes, service, storage) and
wire image upload into InlineEditor multiline mode, NewIssueDialog,
NewProjectDialog, GoalDetail, IssueDetail, and ProjectDetail
description fields. Tighten prompt template editor styling.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 12:50:45 -06:00
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
2026-02-17 20:46:12 -06:00
|
|
|
// Debounced draft saving
|
|
|
|
|
const scheduleSave = useCallback(
|
|
|
|
|
(draft: IssueDraft) => {
|
|
|
|
|
if (draftTimer.current) clearTimeout(draftTimer.current);
|
|
|
|
|
draftTimer.current = setTimeout(() => {
|
|
|
|
|
if (draft.title.trim()) saveDraft(draft);
|
|
|
|
|
}, DEBOUNCE_MS);
|
|
|
|
|
},
|
|
|
|
|
[],
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// Save draft on meaningful changes
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
if (!newIssueOpen) return;
|
2026-02-26 10:32:44 -06:00
|
|
|
scheduleSave({
|
|
|
|
|
title,
|
|
|
|
|
description,
|
|
|
|
|
status,
|
|
|
|
|
priority,
|
2026-03-12 16:11:37 -05:00
|
|
|
assigneeValue,
|
2026-04-06 08:40:38 -05:00
|
|
|
reviewerValue,
|
|
|
|
|
approverValue,
|
2026-02-26 10:32:44 -06:00
|
|
|
projectId,
|
2026-03-13 17:12:25 -05:00
|
|
|
projectWorkspaceId,
|
2026-02-26 10:32:44 -06:00
|
|
|
assigneeModelOverride,
|
|
|
|
|
assigneeThinkingEffort,
|
2026-02-26 16:33:48 -06:00
|
|
|
assigneeChrome,
|
2026-03-13 17:12:25 -05:00
|
|
|
executionWorkspaceMode,
|
|
|
|
|
selectedExecutionWorkspaceId,
|
2026-02-26 10:32:44 -06:00
|
|
|
});
|
|
|
|
|
}, [
|
|
|
|
|
title,
|
|
|
|
|
description,
|
|
|
|
|
status,
|
|
|
|
|
priority,
|
2026-03-12 16:11:37 -05:00
|
|
|
assigneeValue,
|
2026-04-06 08:40:38 -05:00
|
|
|
reviewerValue,
|
|
|
|
|
approverValue,
|
2026-02-26 10:32:44 -06:00
|
|
|
projectId,
|
2026-03-13 17:12:25 -05:00
|
|
|
projectWorkspaceId,
|
2026-02-26 10:32:44 -06:00
|
|
|
assigneeModelOverride,
|
|
|
|
|
assigneeThinkingEffort,
|
2026-02-26 16:33:48 -06:00
|
|
|
assigneeChrome,
|
2026-03-13 17:12:25 -05:00
|
|
|
executionWorkspaceMode,
|
|
|
|
|
selectedExecutionWorkspaceId,
|
2026-02-26 10:32:44 -06:00
|
|
|
newIssueOpen,
|
|
|
|
|
scheduleSave,
|
|
|
|
|
]);
|
2026-02-17 20:46:12 -06:00
|
|
|
|
|
|
|
|
// Restore draft or apply defaults when dialog opens
|
2026-02-17 10:53:20 -06:00
|
|
|
useEffect(() => {
|
2026-02-17 20:46:12 -06:00
|
|
|
if (!newIssueOpen) return;
|
2026-02-26 16:33:48 -06:00
|
|
|
setDialogCompanyId(selectedCompanyId);
|
2026-03-10 09:03:31 -05:00
|
|
|
executionWorkspaceDefaultProjectId.current = null;
|
2026-02-17 20:46:12 -06:00
|
|
|
|
|
|
|
|
const draft = loadDraft();
|
2026-04-06 10:58:59 -05:00
|
|
|
if (newIssueDefaults.parentId) {
|
|
|
|
|
const defaultProjectId = newIssueDefaults.projectId ?? "";
|
|
|
|
|
const defaultProject = orderedProjects.find((project) => project.id === defaultProjectId);
|
2026-04-06 11:34:11 -05:00
|
|
|
const defaultProjectWorkspaceId = newIssueDefaults.projectWorkspaceId
|
|
|
|
|
?? defaultProjectWorkspaceIdForProject(defaultProject);
|
|
|
|
|
const defaultExecutionWorkspaceMode = newIssueDefaults.executionWorkspaceId
|
|
|
|
|
? "reuse_existing"
|
|
|
|
|
: (newIssueDefaults.executionWorkspaceMode ?? defaultExecutionWorkspaceModeForProject(defaultProject));
|
2026-04-06 10:58:59 -05:00
|
|
|
setTitle(newIssueDefaults.title ?? "");
|
|
|
|
|
setDescription(newIssueDefaults.description ?? "");
|
|
|
|
|
setStatus(newIssueDefaults.status ?? "todo");
|
|
|
|
|
setPriority(newIssueDefaults.priority ?? "");
|
|
|
|
|
setProjectId(defaultProjectId);
|
2026-04-06 11:34:11 -05:00
|
|
|
setProjectWorkspaceId(defaultProjectWorkspaceId);
|
2026-04-06 10:58:59 -05:00
|
|
|
setAssigneeValue(assigneeValueFromSelection(newIssueDefaults));
|
|
|
|
|
setAssigneeModelOverride("");
|
|
|
|
|
setAssigneeThinkingEffort("");
|
|
|
|
|
setAssigneeChrome(false);
|
2026-04-06 11:34:11 -05:00
|
|
|
setExecutionWorkspaceMode(defaultExecutionWorkspaceMode);
|
|
|
|
|
setSelectedExecutionWorkspaceId(newIssueDefaults.executionWorkspaceId ?? "");
|
2026-04-06 10:58:59 -05:00
|
|
|
executionWorkspaceDefaultProjectId.current = defaultProjectId || null;
|
|
|
|
|
} else if (newIssueDefaults.title) {
|
2026-03-07 08:26:49 -06:00
|
|
|
setTitle(newIssueDefaults.title);
|
|
|
|
|
setDescription(newIssueDefaults.description ?? "");
|
|
|
|
|
setStatus(newIssueDefaults.status ?? "todo");
|
|
|
|
|
setPriority(newIssueDefaults.priority ?? "");
|
2026-03-13 17:12:25 -05:00
|
|
|
const defaultProjectId = newIssueDefaults.projectId ?? "";
|
|
|
|
|
const defaultProject = orderedProjects.find((project) => project.id === defaultProjectId);
|
|
|
|
|
setProjectId(defaultProjectId);
|
|
|
|
|
setProjectWorkspaceId(defaultProjectWorkspaceIdForProject(defaultProject));
|
2026-03-12 16:11:37 -05:00
|
|
|
setAssigneeValue(assigneeValueFromSelection(newIssueDefaults));
|
2026-04-06 08:40:38 -05:00
|
|
|
setReviewerValue("");
|
|
|
|
|
setApproverValue("");
|
2026-04-06 19:59:17 -05:00
|
|
|
setShowReviewerRow(false);
|
|
|
|
|
setShowApproverRow(false);
|
2026-03-07 08:26:49 -06:00
|
|
|
setAssigneeModelOverride("");
|
|
|
|
|
setAssigneeThinkingEffort("");
|
|
|
|
|
setAssigneeChrome(false);
|
2026-03-13 17:12:25 -05:00
|
|
|
setExecutionWorkspaceMode(defaultExecutionWorkspaceModeForProject(defaultProject));
|
|
|
|
|
setSelectedExecutionWorkspaceId("");
|
|
|
|
|
executionWorkspaceDefaultProjectId.current = defaultProjectId || null;
|
2026-03-07 08:26:49 -06:00
|
|
|
} else if (draft && draft.title.trim()) {
|
2026-03-13 17:12:25 -05:00
|
|
|
const restoredProjectId = newIssueDefaults.projectId ?? draft.projectId;
|
|
|
|
|
const restoredProject = orderedProjects.find((project) => project.id === restoredProjectId);
|
2026-02-17 20:46:12 -06:00
|
|
|
setTitle(draft.title);
|
|
|
|
|
setDescription(draft.description);
|
|
|
|
|
setStatus(draft.status || "todo");
|
|
|
|
|
setPriority(draft.priority);
|
2026-03-12 16:11:37 -05:00
|
|
|
setAssigneeValue(
|
|
|
|
|
newIssueDefaults.assigneeAgentId || newIssueDefaults.assigneeUserId
|
|
|
|
|
? assigneeValueFromSelection(newIssueDefaults)
|
|
|
|
|
: (draft.assigneeValue ?? draft.assigneeId ?? ""),
|
|
|
|
|
);
|
2026-04-06 08:40:38 -05:00
|
|
|
setReviewerValue(draft.reviewerValue ?? "");
|
|
|
|
|
setApproverValue(draft.approverValue ?? "");
|
2026-04-06 19:59:17 -05:00
|
|
|
setShowReviewerRow(!!(draft.reviewerValue));
|
|
|
|
|
setShowApproverRow(!!(draft.approverValue));
|
2026-03-13 17:12:25 -05:00
|
|
|
setProjectId(restoredProjectId);
|
|
|
|
|
setProjectWorkspaceId(draft.projectWorkspaceId ?? defaultProjectWorkspaceIdForProject(restoredProject));
|
2026-02-26 10:32:44 -06:00
|
|
|
setAssigneeModelOverride(draft.assigneeModelOverride ?? "");
|
|
|
|
|
setAssigneeThinkingEffort(draft.assigneeThinkingEffort ?? "");
|
2026-02-26 16:33:48 -06:00
|
|
|
setAssigneeChrome(draft.assigneeChrome ?? false);
|
2026-03-13 17:12:25 -05:00
|
|
|
setExecutionWorkspaceMode(
|
|
|
|
|
draft.executionWorkspaceMode
|
|
|
|
|
?? (draft.useIsolatedExecutionWorkspace ? "isolated_workspace" : defaultExecutionWorkspaceModeForProject(restoredProject)),
|
|
|
|
|
);
|
|
|
|
|
setSelectedExecutionWorkspaceId(draft.selectedExecutionWorkspaceId ?? "");
|
|
|
|
|
executionWorkspaceDefaultProjectId.current = restoredProjectId || null;
|
2026-02-17 20:46:12 -06:00
|
|
|
} else {
|
2026-03-13 17:12:25 -05:00
|
|
|
const defaultProjectId = newIssueDefaults.projectId ?? "";
|
|
|
|
|
const defaultProject = orderedProjects.find((project) => project.id === defaultProjectId);
|
2026-02-17 10:53:20 -06:00
|
|
|
setStatus(newIssueDefaults.status ?? "todo");
|
|
|
|
|
setPriority(newIssueDefaults.priority ?? "");
|
2026-03-13 17:12:25 -05:00
|
|
|
setProjectId(defaultProjectId);
|
|
|
|
|
setProjectWorkspaceId(defaultProjectWorkspaceIdForProject(defaultProject));
|
2026-03-12 16:11:37 -05:00
|
|
|
setAssigneeValue(assigneeValueFromSelection(newIssueDefaults));
|
2026-04-06 08:40:38 -05:00
|
|
|
setReviewerValue("");
|
|
|
|
|
setApproverValue("");
|
2026-04-06 19:59:17 -05:00
|
|
|
setShowReviewerRow(false);
|
|
|
|
|
setShowApproverRow(false);
|
2026-02-26 10:32:44 -06:00
|
|
|
setAssigneeModelOverride("");
|
|
|
|
|
setAssigneeThinkingEffort("");
|
2026-02-26 16:33:48 -06:00
|
|
|
setAssigneeChrome(false);
|
2026-03-13 17:12:25 -05:00
|
|
|
setExecutionWorkspaceMode(defaultExecutionWorkspaceModeForProject(defaultProject));
|
|
|
|
|
setSelectedExecutionWorkspaceId("");
|
|
|
|
|
executionWorkspaceDefaultProjectId.current = defaultProjectId || null;
|
2026-02-17 10:53:20 -06:00
|
|
|
}
|
2026-03-13 17:12:25 -05:00
|
|
|
}, [newIssueOpen, newIssueDefaults, orderedProjects]);
|
2026-02-17 10:53:20 -06:00
|
|
|
|
2026-02-26 10:32:44 -06:00
|
|
|
useEffect(() => {
|
|
|
|
|
if (!supportsAssigneeOverrides) {
|
|
|
|
|
setAssigneeOptionsOpen(false);
|
|
|
|
|
setAssigneeModelOverride("");
|
|
|
|
|
setAssigneeThinkingEffort("");
|
2026-02-26 16:33:48 -06:00
|
|
|
setAssigneeChrome(false);
|
2026-02-26 10:32:44 -06:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const validThinkingValues =
|
|
|
|
|
assigneeAdapterType === "codex_local"
|
|
|
|
|
? ISSUE_THINKING_EFFORT_OPTIONS.codex_local
|
2026-03-04 16:48:54 -06:00
|
|
|
: assigneeAdapterType === "opencode_local"
|
|
|
|
|
? ISSUE_THINKING_EFFORT_OPTIONS.opencode_local
|
2026-03-05 15:24:20 +01:00
|
|
|
: ISSUE_THINKING_EFFORT_OPTIONS.claude_local;
|
2026-02-26 10:32:44 -06:00
|
|
|
if (!validThinkingValues.some((option) => option.value === assigneeThinkingEffort)) {
|
|
|
|
|
setAssigneeThinkingEffort("");
|
|
|
|
|
}
|
|
|
|
|
}, [supportsAssigneeOverrides, assigneeAdapterType, assigneeThinkingEffort]);
|
|
|
|
|
|
2026-02-17 20:46:12 -06:00
|
|
|
// Cleanup timer on unmount
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
return () => {
|
|
|
|
|
if (draftTimer.current) clearTimeout(draftTimer.current);
|
|
|
|
|
};
|
|
|
|
|
}, []);
|
|
|
|
|
|
Add detail pages, property panels, and restyle list pages
New pages: AgentDetail, GoalDetail, IssueDetail, ProjectDetail, Inbox,
MyIssues. New feature components: AgentProperties, GoalProperties,
IssueProperties, ProjectProperties, GoalTree, NewIssueDialog. Add
heartbeats API client. Restyle all list pages (Agents, Issues, Goals,
Projects, Dashboard, Costs, Activity, Org) with EntityRow, FilterBar,
and improved layouts. Add routing for detail views.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 09:57:06 -06:00
|
|
|
function reset() {
|
|
|
|
|
setTitle("");
|
|
|
|
|
setDescription("");
|
|
|
|
|
setStatus("todo");
|
2026-02-17 10:53:20 -06:00
|
|
|
setPriority("");
|
2026-03-12 16:11:37 -05:00
|
|
|
setAssigneeValue("");
|
2026-04-06 08:40:38 -05:00
|
|
|
setReviewerValue("");
|
|
|
|
|
setApproverValue("");
|
2026-04-06 19:59:17 -05:00
|
|
|
setShowReviewerRow(false);
|
|
|
|
|
setShowApproverRow(false);
|
2026-02-17 10:53:20 -06:00
|
|
|
setProjectId("");
|
2026-03-13 17:12:25 -05:00
|
|
|
setProjectWorkspaceId("");
|
2026-02-26 10:32:44 -06:00
|
|
|
setAssigneeOptionsOpen(false);
|
|
|
|
|
setAssigneeModelOverride("");
|
|
|
|
|
setAssigneeThinkingEffort("");
|
2026-02-26 16:33:48 -06:00
|
|
|
setAssigneeChrome(false);
|
2026-03-13 17:12:25 -05:00
|
|
|
setExecutionWorkspaceMode("shared_workspace");
|
|
|
|
|
setSelectedExecutionWorkspaceId("");
|
2026-02-17 10:53:20 -06:00
|
|
|
setExpanded(false);
|
2026-02-26 16:33:48 -06:00
|
|
|
setDialogCompanyId(null);
|
2026-03-14 07:13:59 -05:00
|
|
|
setStagedFiles([]);
|
|
|
|
|
setIsFileDragOver(false);
|
2026-02-26 16:33:48 -06:00
|
|
|
setCompanyOpen(false);
|
2026-03-10 09:03:31 -05:00
|
|
|
executionWorkspaceDefaultProjectId.current = null;
|
2026-02-26 16:33:48 -06:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function handleCompanyChange(companyId: string) {
|
2026-04-06 10:58:59 -05:00
|
|
|
if (isSubIssueMode) return;
|
2026-02-26 16:33:48 -06:00
|
|
|
if (companyId === effectiveCompanyId) return;
|
|
|
|
|
setDialogCompanyId(companyId);
|
2026-03-12 16:11:37 -05:00
|
|
|
setAssigneeValue("");
|
2026-04-06 08:40:38 -05:00
|
|
|
setReviewerValue("");
|
|
|
|
|
setApproverValue("");
|
2026-04-06 19:59:17 -05:00
|
|
|
setShowReviewerRow(false);
|
|
|
|
|
setShowApproverRow(false);
|
2026-02-26 16:33:48 -06:00
|
|
|
setProjectId("");
|
2026-03-13 17:12:25 -05:00
|
|
|
setProjectWorkspaceId("");
|
2026-02-26 16:33:48 -06:00
|
|
|
setAssigneeModelOverride("");
|
|
|
|
|
setAssigneeThinkingEffort("");
|
|
|
|
|
setAssigneeChrome(false);
|
2026-03-13 17:12:25 -05:00
|
|
|
setExecutionWorkspaceMode("shared_workspace");
|
|
|
|
|
setSelectedExecutionWorkspaceId("");
|
Add detail pages, property panels, and restyle list pages
New pages: AgentDetail, GoalDetail, IssueDetail, ProjectDetail, Inbox,
MyIssues. New feature components: AgentProperties, GoalProperties,
IssueProperties, ProjectProperties, GoalTree, NewIssueDialog. Add
heartbeats API client. Restyle all list pages (Agents, Issues, Goals,
Projects, Dashboard, Costs, Activity, Org) with EntityRow, FilterBar,
and improved layouts. Add routing for detail views.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 09:57:06 -06:00
|
|
|
}
|
|
|
|
|
|
2026-02-17 20:46:12 -06:00
|
|
|
function discardDraft() {
|
|
|
|
|
clearDraft();
|
|
|
|
|
reset();
|
|
|
|
|
closeNewIssue();
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-17 12:24:48 -06:00
|
|
|
function handleSubmit() {
|
2026-03-10 21:06:16 -05:00
|
|
|
if (!effectiveCompanyId || !title.trim() || createIssue.isPending) return;
|
2026-02-26 10:32:44 -06:00
|
|
|
const assigneeAdapterOverrides = buildAssigneeAdapterOverrides({
|
|
|
|
|
adapterType: assigneeAdapterType,
|
|
|
|
|
modelOverride: assigneeModelOverride,
|
|
|
|
|
thinkingEffortOverride: assigneeThinkingEffort,
|
2026-02-26 16:33:48 -06:00
|
|
|
chrome: assigneeChrome,
|
2026-02-26 10:32:44 -06:00
|
|
|
});
|
2026-03-10 09:03:31 -05:00
|
|
|
const selectedProject = orderedProjects.find((project) => project.id === projectId);
|
2026-03-17 09:24:28 -05:00
|
|
|
const executionWorkspacePolicy =
|
|
|
|
|
experimentalSettings?.enableIsolatedWorkspaces === true
|
|
|
|
|
? selectedProject?.executionWorkspacePolicy ?? null
|
|
|
|
|
: null;
|
2026-03-17 07:46:40 -05:00
|
|
|
const selectedReusableExecutionWorkspace = deduplicatedReusableWorkspaces.find(
|
2026-03-13 17:12:25 -05:00
|
|
|
(workspace) => workspace.id === selectedExecutionWorkspaceId,
|
|
|
|
|
);
|
|
|
|
|
const requestedExecutionWorkspaceMode =
|
|
|
|
|
executionWorkspaceMode === "reuse_existing"
|
|
|
|
|
? issueExecutionWorkspaceModeForExistingWorkspace(selectedReusableExecutionWorkspace?.mode)
|
|
|
|
|
: executionWorkspaceMode;
|
2026-03-10 09:03:31 -05:00
|
|
|
const executionWorkspaceSettings = executionWorkspacePolicy?.enabled
|
2026-03-13 17:12:25 -05:00
|
|
|
? { mode: requestedExecutionWorkspaceMode }
|
2026-03-10 09:03:31 -05:00
|
|
|
: null;
|
2026-04-06 08:40:38 -05:00
|
|
|
const executionPolicy = buildExecutionPolicy({
|
|
|
|
|
reviewerValues: reviewerValue ? [reviewerValue] : [],
|
|
|
|
|
approverValues: approverValue ? [approverValue] : [],
|
|
|
|
|
});
|
2026-02-17 12:24:48 -06:00
|
|
|
createIssue.mutate({
|
2026-02-26 16:33:48 -06:00
|
|
|
companyId: effectiveCompanyId,
|
2026-03-14 07:13:59 -05:00
|
|
|
stagedFiles,
|
2026-02-17 12:24:48 -06:00
|
|
|
title: title.trim(),
|
|
|
|
|
description: description.trim() || undefined,
|
|
|
|
|
status,
|
|
|
|
|
priority: priority || "medium",
|
2026-03-12 16:11:37 -05:00
|
|
|
...(selectedAssigneeAgentId ? { assigneeAgentId: selectedAssigneeAgentId } : {}),
|
|
|
|
|
...(selectedAssigneeUserId ? { assigneeUserId: selectedAssigneeUserId } : {}),
|
2026-04-06 10:58:59 -05:00
|
|
|
...(newIssueDefaults.parentId ? { parentId: newIssueDefaults.parentId } : {}),
|
|
|
|
|
...(newIssueDefaults.goalId ? { goalId: newIssueDefaults.goalId } : {}),
|
2026-02-17 12:24:48 -06:00
|
|
|
...(projectId ? { projectId } : {}),
|
2026-03-13 17:12:25 -05:00
|
|
|
...(projectWorkspaceId ? { projectWorkspaceId } : {}),
|
2026-02-26 10:32:44 -06:00
|
|
|
...(assigneeAdapterOverrides ? { assigneeAdapterOverrides } : {}),
|
2026-03-13 17:12:25 -05:00
|
|
|
...(executionWorkspacePolicy?.enabled ? { executionWorkspacePreference: executionWorkspaceMode } : {}),
|
|
|
|
|
...(executionWorkspaceMode === "reuse_existing" && selectedExecutionWorkspaceId
|
|
|
|
|
? { executionWorkspaceId: selectedExecutionWorkspaceId }
|
|
|
|
|
: {}),
|
2026-03-10 09:03:31 -05:00
|
|
|
...(executionWorkspaceSettings ? { executionWorkspaceSettings } : {}),
|
2026-04-06 08:40:38 -05:00
|
|
|
...(executionPolicy ? { executionPolicy } : {}),
|
2026-02-17 12:24:48 -06:00
|
|
|
});
|
Add detail pages, property panels, and restyle list pages
New pages: AgentDetail, GoalDetail, IssueDetail, ProjectDetail, Inbox,
MyIssues. New feature components: AgentProperties, GoalProperties,
IssueProperties, ProjectProperties, GoalTree, NewIssueDialog. Add
heartbeats API client. Restyle all list pages (Agents, Issues, Goals,
Projects, Dashboard, Costs, Activity, Org) with EntityRow, FilterBar,
and improved layouts. Add routing for detail views.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 09:57:06 -06:00
|
|
|
}
|
|
|
|
|
|
2026-02-17 10:53:20 -06:00
|
|
|
function handleKeyDown(e: React.KeyboardEvent) {
|
|
|
|
|
if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
handleSubmit();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-14 07:13:59 -05:00
|
|
|
function stageFiles(files: File[]) {
|
|
|
|
|
if (files.length === 0) return;
|
|
|
|
|
setStagedFiles((current) => {
|
|
|
|
|
const next = [...current];
|
|
|
|
|
for (const file of files) {
|
2026-03-14 07:21:21 -05:00
|
|
|
if (isTextDocumentFile(file)) {
|
2026-03-14 07:13:59 -05:00
|
|
|
const baseName = fileBaseName(file.name);
|
|
|
|
|
const documentKey = createUniqueDocumentKey(slugifyDocumentKey(baseName), next);
|
|
|
|
|
next.push({
|
|
|
|
|
id: `${file.name}:${file.size}:${file.lastModified}:${documentKey}`,
|
|
|
|
|
file,
|
|
|
|
|
kind: "document",
|
|
|
|
|
documentKey,
|
|
|
|
|
title: titleizeFilename(baseName),
|
|
|
|
|
});
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
next.push({
|
|
|
|
|
id: `${file.name}:${file.size}:${file.lastModified}`,
|
|
|
|
|
file,
|
|
|
|
|
kind: "attachment",
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
return next;
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function handleStageFilesPicked(evt: ChangeEvent<HTMLInputElement>) {
|
|
|
|
|
stageFiles(Array.from(evt.target.files ?? []));
|
|
|
|
|
if (stageFileInputRef.current) {
|
|
|
|
|
stageFileInputRef.current.value = "";
|
2026-02-25 21:36:06 -06:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-14 07:13:59 -05:00
|
|
|
function handleFileDragEnter(evt: DragEvent<HTMLDivElement>) {
|
|
|
|
|
if (!evt.dataTransfer.types.includes("Files")) return;
|
|
|
|
|
evt.preventDefault();
|
|
|
|
|
setIsFileDragOver(true);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function handleFileDragOver(evt: DragEvent<HTMLDivElement>) {
|
|
|
|
|
if (!evt.dataTransfer.types.includes("Files")) return;
|
|
|
|
|
evt.preventDefault();
|
|
|
|
|
evt.dataTransfer.dropEffect = "copy";
|
|
|
|
|
setIsFileDragOver(true);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function handleFileDragLeave(evt: DragEvent<HTMLDivElement>) {
|
|
|
|
|
if (evt.currentTarget.contains(evt.relatedTarget as Node | null)) return;
|
|
|
|
|
setIsFileDragOver(false);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function handleFileDrop(evt: DragEvent<HTMLDivElement>) {
|
|
|
|
|
if (!evt.dataTransfer.files.length) return;
|
|
|
|
|
evt.preventDefault();
|
|
|
|
|
setIsFileDragOver(false);
|
|
|
|
|
stageFiles(Array.from(evt.dataTransfer.files));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function removeStagedFile(id: string) {
|
|
|
|
|
setStagedFiles((current) => current.filter((file) => file.id !== id));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const hasDraft = title.trim().length > 0 || description.trim().length > 0 || stagedFiles.length > 0;
|
2026-02-17 10:53:20 -06:00
|
|
|
const currentStatus = statuses.find((s) => s.value === status) ?? statuses[1]!;
|
|
|
|
|
const currentPriority = priorities.find((p) => p.value === priority);
|
2026-03-12 16:11:37 -05:00
|
|
|
const currentAssignee = selectedAssigneeAgentId
|
|
|
|
|
? (agents ?? []).find((a) => a.id === selectedAssigneeAgentId)
|
|
|
|
|
: null;
|
2026-03-02 14:20:49 -06:00
|
|
|
const currentProject = orderedProjects.find((project) => project.id === projectId);
|
2026-03-17 09:24:28 -05:00
|
|
|
const currentProjectExecutionWorkspacePolicy =
|
|
|
|
|
experimentalSettings?.enableIsolatedWorkspaces === true
|
|
|
|
|
? currentProject?.executionWorkspacePolicy ?? null
|
|
|
|
|
: null;
|
2026-03-10 09:03:31 -05:00
|
|
|
const currentProjectSupportsExecutionWorkspace = Boolean(currentProjectExecutionWorkspacePolicy?.enabled);
|
2026-03-17 07:46:40 -05:00
|
|
|
const deduplicatedReusableWorkspaces = useMemo(() => {
|
|
|
|
|
const workspaces = reusableExecutionWorkspaces ?? [];
|
|
|
|
|
const seen = new Map<string, typeof workspaces[number]>();
|
|
|
|
|
for (const ws of workspaces) {
|
|
|
|
|
const key = ws.cwd ?? ws.id;
|
|
|
|
|
const existing = seen.get(key);
|
|
|
|
|
if (!existing || new Date(ws.lastUsedAt) > new Date(existing.lastUsedAt)) {
|
|
|
|
|
seen.set(key, ws);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return Array.from(seen.values());
|
|
|
|
|
}, [reusableExecutionWorkspaces]);
|
|
|
|
|
const selectedReusableExecutionWorkspace = deduplicatedReusableWorkspaces.find(
|
2026-03-13 17:12:25 -05:00
|
|
|
(workspace) => workspace.id === selectedExecutionWorkspaceId,
|
|
|
|
|
);
|
2026-04-06 11:34:11 -05:00
|
|
|
const isUsingParentExecutionWorkspace = isSubIssueMode && parentExecutionWorkspaceId
|
|
|
|
|
? executionWorkspaceMode === "reuse_existing" && selectedExecutionWorkspaceId === parentExecutionWorkspaceId
|
|
|
|
|
: false;
|
|
|
|
|
const showParentWorkspaceWarning = isSubIssueMode
|
|
|
|
|
&& currentProjectSupportsExecutionWorkspace
|
|
|
|
|
&& Boolean(parentExecutionWorkspaceId)
|
|
|
|
|
&& !isUsingParentExecutionWorkspace;
|
2026-02-26 10:32:44 -06:00
|
|
|
const assigneeOptionsTitle =
|
|
|
|
|
assigneeAdapterType === "claude_local"
|
|
|
|
|
? "Claude options"
|
|
|
|
|
: assigneeAdapterType === "codex_local"
|
|
|
|
|
? "Codex options"
|
2026-03-04 16:48:54 -06:00
|
|
|
: assigneeAdapterType === "opencode_local"
|
|
|
|
|
? "OpenCode options"
|
2026-02-26 10:32:44 -06:00
|
|
|
: "Agent options";
|
|
|
|
|
const thinkingEffortOptions =
|
|
|
|
|
assigneeAdapterType === "codex_local"
|
|
|
|
|
? ISSUE_THINKING_EFFORT_OPTIONS.codex_local
|
2026-03-04 16:48:54 -06:00
|
|
|
: assigneeAdapterType === "opencode_local"
|
|
|
|
|
? ISSUE_THINKING_EFFORT_OPTIONS.opencode_local
|
2026-02-26 10:32:44 -06:00
|
|
|
: ISSUE_THINKING_EFFORT_OPTIONS.claude_local;
|
2026-03-05 11:19:56 -06:00
|
|
|
const recentAssigneeIds = useMemo(() => getRecentAssigneeIds(), [newIssueOpen]);
|
[codex] Polish issue and operator workflow UI (#4090)
## Thinking Path
> - Paperclip operators spend much of their time in issues, inboxes,
selectors, and rich comment threads.
> - Small interaction problems in those surfaces slow down supervision
of AI-agent work.
> - The branch included related operator quality-of-life fixes for issue
layout, inbox actions, recent selectors, mobile inputs, and chat
rendering stability.
> - These changes are UI-focused and can land independently from
workspace navigation and access-profile work.
> - This pull request groups the operator QoL fixes into one standalone
branch.
> - The benefit is a more stable and efficient board workflow for issue
triage and task editing.
## What Changed
- Widened issue detail content and added a desktop inbox archive action.
- Fixed mobile text-field zoom by keeping touch input font sizes at
16px.
- Prioritized recent picker selections for assignees/projects in issue
and routine flows.
- Showed actionable approvals in the Mine inbox model.
- Fixed issue chat renderer state crashes and hardened tests.
## Verification
- `pnpm install --frozen-lockfile`
- `pnpm exec vitest run ui/src/components/IssueChatThread.test.tsx
ui/src/lib/inbox.test.ts ui/src/lib/recent-selections.test.ts`
- Split integration check: merged last after the other
[PAP-1614](/PAP/issues/PAP-1614) branches with no merge conflicts.
- Confirmed this branch does not include `pnpm-lock.yaml`.
## Risks
- Low to medium risk: mostly UI state, layout, and selection-priority
behavior.
- Visual layout and mobile zoom behavior may need browser/device QA
beyond component tests.
- No database migrations are included.
> For core feature work, check [`ROADMAP.md`](ROADMAP.md) first and
discuss it in `#dev` before opening the PR. Feature PRs that overlap
with planned core work may need to be redirected — check the roadmap
first. See `CONTRIBUTING.md`.
## Model Used
- OpenAI Codex, GPT-5.4 tool-enabled coding model, agentic
code-editing/runtime with local shell and GitHub CLI access; exact
context window and reasoning mode are not exposed by the Paperclip
harness.
## Checklist
- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] If this change affects the UI, I have included before/after
screenshots
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [x] I will address all Greptile and reviewer comments before
requesting merge
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-04-20 06:16:41 -05:00
|
|
|
const recentAssigneeOptionIds = useMemo(
|
|
|
|
|
() => recentAssigneeIds.map((id) => assigneeValueFromSelection({ assigneeAgentId: id })),
|
|
|
|
|
[recentAssigneeIds],
|
|
|
|
|
);
|
|
|
|
|
const recentProjectIds = useMemo(() => getRecentProjectIds(), [newIssueOpen]);
|
2026-02-26 08:53:03 -06:00
|
|
|
const assigneeOptions = useMemo<InlineEntityOption[]>(
|
2026-03-12 16:11:37 -05:00
|
|
|
() => [
|
|
|
|
|
...currentUserAssigneeOption(currentUserId),
|
feat: implement multi-user access and invite flows (#3784)
## Thinking Path
> - Paperclip is the control plane for autonomous AI companies.
> - V1 needs to stay local-first while also supporting shared,
authenticated deployments.
> - Human operators need real identities, company membership, invite
flows, profile surfaces, and company-scoped access controls.
> - Agents and operators also need the existing issue, inbox, workspace,
approval, and plugin flows to keep working under those authenticated
boundaries.
> - This branch accumulated the multi-user implementation, follow-up QA
fixes, workspace/runtime refinements, invite UX improvements,
release-branch conflict resolution, and review hardening.
> - This pull request consolidates that branch onto the current `master`
branch as a single reviewable PR.
> - The benefit is a complete multi-user implementation path with tests
and docs carried forward without dropping existing branch work.
## What Changed
- Added authenticated human-user access surfaces: auth/session routes,
company user directory, profile settings, company access/member
management, join requests, and invite management.
- Added invite creation, invite landing, onboarding, logo/branding,
invite grants, deduped join requests, and authenticated multi-user E2E
coverage.
- Tightened company-scoped and instance-admin authorization across
board, plugin, adapter, access, issue, and workspace routes.
- Added profile-image URL validation hardening, avatar preservation on
name-only profile updates, and join-request uniqueness migration cleanup
for pending human requests.
- Added an atomic member role/status/grants update path so Company
Access saves no longer leave partially updated permissions.
- Improved issue chat, inbox, assignee identity rendering,
sidebar/account/company navigation, workspace routing, and execution
workspace reuse behavior for multi-user operation.
- Added and updated server/UI tests covering auth, invites, membership,
issue workspace inheritance, plugin authz, inbox/chat behavior, and
multi-user flows.
- Merged current `public-gh/master` into this branch, resolved all
conflicts, and verified no `pnpm-lock.yaml` change is included in this
PR diff.
## Verification
- `pnpm exec vitest run server/src/__tests__/issues-service.test.ts
ui/src/components/IssueChatThread.test.tsx ui/src/pages/Inbox.test.tsx`
- `pnpm run preflight:workspace-links && pnpm exec vitest run
server/src/__tests__/plugin-routes-authz.test.ts`
- `pnpm exec vitest run server/src/__tests__/plugin-routes-authz.test.ts
server/src/__tests__/workspace-runtime-service-authz.test.ts
server/src/__tests__/access-validators.test.ts`
- `pnpm exec vitest run
server/src/__tests__/authz-company-access.test.ts
server/src/__tests__/routines-routes.test.ts
server/src/__tests__/sidebar-preferences-routes.test.ts
server/src/__tests__/approval-routes-idempotency.test.ts
server/src/__tests__/openclaw-invite-prompt-route.test.ts
server/src/__tests__/agent-cross-tenant-authz-routes.test.ts
server/src/__tests__/routines-e2e.test.ts`
- `pnpm exec vitest run server/src/__tests__/auth-routes.test.ts
ui/src/pages/CompanyAccess.test.tsx`
- `pnpm --filter @paperclipai/shared typecheck && pnpm --filter
@paperclipai/db typecheck && pnpm --filter @paperclipai/server
typecheck`
- `pnpm --filter @paperclipai/shared typecheck && pnpm --filter
@paperclipai/server typecheck`
- `pnpm --filter @paperclipai/ui typecheck`
- `pnpm db:generate`
- `npx playwright test --config tests/e2e/playwright.config.ts --list`
- Confirmed branch has no uncommitted changes and is `0` commits behind
`public-gh/master` before PR creation.
- Confirmed no `pnpm-lock.yaml` change is staged or present in the PR
diff.
## Risks
- High review surface area: this PR contains the accumulated multi-user
branch plus follow-up fixes, so reviewers should focus especially on
company-boundary enforcement and authenticated-vs-local deployment
behavior.
- UI behavior changed across invites, inbox, issue chat, access
settings, and sidebar navigation; no browser screenshots are included in
this branch-consolidation PR.
- Plugin install, upgrade, and lifecycle/config mutations now require
instance-admin access, which is intentional but may change expectations
for non-admin board users.
- A join-request dedupe migration rejects duplicate pending human
requests before creating unique indexes; deployments with unusual
historical duplicates should review the migration behavior.
- Company member role/status/grant saves now use a new combined
endpoint; older separate endpoints remain for compatibility.
- Full production build was not run locally in this heartbeat; CI should
cover the full matrix.
## Model Used
- OpenAI Codex coding agent, GPT-5-based model, CLI/tool-use
environment. Exact deployed model identifier and context window were not
exposed by the runtime.
## Checklist
- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] If this change affects the UI, I have included before/after
screenshots
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [x] I will address all Greptile and reviewer comments before
requesting merge
Note on screenshots: this is a branch-consolidation PR for an
already-developed multi-user branch, and no browser screenshots were
captured during this heartbeat.
---------
Co-authored-by: dotta <dotta@example.com>
Co-authored-by: Paperclip <noreply@paperclip.ing>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-17 09:44:19 -05:00
|
|
|
...buildCompanyUserInlineOptions(companyMembers?.users, { excludeUserIds: [currentUserId] }),
|
2026-03-12 16:11:37 -05:00
|
|
|
...sortAgentsByRecency(
|
2026-03-05 11:19:56 -06:00
|
|
|
(agents ?? []).filter((agent) => agent.status !== "terminated"),
|
|
|
|
|
recentAssigneeIds,
|
|
|
|
|
).map((agent) => ({
|
2026-03-12 16:11:37 -05:00
|
|
|
id: assigneeValueFromSelection({ assigneeAgentId: agent.id }),
|
2026-03-05 11:19:56 -06:00
|
|
|
label: agent.name,
|
|
|
|
|
searchText: `${agent.name} ${agent.role} ${agent.title ?? ""}`,
|
|
|
|
|
})),
|
2026-03-12 16:11:37 -05:00
|
|
|
],
|
feat: implement multi-user access and invite flows (#3784)
## Thinking Path
> - Paperclip is the control plane for autonomous AI companies.
> - V1 needs to stay local-first while also supporting shared,
authenticated deployments.
> - Human operators need real identities, company membership, invite
flows, profile surfaces, and company-scoped access controls.
> - Agents and operators also need the existing issue, inbox, workspace,
approval, and plugin flows to keep working under those authenticated
boundaries.
> - This branch accumulated the multi-user implementation, follow-up QA
fixes, workspace/runtime refinements, invite UX improvements,
release-branch conflict resolution, and review hardening.
> - This pull request consolidates that branch onto the current `master`
branch as a single reviewable PR.
> - The benefit is a complete multi-user implementation path with tests
and docs carried forward without dropping existing branch work.
## What Changed
- Added authenticated human-user access surfaces: auth/session routes,
company user directory, profile settings, company access/member
management, join requests, and invite management.
- Added invite creation, invite landing, onboarding, logo/branding,
invite grants, deduped join requests, and authenticated multi-user E2E
coverage.
- Tightened company-scoped and instance-admin authorization across
board, plugin, adapter, access, issue, and workspace routes.
- Added profile-image URL validation hardening, avatar preservation on
name-only profile updates, and join-request uniqueness migration cleanup
for pending human requests.
- Added an atomic member role/status/grants update path so Company
Access saves no longer leave partially updated permissions.
- Improved issue chat, inbox, assignee identity rendering,
sidebar/account/company navigation, workspace routing, and execution
workspace reuse behavior for multi-user operation.
- Added and updated server/UI tests covering auth, invites, membership,
issue workspace inheritance, plugin authz, inbox/chat behavior, and
multi-user flows.
- Merged current `public-gh/master` into this branch, resolved all
conflicts, and verified no `pnpm-lock.yaml` change is included in this
PR diff.
## Verification
- `pnpm exec vitest run server/src/__tests__/issues-service.test.ts
ui/src/components/IssueChatThread.test.tsx ui/src/pages/Inbox.test.tsx`
- `pnpm run preflight:workspace-links && pnpm exec vitest run
server/src/__tests__/plugin-routes-authz.test.ts`
- `pnpm exec vitest run server/src/__tests__/plugin-routes-authz.test.ts
server/src/__tests__/workspace-runtime-service-authz.test.ts
server/src/__tests__/access-validators.test.ts`
- `pnpm exec vitest run
server/src/__tests__/authz-company-access.test.ts
server/src/__tests__/routines-routes.test.ts
server/src/__tests__/sidebar-preferences-routes.test.ts
server/src/__tests__/approval-routes-idempotency.test.ts
server/src/__tests__/openclaw-invite-prompt-route.test.ts
server/src/__tests__/agent-cross-tenant-authz-routes.test.ts
server/src/__tests__/routines-e2e.test.ts`
- `pnpm exec vitest run server/src/__tests__/auth-routes.test.ts
ui/src/pages/CompanyAccess.test.tsx`
- `pnpm --filter @paperclipai/shared typecheck && pnpm --filter
@paperclipai/db typecheck && pnpm --filter @paperclipai/server
typecheck`
- `pnpm --filter @paperclipai/shared typecheck && pnpm --filter
@paperclipai/server typecheck`
- `pnpm --filter @paperclipai/ui typecheck`
- `pnpm db:generate`
- `npx playwright test --config tests/e2e/playwright.config.ts --list`
- Confirmed branch has no uncommitted changes and is `0` commits behind
`public-gh/master` before PR creation.
- Confirmed no `pnpm-lock.yaml` change is staged or present in the PR
diff.
## Risks
- High review surface area: this PR contains the accumulated multi-user
branch plus follow-up fixes, so reviewers should focus especially on
company-boundary enforcement and authenticated-vs-local deployment
behavior.
- UI behavior changed across invites, inbox, issue chat, access
settings, and sidebar navigation; no browser screenshots are included in
this branch-consolidation PR.
- Plugin install, upgrade, and lifecycle/config mutations now require
instance-admin access, which is intentional but may change expectations
for non-admin board users.
- A join-request dedupe migration rejects duplicate pending human
requests before creating unique indexes; deployments with unusual
historical duplicates should review the migration behavior.
- Company member role/status/grant saves now use a new combined
endpoint; older separate endpoints remain for compatibility.
- Full production build was not run locally in this heartbeat; CI should
cover the full matrix.
## Model Used
- OpenAI Codex coding agent, GPT-5-based model, CLI/tool-use
environment. Exact deployed model identifier and context window were not
exposed by the runtime.
## Checklist
- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] If this change affects the UI, I have included before/after
screenshots
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [x] I will address all Greptile and reviewer comments before
requesting merge
Note on screenshots: this is a branch-consolidation PR for an
already-developed multi-user branch, and no browser screenshots were
captured during this heartbeat.
---------
Co-authored-by: dotta <dotta@example.com>
Co-authored-by: Paperclip <noreply@paperclip.ing>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-17 09:44:19 -05:00
|
|
|
[agents, companyMembers?.users, currentUserId, recentAssigneeIds],
|
2026-02-26 08:53:03 -06:00
|
|
|
);
|
|
|
|
|
const projectOptions = useMemo<InlineEntityOption[]>(
|
|
|
|
|
() =>
|
2026-03-02 14:20:49 -06:00
|
|
|
orderedProjects.map((project) => ({
|
2026-02-26 08:53:03 -06:00
|
|
|
id: project.id,
|
|
|
|
|
label: project.name,
|
|
|
|
|
searchText: project.description ?? "",
|
|
|
|
|
})),
|
2026-03-02 14:20:49 -06:00
|
|
|
[orderedProjects],
|
2026-02-26 08:53:03 -06:00
|
|
|
);
|
2026-03-10 21:06:16 -05:00
|
|
|
const savedDraft = loadDraft();
|
|
|
|
|
const hasSavedDraft = Boolean(savedDraft?.title.trim() || savedDraft?.description.trim());
|
|
|
|
|
const canDiscardDraft = hasDraft || hasSavedDraft;
|
|
|
|
|
const createIssueErrorMessage =
|
|
|
|
|
createIssue.error instanceof Error ? createIssue.error.message : "Failed to create issue. Try again.";
|
2026-03-14 07:13:59 -05:00
|
|
|
const stagedDocuments = stagedFiles.filter((file) => file.kind === "document");
|
|
|
|
|
const stagedAttachments = stagedFiles.filter((file) => file.kind === "attachment");
|
2026-03-10 09:03:31 -05:00
|
|
|
|
|
|
|
|
const handleProjectChange = useCallback((nextProjectId: string) => {
|
[codex] Polish issue and operator workflow UI (#4090)
## Thinking Path
> - Paperclip operators spend much of their time in issues, inboxes,
selectors, and rich comment threads.
> - Small interaction problems in those surfaces slow down supervision
of AI-agent work.
> - The branch included related operator quality-of-life fixes for issue
layout, inbox actions, recent selectors, mobile inputs, and chat
rendering stability.
> - These changes are UI-focused and can land independently from
workspace navigation and access-profile work.
> - This pull request groups the operator QoL fixes into one standalone
branch.
> - The benefit is a more stable and efficient board workflow for issue
triage and task editing.
## What Changed
- Widened issue detail content and added a desktop inbox archive action.
- Fixed mobile text-field zoom by keeping touch input font sizes at
16px.
- Prioritized recent picker selections for assignees/projects in issue
and routine flows.
- Showed actionable approvals in the Mine inbox model.
- Fixed issue chat renderer state crashes and hardened tests.
## Verification
- `pnpm install --frozen-lockfile`
- `pnpm exec vitest run ui/src/components/IssueChatThread.test.tsx
ui/src/lib/inbox.test.ts ui/src/lib/recent-selections.test.ts`
- Split integration check: merged last after the other
[PAP-1614](/PAP/issues/PAP-1614) branches with no merge conflicts.
- Confirmed this branch does not include `pnpm-lock.yaml`.
## Risks
- Low to medium risk: mostly UI state, layout, and selection-priority
behavior.
- Visual layout and mobile zoom behavior may need browser/device QA
beyond component tests.
- No database migrations are included.
> For core feature work, check [`ROADMAP.md`](ROADMAP.md) first and
discuss it in `#dev` before opening the PR. Feature PRs that overlap
with planned core work may need to be redirected — check the roadmap
first. See `CONTRIBUTING.md`.
## Model Used
- OpenAI Codex, GPT-5.4 tool-enabled coding model, agentic
code-editing/runtime with local shell and GitHub CLI access; exact
context window and reasoning mode are not exposed by the Paperclip
harness.
## Checklist
- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] If this change affects the UI, I have included before/after
screenshots
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [x] I will address all Greptile and reviewer comments before
requesting merge
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-04-20 06:16:41 -05:00
|
|
|
if (nextProjectId) trackRecentProject(nextProjectId);
|
2026-03-10 09:03:31 -05:00
|
|
|
setProjectId(nextProjectId);
|
|
|
|
|
const nextProject = orderedProjects.find((project) => project.id === nextProjectId);
|
|
|
|
|
executionWorkspaceDefaultProjectId.current = nextProjectId || null;
|
2026-03-13 17:12:25 -05:00
|
|
|
setProjectWorkspaceId(defaultProjectWorkspaceIdForProject(nextProject));
|
|
|
|
|
setExecutionWorkspaceMode(defaultExecutionWorkspaceModeForProject(nextProject));
|
|
|
|
|
setSelectedExecutionWorkspaceId("");
|
2026-03-10 09:03:31 -05:00
|
|
|
}, [orderedProjects]);
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
if (!newIssueOpen || !projectId || executionWorkspaceDefaultProjectId.current === projectId) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
const project = orderedProjects.find((entry) => entry.id === projectId);
|
|
|
|
|
if (!project) return;
|
|
|
|
|
executionWorkspaceDefaultProjectId.current = projectId;
|
2026-03-13 17:12:25 -05:00
|
|
|
setProjectWorkspaceId(defaultProjectWorkspaceIdForProject(project));
|
|
|
|
|
setExecutionWorkspaceMode(defaultExecutionWorkspaceModeForProject(project));
|
|
|
|
|
setSelectedExecutionWorkspaceId("");
|
2026-03-16 18:37:59 -05:00
|
|
|
}, [newIssueOpen, orderedProjects, projectId]);
|
2026-02-26 10:32:44 -06:00
|
|
|
const modelOverrideOptions = useMemo<InlineEntityOption[]>(
|
2026-03-05 15:24:20 +01:00
|
|
|
() => {
|
|
|
|
|
return [...(assigneeAdapterModels ?? [])]
|
|
|
|
|
.sort((a, b) => {
|
2026-03-05 15:52:59 +01:00
|
|
|
const providerA = extractProviderIdWithFallback(a.id);
|
|
|
|
|
const providerB = extractProviderIdWithFallback(b.id);
|
2026-03-05 15:24:20 +01:00
|
|
|
const byProvider = providerA.localeCompare(providerB);
|
|
|
|
|
if (byProvider !== 0) return byProvider;
|
|
|
|
|
return a.id.localeCompare(b.id);
|
|
|
|
|
})
|
|
|
|
|
.map((model) => ({
|
|
|
|
|
id: model.id,
|
|
|
|
|
label: model.label,
|
2026-03-05 15:52:59 +01:00
|
|
|
searchText: `${model.id} ${extractProviderIdWithFallback(model.id)}`,
|
2026-03-05 15:24:20 +01:00
|
|
|
}));
|
|
|
|
|
},
|
2026-02-26 10:32:44 -06:00
|
|
|
[assigneeAdapterModels],
|
|
|
|
|
);
|
2026-02-17 10:53:20 -06:00
|
|
|
|
Add detail pages, property panels, and restyle list pages
New pages: AgentDetail, GoalDetail, IssueDetail, ProjectDetail, Inbox,
MyIssues. New feature components: AgentProperties, GoalProperties,
IssueProperties, ProjectProperties, GoalTree, NewIssueDialog. Add
heartbeats API client. Restyle all list pages (Agents, Issues, Goals,
Projects, Dashboard, Costs, Activity, Org) with EntityRow, FilterBar,
and improved layouts. Add routing for detail views.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 09:57:06 -06:00
|
|
|
return (
|
|
|
|
|
<Dialog
|
|
|
|
|
open={newIssueOpen}
|
|
|
|
|
onOpenChange={(open) => {
|
2026-03-10 21:06:16 -05:00
|
|
|
if (!open && !createIssue.isPending) closeNewIssue();
|
Add detail pages, property panels, and restyle list pages
New pages: AgentDetail, GoalDetail, IssueDetail, ProjectDetail, Inbox,
MyIssues. New feature components: AgentProperties, GoalProperties,
IssueProperties, ProjectProperties, GoalTree, NewIssueDialog. Add
heartbeats API client. Restyle all list pages (Agents, Issues, Goals,
Projects, Dashboard, Costs, Activity, Org) with EntityRow, FilterBar,
and improved layouts. Add routing for detail views.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 09:57:06 -06:00
|
|
|
}}
|
|
|
|
|
>
|
2026-02-17 10:53:20 -06:00
|
|
|
<DialogContent
|
|
|
|
|
showCloseButton={false}
|
2026-02-23 15:21:04 -06:00
|
|
|
aria-describedby={undefined}
|
2026-02-17 10:53:20 -06:00
|
|
|
className={cn(
|
2026-04-09 10:26:17 -05:00
|
|
|
"flex h-[calc(100dvh-2rem)] max-h-[calc(100dvh-2rem)] flex-col gap-0 overflow-hidden p-0 sm:h-auto",
|
2026-02-17 20:46:12 -06:00
|
|
|
expanded
|
2026-04-09 10:26:17 -05:00
|
|
|
? "sm:max-w-2xl sm:h-[calc(100dvh-2rem)]"
|
2026-02-17 20:46:12 -06:00
|
|
|
: "sm:max-w-lg"
|
2026-02-17 10:53:20 -06:00
|
|
|
)}
|
|
|
|
|
onKeyDown={handleKeyDown}
|
2026-03-10 21:06:16 -05:00
|
|
|
onEscapeKeyDown={(event) => {
|
|
|
|
|
if (createIssue.isPending) {
|
|
|
|
|
event.preventDefault();
|
|
|
|
|
}
|
|
|
|
|
}}
|
2026-03-05 17:04:25 -06:00
|
|
|
onPointerDownOutside={(event) => {
|
2026-03-10 21:06:16 -05:00
|
|
|
if (createIssue.isPending) {
|
|
|
|
|
event.preventDefault();
|
|
|
|
|
return;
|
|
|
|
|
}
|
2026-03-05 17:04:25 -06:00
|
|
|
// Radix Dialog's modal DismissableLayer calls preventDefault() on
|
|
|
|
|
// pointerdown events that originate outside the Dialog DOM tree.
|
|
|
|
|
// Popover portals render at the body level (outside the Dialog), so
|
|
|
|
|
// touch events on popover content get their default prevented — which
|
|
|
|
|
// kills scroll gesture recognition on mobile. Telling Radix "this
|
|
|
|
|
// event is handled" skips that preventDefault, restoring touch scroll.
|
|
|
|
|
const target = event.detail.originalEvent.target as HTMLElement | null;
|
|
|
|
|
if (target?.closest("[data-radix-popper-content-wrapper]")) {
|
|
|
|
|
event.preventDefault();
|
|
|
|
|
}
|
|
|
|
|
}}
|
2026-02-17 10:53:20 -06:00
|
|
|
>
|
|
|
|
|
{/* Header bar */}
|
2026-02-17 20:46:12 -06:00
|
|
|
<div className="flex items-center justify-between px-4 py-2.5 border-b border-border shrink-0">
|
2026-02-17 10:53:20 -06:00
|
|
|
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
2026-02-26 16:33:48 -06:00
|
|
|
<Popover open={companyOpen} onOpenChange={setCompanyOpen}>
|
|
|
|
|
<PopoverTrigger asChild>
|
|
|
|
|
<button
|
|
|
|
|
className={cn(
|
|
|
|
|
"px-1.5 py-0.5 rounded text-xs font-semibold cursor-pointer hover:opacity-80 transition-opacity",
|
|
|
|
|
!dialogCompany?.brandColor && "bg-muted",
|
|
|
|
|
)}
|
2026-04-06 10:58:59 -05:00
|
|
|
disabled={isSubIssueMode}
|
2026-02-26 16:33:48 -06:00
|
|
|
style={
|
|
|
|
|
dialogCompany?.brandColor
|
|
|
|
|
? {
|
|
|
|
|
backgroundColor: dialogCompany.brandColor,
|
2026-03-23 07:48:50 -05:00
|
|
|
color: pickTextColorForSolidBg(dialogCompany.brandColor),
|
2026-02-26 16:33:48 -06:00
|
|
|
}
|
|
|
|
|
: undefined
|
|
|
|
|
}
|
|
|
|
|
>
|
|
|
|
|
{(dialogCompany?.name ?? "").slice(0, 3).toUpperCase()}
|
|
|
|
|
</button>
|
|
|
|
|
</PopoverTrigger>
|
|
|
|
|
<PopoverContent className="w-48 p-1" align="start">
|
2026-03-04 12:20:14 -06:00
|
|
|
{companies.filter((c) => c.status !== "archived").map((c) => (
|
2026-02-26 16:33:48 -06:00
|
|
|
<button
|
|
|
|
|
key={c.id}
|
|
|
|
|
className={cn(
|
|
|
|
|
"flex items-center gap-2 w-full px-2 py-1.5 text-xs rounded hover:bg-accent/50",
|
|
|
|
|
c.id === effectiveCompanyId && "bg-accent",
|
|
|
|
|
)}
|
|
|
|
|
onClick={() => {
|
|
|
|
|
handleCompanyChange(c.id);
|
|
|
|
|
setCompanyOpen(false);
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
<span
|
|
|
|
|
className={cn(
|
|
|
|
|
"px-1 py-0.5 rounded text-[10px] font-semibold leading-none",
|
|
|
|
|
!c.brandColor && "bg-muted",
|
|
|
|
|
)}
|
|
|
|
|
style={
|
|
|
|
|
c.brandColor
|
|
|
|
|
? {
|
|
|
|
|
backgroundColor: c.brandColor,
|
2026-03-23 07:48:50 -05:00
|
|
|
color: pickTextColorForSolidBg(c.brandColor),
|
2026-02-26 16:33:48 -06:00
|
|
|
}
|
|
|
|
|
: undefined
|
|
|
|
|
}
|
|
|
|
|
>
|
|
|
|
|
{c.name.slice(0, 3).toUpperCase()}
|
|
|
|
|
</span>
|
|
|
|
|
<span className="truncate">{c.name}</span>
|
|
|
|
|
</button>
|
|
|
|
|
))}
|
|
|
|
|
</PopoverContent>
|
|
|
|
|
</Popover>
|
2026-02-17 10:53:20 -06:00
|
|
|
<span className="text-muted-foreground/60">›</span>
|
2026-04-06 10:58:59 -05:00
|
|
|
<span>{isSubIssueMode ? "New sub-issue" : "New issue"}</span>
|
Add detail pages, property panels, and restyle list pages
New pages: AgentDetail, GoalDetail, IssueDetail, ProjectDetail, Inbox,
MyIssues. New feature components: AgentProperties, GoalProperties,
IssueProperties, ProjectProperties, GoalTree, NewIssueDialog. Add
heartbeats API client. Restyle all list pages (Agents, Issues, Goals,
Projects, Dashboard, Costs, Activity, Org) with EntityRow, FilterBar,
and improved layouts. Add routing for detail views.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 09:57:06 -06:00
|
|
|
</div>
|
2026-02-17 10:53:20 -06:00
|
|
|
<div className="flex items-center gap-1">
|
|
|
|
|
<Button
|
|
|
|
|
variant="ghost"
|
|
|
|
|
size="icon-xs"
|
|
|
|
|
className="text-muted-foreground"
|
|
|
|
|
onClick={() => setExpanded(!expanded)}
|
2026-03-10 21:06:16 -05:00
|
|
|
disabled={createIssue.isPending}
|
2026-02-17 10:53:20 -06:00
|
|
|
>
|
|
|
|
|
{expanded ? <Minimize2 className="h-3.5 w-3.5" /> : <Maximize2 className="h-3.5 w-3.5" />}
|
Add detail pages, property panels, and restyle list pages
New pages: AgentDetail, GoalDetail, IssueDetail, ProjectDetail, Inbox,
MyIssues. New feature components: AgentProperties, GoalProperties,
IssueProperties, ProjectProperties, GoalTree, NewIssueDialog. Add
heartbeats API client. Restyle all list pages (Agents, Issues, Goals,
Projects, Dashboard, Costs, Activity, Org) with EntityRow, FilterBar,
and improved layouts. Add routing for detail views.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 09:57:06 -06:00
|
|
|
</Button>
|
2026-02-17 10:53:20 -06:00
|
|
|
<Button
|
|
|
|
|
variant="ghost"
|
|
|
|
|
size="icon-xs"
|
|
|
|
|
className="text-muted-foreground"
|
2026-02-17 20:46:12 -06:00
|
|
|
onClick={() => closeNewIssue()}
|
2026-03-10 21:06:16 -05:00
|
|
|
disabled={createIssue.isPending}
|
2026-02-17 10:53:20 -06:00
|
|
|
>
|
|
|
|
|
<span className="text-lg leading-none">×</span>
|
Add detail pages, property panels, and restyle list pages
New pages: AgentDetail, GoalDetail, IssueDetail, ProjectDetail, Inbox,
MyIssues. New feature components: AgentProperties, GoalProperties,
IssueProperties, ProjectProperties, GoalTree, NewIssueDialog. Add
heartbeats API client. Restyle all list pages (Agents, Issues, Goals,
Projects, Dashboard, Costs, Activity, Org) with EntityRow, FilterBar,
and improved layouts. Add routing for detail views.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 09:57:06 -06:00
|
|
|
</Button>
|
2026-02-17 10:53:20 -06:00
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
2026-04-10 22:26:21 -05:00
|
|
|
<div className="min-h-0 flex-1 overflow-y-auto overscroll-contain">
|
|
|
|
|
{/* Title */}
|
|
|
|
|
<div className="px-4 pt-4 pb-2">
|
|
|
|
|
<textarea
|
2026-02-26 08:53:03 -06:00
|
|
|
className="w-full text-lg font-semibold bg-transparent outline-none resize-none overflow-hidden placeholder:text-muted-foreground/50"
|
2026-02-17 10:53:20 -06:00
|
|
|
placeholder="Issue title"
|
2026-02-26 08:53:03 -06:00
|
|
|
rows={1}
|
2026-02-17 10:53:20 -06:00
|
|
|
value={title}
|
2026-02-26 08:53:03 -06:00
|
|
|
onChange={(e) => {
|
|
|
|
|
setTitle(e.target.value);
|
|
|
|
|
e.target.style.height = "auto";
|
|
|
|
|
e.target.style.height = `${e.target.scrollHeight}px`;
|
|
|
|
|
}}
|
2026-03-10 21:06:16 -05:00
|
|
|
readOnly={createIssue.isPending}
|
Add MarkdownEditor component, asset image upload, and rich description editing
Introduce MarkdownEditor built on @mdxeditor/editor with headings,
lists, links, quotes, image upload with drag-and-drop, and themed CSS
integration. Add asset image upload API (routes, service, storage) and
wire image upload into InlineEditor multiline mode, NewIssueDialog,
NewProjectDialog, GoalDetail, IssueDetail, and ProjectDetail
description fields. Tighten prompt template editor styling.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 12:50:45 -06:00
|
|
|
onKeyDown={(e) => {
|
2026-03-11 11:57:46 +09:00
|
|
|
if (
|
|
|
|
|
e.key === "Enter" &&
|
|
|
|
|
!e.metaKey &&
|
|
|
|
|
!e.ctrlKey &&
|
|
|
|
|
!e.nativeEvent.isComposing
|
|
|
|
|
) {
|
Add MarkdownEditor component, asset image upload, and rich description editing
Introduce MarkdownEditor built on @mdxeditor/editor with headings,
lists, links, quotes, image upload with drag-and-drop, and themed CSS
integration. Add asset image upload API (routes, service, storage) and
wire image upload into InlineEditor multiline mode, NewIssueDialog,
NewProjectDialog, GoalDetail, IssueDetail, and ProjectDetail
description fields. Tighten prompt template editor styling.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 12:50:45 -06:00
|
|
|
e.preventDefault();
|
|
|
|
|
descriptionEditorRef.current?.focus();
|
|
|
|
|
}
|
2026-02-26 08:53:03 -06:00
|
|
|
if (e.key === "Tab" && !e.shiftKey) {
|
|
|
|
|
e.preventDefault();
|
2026-03-12 16:11:37 -05:00
|
|
|
if (assigneeValue) {
|
|
|
|
|
// Assignee already set — skip to project or description
|
|
|
|
|
if (projectId) {
|
|
|
|
|
descriptionEditorRef.current?.focus();
|
|
|
|
|
} else {
|
|
|
|
|
projectSelectorRef.current?.focus();
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
assigneeSelectorRef.current?.focus();
|
|
|
|
|
}
|
2026-02-26 08:53:03 -06:00
|
|
|
}
|
Add MarkdownEditor component, asset image upload, and rich description editing
Introduce MarkdownEditor built on @mdxeditor/editor with headings,
lists, links, quotes, image upload with drag-and-drop, and themed CSS
integration. Add asset image upload API (routes, service, storage) and
wire image upload into InlineEditor multiline mode, NewIssueDialog,
NewProjectDialog, GoalDetail, IssueDetail, and ProjectDetail
description fields. Tighten prompt template editor styling.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 12:50:45 -06:00
|
|
|
}}
|
2026-02-17 10:53:20 -06:00
|
|
|
autoFocus
|
2026-04-10 22:26:21 -05:00
|
|
|
/>
|
|
|
|
|
</div>
|
2026-02-17 10:53:20 -06:00
|
|
|
|
2026-04-10 22:26:21 -05:00
|
|
|
<div className="px-4 pb-2">
|
|
|
|
|
<div className="overflow-x-auto overscroll-x-contain">
|
|
|
|
|
<div className="inline-flex items-center gap-2 text-sm text-muted-foreground flex-wrap sm:flex-nowrap sm:min-w-max">
|
2026-04-06 20:17:08 -05:00
|
|
|
<span className="w-6 shrink-0 text-center">For</span>
|
2026-02-26 08:53:03 -06:00
|
|
|
<InlineEntitySelector
|
|
|
|
|
ref={assigneeSelectorRef}
|
2026-03-12 16:11:37 -05:00
|
|
|
value={assigneeValue}
|
2026-02-26 08:53:03 -06:00
|
|
|
options={assigneeOptions}
|
[codex] Polish issue and operator workflow UI (#4090)
## Thinking Path
> - Paperclip operators spend much of their time in issues, inboxes,
selectors, and rich comment threads.
> - Small interaction problems in those surfaces slow down supervision
of AI-agent work.
> - The branch included related operator quality-of-life fixes for issue
layout, inbox actions, recent selectors, mobile inputs, and chat
rendering stability.
> - These changes are UI-focused and can land independently from
workspace navigation and access-profile work.
> - This pull request groups the operator QoL fixes into one standalone
branch.
> - The benefit is a more stable and efficient board workflow for issue
triage and task editing.
## What Changed
- Widened issue detail content and added a desktop inbox archive action.
- Fixed mobile text-field zoom by keeping touch input font sizes at
16px.
- Prioritized recent picker selections for assignees/projects in issue
and routine flows.
- Showed actionable approvals in the Mine inbox model.
- Fixed issue chat renderer state crashes and hardened tests.
## Verification
- `pnpm install --frozen-lockfile`
- `pnpm exec vitest run ui/src/components/IssueChatThread.test.tsx
ui/src/lib/inbox.test.ts ui/src/lib/recent-selections.test.ts`
- Split integration check: merged last after the other
[PAP-1614](/PAP/issues/PAP-1614) branches with no merge conflicts.
- Confirmed this branch does not include `pnpm-lock.yaml`.
## Risks
- Low to medium risk: mostly UI state, layout, and selection-priority
behavior.
- Visual layout and mobile zoom behavior may need browser/device QA
beyond component tests.
- No database migrations are included.
> For core feature work, check [`ROADMAP.md`](ROADMAP.md) first and
discuss it in `#dev` before opening the PR. Feature PRs that overlap
with planned core work may need to be redirected — check the roadmap
first. See `CONTRIBUTING.md`.
## Model Used
- OpenAI Codex, GPT-5.4 tool-enabled coding model, agentic
code-editing/runtime with local shell and GitHub CLI access; exact
context window and reasoning mode are not exposed by the Paperclip
harness.
## Checklist
- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] If this change affects the UI, I have included before/after
screenshots
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [x] I will address all Greptile and reviewer comments before
requesting merge
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-04-20 06:16:41 -05:00
|
|
|
recentOptionIds={recentAssigneeOptionIds}
|
2026-02-26 08:53:03 -06:00
|
|
|
placeholder="Assignee"
|
2026-03-06 08:04:35 -06:00
|
|
|
disablePortal
|
2026-02-26 08:53:03 -06:00
|
|
|
noneLabel="No assignee"
|
|
|
|
|
searchPlaceholder="Search assignees..."
|
|
|
|
|
emptyMessage="No assignees found."
|
2026-03-12 16:11:37 -05:00
|
|
|
onChange={(value) => {
|
|
|
|
|
const nextAssignee = parseAssigneeValue(value);
|
|
|
|
|
if (nextAssignee.assigneeAgentId) {
|
|
|
|
|
trackRecentAssignee(nextAssignee.assigneeAgentId);
|
|
|
|
|
}
|
|
|
|
|
setAssigneeValue(value);
|
|
|
|
|
}}
|
2026-02-26 08:53:03 -06:00
|
|
|
onConfirm={() => {
|
2026-03-12 16:11:37 -05:00
|
|
|
if (projectId) {
|
|
|
|
|
descriptionEditorRef.current?.focus();
|
|
|
|
|
} else {
|
|
|
|
|
projectSelectorRef.current?.focus();
|
|
|
|
|
}
|
2026-02-26 08:53:03 -06:00
|
|
|
}}
|
|
|
|
|
renderTriggerValue={(option) =>
|
2026-03-12 16:11:37 -05:00
|
|
|
option ? (
|
|
|
|
|
currentAssignee ? (
|
|
|
|
|
<>
|
|
|
|
|
<AgentIcon icon={currentAssignee.icon} className="h-3.5 w-3.5 shrink-0 text-muted-foreground" />
|
|
|
|
|
<span className="truncate">{option.label}</span>
|
|
|
|
|
</>
|
|
|
|
|
) : (
|
2026-02-26 08:53:03 -06:00
|
|
|
<span className="truncate">{option.label}</span>
|
2026-03-12 16:11:37 -05:00
|
|
|
)
|
2026-02-26 08:53:03 -06:00
|
|
|
) : (
|
|
|
|
|
<span className="text-muted-foreground">Assignee</span>
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
renderOption={(option) => {
|
|
|
|
|
if (!option.id) return <span className="truncate">{option.label}</span>;
|
2026-03-12 16:11:37 -05:00
|
|
|
const assignee = parseAssigneeValue(option.id).assigneeAgentId
|
|
|
|
|
? (agents ?? []).find((agent) => agent.id === parseAssigneeValue(option.id).assigneeAgentId)
|
|
|
|
|
: null;
|
2026-02-26 08:53:03 -06:00
|
|
|
return (
|
|
|
|
|
<>
|
2026-03-12 16:11:37 -05:00
|
|
|
{assignee ? <AgentIcon icon={assignee.icon} className="h-3.5 w-3.5 shrink-0 text-muted-foreground" /> : null}
|
2026-02-26 08:53:03 -06:00
|
|
|
<span className="truncate">{option.label}</span>
|
|
|
|
|
</>
|
|
|
|
|
);
|
|
|
|
|
}}
|
|
|
|
|
/>
|
|
|
|
|
<span>in</span>
|
|
|
|
|
<InlineEntitySelector
|
|
|
|
|
ref={projectSelectorRef}
|
|
|
|
|
value={projectId}
|
|
|
|
|
options={projectOptions}
|
[codex] Polish issue and operator workflow UI (#4090)
## Thinking Path
> - Paperclip operators spend much of their time in issues, inboxes,
selectors, and rich comment threads.
> - Small interaction problems in those surfaces slow down supervision
of AI-agent work.
> - The branch included related operator quality-of-life fixes for issue
layout, inbox actions, recent selectors, mobile inputs, and chat
rendering stability.
> - These changes are UI-focused and can land independently from
workspace navigation and access-profile work.
> - This pull request groups the operator QoL fixes into one standalone
branch.
> - The benefit is a more stable and efficient board workflow for issue
triage and task editing.
## What Changed
- Widened issue detail content and added a desktop inbox archive action.
- Fixed mobile text-field zoom by keeping touch input font sizes at
16px.
- Prioritized recent picker selections for assignees/projects in issue
and routine flows.
- Showed actionable approvals in the Mine inbox model.
- Fixed issue chat renderer state crashes and hardened tests.
## Verification
- `pnpm install --frozen-lockfile`
- `pnpm exec vitest run ui/src/components/IssueChatThread.test.tsx
ui/src/lib/inbox.test.ts ui/src/lib/recent-selections.test.ts`
- Split integration check: merged last after the other
[PAP-1614](/PAP/issues/PAP-1614) branches with no merge conflicts.
- Confirmed this branch does not include `pnpm-lock.yaml`.
## Risks
- Low to medium risk: mostly UI state, layout, and selection-priority
behavior.
- Visual layout and mobile zoom behavior may need browser/device QA
beyond component tests.
- No database migrations are included.
> For core feature work, check [`ROADMAP.md`](ROADMAP.md) first and
discuss it in `#dev` before opening the PR. Feature PRs that overlap
with planned core work may need to be redirected — check the roadmap
first. See `CONTRIBUTING.md`.
## Model Used
- OpenAI Codex, GPT-5.4 tool-enabled coding model, agentic
code-editing/runtime with local shell and GitHub CLI access; exact
context window and reasoning mode are not exposed by the Paperclip
harness.
## Checklist
- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] If this change affects the UI, I have included before/after
screenshots
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [x] I will address all Greptile and reviewer comments before
requesting merge
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-04-20 06:16:41 -05:00
|
|
|
recentOptionIds={recentProjectIds}
|
2026-02-26 08:53:03 -06:00
|
|
|
placeholder="Project"
|
2026-03-06 08:04:35 -06:00
|
|
|
disablePortal
|
2026-02-26 08:53:03 -06:00
|
|
|
noneLabel="No project"
|
|
|
|
|
searchPlaceholder="Search projects..."
|
|
|
|
|
emptyMessage="No projects found."
|
2026-03-10 09:03:31 -05:00
|
|
|
onChange={handleProjectChange}
|
2026-02-26 08:53:03 -06:00
|
|
|
onConfirm={() => {
|
|
|
|
|
descriptionEditorRef.current?.focus();
|
|
|
|
|
}}
|
|
|
|
|
renderTriggerValue={(option) =>
|
|
|
|
|
option && currentProject ? (
|
|
|
|
|
<>
|
|
|
|
|
<span
|
|
|
|
|
className="h-3.5 w-3.5 shrink-0 rounded-sm"
|
|
|
|
|
style={{ backgroundColor: currentProject.color ?? "#6366f1" }}
|
|
|
|
|
/>
|
|
|
|
|
<span className="truncate">{option.label}</span>
|
|
|
|
|
</>
|
|
|
|
|
) : (
|
|
|
|
|
<span className="text-muted-foreground">Project</span>
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
renderOption={(option) => {
|
|
|
|
|
if (!option.id) return <span className="truncate">{option.label}</span>;
|
2026-03-02 14:20:49 -06:00
|
|
|
const project = orderedProjects.find((item) => item.id === option.id);
|
2026-02-26 08:53:03 -06:00
|
|
|
return (
|
|
|
|
|
<>
|
|
|
|
|
<span
|
|
|
|
|
className="h-3.5 w-3.5 shrink-0 rounded-sm"
|
|
|
|
|
style={{ backgroundColor: project?.color ?? "#6366f1" }}
|
|
|
|
|
/>
|
|
|
|
|
<span className="truncate">{option.label}</span>
|
|
|
|
|
</>
|
|
|
|
|
);
|
|
|
|
|
}}
|
|
|
|
|
/>
|
2026-04-06 19:59:17 -05:00
|
|
|
|
|
|
|
|
{/* Three-dot menu to add Reviewer / Approver rows */}
|
|
|
|
|
<Popover open={participantMenuOpen} onOpenChange={setParticipantMenuOpen}>
|
|
|
|
|
<PopoverTrigger asChild>
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
className="inline-flex items-center justify-center rounded-md p-1 text-muted-foreground hover:bg-accent/50 transition-colors"
|
|
|
|
|
title="Add reviewer or approver"
|
|
|
|
|
>
|
|
|
|
|
<MoreHorizontal className="h-4 w-4" />
|
|
|
|
|
</button>
|
|
|
|
|
</PopoverTrigger>
|
|
|
|
|
<PopoverContent className="w-44 p-1" align="start">
|
|
|
|
|
<button
|
|
|
|
|
className={cn(
|
|
|
|
|
"flex items-center gap-2 w-full px-2 py-1.5 text-xs rounded hover:bg-accent/50",
|
|
|
|
|
showReviewerRow && "bg-accent",
|
|
|
|
|
)}
|
|
|
|
|
onClick={() => {
|
|
|
|
|
setShowReviewerRow((v) => !v);
|
|
|
|
|
if (showReviewerRow) setReviewerValue("");
|
|
|
|
|
setParticipantMenuOpen(false);
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
<Eye className="h-3 w-3" />
|
|
|
|
|
Reviewer
|
|
|
|
|
</button>
|
|
|
|
|
<button
|
|
|
|
|
className={cn(
|
|
|
|
|
"flex items-center gap-2 w-full px-2 py-1.5 text-xs rounded hover:bg-accent/50",
|
|
|
|
|
showApproverRow && "bg-accent",
|
|
|
|
|
)}
|
|
|
|
|
onClick={() => {
|
|
|
|
|
setShowApproverRow((v) => !v);
|
|
|
|
|
if (showApproverRow) setApproverValue("");
|
|
|
|
|
setParticipantMenuOpen(false);
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
<ShieldCheck className="h-3 w-3" />
|
|
|
|
|
Approver
|
|
|
|
|
</button>
|
|
|
|
|
</PopoverContent>
|
|
|
|
|
</Popover>
|
2026-04-10 22:26:21 -05:00
|
|
|
</div>
|
2026-02-26 08:53:03 -06:00
|
|
|
</div>
|
2026-04-06 19:59:17 -05:00
|
|
|
|
2026-04-10 22:26:21 -05:00
|
|
|
{/* Reviewer row */}
|
|
|
|
|
{showReviewerRow && (
|
|
|
|
|
<div className="flex items-center gap-2 text-sm text-muted-foreground mt-1">
|
|
|
|
|
<span className="w-6 shrink-0 flex items-center justify-center"><Eye className="h-3.5 w-3.5" /></span>
|
|
|
|
|
<InlineEntitySelector
|
2026-04-06 19:59:17 -05:00
|
|
|
value={reviewerValue}
|
|
|
|
|
options={assigneeOptions}
|
[codex] Polish issue and operator workflow UI (#4090)
## Thinking Path
> - Paperclip operators spend much of their time in issues, inboxes,
selectors, and rich comment threads.
> - Small interaction problems in those surfaces slow down supervision
of AI-agent work.
> - The branch included related operator quality-of-life fixes for issue
layout, inbox actions, recent selectors, mobile inputs, and chat
rendering stability.
> - These changes are UI-focused and can land independently from
workspace navigation and access-profile work.
> - This pull request groups the operator QoL fixes into one standalone
branch.
> - The benefit is a more stable and efficient board workflow for issue
triage and task editing.
## What Changed
- Widened issue detail content and added a desktop inbox archive action.
- Fixed mobile text-field zoom by keeping touch input font sizes at
16px.
- Prioritized recent picker selections for assignees/projects in issue
and routine flows.
- Showed actionable approvals in the Mine inbox model.
- Fixed issue chat renderer state crashes and hardened tests.
## Verification
- `pnpm install --frozen-lockfile`
- `pnpm exec vitest run ui/src/components/IssueChatThread.test.tsx
ui/src/lib/inbox.test.ts ui/src/lib/recent-selections.test.ts`
- Split integration check: merged last after the other
[PAP-1614](/PAP/issues/PAP-1614) branches with no merge conflicts.
- Confirmed this branch does not include `pnpm-lock.yaml`.
## Risks
- Low to medium risk: mostly UI state, layout, and selection-priority
behavior.
- Visual layout and mobile zoom behavior may need browser/device QA
beyond component tests.
- No database migrations are included.
> For core feature work, check [`ROADMAP.md`](ROADMAP.md) first and
discuss it in `#dev` before opening the PR. Feature PRs that overlap
with planned core work may need to be redirected — check the roadmap
first. See `CONTRIBUTING.md`.
## Model Used
- OpenAI Codex, GPT-5.4 tool-enabled coding model, agentic
code-editing/runtime with local shell and GitHub CLI access; exact
context window and reasoning mode are not exposed by the Paperclip
harness.
## Checklist
- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] If this change affects the UI, I have included before/after
screenshots
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [x] I will address all Greptile and reviewer comments before
requesting merge
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-04-20 06:16:41 -05:00
|
|
|
recentOptionIds={recentAssigneeOptionIds}
|
2026-04-06 19:59:17 -05:00
|
|
|
placeholder="Reviewer"
|
|
|
|
|
disablePortal
|
|
|
|
|
noneLabel="No reviewer"
|
|
|
|
|
searchPlaceholder="Search reviewers..."
|
|
|
|
|
emptyMessage="No reviewers found."
|
|
|
|
|
onChange={setReviewerValue}
|
|
|
|
|
renderTriggerValue={(option) =>
|
|
|
|
|
option ? (
|
|
|
|
|
<>
|
|
|
|
|
{(() => {
|
|
|
|
|
const reviewer = parseAssigneeValue(option.id).assigneeAgentId
|
|
|
|
|
? (agents ?? []).find((a) => a.id === parseAssigneeValue(option.id).assigneeAgentId)
|
|
|
|
|
: null;
|
|
|
|
|
return reviewer ? <AgentIcon icon={reviewer.icon} className="h-3.5 w-3.5 shrink-0 text-muted-foreground" /> : null;
|
|
|
|
|
})()}
|
|
|
|
|
<span className="truncate">{option.label}</span>
|
|
|
|
|
</>
|
|
|
|
|
) : (
|
|
|
|
|
<span className="text-muted-foreground">Reviewer</span>
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
renderOption={(option) => {
|
|
|
|
|
if (!option.id) return <span className="truncate">{option.label}</span>;
|
|
|
|
|
const reviewer = parseAssigneeValue(option.id).assigneeAgentId
|
|
|
|
|
? (agents ?? []).find((agent) => agent.id === parseAssigneeValue(option.id).assigneeAgentId)
|
|
|
|
|
: null;
|
|
|
|
|
return (
|
|
|
|
|
<>
|
|
|
|
|
{reviewer ? <AgentIcon icon={reviewer.icon} className="h-3.5 w-3.5 shrink-0 text-muted-foreground" /> : null}
|
|
|
|
|
<span className="truncate">{option.label}</span>
|
|
|
|
|
</>
|
|
|
|
|
);
|
|
|
|
|
}}
|
2026-04-10 22:26:21 -05:00
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
2026-04-06 19:59:17 -05:00
|
|
|
|
2026-04-10 22:26:21 -05:00
|
|
|
{/* Approver row */}
|
|
|
|
|
{showApproverRow && (
|
|
|
|
|
<div className="flex items-center gap-2 text-sm text-muted-foreground mt-1">
|
|
|
|
|
<span className="w-6 shrink-0 flex items-center justify-center"><ShieldCheck className="h-3.5 w-3.5" /></span>
|
|
|
|
|
<InlineEntitySelector
|
2026-04-06 19:59:17 -05:00
|
|
|
value={approverValue}
|
|
|
|
|
options={assigneeOptions}
|
[codex] Polish issue and operator workflow UI (#4090)
## Thinking Path
> - Paperclip operators spend much of their time in issues, inboxes,
selectors, and rich comment threads.
> - Small interaction problems in those surfaces slow down supervision
of AI-agent work.
> - The branch included related operator quality-of-life fixes for issue
layout, inbox actions, recent selectors, mobile inputs, and chat
rendering stability.
> - These changes are UI-focused and can land independently from
workspace navigation and access-profile work.
> - This pull request groups the operator QoL fixes into one standalone
branch.
> - The benefit is a more stable and efficient board workflow for issue
triage and task editing.
## What Changed
- Widened issue detail content and added a desktop inbox archive action.
- Fixed mobile text-field zoom by keeping touch input font sizes at
16px.
- Prioritized recent picker selections for assignees/projects in issue
and routine flows.
- Showed actionable approvals in the Mine inbox model.
- Fixed issue chat renderer state crashes and hardened tests.
## Verification
- `pnpm install --frozen-lockfile`
- `pnpm exec vitest run ui/src/components/IssueChatThread.test.tsx
ui/src/lib/inbox.test.ts ui/src/lib/recent-selections.test.ts`
- Split integration check: merged last after the other
[PAP-1614](/PAP/issues/PAP-1614) branches with no merge conflicts.
- Confirmed this branch does not include `pnpm-lock.yaml`.
## Risks
- Low to medium risk: mostly UI state, layout, and selection-priority
behavior.
- Visual layout and mobile zoom behavior may need browser/device QA
beyond component tests.
- No database migrations are included.
> For core feature work, check [`ROADMAP.md`](ROADMAP.md) first and
discuss it in `#dev` before opening the PR. Feature PRs that overlap
with planned core work may need to be redirected — check the roadmap
first. See `CONTRIBUTING.md`.
## Model Used
- OpenAI Codex, GPT-5.4 tool-enabled coding model, agentic
code-editing/runtime with local shell and GitHub CLI access; exact
context window and reasoning mode are not exposed by the Paperclip
harness.
## Checklist
- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] If this change affects the UI, I have included before/after
screenshots
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [x] I will address all Greptile and reviewer comments before
requesting merge
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-04-20 06:16:41 -05:00
|
|
|
recentOptionIds={recentAssigneeOptionIds}
|
2026-04-06 19:59:17 -05:00
|
|
|
placeholder="Approver"
|
|
|
|
|
disablePortal
|
|
|
|
|
noneLabel="No approver"
|
|
|
|
|
searchPlaceholder="Search approvers..."
|
|
|
|
|
emptyMessage="No approvers found."
|
|
|
|
|
onChange={setApproverValue}
|
|
|
|
|
renderTriggerValue={(option) =>
|
|
|
|
|
option ? (
|
|
|
|
|
<>
|
|
|
|
|
{(() => {
|
|
|
|
|
const approver = parseAssigneeValue(option.id).assigneeAgentId
|
|
|
|
|
? (agents ?? []).find((a) => a.id === parseAssigneeValue(option.id).assigneeAgentId)
|
|
|
|
|
: null;
|
|
|
|
|
return approver ? <AgentIcon icon={approver.icon} className="h-3.5 w-3.5 shrink-0 text-muted-foreground" /> : null;
|
|
|
|
|
})()}
|
|
|
|
|
<span className="truncate">{option.label}</span>
|
|
|
|
|
</>
|
|
|
|
|
) : (
|
|
|
|
|
<span className="text-muted-foreground">Approver</span>
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
renderOption={(option) => {
|
|
|
|
|
if (!option.id) return <span className="truncate">{option.label}</span>;
|
|
|
|
|
const approver = parseAssigneeValue(option.id).assigneeAgentId
|
|
|
|
|
? (agents ?? []).find((agent) => agent.id === parseAssigneeValue(option.id).assigneeAgentId)
|
|
|
|
|
: null;
|
|
|
|
|
return (
|
|
|
|
|
<>
|
|
|
|
|
{approver ? <AgentIcon icon={approver.icon} className="h-3.5 w-3.5 shrink-0 text-muted-foreground" /> : null}
|
|
|
|
|
<span className="truncate">{option.label}</span>
|
|
|
|
|
</>
|
|
|
|
|
);
|
|
|
|
|
}}
|
2026-04-10 22:26:21 -05:00
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
2026-02-26 08:53:03 -06:00
|
|
|
|
2026-04-10 22:26:21 -05:00
|
|
|
{isSubIssueMode ? (
|
|
|
|
|
<div className="px-4 pb-2">
|
2026-04-06 11:27:41 -05:00
|
|
|
<div className="max-w-full rounded-md border border-border bg-muted/30 px-2.5 py-1.5 text-xs text-muted-foreground">
|
|
|
|
|
<div className="flex items-center gap-1.5">
|
|
|
|
|
<ListTree className="h-3.5 w-3.5 shrink-0" />
|
|
|
|
|
<span className="shrink-0">Sub-issue of</span>
|
|
|
|
|
<span className="font-medium text-foreground">{parentIssueLabel}</span>
|
|
|
|
|
</div>
|
2026-04-06 10:58:59 -05:00
|
|
|
{newIssueDefaults.parentTitle ? (
|
2026-04-06 11:27:41 -05:00
|
|
|
<div className="pl-5 text-foreground/80 truncate">
|
|
|
|
|
{newIssueDefaults.parentTitle}
|
|
|
|
|
</div>
|
2026-04-06 10:58:59 -05:00
|
|
|
) : null}
|
|
|
|
|
</div>
|
2026-04-10 22:26:21 -05:00
|
|
|
</div>
|
|
|
|
|
) : null}
|
2026-04-06 10:58:59 -05:00
|
|
|
|
2026-04-10 22:26:21 -05:00
|
|
|
{currentProject && currentProjectSupportsExecutionWorkspace && (
|
|
|
|
|
<div className="px-4 py-3 space-y-2">
|
2026-03-17 09:49:12 -05:00
|
|
|
<div className="space-y-1.5">
|
|
|
|
|
<div className="text-xs font-medium">Execution workspace</div>
|
|
|
|
|
<div className="text-[11px] text-muted-foreground">
|
|
|
|
|
Control whether this issue runs in the shared workspace, a new isolated workspace, or an existing one.
|
|
|
|
|
</div>
|
|
|
|
|
<select
|
|
|
|
|
className="w-full rounded border border-border bg-transparent px-2 py-1.5 text-xs outline-none"
|
|
|
|
|
value={executionWorkspaceMode}
|
|
|
|
|
onChange={(e) => {
|
|
|
|
|
setExecutionWorkspaceMode(e.target.value);
|
|
|
|
|
if (e.target.value !== "reuse_existing") {
|
|
|
|
|
setSelectedExecutionWorkspaceId("");
|
|
|
|
|
}
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
{EXECUTION_WORKSPACE_MODES.map((option) => (
|
|
|
|
|
<option key={option.value} value={option.value}>
|
|
|
|
|
{option.label}
|
|
|
|
|
</option>
|
|
|
|
|
))}
|
|
|
|
|
</select>
|
|
|
|
|
{executionWorkspaceMode === "reuse_existing" && (
|
2026-03-13 17:12:25 -05:00
|
|
|
<select
|
|
|
|
|
className="w-full rounded border border-border bg-transparent px-2 py-1.5 text-xs outline-none"
|
2026-03-17 09:49:12 -05:00
|
|
|
value={selectedExecutionWorkspaceId}
|
|
|
|
|
onChange={(e) => setSelectedExecutionWorkspaceId(e.target.value)}
|
2026-03-13 17:12:25 -05:00
|
|
|
>
|
2026-03-17 09:49:12 -05:00
|
|
|
<option value="">Choose an existing workspace</option>
|
|
|
|
|
{deduplicatedReusableWorkspaces.map((workspace) => (
|
|
|
|
|
<option key={workspace.id} value={workspace.id}>
|
|
|
|
|
{workspace.name} · {workspace.status} · {workspace.branchName ?? workspace.cwd ?? workspace.id.slice(0, 8)}
|
2026-03-13 17:12:25 -05:00
|
|
|
</option>
|
|
|
|
|
))}
|
|
|
|
|
</select>
|
2026-03-17 09:49:12 -05:00
|
|
|
)}
|
|
|
|
|
{executionWorkspaceMode === "reuse_existing" && selectedReusableExecutionWorkspace && (
|
|
|
|
|
<div className="text-[11px] text-muted-foreground">
|
|
|
|
|
Reusing {selectedReusableExecutionWorkspace.name} from {selectedReusableExecutionWorkspace.branchName ?? selectedReusableExecutionWorkspace.cwd ?? "existing execution workspace"}.
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
2026-04-06 11:34:11 -05:00
|
|
|
{showParentWorkspaceWarning ? (
|
|
|
|
|
<div className="rounded-md border border-amber-300/60 bg-amber-50 px-2 py-1.5 text-[11px] text-amber-900 dark:border-amber-800/70 dark:bg-amber-950/30 dark:text-amber-100">
|
|
|
|
|
Warning: this sub-issue will no longer use the parent issue workspace{parentExecutionWorkspaceLabel ? ` (${parentExecutionWorkspaceLabel})` : ""}.
|
|
|
|
|
</div>
|
|
|
|
|
) : null}
|
2026-03-17 09:49:12 -05:00
|
|
|
</div>
|
2026-04-10 22:26:21 -05:00
|
|
|
</div>
|
|
|
|
|
)}
|
2026-03-10 09:03:31 -05:00
|
|
|
|
2026-04-10 22:26:21 -05:00
|
|
|
{supportsAssigneeOverrides && (
|
|
|
|
|
<div className="px-4 pb-2">
|
2026-02-26 10:32:44 -06:00
|
|
|
<button
|
|
|
|
|
className="inline-flex items-center gap-1.5 text-xs font-medium text-muted-foreground hover:text-foreground transition-colors"
|
|
|
|
|
onClick={() => setAssigneeOptionsOpen((open) => !open)}
|
|
|
|
|
>
|
|
|
|
|
{assigneeOptionsOpen ? <ChevronDown className="h-3 w-3" /> : <ChevronRight className="h-3 w-3" />}
|
|
|
|
|
{assigneeOptionsTitle}
|
|
|
|
|
</button>
|
|
|
|
|
{assigneeOptionsOpen && (
|
|
|
|
|
<div className="mt-2 rounded-md border border-border p-3 bg-muted/20 space-y-3">
|
|
|
|
|
<div className="space-y-1.5">
|
|
|
|
|
<div className="text-xs text-muted-foreground">Model</div>
|
|
|
|
|
<InlineEntitySelector
|
|
|
|
|
value={assigneeModelOverride}
|
|
|
|
|
options={modelOverrideOptions}
|
|
|
|
|
placeholder="Default model"
|
2026-03-06 08:04:35 -06:00
|
|
|
disablePortal
|
2026-02-26 10:32:44 -06:00
|
|
|
noneLabel="Default model"
|
|
|
|
|
searchPlaceholder="Search models..."
|
|
|
|
|
emptyMessage="No models found."
|
|
|
|
|
onChange={setAssigneeModelOverride}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="space-y-1.5">
|
|
|
|
|
<div className="text-xs text-muted-foreground">Thinking effort</div>
|
|
|
|
|
<div className="flex items-center gap-1.5 flex-wrap">
|
|
|
|
|
{thinkingEffortOptions.map((option) => (
|
|
|
|
|
<button
|
|
|
|
|
key={option.value || "default"}
|
|
|
|
|
className={cn(
|
|
|
|
|
"px-2 py-1 rounded-md text-xs border border-border hover:bg-accent/50 transition-colors",
|
|
|
|
|
assigneeThinkingEffort === option.value && "bg-accent"
|
|
|
|
|
)}
|
|
|
|
|
onClick={() => setAssigneeThinkingEffort(option.value)}
|
|
|
|
|
>
|
|
|
|
|
{option.label}
|
|
|
|
|
</button>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
2026-02-26 16:33:48 -06:00
|
|
|
{assigneeAdapterType === "claude_local" && (
|
|
|
|
|
<div className="flex items-center justify-between rounded-md border border-border px-2 py-1.5">
|
|
|
|
|
<div className="text-xs text-muted-foreground">Enable Chrome (--chrome)</div>
|
2026-04-04 10:00:39 -05:00
|
|
|
<ToggleSwitch
|
|
|
|
|
checked={assigneeChrome}
|
|
|
|
|
onCheckedChange={() => setAssigneeChrome((value) => !value)}
|
|
|
|
|
/>
|
2026-02-26 16:33:48 -06:00
|
|
|
</div>
|
|
|
|
|
)}
|
2026-02-26 10:32:44 -06:00
|
|
|
</div>
|
|
|
|
|
)}
|
2026-04-10 22:26:21 -05:00
|
|
|
</div>
|
|
|
|
|
)}
|
2026-02-26 10:32:44 -06:00
|
|
|
|
2026-04-10 22:26:21 -05:00
|
|
|
{/* Description */}
|
2026-03-14 07:13:59 -05:00
|
|
|
<div
|
2026-04-10 22:26:21 -05:00
|
|
|
className="border-t border-border/60 px-4 pb-2 pt-3"
|
|
|
|
|
onDragEnter={handleFileDragEnter}
|
|
|
|
|
onDragOver={handleFileDragOver}
|
|
|
|
|
onDragLeave={handleFileDragLeave}
|
|
|
|
|
onDrop={handleFileDrop}
|
2026-03-14 07:13:59 -05:00
|
|
|
>
|
2026-04-10 22:26:21 -05:00
|
|
|
<div
|
|
|
|
|
className={cn(
|
|
|
|
|
"rounded-md transition-colors",
|
|
|
|
|
isFileDragOver && "bg-accent/20",
|
|
|
|
|
)}
|
|
|
|
|
>
|
|
|
|
|
<MarkdownEditor
|
|
|
|
|
ref={descriptionEditorRef}
|
|
|
|
|
value={description}
|
|
|
|
|
onChange={setDescription}
|
|
|
|
|
placeholder="Add description..."
|
|
|
|
|
bordered={false}
|
|
|
|
|
mentions={mentionOptions}
|
|
|
|
|
contentClassName={cn("text-sm text-muted-foreground pb-12", expanded ? "min-h-[220px]" : "min-h-[120px]")}
|
|
|
|
|
imageUploadHandler={async (file) => {
|
|
|
|
|
const asset = await uploadDescriptionImage.mutateAsync(file);
|
|
|
|
|
return asset.contentPath;
|
|
|
|
|
}}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
{stagedFiles.length > 0 ? (
|
|
|
|
|
<div className="mt-4 space-y-3 rounded-lg border border-border/70 p-3">
|
2026-03-14 07:13:59 -05:00
|
|
|
{stagedDocuments.length > 0 ? (
|
|
|
|
|
<div className="space-y-2">
|
|
|
|
|
<div className="text-xs font-medium text-muted-foreground">Documents</div>
|
|
|
|
|
<div className="space-y-2">
|
|
|
|
|
{stagedDocuments.map((file) => (
|
|
|
|
|
<div key={file.id} className="flex items-start justify-between gap-3 rounded-md border border-border/70 px-3 py-2">
|
|
|
|
|
<div className="min-w-0">
|
|
|
|
|
<div className="flex items-center gap-2">
|
|
|
|
|
<span className="rounded-full border border-border px-2 py-0.5 font-mono text-[10px] uppercase tracking-[0.16em] text-muted-foreground">
|
|
|
|
|
{file.documentKey}
|
|
|
|
|
</span>
|
|
|
|
|
<span className="truncate text-sm">{file.file.name}</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="mt-1 flex items-center gap-2 text-[11px] text-muted-foreground">
|
|
|
|
|
<FileText className="h-3.5 w-3.5" />
|
|
|
|
|
<span>{file.title || file.file.name}</span>
|
|
|
|
|
<span>•</span>
|
|
|
|
|
<span>{formatFileSize(file.file)}</span>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<Button
|
|
|
|
|
variant="ghost"
|
|
|
|
|
size="icon-xs"
|
|
|
|
|
className="shrink-0 text-muted-foreground"
|
|
|
|
|
onClick={() => removeStagedFile(file.id)}
|
|
|
|
|
disabled={createIssue.isPending}
|
|
|
|
|
title="Remove document"
|
|
|
|
|
>
|
|
|
|
|
<X className="h-3.5 w-3.5" />
|
|
|
|
|
</Button>
|
|
|
|
|
</div>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
) : null}
|
|
|
|
|
|
|
|
|
|
{stagedAttachments.length > 0 ? (
|
|
|
|
|
<div className="space-y-2">
|
|
|
|
|
<div className="text-xs font-medium text-muted-foreground">Attachments</div>
|
|
|
|
|
<div className="space-y-2">
|
|
|
|
|
{stagedAttachments.map((file) => (
|
|
|
|
|
<div key={file.id} className="flex items-start justify-between gap-3 rounded-md border border-border/70 px-3 py-2">
|
|
|
|
|
<div className="min-w-0">
|
|
|
|
|
<div className="flex items-center gap-2">
|
|
|
|
|
<Paperclip className="h-3.5 w-3.5 shrink-0 text-muted-foreground" />
|
|
|
|
|
<span className="truncate text-sm">{file.file.name}</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="mt-1 text-[11px] text-muted-foreground">
|
|
|
|
|
{file.file.type || "application/octet-stream"} • {formatFileSize(file.file)}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<Button
|
|
|
|
|
variant="ghost"
|
|
|
|
|
size="icon-xs"
|
|
|
|
|
className="shrink-0 text-muted-foreground"
|
|
|
|
|
onClick={() => removeStagedFile(file.id)}
|
|
|
|
|
disabled={createIssue.isPending}
|
|
|
|
|
title="Remove attachment"
|
|
|
|
|
>
|
|
|
|
|
<X className="h-3.5 w-3.5" />
|
|
|
|
|
</Button>
|
|
|
|
|
</div>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
) : null}
|
2026-04-10 22:26:21 -05:00
|
|
|
</div>
|
|
|
|
|
) : null}
|
|
|
|
|
</div>
|
2026-02-17 10:53:20 -06:00
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* Property chips bar */}
|
2026-02-17 20:46:12 -06:00
|
|
|
<div className="flex items-center gap-1.5 px-4 py-2 border-t border-border flex-wrap shrink-0">
|
2026-02-17 10:53:20 -06:00
|
|
|
{/* Status chip */}
|
|
|
|
|
<Popover open={statusOpen} onOpenChange={setStatusOpen}>
|
|
|
|
|
<PopoverTrigger asChild>
|
|
|
|
|
<button className="inline-flex items-center gap-1.5 rounded-md border border-border px-2 py-1 text-xs hover:bg-accent/50 transition-colors">
|
|
|
|
|
<CircleDot className={cn("h-3 w-3", currentStatus.color)} />
|
|
|
|
|
{currentStatus.label}
|
|
|
|
|
</button>
|
|
|
|
|
</PopoverTrigger>
|
|
|
|
|
<PopoverContent className="w-36 p-1" align="start">
|
|
|
|
|
{statuses.map((s) => (
|
|
|
|
|
<button
|
|
|
|
|
key={s.value}
|
|
|
|
|
className={cn(
|
|
|
|
|
"flex items-center gap-2 w-full px-2 py-1.5 text-xs rounded hover:bg-accent/50",
|
|
|
|
|
s.value === status && "bg-accent"
|
|
|
|
|
)}
|
|
|
|
|
onClick={() => { setStatus(s.value); setStatusOpen(false); }}
|
|
|
|
|
>
|
|
|
|
|
<CircleDot className={cn("h-3 w-3", s.color)} />
|
|
|
|
|
{s.label}
|
|
|
|
|
</button>
|
|
|
|
|
))}
|
|
|
|
|
</PopoverContent>
|
|
|
|
|
</Popover>
|
|
|
|
|
|
|
|
|
|
{/* Priority chip */}
|
|
|
|
|
<Popover open={priorityOpen} onOpenChange={setPriorityOpen}>
|
|
|
|
|
<PopoverTrigger asChild>
|
|
|
|
|
<button className="inline-flex items-center gap-1.5 rounded-md border border-border px-2 py-1 text-xs hover:bg-accent/50 transition-colors">
|
|
|
|
|
{currentPriority ? (
|
|
|
|
|
<>
|
|
|
|
|
<currentPriority.icon className={cn("h-3 w-3", currentPriority.color)} />
|
|
|
|
|
{currentPriority.label}
|
|
|
|
|
</>
|
|
|
|
|
) : (
|
|
|
|
|
<>
|
|
|
|
|
<Minus className="h-3 w-3 text-muted-foreground" />
|
|
|
|
|
Priority
|
|
|
|
|
</>
|
|
|
|
|
)}
|
|
|
|
|
</button>
|
|
|
|
|
</PopoverTrigger>
|
|
|
|
|
<PopoverContent className="w-36 p-1" align="start">
|
|
|
|
|
{priorities.map((p) => (
|
|
|
|
|
<button
|
|
|
|
|
key={p.value}
|
|
|
|
|
className={cn(
|
|
|
|
|
"flex items-center gap-2 w-full px-2 py-1.5 text-xs rounded hover:bg-accent/50",
|
|
|
|
|
p.value === priority && "bg-accent"
|
|
|
|
|
)}
|
|
|
|
|
onClick={() => { setPriority(p.value); setPriorityOpen(false); }}
|
|
|
|
|
>
|
|
|
|
|
<p.icon className={cn("h-3 w-3", p.color)} />
|
|
|
|
|
{p.label}
|
|
|
|
|
</button>
|
|
|
|
|
))}
|
|
|
|
|
</PopoverContent>
|
|
|
|
|
</Popover>
|
|
|
|
|
|
2026-04-06 19:59:17 -05:00
|
|
|
{/* Labels chip — disabled, not wired up yet */}
|
|
|
|
|
{/* <button className="inline-flex items-center gap-1.5 rounded-md border border-border px-2 py-1 text-xs hover:bg-accent/50 transition-colors text-muted-foreground">
|
2026-02-17 10:53:20 -06:00
|
|
|
<Tag className="h-3 w-3" />
|
|
|
|
|
Labels
|
2026-04-06 19:59:17 -05:00
|
|
|
</button> */}
|
2026-02-17 10:53:20 -06:00
|
|
|
|
2026-02-25 21:36:06 -06:00
|
|
|
<input
|
2026-03-14 07:13:59 -05:00
|
|
|
ref={stageFileInputRef}
|
2026-02-25 21:36:06 -06:00
|
|
|
type="file"
|
2026-03-14 07:13:59 -05:00
|
|
|
accept={STAGED_FILE_ACCEPT}
|
2026-02-25 21:36:06 -06:00
|
|
|
className="hidden"
|
2026-03-14 07:13:59 -05:00
|
|
|
onChange={handleStageFilesPicked}
|
|
|
|
|
multiple
|
2026-02-25 21:36:06 -06:00
|
|
|
/>
|
|
|
|
|
<button
|
2026-04-06 19:49:50 -05:00
|
|
|
className="inline-flex items-center gap-1.5 rounded-md border border-border px-2 py-1 text-xs hover:bg-accent/50 transition-colors text-muted-foreground"
|
2026-03-14 07:13:59 -05:00
|
|
|
onClick={() => stageFileInputRef.current?.click()}
|
|
|
|
|
disabled={createIssue.isPending}
|
2026-02-25 21:36:06 -06:00
|
|
|
>
|
|
|
|
|
<Paperclip className="h-3 w-3" />
|
2026-03-14 07:21:21 -05:00
|
|
|
Upload
|
2026-02-25 21:36:06 -06:00
|
|
|
</button>
|
|
|
|
|
|
2026-02-17 10:53:20 -06:00
|
|
|
{/* More (dates) */}
|
|
|
|
|
<Popover open={moreOpen} onOpenChange={setMoreOpen}>
|
|
|
|
|
<PopoverTrigger asChild>
|
|
|
|
|
<button className="inline-flex items-center justify-center rounded-md border border-border p-1 text-xs hover:bg-accent/50 transition-colors text-muted-foreground">
|
|
|
|
|
<MoreHorizontal className="h-3 w-3" />
|
|
|
|
|
</button>
|
|
|
|
|
</PopoverTrigger>
|
|
|
|
|
<PopoverContent className="w-44 p-1" align="start">
|
|
|
|
|
<button className="flex items-center gap-2 w-full px-2 py-1.5 text-xs rounded hover:bg-accent/50 text-muted-foreground">
|
|
|
|
|
<Calendar className="h-3 w-3" />
|
|
|
|
|
Start date
|
|
|
|
|
</button>
|
|
|
|
|
<button className="flex items-center gap-2 w-full px-2 py-1.5 text-xs rounded hover:bg-accent/50 text-muted-foreground">
|
|
|
|
|
<Calendar className="h-3 w-3" />
|
|
|
|
|
Due date
|
|
|
|
|
</button>
|
|
|
|
|
</PopoverContent>
|
|
|
|
|
</Popover>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* Footer */}
|
2026-02-17 20:46:12 -06:00
|
|
|
<div className="flex items-center justify-between px-4 py-2.5 border-t border-border shrink-0">
|
|
|
|
|
<Button
|
|
|
|
|
variant="ghost"
|
|
|
|
|
size="sm"
|
|
|
|
|
className="text-muted-foreground"
|
|
|
|
|
onClick={discardDraft}
|
2026-03-10 21:06:16 -05:00
|
|
|
disabled={createIssue.isPending || !canDiscardDraft}
|
2026-02-17 20:46:12 -06:00
|
|
|
>
|
|
|
|
|
Discard Draft
|
|
|
|
|
</Button>
|
2026-03-10 21:06:16 -05:00
|
|
|
<div className="flex items-center gap-3">
|
|
|
|
|
<div className="min-h-5 text-right">
|
|
|
|
|
{createIssue.isPending ? (
|
|
|
|
|
<span className="inline-flex items-center gap-1.5 text-xs font-medium text-muted-foreground">
|
|
|
|
|
<Loader2 className="h-3 w-3 animate-spin" />
|
|
|
|
|
Creating issue...
|
|
|
|
|
</span>
|
|
|
|
|
) : createIssue.isError ? (
|
|
|
|
|
<span className="text-xs text-destructive">{createIssueErrorMessage}</span>
|
|
|
|
|
) : null}
|
|
|
|
|
</div>
|
|
|
|
|
<Button
|
|
|
|
|
size="sm"
|
|
|
|
|
className="min-w-[8.5rem] disabled:opacity-100"
|
|
|
|
|
disabled={!title.trim() || createIssue.isPending}
|
|
|
|
|
onClick={handleSubmit}
|
|
|
|
|
aria-busy={createIssue.isPending}
|
|
|
|
|
>
|
|
|
|
|
<span className="inline-flex items-center justify-center gap-1.5">
|
|
|
|
|
{createIssue.isPending ? <Loader2 className="h-3.5 w-3.5 animate-spin" /> : null}
|
2026-04-06 10:58:59 -05:00
|
|
|
<span>{createIssue.isPending ? "Creating..." : isSubIssueMode ? "Create Sub-Issue" : "Create Issue"}</span>
|
2026-03-10 21:06:16 -05:00
|
|
|
</span>
|
|
|
|
|
</Button>
|
|
|
|
|
</div>
|
2026-02-17 10:53:20 -06:00
|
|
|
</div>
|
Add detail pages, property panels, and restyle list pages
New pages: AgentDetail, GoalDetail, IssueDetail, ProjectDetail, Inbox,
MyIssues. New feature components: AgentProperties, GoalProperties,
IssueProperties, ProjectProperties, GoalTree, NewIssueDialog. Add
heartbeats API client. Restyle all list pages (Agents, Issues, Goals,
Projects, Dashboard, Costs, Activity, Org) with EntityRow, FilterBar,
and improved layouts. Add routing for detail views.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 09:57:06 -06:00
|
|
|
</DialogContent>
|
|
|
|
|
</Dialog>
|
|
|
|
|
);
|
|
|
|
|
}
|