2026-02-20 13:56:34 -06:00
|
|
|
import { useEffect, useRef, type ReactNode } from "react";
|
[codex] Improve issue detail and issue-list UX (#3678)
## Thinking Path
> - Paperclip orchestrates AI agents for zero-human companies
> - A core part of that is the operator experience around reading issue
state, agent chat, and sub-task structure
> - The current branch had a long run of issue-detail and issue-list UX
fixes that all improve how humans follow and steer active work
> - Those changes mostly live in the UI/chat surface and should be
reviewed together instead of mixed with workspace/runtime work
> - This pull request packages the issue-detail, chat, markdown, and
sub-issue list improvements into one standalone change
> - The benefit is a cleaner, less jumpy, more reliable issue workflow
on desktop and mobile without coupling it to unrelated server/runtime
refactors
## What Changed
- Stabilized issue chat runtime wiring, optimistic comment handling,
queued-comment cancellation, and composer anchoring during live updates
- Fixed several issue-detail rendering and navigation regressions
including placeholder bleed, local polling scope, mobile inbox-to-issue
transitions, and visible refresh resets
- Improved markdown and rich-content handling with advisory image
normalization, editor fallback behavior, touch mention recovery, and
`issue:` quicklook links
- Refined sub-issue behavior with parent-derived defaults, current-user
inheritance fixes, empty-state cleanup, and a reusable issue-list
presentation for sub-issues
- Added targeted UI tests for the new issue-detail, chat scroll/message,
placeholder-data, markdown, and issue-list behaviors
## Verification
- `pnpm vitest run ui/src/components/IssueChatThread.test.tsx
ui/src/components/MarkdownEditor.test.tsx
ui/src/components/IssuesList.test.tsx
ui/src/context/LiveUpdatesProvider.test.tsx
ui/src/lib/issue-chat-messages.test.ts
ui/src/lib/issue-chat-scroll.test.ts
ui/src/lib/issue-detail-subissues.test.ts
ui/src/lib/query-placeholder-data.test.tsx
ui/src/hooks/usePaperclipIssueRuntime.test.tsx`
## Risks
- Medium: this branch touches the highest-traffic issue-detail UI paths,
so regressions would show up as chat/thread or sub-issue UX glitches
- The changes are UI-heavy and would benefit from reviewer screenshots
or a quick manual browser pass before merge
## 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)
- [x] 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
- [ ] 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 12:50:48 -05:00
|
|
|
import { useQuery, useQueryClient, type InfiniteData, type QueryClient } from "@tanstack/react-query";
|
|
|
|
|
import type { Agent, Issue, IssueComment, LiveEvent } from "@paperclipai/shared";
|
2026-03-22 07:16:13 -05:00
|
|
|
import type { RunForIssue } from "../api/activity";
|
|
|
|
|
import type { ActiveRunForIssue, LiveRunForIssue } from "../api/heartbeats";
|
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 type { CompanyUserDirectoryResponse } from "../api/access";
|
[codex] Improve issue detail and issue-list UX (#3678)
## Thinking Path
> - Paperclip orchestrates AI agents for zero-human companies
> - A core part of that is the operator experience around reading issue
state, agent chat, and sub-task structure
> - The current branch had a long run of issue-detail and issue-list UX
fixes that all improve how humans follow and steer active work
> - Those changes mostly live in the UI/chat surface and should be
reviewed together instead of mixed with workspace/runtime work
> - This pull request packages the issue-detail, chat, markdown, and
sub-issue list improvements into one standalone change
> - The benefit is a cleaner, less jumpy, more reliable issue workflow
on desktop and mobile without coupling it to unrelated server/runtime
refactors
## What Changed
- Stabilized issue chat runtime wiring, optimistic comment handling,
queued-comment cancellation, and composer anchoring during live updates
- Fixed several issue-detail rendering and navigation regressions
including placeholder bleed, local polling scope, mobile inbox-to-issue
transitions, and visible refresh resets
- Improved markdown and rich-content handling with advisory image
normalization, editor fallback behavior, touch mention recovery, and
`issue:` quicklook links
- Refined sub-issue behavior with parent-derived defaults, current-user
inheritance fixes, empty-state cleanup, and a reusable issue-list
presentation for sub-issues
- Added targeted UI tests for the new issue-detail, chat scroll/message,
placeholder-data, markdown, and issue-list behaviors
## Verification
- `pnpm vitest run ui/src/components/IssueChatThread.test.tsx
ui/src/components/MarkdownEditor.test.tsx
ui/src/components/IssuesList.test.tsx
ui/src/context/LiveUpdatesProvider.test.tsx
ui/src/lib/issue-chat-messages.test.ts
ui/src/lib/issue-chat-scroll.test.ts
ui/src/lib/issue-detail-subissues.test.ts
ui/src/lib/query-placeholder-data.test.tsx
ui/src/hooks/usePaperclipIssueRuntime.test.tsx`
## Risks
- Medium: this branch touches the highest-traffic issue-detail UI paths,
so regressions would show up as chat/thread or sub-issue UX glitches
- The changes are UI-heavy and would benefit from reviewer screenshots
or a quick manual browser pass before merge
## 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)
- [x] 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
- [ ] 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 12:50:48 -05:00
|
|
|
import { issuesApi } from "../api/issues";
|
2026-03-07 08:31:59 -06:00
|
|
|
import { authApi } from "../api/auth";
|
2026-02-17 12:24:48 -06:00
|
|
|
import { useCompany } from "./CompanyContext";
|
2026-02-20 13:47:13 -06:00
|
|
|
import type { ToastInput } from "./ToastContext";
|
[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 "./ToastContext";
|
[codex] Improve issue detail and issue-list UX (#3678)
## Thinking Path
> - Paperclip orchestrates AI agents for zero-human companies
> - A core part of that is the operator experience around reading issue
state, agent chat, and sub-task structure
> - The current branch had a long run of issue-detail and issue-list UX
fixes that all improve how humans follow and steer active work
> - Those changes mostly live in the UI/chat surface and should be
reviewed together instead of mixed with workspace/runtime work
> - This pull request packages the issue-detail, chat, markdown, and
sub-issue list improvements into one standalone change
> - The benefit is a cleaner, less jumpy, more reliable issue workflow
on desktop and mobile without coupling it to unrelated server/runtime
refactors
## What Changed
- Stabilized issue chat runtime wiring, optimistic comment handling,
queued-comment cancellation, and composer anchoring during live updates
- Fixed several issue-detail rendering and navigation regressions
including placeholder bleed, local polling scope, mobile inbox-to-issue
transitions, and visible refresh resets
- Improved markdown and rich-content handling with advisory image
normalization, editor fallback behavior, touch mention recovery, and
`issue:` quicklook links
- Refined sub-issue behavior with parent-derived defaults, current-user
inheritance fixes, empty-state cleanup, and a reusable issue-list
presentation for sub-issues
- Added targeted UI tests for the new issue-detail, chat scroll/message,
placeholder-data, markdown, and issue-list behaviors
## Verification
- `pnpm vitest run ui/src/components/IssueChatThread.test.tsx
ui/src/components/MarkdownEditor.test.tsx
ui/src/components/IssuesList.test.tsx
ui/src/context/LiveUpdatesProvider.test.tsx
ui/src/lib/issue-chat-messages.test.ts
ui/src/lib/issue-chat-scroll.test.ts
ui/src/lib/issue-detail-subissues.test.ts
ui/src/lib/query-placeholder-data.test.tsx
ui/src/hooks/usePaperclipIssueRuntime.test.tsx`
## Risks
- Medium: this branch touches the highest-traffic issue-detail UI paths,
so regressions would show up as chat/thread or sub-issue UX glitches
- The changes are UI-heavy and would benefit from reviewer screenshots
or a quick manual browser pass before merge
## 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)
- [x] 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
- [ ] 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 12:50:48 -05:00
|
|
|
import { upsertIssueCommentInPages } from "../lib/optimistic-issue-comments";
|
[codex] Polish issue board workflows (#4224)
## Thinking Path
> - Paperclip orchestrates AI agents for zero-human companies
> - Human operators supervise that work through issue lists, issue
detail, comments, inbox groups, markdown references, and
profile/activity surfaces
> - The branch had many small UI fixes that improve the operator loop
but do not need to ship with backend runtime migrations
> - These changes belong together as board workflow polish because they
affect scanning, navigation, issue context, comment state, and markdown
clarity
> - This pull request groups the UI-only slice so it can merge
independently from runtime/backend changes
> - The benefit is a clearer board experience with better issue context,
steadier optimistic updates, and more predictable keyboard navigation
## What Changed
- Improves issue properties, sub-issue actions, blocker chips, and issue
list/detail refresh behavior.
- Adds blocker context above the issue composer and stabilizes
queued/interrupted comment UI state.
- Improves markdown issue/GitHub link rendering and opens external
markdown links in a new tab.
- Adds inbox group keyboard navigation and fold/unfold support.
- Polishes activity/avatar/profile/settings/workspace presentation
details.
## Verification
- `pnpm exec vitest run ui/src/components/IssueProperties.test.tsx
ui/src/components/IssueChatThread.test.tsx
ui/src/components/MarkdownBody.test.tsx ui/src/lib/inbox.test.ts
ui/src/lib/optimistic-issue-comments.test.ts`
## Risks
- Low to medium risk: changes are UI-focused but cover high-traffic
issue and inbox surfaces.
- This branch intentionally does not include the backend runtime changes
from the companion PR; where UI calls newer API filters, unsupported
servers should continue to fail visibly through existing API error
handling.
- Visual screenshots were not captured in this heartbeat; targeted
component/helper tests cover the changed behavior.
> 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-based coding agent runtime, shell/git tool use
enabled. Exact hosted model build and context window are not exposed in
this Paperclip heartbeat environment.
## 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
- [ ] 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
2026-04-21 12:25:34 -05:00
|
|
|
import { clearIssueExecutionRun, removeLiveRunById } from "../lib/optimistic-issue-runs";
|
2026-02-17 12:24:48 -06:00
|
|
|
import { queryKeys } from "../lib/queryKeys";
|
2026-03-22 07:16:13 -05:00
|
|
|
import { toCompanyRelativePath } from "../lib/company-routes";
|
|
|
|
|
import { useLocation } from "../lib/router";
|
2026-02-17 12:24:48 -06:00
|
|
|
|
2026-02-20 13:56:34 -06:00
|
|
|
const TOAST_COOLDOWN_WINDOW_MS = 10_000;
|
|
|
|
|
const TOAST_COOLDOWN_MAX = 3;
|
|
|
|
|
const RECONNECT_SUPPRESS_MS = 2000;
|
2026-03-30 07:17:23 -05:00
|
|
|
const SOCKET_CONNECTING = 0;
|
|
|
|
|
const SOCKET_OPEN = 1;
|
[codex] Polish issue board workflows (#4224)
## Thinking Path
> - Paperclip orchestrates AI agents for zero-human companies
> - Human operators supervise that work through issue lists, issue
detail, comments, inbox groups, markdown references, and
profile/activity surfaces
> - The branch had many small UI fixes that improve the operator loop
but do not need to ship with backend runtime migrations
> - These changes belong together as board workflow polish because they
affect scanning, navigation, issue context, comment state, and markdown
clarity
> - This pull request groups the UI-only slice so it can merge
independently from runtime/backend changes
> - The benefit is a clearer board experience with better issue context,
steadier optimistic updates, and more predictable keyboard navigation
## What Changed
- Improves issue properties, sub-issue actions, blocker chips, and issue
list/detail refresh behavior.
- Adds blocker context above the issue composer and stabilizes
queued/interrupted comment UI state.
- Improves markdown issue/GitHub link rendering and opens external
markdown links in a new tab.
- Adds inbox group keyboard navigation and fold/unfold support.
- Polishes activity/avatar/profile/settings/workspace presentation
details.
## Verification
- `pnpm exec vitest run ui/src/components/IssueProperties.test.tsx
ui/src/components/IssueChatThread.test.tsx
ui/src/components/MarkdownBody.test.tsx ui/src/lib/inbox.test.ts
ui/src/lib/optimistic-issue-comments.test.ts`
## Risks
- Low to medium risk: changes are UI-focused but cover high-traffic
issue and inbox surfaces.
- This branch intentionally does not include the backend runtime changes
from the companion PR; where UI calls newer API filters, unsupported
servers should continue to fail visibly through existing API error
handling.
- Visual screenshots were not captured in this heartbeat; targeted
component/helper tests cover the changed behavior.
> 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-based coding agent runtime, shell/git tool use
enabled. Exact hosted model build and context window are not exposed in
this Paperclip heartbeat environment.
## 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
- [ ] 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
2026-04-21 12:25:34 -05:00
|
|
|
const TERMINAL_RUN_STATUSES = new Set(["succeeded", "failed", "cancelled", "timed_out"]);
|
2026-03-30 07:17:23 -05:00
|
|
|
|
|
|
|
|
type LiveUpdatesSocketLike = {
|
|
|
|
|
readyState: number;
|
|
|
|
|
onopen: ((this: WebSocket, ev: Event) => unknown) | null;
|
|
|
|
|
onmessage: ((this: WebSocket, ev: MessageEvent) => unknown) | null;
|
|
|
|
|
onerror: ((this: WebSocket, ev: Event) => unknown) | null;
|
|
|
|
|
onclose: ((this: WebSocket, ev: CloseEvent) => unknown) | null;
|
|
|
|
|
close: (code?: number, reason?: string) => void;
|
|
|
|
|
};
|
2026-02-20 13:56:34 -06:00
|
|
|
|
2026-02-17 12:24:48 -06:00
|
|
|
function readString(value: unknown): string | null {
|
|
|
|
|
return typeof value === "string" && value.length > 0 ? value : null;
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-20 13:47:13 -06:00
|
|
|
function readRecord(value: unknown): Record<string, unknown> | null {
|
|
|
|
|
if (typeof value !== "object" || value === null || Array.isArray(value)) return null;
|
|
|
|
|
return value as Record<string, unknown>;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function shortId(value: string) {
|
|
|
|
|
return value.slice(0, 8);
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-20 14:03:43 -06:00
|
|
|
function resolveAgentName(
|
|
|
|
|
queryClient: QueryClient,
|
|
|
|
|
companyId: string,
|
|
|
|
|
agentId: string,
|
|
|
|
|
): string | null {
|
|
|
|
|
const agents = queryClient.getQueryData<Agent[]>(queryKeys.agents.list(companyId));
|
|
|
|
|
if (!agents) return null;
|
|
|
|
|
const agent = agents.find((a) => a.id === agentId);
|
|
|
|
|
return agent?.name ?? null;
|
|
|
|
|
}
|
|
|
|
|
|
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
|
|
|
function resolveUserName(
|
|
|
|
|
queryClient: QueryClient,
|
|
|
|
|
companyId: string,
|
|
|
|
|
userId: string,
|
|
|
|
|
): string | null {
|
|
|
|
|
const directory = queryClient.getQueryData<CompanyUserDirectoryResponse>(
|
|
|
|
|
queryKeys.access.companyUserDirectory(companyId),
|
|
|
|
|
);
|
|
|
|
|
if (!directory) return null;
|
|
|
|
|
const entry = directory.users.find((u) => u.principalId === userId);
|
|
|
|
|
return entry?.user?.name?.trim() || entry?.user?.email?.trim() || null;
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-20 14:03:43 -06:00
|
|
|
function truncate(text: string, max: number): string {
|
|
|
|
|
if (text.length <= max) return text;
|
|
|
|
|
return text.slice(0, max - 1) + "\u2026";
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-20 15:48:42 -06:00
|
|
|
function resolveActorLabel(
|
|
|
|
|
queryClient: QueryClient,
|
|
|
|
|
companyId: string,
|
|
|
|
|
actorType: string | null,
|
|
|
|
|
actorId: string | null,
|
|
|
|
|
): string {
|
|
|
|
|
if (actorType === "agent" && actorId) {
|
|
|
|
|
return resolveAgentName(queryClient, companyId, actorId) ?? `Agent ${shortId(actorId)}`;
|
|
|
|
|
}
|
|
|
|
|
if (actorType === "system") return "System";
|
|
|
|
|
if (actorType === "user" && actorId) {
|
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 resolveUserName(queryClient, companyId, actorId) ?? "Board";
|
2026-02-20 15:48:42 -06:00
|
|
|
}
|
|
|
|
|
return "Someone";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
interface IssueToastContext {
|
|
|
|
|
ref: string;
|
|
|
|
|
title: string | null;
|
|
|
|
|
label: string;
|
|
|
|
|
href: string;
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-22 07:16:13 -05:00
|
|
|
interface VisibleRouteOptions {
|
|
|
|
|
isForegrounded?: boolean;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
interface VisibleIssueRouteContext {
|
[codex] Improve issue detail and issue-list UX (#3678)
## Thinking Path
> - Paperclip orchestrates AI agents for zero-human companies
> - A core part of that is the operator experience around reading issue
state, agent chat, and sub-task structure
> - The current branch had a long run of issue-detail and issue-list UX
fixes that all improve how humans follow and steer active work
> - Those changes mostly live in the UI/chat surface and should be
reviewed together instead of mixed with workspace/runtime work
> - This pull request packages the issue-detail, chat, markdown, and
sub-issue list improvements into one standalone change
> - The benefit is a cleaner, less jumpy, more reliable issue workflow
on desktop and mobile without coupling it to unrelated server/runtime
refactors
## What Changed
- Stabilized issue chat runtime wiring, optimistic comment handling,
queued-comment cancellation, and composer anchoring during live updates
- Fixed several issue-detail rendering and navigation regressions
including placeholder bleed, local polling scope, mobile inbox-to-issue
transitions, and visible refresh resets
- Improved markdown and rich-content handling with advisory image
normalization, editor fallback behavior, touch mention recovery, and
`issue:` quicklook links
- Refined sub-issue behavior with parent-derived defaults, current-user
inheritance fixes, empty-state cleanup, and a reusable issue-list
presentation for sub-issues
- Added targeted UI tests for the new issue-detail, chat scroll/message,
placeholder-data, markdown, and issue-list behaviors
## Verification
- `pnpm vitest run ui/src/components/IssueChatThread.test.tsx
ui/src/components/MarkdownEditor.test.tsx
ui/src/components/IssuesList.test.tsx
ui/src/context/LiveUpdatesProvider.test.tsx
ui/src/lib/issue-chat-messages.test.ts
ui/src/lib/issue-chat-scroll.test.ts
ui/src/lib/issue-detail-subissues.test.ts
ui/src/lib/query-placeholder-data.test.tsx
ui/src/hooks/usePaperclipIssueRuntime.test.tsx`
## Risks
- Medium: this branch touches the highest-traffic issue-detail UI paths,
so regressions would show up as chat/thread or sub-issue UX glitches
- The changes are UI-heavy and would benefit from reviewer screenshots
or a quick manual browser pass before merge
## 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)
- [x] 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
- [ ] 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 12:50:48 -05:00
|
|
|
routeIssueRef: string;
|
2026-03-22 07:16:13 -05:00
|
|
|
issueRefs: Set<string>;
|
|
|
|
|
assigneeAgentId: string | null;
|
|
|
|
|
runIds: Set<string>;
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-23 14:41:21 -06:00
|
|
|
function resolveIssueQueryRefs(
|
|
|
|
|
queryClient: QueryClient,
|
|
|
|
|
companyId: string,
|
|
|
|
|
issueId: string,
|
|
|
|
|
details: Record<string, unknown> | null,
|
|
|
|
|
): string[] {
|
|
|
|
|
const refs = new Set<string>([issueId]);
|
|
|
|
|
const detailIssue = queryClient.getQueryData<Issue>(queryKeys.issues.detail(issueId));
|
|
|
|
|
const listIssues = queryClient.getQueryData<Issue[]>(queryKeys.issues.list(companyId));
|
|
|
|
|
const detailsIdentifier =
|
|
|
|
|
readString(details?.identifier) ??
|
|
|
|
|
readString(details?.issueIdentifier);
|
|
|
|
|
|
|
|
|
|
if (detailsIdentifier) refs.add(detailsIdentifier);
|
|
|
|
|
|
|
|
|
|
if (detailIssue?.id) refs.add(detailIssue.id);
|
|
|
|
|
if (detailIssue?.identifier) refs.add(detailIssue.identifier);
|
|
|
|
|
|
|
|
|
|
const listIssue = listIssues?.find((issue) => {
|
|
|
|
|
if (issue.id === issueId) return true;
|
|
|
|
|
if (issue.identifier && issue.identifier === issueId) return true;
|
|
|
|
|
if (detailsIdentifier && issue.identifier === detailsIdentifier) return true;
|
|
|
|
|
return false;
|
|
|
|
|
});
|
|
|
|
|
if (listIssue?.id) refs.add(listIssue.id);
|
|
|
|
|
if (listIssue?.identifier) refs.add(listIssue.identifier);
|
|
|
|
|
|
|
|
|
|
return Array.from(refs);
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-20 15:48:42 -06:00
|
|
|
function resolveIssueToastContext(
|
|
|
|
|
queryClient: QueryClient,
|
|
|
|
|
companyId: string,
|
|
|
|
|
issueId: string,
|
|
|
|
|
details: Record<string, unknown> | null,
|
|
|
|
|
): IssueToastContext {
|
2026-02-23 14:41:21 -06:00
|
|
|
const issueRefs = resolveIssueQueryRefs(queryClient, companyId, issueId, details);
|
|
|
|
|
const detailIssue = issueRefs
|
|
|
|
|
.map((ref) => queryClient.getQueryData<Issue>(queryKeys.issues.detail(ref)))
|
|
|
|
|
.find((issue): issue is Issue => !!issue);
|
2026-02-20 15:48:42 -06:00
|
|
|
const listIssue = queryClient
|
|
|
|
|
.getQueryData<Issue[]>(queryKeys.issues.list(companyId))
|
2026-02-23 14:41:21 -06:00
|
|
|
?.find((issue) => issueRefs.some((ref) => issue.id === ref || issue.identifier === ref));
|
2026-02-20 15:48:42 -06:00
|
|
|
const cachedIssue = detailIssue ?? listIssue ?? null;
|
|
|
|
|
const ref =
|
|
|
|
|
readString(details?.identifier) ??
|
|
|
|
|
readString(details?.issueIdentifier) ??
|
|
|
|
|
cachedIssue?.identifier ??
|
|
|
|
|
`Issue ${shortId(issueId)}`;
|
|
|
|
|
const title =
|
|
|
|
|
readString(details?.title) ??
|
|
|
|
|
readString(details?.issueTitle) ??
|
|
|
|
|
cachedIssue?.title ??
|
|
|
|
|
null;
|
|
|
|
|
return {
|
|
|
|
|
ref,
|
|
|
|
|
title,
|
|
|
|
|
label: title ? `${ref} - ${truncate(title, 72)}` : ref,
|
2026-02-20 16:04:05 -06:00
|
|
|
href: `/issues/${cachedIssue?.identifier ?? issueId}`,
|
2026-02-20 15:48:42 -06:00
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-22 07:16:13 -05:00
|
|
|
function isPageForegrounded(): boolean {
|
|
|
|
|
if (typeof document === "undefined") return false;
|
|
|
|
|
if (document.visibilityState !== "visible") return false;
|
|
|
|
|
if (typeof document.hasFocus === "function" && !document.hasFocus()) return false;
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function resolveVisibleIssueRouteContext(
|
|
|
|
|
queryClient: QueryClient,
|
|
|
|
|
pathname: string,
|
|
|
|
|
options?: VisibleRouteOptions,
|
|
|
|
|
): VisibleIssueRouteContext | null {
|
|
|
|
|
const isForegrounded = options?.isForegrounded ?? isPageForegrounded();
|
|
|
|
|
if (!isForegrounded) return null;
|
|
|
|
|
|
|
|
|
|
const relativePath = toCompanyRelativePath(pathname);
|
|
|
|
|
const segments = relativePath.split("/").filter(Boolean);
|
|
|
|
|
if (segments[0] !== "issues" || !segments[1]) return null;
|
|
|
|
|
|
|
|
|
|
const issueRef = decodeURIComponent(segments[1]);
|
|
|
|
|
const issue = queryClient.getQueryData<Issue>(queryKeys.issues.detail(issueRef)) ?? null;
|
|
|
|
|
const issueRefs = new Set<string>([issueRef]);
|
|
|
|
|
if (issue?.id) issueRefs.add(issue.id);
|
|
|
|
|
if (issue?.identifier) issueRefs.add(issue.identifier);
|
|
|
|
|
|
|
|
|
|
const runIds = new Set<string>();
|
|
|
|
|
const activeRun = queryClient.getQueryData<ActiveRunForIssue | null>(queryKeys.issues.activeRun(issueRef));
|
|
|
|
|
const liveRuns = queryClient.getQueryData<LiveRunForIssue[]>(queryKeys.issues.liveRuns(issueRef)) ?? [];
|
|
|
|
|
const linkedRuns = queryClient.getQueryData<RunForIssue[]>(queryKeys.issues.runs(issueRef)) ?? [];
|
|
|
|
|
|
|
|
|
|
if (activeRun?.id) runIds.add(activeRun.id);
|
|
|
|
|
for (const run of liveRuns) {
|
|
|
|
|
if (run.id) runIds.add(run.id);
|
|
|
|
|
}
|
|
|
|
|
for (const run of linkedRuns) {
|
|
|
|
|
if (run.runId) runIds.add(run.runId);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return {
|
[codex] Improve issue detail and issue-list UX (#3678)
## Thinking Path
> - Paperclip orchestrates AI agents for zero-human companies
> - A core part of that is the operator experience around reading issue
state, agent chat, and sub-task structure
> - The current branch had a long run of issue-detail and issue-list UX
fixes that all improve how humans follow and steer active work
> - Those changes mostly live in the UI/chat surface and should be
reviewed together instead of mixed with workspace/runtime work
> - This pull request packages the issue-detail, chat, markdown, and
sub-issue list improvements into one standalone change
> - The benefit is a cleaner, less jumpy, more reliable issue workflow
on desktop and mobile without coupling it to unrelated server/runtime
refactors
## What Changed
- Stabilized issue chat runtime wiring, optimistic comment handling,
queued-comment cancellation, and composer anchoring during live updates
- Fixed several issue-detail rendering and navigation regressions
including placeholder bleed, local polling scope, mobile inbox-to-issue
transitions, and visible refresh resets
- Improved markdown and rich-content handling with advisory image
normalization, editor fallback behavior, touch mention recovery, and
`issue:` quicklook links
- Refined sub-issue behavior with parent-derived defaults, current-user
inheritance fixes, empty-state cleanup, and a reusable issue-list
presentation for sub-issues
- Added targeted UI tests for the new issue-detail, chat scroll/message,
placeholder-data, markdown, and issue-list behaviors
## Verification
- `pnpm vitest run ui/src/components/IssueChatThread.test.tsx
ui/src/components/MarkdownEditor.test.tsx
ui/src/components/IssuesList.test.tsx
ui/src/context/LiveUpdatesProvider.test.tsx
ui/src/lib/issue-chat-messages.test.ts
ui/src/lib/issue-chat-scroll.test.ts
ui/src/lib/issue-detail-subissues.test.ts
ui/src/lib/query-placeholder-data.test.tsx
ui/src/hooks/usePaperclipIssueRuntime.test.tsx`
## Risks
- Medium: this branch touches the highest-traffic issue-detail UI paths,
so regressions would show up as chat/thread or sub-issue UX glitches
- The changes are UI-heavy and would benefit from reviewer screenshots
or a quick manual browser pass before merge
## 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)
- [x] 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
- [ ] 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 12:50:48 -05:00
|
|
|
routeIssueRef: issueRef,
|
2026-03-22 07:16:13 -05:00
|
|
|
issueRefs,
|
|
|
|
|
assigneeAgentId: issue?.assigneeAgentId ?? null,
|
|
|
|
|
runIds,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function buildIssueRefsForPayload(entityId: string, details: Record<string, unknown> | null): Set<string> {
|
|
|
|
|
const refs = new Set<string>([entityId]);
|
|
|
|
|
const identifier = readString(details?.identifier) ?? readString(details?.issueIdentifier);
|
|
|
|
|
if (identifier) refs.add(identifier);
|
|
|
|
|
return refs;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function overlaps(a: Set<string>, b: Set<string>): boolean {
|
|
|
|
|
for (const value of a) {
|
|
|
|
|
if (b.has(value)) return true;
|
|
|
|
|
}
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function shouldSuppressActivityToastForVisibleIssue(
|
|
|
|
|
queryClient: QueryClient,
|
|
|
|
|
pathname: string,
|
|
|
|
|
payload: Record<string, unknown>,
|
|
|
|
|
options?: VisibleRouteOptions,
|
|
|
|
|
): boolean {
|
|
|
|
|
const entityType = readString(payload.entityType);
|
|
|
|
|
const entityId = readString(payload.entityId);
|
|
|
|
|
if (entityType !== "issue" || !entityId) return false;
|
|
|
|
|
|
|
|
|
|
const context = resolveVisibleIssueRouteContext(queryClient, pathname, options);
|
|
|
|
|
if (!context) return false;
|
|
|
|
|
|
|
|
|
|
return overlaps(context.issueRefs, buildIssueRefsForPayload(entityId, readRecord(payload.details)));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function shouldSuppressRunStatusToastForVisibleIssue(
|
|
|
|
|
queryClient: QueryClient,
|
|
|
|
|
pathname: string,
|
|
|
|
|
payload: Record<string, unknown>,
|
|
|
|
|
options?: VisibleRouteOptions,
|
|
|
|
|
): boolean {
|
|
|
|
|
const context = resolveVisibleIssueRouteContext(queryClient, pathname, options);
|
|
|
|
|
if (!context) return false;
|
|
|
|
|
|
|
|
|
|
const runId = readString(payload.runId);
|
|
|
|
|
if (runId && context.runIds.has(runId)) return true;
|
|
|
|
|
|
|
|
|
|
const agentId = readString(payload.agentId);
|
|
|
|
|
return !!agentId && !!context.assigneeAgentId && agentId === context.assigneeAgentId;
|
|
|
|
|
}
|
|
|
|
|
|
[codex] improve issue and routine UI responsiveness (#3744)
## Thinking Path
> - Paperclip orchestrates AI agents for zero-human companies
> - Operators rely on issue, inbox, and routine views to understand what
the company is doing in real time
> - Those views need to stay fast and readable even when issue lists,
markdown comments, and run metadata get large
> - The current branch had a coherent set of UI and live-update
improvements spread across issue search, issue detail rendering, routine
affordances, and workspace lookups
> - This pull request groups those board-facing changes into one
standalone branch that can merge independently of the heartbeat/runtime
work
> - The benefit is a faster, clearer issue and routine workflow without
changing the underlying task model
## What Changed
- Show routine execution issues by default and rename the filter to
`Hide routine runs` so the default state no longer looks like an active
filter.
- Show the routine name in the run dialog and tighten the issue
properties pane with a workspace link, copy-on-click behavior, and an
inline parent arrow.
- Reduce issue detail rerenders, keep queued issue chat mounted, improve
issues page search responsiveness, and speed up issues first paint.
- Add inbox "other search results", refresh visible issue runs after
status updates, and optimize workspace lookups through summary-mode
execution workspace queries.
- Improve markdown wrapping and scrolling behavior for long strings and
self-comment code blocks.
- Relax the markdown sanitizer assertion so the test still validates
safety after the new wrap-friendly inline styles.
## Verification
- `pnpm vitest run ui/src/components/IssuesList.test.tsx
ui/src/lib/inbox.test.ts ui/src/pages/Issues.test.tsx
ui/src/context/BreadcrumbContext.test.tsx
ui/src/context/LiveUpdatesProvider.test.ts
ui/src/components/MarkdownBody.test.tsx
ui/src/api/execution-workspaces.test.ts
server/src/__tests__/execution-workspaces-routes.test.ts`
## Risks
- This touches several issue-facing UI surfaces at once, so regressions
would most likely show up as stale rendering, search result mismatches,
or small markdown presentation differences.
- The workspace lookup optimization depends on the summary-mode route
shape staying aligned between server and UI.
## Model Used
- OpenAI Codex, GPT-5-based coding agent in the Codex CLI environment.
Exact backend model deployment ID was not exposed in-session.
Tool-assisted editing and shell execution were used.
## 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
- [ ] 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-15 15:54:05 -05:00
|
|
|
function invalidateVisibleIssueRunQueries(
|
|
|
|
|
queryClient: QueryClient,
|
|
|
|
|
pathname: string,
|
|
|
|
|
payload: Record<string, unknown>,
|
|
|
|
|
options?: VisibleRouteOptions,
|
|
|
|
|
): boolean {
|
|
|
|
|
const context = resolveVisibleIssueRouteContext(queryClient, pathname, options);
|
|
|
|
|
if (!context) return false;
|
|
|
|
|
|
|
|
|
|
const runId = readString(payload.runId);
|
|
|
|
|
const agentId = readString(payload.agentId);
|
|
|
|
|
const matchesVisibleIssue =
|
|
|
|
|
(runId !== null && context.runIds.has(runId)) ||
|
|
|
|
|
(!!agentId && !!context.assigneeAgentId && agentId === context.assigneeAgentId);
|
|
|
|
|
if (!matchesVisibleIssue) return false;
|
|
|
|
|
|
[codex] Polish issue board workflows (#4224)
## Thinking Path
> - Paperclip orchestrates AI agents for zero-human companies
> - Human operators supervise that work through issue lists, issue
detail, comments, inbox groups, markdown references, and
profile/activity surfaces
> - The branch had many small UI fixes that improve the operator loop
but do not need to ship with backend runtime migrations
> - These changes belong together as board workflow polish because they
affect scanning, navigation, issue context, comment state, and markdown
clarity
> - This pull request groups the UI-only slice so it can merge
independently from runtime/backend changes
> - The benefit is a clearer board experience with better issue context,
steadier optimistic updates, and more predictable keyboard navigation
## What Changed
- Improves issue properties, sub-issue actions, blocker chips, and issue
list/detail refresh behavior.
- Adds blocker context above the issue composer and stabilizes
queued/interrupted comment UI state.
- Improves markdown issue/GitHub link rendering and opens external
markdown links in a new tab.
- Adds inbox group keyboard navigation and fold/unfold support.
- Polishes activity/avatar/profile/settings/workspace presentation
details.
## Verification
- `pnpm exec vitest run ui/src/components/IssueProperties.test.tsx
ui/src/components/IssueChatThread.test.tsx
ui/src/components/MarkdownBody.test.tsx ui/src/lib/inbox.test.ts
ui/src/lib/optimistic-issue-comments.test.ts`
## Risks
- Low to medium risk: changes are UI-focused but cover high-traffic
issue and inbox surfaces.
- This branch intentionally does not include the backend runtime changes
from the companion PR; where UI calls newer API filters, unsupported
servers should continue to fail visibly through existing API error
handling.
- Visual screenshots were not captured in this heartbeat; targeted
component/helper tests cover the changed behavior.
> 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-based coding agent runtime, shell/git tool use
enabled. Exact hosted model build and context window are not exposed in
this Paperclip heartbeat environment.
## 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
- [ ] 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
2026-04-21 12:25:34 -05:00
|
|
|
const status = readString(payload.status);
|
|
|
|
|
if (runId && status && TERMINAL_RUN_STATUSES.has(status)) {
|
|
|
|
|
queryClient.setQueryData(
|
|
|
|
|
queryKeys.issues.liveRuns(context.routeIssueRef),
|
|
|
|
|
(current: LiveRunForIssue[] | undefined) => removeLiveRunById(current, runId),
|
|
|
|
|
);
|
|
|
|
|
queryClient.setQueryData(
|
|
|
|
|
queryKeys.issues.activeRun(context.routeIssueRef),
|
|
|
|
|
(current: ActiveRunForIssue | null | undefined) => (current?.id === runId ? null : current),
|
|
|
|
|
);
|
|
|
|
|
queryClient.setQueryData(
|
|
|
|
|
queryKeys.issues.detail(context.routeIssueRef),
|
|
|
|
|
(current: Issue | undefined) => clearIssueExecutionRun(current, runId),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
[codex] improve issue and routine UI responsiveness (#3744)
## Thinking Path
> - Paperclip orchestrates AI agents for zero-human companies
> - Operators rely on issue, inbox, and routine views to understand what
the company is doing in real time
> - Those views need to stay fast and readable even when issue lists,
markdown comments, and run metadata get large
> - The current branch had a coherent set of UI and live-update
improvements spread across issue search, issue detail rendering, routine
affordances, and workspace lookups
> - This pull request groups those board-facing changes into one
standalone branch that can merge independently of the heartbeat/runtime
work
> - The benefit is a faster, clearer issue and routine workflow without
changing the underlying task model
## What Changed
- Show routine execution issues by default and rename the filter to
`Hide routine runs` so the default state no longer looks like an active
filter.
- Show the routine name in the run dialog and tighten the issue
properties pane with a workspace link, copy-on-click behavior, and an
inline parent arrow.
- Reduce issue detail rerenders, keep queued issue chat mounted, improve
issues page search responsiveness, and speed up issues first paint.
- Add inbox "other search results", refresh visible issue runs after
status updates, and optimize workspace lookups through summary-mode
execution workspace queries.
- Improve markdown wrapping and scrolling behavior for long strings and
self-comment code blocks.
- Relax the markdown sanitizer assertion so the test still validates
safety after the new wrap-friendly inline styles.
## Verification
- `pnpm vitest run ui/src/components/IssuesList.test.tsx
ui/src/lib/inbox.test.ts ui/src/pages/Issues.test.tsx
ui/src/context/BreadcrumbContext.test.tsx
ui/src/context/LiveUpdatesProvider.test.ts
ui/src/components/MarkdownBody.test.tsx
ui/src/api/execution-workspaces.test.ts
server/src/__tests__/execution-workspaces-routes.test.ts`
## Risks
- This touches several issue-facing UI surfaces at once, so regressions
would most likely show up as stale rendering, search result mismatches,
or small markdown presentation differences.
- The workspace lookup optimization depends on the summary-mode route
shape staying aligned between server and UI.
## Model Used
- OpenAI Codex, GPT-5-based coding agent in the Codex CLI environment.
Exact backend model deployment ID was not exposed in-session.
Tool-assisted editing and shell execution were used.
## 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
- [ ] 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-15 15:54:05 -05:00
|
|
|
queryClient.invalidateQueries({ queryKey: queryKeys.issues.detail(context.routeIssueRef) });
|
|
|
|
|
queryClient.invalidateQueries({ queryKey: queryKeys.issues.activity(context.routeIssueRef) });
|
|
|
|
|
queryClient.invalidateQueries({ queryKey: queryKeys.issues.runs(context.routeIssueRef) });
|
|
|
|
|
queryClient.invalidateQueries({ queryKey: queryKeys.issues.liveRuns(context.routeIssueRef) });
|
|
|
|
|
queryClient.invalidateQueries({ queryKey: queryKeys.issues.activeRun(context.routeIssueRef) });
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-22 07:16:13 -05:00
|
|
|
function shouldSuppressAgentStatusToastForVisibleIssue(
|
|
|
|
|
queryClient: QueryClient,
|
|
|
|
|
pathname: string,
|
|
|
|
|
payload: Record<string, unknown>,
|
|
|
|
|
options?: VisibleRouteOptions,
|
|
|
|
|
): boolean {
|
|
|
|
|
const context = resolveVisibleIssueRouteContext(queryClient, pathname, options);
|
|
|
|
|
if (!context?.assigneeAgentId) return false;
|
|
|
|
|
|
|
|
|
|
const agentId = readString(payload.agentId);
|
|
|
|
|
return !!agentId && agentId === context.assigneeAgentId;
|
|
|
|
|
}
|
|
|
|
|
|
[codex] Improve issue detail and issue-list UX (#3678)
## Thinking Path
> - Paperclip orchestrates AI agents for zero-human companies
> - A core part of that is the operator experience around reading issue
state, agent chat, and sub-task structure
> - The current branch had a long run of issue-detail and issue-list UX
fixes that all improve how humans follow and steer active work
> - Those changes mostly live in the UI/chat surface and should be
reviewed together instead of mixed with workspace/runtime work
> - This pull request packages the issue-detail, chat, markdown, and
sub-issue list improvements into one standalone change
> - The benefit is a cleaner, less jumpy, more reliable issue workflow
on desktop and mobile without coupling it to unrelated server/runtime
refactors
## What Changed
- Stabilized issue chat runtime wiring, optimistic comment handling,
queued-comment cancellation, and composer anchoring during live updates
- Fixed several issue-detail rendering and navigation regressions
including placeholder bleed, local polling scope, mobile inbox-to-issue
transitions, and visible refresh resets
- Improved markdown and rich-content handling with advisory image
normalization, editor fallback behavior, touch mention recovery, and
`issue:` quicklook links
- Refined sub-issue behavior with parent-derived defaults, current-user
inheritance fixes, empty-state cleanup, and a reusable issue-list
presentation for sub-issues
- Added targeted UI tests for the new issue-detail, chat scroll/message,
placeholder-data, markdown, and issue-list behaviors
## Verification
- `pnpm vitest run ui/src/components/IssueChatThread.test.tsx
ui/src/components/MarkdownEditor.test.tsx
ui/src/components/IssuesList.test.tsx
ui/src/context/LiveUpdatesProvider.test.tsx
ui/src/lib/issue-chat-messages.test.ts
ui/src/lib/issue-chat-scroll.test.ts
ui/src/lib/issue-detail-subissues.test.ts
ui/src/lib/query-placeholder-data.test.tsx
ui/src/hooks/usePaperclipIssueRuntime.test.tsx`
## Risks
- Medium: this branch touches the highest-traffic issue-detail UI paths,
so regressions would show up as chat/thread or sub-issue UX glitches
- The changes are UI-heavy and would benefit from reviewer screenshots
or a quick manual browser pass before merge
## 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)
- [x] 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
- [ ] 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 12:50:48 -05:00
|
|
|
function shouldDeferIssueRefetchForVisibleAgentActivity(
|
|
|
|
|
queryClient: QueryClient,
|
|
|
|
|
pathname: string,
|
|
|
|
|
payload: Record<string, unknown>,
|
|
|
|
|
options?: VisibleRouteOptions,
|
|
|
|
|
): boolean {
|
|
|
|
|
const entityType = readString(payload.entityType);
|
|
|
|
|
const entityId = readString(payload.entityId);
|
|
|
|
|
const actorType = readString(payload.actorType);
|
|
|
|
|
const action = readString(payload.action);
|
|
|
|
|
const details = readRecord(payload.details);
|
|
|
|
|
|
|
|
|
|
if (entityType !== "issue" || !entityId) return false;
|
|
|
|
|
if (actorType !== "agent" && actorType !== "system") return false;
|
|
|
|
|
if (action !== "issue.updated") return false;
|
|
|
|
|
if (readString(details?.source) === "comment") return false;
|
|
|
|
|
|
|
|
|
|
const context = resolveVisibleIssueRouteContext(queryClient, pathname, options);
|
|
|
|
|
if (!context) return false;
|
|
|
|
|
|
|
|
|
|
return overlaps(context.issueRefs, buildIssueRefsForPayload(entityId, details));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function shouldDeferVisibleIssueCommentActivity(
|
|
|
|
|
queryClient: QueryClient,
|
|
|
|
|
pathname: string,
|
|
|
|
|
payload: Record<string, unknown>,
|
|
|
|
|
options?: VisibleRouteOptions,
|
|
|
|
|
): boolean {
|
|
|
|
|
const entityType = readString(payload.entityType);
|
|
|
|
|
const entityId = readString(payload.entityId);
|
|
|
|
|
const action = readString(payload.action);
|
|
|
|
|
const details = readRecord(payload.details);
|
|
|
|
|
|
|
|
|
|
if (entityType !== "issue" || !entityId) return false;
|
|
|
|
|
if (action !== "issue.comment_added") return false;
|
|
|
|
|
|
|
|
|
|
const context = resolveVisibleIssueRouteContext(queryClient, pathname, options);
|
|
|
|
|
if (!context) return false;
|
|
|
|
|
|
|
|
|
|
return overlaps(context.issueRefs, buildIssueRefsForPayload(entityId, details));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function hydrateVisibleIssueComment(
|
|
|
|
|
queryClient: QueryClient,
|
|
|
|
|
pathname: string,
|
|
|
|
|
payload: Record<string, unknown>,
|
|
|
|
|
options?: VisibleRouteOptions,
|
|
|
|
|
) {
|
|
|
|
|
const entityType = readString(payload.entityType);
|
|
|
|
|
const action = readString(payload.action);
|
|
|
|
|
const details = readRecord(payload.details);
|
|
|
|
|
const commentId = readString(details?.commentId);
|
|
|
|
|
|
|
|
|
|
if (entityType !== "issue" || action !== "issue.comment_added" || !commentId) return false;
|
|
|
|
|
|
|
|
|
|
const context = resolveVisibleIssueRouteContext(queryClient, pathname, options);
|
|
|
|
|
if (!context) return false;
|
|
|
|
|
|
|
|
|
|
const entityId = readString(payload.entityId);
|
|
|
|
|
if (!entityId || !overlaps(context.issueRefs, buildIssueRefsForPayload(entityId, details))) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const comment = await issuesApi.getComment(context.routeIssueRef, commentId);
|
|
|
|
|
queryClient.setQueryData<InfiniteData<IssueComment[], string | null> | undefined>(
|
|
|
|
|
queryKeys.issues.comments(context.routeIssueRef),
|
|
|
|
|
(current) => {
|
|
|
|
|
if (!current) {
|
|
|
|
|
return {
|
|
|
|
|
pages: [[comment]],
|
|
|
|
|
pageParams: [null],
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
...current,
|
|
|
|
|
pages: upsertIssueCommentInPages(current.pages, comment),
|
|
|
|
|
};
|
|
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
return true;
|
|
|
|
|
} catch {
|
|
|
|
|
queryClient.invalidateQueries({ queryKey: queryKeys.issues.comments(context.routeIssueRef) });
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-20 13:47:13 -06:00
|
|
|
const ISSUE_TOAST_ACTIONS = new Set(["issue.created", "issue.updated", "issue.comment_added"]);
|
2026-03-28 19:33:52 -05:00
|
|
|
const AGENT_TOAST_STATUSES = new Set(["error"]);
|
|
|
|
|
const RUN_TOAST_STATUSES = new Set(["failed", "timed_out", "cancelled"]);
|
2026-02-20 13:47:13 -06:00
|
|
|
|
2026-02-20 14:03:43 -06:00
|
|
|
function describeIssueUpdate(details: Record<string, unknown> | null): string | null {
|
|
|
|
|
if (!details) return null;
|
|
|
|
|
const changes: string[] = [];
|
2026-02-20 15:48:42 -06:00
|
|
|
if (typeof details.status === "string") changes.push(`status -> ${details.status.replace(/_/g, " ")}`);
|
|
|
|
|
if (typeof details.priority === "string") changes.push(`priority -> ${details.priority}`);
|
2026-02-26 16:33:48 -06:00
|
|
|
if (typeof details.assigneeAgentId === "string" || typeof details.assigneeUserId === "string") {
|
|
|
|
|
changes.push("reassigned");
|
|
|
|
|
} else if (details.assigneeAgentId === null || details.assigneeUserId === null) {
|
|
|
|
|
changes.push("unassigned");
|
|
|
|
|
}
|
2026-02-20 15:48:42 -06:00
|
|
|
if (details.reopened === true) {
|
|
|
|
|
const from = readString(details.reopenedFrom);
|
|
|
|
|
changes.push(from ? `reopened from ${from.replace(/_/g, " ")}` : "reopened");
|
|
|
|
|
}
|
|
|
|
|
if (typeof details.title === "string") changes.push("title changed");
|
|
|
|
|
if (typeof details.description === "string") changes.push("description changed");
|
2026-02-20 14:03:43 -06:00
|
|
|
if (changes.length > 0) return changes.join(", ");
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function buildActivityToast(
|
2026-02-20 15:48:42 -06:00
|
|
|
queryClient: QueryClient,
|
|
|
|
|
companyId: string,
|
2026-02-20 14:03:43 -06:00
|
|
|
payload: Record<string, unknown>,
|
2026-03-07 08:31:59 -06:00
|
|
|
currentActor: { userId: string | null; agentId: string | null },
|
2026-02-20 14:03:43 -06:00
|
|
|
): ToastInput | null {
|
2026-02-20 13:47:13 -06:00
|
|
|
const entityType = readString(payload.entityType);
|
|
|
|
|
const entityId = readString(payload.entityId);
|
|
|
|
|
const action = readString(payload.action);
|
|
|
|
|
const details = readRecord(payload.details);
|
2026-02-20 14:03:43 -06:00
|
|
|
const actorId = readString(payload.actorId);
|
|
|
|
|
const actorType = readString(payload.actorType);
|
2026-02-20 13:47:13 -06:00
|
|
|
|
|
|
|
|
if (entityType !== "issue" || !entityId || !action || !ISSUE_TOAST_ACTIONS.has(action)) {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-20 15:48:42 -06:00
|
|
|
const issue = resolveIssueToastContext(queryClient, companyId, entityId, details);
|
|
|
|
|
const actor = resolveActorLabel(queryClient, companyId, actorType, actorId);
|
2026-03-07 08:42:58 -06:00
|
|
|
const isSelfActivity =
|
|
|
|
|
(actorType === "user" && !!currentActor.userId && actorId === currentActor.userId) ||
|
|
|
|
|
(actorType === "agent" && !!currentActor.agentId && actorId === currentActor.agentId);
|
|
|
|
|
if (isSelfActivity) return null;
|
2026-02-20 13:47:13 -06:00
|
|
|
|
|
|
|
|
if (action === "issue.created") {
|
|
|
|
|
return {
|
2026-02-20 15:48:42 -06:00
|
|
|
title: `${actor} created ${issue.ref}`,
|
|
|
|
|
body: issue.title ? truncate(issue.title, 96) : undefined,
|
2026-02-20 13:47:13 -06:00
|
|
|
tone: "success",
|
2026-02-20 15:48:42 -06:00
|
|
|
action: { label: `View ${issue.ref}`, href: issue.href },
|
2026-02-20 13:47:13 -06:00
|
|
|
dedupeKey: `activity:${action}:${entityId}`,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (action === "issue.updated") {
|
2026-03-07 10:32:22 -06:00
|
|
|
if (readString(details?.source) === "comment") {
|
|
|
|
|
// Comment-driven updates emit a paired comment event; show one combined toast on the comment event.
|
2026-03-02 16:44:03 -06:00
|
|
|
return null;
|
|
|
|
|
}
|
2026-02-20 14:03:43 -06:00
|
|
|
const changeDesc = describeIssueUpdate(details);
|
2026-02-20 15:48:42 -06:00
|
|
|
const body = changeDesc
|
|
|
|
|
? issue.title
|
|
|
|
|
? `${truncate(issue.title, 64)} - ${changeDesc}`
|
|
|
|
|
: changeDesc
|
|
|
|
|
: issue.title
|
|
|
|
|
? truncate(issue.title, 96)
|
|
|
|
|
: issue.label;
|
2026-02-20 13:47:13 -06:00
|
|
|
return {
|
2026-02-20 15:48:42 -06:00
|
|
|
title: `${actor} updated ${issue.ref}`,
|
2026-02-20 14:03:43 -06:00
|
|
|
body: truncate(body, 100),
|
2026-02-20 13:47:13 -06:00
|
|
|
tone: "info",
|
2026-02-20 15:48:42 -06:00
|
|
|
action: { label: `View ${issue.ref}`, href: issue.href },
|
2026-02-20 13:47:13 -06:00
|
|
|
dedupeKey: `activity:${action}:${entityId}`,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const commentId = readString(details?.commentId);
|
2026-02-20 15:52:53 -06:00
|
|
|
const bodySnippet = readString(details?.bodySnippet);
|
2026-03-02 16:44:03 -06:00
|
|
|
const reopened = details?.reopened === true;
|
2026-03-07 10:32:22 -06:00
|
|
|
const updated = details?.updated === true;
|
2026-03-02 16:44:03 -06:00
|
|
|
const reopenedFrom = readString(details?.reopenedFrom);
|
|
|
|
|
const reopenedLabel = reopened
|
|
|
|
|
? reopenedFrom
|
|
|
|
|
? `reopened from ${reopenedFrom.replace(/_/g, " ")}`
|
|
|
|
|
: "reopened"
|
|
|
|
|
: null;
|
2026-03-07 10:32:22 -06:00
|
|
|
const title = reopened
|
|
|
|
|
? `${actor} reopened and commented on ${issue.ref}`
|
|
|
|
|
: updated
|
|
|
|
|
? `${actor} commented and updated ${issue.ref}`
|
|
|
|
|
: `${actor} commented on ${issue.ref}`;
|
2026-03-02 16:44:03 -06:00
|
|
|
const body = bodySnippet
|
|
|
|
|
? reopenedLabel
|
|
|
|
|
? `${reopenedLabel} - ${bodySnippet.replace(/^#+\s*/m, "").replace(/\n/g, " ")}`
|
|
|
|
|
: bodySnippet.replace(/^#+\s*/m, "").replace(/\n/g, " ")
|
|
|
|
|
: reopenedLabel
|
|
|
|
|
? issue.title
|
|
|
|
|
? `${reopenedLabel} - ${issue.title}`
|
|
|
|
|
: reopenedLabel
|
|
|
|
|
: issue.title ?? undefined;
|
2026-02-20 13:47:13 -06:00
|
|
|
return {
|
2026-03-02 16:44:03 -06:00
|
|
|
title,
|
|
|
|
|
body: body ? truncate(body, 96) : undefined,
|
2026-02-20 13:47:13 -06:00
|
|
|
tone: "info",
|
2026-02-20 15:48:42 -06:00
|
|
|
action: { label: `View ${issue.ref}`, href: issue.href },
|
2026-02-20 13:47:13 -06:00
|
|
|
dedupeKey: `activity:${action}:${entityId}:${commentId ?? "na"}`,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-06 14:28:25 -06:00
|
|
|
function buildJoinRequestToast(
|
|
|
|
|
payload: Record<string, unknown>,
|
|
|
|
|
): ToastInput | null {
|
|
|
|
|
const entityType = readString(payload.entityType);
|
|
|
|
|
const action = readString(payload.action);
|
|
|
|
|
const entityId = readString(payload.entityId);
|
|
|
|
|
const details = readRecord(payload.details);
|
|
|
|
|
|
|
|
|
|
if (entityType !== "join_request" || !action || !entityId) return null;
|
|
|
|
|
if (action !== "join.requested" && action !== "join.request_replayed") return null;
|
|
|
|
|
|
|
|
|
|
const requestType = readString(details?.requestType);
|
|
|
|
|
const label = requestType === "agent" ? "Agent" : "Someone";
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
title: `${label} wants to join`,
|
|
|
|
|
body: "A new join request is waiting for approval.",
|
|
|
|
|
tone: "info",
|
2026-03-26 08:19:16 -05:00
|
|
|
action: { label: "View inbox", href: "/inbox/mine" },
|
2026-03-06 14:28:25 -06:00
|
|
|
dedupeKey: `join-request:${entityId}`,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-20 14:03:43 -06:00
|
|
|
function buildAgentStatusToast(
|
|
|
|
|
payload: Record<string, unknown>,
|
|
|
|
|
nameOf: (id: string) => string | null,
|
2026-02-20 15:52:53 -06:00
|
|
|
queryClient: QueryClient,
|
|
|
|
|
companyId: string,
|
2026-02-20 14:03:43 -06:00
|
|
|
): ToastInput | null {
|
2026-02-20 13:47:13 -06:00
|
|
|
const agentId = readString(payload.agentId);
|
|
|
|
|
const status = readString(payload.status);
|
|
|
|
|
if (!agentId || !status || !AGENT_TOAST_STATUSES.has(status)) return null;
|
|
|
|
|
|
2026-03-02 16:44:03 -06:00
|
|
|
const tone = status === "error" ? "error" : "info";
|
2026-02-20 14:03:43 -06:00
|
|
|
const name = nameOf(agentId) ?? `Agent ${shortId(agentId)}`;
|
2026-02-20 13:47:13 -06:00
|
|
|
const title =
|
|
|
|
|
status === "running"
|
2026-02-20 14:03:43 -06:00
|
|
|
? `${name} started`
|
2026-03-02 16:44:03 -06:00
|
|
|
: `${name} errored`;
|
2026-02-20 13:47:13 -06:00
|
|
|
|
2026-02-20 15:52:53 -06:00
|
|
|
const agents = queryClient.getQueryData<Agent[]>(queryKeys.agents.list(companyId));
|
|
|
|
|
const agent = agents?.find((a) => a.id === agentId);
|
|
|
|
|
const body = agent?.title ?? undefined;
|
|
|
|
|
|
2026-02-20 13:47:13 -06:00
|
|
|
return {
|
|
|
|
|
title,
|
2026-02-20 15:52:53 -06:00
|
|
|
body,
|
2026-02-20 13:47:13 -06:00
|
|
|
tone,
|
|
|
|
|
action: { label: "View agent", href: `/agents/${agentId}` },
|
|
|
|
|
dedupeKey: `agent-status:${agentId}:${status}`,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-20 14:03:43 -06:00
|
|
|
function buildRunStatusToast(
|
|
|
|
|
payload: Record<string, unknown>,
|
|
|
|
|
nameOf: (id: string) => string | null,
|
|
|
|
|
): ToastInput | null {
|
2026-02-20 13:47:13 -06:00
|
|
|
const runId = readString(payload.runId);
|
|
|
|
|
const agentId = readString(payload.agentId);
|
|
|
|
|
const status = readString(payload.status);
|
2026-03-28 19:33:52 -05:00
|
|
|
if (!runId || !agentId || !status || !RUN_TOAST_STATUSES.has(status)) return null;
|
2026-02-20 13:47:13 -06:00
|
|
|
|
|
|
|
|
const error = readString(payload.error);
|
2026-02-20 15:52:53 -06:00
|
|
|
const triggerDetail = readString(payload.triggerDetail);
|
2026-02-20 14:03:43 -06:00
|
|
|
const name = nameOf(agentId) ?? `Agent ${shortId(agentId)}`;
|
2026-02-20 13:47:13 -06:00
|
|
|
const tone = status === "succeeded" ? "success" : status === "cancelled" ? "warn" : "error";
|
2026-02-20 15:52:53 -06:00
|
|
|
const statusLabel =
|
|
|
|
|
status === "succeeded" ? "succeeded"
|
|
|
|
|
: status === "failed" ? "failed"
|
|
|
|
|
: status === "timed_out" ? "timed out"
|
|
|
|
|
: "cancelled";
|
|
|
|
|
const title = `${name} run ${statusLabel}`;
|
|
|
|
|
|
|
|
|
|
let body: string | undefined;
|
|
|
|
|
if (error) {
|
|
|
|
|
body = truncate(error, 100);
|
|
|
|
|
} else if (triggerDetail) {
|
|
|
|
|
body = `Trigger: ${triggerDetail}`;
|
|
|
|
|
}
|
2026-02-20 13:47:13 -06:00
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
title,
|
2026-02-20 15:52:53 -06:00
|
|
|
body,
|
2026-02-20 13:47:13 -06:00
|
|
|
tone,
|
|
|
|
|
ttlMs: status === "succeeded" ? 5000 : 7000,
|
|
|
|
|
action: { label: "View run", href: `/agents/${agentId}/runs/${runId}` },
|
|
|
|
|
dedupeKey: `run-status:${runId}:${status}`,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-17 12:24:48 -06:00
|
|
|
function invalidateHeartbeatQueries(
|
|
|
|
|
queryClient: ReturnType<typeof useQueryClient>,
|
|
|
|
|
companyId: string,
|
|
|
|
|
payload: Record<string, unknown>,
|
|
|
|
|
) {
|
2026-02-20 12:50:34 -06:00
|
|
|
queryClient.invalidateQueries({ queryKey: queryKeys.liveRuns(companyId) });
|
2026-02-17 12:24:48 -06:00
|
|
|
queryClient.invalidateQueries({ queryKey: queryKeys.heartbeats(companyId) });
|
|
|
|
|
queryClient.invalidateQueries({ queryKey: queryKeys.agents.list(companyId) });
|
|
|
|
|
queryClient.invalidateQueries({ queryKey: queryKeys.dashboard(companyId) });
|
|
|
|
|
queryClient.invalidateQueries({ queryKey: queryKeys.costs(companyId) });
|
2026-02-20 10:32:32 -06:00
|
|
|
queryClient.invalidateQueries({ queryKey: queryKeys.sidebarBadges(companyId) });
|
2026-02-17 12:24:48 -06:00
|
|
|
|
|
|
|
|
const agentId = readString(payload.agentId);
|
|
|
|
|
if (agentId) {
|
|
|
|
|
queryClient.invalidateQueries({ queryKey: queryKeys.agents.detail(agentId) });
|
|
|
|
|
queryClient.invalidateQueries({ queryKey: queryKeys.heartbeats(companyId, agentId) });
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function invalidateActivityQueries(
|
|
|
|
|
queryClient: ReturnType<typeof useQueryClient>,
|
|
|
|
|
companyId: string,
|
|
|
|
|
payload: Record<string, unknown>,
|
2026-04-09 10:26:17 -05:00
|
|
|
currentActor: { userId: string | null; agentId: string | null },
|
[codex] Improve issue detail and issue-list UX (#3678)
## Thinking Path
> - Paperclip orchestrates AI agents for zero-human companies
> - A core part of that is the operator experience around reading issue
state, agent chat, and sub-task structure
> - The current branch had a long run of issue-detail and issue-list UX
fixes that all improve how humans follow and steer active work
> - Those changes mostly live in the UI/chat surface and should be
reviewed together instead of mixed with workspace/runtime work
> - This pull request packages the issue-detail, chat, markdown, and
sub-issue list improvements into one standalone change
> - The benefit is a cleaner, less jumpy, more reliable issue workflow
on desktop and mobile without coupling it to unrelated server/runtime
refactors
## What Changed
- Stabilized issue chat runtime wiring, optimistic comment handling,
queued-comment cancellation, and composer anchoring during live updates
- Fixed several issue-detail rendering and navigation regressions
including placeholder bleed, local polling scope, mobile inbox-to-issue
transitions, and visible refresh resets
- Improved markdown and rich-content handling with advisory image
normalization, editor fallback behavior, touch mention recovery, and
`issue:` quicklook links
- Refined sub-issue behavior with parent-derived defaults, current-user
inheritance fixes, empty-state cleanup, and a reusable issue-list
presentation for sub-issues
- Added targeted UI tests for the new issue-detail, chat scroll/message,
placeholder-data, markdown, and issue-list behaviors
## Verification
- `pnpm vitest run ui/src/components/IssueChatThread.test.tsx
ui/src/components/MarkdownEditor.test.tsx
ui/src/components/IssuesList.test.tsx
ui/src/context/LiveUpdatesProvider.test.tsx
ui/src/lib/issue-chat-messages.test.ts
ui/src/lib/issue-chat-scroll.test.ts
ui/src/lib/issue-detail-subissues.test.ts
ui/src/lib/query-placeholder-data.test.tsx
ui/src/hooks/usePaperclipIssueRuntime.test.tsx`
## Risks
- Medium: this branch touches the highest-traffic issue-detail UI paths,
so regressions would show up as chat/thread or sub-issue UX glitches
- The changes are UI-heavy and would benefit from reviewer screenshots
or a quick manual browser pass before merge
## 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)
- [x] 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
- [ ] 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 12:50:48 -05:00
|
|
|
options?: { pathname?: string; isForegrounded?: boolean },
|
2026-02-17 12:24:48 -06:00
|
|
|
) {
|
|
|
|
|
queryClient.invalidateQueries({ queryKey: queryKeys.activity(companyId) });
|
|
|
|
|
queryClient.invalidateQueries({ queryKey: queryKeys.dashboard(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
|
|
|
queryClient.invalidateQueries({ queryKey: queryKeys.sidebarBadges(companyId) });
|
2026-02-17 12:24:48 -06:00
|
|
|
|
|
|
|
|
const entityType = readString(payload.entityType);
|
|
|
|
|
const entityId = readString(payload.entityId);
|
2026-04-07 18:11:41 -05:00
|
|
|
const action = readString(payload.action);
|
2026-04-09 10:26:17 -05:00
|
|
|
const actorType = readString(payload.actorType);
|
|
|
|
|
const actorId = readString(payload.actorId);
|
2026-02-17 12:24:48 -06:00
|
|
|
|
|
|
|
|
if (entityType === "issue") {
|
|
|
|
|
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) });
|
2026-02-17 12:24:48 -06:00
|
|
|
if (entityId) {
|
2026-02-23 14:41:21 -06:00
|
|
|
const details = readRecord(payload.details);
|
2026-04-09 10:26:17 -05:00
|
|
|
const selfCommentActivity =
|
|
|
|
|
((action === "issue.comment_added") ||
|
|
|
|
|
(action === "issue.updated" && readString(details?.source) === "comment")) &&
|
|
|
|
|
((actorType === "user" && !!currentActor.userId && actorId === currentActor.userId) ||
|
|
|
|
|
(actorType === "agent" && !!currentActor.agentId && actorId === currentActor.agentId));
|
[codex] Improve issue detail and issue-list UX (#3678)
## Thinking Path
> - Paperclip orchestrates AI agents for zero-human companies
> - A core part of that is the operator experience around reading issue
state, agent chat, and sub-task structure
> - The current branch had a long run of issue-detail and issue-list UX
fixes that all improve how humans follow and steer active work
> - Those changes mostly live in the UI/chat surface and should be
reviewed together instead of mixed with workspace/runtime work
> - This pull request packages the issue-detail, chat, markdown, and
sub-issue list improvements into one standalone change
> - The benefit is a cleaner, less jumpy, more reliable issue workflow
on desktop and mobile without coupling it to unrelated server/runtime
refactors
## What Changed
- Stabilized issue chat runtime wiring, optimistic comment handling,
queued-comment cancellation, and composer anchoring during live updates
- Fixed several issue-detail rendering and navigation regressions
including placeholder bleed, local polling scope, mobile inbox-to-issue
transitions, and visible refresh resets
- Improved markdown and rich-content handling with advisory image
normalization, editor fallback behavior, touch mention recovery, and
`issue:` quicklook links
- Refined sub-issue behavior with parent-derived defaults, current-user
inheritance fixes, empty-state cleanup, and a reusable issue-list
presentation for sub-issues
- Added targeted UI tests for the new issue-detail, chat scroll/message,
placeholder-data, markdown, and issue-list behaviors
## Verification
- `pnpm vitest run ui/src/components/IssueChatThread.test.tsx
ui/src/components/MarkdownEditor.test.tsx
ui/src/components/IssuesList.test.tsx
ui/src/context/LiveUpdatesProvider.test.tsx
ui/src/lib/issue-chat-messages.test.ts
ui/src/lib/issue-chat-scroll.test.ts
ui/src/lib/issue-detail-subissues.test.ts
ui/src/lib/query-placeholder-data.test.tsx
ui/src/hooks/usePaperclipIssueRuntime.test.tsx`
## Risks
- Medium: this branch touches the highest-traffic issue-detail UI paths,
so regressions would show up as chat/thread or sub-issue UX glitches
- The changes are UI-heavy and would benefit from reviewer screenshots
or a quick manual browser pass before merge
## 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)
- [x] 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
- [ ] 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 12:50:48 -05:00
|
|
|
const visibleIssueAgentActivity =
|
|
|
|
|
!!options?.pathname &&
|
|
|
|
|
shouldDeferIssueRefetchForVisibleAgentActivity(
|
|
|
|
|
queryClient,
|
|
|
|
|
options.pathname,
|
|
|
|
|
payload,
|
|
|
|
|
{ isForegrounded: options.isForegrounded },
|
|
|
|
|
);
|
|
|
|
|
const visibleIssueCommentActivity =
|
|
|
|
|
!!options?.pathname &&
|
|
|
|
|
shouldDeferVisibleIssueCommentActivity(
|
|
|
|
|
queryClient,
|
|
|
|
|
options.pathname,
|
|
|
|
|
payload,
|
|
|
|
|
{ isForegrounded: options.isForegrounded },
|
|
|
|
|
);
|
2026-02-23 14:41:21 -06:00
|
|
|
const issueRefs = resolveIssueQueryRefs(queryClient, companyId, entityId, details);
|
|
|
|
|
for (const ref of issueRefs) {
|
[codex] Improve issue detail and issue-list UX (#3678)
## Thinking Path
> - Paperclip orchestrates AI agents for zero-human companies
> - A core part of that is the operator experience around reading issue
state, agent chat, and sub-task structure
> - The current branch had a long run of issue-detail and issue-list UX
fixes that all improve how humans follow and steer active work
> - Those changes mostly live in the UI/chat surface and should be
reviewed together instead of mixed with workspace/runtime work
> - This pull request packages the issue-detail, chat, markdown, and
sub-issue list improvements into one standalone change
> - The benefit is a cleaner, less jumpy, more reliable issue workflow
on desktop and mobile without coupling it to unrelated server/runtime
refactors
## What Changed
- Stabilized issue chat runtime wiring, optimistic comment handling,
queued-comment cancellation, and composer anchoring during live updates
- Fixed several issue-detail rendering and navigation regressions
including placeholder bleed, local polling scope, mobile inbox-to-issue
transitions, and visible refresh resets
- Improved markdown and rich-content handling with advisory image
normalization, editor fallback behavior, touch mention recovery, and
`issue:` quicklook links
- Refined sub-issue behavior with parent-derived defaults, current-user
inheritance fixes, empty-state cleanup, and a reusable issue-list
presentation for sub-issues
- Added targeted UI tests for the new issue-detail, chat scroll/message,
placeholder-data, markdown, and issue-list behaviors
## Verification
- `pnpm vitest run ui/src/components/IssueChatThread.test.tsx
ui/src/components/MarkdownEditor.test.tsx
ui/src/components/IssuesList.test.tsx
ui/src/context/LiveUpdatesProvider.test.tsx
ui/src/lib/issue-chat-messages.test.ts
ui/src/lib/issue-chat-scroll.test.ts
ui/src/lib/issue-detail-subissues.test.ts
ui/src/lib/query-placeholder-data.test.tsx
ui/src/hooks/usePaperclipIssueRuntime.test.tsx`
## Risks
- Medium: this branch touches the highest-traffic issue-detail UI paths,
so regressions would show up as chat/thread or sub-issue UX glitches
- The changes are UI-heavy and would benefit from reviewer screenshots
or a quick manual browser pass before merge
## 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)
- [x] 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
- [ ] 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 12:50:48 -05:00
|
|
|
const invalidationOptions =
|
|
|
|
|
(selfCommentActivity || visibleIssueAgentActivity || visibleIssueCommentActivity)
|
|
|
|
|
? { refetchType: "inactive" as const }
|
|
|
|
|
: undefined;
|
2026-04-09 10:26:17 -05:00
|
|
|
queryClient.invalidateQueries({ queryKey: queryKeys.issues.detail(ref), ...invalidationOptions });
|
|
|
|
|
queryClient.invalidateQueries({ queryKey: queryKeys.issues.activity(ref), ...invalidationOptions });
|
2026-04-07 18:11:41 -05:00
|
|
|
if (action === "issue.comment_added") {
|
2026-04-09 10:26:17 -05:00
|
|
|
queryClient.invalidateQueries({ queryKey: queryKeys.issues.comments(ref), ...invalidationOptions });
|
2026-04-07 18:11:41 -05:00
|
|
|
}
|
[codex] Add structured issue-thread interactions (#4244)
## Thinking Path
> - Paperclip orchestrates AI agents for zero-human companies.
> - Operators supervise that work through issues, comments, approvals,
and the board UI.
> - Some agent proposals need structured board/user decisions, not
hidden markdown conventions or heavyweight governed approvals.
> - Issue-thread interactions already provide a natural thread-native
surface for proposed tasks and questions.
> - This pull request extends that surface with request confirmations,
richer interaction cards, and agent/plugin/MCP helpers.
> - The benefit is that plan approvals and yes/no decisions become
explicit, auditable, and resumable without losing the single-issue
workflow.
## What Changed
- Added persisted issue-thread interactions for suggested tasks,
structured questions, and request confirmations.
- Added board UI cards for interaction review, selection, question
answers, and accept/reject confirmation flows.
- Added MCP and plugin SDK helpers for creating interaction cards from
agents/plugins.
- Updated agent wake instructions, onboarding assets, Paperclip skill
docs, and public docs to prefer structured confirmations for
issue-scoped decisions.
- Rebased the branch onto `public-gh/master` and renumbered branch
migrations to `0063` and `0064`; the idempotency migration uses `ADD
COLUMN IF NOT EXISTS` for old branch users.
## Verification
- `git diff --check public-gh/master..HEAD`
- `pnpm exec vitest run packages/adapter-utils/src/server-utils.test.ts
packages/mcp-server/src/tools.test.ts
packages/shared/src/issue-thread-interactions.test.ts
ui/src/lib/issue-thread-interactions.test.ts
ui/src/lib/issue-chat-messages.test.ts
ui/src/components/IssueThreadInteractionCard.test.tsx
ui/src/components/IssueChatThread.test.tsx
server/src/__tests__/issue-thread-interaction-routes.test.ts
server/src/__tests__/issue-thread-interactions-service.test.ts
server/src/services/issue-thread-interactions.test.ts` -> 9 files / 79
tests passed
- `pnpm -r typecheck` -> passed, including `packages/db` migration
numbering check
## Risks
- Medium: this adds a new issue-thread interaction model across
db/shared/server/ui/plugin surfaces.
- Migration risk is reduced by placing this branch after current master
migrations (`0063`, `0064`) and making the idempotency column add
idempotent for users who applied the old branch numbering.
- UI interaction behavior is covered by component tests, but this PR
does not include browser screenshots.
> 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-class coding agent runtime. Exact model ID and
context window are not exposed in this Paperclip run; tool use and local
shell/code execution were 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)
- [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
- [ ] 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-21 20:15:11 -05:00
|
|
|
if (action?.startsWith("issue.thread_interaction_")) {
|
|
|
|
|
queryClient.invalidateQueries({ queryKey: queryKeys.issues.interactions(ref), ...invalidationOptions });
|
|
|
|
|
}
|
2026-02-23 14:41:21 -06:00
|
|
|
}
|
2026-02-17 12:24:48 -06:00
|
|
|
}
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (entityType === "agent") {
|
|
|
|
|
queryClient.invalidateQueries({ queryKey: queryKeys.agents.list(companyId) });
|
|
|
|
|
queryClient.invalidateQueries({ queryKey: queryKeys.org(companyId) });
|
|
|
|
|
if (entityId) {
|
|
|
|
|
queryClient.invalidateQueries({ queryKey: queryKeys.agents.detail(entityId) });
|
|
|
|
|
queryClient.invalidateQueries({ queryKey: queryKeys.heartbeats(companyId, entityId) });
|
|
|
|
|
}
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (entityType === "project") {
|
|
|
|
|
queryClient.invalidateQueries({ queryKey: queryKeys.projects.list(companyId) });
|
|
|
|
|
if (entityId) queryClient.invalidateQueries({ queryKey: queryKeys.projects.detail(entityId) });
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (entityType === "goal") {
|
|
|
|
|
queryClient.invalidateQueries({ queryKey: queryKeys.goals.list(companyId) });
|
|
|
|
|
if (entityId) queryClient.invalidateQueries({ queryKey: queryKeys.goals.detail(entityId) });
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (entityType === "approval") {
|
|
|
|
|
queryClient.invalidateQueries({ queryKey: queryKeys.approvals.list(companyId) });
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-06 12:13:25 -06:00
|
|
|
if (entityType === "join_request") {
|
|
|
|
|
queryClient.invalidateQueries({ queryKey: queryKeys.access.joinRequests(companyId) });
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-17 12:24:48 -06:00
|
|
|
if (entityType === "cost_event") {
|
|
|
|
|
queryClient.invalidateQueries({ queryKey: queryKeys.costs(companyId) });
|
feat(ui): add resource and usage dashboard (/usage route)
adds a new /usage page that lets board operators see how much each ai
provider is consuming across any date window, with per-model breakdowns,
rolling 5h/24h/7d burn windows, weekly budget bars, and a deficit notch
when projected spend is on track to exceed the monthly budget.
- new GET /companies/:id/costs/by-provider endpoint aggregates cost events
by provider + model with pro-rated billing type splits from heartbeat runs
- new GET /companies/:id/costs/window-spend endpoint returns rolling window
spend (5h, 24h, 7d) per provider with no schema changes
- QuotaBar: reusable boxed-border progress bar with green/yellow/red
threshold fill colors and optional deficit notch
- ProviderQuotaCard: per-provider card showing budget allocation bars,
rolling windows, subscription usage, and model breakdown with token/cost
share overlays
- Usage page: date preset toggles (mtd, 7d, 30d, ytd, all, custom),
provider tabs, 30s polling plus ws invalidation on cost_event
- custom date range blocks queries until both dates are selected and
treats boundaries as local-time (not utc midnight) so full days are
included regardless of timezone
- query key to timestamp is floored to the nearest minute to prevent
cache churn on every 30s refetch tick
2026-03-08 03:18:37 +05:30
|
|
|
queryClient.invalidateQueries({ queryKey: queryKeys.usageByProvider(companyId) });
|
|
|
|
|
queryClient.invalidateQueries({ queryKey: queryKeys.usageWindowSpend(companyId) });
|
2026-03-08 19:04:27 +05:30
|
|
|
// usageQuotaWindows is intentionally excluded: quota windows come from external provider
|
|
|
|
|
// apis on a 5-minute poll and do not change in response to cost events logged by agents
|
2026-02-17 12:24:48 -06:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-19 16:45:08 -05:00
|
|
|
if (entityType === "routine" || entityType === "routine_trigger" || entityType === "routine_run") {
|
|
|
|
|
queryClient.invalidateQueries({ queryKey: ["routines"] });
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-17 12:24:48 -06:00
|
|
|
if (entityType === "company") {
|
|
|
|
|
queryClient.invalidateQueries({ queryKey: queryKeys.companies.all });
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-20 13:56:34 -06:00
|
|
|
interface ToastGate {
|
|
|
|
|
cooldownHits: Map<string, number[]>;
|
|
|
|
|
suppressUntil: number;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function shouldSuppressToast(gate: ToastGate, category: string): boolean {
|
|
|
|
|
const now = Date.now();
|
|
|
|
|
if (now < gate.suppressUntil) return true;
|
|
|
|
|
|
|
|
|
|
const hits = gate.cooldownHits.get(category);
|
|
|
|
|
if (!hits) return false;
|
|
|
|
|
|
|
|
|
|
const recent = hits.filter((t) => now - t < TOAST_COOLDOWN_WINDOW_MS);
|
|
|
|
|
gate.cooldownHits.set(category, recent);
|
|
|
|
|
return recent.length >= TOAST_COOLDOWN_MAX;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function recordToastHit(gate: ToastGate, category: string) {
|
|
|
|
|
const now = Date.now();
|
|
|
|
|
const hits = gate.cooldownHits.get(category) ?? [];
|
|
|
|
|
hits.push(now);
|
|
|
|
|
gate.cooldownHits.set(category, hits);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function gatedPushToast(
|
|
|
|
|
gate: ToastGate,
|
|
|
|
|
pushToast: (toast: ToastInput) => string | null,
|
|
|
|
|
category: string,
|
|
|
|
|
toast: ToastInput,
|
|
|
|
|
) {
|
|
|
|
|
if (shouldSuppressToast(gate, category)) return;
|
|
|
|
|
const id = pushToast(toast);
|
|
|
|
|
if (id !== null) recordToastHit(gate, category);
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-17 12:24:48 -06:00
|
|
|
function handleLiveEvent(
|
2026-02-20 14:03:43 -06:00
|
|
|
queryClient: QueryClient,
|
2026-02-17 12:24:48 -06:00
|
|
|
expectedCompanyId: string,
|
2026-03-22 07:16:13 -05:00
|
|
|
pathname: string,
|
2026-02-17 12:24:48 -06:00
|
|
|
event: LiveEvent,
|
2026-02-20 13:47:13 -06:00
|
|
|
pushToast: (toast: ToastInput) => string | null,
|
2026-02-20 13:56:34 -06:00
|
|
|
gate: ToastGate,
|
2026-03-07 08:31:59 -06:00
|
|
|
currentActor: { userId: string | null; agentId: string | null },
|
2026-02-17 12:24:48 -06:00
|
|
|
) {
|
|
|
|
|
if (event.companyId !== expectedCompanyId) return;
|
|
|
|
|
|
2026-02-20 14:03:43 -06:00
|
|
|
const nameOf = (id: string) => resolveAgentName(queryClient, expectedCompanyId, id);
|
2026-02-17 12:24:48 -06:00
|
|
|
const payload = event.payload ?? {};
|
|
|
|
|
if (event.type === "heartbeat.run.log") {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-20 12:50:34 -06:00
|
|
|
if (event.type === "heartbeat.run.queued" || event.type === "heartbeat.run.status") {
|
2026-02-17 12:24:48 -06:00
|
|
|
invalidateHeartbeatQueries(queryClient, expectedCompanyId, payload);
|
[codex] improve issue and routine UI responsiveness (#3744)
## Thinking Path
> - Paperclip orchestrates AI agents for zero-human companies
> - Operators rely on issue, inbox, and routine views to understand what
the company is doing in real time
> - Those views need to stay fast and readable even when issue lists,
markdown comments, and run metadata get large
> - The current branch had a coherent set of UI and live-update
improvements spread across issue search, issue detail rendering, routine
affordances, and workspace lookups
> - This pull request groups those board-facing changes into one
standalone branch that can merge independently of the heartbeat/runtime
work
> - The benefit is a faster, clearer issue and routine workflow without
changing the underlying task model
## What Changed
- Show routine execution issues by default and rename the filter to
`Hide routine runs` so the default state no longer looks like an active
filter.
- Show the routine name in the run dialog and tighten the issue
properties pane with a workspace link, copy-on-click behavior, and an
inline parent arrow.
- Reduce issue detail rerenders, keep queued issue chat mounted, improve
issues page search responsiveness, and speed up issues first paint.
- Add inbox "other search results", refresh visible issue runs after
status updates, and optimize workspace lookups through summary-mode
execution workspace queries.
- Improve markdown wrapping and scrolling behavior for long strings and
self-comment code blocks.
- Relax the markdown sanitizer assertion so the test still validates
safety after the new wrap-friendly inline styles.
## Verification
- `pnpm vitest run ui/src/components/IssuesList.test.tsx
ui/src/lib/inbox.test.ts ui/src/pages/Issues.test.tsx
ui/src/context/BreadcrumbContext.test.tsx
ui/src/context/LiveUpdatesProvider.test.ts
ui/src/components/MarkdownBody.test.tsx
ui/src/api/execution-workspaces.test.ts
server/src/__tests__/execution-workspaces-routes.test.ts`
## Risks
- This touches several issue-facing UI surfaces at once, so regressions
would most likely show up as stale rendering, search result mismatches,
or small markdown presentation differences.
- The workspace lookup optimization depends on the summary-mode route
shape staying aligned between server and UI.
## Model Used
- OpenAI Codex, GPT-5-based coding agent in the Codex CLI environment.
Exact backend model deployment ID was not exposed in-session.
Tool-assisted editing and shell execution were used.
## 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
- [ ] 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-15 15:54:05 -05:00
|
|
|
invalidateVisibleIssueRunQueries(queryClient, pathname, payload);
|
2026-02-20 13:47:13 -06:00
|
|
|
if (event.type === "heartbeat.run.status") {
|
2026-02-20 14:03:43 -06:00
|
|
|
const toast = buildRunStatusToast(payload, nameOf);
|
2026-03-22 07:16:13 -05:00
|
|
|
if (
|
|
|
|
|
toast &&
|
|
|
|
|
!shouldSuppressRunStatusToastForVisibleIssue(queryClient, pathname, payload)
|
|
|
|
|
) {
|
|
|
|
|
gatedPushToast(gate, pushToast, "run-status", toast);
|
|
|
|
|
}
|
2026-02-20 13:47:13 -06:00
|
|
|
}
|
2026-02-17 12:24:48 -06:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-20 12:50:34 -06:00
|
|
|
if (event.type === "heartbeat.run.event") {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-17 12:24:48 -06:00
|
|
|
if (event.type === "agent.status") {
|
|
|
|
|
queryClient.invalidateQueries({ queryKey: queryKeys.agents.list(expectedCompanyId) });
|
|
|
|
|
queryClient.invalidateQueries({ queryKey: queryKeys.dashboard(expectedCompanyId) });
|
|
|
|
|
queryClient.invalidateQueries({ queryKey: queryKeys.org(expectedCompanyId) });
|
|
|
|
|
const agentId = readString(payload.agentId);
|
|
|
|
|
if (agentId) queryClient.invalidateQueries({ queryKey: queryKeys.agents.detail(agentId) });
|
2026-02-20 15:52:53 -06:00
|
|
|
const toast = buildAgentStatusToast(payload, nameOf, queryClient, expectedCompanyId);
|
2026-03-22 07:16:13 -05:00
|
|
|
if (
|
|
|
|
|
toast &&
|
|
|
|
|
!shouldSuppressAgentStatusToastForVisibleIssue(queryClient, pathname, payload)
|
|
|
|
|
) {
|
|
|
|
|
gatedPushToast(gate, pushToast, "agent-status", toast);
|
|
|
|
|
}
|
2026-02-17 12:24:48 -06:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (event.type === "activity.logged") {
|
[codex] Improve issue detail and issue-list UX (#3678)
## Thinking Path
> - Paperclip orchestrates AI agents for zero-human companies
> - A core part of that is the operator experience around reading issue
state, agent chat, and sub-task structure
> - The current branch had a long run of issue-detail and issue-list UX
fixes that all improve how humans follow and steer active work
> - Those changes mostly live in the UI/chat surface and should be
reviewed together instead of mixed with workspace/runtime work
> - This pull request packages the issue-detail, chat, markdown, and
sub-issue list improvements into one standalone change
> - The benefit is a cleaner, less jumpy, more reliable issue workflow
on desktop and mobile without coupling it to unrelated server/runtime
refactors
## What Changed
- Stabilized issue chat runtime wiring, optimistic comment handling,
queued-comment cancellation, and composer anchoring during live updates
- Fixed several issue-detail rendering and navigation regressions
including placeholder bleed, local polling scope, mobile inbox-to-issue
transitions, and visible refresh resets
- Improved markdown and rich-content handling with advisory image
normalization, editor fallback behavior, touch mention recovery, and
`issue:` quicklook links
- Refined sub-issue behavior with parent-derived defaults, current-user
inheritance fixes, empty-state cleanup, and a reusable issue-list
presentation for sub-issues
- Added targeted UI tests for the new issue-detail, chat scroll/message,
placeholder-data, markdown, and issue-list behaviors
## Verification
- `pnpm vitest run ui/src/components/IssueChatThread.test.tsx
ui/src/components/MarkdownEditor.test.tsx
ui/src/components/IssuesList.test.tsx
ui/src/context/LiveUpdatesProvider.test.tsx
ui/src/lib/issue-chat-messages.test.ts
ui/src/lib/issue-chat-scroll.test.ts
ui/src/lib/issue-detail-subissues.test.ts
ui/src/lib/query-placeholder-data.test.tsx
ui/src/hooks/usePaperclipIssueRuntime.test.tsx`
## Risks
- Medium: this branch touches the highest-traffic issue-detail UI paths,
so regressions would show up as chat/thread or sub-issue UX glitches
- The changes are UI-heavy and would benefit from reviewer screenshots
or a quick manual browser pass before merge
## 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)
- [x] 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
- [ ] 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 12:50:48 -05:00
|
|
|
invalidateActivityQueries(queryClient, expectedCompanyId, payload, currentActor, { pathname });
|
|
|
|
|
if (shouldDeferVisibleIssueCommentActivity(queryClient, pathname, payload)) {
|
|
|
|
|
void hydrateVisibleIssueComment(queryClient, pathname, payload);
|
|
|
|
|
}
|
2026-02-20 13:56:34 -06:00
|
|
|
const action = readString(payload.action);
|
2026-03-06 14:28:25 -06:00
|
|
|
const toast =
|
2026-03-07 08:31:59 -06:00
|
|
|
buildActivityToast(queryClient, expectedCompanyId, payload, currentActor) ??
|
2026-03-06 14:28:25 -06:00
|
|
|
buildJoinRequestToast(payload);
|
2026-03-22 07:16:13 -05:00
|
|
|
if (
|
|
|
|
|
toast &&
|
|
|
|
|
!shouldSuppressActivityToastForVisibleIssue(queryClient, pathname, payload)
|
|
|
|
|
) {
|
|
|
|
|
gatedPushToast(gate, pushToast, `activity:${action ?? "unknown"}`, toast);
|
|
|
|
|
}
|
2026-02-17 12:24:48 -06:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-30 07:17:23 -05:00
|
|
|
function resolveLiveCompanyId(
|
|
|
|
|
selectedCompanyId: string | null,
|
|
|
|
|
selectedCompanyLiveId: string | null,
|
|
|
|
|
): string | null {
|
|
|
|
|
return selectedCompanyId && selectedCompanyId === selectedCompanyLiveId
|
|
|
|
|
? selectedCompanyId
|
|
|
|
|
: null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function resetSocketHandlers(target: LiveUpdatesSocketLike) {
|
|
|
|
|
target.onopen = null;
|
|
|
|
|
target.onmessage = null;
|
|
|
|
|
target.onerror = null;
|
|
|
|
|
target.onclose = null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function closeSocketQuietly(target: LiveUpdatesSocketLike | null, reason: string) {
|
|
|
|
|
if (!target) return;
|
|
|
|
|
|
|
|
|
|
if (target.readyState === SOCKET_CONNECTING) {
|
|
|
|
|
// Let the handshake complete and then close. Calling close() while the
|
|
|
|
|
// socket is still CONNECTING is what triggers the noisy browser error.
|
|
|
|
|
target.onopen = () => {
|
|
|
|
|
resetSocketHandlers(target);
|
|
|
|
|
target.close(1000, reason);
|
|
|
|
|
};
|
|
|
|
|
target.onmessage = null;
|
|
|
|
|
target.onerror = () => undefined;
|
|
|
|
|
target.onclose = null;
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
resetSocketHandlers(target);
|
|
|
|
|
|
|
|
|
|
if (target.readyState === SOCKET_OPEN) {
|
|
|
|
|
target.close(1000, reason);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-17 16:14:43 -05:00
|
|
|
export const __liveUpdatesTestUtils = {
|
2026-03-28 19:33:52 -05:00
|
|
|
buildAgentStatusToast,
|
|
|
|
|
buildRunStatusToast,
|
2026-03-30 07:17:23 -05:00
|
|
|
closeSocketQuietly,
|
[codex] Improve issue detail and issue-list UX (#3678)
## Thinking Path
> - Paperclip orchestrates AI agents for zero-human companies
> - A core part of that is the operator experience around reading issue
state, agent chat, and sub-task structure
> - The current branch had a long run of issue-detail and issue-list UX
fixes that all improve how humans follow and steer active work
> - Those changes mostly live in the UI/chat surface and should be
reviewed together instead of mixed with workspace/runtime work
> - This pull request packages the issue-detail, chat, markdown, and
sub-issue list improvements into one standalone change
> - The benefit is a cleaner, less jumpy, more reliable issue workflow
on desktop and mobile without coupling it to unrelated server/runtime
refactors
## What Changed
- Stabilized issue chat runtime wiring, optimistic comment handling,
queued-comment cancellation, and composer anchoring during live updates
- Fixed several issue-detail rendering and navigation regressions
including placeholder bleed, local polling scope, mobile inbox-to-issue
transitions, and visible refresh resets
- Improved markdown and rich-content handling with advisory image
normalization, editor fallback behavior, touch mention recovery, and
`issue:` quicklook links
- Refined sub-issue behavior with parent-derived defaults, current-user
inheritance fixes, empty-state cleanup, and a reusable issue-list
presentation for sub-issues
- Added targeted UI tests for the new issue-detail, chat scroll/message,
placeholder-data, markdown, and issue-list behaviors
## Verification
- `pnpm vitest run ui/src/components/IssueChatThread.test.tsx
ui/src/components/MarkdownEditor.test.tsx
ui/src/components/IssuesList.test.tsx
ui/src/context/LiveUpdatesProvider.test.tsx
ui/src/lib/issue-chat-messages.test.ts
ui/src/lib/issue-chat-scroll.test.ts
ui/src/lib/issue-detail-subissues.test.ts
ui/src/lib/query-placeholder-data.test.tsx
ui/src/hooks/usePaperclipIssueRuntime.test.tsx`
## Risks
- Medium: this branch touches the highest-traffic issue-detail UI paths,
so regressions would show up as chat/thread or sub-issue UX glitches
- The changes are UI-heavy and would benefit from reviewer screenshots
or a quick manual browser pass before merge
## 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)
- [x] 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
- [ ] 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 12:50:48 -05:00
|
|
|
hydrateVisibleIssueComment,
|
2026-03-17 16:14:43 -05:00
|
|
|
invalidateActivityQueries,
|
[codex] improve issue and routine UI responsiveness (#3744)
## Thinking Path
> - Paperclip orchestrates AI agents for zero-human companies
> - Operators rely on issue, inbox, and routine views to understand what
the company is doing in real time
> - Those views need to stay fast and readable even when issue lists,
markdown comments, and run metadata get large
> - The current branch had a coherent set of UI and live-update
improvements spread across issue search, issue detail rendering, routine
affordances, and workspace lookups
> - This pull request groups those board-facing changes into one
standalone branch that can merge independently of the heartbeat/runtime
work
> - The benefit is a faster, clearer issue and routine workflow without
changing the underlying task model
## What Changed
- Show routine execution issues by default and rename the filter to
`Hide routine runs` so the default state no longer looks like an active
filter.
- Show the routine name in the run dialog and tighten the issue
properties pane with a workspace link, copy-on-click behavior, and an
inline parent arrow.
- Reduce issue detail rerenders, keep queued issue chat mounted, improve
issues page search responsiveness, and speed up issues first paint.
- Add inbox "other search results", refresh visible issue runs after
status updates, and optimize workspace lookups through summary-mode
execution workspace queries.
- Improve markdown wrapping and scrolling behavior for long strings and
self-comment code blocks.
- Relax the markdown sanitizer assertion so the test still validates
safety after the new wrap-friendly inline styles.
## Verification
- `pnpm vitest run ui/src/components/IssuesList.test.tsx
ui/src/lib/inbox.test.ts ui/src/pages/Issues.test.tsx
ui/src/context/BreadcrumbContext.test.tsx
ui/src/context/LiveUpdatesProvider.test.ts
ui/src/components/MarkdownBody.test.tsx
ui/src/api/execution-workspaces.test.ts
server/src/__tests__/execution-workspaces-routes.test.ts`
## Risks
- This touches several issue-facing UI surfaces at once, so regressions
would most likely show up as stale rendering, search result mismatches,
or small markdown presentation differences.
- The workspace lookup optimization depends on the summary-mode route
shape staying aligned between server and UI.
## Model Used
- OpenAI Codex, GPT-5-based coding agent in the Codex CLI environment.
Exact backend model deployment ID was not exposed in-session.
Tool-assisted editing and shell execution were used.
## 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
- [ ] 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-15 15:54:05 -05:00
|
|
|
invalidateVisibleIssueRunQueries,
|
2026-03-30 07:17:23 -05:00
|
|
|
resolveLiveCompanyId,
|
[codex] Improve issue detail and issue-list UX (#3678)
## Thinking Path
> - Paperclip orchestrates AI agents for zero-human companies
> - A core part of that is the operator experience around reading issue
state, agent chat, and sub-task structure
> - The current branch had a long run of issue-detail and issue-list UX
fixes that all improve how humans follow and steer active work
> - Those changes mostly live in the UI/chat surface and should be
reviewed together instead of mixed with workspace/runtime work
> - This pull request packages the issue-detail, chat, markdown, and
sub-issue list improvements into one standalone change
> - The benefit is a cleaner, less jumpy, more reliable issue workflow
on desktop and mobile without coupling it to unrelated server/runtime
refactors
## What Changed
- Stabilized issue chat runtime wiring, optimistic comment handling,
queued-comment cancellation, and composer anchoring during live updates
- Fixed several issue-detail rendering and navigation regressions
including placeholder bleed, local polling scope, mobile inbox-to-issue
transitions, and visible refresh resets
- Improved markdown and rich-content handling with advisory image
normalization, editor fallback behavior, touch mention recovery, and
`issue:` quicklook links
- Refined sub-issue behavior with parent-derived defaults, current-user
inheritance fixes, empty-state cleanup, and a reusable issue-list
presentation for sub-issues
- Added targeted UI tests for the new issue-detail, chat scroll/message,
placeholder-data, markdown, and issue-list behaviors
## Verification
- `pnpm vitest run ui/src/components/IssueChatThread.test.tsx
ui/src/components/MarkdownEditor.test.tsx
ui/src/components/IssuesList.test.tsx
ui/src/context/LiveUpdatesProvider.test.tsx
ui/src/lib/issue-chat-messages.test.ts
ui/src/lib/issue-chat-scroll.test.ts
ui/src/lib/issue-detail-subissues.test.ts
ui/src/lib/query-placeholder-data.test.tsx
ui/src/hooks/usePaperclipIssueRuntime.test.tsx`
## Risks
- Medium: this branch touches the highest-traffic issue-detail UI paths,
so regressions would show up as chat/thread or sub-issue UX glitches
- The changes are UI-heavy and would benefit from reviewer screenshots
or a quick manual browser pass before merge
## 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)
- [x] 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
- [ ] 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 12:50:48 -05:00
|
|
|
shouldDeferIssueRefetchForVisibleAgentActivity,
|
|
|
|
|
shouldDeferVisibleIssueCommentActivity,
|
2026-03-22 07:16:13 -05:00
|
|
|
shouldSuppressActivityToastForVisibleIssue,
|
|
|
|
|
shouldSuppressRunStatusToastForVisibleIssue,
|
|
|
|
|
shouldSuppressAgentStatusToastForVisibleIssue,
|
2026-03-17 16:14:43 -05:00
|
|
|
};
|
|
|
|
|
|
2026-02-17 12:24:48 -06:00
|
|
|
export function LiveUpdatesProvider({ children }: { children: ReactNode }) {
|
2026-03-15 20:13:09 -03:00
|
|
|
const { 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();
|
2026-03-22 07:16:13 -05:00
|
|
|
const location = useLocation();
|
2026-02-20 13:56:34 -06:00
|
|
|
const gateRef = useRef<ToastGate>({ cooldownHits: new Map(), suppressUntil: 0 });
|
2026-03-23 17:16:10 -05:00
|
|
|
const pathnameRef = useRef(location.pathname);
|
2026-03-15 20:13:09 -03:00
|
|
|
const { data: session, status: sessionStatus } = useQuery({
|
2026-03-07 08:31:59 -06:00
|
|
|
queryKey: queryKeys.auth.session,
|
|
|
|
|
queryFn: () => authApi.getSession(),
|
|
|
|
|
retry: false,
|
|
|
|
|
});
|
|
|
|
|
const currentUserId = session?.user?.id ?? session?.session?.userId ?? null;
|
2026-03-15 20:13:09 -03:00
|
|
|
const socketAuthKey = session?.session?.id ?? currentUserId ?? "signed_out";
|
2026-03-30 07:17:23 -05:00
|
|
|
const liveCompanyId = resolveLiveCompanyId(selectedCompanyId, selectedCompany?.id ?? null);
|
2026-03-15 20:13:09 -03:00
|
|
|
const canConnectSocket = sessionStatus === "success" && session !== null && liveCompanyId !== null;
|
|
|
|
|
const currentActorRef = useRef<{ userId: string | null; agentId: string | null }>({
|
|
|
|
|
userId: currentUserId,
|
|
|
|
|
agentId: null,
|
|
|
|
|
});
|
2026-02-17 12:24:48 -06:00
|
|
|
|
2026-03-23 17:16:10 -05:00
|
|
|
useEffect(() => {
|
|
|
|
|
pathnameRef.current = location.pathname;
|
|
|
|
|
}, [location.pathname]);
|
|
|
|
|
|
2026-02-17 12:24:48 -06:00
|
|
|
useEffect(() => {
|
2026-03-15 20:13:09 -03:00
|
|
|
currentActorRef.current = {
|
|
|
|
|
userId: currentUserId,
|
|
|
|
|
agentId: null,
|
|
|
|
|
};
|
|
|
|
|
}, [currentUserId]);
|
2026-02-17 12:24:48 -06:00
|
|
|
|
|
|
|
|
useEffect(() => {
|
2026-03-15 20:13:09 -03:00
|
|
|
if (!canConnectSocket || !liveCompanyId) return;
|
2026-02-17 12:24:48 -06:00
|
|
|
|
|
|
|
|
let closed = false;
|
|
|
|
|
let reconnectAttempt = 0;
|
|
|
|
|
let reconnectTimer: number | null = null;
|
|
|
|
|
let socket: WebSocket | null = null;
|
|
|
|
|
|
|
|
|
|
const clearReconnect = () => {
|
|
|
|
|
if (reconnectTimer !== null) {
|
|
|
|
|
window.clearTimeout(reconnectTimer);
|
|
|
|
|
reconnectTimer = null;
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const scheduleReconnect = () => {
|
|
|
|
|
if (closed) return;
|
|
|
|
|
reconnectAttempt += 1;
|
|
|
|
|
const delayMs = Math.min(15000, 1000 * 2 ** Math.min(reconnectAttempt - 1, 4));
|
|
|
|
|
reconnectTimer = window.setTimeout(() => {
|
|
|
|
|
reconnectTimer = null;
|
|
|
|
|
connect();
|
|
|
|
|
}, delayMs);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const connect = () => {
|
|
|
|
|
if (closed) return;
|
|
|
|
|
const protocol = window.location.protocol === "https:" ? "wss" : "ws";
|
2026-03-15 20:13:09 -03:00
|
|
|
const url = `${protocol}://${window.location.host}/api/companies/${encodeURIComponent(liveCompanyId)}/events/ws`;
|
|
|
|
|
const nextSocket = new WebSocket(url);
|
|
|
|
|
socket = nextSocket;
|
|
|
|
|
|
|
|
|
|
nextSocket.onopen = () => {
|
|
|
|
|
if (closed || socket !== nextSocket) {
|
|
|
|
|
closeSocketQuietly(nextSocket, "stale_connection");
|
|
|
|
|
return;
|
|
|
|
|
}
|
2026-02-20 13:56:34 -06:00
|
|
|
if (reconnectAttempt > 0) {
|
|
|
|
|
gateRef.current.suppressUntil = Date.now() + RECONNECT_SUPPRESS_MS;
|
|
|
|
|
}
|
2026-02-17 12:24:48 -06:00
|
|
|
reconnectAttempt = 0;
|
|
|
|
|
};
|
|
|
|
|
|
2026-03-15 20:13:09 -03:00
|
|
|
nextSocket.onmessage = (message) => {
|
2026-02-17 12:24:48 -06:00
|
|
|
const raw = typeof message.data === "string" ? message.data : "";
|
|
|
|
|
if (!raw) return;
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const parsed = JSON.parse(raw) as LiveEvent;
|
2026-03-30 07:17:23 -05:00
|
|
|
handleLiveEvent(queryClient, liveCompanyId, pathnameRef.current, parsed, pushToast, gateRef.current, {
|
2026-03-15 20:13:09 -03:00
|
|
|
userId: currentActorRef.current.userId,
|
|
|
|
|
agentId: currentActorRef.current.agentId,
|
2026-03-07 08:31:59 -06:00
|
|
|
});
|
2026-02-17 12:24:48 -06:00
|
|
|
} catch {
|
|
|
|
|
// Ignore non-JSON payloads.
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2026-03-15 20:13:09 -03:00
|
|
|
nextSocket.onerror = () => {
|
|
|
|
|
// Wait for onclose to drive the reconnect. Self-closing here is what
|
|
|
|
|
// produces the "closed before connection established" browser noise.
|
2026-02-17 12:24:48 -06:00
|
|
|
};
|
|
|
|
|
|
2026-03-15 20:13:09 -03:00
|
|
|
nextSocket.onclose = () => {
|
|
|
|
|
if (socket !== nextSocket) return;
|
|
|
|
|
socket = null;
|
2026-02-17 12:24:48 -06:00
|
|
|
if (closed) return;
|
|
|
|
|
scheduleReconnect();
|
|
|
|
|
};
|
|
|
|
|
};
|
|
|
|
|
|
2026-03-15 20:13:09 -03:00
|
|
|
// Delay initial connect slightly so React StrictMode's double-invoke
|
|
|
|
|
// cleanup fires before the WebSocket is created, avoiding the
|
|
|
|
|
// "WebSocket closed before connection established" dev-mode error.
|
|
|
|
|
const connectTimer = window.setTimeout(connect, 0);
|
2026-02-17 12:24:48 -06:00
|
|
|
|
|
|
|
|
return () => {
|
|
|
|
|
closed = true;
|
2026-03-15 20:13:09 -03:00
|
|
|
window.clearTimeout(connectTimer);
|
2026-02-17 12:24:48 -06:00
|
|
|
clearReconnect();
|
2026-03-15 20:13:09 -03:00
|
|
|
const activeSocket = socket;
|
|
|
|
|
socket = null;
|
|
|
|
|
closeSocketQuietly(activeSocket, "provider_unmount");
|
2026-02-17 12:24:48 -06:00
|
|
|
};
|
2026-03-15 20:13:09 -03:00
|
|
|
}, [queryClient, liveCompanyId, pushToast, canConnectSocket, socketAuthKey]);
|
2026-02-17 12:24:48 -06:00
|
|
|
|
|
|
|
|
return <>{children}</>;
|
|
|
|
|
}
|