@payconductor/houndsman 0.7.0 → 0.8.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/README.md +20 -0
- package/package.json +1 -1
- package/src/cli/commands/command.ts +10 -1
- package/src/cli/commands/context-command.ts +40 -2
- package/src/cli/commands/hunt-command.ts +28 -2
- package/src/cli/commands/review-command.ts +30 -2
- package/src/cli/commands/worktree-command.ts +118 -0
- package/src/cli/help.ts +12 -2
- package/src/cli/index.ts +14 -4
- package/src/core/ast/service.ts +7 -2
- package/src/core/brief.ts +1 -0
- package/src/core/commands/context.ts +9 -1
- package/src/core/commands/findings.ts +11 -5
- package/src/core/commands/review.ts +13 -0
- package/src/core/commands/scaffold.ts +4 -2
- package/src/core/config.ts +13 -0
- package/src/core/defaults.ts +7 -0
- package/src/core/engine/houndsman-engine.ts +26 -3
- package/src/core/engine/review-scope.ts +17 -22
- package/src/core/engine/review-session.ts +25 -2
- package/src/core/engine/review-workspace.ts +204 -0
- package/src/core/jev/jev-detector.ts +10 -2
- package/src/core/orchestrator.ts +62 -13
- package/src/core/project-config.ts +12 -0
- package/src/core/services/git-repository.ts +46 -0
- package/src/core/services/pr-resolver.ts +147 -0
- package/src/core/services/worktree-manager.ts +269 -0
- package/src/core/storage/file/file-storage.ts +14 -1
- package/src/core/storage/paths.ts +30 -1
- package/src/core/storage/sqlite/mappers.ts +4 -0
- package/src/core/storage/sqlite/schema.ts +13 -3
- package/src/core/storage/sqlite/sqlite-storage.ts +28 -8
- package/src/core/storage/types.ts +12 -0
- package/src/core/storage/work-area.ts +20 -1
- package/src/core/types.ts +2 -0
- package/src/harness/hunt/hunt-options.ts +17 -0
- package/src/harness/hunt/hunt-session.ts +7 -20
- package/src/index.ts +31 -0
- package/src/server/client/App.tsx +49 -11
- package/src/server/client/DeleteRunModal.tsx +163 -0
- package/src/server/client/ResumePanel.tsx +8 -8
- package/src/server/client/Sidebar.tsx +56 -1
- package/src/server/client/icons.tsx +10 -0
- package/src/server/client/utils/types.ts +26 -0
- package/src/server/index.ts +97 -4
- package/src/server/pr-inbox.ts +15 -4
- package/src/server/run-repository.ts +103 -2
- package/src/skill/README.md +11 -3
- package/src/skill/SKILL.md +44 -10
- package/src/skill/hunt.md +33 -7
- package/src/tests/__snapshots__/baseline.test.ts.snap +94 -24
- package/src/tests/cli.test.ts +2 -1
- package/src/tests/server.test.ts +38 -0
- package/src/tests/worktree.test.ts +369 -0
|
@@ -33,7 +33,7 @@ import { migrate } from "./schema";
|
|
|
33
33
|
|
|
34
34
|
/** Colunas mutáveis de review (camel → snake). */
|
|
35
35
|
const REVIEW_PATCH_COLUMNS: Record<
|
|
36
|
-
Exclude<keyof ReviewPatch, "resetStartedAt" | "resetHeartbeat">,
|
|
36
|
+
Exclude<keyof ReviewPatch, "resetStartedAt" | "resetHeartbeat" | "resetWorkspace">,
|
|
37
37
|
string
|
|
38
38
|
> = {
|
|
39
39
|
branch: "branch",
|
|
@@ -43,6 +43,8 @@ const REVIEW_PATCH_COLUMNS: Record<
|
|
|
43
43
|
headSha: "head_sha",
|
|
44
44
|
reviewedSha: "reviewed_sha",
|
|
45
45
|
reviewedAt: "reviewed_at",
|
|
46
|
+
worktreePath: "worktree_path",
|
|
47
|
+
prNumber: "pr_number",
|
|
46
48
|
status: "status",
|
|
47
49
|
startedAt: "started_at",
|
|
48
50
|
heartbeatAt: "heartbeat_at",
|
|
@@ -70,13 +72,24 @@ export class SQLiteStorageProvider implements StorageProvider {
|
|
|
70
72
|
const existing = this.findReview(input.repo.key, input.branch);
|
|
71
73
|
if (existing) {
|
|
72
74
|
// O repo pode ter sido movido; a identidade continua a mesma.
|
|
73
|
-
|
|
75
|
+
const moved =
|
|
74
76
|
existing.repoRoot !== input.repo.root ||
|
|
75
|
-
existing.repoRemote !== input.repo.remote
|
|
76
|
-
|
|
77
|
+
existing.repoRemote !== input.repo.remote;
|
|
78
|
+
const workspace =
|
|
79
|
+
(input.worktreePath !== undefined &&
|
|
80
|
+
existing.worktreePath !== input.worktreePath) ||
|
|
81
|
+
(input.prNumber !== undefined && existing.prNumber !== input.prNumber);
|
|
82
|
+
if (moved || workspace) {
|
|
77
83
|
this.db.run(
|
|
78
|
-
|
|
79
|
-
|
|
84
|
+
`UPDATE reviews SET repo_root = ?, repo_remote = ?, worktree_path = ?,
|
|
85
|
+
pr_number = ? WHERE id = ?`,
|
|
86
|
+
[
|
|
87
|
+
input.repo.root,
|
|
88
|
+
input.repo.remote ?? null,
|
|
89
|
+
input.worktreePath ?? existing.worktreePath ?? null,
|
|
90
|
+
input.prNumber ?? existing.prNumber ?? null,
|
|
91
|
+
existing.id,
|
|
92
|
+
],
|
|
80
93
|
);
|
|
81
94
|
}
|
|
82
95
|
return this.requireReview(existing.id);
|
|
@@ -92,6 +105,8 @@ export class SQLiteStorageProvider implements StorageProvider {
|
|
|
92
105
|
slug: input.slug,
|
|
93
106
|
base: input.base,
|
|
94
107
|
head: input.head,
|
|
108
|
+
worktreePath: input.worktreePath,
|
|
109
|
+
prNumber: input.prNumber,
|
|
95
110
|
status: null,
|
|
96
111
|
createdAt: now,
|
|
97
112
|
updatedAt: now,
|
|
@@ -99,8 +114,8 @@ export class SQLiteStorageProvider implements StorageProvider {
|
|
|
99
114
|
this.db.run(
|
|
100
115
|
`INSERT INTO reviews
|
|
101
116
|
(id, repo_key, repo_root, repo_remote, branch, slug, base, head,
|
|
102
|
-
created_at, updated_at)
|
|
103
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
117
|
+
worktree_path, pr_number, created_at, updated_at)
|
|
118
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
104
119
|
[
|
|
105
120
|
record.id,
|
|
106
121
|
record.repoKey,
|
|
@@ -110,6 +125,8 @@ export class SQLiteStorageProvider implements StorageProvider {
|
|
|
110
125
|
record.slug,
|
|
111
126
|
record.base ?? null,
|
|
112
127
|
record.head ?? null,
|
|
128
|
+
record.worktreePath ?? null,
|
|
129
|
+
record.prNumber ?? null,
|
|
113
130
|
record.createdAt,
|
|
114
131
|
record.updatedAt,
|
|
115
132
|
],
|
|
@@ -163,6 +180,9 @@ export class SQLiteStorageProvider implements StorageProvider {
|
|
|
163
180
|
if (patch.resetHeartbeat) {
|
|
164
181
|
sets.push("heartbeat_at = NULL");
|
|
165
182
|
}
|
|
183
|
+
if (patch.resetWorkspace) {
|
|
184
|
+
sets.push("worktree_path = NULL", "pr_number = NULL");
|
|
185
|
+
}
|
|
166
186
|
if (patch.updatedAt === undefined) {
|
|
167
187
|
sets.push("updated_at = ?");
|
|
168
188
|
params.push(now);
|
|
@@ -34,6 +34,10 @@ export type ReviewRecord = {
|
|
|
34
34
|
headSha?: string;
|
|
35
35
|
reviewedSha?: string;
|
|
36
36
|
reviewedAt?: string;
|
|
37
|
+
/** Worktree onde a revisão roda (ausente = clone principal). */
|
|
38
|
+
worktreePath?: string;
|
|
39
|
+
/** Número da PR revisada (quando iniciada via `--pr`). */
|
|
40
|
+
prNumber?: number;
|
|
37
41
|
status: ReviewStatus | null;
|
|
38
42
|
startedAt?: string;
|
|
39
43
|
/**
|
|
@@ -56,6 +60,8 @@ export type ReviewPatch = Partial<
|
|
|
56
60
|
| "headSha"
|
|
57
61
|
| "reviewedSha"
|
|
58
62
|
| "reviewedAt"
|
|
63
|
+
| "worktreePath"
|
|
64
|
+
| "prNumber"
|
|
59
65
|
| "status"
|
|
60
66
|
| "startedAt"
|
|
61
67
|
| "heartbeatAt"
|
|
@@ -66,6 +72,8 @@ export type ReviewPatch = Partial<
|
|
|
66
72
|
resetStartedAt?: boolean;
|
|
67
73
|
/** Limpa o heartbeat (execução terminou). */
|
|
68
74
|
resetHeartbeat?: boolean;
|
|
75
|
+
/** Limpa `worktreePath`/`prNumber` (review voltou ao clone principal). */
|
|
76
|
+
resetWorkspace?: boolean;
|
|
69
77
|
};
|
|
70
78
|
|
|
71
79
|
/** Entrada de `getOrCreateReview`. */
|
|
@@ -75,6 +83,10 @@ export type CreateReviewInput = {
|
|
|
75
83
|
slug: string;
|
|
76
84
|
base?: string;
|
|
77
85
|
head?: string;
|
|
86
|
+
/** Worktree onde a revisão roda (ausente = clone principal). */
|
|
87
|
+
worktreePath?: string;
|
|
88
|
+
/** Número da PR revisada (quando iniciada via `--pr`). */
|
|
89
|
+
prNumber?: number;
|
|
78
90
|
now?: string;
|
|
79
91
|
};
|
|
80
92
|
|
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
CONTEXT_FILE,
|
|
15
15
|
CONTEXT_SUMMARY_FILE,
|
|
16
16
|
displayPath,
|
|
17
|
+
PROJECT_DIR,
|
|
17
18
|
} from "../paths";
|
|
18
19
|
import type { ContextPaths } from "../pr-context";
|
|
19
20
|
import {
|
|
@@ -35,12 +36,26 @@ export type FindingsIngest = {
|
|
|
35
36
|
warnings: string[];
|
|
36
37
|
};
|
|
37
38
|
|
|
39
|
+
/**
|
|
40
|
+
* Raiz do staging da review.
|
|
41
|
+
*
|
|
42
|
+
* Com worktree, tudo (briefs/findings/contexto) fica dentro dele — assim os
|
|
43
|
+
* agentes leem e escrevem dentro do workspace aberto no editor, sem prompts de
|
|
44
|
+
* permissão. Sem worktree, mantém o diretório global do houndsman.
|
|
45
|
+
*/
|
|
46
|
+
export function reviewWorkBase(review: ReviewRecord): string {
|
|
47
|
+
const worktree = review.worktreePath;
|
|
48
|
+
return worktree && existsSync(worktree)
|
|
49
|
+
? join(worktree, PROJECT_DIR, "work")
|
|
50
|
+
: workRoot();
|
|
51
|
+
}
|
|
52
|
+
|
|
38
53
|
/** Staging de uma review (briefs/findings/contexto). */
|
|
39
54
|
export class ReviewWorkArea {
|
|
40
55
|
readonly review: ReviewRecord;
|
|
41
56
|
readonly root: string;
|
|
42
57
|
|
|
43
|
-
constructor(review: ReviewRecord, base: string =
|
|
58
|
+
constructor(review: ReviewRecord, base: string = reviewWorkBase(review)) {
|
|
44
59
|
this.review = review;
|
|
45
60
|
this.root = join(base, review.id);
|
|
46
61
|
}
|
|
@@ -86,6 +101,10 @@ export class ReviewWorkArea {
|
|
|
86
101
|
/** Garante os diretórios; `clean` recria a área do zero. */
|
|
87
102
|
ensure(clean = false): void {
|
|
88
103
|
if (clean) this.clean();
|
|
104
|
+
mkdirSync(this.root, { recursive: true });
|
|
105
|
+
// Staging nunca deve sujar `git status` (especialmente dentro do worktree).
|
|
106
|
+
const ignore = join(this.root, ".gitignore");
|
|
107
|
+
if (!existsSync(ignore)) writeFileSync(ignore, "*\n");
|
|
89
108
|
mkdirSync(this.briefsDir, { recursive: true });
|
|
90
109
|
mkdirSync(this.findingsDir, { recursive: true });
|
|
91
110
|
}
|
package/src/core/types.ts
CHANGED
|
@@ -135,6 +135,8 @@ export type WatchdogRenderArgs = {
|
|
|
135
135
|
branch: string;
|
|
136
136
|
outputPath: string;
|
|
137
137
|
outputFormatPath: string;
|
|
138
|
+
/** Worktree onde a review roda (ausente = clone principal). */
|
|
139
|
+
worktree?: string;
|
|
138
140
|
docs: readonly string[];
|
|
139
141
|
context?: WatchdogRenderContext | null;
|
|
140
142
|
candidates?: readonly WatchdogRenderCandidate[];
|
|
@@ -24,6 +24,12 @@ export type HuntOptions = {
|
|
|
24
24
|
concurrency?: number;
|
|
25
25
|
provider?: AiProviderKind;
|
|
26
26
|
model?: string;
|
|
27
|
+
/** Número da PR (`--pr`) — o comando resolve/cria o worktree. */
|
|
28
|
+
pr?: number;
|
|
29
|
+
/** Worktree informado via flag (relativo ao cwd ou absoluto). */
|
|
30
|
+
worktree?: string;
|
|
31
|
+
/** Worktree resolvido (absoluto) para persistir na review. */
|
|
32
|
+
worktreePath?: string;
|
|
27
33
|
};
|
|
28
34
|
|
|
29
35
|
/** Defaults do hunt. */
|
|
@@ -54,6 +60,17 @@ export function parseHuntArgs(args: string[]): HuntOptions {
|
|
|
54
60
|
const model = parser.value("--model");
|
|
55
61
|
const maxCalls = parser.value("--max-calls");
|
|
56
62
|
const concurrency = parser.value("--concurrency");
|
|
63
|
+
const pr = parser.value("--pr");
|
|
64
|
+
const worktree = parser.value("--worktree");
|
|
65
|
+
if (pr) {
|
|
66
|
+
const number = Number.parseInt(pr, 10);
|
|
67
|
+
if (Number.isFinite(number) && number > 0) options.pr = number;
|
|
68
|
+
else throw new Error(`--pr inválido: '${pr}'.`);
|
|
69
|
+
}
|
|
70
|
+
if (worktree) options.worktree = worktree;
|
|
71
|
+
if (options.pr !== undefined && options.worktree !== undefined) {
|
|
72
|
+
throw new Error("Use --pr ou --worktree, não os dois.");
|
|
73
|
+
}
|
|
57
74
|
if (base) options.base = base;
|
|
58
75
|
if (head) options.head = head;
|
|
59
76
|
if (branch) options.branch = branch;
|
|
@@ -14,11 +14,9 @@ import {
|
|
|
14
14
|
type ModelProvider,
|
|
15
15
|
} from "../../core/ai";
|
|
16
16
|
import type { AstAnalysis } from "../../core/ast/types";
|
|
17
|
-
import { collectDiff } from "../../core/diff-source";
|
|
18
17
|
import type { HoundsmanEngine } from "../../core/engine/houndsman-engine";
|
|
19
18
|
import type { RunClassification } from "../../core/engine/review-scope";
|
|
20
19
|
import { parseEntries } from "../../core/file-classifier";
|
|
21
|
-
import { runGit, tryGit } from "../../core/git-runner";
|
|
22
20
|
import type { LoggerLike } from "../../core/logging";
|
|
23
21
|
import type { PrepareResult } from "../../core/orchestrator";
|
|
24
22
|
import { defaultOptions, type ReviewOptions } from "../../core/orchestrator";
|
|
@@ -67,6 +65,8 @@ export class HuntSession {
|
|
|
67
65
|
clean: options.clean,
|
|
68
66
|
noAi: true,
|
|
69
67
|
finalize: false,
|
|
68
|
+
pr: options.pr,
|
|
69
|
+
worktreePath: options.worktreePath,
|
|
70
70
|
};
|
|
71
71
|
|
|
72
72
|
const session = engine.session(reviewOptions);
|
|
@@ -137,7 +137,7 @@ export class HuntSession {
|
|
|
137
137
|
reporter,
|
|
138
138
|
config: this.deps.config,
|
|
139
139
|
ast: engine.ast,
|
|
140
|
-
readFile: defaultFileReader(
|
|
140
|
+
readFile: defaultFileReader(engine.root),
|
|
141
141
|
});
|
|
142
142
|
const runner = new AgentRunner(factory);
|
|
143
143
|
const scheduler = new AgentScheduler(
|
|
@@ -265,11 +265,11 @@ export class HuntSession {
|
|
|
265
265
|
work: import("../../core/storage/work-area").ReviewWorkArea,
|
|
266
266
|
) {
|
|
267
267
|
const { engine, options } = this.deps;
|
|
268
|
-
const diff = collectDiff(options.base, options.head);
|
|
268
|
+
const diff = engine.git.collectDiff(options.base, options.head);
|
|
269
269
|
const entries = parseEntries(
|
|
270
|
-
|
|
270
|
+
engine.git.diffNameStatus(options.base, options.head),
|
|
271
271
|
);
|
|
272
|
-
const baseRef = mergeBase(options.base, options.head) ?? options.base;
|
|
272
|
+
const baseRef = engine.git.mergeBase(options.base, options.head) ?? options.base;
|
|
273
273
|
const ast: Map<string, AstAnalysis> = options.noAst
|
|
274
274
|
? new Map()
|
|
275
275
|
: await engine.ast.analyze(entries, diff, baseRef);
|
|
@@ -283,7 +283,7 @@ export class HuntSession {
|
|
|
283
283
|
context,
|
|
284
284
|
ast,
|
|
285
285
|
impact,
|
|
286
|
-
repoRoot:
|
|
286
|
+
repoRoot: engine.root,
|
|
287
287
|
diff,
|
|
288
288
|
branch: review.branch,
|
|
289
289
|
watchdog: options.watchdog,
|
|
@@ -357,16 +357,3 @@ function readContextSummary(
|
|
|
357
357
|
return null;
|
|
358
358
|
}
|
|
359
359
|
}
|
|
360
|
-
|
|
361
|
-
/** Merge-base best effort. */
|
|
362
|
-
function mergeBase(base: string, head: string): string | null {
|
|
363
|
-
const result = tryGit(["merge-base", base, head]);
|
|
364
|
-
if (!result.ok) return null;
|
|
365
|
-
const sha = result.stdout.trim();
|
|
366
|
-
return sha.length > 0 ? sha : null;
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
/** Raiz do repositório para leitura das tools. */
|
|
370
|
-
function fileRoot(): string {
|
|
371
|
-
return process.cwd();
|
|
372
|
-
}
|
package/src/index.ts
CHANGED
|
@@ -89,6 +89,14 @@ export { buildCarryover, mergeCarryover } from "./core/engine/carryover";
|
|
|
89
89
|
export { HoundsmanEngine } from "./core/engine/houndsman-engine";
|
|
90
90
|
export { ReviewScopeResolver } from "./core/engine/review-scope";
|
|
91
91
|
export { ReviewSession } from "./core/engine/review-session";
|
|
92
|
+
export type {
|
|
93
|
+
ResolvedWorkspace,
|
|
94
|
+
WorkspaceRequest,
|
|
95
|
+
} from "./core/engine/review-workspace";
|
|
96
|
+
export {
|
|
97
|
+
mainRootOf,
|
|
98
|
+
resolveReviewWorkspace,
|
|
99
|
+
} from "./core/engine/review-workspace";
|
|
92
100
|
export type { HoundsmanEnv } from "./core/env";
|
|
93
101
|
export { ENV_NAMES, env, readEnv } from "./core/env";
|
|
94
102
|
export {
|
|
@@ -171,8 +179,26 @@ export { FixBatchBuilder } from "./core/services/fix-batch-builder";
|
|
|
171
179
|
export type { GhResult, GhRunner } from "./core/services/gh-client";
|
|
172
180
|
export { GhClient } from "./core/services/gh-client";
|
|
173
181
|
export { GitRepository } from "./core/services/git-repository";
|
|
182
|
+
export type {
|
|
183
|
+
PrRefs,
|
|
184
|
+
PrResolveResult,
|
|
185
|
+
PullRequestInfo,
|
|
186
|
+
} from "./core/services/pr-resolver";
|
|
187
|
+
export { PrResolver } from "./core/services/pr-resolver";
|
|
174
188
|
export { ProjectFiles } from "./core/services/project-files";
|
|
175
189
|
export { Verifier } from "./core/services/verifier";
|
|
190
|
+
export type {
|
|
191
|
+
EnsureWorktreeInput,
|
|
192
|
+
EnsureWorktreeResult,
|
|
193
|
+
RemoveWorktreeResult,
|
|
194
|
+
WorktreeEntry,
|
|
195
|
+
WorktreeManagerOptions,
|
|
196
|
+
} from "./core/services/worktree-manager";
|
|
197
|
+
export {
|
|
198
|
+
parseWorktreeList,
|
|
199
|
+
repoSlug,
|
|
200
|
+
WorktreeManager,
|
|
201
|
+
} from "./core/services/worktree-manager";
|
|
176
202
|
export {
|
|
177
203
|
createMemoryStorage,
|
|
178
204
|
createStorage,
|
|
@@ -226,4 +252,9 @@ export {
|
|
|
226
252
|
} from "./core/watchdog-registry";
|
|
227
253
|
export type { ServerOptions } from "./server";
|
|
228
254
|
export { createRequestHandler, HoundsmanServer, startServer } from "./server";
|
|
255
|
+
export type {
|
|
256
|
+
DeleteRunResult,
|
|
257
|
+
RunDetail,
|
|
258
|
+
RunListItem,
|
|
259
|
+
} from "./server/run-repository";
|
|
229
260
|
export { RunRepository } from "./server/run-repository";
|
|
@@ -8,6 +8,7 @@ import { useCallback, useEffect, useMemo, useRef, useState } from "preact/hooks"
|
|
|
8
8
|
import { ArtifactsPanel } from "./ArtifactsPanel";
|
|
9
9
|
import { AutoRefresh } from "./AutoRefresh";
|
|
10
10
|
import { CommentsPanel } from "./CommentsPanel";
|
|
11
|
+
import { DeleteRunModal } from "./DeleteRunModal";
|
|
11
12
|
import { Filters, type FiltersState } from "./Filters";
|
|
12
13
|
import { FindingsList } from "./FindingsList";
|
|
13
14
|
import { FileText, Layers, ListIcon, MessageSquare, Refresh, Send } from "./icons";
|
|
@@ -17,7 +18,13 @@ import { SubmitModal } from "./SubmitModal";
|
|
|
17
18
|
import { SummaryBar } from "./SummaryBar";
|
|
18
19
|
import { api, computeSummary } from "./utils/api";
|
|
19
20
|
import { AUTO_REFRESH_MS, TAB_CAPTIONS } from "./utils/constants";
|
|
20
|
-
import type {
|
|
21
|
+
import type {
|
|
22
|
+
DeleteRunResponse,
|
|
23
|
+
EditorContext,
|
|
24
|
+
Finding,
|
|
25
|
+
FindingsResponse,
|
|
26
|
+
RunSummary,
|
|
27
|
+
} from "./utils/types";
|
|
21
28
|
|
|
22
29
|
type Tab = "resume" | "findings" | "artifacts" | "comments";
|
|
23
30
|
|
|
@@ -67,6 +74,7 @@ export function App() {
|
|
|
67
74
|
const [refreshing, setRefreshing] = useState(false);
|
|
68
75
|
const [syncing, setSyncing] = useState(false);
|
|
69
76
|
const [submitOpen, setSubmitOpen] = useState(false);
|
|
77
|
+
const [deleteOpen, setDeleteOpen] = useState(false);
|
|
70
78
|
/** Editor detectado pelo servidor — base dos links de código. */
|
|
71
79
|
const [editor, setEditor] = useState<EditorContext>({ kind: "vscode" });
|
|
72
80
|
/** Refs espelhando estado volátil — o polling não deve recriar o intervalo. */
|
|
@@ -212,8 +220,28 @@ export function App() {
|
|
|
212
220
|
|
|
213
221
|
const summary = useMemo(() => computeSummary(data?.findings ?? []), [data]);
|
|
214
222
|
|
|
215
|
-
const
|
|
216
|
-
|
|
223
|
+
const selectedRun = runs.find((run) => run.slug === slug) ?? null;
|
|
224
|
+
const branch = data?.branch ?? selectedRun?.branch ?? slug ?? "";
|
|
225
|
+
|
|
226
|
+
/** Após excluir: limpa a seleção e escolhe a próxima run da lista. */
|
|
227
|
+
const afterDelete = useCallback((result: DeleteRunResponse) => {
|
|
228
|
+
setDeleteOpen(false);
|
|
229
|
+
setData(null);
|
|
230
|
+
setSlug(null);
|
|
231
|
+
if (result.warning) {
|
|
232
|
+
setError(
|
|
233
|
+
`Execução excluída, mas o worktree não foi removido: ${result.warning}`,
|
|
234
|
+
);
|
|
235
|
+
} else {
|
|
236
|
+
setError("");
|
|
237
|
+
}
|
|
238
|
+
api<{ runs: RunSummary[] }>("/api/runs")
|
|
239
|
+
.then(({ runs: list }) => {
|
|
240
|
+
setRuns(list);
|
|
241
|
+
setSlug(list[0]?.slug ?? null);
|
|
242
|
+
})
|
|
243
|
+
.catch((err) => setError(String(err)));
|
|
244
|
+
}, []);
|
|
217
245
|
|
|
218
246
|
return (
|
|
219
247
|
<div class="app">
|
|
@@ -290,7 +318,7 @@ export function App() {
|
|
|
290
318
|
branch={branch}
|
|
291
319
|
findings={data.findings}
|
|
292
320
|
summary={summary}
|
|
293
|
-
|
|
321
|
+
onRequestDelete={() => setDeleteOpen(true)}
|
|
294
322
|
/>
|
|
295
323
|
)}
|
|
296
324
|
{slug && tab === "findings" && data && (
|
|
@@ -312,13 +340,23 @@ export function App() {
|
|
|
312
340
|
{!slug && !error && <p class="empty">Nenhuma execução para triar.</p>}
|
|
313
341
|
</div>
|
|
314
342
|
{slug && (
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
343
|
+
<>
|
|
344
|
+
<SubmitModal
|
|
345
|
+
slug={slug}
|
|
346
|
+
branch={branch}
|
|
347
|
+
open={submitOpen}
|
|
348
|
+
onClose={() => setSubmitOpen(false)}
|
|
349
|
+
onSubmitted={afterSubmit}
|
|
350
|
+
/>
|
|
351
|
+
<DeleteRunModal
|
|
352
|
+
slug={slug}
|
|
353
|
+
branch={branch}
|
|
354
|
+
worktreePath={selectedRun?.worktreePath}
|
|
355
|
+
open={deleteOpen}
|
|
356
|
+
onClose={() => setDeleteOpen(false)}
|
|
357
|
+
onDeleted={afterDelete}
|
|
358
|
+
/>
|
|
359
|
+
</>
|
|
322
360
|
)}
|
|
323
361
|
</main>
|
|
324
362
|
</div>
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Modal "Excluir execução": confirma a remoção definitiva da run.
|
|
3
|
+
*
|
|
4
|
+
* Apaga a review, findings, histórico de triagem, artefatos e o staging local.
|
|
5
|
+
* Quando a run tem worktree, oferece removê-lo junto (alterações não commitadas
|
|
6
|
+
* nele são descartadas).
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { useEffect, useState } from "preact/hooks";
|
|
10
|
+
import { Spinner, Trash } from "./icons";
|
|
11
|
+
import { api } from "./utils/api";
|
|
12
|
+
import type { DeleteRunResponse } from "./utils/types";
|
|
13
|
+
|
|
14
|
+
export function DeleteRunModal({
|
|
15
|
+
slug,
|
|
16
|
+
branch,
|
|
17
|
+
worktreePath,
|
|
18
|
+
open,
|
|
19
|
+
onClose,
|
|
20
|
+
onDeleted,
|
|
21
|
+
}: {
|
|
22
|
+
slug: string;
|
|
23
|
+
branch: string;
|
|
24
|
+
worktreePath?: string;
|
|
25
|
+
open: boolean;
|
|
26
|
+
onClose: () => void;
|
|
27
|
+
onDeleted: (result: DeleteRunResponse) => void;
|
|
28
|
+
}) {
|
|
29
|
+
const [removeWorktree, setRemoveWorktree] = useState(Boolean(worktreePath));
|
|
30
|
+
const [deleting, setDeleting] = useState(false);
|
|
31
|
+
const [error, setError] = useState("");
|
|
32
|
+
|
|
33
|
+
useEffect(() => {
|
|
34
|
+
if (!open) return;
|
|
35
|
+
setRemoveWorktree(Boolean(worktreePath));
|
|
36
|
+
setDeleting(false);
|
|
37
|
+
setError("");
|
|
38
|
+
}, [open, worktreePath]);
|
|
39
|
+
|
|
40
|
+
useEffect(() => {
|
|
41
|
+
if (!open) return;
|
|
42
|
+
const onKey = (event: KeyboardEvent) => {
|
|
43
|
+
if (event.key === "Escape" && !deleting) onClose();
|
|
44
|
+
};
|
|
45
|
+
document.addEventListener("keydown", onKey);
|
|
46
|
+
return () => document.removeEventListener("keydown", onKey);
|
|
47
|
+
}, [open, deleting, onClose]);
|
|
48
|
+
|
|
49
|
+
if (!open) return null;
|
|
50
|
+
|
|
51
|
+
const confirm = async () => {
|
|
52
|
+
setDeleting(true);
|
|
53
|
+
setError("");
|
|
54
|
+
try {
|
|
55
|
+
const suffix = removeWorktree && worktreePath ? "?worktree=1" : "";
|
|
56
|
+
const result = await api<DeleteRunResponse>(`/api/runs/${slug}${suffix}`, {
|
|
57
|
+
method: "DELETE",
|
|
58
|
+
});
|
|
59
|
+
if (!result.deleted) throw new Error("A execução não foi encontrada.");
|
|
60
|
+
onDeleted(result);
|
|
61
|
+
} catch (err) {
|
|
62
|
+
setError(err instanceof Error ? err.message : String(err));
|
|
63
|
+
} finally {
|
|
64
|
+
setDeleting(false);
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
return (
|
|
69
|
+
<div class="modal-layer">
|
|
70
|
+
<button
|
|
71
|
+
type="button"
|
|
72
|
+
class="modal-backdrop"
|
|
73
|
+
aria-label="Fechar"
|
|
74
|
+
title="Fechar"
|
|
75
|
+
onClick={() => {
|
|
76
|
+
if (!deleting) onClose();
|
|
77
|
+
}}
|
|
78
|
+
/>
|
|
79
|
+
<div
|
|
80
|
+
class="modal"
|
|
81
|
+
role="dialog"
|
|
82
|
+
aria-modal="true"
|
|
83
|
+
aria-label="Excluir execução"
|
|
84
|
+
>
|
|
85
|
+
<header class="modal-head">
|
|
86
|
+
<div>
|
|
87
|
+
<h2 class="modal-title">Excluir execução</h2>
|
|
88
|
+
<p class="modal-sub">
|
|
89
|
+
Esta ação é permanente e apaga a revisão de{" "}
|
|
90
|
+
<code>{branch}</code>.
|
|
91
|
+
</p>
|
|
92
|
+
</div>
|
|
93
|
+
<button
|
|
94
|
+
type="button"
|
|
95
|
+
class="modal-close"
|
|
96
|
+
title="Fechar"
|
|
97
|
+
disabled={deleting}
|
|
98
|
+
onClick={onClose}
|
|
99
|
+
>
|
|
100
|
+
×
|
|
101
|
+
</button>
|
|
102
|
+
</header>
|
|
103
|
+
|
|
104
|
+
<div class="modal-body">
|
|
105
|
+
<p class="modal-warn">
|
|
106
|
+
Serão removidos os findings, o histórico de triagem, todos os
|
|
107
|
+
artefatos (briefs, classificação, manifesto, relatório, verify) e
|
|
108
|
+
o staging local desta run. O envio já feito ao GitHub não é
|
|
109
|
+
desfeito.
|
|
110
|
+
</p>
|
|
111
|
+
|
|
112
|
+
{worktreePath && (
|
|
113
|
+
<label class="modal-check">
|
|
114
|
+
<input
|
|
115
|
+
type="checkbox"
|
|
116
|
+
checked={removeWorktree}
|
|
117
|
+
disabled={deleting}
|
|
118
|
+
onChange={(event) =>
|
|
119
|
+
setRemoveWorktree(event.currentTarget.checked)
|
|
120
|
+
}
|
|
121
|
+
/>
|
|
122
|
+
<span>
|
|
123
|
+
Excluir também o worktree desta execução. Alterações não
|
|
124
|
+
commitadas nele serão perdidas.
|
|
125
|
+
<br />
|
|
126
|
+
<code>{worktreePath}</code>
|
|
127
|
+
</span>
|
|
128
|
+
</label>
|
|
129
|
+
)}
|
|
130
|
+
|
|
131
|
+
{error && <p class="error">{error}</p>}
|
|
132
|
+
</div>
|
|
133
|
+
|
|
134
|
+
<footer class="modal-foot">
|
|
135
|
+
<span class="submit-label">Run: {branch}</span>
|
|
136
|
+
<div class="submit-actions">
|
|
137
|
+
<button
|
|
138
|
+
type="button"
|
|
139
|
+
class="btn"
|
|
140
|
+
disabled={deleting}
|
|
141
|
+
onClick={onClose}
|
|
142
|
+
>
|
|
143
|
+
Cancelar
|
|
144
|
+
</button>
|
|
145
|
+
<button
|
|
146
|
+
type="button"
|
|
147
|
+
class="btn danger"
|
|
148
|
+
disabled={deleting}
|
|
149
|
+
onClick={() => void confirm()}
|
|
150
|
+
>
|
|
151
|
+
{deleting ? (
|
|
152
|
+
<Spinner size={15} class="icon spin" />
|
|
153
|
+
) : (
|
|
154
|
+
<Trash size={15} />
|
|
155
|
+
)}
|
|
156
|
+
{deleting ? "Excluindo…" : "Excluir execução"}
|
|
157
|
+
</button>
|
|
158
|
+
</div>
|
|
159
|
+
</footer>
|
|
160
|
+
</div>
|
|
161
|
+
</div>
|
|
162
|
+
);
|
|
163
|
+
}
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
|
|
11
11
|
import type { ComponentChildren } from "preact";
|
|
12
12
|
import { useEffect, useMemo, useState } from "preact/hooks";
|
|
13
|
-
import { ExternalLink, Layers } from "./icons";
|
|
13
|
+
import { ExternalLink, Layers, Trash } from "./icons";
|
|
14
14
|
import { MarkdownBlocks } from "./MarkdownText";
|
|
15
15
|
import { api } from "./utils/api";
|
|
16
16
|
import { SEVERITY_LABELS, SEVERITY_ORDER } from "./utils/constants";
|
|
@@ -79,13 +79,13 @@ export function ResumePanel({
|
|
|
79
79
|
branch,
|
|
80
80
|
findings,
|
|
81
81
|
summary,
|
|
82
|
-
|
|
82
|
+
onRequestDelete,
|
|
83
83
|
}: {
|
|
84
84
|
slug: string;
|
|
85
85
|
branch: string;
|
|
86
86
|
findings: Finding[];
|
|
87
87
|
summary: Summary;
|
|
88
|
-
|
|
88
|
+
onRequestDelete: () => void;
|
|
89
89
|
}) {
|
|
90
90
|
const [resume, setResume] = useState("");
|
|
91
91
|
const [detail, setDetail] = useState<RunDetail | null>(null);
|
|
@@ -241,12 +241,12 @@ export function ResumePanel({
|
|
|
241
241
|
<span class={`resume-status ${status.tone}`}>{status.label}</span>
|
|
242
242
|
<button
|
|
243
243
|
type="button"
|
|
244
|
-
class="btn"
|
|
245
|
-
title="
|
|
246
|
-
onClick={
|
|
244
|
+
class="btn danger"
|
|
245
|
+
title="Excluir esta execução, seus findings e artefatos locais"
|
|
246
|
+
onClick={onRequestDelete}
|
|
247
247
|
>
|
|
248
|
-
<
|
|
249
|
-
|
|
248
|
+
<Trash size={14} />
|
|
249
|
+
Excluir
|
|
250
250
|
</button>
|
|
251
251
|
</div>
|
|
252
252
|
</section>
|