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 +1 -1
- package/src/0-path-value-core/PathRouter.ts +2 -2
- package/src/0-path-value-core/archiveLocks/ArchiveLocks2.ts +18 -16
- package/src/2-proxy/PathValueProxyWatcher.ts +1 -1
- package/src/5-diagnostics/Modal.tsx +10 -2
- package/src/diagnostics/FunctionCallInfoState.ts +29 -89
- package/src/diagnostics/logs/errorTickets/TicketPage.tsx +151 -32
- package/src/diagnostics/logs/errorTickets/autoFixer.ts +303 -57
- package/src/diagnostics/logs/errorTickets/autoFixerController.ts +4 -4
- package/src/diagnostics/logs/errorTickets/ticketTypes.ts +17 -0
- package/src/diagnostics/logs/errorTickets/tickets.ts +49 -24
- package/src/diagnostics/statsDefinitions.tsx +8 -19
- package/src/library-components/URLParam.ts +11 -3
package/package.json
CHANGED
|
@@ -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
|
|
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 =
|
|
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 (
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
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,
|
|
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.
|
|
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
|
|
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
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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
|
-
|
|
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
|
|
119
|
-
|
|
120
|
-
|
|
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
|
-
|
|
136
|
-
|
|
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
|
-
<
|
|
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
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
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}>
|