spaps-issue-reporting-react 0.4.0 → 0.4.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,21 @@
1
1
  # Changelog
2
2
 
3
+ ## [Unreleased]
4
+
5
+ - Added the reporter-visible operator-to-reporter conversation thread (`IssueReportMessageThread`), rendered inside the issue detail modal when the client implements `issueReporting.listMessages`/`submitMessage`. It shows only `active`, reporter-visible projection rows (retracted/superseded/non-visible excluded), distinguishes operator vs reporter authorship and message kind, exposes a `needs_response` affordance plus a floating-entrypoint badge, and includes a clarification-response composer that submits with a client-generated `idempotency_key` and handles `409 ISSUE_REPORT_MESSAGE_CONFLICT`.
6
+
7
+ ## [0.4.1] - 2026-05-16
8
+
9
+ - Added optional screenshot attachment UI that uploads private PNG/JPEG/WebP evidence through `issueReporting.uploadAttachment`, validates size/count locally, and submits only backend attachment IDs.
10
+
11
+ ## [0.4.0] - 2026-05-11
12
+
13
+ - Maintenance: align the React issue-reporting package version after release automation.
14
+
15
+ ## [0.3.0] - 2026-05-11
16
+
17
+ - Maintenance: align the React issue-reporting package version after release automation.
18
+
3
19
  ## [0.2.0] - 2026-05-09
4
20
 
5
21
  - Added voice input support through SPAPS-minted ElevenLabs realtime tokens.
package/README.md CHANGED
@@ -18,6 +18,7 @@ This package targets `Node.js >=18` and React 18+. Voice input uses the package'
18
18
  | --- | --- |
19
19
  | A visible issue-report entry point | Floating button with open/recent state |
20
20
  | A guided reporting flow | Page-first create flow, optional section picking, create/edit/reply modal |
21
+ | Screenshot evidence | Optional picker that uploads private PNG/JPEG/WebP attachments through the host client |
21
22
  | A lightweight integration contract | Any client that exposes `issueReporting.*` methods |
22
23
  | App-level control | Eligibility, reporter identity, scope, page policy, and copy stay in your app |
23
24
 
