querysub 0.680.0 → 0.682.0

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "querysub",
3
- "version": "0.680.0",
3
+ "version": "0.682.0",
4
4
  "main": "index.js",
5
5
  "license": "MIT",
6
6
  "note1": "note on node-forge fork, see https://github.com/digitalbazaar/forge/issues/744 for details",
@@ -322,9 +322,9 @@ export class PathRouter {
322
322
  // - If this becomes an issue we COULD filter, as we can do it quickly, but I don't think it is required, as all the present usecases prefilter anyways.
323
323
  @measureFnc
324
324
  public static async getPathIdentifierTargets(values: PathValue[], ourSpec: AuthoritySpec): Promise<Map<string, PathValue[]>> {
325
- // NOTE: The file size limit is 1024 bytes. But we also have our folder, etc, so we want to add enough buffer
325
+ // NOTE: The identifier becomes a single directory name, which linux caps at 255 bytes (see MAX_IDENTIFIER_LENGTH), so the range numbers plus this many prefix hashes must always fit within that.
326
326
  // - Shorter hashes means we can store more, but there's a point when the collisions make it less useful.
327
- const MAX_PREFIXES_PER_FILE = 50;
327
+ const MAX_PREFIXES_PER_FILE = 20;
328
328
  const PREFIX_COVER_FRACTION = 0.99;
329
329
  const TARGET_VALUES_PER_SHARD_GROUP = 10 * 1000 * 1000;
330
330
  const TARGET_SHARD_SIZE = 50 * 1000;
@@ -209,7 +209,7 @@ export function createArchiveLocker2(config: {
209
209
  readFiles: (files: FileInfo[]) => Promise<(Buffer | undefined)[]>,
210
210
  ) => Promise<ArchiveTransaction[]>
211
211
  ): Promise<"accepted" | "rejected"> {
212
- let files = await locker.getFiles();
212
+ let files = await locker.getFiles({ forTransaction: true });
213
213
  await saveSnapshot({ files: files.map(a => a.file) });
214
214
  let readFiles = async (files: FileInfo[]) => {
215
215
  let pendingFiles = files.slice();
@@ -494,21 +494,23 @@ class TransactionLocker {
494
494
  let time = Date.now();
495
495
  let files = await this.storage.getKeys(config);
496
496
  this.perf.checkpoint("getKeys");
497
- if (this.lastFilesRead && this.lastFilesReadTime) {
498
- let prevFiles = new Set(this.lastFilesRead.map(a => a.file));
499
- let suspiciousThreshold = this.lastFilesReadTime - ARCHIVE_PROPAGATION_TIME * 10;
500
- let newFiles = files.filter(a => !prevFiles.has(a.file));
501
- let veryBadFiles = newFiles.filter(x => x.createTime < suspiciousThreshold);
502
- if (veryBadFiles.length > 0) {
503
- console.error(`Old read was not exhaustive, because we just found files which existed well before it, but that it did not read. This likely means our propagation assumptions are wrong, and propagation is much slower than anticipated. OR, getKeys is flakey and randomly misses files? Either way, this will BREAK THE DATABASE! Any node experiencing this might start deleting valid database files, which is very bad!`, {
504
- newlyAppearingOldFiles: veryBadFiles.map(x => x.file),
505
- prevFiles: this.lastFilesRead.map(x => x.file),
506
- newFiles: files.map(x => x.file),
507
- });
497
+ if (!config?.fallbacks) {
498
+ if (this.lastFilesRead && this.lastFilesReadTime) {
499
+ let prevFiles = new Set(this.lastFilesRead.map(a => a.file));
500
+ let suspiciousThreshold = this.lastFilesReadTime - ARCHIVE_PROPAGATION_TIME * 10;
501
+ let newFiles = files.filter(a => !prevFiles.has(a.file));
502
+ let veryBadFiles = newFiles.filter(x => x.createTime < suspiciousThreshold);
503
+ if (veryBadFiles.length > 0) {
504
+ console.error(`Old read was not exhaustive, because we just found files which existed well before it, but that it did not read. This likely means our propagation assumptions are wrong, and propagation is much slower than anticipated. OR, getKeys is flakey and randomly misses files? Either way, this will BREAK THE DATABASE! Any node experiencing this might start deleting valid database files, which is very bad!`, {
505
+ newlyAppearingOldFiles: veryBadFiles.map(x => x.file),
506
+ prevFiles: this.lastFilesRead.map(x => x.file),
507
+ newFiles: files.map(x => x.file),
508
+ });
509
+ }
508
510
  }
511
+ this.lastFilesRead = files;
512
+ this.lastFilesReadTime = time;
509
513
  }
510
- this.lastFilesRead = files;
511
- this.lastFilesReadTime = time;
512
514
 
513
515
  let transactions: (Transaction & {
514
516
  seqNum: number;
@@ -794,7 +796,7 @@ class TransactionLocker {
794
796
  /** Only returns data files (no transaction files, or confirmations).
795
797
  * - Might run a transaction
796
798
  */
797
- public async getFiles(): Promise<FileInfo[]> {
799
+ public async getFiles(config?: { forTransaction?: boolean }): Promise<FileInfo[]> {
798
800
  let time = Date.now();
799
801
  let storageAlivePromise = errorToUndefinedSilent(this.storage.getSyncStatus());
800
802
  try {
@@ -805,7 +807,7 @@ class TransactionLocker {
805
807
  return obj.dataFiles;
806
808
  } catch (e) {
807
809
  let duration0 = Date.now() - time;
808
- if (await storageAlivePromise) {
810
+ if (config?.forTransaction || await storageAlivePromise) {
809
811
  throw e;
810
812
  }
811
813
  let duration = Date.now() - time;
@@ -1383,7 +1383,7 @@ export class PathValueProxyWatcher {
1383
1383
  },
1384
1384
  code() {
1385
1385
  return currentReadSource.temporaryOverride(options.overrides, () =>
1386
- runCodeWithDatabase(proxy, baseFunction)
1386
+ runCodeWithDatabase(proxy, curFunction)
1387
1387
  );
1388
1388
  },
1389
1389
  }) as Result;
@@ -83,13 +83,21 @@ export function showModal(config: {
83
83
  } {
84
84
  ensureRendering();
85
85
  let id = nextId();
86
- Querysub.commit(() => {
86
+ if (Querysub.isInSyncedCall()) {
87
87
  data().modals[id] = atomicObjectWriteNoFreeze({
88
88
  value: config.content,
89
89
  onClose: config.onClose,
90
90
  onlyExplicitClose: config.onlyCloseExplicitly,
91
91
  });
92
- });
92
+ } else {
93
+ Querysub.commit(() => {
94
+ data().modals[id] = atomicObjectWriteNoFreeze({
95
+ value: config.content,
96
+ onClose: config.onClose,
97
+ onlyExplicitClose: config.onlyCloseExplicitly,
98
+ });
99
+ });
100
+ }
93
101
  function close() {
94
102
  Querysub.commit(() => closeModal(id));
95
103
  }
@@ -2,57 +2,41 @@ import { Querysub } from "../4-querysub/Querysub";
2
2
  import { onPredictionFinished } from "../4-querysub/querysubPrediction";
3
3
  import { lazy } from "socket-function/src/caching";
4
4
  import { t } from "../2-proxy/schema2";
5
- import { StatsValue, createStatsValue, addToStatsValue } from "socket-function/src/profiling/stats";
6
5
  import { authorityStorage } from "../0-path-value-core/pathValueCore";
7
6
  import { pathWatcher } from "../0-path-value-core/PathWatcher";
8
7
  import { proxyWatcher } from "../2-proxy/PathValueProxyWatcher";
9
8
 
10
- interface CallStatsData {
9
+ export interface FunctionCallStats {
11
10
  totalCalls: number;
12
- totalInternalReruns: number;
13
- totalFullReruns: number;
14
- callsWithMultipleInternalRuns: number;
15
- callsWithCascadingRuns: number;
16
- totalInternalRerunsForCascading: number;
17
11
  maxInternalReruns: number;
18
- callsWithMultipleFullRuns: number;
19
- evalTimeStats: StatsValue;
20
- timeTakenStats: StatsValue;
21
- totalTimeStats: StatsValue;
12
+ maxFullReruns: number;
13
+ maxEvalTime: number;
14
+ maxTotalTime: number;
15
+ }
16
+
17
+ interface CallStatsData extends FunctionCallStats {
22
18
  localPathCount: number;
23
19
  remotePathCount: number;
24
20
  totalValueCount: number;
25
21
  proxyWatcherCount: number;
26
22
  callsByServer: { [creatorId: string]: number };
27
23
  perFunctionStats: {
28
- [functionId: string]: {
29
- totalCalls: number;
30
- totalInternalReruns: number;
31
- totalFullReruns: number;
32
- callsWithMultipleInternalRuns: number;
33
- callsWithCascadingRuns: number;
34
- totalInternalRerunsForCascading: number;
35
- maxInternalReruns: number;
36
- callsWithMultipleFullRuns: number;
37
- evalTimeStats: StatsValue;
38
- timeTakenStats: StatsValue;
39
- totalTimeStats: StatsValue;
40
- }
24
+ [functionId: string]: FunctionCallStats;
25
+ };
26
+ }
27
+
28
+ function createFunctionCallStats(): FunctionCallStats {
29
+ return {
30
+ totalCalls: 0,
31
+ maxInternalReruns: 0,
32
+ maxFullReruns: 0,
33
+ maxEvalTime: 0,
34
+ maxTotalTime: 0,
41
35
  };
42
36
  }
43
37
 
44
38
  let statsData: CallStatsData = {
45
- totalCalls: 0,
46
- totalInternalReruns: 0,
47
- totalFullReruns: 0,
48
- callsWithMultipleInternalRuns: 0,
49
- callsWithCascadingRuns: 0,
50
- totalInternalRerunsForCascading: 0,
51
- maxInternalReruns: 0,
52
- callsWithMultipleFullRuns: 0,
53
- evalTimeStats: createStatsValue(),
54
- timeTakenStats: createStatsValue(),
55
- totalTimeStats: createStatsValue(),
39
+ ...createFunctionCallStats(),
56
40
  localPathCount: 0,
57
41
  remotePathCount: 0,
58
42
  totalValueCount: 0,
@@ -76,64 +60,20 @@ export function callState(): CallStatsData {
76
60
 
77
61
  export let ensureSubscribed = lazy(() => {
78
62
  onPredictionFinished((data) => {
79
- statsData.totalCalls++;
80
63
  statsData.callsByServer[data.creatorId] = (statsData.callsByServer[data.creatorId] || 0) + 1;
81
- statsData.totalInternalReruns += data.result.totalInternalLoopCount - 1;
82
- statsData.totalFullReruns += data.result.outerLoopCount - 1;
83
- addToStatsValue(statsData.evalTimeStats, data.result.evalTime);
84
- addToStatsValue(statsData.timeTakenStats, data.result.timeTaken);
85
- addToStatsValue(statsData.totalTimeStats, data.result.totalTime);
86
-
87
- if (data.result.totalInternalLoopCount > 1) {
88
- statsData.callsWithMultipleInternalRuns++;
89
- }
90
- if (data.result.totalInternalLoopCount > 2) {
91
- statsData.callsWithCascadingRuns++;
92
- statsData.totalInternalRerunsForCascading += data.result.totalInternalLoopCount - 1;
93
- if (data.result.totalInternalLoopCount > statsData.maxInternalReruns) {
94
- statsData.maxInternalReruns = data.result.totalInternalLoopCount;
95
- }
96
- }
97
- if (data.result.outerLoopCount > 1) {
98
- statsData.callsWithMultipleFullRuns++;
99
- }
100
-
101
- if (!(data.functionId in statsData.perFunctionStats)) {
102
- statsData.perFunctionStats[data.functionId] = {
103
- totalCalls: 0,
104
- totalInternalReruns: 0,
105
- totalFullReruns: 0,
106
- callsWithMultipleInternalRuns: 0,
107
- callsWithCascadingRuns: 0,
108
- totalInternalRerunsForCascading: 0,
109
- maxInternalReruns: 0,
110
- callsWithMultipleFullRuns: 0,
111
- evalTimeStats: createStatsValue(),
112
- timeTakenStats: createStatsValue(),
113
- totalTimeStats: createStatsValue(),
114
- };
115
- }
116
64
 
117
65
  let fnStats = statsData.perFunctionStats[data.functionId];
118
- fnStats.totalCalls++;
119
- fnStats.totalInternalReruns += data.result.totalInternalLoopCount - 1;
120
- fnStats.totalFullReruns += data.result.outerLoopCount - 1;
121
- addToStatsValue(fnStats.evalTimeStats, data.result.evalTime);
122
- addToStatsValue(fnStats.timeTakenStats, data.result.timeTaken);
123
- addToStatsValue(fnStats.totalTimeStats, data.result.totalTime);
124
-
125
- if (data.result.totalInternalLoopCount > 1) {
126
- fnStats.callsWithMultipleInternalRuns++;
127
- }
128
- if (data.result.totalInternalLoopCount > 2) {
129
- fnStats.callsWithCascadingRuns++;
130
- fnStats.totalInternalRerunsForCascading += data.result.totalInternalLoopCount - 1;
131
- if (data.result.totalInternalLoopCount > fnStats.maxInternalReruns) {
132
- fnStats.maxInternalReruns = data.result.totalInternalLoopCount;
133
- }
66
+ if (!fnStats) {
67
+ fnStats = createFunctionCallStats();
68
+ statsData.perFunctionStats[data.functionId] = fnStats;
134
69
  }
135
- if (data.result.outerLoopCount > 1) {
136
- fnStats.callsWithMultipleFullRuns++;
70
+
71
+ for (let stats of [statsData as FunctionCallStats, fnStats]) {
72
+ stats.totalCalls++;
73
+ stats.maxInternalReruns = Math.max(stats.maxInternalReruns, data.result.totalInternalLoopCount - 1);
74
+ stats.maxFullReruns = Math.max(stats.maxFullReruns, data.result.outerLoopCount - 1);
75
+ stats.maxEvalTime = Math.max(stats.maxEvalTime, data.result.evalTime);
76
+ stats.maxTotalTime = Math.max(stats.maxTotalTime, data.result.totalTime);
137
77
  }
138
78
 
139
79
  Querysub.commit(() => {
@@ -10,17 +10,22 @@ import { URLParam } from "../../../library-components/URLParam";
10
10
  import { mainResets } from "../../../library-components/urlResetGroups";
11
11
  import { ATag } from "../../../library-components/ATag";
12
12
  import { Button } from "../../../library-components/Button";
13
+ import { DropdownSelector } from "../../../library-components/DropdownSelector";
13
14
  import { InputLabel } from "../../../library-components/InputLabel";
14
15
  import { LogDatum } from "../diskLogger";
15
16
  import { managementPageURL, showingManagementURL } from "../../managementPages";
16
17
  import { TicketsController, watchTickets } from "./tickets";
17
- import { isTicketFinished, Ticket, TicketComment, TicketPatchFile, TicketState, TICKET_STATES } from "./ticketTypes";
18
+ import { AUTOFIXER_MODEL_OPTIONS, isTicketFinished, Ticket, TicketComment, TicketPatchFile, TicketState, TICKET_STATES } from "./ticketTypes";
18
19
 
19
20
  export const ticketIdURL = new URLParam("ticketid", "");
21
+ // Empty means claude's default model.
22
+ export const aiModelURL = new URLParam("aimodel", "");
20
23
  // "unfinished" hides tickets in a final state (fixed / not-a-bug). Resets when the page changes, so the filter doesn't confusingly stick around.
21
24
  export const ticketFilterURL = new URLParam("ticketfilter", "", { reset: [mainResets] });
22
25
 
23
26
  const TITLE_MAX_LENGTH = 200;
27
+ // Ticket change notifications already refresh the AI status on every AI step; the poll just catches transitions that don't touch the ticket (e.g. the AI server restarting) and keeps the elapsed time ticking.
28
+ const AI_STATUS_POLL_INTERVAL = 5000;
24
29
  const COMMENT_TEXTAREA_MIN_HEIGHT = 250;
25
30
  const COMMENT_TEXTAREA_COLLAPSED_HEIGHT = 40;
26
31
 
@@ -167,6 +172,7 @@ export class TicketPage extends qreact.Component {
167
172
  let controller = getController();
168
173
  controller.getTickets.refreshAll();
169
174
  controller.getTicket.refreshAll();
175
+ controller.getAIStatus.refreshAll();
170
176
  }).then(unwatch => {
171
177
  this.unwatch = unwatch;
172
178
  });
@@ -221,7 +227,7 @@ class TicketList extends qreact.Component {
221
227
  <div className={css.hbox(8).alignItems("center")}>
222
228
  <span>Run</span>
223
229
  <code className={css.pad2(8, 4).hsl(220, 15, 15).colorhsl(120, 60, 70).fontSize(13).borderRadius(3)}>yarn autofix</code>
224
- <span>to automatically investigate and fix open tickets.</span>
230
+ <span>to automatically investigate and fix open tickets, or open a ticket and use "Run AI Investigation" to run one on the server you are connected to.</span>
225
231
  </div>
226
232
  <div className={css.vbox(8).pad2(12).bord2(210, 50, 60).hsl(210, 50, 96).fillWidth}>
227
233
  <strong>Create Custom Ticket</strong>
@@ -324,7 +330,10 @@ class TicketDetail extends qreact.Component<{ ticketId: string }> {
324
330
  }
325
331
 
326
332
  let sortedComments = [...ticket.comments];
327
- sort(sortedComments, x => x.time);
333
+ sort(sortedComments, x => -x.time);
334
+ // Chronological, because patches on the same file build on each other in the order they were proposed.
335
+ let patchComments = ticket.comments.filter(c => c.kind === "patch" && c.patchFiles);
336
+ sort(patchComments, x => x.time);
328
337
  return <div className={css.vbox(16).pad2(16).fillBoth.maxWidth("100%").minHeight(0)}>
329
338
  <div className={css.hbox(16).alignItems("center")}>
330
339
  <ATag values={[ticketIdURL.getOverride("")]}>← All Tickets</ATag>
@@ -368,6 +377,8 @@ class TicketDetail extends qreact.Component<{ ticketId: string }> {
368
377
  </Button>
369
378
  </div>
370
379
 
380
+ <AIRunPanel ticketId={this.props.ticketId} />
381
+
371
382
  <div className={css.vbox(8).pad2(12).bord2(0, 50, 70).hsl(0, 30, 96).fillWidth}>
372
383
  <div
373
384
  className={css.hbox(8).button}
@@ -398,7 +409,24 @@ class TicketDetail extends qreact.Component<{ ticketId: string }> {
398
409
  </div>
399
410
 
400
411
  <div className={css.vbox(12).fillWidth.flexGrow(1).minHeight(0).overflowAuto}>
401
- <h3>Comments ({sortedComments.length})</h3>
412
+ <div className={css.hbox(16).alignItems("center")}>
413
+ <h3 className={css.margin(0)}>Comments ({sortedComments.length})</h3>
414
+ {patchComments.length > 0 && (
415
+ <Button
416
+ hue={120}
417
+ onClick={() => {
418
+ let ticketId = this.props.ticketId;
419
+ let commentIds = patchComments.map(c => c.id);
420
+ Querysub.onCommitFinished(async () => {
421
+ await getController().applyPatches.promise(ticketId, commentIds);
422
+ resetTicketData();
423
+ });
424
+ }}
425
+ >
426
+ Apply All Patches ({patchComments.length})
427
+ </Button>
428
+ )}
429
+ </div>
402
430
  {sortedComments.map(comment => (
403
431
  <TicketCommentItem key={comment.id} ticketId={this.props.ticketId} comment={comment} />
404
432
  ))}
@@ -444,6 +472,99 @@ class TicketDetail extends qreact.Component<{ ticketId: string }> {
444
472
  }
445
473
  }
446
474
 
475
+ // Controls the AI investigation running on the server this browser is connected to (in debug mode, the local dev server). Progress arrives live through the normal ticket change notifications, since every AI step is recorded as a ticket comment.
476
+ class AIRunPanel extends qreact.Component<{ ticketId: string }> {
477
+ pollInterval: ReturnType<typeof setInterval> | undefined = undefined;
478
+
479
+ componentDidMount() {
480
+ this.pollInterval = setInterval(() => {
481
+ getController().getAIStatus.refreshAll();
482
+ }, AI_STATUS_POLL_INTERVAL);
483
+ }
484
+
485
+ componentWillUnmount() {
486
+ if (this.pollInterval !== undefined) {
487
+ clearInterval(this.pollInterval);
488
+ }
489
+ }
490
+
491
+ render() {
492
+ let ticketId = this.props.ticketId;
493
+ let status = getController().getAIStatus();
494
+
495
+ let stop = () => {
496
+ Querysub.onCommitFinished(async () => {
497
+ await getController().stopAIInvestigation.promise(ticketId);
498
+ getController().getAIStatus.refreshAll();
499
+ });
500
+ };
501
+
502
+ let contents: qreact.VNode;
503
+ if (!status) {
504
+ contents = <span className={css.colorhsl(0, 0, 40)}>Checking AI status...</span>;
505
+ } else if (status.runningTicketId === ticketId) {
506
+ contents = <>
507
+ <span className={css.colorhsl(280, 70, 35).boldStyle}>
508
+ 🤖 Investigating this ticket for {formatTime(Date.now() - (status.runStartTime ?? Date.now()))}{status.runningModel && ` (${status.runningModel})` || ""}
509
+ </span>
510
+ {status.stopRequested && (
511
+ <span className={css.colorhsl(0, 70, 40).boldStyle}>Stopping after the current step...</span>
512
+ ) || (
513
+ <Button hue={0} onClick={stop}>
514
+ Stop (finishes current step)
515
+ </Button>
516
+ )}
517
+ </>;
518
+ } else if (status.queuedTicketIds.includes(ticketId)) {
519
+ contents = <>
520
+ <span className={css.colorhsl(280, 70, 35).boldStyle}>
521
+ Queued{status.runningTicketId && ` behind "${status.runningTicketTitle}"` || ""}
522
+ </span>
523
+ <Button hue={0} onClick={stop}>
524
+ Cancel
525
+ </Button>
526
+ </>;
527
+ } else {
528
+ contents = <>
529
+ <DropdownSelector
530
+ title="Model"
531
+ value={aiModelURL.value}
532
+ onChange={value => {
533
+ aiModelURL.value = value;
534
+ }}
535
+ options={[
536
+ { value: "", label: `Default${status.defaultModel && ` (${status.defaultModel})` || ""}` },
537
+ ...AUTOFIXER_MODEL_OPTIONS.map(m => ({ value: m, label: m })),
538
+ ]}
539
+ />
540
+ <Button
541
+ hue={280}
542
+ onClick={() => {
543
+ let model = aiModelURL.value || undefined;
544
+ Querysub.onCommitFinished(async () => {
545
+ await getController().startAIInvestigation.promise(ticketId, model);
546
+ getController().getAIStatus.refreshAll();
547
+ resetTicketData();
548
+ });
549
+ }}
550
+ >
551
+ Run AI Investigation
552
+ </Button>
553
+ {status.runningTicketId && (
554
+ <span className={css.colorhsl(0, 0, 40).fontSize(12)}>
555
+ The AI is currently working on "{status.runningTicketTitle}" — this run will queue behind it.
556
+ </span>
557
+ )}
558
+ </>;
559
+ }
560
+
561
+ return <div className={css.hbox(12).alignItems("center").wrap.pad2(12).bord2(280, 50, 60).hsl(280, 40, 96).fillWidth}>
562
+ <strong>AI Investigation</strong>
563
+ {contents}
564
+ </div>;
565
+ }
566
+ }
567
+
447
568
  // Pulls the interesting parts out of a searchLogs tool-call comment so they can be shown inline. startTime/endTime may be epoch ms or strings — either way the Date constructor parses them.
448
569
  function parseSearchLogsInfo(comment: TicketComment): {
449
570
  rangeText: string;
@@ -624,35 +745,33 @@ class TicketCommentItem extends qreact.Component<{
624
745
  <PatchFileView key={idx} patchFile={patchFile} />
625
746
  ))}
626
747
  <div className={css.hbox(12).alignItems("center")}>
748
+ <Button
749
+ hue={120}
750
+ onClick={() => {
751
+ let ticketId = this.props.ticketId;
752
+ let commentId = comment.id;
753
+ Querysub.onCommitFinished(async () => {
754
+ await getController().applyPatches.promise(ticketId, [commentId]);
755
+ resetTicketData();
756
+ });
757
+ }}
758
+ >
759
+ Apply Patch
760
+ </Button>
627
761
  {comment.patchStatus === "pending" && (
628
- <>
629
- <Button
630
- hue={120}
631
- onClick={() => {
632
- let ticketId = this.props.ticketId;
633
- let commentId = comment.id;
634
- Querysub.onCommitFinished(async () => {
635
- await getController().applyPatch.promise(ticketId, commentId);
636
- resetTicketData();
637
- });
638
- }}
639
- >
640
- Apply Patch
641
- </Button>
642
- <Button
643
- hue={0}
644
- onClick={() => {
645
- let ticketId = this.props.ticketId;
646
- let commentId = comment.id;
647
- Querysub.onCommitFinished(async () => {
648
- await getController().setPatchStatus.promise(ticketId, commentId, "rejected");
649
- resetTicketData();
650
- });
651
- }}
652
- >
653
- Reject Patch
654
- </Button>
655
- </>
762
+ <Button
763
+ hue={0}
764
+ onClick={() => {
765
+ let ticketId = this.props.ticketId;
766
+ let commentId = comment.id;
767
+ Querysub.onCommitFinished(async () => {
768
+ await getController().setPatchStatuses.promise(ticketId, [commentId], "rejected");
769
+ resetTicketData();
770
+ });
771
+ }}
772
+ >
773
+ Reject Patch
774
+ </Button>
656
775
  )}
657
776
  {comment.patchStatus === "applied" && (
658
777
  <span className={css.colorhsl(120, 80, 25).boldStyle}>