@@ -71,6 +72,12 @@ export function AppShell() {
71
72
 
72
73
  - A client with `issueReporting.getStatus`, `list`, `get`, `create`, `update`, and `reply`.
73
74
  - A client with `issueReporting.createVoiceToken` when `inputModes` includes `voice`.
75
+ - A client with `issueReporting.uploadAttachment(file, { filename })` when you want screenshot uploads.
76
+ - Optionally, a client with `issueReporting.listMessages(issueReportId)` and
77
+ `issueReporting.submitMessage(issueReportId, { body, idempotency_key })` to wire the
78
+ operator-to-reporter message thread (and its `needs_response` badge). When these are present,
79
+ the bundled `IssueReportMessageThread` renders the conversation inside the issue detail modal
80
+ (see [Conversation Thread](#conversation-thread)).
74
81
  - Any auth and token refresh behavior needed by that client.
75
82
  - Eligibility rules such as feature flags, account state, or role checks.
76
83
  - The current principal ID and optional role hint passed into the provider.
@@ -78,6 +85,7 @@ export function AppShell() {
78
85
  supports `mine` only; use `allowTenantScope` only with a client that implements tenant reads.
79
86
  - Whether a page defaults to `general_page`, `surface_required`, or `surface_preferred` reporting.
80
87
  - Whether the app allows `["text"]`, `["voice"]`, or `["text", "voice"]` report input.
88
+ - User-facing screenshot sensitivity warnings for surfaces that may capture private data.
81
89
  - Styling integration if your build strips package utility classes.
82
90
  - Any app-specific copy overrides.
83
91
  - Origin registration on the owning SPAPS application if the browser calls SPAPS directly with a publishable key.
@@ -127,18 +135,75 @@ The SPAPS server also needs `ELEVENLABS_API_KEY`. The browser calls your normal
127
135
 
128
136
  When text and voice are both enabled, the modal keeps the textarea available and lets the user append a committed transcript into the editable note.
129
137
 
138
+ ## Screenshot Attachments
139
+
140
+ Screenshot attachments are opt-in through the client contract. If the client passed to `IssueReportingProvider` exposes `issueReporting.uploadAttachment`, the modal renders a screenshot picker. If the method is absent, the picker stays hidden and text/voice reporting works unchanged.
141
+
142
+ ```tsx
143
+ const client = {
144
+ issueReporting: {
145
+ ...spaps.issueReporting,
146
+ uploadAttachment: (file: Blob, options?: { filename?: string }) =>
147
+ spaps.issueReporting.uploadAttachment(file, options),
148
+ },
149
+ };
150
+ ```
151
+
152
+ The picker accepts PNG, JPEG, and WebP files, enforces the 10 MiB per-file limit and 5-screenshot report limit, previews pending files, and preserves existing attachments while editing. On submit, it uploads pending files first and sends only attachment IDs through `attachment_ids`, `add_attachment_ids`, or `remove_attachment_ids`.
153
+
154
+ This package does not store screenshots, generate public URLs, or redact image content. SPAPS stores screenshots through its shared hosted-asset boundary as private objects, and access URLs are minted by the backend after authorization. Host apps should warn users before upload when a screenshot might include PHI, credentials, payment data, or other sensitive content.
155
+
156
+ ## Conversation Thread
157
+
158
+ Support operators can ask a reporter for clarification and post a final resolution message. Those messages flow into an allowlisted, reporter-visible projection that the reporter reads back as a conversation. This package renders that surface when the client implements the two optional message methods:
159
+
160
+ ```tsx
161
+ const client = {
162
+ issueReporting: {
163
+ ...spaps.issueReporting,
164
+ listMessages: (issueReportId: string) =>
165
+ spaps.issueReporting.listMessages(issueReportId),
166
+ submitMessage: (issueReportId, payload) =>
167
+ spaps.issueReporting.submitMessage(issueReportId, payload),
168
+ },
169
+ };
170
+ ```
171
+
172
+ When `listMessages` is present, `FloatingIssueReportButton` mounts the exported `IssueReportMessageThread` inside the issue detail modal (the Edit/Reply views). The thread:
173
+
174
+ - Renders only `active`, `reporter_visible` projection rows in chronological order. Retracted and superseded operator messages, non-visible rows, and raw `support_case_events` payloads are never displayed. The backend filters this; `selectReporterVisibleMessages` re-asserts the same rule in the UI.
175
+ - Distinguishes operator (`Support`) vs reporter (`You`) authorship, labels each message kind (`clarification_request`, `reporter_response`, `final_response`), and shows a relative timestamp.
176
+ - Shows a **needs-response** affordance when the API's `needs_response` flag is set (an open clarification request awaiting an answer), and surfaces a matching dot badge on the floating entrypoint while that thread is open.
177
+ - Renders a reporter clarification-response composer (shown only when the client implements `submitMessage`). It calls `submitMessage(issueReportId, { body, idempotency_key })` with a client-generated `idempotency_key`. A key reused with a different body returns `409 ISSUE_REPORT_MESSAGE_CONFLICT`; the composer detects this (`isReporterMessageConflict`), shows the conflict copy, and rotates the key for retry.
178
+ - Exposes loading, empty, and error (with retry) states.
179
+
180
+ The composer does not reopen a closed case; reopen semantics stay with the Reply flow. You can also mount `IssueReportMessageThread` directly (outside the modal) inside your own issue detail view:
181
+
182
+ ```tsx
183
+ import { IssueReportMessageThread } from "spaps-issue-reporting-react";
184
+
185
+ <IssueReportMessageThread issueReportId={issue.id} />;
186
+ ```
187
+
188
+ All thread strings are overridable through the provider `copy` prop. The thread copy keys are: `threadTitle`, `threadDescription`, `threadLoading`, `threadLoadFailed`, `threadEmpty`, `threadNeedsResponseBadge`, `threadAuthorOperator`, `threadAuthorReporter`, `threadKindClarificationRequest`, `threadKindReporterResponse`, `threadKindFinalResponse`, `threadResponseLabel`, `threadResponsePlaceholder`, `threadResponseSubmitAction`, `threadResponseSubmittingAction`, `threadResponseConflict`, and `threadResponseFailed`.
189
+
130
190
  ## Exported Surface
131
191
 
132
192
  | Export | Purpose |
133
193
  | --- | --- |
134
194
  | `IssueReportingProvider` | Owns queries, modal state, copy, scope, and report-mode behavior |
135
195
  | `IssueReportingPageConfig` | Per-page override for `general_page`, `surface_required`, or `surface_preferred` create policy |
136
- | `FloatingIssueReportButton` | Renders the floating entry point, popover, and modal |
196
+ | `FloatingIssueReportButton` | Renders the floating entry point, popover, modal, and (when the client supports messages) the conversation thread |
197
+ | `IssueReportMessageThread` | Renders the reporter-visible operator-to-reporter conversation for one issue report, with a clarification-response composer |
137
198
  | `ReportableSection` | Marks a region as selectable when report mode is active |
138
199
  | `useIssueReporting` | Full provider state, including `startNewIssue()`, `openPageIssueModal()`, and `enterReportMode()` |
139
200
  | `useIssueReportingStatus` | Query helper for summary state |
140
201
  | `useIssueReportingHistory` | Query helper for filtered history lists |
141
202
  | `useIssueReportingMutations` | Mutation helpers for create, update, and reply flows |
203
+ | `useIssueReportingMessages` | Query helper for one issue report's reporter-visible message thread |
204
+ | `useIssueReportingMessageMutation` | Mutation helper for submitting a reporter clarification response |
205
+ | `selectReporterVisibleMessages` | Filters a message list to `active`, reporter-visible rows in chronological order |
206
+ | `isReporterMessageConflict` | Detects a `409 ISSUE_REPORT_MESSAGE_CONFLICT` from the client transport |
142
207
  | `useReportMode` | Low-level selection state for custom wrappers |
143
208
 
144
209
  ## Styling Notes
@@ -192,6 +257,14 @@ Pass `inputModes={["voice"]}` or `inputModes={["text", "voice"]}` to the provide
192
257
 
193
258
  Confirm that the SPAPS server has `ELEVENLABS_API_KEY` set and that the current user is authenticated and eligible for issue reporting.
194
259
 
260
+ ### Screenshot controls do not appear
261
+
262
+ Expose `issueReporting.uploadAttachment` on the client passed to `IssueReportingProvider`. The package intentionally hides the picker when the upload helper is absent.
263
+
264
+ ### The conversation thread does not appear
265
+
266
+ Expose `issueReporting.listMessages` on the client passed to `IssueReportingProvider`, then open an existing issue's detail (Edit/Reply). The thread is intentionally hidden when the client cannot list messages. The response composer additionally requires `issueReporting.submitMessage`; without it the thread renders read-only.
267
+
195
268
  ## Limitations
196
269
 
197
270
  - This package assumes React Query is already part of the host app.
@@ -228,10 +301,19 @@ No. It only renders scope choices that your app explicitly allows.
228
301
 
229
302
  No. This package is UI only. If your browser app calls SPAPS directly with a publishable key, the relevant SPAPS application row must include that browser origin in `allowed_origins`.
230
303
 
304
+ ## Design system
305
+
306
+ This package is intentionally theme-neutral and uses accessible Radix behavior
307
+ primitives plus a restrained slate palette so it blends into the host app. The
308
+ Design System Registry (DSR) adoption decision for this surface is an **explicit
309
+ deferral** because no suitable theme-neutral registry item exists yet. The
310
+ repository-level `docs/DSR-DECISION.md` captures the reasoning, file-level audit,
311
+ and revisit trigger.
312
+
231
313
  ## Metadata
232
314
 
233
315
  - `package_name`: `spaps-issue-reporting-react`
234
- - `latest_version`: `0.2.0`
316
+ - `latest_version`: `0.4.1`
235
317
  - `minimum_runtime`: `Node.js >=18.0.0`
236
318
  - `api_base_url`: `https://api.sweetpotato.dev`
237
319
 
package/dist/index.d.mts CHANGED
@@ -2,8 +2,8 @@ import * as react_jsx_runtime from 'react/jsx-runtime';
2
2
  import * as React from 'react';
3
3
  import React__default, { ReactNode } from 'react';
4
4
  import * as spaps_types from 'spaps-types';
5
- import { IssueReportScope, IssueReportStatusResult, IssueReportStatus, IssueReportListResult, IssueReport, CreateIssueReportRequest, UpdateIssueReportRequest, ReplyIssueReportRequest, IssueReportingVoiceTokenResult, IssueReportingInputMode, IssueReportingVoiceProvider } from 'spaps-types';
6
- export { CreateIssueReportRequest, IssueReport, IssueReportListResult, IssueReportScope, IssueReportStatus, IssueReportStatusResult, ReplyIssueReportRequest, UpdateIssueReportRequest } from 'spaps-types';
5
+ import { IssueReportScope, IssueReportStatusResult, IssueReportStatus, IssueReportListResult, IssueReport, CreateIssueReportRequest, UpdateIssueReportRequest, ReplyIssueReportRequest, IssueReportingVoiceTokenResult, IssueReportAttachmentOut, ListIssueReportMessagesResponse, CreateReporterMessageRequest, IssueReportMessage, IssueReportingInputMode, IssueReportingVoiceProvider } from 'spaps-types';
6
+ export { CreateIssueReportRequest, CreateReporterMessageRequest, IssueReport, IssueReportAttachmentOut, IssueReportListResult, IssueReportMessage, IssueReportScope, IssueReportStatus, IssueReportStatusResult, ListIssueReportMessagesResponse, ReplyIssueReportRequest, UpdateIssueReportRequest } from 'spaps-types';
7
7
  import * as _tanstack_query_core from '@tanstack/query-core';
8
8
  import * as _tanstack_react_query from '@tanstack/react-query';
9
9
 
@@ -36,6 +36,11 @@ interface IssueReportingClient {
36
36
  update: (issueReportId: string, payload: UpdateIssueReportRequest) => Promise<IssueReport>;
37
37
  reply: (issueReportId: string, payload: ReplyIssueReportRequest) => Promise<IssueReport>;
38
38
  createVoiceToken?: () => Promise<IssueReportingVoiceTokenResult>;
39
+ uploadAttachment?: (file: Blob, options?: {
40
+ filename?: string;
41
+ }) => Promise<IssueReportAttachmentOut>;
42
+ listMessages?: (issueReportId: string) => Promise<ListIssueReportMessagesResponse>;
43
+ submitMessage?: (issueReportId: string, payload: CreateReporterMessageRequest) => Promise<IssueReportMessage>;
39
44
  };
40
45
  }
41
46
  interface ReportableTargetDescriptor {
@@ -93,6 +98,23 @@ interface IssueReportingCopy {
93
98
  originHumanLabel: string;
94
99
  originMachineLabel: string;
95
100
  machineOriginFallback: string;
101
+ threadTitle: string;
102
+ threadDescription: string;
103
+ threadLoading: string;
104
+ threadLoadFailed: string;
105
+ threadEmpty: string;
106
+ threadNeedsResponseBadge: string;
107
+ threadAuthorOperator: string;
108
+ threadAuthorReporter: string;
109
+ threadKindClarificationRequest: string;
110
+ threadKindReporterResponse: string;
111
+ threadKindFinalResponse: string;
112
+ threadResponsePlaceholder: string;
113
+ threadResponseLabel: string;
114
+ threadResponseSubmitAction: string;
115
+ threadResponseSubmittingAction: string;
116
+ threadResponseConflict: string;
117
+ threadResponseFailed: string;
96
118
  }
97
119
  interface IssueReportingProviderProps {
98
120
  client: IssueReportingClient;
@@ -117,6 +139,9 @@ interface IssueReportingPageConfigProps {
117
139
  createMode: IssueReportingCreateMode;
118
140
  }
119
141
 
142
+ declare function IssueReportMessageThread({ issueReportId, }: {
143
+ issueReportId: string;
144
+ }): react_jsx_runtime.JSX.Element | null;
120
145
  declare function FloatingIssueReportButton({ className, positionClassName, }: FloatingIssueReportButtonProps): react_jsx_runtime.JSX.Element | null;
121
146
  declare function ReportableSection({ reportableName, children, className, as: Component, }: {
122
147
  reportableName: ReportableInput;
@@ -171,6 +196,8 @@ type IssueReportingContextValue = {
171
196
  voice: Required<Pick<IssueReportingVoiceConfig, "provider" | "modelId" | "requireMicrophonePermission">> & {
172
197
  microphone?: IssueReportingVoiceConfig["microphone"];
173
198
  };
199
+ needsResponseIssueIds: string[];
200
+ setNeedsResponse: (issueReportId: string, needsResponse: boolean) => void;
174
201
  };
175
202
  declare const defaultIssueReportingCopy: IssueReportingCopy;
176
203
  declare const issueReportingKeys: {
@@ -178,6 +205,7 @@ declare const issueReportingKeys: {
178
205
  status: (scope: IssueReportScope) => readonly ["spaps-issue-reporting", "status", IssueReportScope];
179
206
  history: (scope: IssueReportScope) => readonly ["spaps-issue-reporting", "history", IssueReportScope];
180
207
  detail: (issueReportId: string) => readonly ["spaps-issue-reporting", "detail", string];
208
+ messages: (issueReportId: string) => readonly ["spaps-issue-reporting", "messages", string];
181
209
  };
182
210
  declare function isOpenIssueStatus(status: IssueReportStatus): boolean;
183
211
  declare function isClosedIssueStatus(status: IssueReportStatus): boolean;
@@ -189,6 +217,25 @@ declare function getEntryPointState(status?: {
189
217
  has_recent_resolved: boolean;
190
218
  } | null): "open" | "recent_resolved" | "neutral";
191
219
  declare function getEntryPointClassName(state: "open" | "recent_resolved" | "neutral"): string;
220
+ /**
221
+ * Defensive reporter-visibility filter for the conversation surface.
222
+ *
223
+ * The reporter `GET /messages` endpoint already returns only `active`,
224
+ * reporter-visible projection rows (retracted/superseded operator messages and
225
+ * raw `support_case_events` payloads are excluded server-side). This filter
226
+ * re-asserts that contract in the UI so a non-allowlisted row can never be
227
+ * rendered to a reporter even if a client or fixture passes one through. Rows
228
+ * are returned in chronological (`created_at`) order.
229
+ */
230
+ declare function selectReporterVisibleMessages(messages: IssueReportMessage[]): IssueReportMessage[];
231
+ /**
232
+ * Detect the idempotency-key conflict surfaced by the reporter message API as
233
+ * `409 ISSUE_REPORT_MESSAGE_CONFLICT`. The provider only depends on the
234
+ * abstract `IssueReportingClient` contract, so this inspects whatever shape the
235
+ * host client throws (Error message, `code`, or HTTP `status`) without binding
236
+ * to a specific transport.
237
+ */
238
+ declare function isReporterMessageConflict(error: unknown): boolean;
192
239
  declare function getIssueNoteLengthMessage(note: string, copy: IssueReportingCopy): string;
193
240
  declare function useIssueReporting(): IssueReportingContextValue;
194
241
  declare function IssueReportingPageConfig({ createMode, }: IssueReportingPageConfigProps): react_jsx_runtime.JSX.Element;
@@ -373,17 +420,45 @@ declare function useIssueReportingMutations(): {
373
420
  target: ResolvedTarget;
374
421
  note: string;
375
422
  reporter_role_hint?: string;
423
+ attachment_ids?: string[];
376
424
  }, unknown>;
377
425
  updateMutation: _tanstack_react_query.UseMutationResult<IssueReport, Error, {
378
426
  issueReportId: string;
379
427
  note: string;
428
+ add_attachment_ids?: string[];
429
+ remove_attachment_ids?: string[];
380
430
  }, unknown>;
381
431
  replyMutation: _tanstack_react_query.UseMutationResult<IssueReport, Error, {
382
432
  issueReportId: string;
383
433
  note: string;
384
434
  reporterRoleHint?: string;
435
+ attachment_ids?: string[];
385
436
  }, unknown>;
386
437
  };
438
+ /**
439
+ * Query helper for the reporter-visible message thread of one issue report.
440
+ *
441
+ * Keyed by `issue_report_id`. Enabled only when the caller is eligible, an
442
+ * issue id is provided, and the host client implements the optional
443
+ * `listMessages` contract method. The query returns the raw server response so
444
+ * consumers can read `needs_response` for the badge; rendering code should pass
445
+ * `data.items` through {@link selectReporterVisibleMessages} before display.
446
+ */
447
+ declare function useIssueReportingMessages(issueReportId: string | null): _tanstack_react_query.UseQueryResult<ListIssueReportMessagesResponse, Error>;
448
+ /**
449
+ * Mutation helper for submitting a reporter clarification response.
450
+ *
451
+ * Calls the host client's `submitMessage(issueReportId, { body, idempotency_key })`
452
+ * and, on success, invalidates the thread query so the new `reporter_response`
453
+ * row and refreshed `needs_response` flag are re-fetched. The caller supplies a
454
+ * client-generated `idempotency_key` for safe retries; a key reused with a
455
+ * different body surfaces as a `409` conflict the UI can detect with
456
+ * {@link isReporterMessageConflict}.
457
+ */
458
+ declare function useIssueReportingMessageMutation(issueReportId: string | null): _tanstack_react_query.UseMutationResult<IssueReportMessage, unknown, {
459
+ body: string;
460
+ idempotency_key?: string;
461
+ }, unknown>;
387
462
  declare function IssueReportingProvider({ client, isEligible, reporterRoleHint, principalId, getPageUrl, defaultScope, allowTenantScope, defaultCreateMode, inputModes, defaultInputMode, voice, copy, children, }: IssueReportingProviderProps): react_jsx_runtime.JSX.Element;
388
463
 
389
464
  interface ReportModeContextValue {
@@ -397,4 +472,4 @@ interface ReportModeContextValue {
397
472
  declare const ReportModeContext: React.Context<ReportModeContextValue | null>;
398
473
  declare function useReportMode(): ReportModeContextValue | null;
399
474
 
400
- export { FloatingIssueReportButton, type FloatingIssueReportButtonProps, type IssueHistoryFilter, type IssueReportingClient, type IssueReportingCopy, type IssueReportingCreateMode, IssueReportingPageConfig, type IssueReportingPageConfigProps, IssueReportingProvider, type IssueReportingProviderProps, ReportModeContext, type ReportModeContextValue, type ReportableInput, ReportableSection, type ReportableTargetDescriptor, defaultIssueReportingCopy, filterIssueReports, getEntryPointClassName, getEntryPointState, getIssueNoteLengthMessage, getIssueStatusBadgeLabel, getIssueStatusClassName, isClosedIssueStatus, isOpenIssueStatus, issueReportingKeys, useIssueReporting, useIssueReportingHistory, useIssueReportingMutations, useIssueReportingStatus, useReportMode };
475
+ export { FloatingIssueReportButton, type FloatingIssueReportButtonProps, type IssueHistoryFilter, IssueReportMessageThread, type IssueReportingClient, type IssueReportingCopy, type IssueReportingCreateMode, IssueReportingPageConfig, type IssueReportingPageConfigProps, IssueReportingProvider, type IssueReportingProviderProps, ReportModeContext, type ReportModeContextValue, type ReportableInput, ReportableSection, type ReportableTargetDescriptor, defaultIssueReportingCopy, filterIssueReports, getEntryPointClassName, getEntryPointState, getIssueNoteLengthMessage, getIssueStatusBadgeLabel, getIssueStatusClassName, isClosedIssueStatus, isOpenIssueStatus, isReporterMessageConflict, issueReportingKeys, selectReporterVisibleMessages, useIssueReporting, useIssueReportingHistory, useIssueReportingMessageMutation, useIssueReportingMessages, useIssueReportingMutations, useIssueReportingStatus, useReportMode };
package/dist/index.d.ts CHANGED
@@ -2,8 +2,8 @@ import * as react_jsx_runtime from 'react/jsx-runtime';
2
2
  import * as React from 'react';
3
3
  import React__default, { ReactNode } from 'react';
4
4
  import * as spaps_types from 'spaps-types';
5
- import { IssueReportScope, IssueReportStatusResult, IssueReportStatus, IssueReportListResult, IssueReport, CreateIssueReportRequest, UpdateIssueReportRequest, ReplyIssueReportRequest, IssueReportingVoiceTokenResult, IssueReportingInputMode, IssueReportingVoiceProvider } from 'spaps-types';
6
- export { CreateIssueReportRequest, IssueReport, IssueReportListResult, IssueReportScope, IssueReportStatus, IssueReportStatusResult, ReplyIssueReportRequest, UpdateIssueReportRequest } from 'spaps-types';
5
+ import { IssueReportScope, IssueReportStatusResult, IssueReportStatus, IssueReportListResult, IssueReport, CreateIssueReportRequest, UpdateIssueReportRequest, ReplyIssueReportRequest, IssueReportingVoiceTokenResult, IssueReportAttachmentOut, ListIssueReportMessagesResponse, CreateReporterMessageRequest, IssueReportMessage, IssueReportingInputMode, IssueReportingVoiceProvider } from 'spaps-types';
6
+ export { CreateIssueReportRequest, CreateReporterMessageRequest, IssueReport, IssueReportAttachmentOut, IssueReportListResult, IssueReportMessage, IssueReportScope, IssueReportStatus, IssueReportStatusResult, ListIssueReportMessagesResponse, ReplyIssueReportRequest, UpdateIssueReportRequest } from 'spaps-types';
7
7
  import * as _tanstack_query_core from '@tanstack/query-core';
8
8
  import * as _tanstack_react_query from '@tanstack/react-query';
9
9
 
@@ -36,6 +36,11 @@ interface IssueReportingClient {
36
36
  update: (issueReportId: string, payload: UpdateIssueReportRequest) => Promise<IssueReport>;
37
37
  reply: (issueReportId: string, payload: ReplyIssueReportRequest) => Promise<IssueReport>;
38
38
  createVoiceToken?: () => Promise<IssueReportingVoiceTokenResult>;
39
+ uploadAttachment?: (file: Blob, options?: {
40
+ filename?: string;
41
+ }) => Promise<IssueReportAttachmentOut>;
42
+ listMessages?: (issueReportId: string) => Promise<ListIssueReportMessagesResponse>;
43
+ submitMessage?: (issueReportId: string, payload: CreateReporterMessageRequest) => Promise<IssueReportMessage>;
39
44
  };
40
45
  }
41
46
  interface ReportableTargetDescriptor {
@@ -93,6 +98,23 @@ interface IssueReportingCopy {
93
98
  originHumanLabel: string;
94
99
  originMachineLabel: string;
95
100
  machineOriginFallback: string;
101
+ threadTitle: string;
102
+ threadDescription: string;
103
+ threadLoading: string;
104
+ threadLoadFailed: string;
105
+ threadEmpty: string;
106
+ threadNeedsResponseBadge: string;
107
+ threadAuthorOperator: string;
108
+ threadAuthorReporter: string;
109
+ threadKindClarificationRequest: string;
110
+ threadKindReporterResponse: string;
111
+ threadKindFinalResponse: string;
112
+ threadResponsePlaceholder: string;
113
+ threadResponseLabel: string;
114
+ threadResponseSubmitAction: string;
115
+ threadResponseSubmittingAction: string;
116
+ threadResponseConflict: string;
117
+ threadResponseFailed: string;
96
118
  }
97
119
  interface IssueReportingProviderProps {
98
120
  client: IssueReportingClient;
@@ -117,6 +139,9 @@ interface IssueReportingPageConfigProps {
117
139
  createMode: IssueReportingCreateMode;
118
140
  }
119
141
 
142
+ declare function IssueReportMessageThread({ issueReportId, }: {
143
+ issueReportId: string;
144
+ }): react_jsx_runtime.JSX.Element | null;
120
145
  declare function FloatingIssueReportButton({ className, positionClassName, }: FloatingIssueReportButtonProps): react_jsx_runtime.JSX.Element | null;
121
146
  declare function ReportableSection({ reportableName, children, className, as: Component, }: {
122
147
  reportableName: ReportableInput;
@@ -171,6 +196,8 @@ type IssueReportingContextValue = {
171
196
  voice: Required<Pick<IssueReportingVoiceConfig, "provider" | "modelId" | "requireMicrophonePermission">> & {
172
197
  microphone?: IssueReportingVoiceConfig["microphone"];
173
198
  };
199
+ needsResponseIssueIds: string[];
200
+ setNeedsResponse: (issueReportId: string, needsResponse: boolean) => void;
174
201
  };
175
202
  declare const defaultIssueReportingCopy: IssueReportingCopy;
176
203
  declare const issueReportingKeys: {
@@ -178,6 +205,7 @@ declare const issueReportingKeys: {
178
205
  status: (scope: IssueReportScope) => readonly ["spaps-issue-reporting", "status", IssueReportScope];
179
206
  history: (scope: IssueReportScope) => readonly ["spaps-issue-reporting", "history", IssueReportScope];
180
207
  detail: (issueReportId: string) => readonly ["spaps-issue-reporting", "detail", string];
208
+ messages: (issueReportId: string) => readonly ["spaps-issue-reporting", "messages", string];
181
209
  };
182
210
  declare function isOpenIssueStatus(status: IssueReportStatus): boolean;
183
211
  declare function isClosedIssueStatus(status: IssueReportStatus): boolean;
@@ -189,6 +217,25 @@ declare function getEntryPointState(status?: {
189
217
  has_recent_resolved: boolean;
190
218
  } | null): "open" | "recent_resolved" | "neutral";
191
219
  declare function getEntryPointClassName(state: "open" | "recent_resolved" | "neutral"): string;
220
+ /**
221
+ * Defensive reporter-visibility filter for the conversation surface.
222
+ *
223
+ * The reporter `GET /messages` endpoint already returns only `active`,
224
+ * reporter-visible projection rows (retracted/superseded operator messages and
225
+ * raw `support_case_events` payloads are excluded server-side). This filter
226
+ * re-asserts that contract in the UI so a non-allowlisted row can never be
227
+ * rendered to a reporter even if a client or fixture passes one through. Rows
228
+ * are returned in chronological (`created_at`) order.
229
+ */
230
+ declare function selectReporterVisibleMessages(messages: IssueReportMessage[]): IssueReportMessage[];
231
+ /**
232
+ * Detect the idempotency-key conflict surfaced by the reporter message API as
233
+ * `409 ISSUE_REPORT_MESSAGE_CONFLICT`. The provider only depends on the
234
+ * abstract `IssueReportingClient` contract, so this inspects whatever shape the
235
+ * host client throws (Error message, `code`, or HTTP `status`) without binding
236
+ * to a specific transport.
237
+ */
238
+ declare function isReporterMessageConflict(error: unknown): boolean;
192
239
  declare function getIssueNoteLengthMessage(note: string, copy: IssueReportingCopy): string;
193
240
  declare function useIssueReporting(): IssueReportingContextValue;
194
241
  declare function IssueReportingPageConfig({ createMode, }: IssueReportingPageConfigProps): react_jsx_runtime.JSX.Element;
@@ -373,17 +420,45 @@ declare function useIssueReportingMutations(): {
373
420
  target: ResolvedTarget;
374
421
  note: string;
375
422
  reporter_role_hint?: string;
423
+ attachment_ids?: string[];
376
424
  }, unknown>;
377
425
  updateMutation: _tanstack_react_query.UseMutationResult<IssueReport, Error, {
378
426
  issueReportId: string;
379
427
  note: string;
428
+ add_attachment_ids?: string[];
429
+ remove_attachment_ids?: string[];
380
430
  }, unknown>;
381
431
  replyMutation: _tanstack_react_query.UseMutationResult<IssueReport, Error, {
382
432
  issueReportId: string;
383
433
  note: string;
384
434
  reporterRoleHint?: string;
435
+ attachment_ids?: string[];
385
436
  }, unknown>;
386
437
  };
438
+ /**
439
+ * Query helper for the reporter-visible message thread of one issue report.
440
+ *
441
+ * Keyed by `issue_report_id`. Enabled only when the caller is eligible, an
442
+ * issue id is provided, and the host client implements the optional
443
+ * `listMessages` contract method. The query returns the raw server response so
444
+ * consumers can read `needs_response` for the badge; rendering code should pass
445
+ * `data.items` through {@link selectReporterVisibleMessages} before display.
446
+ */
447
+ declare function useIssueReportingMessages(issueReportId: string | null): _tanstack_react_query.UseQueryResult<ListIssueReportMessagesResponse, Error>;
448
+ /**
449
+ * Mutation helper for submitting a reporter clarification response.
450
+ *
451
+ * Calls the host client's `submitMessage(issueReportId, { body, idempotency_key })`
452
+ * and, on success, invalidates the thread query so the new `reporter_response`
453
+ * row and refreshed `needs_response` flag are re-fetched. The caller supplies a
454
+ * client-generated `idempotency_key` for safe retries; a key reused with a
455
+ * different body surfaces as a `409` conflict the UI can detect with
456
+ * {@link isReporterMessageConflict}.
457
+ */
458
+ declare function useIssueReportingMessageMutation(issueReportId: string | null): _tanstack_react_query.UseMutationResult<IssueReportMessage, unknown, {
459
+ body: string;
460
+ idempotency_key?: string;
461
+ }, unknown>;
387
462
  declare function IssueReportingProvider({ client, isEligible, reporterRoleHint, principalId, getPageUrl, defaultScope, allowTenantScope, defaultCreateMode, inputModes, defaultInputMode, voice, copy, children, }: IssueReportingProviderProps): react_jsx_runtime.JSX.Element;
388
463
 
389
464
  interface ReportModeContextValue {
@@ -397,4 +472,4 @@ interface ReportModeContextValue {
397
472
  declare const ReportModeContext: React.Context<ReportModeContextValue | null>;
398
473
  declare function useReportMode(): ReportModeContextValue | null;
399
474
 
400
- export { FloatingIssueReportButton, type FloatingIssueReportButtonProps, type IssueHistoryFilter, type IssueReportingClient, type IssueReportingCopy, type IssueReportingCreateMode, IssueReportingPageConfig, type IssueReportingPageConfigProps, IssueReportingProvider, type IssueReportingProviderProps, ReportModeContext, type ReportModeContextValue, type ReportableInput, ReportableSection, type ReportableTargetDescriptor, defaultIssueReportingCopy, filterIssueReports, getEntryPointClassName, getEntryPointState, getIssueNoteLengthMessage, getIssueStatusBadgeLabel, getIssueStatusClassName, isClosedIssueStatus, isOpenIssueStatus, issueReportingKeys, useIssueReporting, useIssueReportingHistory, useIssueReportingMutations, useIssueReportingStatus, useReportMode };
475
+ export { FloatingIssueReportButton, type FloatingIssueReportButtonProps, type IssueHistoryFilter, IssueReportMessageThread, type IssueReportingClient, type IssueReportingCopy, type IssueReportingCreateMode, IssueReportingPageConfig, type IssueReportingPageConfigProps, IssueReportingProvider, type IssueReportingProviderProps, ReportModeContext, type ReportModeContextValue, type ReportableInput, ReportableSection, type ReportableTargetDescriptor, defaultIssueReportingCopy, filterIssueReports, getEntryPointClassName, getEntryPointState, getIssueNoteLengthMessage, getIssueStatusBadgeLabel, getIssueStatusClassName, isClosedIssueStatus, isOpenIssueStatus, isReporterMessageConflict, issueReportingKeys, selectReporterVisibleMessages, useIssueReporting, useIssueReportingHistory, useIssueReportingMessageMutation, useIssueReportingMessages, useIssueReportingMutations, useIssueReportingStatus, useReportMode };