@tequila99/githuman 0.2.1 → 0.2.3

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.
Files changed (65) hide show
  1. package/README.md +1 -13
  2. package/dist/cli/commands/export.js +2 -2
  3. package/dist/cli/index.js +9 -4
  4. package/dist/server/app-version.js +11 -0
  5. package/dist/server/app.js +8 -2
  6. package/dist/server/routes/app-info.js +6 -0
  7. package/dist/server/routes/comments.js +8 -5
  8. package/dist/server/routes/events.js +3 -2
  9. package/dist/server/routes/export.js +2 -2
  10. package/dist/server/services/export.service.js +207 -26
  11. package/dist/server/services/git.service.js +3 -0
  12. package/dist/shared/types.js +14 -1
  13. package/dist/web/assets/AppPage-B9tWPrzn.js +1 -0
  14. package/dist/web/assets/CreateReviewFabButton-DcXpOlBP.js +1 -0
  15. package/dist/web/assets/{ErrorNotFound-DMYc7a3H.js → ErrorNotFound-BuL12Ld8.js} +1 -1
  16. package/dist/web/assets/{IndexPage-BQ0T9rLY.css → IndexPage-CsgdLXZh.css} +1 -1
  17. package/dist/web/assets/IndexPage-y9DcUqW9.js +1 -0
  18. package/dist/web/assets/MainLayout-BRPoh8y0.css +1 -0
  19. package/dist/web/assets/MainLayout-fQytypS5.js +1 -0
  20. package/dist/web/assets/{Platform-CEDL_PgC.js → Platform-DDGYwfzl.js} +1 -1
  21. package/dist/web/assets/QBtn-DGiC5uLo.js +1 -0
  22. package/dist/web/assets/QTooltip-CPN6UvuX.js +1 -0
  23. package/dist/web/assets/ReviewDetailPage-CL6A5zjH.css +1 -0
  24. package/dist/web/assets/ReviewDetailPage-aMMuYtax.js +1 -0
  25. package/dist/web/assets/ReviewsPage-B1CTHU13.js +1 -0
  26. package/dist/web/assets/ReviewsPage-Lb74-4L2.css +1 -0
  27. package/dist/web/assets/StatusSwitcher-Bj-XCwSr.js +3 -0
  28. package/dist/web/assets/StatusSwitcher-BydQq1LI.css +1 -0
  29. package/dist/web/assets/app-info-store-Dw8pPU-w.js +1 -0
  30. package/dist/web/assets/{event-BQt_DXaR.js → event-D0kaqRwq.js} +1 -1
  31. package/dist/web/assets/format-Bb6ahGqi.js +1 -0
  32. package/dist/web/assets/i18n-Dw0eMaZf.js +1 -0
  33. package/dist/web/assets/{index-Dl3zmXcR.js → index-i1Dlwasy.js} +2 -2
  34. package/dist/web/assets/init-app-CPTVCTFc.js +1 -0
  35. package/dist/web/assets/pinia-ClnoFqXt.js +1 -0
  36. package/dist/web/assets/{pinia-DK4OjQ7o.js → pinia-jMX0pzvu.js} +1 -1
  37. package/dist/web/assets/runtime-core.esm-bundler-Db5Q9Gsb.js +1 -0
  38. package/dist/web/assets/theme-CgDsrcvS.js +1 -0
  39. package/dist/web/assets/{use-app-theme-DvzGyhQd.js → use-app-theme-D0Pl-VfI.js} +1 -1
  40. package/dist/web/assets/use-server-events-pJWjsMIC.js +1 -0
  41. package/dist/web/assets/{vue-i18n.runtime-DSeWNwrW.js → vue-i18n.runtime-CxBM6QPV.js} +2 -2
  42. package/dist/web/index.html +5 -5
  43. package/package.json +9 -8
  44. package/dist/web/assets/AppPage-VD8mwL62.js +0 -1
  45. package/dist/web/assets/CreateReviewFabButton-B4Ut5O1Z.js +0 -1
  46. package/dist/web/assets/IndexPage-_YmeSrRQ.js +0 -1
  47. package/dist/web/assets/MainLayout-CC5JJvA9.css +0 -1
  48. package/dist/web/assets/MainLayout-D2-McXhW.js +0 -1
  49. package/dist/web/assets/QBtn-Cf6WtiKt.js +0 -1
  50. package/dist/web/assets/QItemSection-DA4jtl1i.js +0 -1
  51. package/dist/web/assets/QList-BeOKfebe.js +0 -1
  52. package/dist/web/assets/QTooltip-DAb5vBzv.js +0 -1
  53. package/dist/web/assets/ReviewDetailPage-BlTGnC8d.js +0 -1
  54. package/dist/web/assets/ReviewDetailPage-DJHzinT1.css +0 -1
  55. package/dist/web/assets/ReviewsPage-Cu0NzNHQ.js +0 -1
  56. package/dist/web/assets/ReviewsPage-D9TFy2OP.css +0 -1
  57. package/dist/web/assets/StatusSwitcher-BQBbWZPW.js +0 -3
  58. package/dist/web/assets/StatusSwitcher-CZVJtHnr.css +0 -1
  59. package/dist/web/assets/format-B_1hRYcb.js +0 -1
  60. package/dist/web/assets/i18n-D0uDA2bL.js +0 -1
  61. package/dist/web/assets/init-app-DDJ8U-8D.js +0 -1
  62. package/dist/web/assets/pinia-C3vnQv9G.js +0 -1
  63. package/dist/web/assets/repository-store-CBxek7MZ.js +0 -1
  64. package/dist/web/assets/runtime-core.esm-bundler-Wxr8ezGp.js +0 -1
  65. package/dist/web/assets/theme-CQChVMvK.js +0 -1
package/README.md CHANGED
@@ -18,23 +18,11 @@ staged/unstaged diff, сравнение веток и коммитов, бра
18
18
  синтаксиса (shiki).
19
19
  - Live-обновления через SSE: интерфейс сам обновляется при изменении
20
20
  файлов в репозитории — удобно смотреть, как редактирует код AI-агент.
21
+ После перезапуска `serve` уже открытые вкладки перезагружаются сами.
21
22
  - Ручной/авто выбор тёмной-светлой темы.
22
23
  - CLI: `serve` (поднять сервер), `list`/`export` (быстрый доступ к
23
24
  сохранённым ревью без веб-интерфейса).
24
25
 
25
- ## Известное ограничение (WIP)
26
-
27
- Бэкенд полностью поддерживает модель "ревью" — создание ревью
28
- (staged/branch/commits), инлайн-комментарии, статусы
29
- (`in_progress`/`approved`/`changes_requested`), экспорт в Markdown/JSON — всё
30
- это покрыто тестами (`src/server/routes/reviews.ts`, `comments.ts`,
31
- `export.ts`). **Соответствующий экран во фронтенде пока не реализован** —
32
- `ReviewsPage.vue` сейчас заглушка. Работает только просмотр diff/файлов;
33
- создание ревью и комментирование через UI недоступно. Это следующий
34
- логический кусок работы, не забытая функциональность — трекается в
35
- `.claude/docs/adr/0014-quasar-frontend-migration.md` (не публикуется, файл
36
- только для локальной разработки).
37
-
38
26
  ## Установка и запуск
39
27
 
40
28
  Требуется Node.js ≥ 24 (используется нативный `node:sqlite`).
@@ -39,12 +39,12 @@ function resolveReviewId(db, id) {
39
39
  }
40
40
  return latest.id;
41
41
  }
42
- export function runExport(db, id, format = 'markdown') {
42
+ export async function runExport(db, id, format, repositoryPath) {
43
43
  const reviewId = resolveReviewId(db, id);
44
44
  try {
45
45
  return format === 'json'
46
46
  ? JSON.stringify(exportAsJson(db, reviewId), null, 2)
47
- : exportAsMarkdown(db, reviewId);
47
+ : await exportAsMarkdown(db, reviewId, repositoryPath);
48
48
  }
49
49
  catch (error) {
50
50
  if (error instanceof ExportNotFoundError) {
package/dist/cli/index.js CHANGED
@@ -6,12 +6,17 @@ import { formatStartupMessage } from "./startup-message.js";
6
6
  import { parseListArgs, runList } from "./commands/list.js";
7
7
  import { parseExportArgs, runExport } from "./commands/export.js";
8
8
  import { createFileDatabase } from "../server/db/index.js";
9
+ import { getAppVersion } from "../server/app-version.js";
9
10
  /** Opens the reviews DB for the repo at cwd, shared by `list` and `export`. */
10
11
  async function openReviewsDb(dbPrefix) {
11
12
  const repositoryPath = await resolveRepositoryPath(process.cwd());
12
- return createFileDatabase(resolveReviewsDbPath(repositoryPath, dbPrefix));
13
+ return {
14
+ db: createFileDatabase(resolveReviewsDbPath(repositoryPath, dbPrefix)),
15
+ repositoryPath
16
+ };
13
17
  }
14
18
  async function main(argv) {
19
+ console.log(`githuman v${getAppVersion()}`);
15
20
  const [command, ...rest] = argv;
16
21
  switch (command) {
17
22
  case 'serve': {
@@ -22,14 +27,14 @@ async function main(argv) {
22
27
  }
23
28
  case 'list': {
24
29
  const options = parseListArgs(rest);
25
- const db = await openReviewsDb(options.dbPrefix);
30
+ const { db } = await openReviewsDb(options.dbPrefix);
26
31
  console.log(runList(db, options));
27
32
  return;
28
33
  }
29
34
  case 'export': {
30
35
  const options = parseExportArgs(rest);
31
- const db = await openReviewsDb(options.dbPrefix);
32
- const output = runExport(db, options.id, options.format);
36
+ const { db, repositoryPath } = await openReviewsDb(options.dbPrefix);
37
+ const output = await runExport(db, options.id, options.format, repositoryPath);
33
38
  if (options.output) {
34
39
  writeFileSync(options.output, output);
35
40
  }
@@ -0,0 +1,11 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { fileURLToPath } from 'node:url';
3
+ let cachedVersion;
4
+ export function getAppVersion() {
5
+ if (cachedVersion === undefined) {
6
+ const pkgPath = fileURLToPath(new URL('../../package.json', import.meta.url));
7
+ const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
8
+ cachedVersion = pkg.version;
9
+ }
10
+ return cachedVersion;
11
+ }
@@ -1,3 +1,4 @@
1
+ import { randomUUID } from 'node:crypto';
1
2
  import { readFileSync } from 'node:fs';
2
3
  import { join } from 'node:path';
3
4
  import Fastify from 'fastify';
@@ -7,6 +8,7 @@ import fastifyStatic from '@fastify/static';
7
8
  // namespace under NodeNext — cast it back to the plugin function type.
8
9
  import fastifySseImport from '@fastify/sse';
9
10
  import { healthRoutes } from "./routes/health.js";
11
+ import { appInfoRoutes } from "./routes/app-info.js";
10
12
  import { diffRoutes } from "./routes/diff.js";
11
13
  import { gitRoutes } from "./routes/git.js";
12
14
  import { reviewRoutes } from "./routes/reviews.js";
@@ -25,12 +27,16 @@ export function buildApp(options = {}) {
25
27
  const eventBus = options.eventBus ?? createEventBus();
26
28
  app.register(fastifySse);
27
29
  app.register(healthRoutes);
30
+ app.register(appInfoRoutes);
28
31
  app.register(diffRoutes, { repositoryPath });
29
32
  app.register(gitRoutes, { repositoryPath });
30
33
  app.register(reviewRoutes, { repositoryPath, db, eventBus });
31
34
  app.register(commentRoutes, { db, eventBus });
32
- app.register(exportRoutes, { db });
33
- app.register(eventRoutes, { eventBus });
35
+ app.register(exportRoutes, { db, repositoryPath });
36
+ app.register(eventRoutes, {
37
+ eventBus,
38
+ instanceId: options.instanceId ?? randomUUID()
39
+ });
34
40
  if (options.watchFiles) {
35
41
  const watcher = watchRepository(repositoryPath, () => {
36
42
  eventBus.publish({ type: 'files:changed' });
@@ -0,0 +1,6 @@
1
+ import { getAppVersion } from "../app-version.js";
2
+ export async function appInfoRoutes(app) {
3
+ app.get('/api/app-info', async () => {
4
+ return { version: getAppVersion() };
5
+ });
6
+ }
@@ -1,18 +1,21 @@
1
1
  import { Type } from '@sinclair/typebox';
2
2
  import { getReview } from "../services/review.service.js";
3
3
  import { createComment, getComments, editComment, removeComment, resolveComment, unresolveComment, ValidationError } from "../services/comment.service.js";
4
+ // Null goes first in each union: Fastify's Ajv coerces types, and with
5
+ // Integer (or String) first it turns a JSON null into 0 (or "") before the
6
+ // Null branch is ever tried — a file-level comment would be stored as line 0.
4
7
  const CreateCommentBody = Type.Object({
5
8
  filePath: Type.String({ minLength: 1 }),
6
- lineNumber: Type.Optional(Type.Union([Type.Integer(), Type.Null()])),
7
- lineNumberEnd: Type.Optional(Type.Union([Type.Integer(), Type.Null()])),
9
+ lineNumber: Type.Optional(Type.Union([Type.Null(), Type.Integer()])),
10
+ lineNumberEnd: Type.Optional(Type.Union([Type.Null(), Type.Integer()])),
8
11
  lineType: Type.Optional(Type.Union([
12
+ Type.Null(),
9
13
  Type.Literal('added'),
10
14
  Type.Literal('removed'),
11
- Type.Literal('context'),
12
- Type.Null()
15
+ Type.Literal('context')
13
16
  ])),
14
17
  content: Type.String(),
15
- suggestion: Type.Optional(Type.Union([Type.String(), Type.Null()]))
18
+ suggestion: Type.Optional(Type.Union([Type.Null(), Type.String()]))
16
19
  });
17
20
  const UpdateCommentBody = Type.Object({
18
21
  content: Type.String()
@@ -1,8 +1,9 @@
1
1
  export async function eventRoutes(app, opts) {
2
- const { eventBus } = opts;
2
+ const { eventBus, instanceId } = opts;
3
+ const hello = { instanceId };
3
4
  app.get('/api/events', { sse: true }, async (request, reply) => {
4
5
  reply.sse.keepAlive();
5
- await reply.sse.send({ event: 'connected', data: {} });
6
+ await reply.sse.send({ event: 'connected', data: hello });
6
7
  const unsubscribe = eventBus.subscribe(event => {
7
8
  void reply.sse.send({
8
9
  event: event.type,
@@ -5,14 +5,14 @@ const ExportQuery = Type.Object({
5
5
  format: Type.Union([Type.Literal('json'), Type.Literal('markdown')])
6
6
  });
7
7
  export async function exportRoutes(app, opts) {
8
- const { db } = opts;
8
+ const { db, repositoryPath } = opts;
9
9
  const typedApp = app.withTypeProvider();
10
10
  typedApp.get('/api/reviews/:id/export', { schema: { params: ExportParams, querystring: ExportQuery } }, async (request, reply) => {
11
11
  try {
12
12
  if (request.query.format === 'json') {
13
13
  return exportAsJson(db, request.params.id);
14
14
  }
15
- const markdown = exportAsMarkdown(db, request.params.id);
15
+ const markdown = await exportAsMarkdown(db, request.params.id, repositoryPath);
16
16
  reply.type('text/markdown');
17
17
  return markdown;
18
18
  }
@@ -1,5 +1,6 @@
1
1
  import { findReviewById } from "../repositories/review.repo.js";
2
2
  import { listCommentsByReview } from "../repositories/comment.repo.js";
3
+ import { getFileAtRef } from "./git.service.js";
3
4
  export class ExportNotFoundError extends Error {
4
5
  }
5
6
  const STATUS_LABELS = {
@@ -26,46 +27,226 @@ function diffLinePrefix(line) {
26
27
  return '-';
27
28
  return ' ';
28
29
  }
29
- export function formatReviewAsMarkdown(review, files, comments) {
30
+ function filePathOf(file) {
31
+ return file.newPath || file.oldPath;
32
+ }
33
+ /**
34
+ * A fence one backtick longer than the longest backtick run inside `text`, so
35
+ * code or a suggestion that itself contains ``` can't close the block early.
36
+ */
37
+ function fenceFor(text) {
38
+ return '`'.repeat(Math.max(3, longestBacktickRun(text) + 1));
39
+ }
40
+ function longestBacktickRun(text) {
41
+ return Math.max(0, ...(text.match(/`+/g) ?? []).map(run => run.length));
42
+ }
43
+ /** Inline code that survives backticks in `text` (legal in git paths). */
44
+ function inlineCode(text) {
45
+ const delimiter = '`'.repeat(longestBacktickRun(text) + 1);
46
+ // CommonMark strips one space from each end when both ends have one.
47
+ const pad = text.startsWith('`') ||
48
+ text.endsWith('`') ||
49
+ (text.startsWith(' ') && text.endsWith(' '))
50
+ ? ' '
51
+ : '';
52
+ return `${delimiter}${pad}${text}${pad}${delimiter}`;
53
+ }
54
+ function extensionOf(path) {
55
+ const name = path.slice(path.lastIndexOf('/') + 1);
56
+ const dot = name.lastIndexOf('.');
57
+ return dot > 0 ? name.slice(dot + 1).toLowerCase() : '';
58
+ }
59
+ /**
60
+ * `lineNumberEnd` is null on comments saved before ranges existed (ADR 0017).
61
+ * Line 0 is how the API used to store a whole-file comment's null (a type
62
+ * coercion bug, since fixed), so existing databases still contain it.
63
+ */
64
+ function rangeOf(comment) {
65
+ if (comment.lineNumber === null || comment.lineNumber <= 0)
66
+ return null;
67
+ return [comment.lineNumber, comment.lineNumberEnd ?? comment.lineNumber];
68
+ }
69
+ function rangeLabel([start, end]) {
70
+ return start === end ? `Строка ${start}` : `Строки ${start}–${end}`;
71
+ }
72
+ /**
73
+ * Same line selection as ReviewDetailPage's comments-only view. Anchor column
74
+ * as in DiffHunkView.vue: 'removed' comments count old-side numbers.
75
+ */
76
+ function diffLinesFor(file, comment, [start, end]) {
77
+ const out = [];
78
+ // Where the first covered line sits in the snapshot: old- and new-side
79
+ // numbers aren't comparable, so items are ordered by this instead.
80
+ let position = Number.MAX_SAFE_INTEGER;
81
+ if (!file)
82
+ return { lines: out, position };
83
+ const numberOf = (line) => comment.lineType === 'removed' ? line.oldLineNumber : line.newLineNumber;
84
+ let offset = 0;
85
+ for (const hunk of file.hunks) {
86
+ const covered = hunk.lines.flatMap((line, index) => {
87
+ const n = numberOf(line);
88
+ return n !== null && n >= start && n <= end ? [{ line, index }] : [];
89
+ });
90
+ if (covered.length > 0) {
91
+ position = Math.min(position, offset + covered[0].index);
92
+ // A range spanning hunks (not possible from the UI) keeps the gap visible.
93
+ if (out.length > 0)
94
+ out.push('@@ … @@');
95
+ for (const { line } of covered) {
96
+ out.push(`${diffLinePrefix(line)}${line.content}`);
97
+ }
98
+ }
99
+ offset += hunk.lines.length;
100
+ }
101
+ return { lines: out, position };
102
+ }
103
+ /** A trailing newline doesn't start another line; CRLF endings are dropped. */
104
+ function splitFileLines(content) {
105
+ const lines = content.split('\n').map(line => line.replace(/\r$/, ''));
106
+ if (lines.at(-1) === '')
107
+ lines.pop();
108
+ return lines;
109
+ }
110
+ function indentBlock(text, indent) {
111
+ return text.split('\n').map(line => (line === '' ? '' : `${indent}${line}`));
112
+ }
113
+ function renderItem(item, index) {
114
+ const marker = `${index + 1}. `;
115
+ const indent = ' '.repeat(marker.length);
116
+ const out = [`${marker}${item.heading}`];
117
+ if (item.code) {
118
+ const body = item.code.lines.join('\n');
119
+ const fence = fenceFor(body);
120
+ out.push('');
121
+ out.push(`${indent}${fence}${item.code.lang}`);
122
+ out.push(...item.code.lines.map(line => `${indent}${line}`));
123
+ out.push(`${indent}${fence}`);
124
+ }
125
+ else if (item.missingNote) {
126
+ out.push('');
127
+ out.push(`${indent}_${item.missingNote}_`);
128
+ }
129
+ out.push('');
130
+ out.push(...indentBlock(item.comment.content, indent));
131
+ if (item.comment.suggestion) {
132
+ const fence = fenceFor(item.comment.suggestion);
133
+ out.push('');
134
+ out.push(`${indent}Предложение:`);
135
+ out.push('');
136
+ out.push(`${indent}${fence}`);
137
+ out.push(...item.comment.suggestion.split('\n').map(l => `${indent}${l}`));
138
+ out.push(`${indent}${fence}`);
139
+ }
140
+ out.push('');
141
+ return out;
142
+ }
143
+ /**
144
+ * Full-file comments (lineType null) quote `fileLines`, the file as on disk
145
+ * (null = unreadable), not the snapshot — the same exception to "review =
146
+ * frozen snapshot" as the UI's full-file view (#30).
147
+ */
148
+ export function formatReviewAsMarkdown(review, files, comments, fileLines = new Map()) {
30
149
  const lines = [];
31
- lines.push(`# Ревью: ${review.repositoryPath}`);
150
+ // A user-given name may contain newlines; keep it on the heading line.
151
+ const title = (review.name ?? review.repositoryPath).replace(/\s*[\r\n]+\s*/g, ' ');
152
+ lines.push(`# Ревью: ${title}`);
32
153
  lines.push('');
154
+ lines.push(`- Репозиторий: ${review.repositoryPath}`);
33
155
  lines.push(`- Источник: ${review.sourceType}${review.sourceRef ? ` (${review.sourceRef})` : ''}`);
34
156
  lines.push(`- Статус: ${STATUS_LABELS[review.status]}`);
35
157
  lines.push(`- Создано: ${review.createdAt}`);
36
158
  lines.push('');
37
- const commentsByFile = new Map();
38
- for (const comment of comments) {
39
- const existing = commentsByFile.get(comment.filePath) ?? [];
40
- commentsByFile.set(comment.filePath, [...existing, comment]);
41
- }
42
- for (const file of files) {
43
- lines.push(`## ${file.newPath} (+${file.additions}/-${file.deletions})`);
159
+ const open = comments.filter(comment => !comment.resolved);
160
+ if (open.length === 0) {
161
+ lines.push('Открытых замечаний нет.');
44
162
  lines.push('');
45
- lines.push('```diff');
46
- for (const hunk of file.hunks) {
47
- for (const line of hunk.lines) {
48
- lines.push(`${diffLinePrefix(line)}${line.content}`);
49
- }
163
+ return lines.join('\n');
164
+ }
165
+ const filesByPath = new Map(files.map(file => [filePathOf(file), file]));
166
+ const byFile = new Map();
167
+ for (const comment of open) {
168
+ const list = byFile.get(comment.filePath);
169
+ if (list) {
170
+ list.push(comment);
50
171
  }
51
- lines.push('```');
52
- lines.push('');
53
- const fileComments = commentsByFile.get(file.newPath) ?? [];
54
- if (fileComments.length > 0) {
55
- lines.push('**Комментарии:**');
56
- lines.push('');
57
- for (const comment of fileComments) {
58
- const location = comment.lineNumber !== null ? `Строка ${comment.lineNumber}` : 'Файл';
59
- lines.push(`- ${location}: ${comment.content}`);
172
+ else {
173
+ byFile.set(comment.filePath, [comment]);
174
+ }
175
+ }
176
+ // Snapshot order first; a commented file missing from the snapshot still
177
+ // gets its section rather than silently dropping the comment.
178
+ const paths = [
179
+ ...files.map(filePathOf).filter(path => byFile.has(path)),
180
+ ...[...byFile.keys()].filter(path => !filesByPath.has(path))
181
+ ];
182
+ for (const path of paths) {
183
+ const file = filesByPath.get(path);
184
+ const items = [];
185
+ for (const comment of byFile.get(path) ?? []) {
186
+ const range = rangeOf(comment);
187
+ if (!range) {
188
+ items.push({
189
+ heading: 'Файл целиком',
190
+ code: null,
191
+ missingNote: null,
192
+ comment,
193
+ order: -1
194
+ });
195
+ continue;
196
+ }
197
+ if (comment.lineType === null) {
198
+ const content = fileLines.get(path) ?? null;
199
+ const [start, end] = range;
200
+ const available = content !== null && start <= end && end <= content.length;
201
+ items.push({
202
+ heading: `${rangeLabel(range)} (файл целиком)`,
203
+ code: available
204
+ ? { lang: extensionOf(path), lines: content.slice(start - 1, end) }
205
+ : null,
206
+ missingNote: available
207
+ ? null
208
+ : 'Этих строк нет в текущей версии файла — код не показан',
209
+ comment,
210
+ // After all diff comments, in file-line order.
211
+ order: Number.MAX_SAFE_INTEGER / 2 + start
212
+ });
213
+ continue;
60
214
  }
61
- lines.push('');
215
+ const { lines: code, position } = diffLinesFor(file, comment, range);
216
+ items.push({
217
+ heading: rangeLabel(range),
218
+ code: code.length > 0 ? { lang: 'diff', lines: code } : null,
219
+ missingNote: code.length > 0 ? null : 'Строки не найдены в снимке diff',
220
+ comment,
221
+ order: code.length > 0 ? position : Number.MAX_SAFE_INTEGER / 4
222
+ });
62
223
  }
224
+ items.sort((a, b) => a.order - b.order);
225
+ lines.push(`## ${inlineCode(path)}`);
226
+ lines.push('');
227
+ items.forEach((item, index) => lines.push(...renderItem(item, index)));
63
228
  }
64
229
  return lines.join('\n');
65
230
  }
66
- export function exportAsMarkdown(db, reviewId) {
231
+ /**
232
+ * Reads from `repositoryPath` (where the server/CLI runs), not the path stored
233
+ * with the review: that one goes stale if the repository was moved.
234
+ */
235
+ export async function exportAsMarkdown(db, reviewId, repositoryPath) {
67
236
  const review = getReviewOrThrow(db, reviewId);
68
237
  const comments = listCommentsByReview(db, reviewId);
69
238
  const files = JSON.parse(review.snapshotData);
70
- return formatReviewAsMarkdown(review, files, comments);
239
+ const fullFilePaths = new Set(comments
240
+ .filter(c => !c.resolved && c.lineType === null && rangeOf(c) !== null)
241
+ .map(c => c.filePath));
242
+ const fileLines = new Map(await Promise.all([...fullFilePaths].map(async (path) => {
243
+ try {
244
+ const { content, isBinary } = await getFileAtRef(repositoryPath, 'WORKTREE', path);
245
+ return [path, isBinary ? null : splitFileLines(content)];
246
+ }
247
+ catch {
248
+ return [path, null];
249
+ }
250
+ })));
251
+ return formatReviewAsMarkdown(review, files, comments, fileLines);
71
252
  }
@@ -167,6 +167,9 @@ export async function getFilesAtRefBatch(repoPath, requests) {
167
167
  reject(err instanceof Error ? err : new Error(String(err)));
168
168
  }
169
169
  });
170
+ // git may exit before reading stdin (e.g. not a repository): the EPIPE
171
+ // would crash the process, and 'close' already reports the failure.
172
+ child.stdin.on('error', () => { });
170
173
  child.stdin.write(specs.join('\n') + '\n');
171
174
  child.stdin.end();
172
175
  });
@@ -1,4 +1,17 @@
1
1
  /**
2
2
  * Shared types between server and web client
3
3
  */
4
- export {};
4
+ /**
5
+ * Event names sent over the SSE stream (`GET /api/events`). A runtime list,
6
+ * not just a type: the web client attaches one EventSource listener per
7
+ * name, since EventSource has no catch-all for named events.
8
+ */
9
+ export const SERVER_EVENT_TYPES = [
10
+ 'review:created',
11
+ 'review:updated',
12
+ 'review:deleted',
13
+ 'comment:created',
14
+ 'comment:updated',
15
+ 'comment:deleted',
16
+ 'files:changed'
17
+ ];
@@ -0,0 +1 @@
1
+ import{E as e,F as t,H as n,M as r,O as i,Q as a,S as o,U as s,W as c,b as l,et as u,g as ee,h as d,it as f,l as p,s as m,v as h,w as g}from"./runtime-core.esm-bundler-Db5Q9Gsb.js";import{D as _,P as te,S as v,f as ne,g as y,j as b,m as x,t as S,w as C,y as w}from"./QBtn-DGiC5uLo.js";import{d as T,f as E,i as D,u as re}from"./event-D0kaqRwq.js";import{B as ie,D as ae,E as oe,G as O,H as k,I as A,J as j,K as M,L as N,R as se,S as ce,T as P,U as F,V as I,W as le,X as ue,Y as de,Z as fe,_ as pe,ot as L,q as me,v as R,w as he,x as z,z as ge}from"./index-i1Dlwasy.js";import{n as _e}from"./vue-i18n.runtime-CxBM6QPV.js";import{n as ve}from"./pinia-jMX0pzvu.js";import{r as B}from"./client-De34x_P_.js";import{_ as ye,c as be,d as xe,f as Se,g as V,l as Ce,r as we,s as Te,t as H,u as U}from"./format-Bb6ahGqi.js";function W(e){if(e.length<2||e[0]!==`"`||e[e.length-1]!==`"`)return e;let t=e.slice(1,-1),n=[];for(let e=0;e<t.length;e++){let r=t[e];r===`\\`&&/^[0-7]{3}$/.test(t.slice(e+1,e+4))?(n.push(parseInt(t.slice(e+1,e+4),8)),e+=3):r===`\\`&&t[e+1]===`"`?(n.push(34),e+=1):r===`\\`&&t[e+1]===`\\`?(n.push(92),e+=1):n.push((r??``).charCodeAt(0))}return new TextDecoder(`utf-8`).decode(new Uint8Array(n))}function G(e){let t=null,n=null;function r(){let r=e().finally(()=>{t===r&&!n&&(t=null)});return t=r,r}return()=>t?(n??=t.catch(()=>{}).then(()=>(n=null,r())),n):r()}function Ee(e){return{...e,oldPath:W(e.oldPath),newPath:W(e.newPath)}}function De(e,t){let n=new Map(e.map(e=>[e.newPath||e.oldPath,e]));return t.map(e=>{let t=n.get(e.newPath||e.oldPath);return t&&JSON.stringify(t)===JSON.stringify(e)?t:e})}var Oe=ve(`diff`,()=>{let e=a([]),t=a([]),n=a(!1),r=a(!1),i=a(null),o=m(()=>n.value&&!r.value),s=m(()=>[...e.value,...t.value].map(e=>e.newPath||e.oldPath)),c=G(l);async function l(){n.value=!0;try{let[n,r]=await Promise.all([B(`/api/diff/staged`),B(`/api/diff/unstaged`)]);e.value=De(e.value,n.map(Ee)),t.value=De(t.value,r.map(Ee)),i.value=null}catch(n){r.value||(e.value=[],t.value=[]),i.value=n instanceof Error?n.message:String(n)}finally{n.value=!1,r.value=!0}}return{stagedFiles:e,unstagedFiles:t,loading:n,initialLoading:o,error:i,changedPaths:s,fetchDiff:c}}),ke=T({props:[`store`,`barStyle`,`verticalBarStyle`,`horizontalBarStyle`],setup(e){return()=>[h(`div`,{class:e.store.scroll.vertical.barClass.value,style:[e.barStyle,e.verticalBarStyle],"aria-hidden":`true`,onMousedown:e.store.onVerticalMousedown}),h(`div`,{class:e.store.scroll.horizontal.barClass.value,style:[e.barStyle,e.horizontalBarStyle],"aria-hidden":`true`,onMousedown:e.store.onHorizontalMousedown}),c(h(`div`,{ref:e.store.scroll.vertical.ref,class:e.store.scroll.vertical.thumbClass.value,style:e.store.scroll.vertical.style.value,"aria-hidden":`true`}),e.store.thumbVertDir),c(h(`div`,{ref:e.store.scroll.horizontal.ref,class:e.store.scroll.horizontal.thumbClass.value,style:e.store.scroll.horizontal.style.value,"aria-hidden":`true`}),e.store.thumbHorizDir)]}}),Ae=[`vertical`,`horizontal`],K={vertical:{offset:`offsetY`,scroll:`scrollTop`,dir:`down`,dist:`y`},horizontal:{offset:`offsetX`,scroll:`scrollLeft`,dir:`right`,dist:`x`}},q={prevent:!0,mouse:!0,mouseAllDir:!0},je=e=>e>=250?50:Math.ceil(e/5),J=(e,t)=>t?-e:e;function Me(e,t){return Ae.includes(e)?!0:(console.error(`[QScrollArea]: wrong first param of ${t} (vertical/horizontal)`),!1)}var Ne=T({name:`QScrollArea`,props:{...R,thumbStyle:Object,verticalThumbStyle:Object,horizontalThumbStyle:Object,barStyle:[Array,String,Object],verticalBarStyle:[Array,String,Object],horizontalBarStyle:[Array,String,Object],verticalOffset:{type:Array,default:()=>[0,0]},horizontalOffset:{type:Array,default:()=>[0,0]},contentStyle:[Array,String,Object],contentActiveStyle:[Array,String,Object],delay:{type:[String,Number],default:1e3},visible:{type:Boolean,default:null},tabindex:[String,Number],onScroll:Function},setup(t,{slots:r,emit:o}){let s=a(!1),c=a(!1),l=a(!1),d={vertical:a(0),horizontal:a(0)},f={vertical:{ref:u(null),position:a(0),size:a(0)},horizontal:{ref:u(null),position:a(0),size:a(0)}},{proxy:p}=ee(),_=v(),te=pe(t,_),y=null,b,x=u(null),S=m(()=>`q-scrollarea`+(te()?` q-scrollarea--dark`:``));Object.assign(d,{verticalInner:m(()=>d.vertical.value-t.verticalOffset[0]-t.verticalOffset[1]),horizontalInner:m(()=>d.horizontal.value-t.horizontalOffset[0]-t.horizontalOffset[1])}),f.vertical.percentage=m(()=>{let e=f.vertical.size.value-d.vertical.value;if(e<=0)return 0;let t=H(f.vertical.position.value/e,0,1);return Math.round(t*1e4)/1e4}),f.vertical.thumbHidden=m(()=>!(t.visible===null?l.value:t.visible)&&!s.value&&!c.value||f.vertical.size.value<=d.vertical.value+1),f.vertical.thumbStart=m(()=>t.verticalOffset[0]+f.vertical.percentage.value*(d.verticalInner.value-f.vertical.thumbSize.value)),f.vertical.thumbSize=m(()=>Math.round(H(d.verticalInner.value*d.verticalInner.value/f.vertical.size.value,je(d.verticalInner.value),d.verticalInner.value))),f.vertical.style=m(()=>({...t.thumbStyle,...t.verticalThumbStyle,top:`${f.vertical.thumbStart.value}px`,height:`${f.vertical.thumbSize.value}px`,[_.lang.rtl?`left`:`right`]:`${t.horizontalOffset[+!_.lang.rtl]}px`})),f.vertical.thumbClass=m(()=>`q-scrollarea__thumb q-scrollarea__thumb--v absolute-right`+(f.vertical.thumbHidden.value?` q-scrollarea__thumb--invisible`:``)),f.vertical.barClass=m(()=>`q-scrollarea__bar q-scrollarea__bar--v absolute-right`+(f.vertical.thumbHidden.value?` q-scrollarea__bar--invisible`:``)),f.horizontal.percentage=m(()=>{let e=f.horizontal.size.value-d.horizontal.value;if(e<=0)return 0;let t=H(J(f.horizontal.position.value,_.lang.rtl)/e,0,1);return Math.round(t*1e4)/1e4}),f.horizontal.thumbHidden=m(()=>!(t.visible===null?l.value:t.visible)&&!s.value&&!c.value||f.horizontal.size.value<=d.horizontal.value+1),f.horizontal.thumbStart=m(()=>t.horizontalOffset[+!!_.lang.rtl]+f.horizontal.percentage.value*(d.horizontalInner.value-f.horizontal.thumbSize.value)),f.horizontal.thumbSize=m(()=>Math.round(H(d.horizontalInner.value*d.horizontalInner.value/f.horizontal.size.value,je(d.horizontalInner.value),d.horizontalInner.value))),f.horizontal.style=m(()=>({...t.thumbStyle,...t.horizontalThumbStyle,[_.lang.rtl?`right`:`left`]:`${f.horizontal.thumbStart.value}px`,width:`${f.horizontal.thumbSize.value}px`,bottom:`${t.verticalOffset[1]}px`})),f.horizontal.thumbClass=m(()=>`q-scrollarea__thumb q-scrollarea__thumb--h absolute-bottom`+(f.horizontal.thumbHidden.value?` q-scrollarea__thumb--invisible`:``)),f.horizontal.barClass=m(()=>`q-scrollarea__bar q-scrollarea__bar--h absolute-bottom`+(f.horizontal.thumbHidden.value?` q-scrollarea__bar--invisible`:``));let C=m(()=>t.tabindex===void 0?f.vertical.size.value>d.vertical.value+1||f.horizontal.size.value>d.horizontal.value+1?0:void 0:t.tabindex),w=m(()=>f.vertical.thumbHidden.value&&f.horizontal.thumbHidden.value?t.contentStyle:t.contentActiveStyle);function T(){return{verticalPosition:f.vertical.position.value,verticalPercentage:f.vertical.percentage.value,verticalSize:f.vertical.size.value,verticalContainerSize:d.vertical.value,verticalContainerInnerSize:d.verticalInner.value,horizontalPosition:f.horizontal.position.value,horizontalPercentage:f.horizontal.percentage.value,horizontalSize:f.horizontal.size.value,horizontalContainerSize:d.horizontal.value,horizontalContainerInnerSize:d.horizontalInner.value}}let E=L(()=>{let e=T();e.ref=p,o(`scroll`,e)},0);function D(e,t,n){Me(e,`setScrollPosition`)&&(e===`vertical`?N:A)(x.value,t,n)}function re({height:e,width:t}){let n=!1;d.vertical.value!==e&&(d.vertical.value=e,n=!0),d.horizontal.value!==t&&(d.horizontal.value=t,n=!0),n&&k()}function ie({position:e}){let t=!1;f.vertical.position.value!==e.top&&(f.vertical.position.value=e.top,t=!0),f.horizontal.position.value!==e.left&&(f.horizontal.position.value=e.left,t=!0),t&&k()}function ae({height:e,width:t}){f.horizontal.size.value!==t&&(f.horizontal.size.value=t,k()),f.vertical.size.value!==e&&(f.vertical.size.value=e,k())}function oe(e,t){let n=f[t];if(e.isFirst){if(n.thumbHidden.value)return;b=t===`horizontal`?J(n.position.value,_.lang.rtl):n.position.value,c.value=!0}else if(!c.value)return;e.isFinal&&(c.value=!1);let r=K[t],i=(n.size.value-d[t].value)/(d[t+`Inner`].value-n.thumbSize.value),a=e.distance[r.dist],o=(e.direction===r.dir?1:-1)*(t===`horizontal`&&_.lang.rtl?-1:1),s=b+o*a*i;j(t===`horizontal`?J(s,_.lang.rtl):s,t)}function O(e,n){let r=f[n];if(!r.thumbHidden.value){let i=n===`horizontal`&&_.lang.rtl,a=n===`vertical`?t.verticalOffset[0]:t.horizontalOffset[+!!i],o=(i?d.horizontal.value-e.offsetX:e[K[n].offset])-a,s=r.thumbStart.value-a;if(o<s||o>s+r.thumbSize.value){let e=o-r.thumbSize.value/2,t=H(e/(d[n+`Inner`].value-r.thumbSize.value),0,1);j(i?J(t*Math.max(0,r.size.value-d[n].value),_.lang.rtl):t*Math.max(0,r.size.value-d[n].value),n)}r.ref.value!==null&&r.ref.value.dispatchEvent(new MouseEvent(e.type,e))}}function k(){s.value=!0,y!==null&&clearTimeout(y),y=setTimeout(()=>{y=null,s.value=!1},t.delay),t.onScroll!==void 0&&E()}function j(e,t){x.value[K[t].scroll]=e}let M=null;function se(){M!==null&&clearTimeout(M),M=setTimeout(()=>{M=null,l.value=!0},_.platform.is.ios?50:0)}function ce(){M!==null&&(clearTimeout(M),M=null),l.value=!1}let P=null;n(()=>_.lang.rtl,(e,t)=>{x.value!==null&&A(x.value,J(J(f.horizontal.position.value,t),e))}),i(()=>{P={top:f.vertical.position.value,left:f.horizontal.position.value}}),g(()=>{if(P===null)return;let e=x.value;e!==null&&(A(e,P.left),N(e,P.top))}),e(E.cancel),Object.assign(p,{getScrollTarget:()=>x.value,getScroll:T,getScrollPosition:()=>({top:f.vertical.position.value,left:f.horizontal.position.value}),getScrollPercentage:()=>({top:f.vertical.percentage.value,left:f.horizontal.percentage.value}),setScrollPosition:D,setScrollPercentage(e,t,n){if(!Me(e,`setScrollPercentage`))return;let r=t*(f[e].size.value-d[e].value);D(e,e===`horizontal`?J(r,_.lang.rtl):r,n)}});let F={scroll:f,thumbVertDir:[[we,e=>{oe(e,`vertical`)},void 0,{vertical:!0,...q}]],thumbHorizDir:[[we,e=>{oe(e,`horizontal`)},void 0,{horizontal:!0,...q}]],onVerticalMousedown(e){O(e,`vertical`)},onHorizontalMousedown(e){O(e,`horizontal`)}};return()=>h(`div`,{class:S.value,onMouseenter:se,onMouseleave:ce},[h(`div`,{ref:x,class:`q-scrollarea__container scroll relative-position fit hide-scrollbar`,tabindex:C.value},[h(`div`,{class:`q-scrollarea__content absolute`,style:w.value},ne(r.default,[h(V,{debounce:0,onResize:ae})])),h(ye,{axis:`both`,onScroll:ie})]),h(V,{debounce:0,onResize:re}),h(ke,{store:F,barStyle:t.barStyle,verticalBarStyle:t.verticalBarStyle,horizontalBarStyle:t.horizontalBarStyle})])}}),Pe=d({__name:`CancelButton`,setup(e){let{t}=_e();return(e,n)=>(r(),p(S,o({padding:`2px 8px`},e.$attrs,{dense:``,flat:``,color:`warning`,label:f(t)(`reviews.comments.cancel`)}),null,16,[`label`]))}}),Fe=d({__name:`CreateButton`,setup(e){let{t}=_e();return(e,n)=>(r(),p(S,o({padding:`2px 8px`,label:f(t)(`reviews.create.submit`)},e.$attrs,{unelevated:``,dense:``,color:`accent`}),null,16,[`label`]))}}),Ie={hover:Boolean,hoverDelay:{type:Number,default:0},hoverHideDelay:{type:Number,default:150}};function Le({props:e,canShow:t,show:n,canHide:r,hide:i}){let{removeTimeout:a,registerTimeout:o}=fe();function s(r){e.hover===!0&&r.pointerType!==`touch`&&(a(),t(r)&&(e.hoverDelay>0?o(()=>{n(r)},e.hoverDelay):n(r)))}function c(t){a(),r(t)&&o(()=>{i(t)},e.hoverHideDelay)}function l(t){e.hover===!0&&t.pointerType!==`touch`&&c(t)}function u(e){e.pointerType!==`touch`&&a()}return{clearHoverTimer:a,hoverShow:s,hoverHide:l,scheduleHoverHide:c,onHoverContentEnter:u}}var Y=null,{notPassiveCapture:X,passiveCapture:Re}=D,Z=[];function Q(e){Y!==null&&(clearTimeout(Y),Y=null);let t=e.target;if(t===void 0||t.nodeType===8||t.classList.contains(`no-pointer-events`))return;let n=null,r=!1;for(let e=F.length-1;e>=0;e--){let t=F[e].$,i=t.type.name;if(i!==`QTooltip`){if(i===`QDialog`){if(t.props.seamless!==!0){if(r===!1)return;n=new Set(F.slice(e+1).map(e=>e.contentEl));break}continue}r=!0}}for(let r=Z.length-1;r>=0;r--){let i=Z[r];if(n!==null&&!n.has(i.innerRef.value))return;if((i.anchorEl.value===null||!i.anchorEl.value.contains(t)&&!z(i.anchorEl.value,t))&&(t===document.body||i.innerRef.value!==null&&!i.innerRef.value.contains(t)&&!z(i.innerRef.value,t)))e.qClickOutside=!0,i.onClickOutside(e);else return}}function ze(e){e.pointerType!==`touch`&&(Y!==null&&clearTimeout(Y),Y=setTimeout(()=>{Y=null,Q(e)},0))}function Be(e){Z.push(e),Z.length===1&&(document.addEventListener(`pointerdown`,ze,Re),document.addEventListener(`mousedown`,Q,X),document.addEventListener(`touchstart`,Q,X))}function Ve(e){let t=Z.indexOf(e);t!==-1&&(Z.splice(t,1),Z.length===0&&(Y!==null&&(clearTimeout(Y),Y=null),document.removeEventListener(`pointerdown`,ze,Re),document.removeEventListener(`mousedown`,Q,X),document.removeEventListener(`touchstart`,Q,X)))}var He=`a[href], button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex^="-"])`,Ue=T({name:`QMenu`,inheritAttrs:!1,props:{...Se,...j,...R,...O,persistent:Boolean,autoClose:Boolean,separateClosePopup:Boolean,noEscDismiss:Boolean,noRouteDismiss:Boolean,noRefocus:Boolean,noFocus:Boolean,fit:Boolean,cover:Boolean,square:Boolean,anchor:{type:String,validator:U},self:{type:String,validator:U},offset:{type:Array,validator:Ce},touchPosition:Boolean,...Ie,maxHeight:{type:String,default:null},maxWidth:{type:String,default:null}},emits:[...me,`click`,`escapeKey`],setup(t,{slots:r,emit:i,attrs:o}){let s=null,c,l=!1,d=ee(),{proxy:f}=d,p=v(),g=u(null),_=a(!1),ne=m(()=>!t.persistent&&!t.noRouteDismiss),b=pe(t,p),{registerTick:S,removeTick:C}=de(),{registerTransitionEnd:T}=ue(t),{transitionProps:E,transitionStyle:D}=le(t),{anchorEl:O,canShow:A,anchorEvents:j}=xe({showing:_,getPopupRole:()=>o.role}),{show:N,hide:F}=M({showing:_,canShow:A,canHide(e){return l&&!_e.value?e?.type!==`click`:!0},handleShow:Ee,handleHide:De,handleRouteChange:Oe,hideOnRouteChange:ne,processOnMount:!0}),{clearHoverTimer:I,hoverShow:fe,scheduleHoverHide:L,onHoverContentEnter:me}=Le({props:t,canShow:()=>!_.value,show:N,canHide:e=>_.value&&!W(e.relatedTarget),hide:F});Object.assign(j,{hoverShow:fe,hoverHide:G});let{showPortal:R,hidePortal:z,portalIsAccessible:_e,renderPortal:ve}=se(d,g,J,`menu`),B={anchorEl:O,innerRef:g,onClickOutside(e){if(!t.persistent&&_.value)return F(e),(e.type===`touchstart`||e.target.classList.contains(`q-dialog__backdrop`))&&re(e),!0}},ye=m(()=>be(t.anchor||(t.cover?`center middle`:`bottom start`),p.lang.rtl)),Se=m(()=>t.cover?ye.value:be(t.self||`top start`,p.lang.rtl)),V=Te({props:t,$q:p,anchorEl:O,innerRef:g,showing:_,anchorOrigin:ye,selfOrigin:Se}),Ce=m(()=>`q-menu scroll`+(V.viaCssAnchor?``:` q-position-engine`)+(t.square?` q-menu--square`:``)+(b()?` q-menu--dark q-dark`:``)),we=m(()=>t.autoClose?{onClick:Ae}:{}),H=m(()=>_.value&&!t.persistent);n(H,e=>{e?(oe(q),Be(B)):(ae(q),Ve(B))});function U(){ge(()=>{let e=g.value;e&&e.contains!==void 0&&!e.contains(document.activeElement)&&(e=e.querySelector(`[autofocus][tabindex], [data-autofocus][tabindex]`)||e.querySelector(`[autofocus] [tabindex], [data-autofocus] [tabindex]`)||e.querySelector(`[autofocus], [data-autofocus]`)||e,e.focus({preventScroll:!0}))})}function W(e){if(e==null)return!1;if(O.value!==null&&O.value.contains(e)||g.value!==null&&g.value.contains(e))return!0;let t=k(e);for(;t!=null;){if(t===f)return!0;t=w(t)}return!1}function G(e){if(e.pointerType===`touch`)return;L(e);let t=w(f);for(;t!=null;)t.$options.name===`QMenu`&&t.$props.hover===!0&&t.__qHoverHide?.(e),t=w(t)}function Ee(e){l=t.hover&&e?.type===`pointerenter`,I(),s=t.noRefocus||l?null:document.activeElement,he(K),R(!1,l),V.handleShow(!l&&(t.touchPosition||t.contextMenu)?e:void 0),!t.noFocus&&!l&&document.activeElement.blur(),S(()=>{V.handleTick(),!t.noFocus&&!l&&U()}),T(()=>{p.platform.is.ios&&(c=t.autoClose,g.value.click()),V.viaCssAnchor||V.track(),R(!0),i(`show`,e)})}function De(e){if(l=!1,C(),z(),ke(!0),s!==null&&(e===void 0||!e.qClickOutside)){let t=(e?.type.indexOf(`key`)===0?s.closest(`[tabindex]:not([tabindex^="-"])`):void 0)||s;s=null,ge(()=>{t.isConnected&&t.focus({preventScroll:!0})})}T(()=>{z(!0),V.releaseAnchor(!1),i(`hide`,e)})}function Oe(){s=null}function ke(e){I(),V.releaseAnchor(e),(e||_.value)&&(P(K),Ve(B),ae(q)),e||(s=null)}function Ae(e){c?c=!1:(ie(f,e),i(`click`,e))}function K(e){H.value&&!t.noFocus&&!l&&!y(g.value,e.target)&&!ce(g.value,e.target)&&U()}function q(e){t.noEscDismiss||(i(`escapeKey`),F(e))}function je(e){if(e.keyCode!==9||e.defaultPrevented||!H.value||g.value===null)return;let t=g.value,n=t.querySelectorAll(He),r=n.length===0?null:n[e.shiftKey?0:n.length-1];(r===null||document.activeElement===r||e.shiftKey&&document.activeElement===t)&&(P(K),s!==null&&s.isConnected&&(s.closest(`[tabindex]:not([tabindex^="-"])`)||s).focus({preventScroll:!0}),s=null,F(e))}function J(){return h(te,E(),()=>_.value?h(`div`,{...o,ref:g,tabindex:-1,onKeydown:[].concat(o.onKeydown||[],je),...t.hover?{onPointerenter:[].concat(o.onPointerenter||[],me),onPointerleave:[].concat(o.onPointerleave||[],G)}:{},class:[Ce.value,o.class],style:[o.style,D(),V.positionStyle.value],...we.value},x(r.default)):null)}return e(()=>{ke(!1)}),Object.assign(f,{focus:U,updatePosition:V.updatePosition}),f.__qHoverHide=L,ve}});function We(e){return e===!1?0:e===!0||e===void 0?1:Number.parseInt(e,10)||0}function $(e){let t=e.currentTarget,n=t.__qclosepopup;n!==0&&setTimeout(()=>{let r=k(t);r!==void 0&&I(r,e,n)},0)}function Ge(e){b(e,13)&&$(e)}var Ke=E({name:`close-popup`,beforeMount(e,{value:t}){e.__qclosepopup=We(t),e.addEventListener(`click`,$),e.addEventListener(`keyup`,Ge)},updated(e,{value:t,oldValue:n}){t!==n&&e.__qclosepopup!==void 0&&(e.__qclosepopup=We(t))},beforeUnmount(e){e.__qclosepopup!==void 0&&(e.removeEventListener(`click`,$),e.removeEventListener(`keyup`,Ge),e.__qclosepopup=void 0)}}),qe=T({name:`QPage`,props:{padding:Boolean,styleFn:Function},setup(e,{slots:t}){let n=v(),r=l(_,C);if(r===C)return console.error(`QPage needs to be a deep child of QLayout`),C;if(l(`_q_pc_`,C)===C)return console.error(`QPage needs to be child of QPageContainer`),C;let i=m(()=>{let t=(r.header.space?r.header.size:0)+(r.footer.space?r.footer.size:0);if(typeof e.styleFn==`function`){let i=r.isContainer.value?r.containerHeight.value:n.screen.height;return e.styleFn(t,i)}return{minHeight:r.isContainer.value?r.containerHeight.value-t+`px`:n.screen.height===0?t===0?`100vh`:`calc(100vh - ${t}px)`:n.screen.height-t+`px`}}),a=m(()=>`q-page${e.padding?` q-layout-padding`:``}`);return()=>h(`main`,{class:a.value,style:i.value},x(t.default))}}),Je=d({__name:`AppPage`,setup(e){function n(e,t){return{height:`${t-e}px`}}return(e,i)=>(r(),p(qe,{"style-fn":n},{default:s(()=>[t(e.$slots,`default`)]),_:3}))}});export{Ie as a,Ne as c,W as d,Le as i,Oe as l,Ke as n,Fe as o,Ue as r,Pe as s,Je as t,G as u};
@@ -0,0 +1 @@
1
+ import{H as e,M as t,N as n,Q as r,S as i,U as a,W as o,b as s,bt as c,c as l,d as u,et as d,g as f,h as p,it as m,l as h,m as g,r as _,s as v,v as y,x as b,z as x}from"./runtime-core.esm-bundler-Db5Q9Gsb.js";import{C as S,D as C,R as w,S as T,T as ee,c as E,f as te,m as D,t as O,w as k}from"./QBtn-DGiC5uLo.js";import{d as A}from"./event-D0kaqRwq.js";import{J as j,K as ne,d as re,g as M,h as N,m as P,n as F,q as I,y as L}from"./index-i1Dlwasy.js";import{n as ie}from"./vue-i18n.runtime-CxBM6QPV.js";import{n as R,r as z}from"./pinia-jMX0pzvu.js";import{a as B,i as V,n as H,r as U,t as W}from"./client-De34x_P_.js";import{r as G}from"./format-Bb6ahGqi.js";import{a as K,i as q,l as J,n as Y,o as ae,s as oe}from"./AppPage-B9tWPrzn.js";var X=R(`active-review`,()=>{let e=r(null),t=r([]),n=r(!1),i=v(()=>{let e=new Map;for(let n of t.value){let t=e.get(n.filePath);t?t.push(n):e.set(n.filePath,[n])}return e});async function a(){n.value=!0;try{let n=(await U(`/api/reviews`)).filter(e=>e.status===`in_progress`).sort((e,t)=>t.createdAt.localeCompare(e.createdAt));e.value=n[0]??null,t.value=e.value?await U(`/api/reviews/${e.value.id}/comments`):[]}finally{n.value=!1}}function o(n){e.value=n,t.value=[]}async function s(t){e.value&&(await V(`/api/reviews/${e.value.id}`,{status:t}),await a())}async function c(n){if(!e.value)throw Error(`No active review`);let r=await B(`/api/reviews/${e.value.id}/comments`,n);return t.value=[...t.value,r],r}async function l(e,n){let r=await V(`/api/comments/${e}`,{content:n});t.value=t.value.map(t=>t.id===e?r:t)}async function u(e){await H(`/api/comments/${e}`),t.value=t.value.filter(t=>t.id!==e)}async function d(e){let n=await V(`/api/comments/${e}/resolve`);t.value=t.value.map(t=>t.id===e?n:t)}async function f(e){let n=await V(`/api/comments/${e}/unresolve`);t.value=t.value.map(t=>t.id===e?n:t)}return{activeReview:e,comments:t,loading:n,commentsByFile:i,refresh:a,setActiveReview:o,setStatus:s,createComment:c,editComment:l,deleteComment:u,resolveComment:d,unresolveComment:f}}),se=p({__name:`SearchInput`,props:{modelValue:{},modelModifiers:{}},emits:[`update:modelValue`],setup(e){let n=x(e,`modelValue`);return(e,r)=>(t(),h(F,i({modelValue:n.value,"onUpdate:modelValue":r[0]||=e=>n.value=e},e.$attrs,{dense:``,outlined:``,clearable:``}),{prepend:a(()=>[g(E,{name:`search`,size:`xs`})]),_:1},16,[`modelValue`]))}}),ce=A({name:`QItemLabel`,props:{overline:Boolean,caption:Boolean,header:Boolean,lines:[Number,String]},setup(e,{slots:t}){let n=v(()=>Number.parseInt(e.lines,10)),r=v(()=>`q-item__label`+(e.overline?` q-item__label--overline text-overline`:``)+(e.caption?` q-item__label--caption text-caption`:``)+(e.header?` q-item__label--header`:``)+(n.value===1?` ellipsis`:``)),i=v(()=>e.lines!==void 0&&n.value>1?{overflow:`hidden`,display:`-webkit-box`,"-webkit-box-orient":`vertical`,"-webkit-line-clamp":n.value}:null);return()=>y(`div`,{style:i.value,class:r.value},D(t.default))}}),le=[`top`,`right`,`bottom`,`left`],Z={type:{type:String,default:`a`},outline:Boolean,push:Boolean,flat:Boolean,unelevated:Boolean,color:String,textColor:String,glossy:Boolean,square:Boolean,padding:String,label:{type:[String,Number],default:``},labelPosition:{type:String,default:`right`,validator:e=>le.includes(e)},externalLabel:Boolean,hideLabel:{type:Boolean},labelClass:[Array,String,Object],labelStyle:[Array,String,Object],disable:Boolean,tabindex:[Number,String]},ue=[`label`],de=Object.keys(Z).filter(e=>!ue.includes(e)&&O.props[e]!==void 0);function fe(e){let t={};for(let n of de)t[n]=e[n];return t}function pe(e,t){return{formClass:v(()=>`q-fab--form-${e.square?`square`:`rounded`}`),stacked:v(()=>!e.externalLabel&&[`top`,`bottom`].includes(e.labelPosition)),labelProps:v(()=>{if(e.externalLabel){let n=e.hideLabel===null?!t.value:e.hideLabel;return{action:`push`,data:{class:[e.labelClass,`q-fab__label q-tooltip--style q-fab__label--external q-fab__label--external-${e.labelPosition}`+(n?` q-fab__label--external-hidden`:``)],style:e.labelStyle}}}return{action:[`left`,`top`].includes(e.labelPosition)?`unshift`:`push`,data:{class:[e.labelClass,`q-fab__label q-fab__label--internal q-fab__label--internal-${e.labelPosition}`+(e.hideLabel?` q-fab__label--internal-hidden`:``)],style:e.labelStyle}}})}}var me=[`up`,`right`,`down`,`left`],he=[`left`,`center`,`right`],Q=180,ge=A({name:`QFab`,props:{...Z,...j,icon:String,activeIcon:String,hideIcon:Boolean,hideLabel:{...Z.hideLabel,default:null},direction:{type:String,default:`right`,validator:e=>me.includes(e)},...K,persistent:Boolean,stagger:{type:Number,default:40},verticalActionsAlign:{type:String,default:`center`,validator:e=>he.includes(e)}},emits:I,setup(t,{slots:i}){let a=d(null),o=r(t.modelValue===!0),s=re(),c=T(),{formClass:l,stacked:u,labelProps:f}=pe(t,o),p=v(()=>!t.persistent),{show:m,hide:h,toggle:g}=ne({showing:o,hideOnRouteChange:p}),_=0,{clearHoverTimer:b,hoverShow:x,hoverHide:S}=q({props:t,canShow:()=>!o.value,show:e=>{_=Date.now(),m(e)},canHide:()=>o.value,hide:h});e(o,b);function C(e){o.value&&Date.now()-_<Q||g(e)}let w=v(()=>({opened:o.value})),k=v(()=>`q-fab z-fab row inline justify-center q-fab--align-${t.verticalActionsAlign} ${l.value}`+(o.value?` q-fab--opened`:` q-fab--closed`)),A=v(()=>`q-fab__actions flex no-wrap inline q-fab__actions--${t.direction} q-fab__actions--${o.value?`opened`:`closed`}`),j=v(()=>t.hover?{onPointerenter:x,onPointerleave:S}:{}),M=v(()=>{let e={id:s.value,style:`--q-fab-stagger: ${t.stagger}ms`};return o.value||(e[`aria-hidden`]=`true`),e}),N=v(()=>`q-fab__icon-holder q-fab__icon-holder--${o.value?`opened`:`closed`}`);function P(e,n){let r=i[e],a=`q-fab__${e} absolute-full`;return r===void 0?y(E,{class:a,name:t[n]||c.iconSet.fab[n]}):y(`div`,{class:a},r(w.value))}function F(){let e=[];return t.hideIcon||e.push(y(`div`,{class:N.value},[P(`icon`,`icon`),P(`active-icon`,`activeIcon`)])),(t.label!==``||i.label!==void 0)&&e[f.value.action](y(`div`,f.value.data,i.label===void 0?[t.label]:i.label(w.value))),te(i.tooltip,e)}return n(ee,{showing:o,onChildClick(e){h(e),e?.qAvoidFocus!==!0&&a.value?.$el.focus()}}),()=>y(`div`,{class:k.value,...j.value},[y(O,{ref:a,class:l.value,...fe(t),noWrap:!0,stack:u.value,noCaps:!0,fab:!0,"aria-expanded":o.value?`true`:`false`,"aria-controls":s.value,onClick:C},F),y(`div`,{class:A.value,...M.value},D(i.default))])}}),_e={position:{type:String,default:`bottom-right`,validator:e=>[`top-right`,`top-left`,`bottom-right`,`bottom-left`,`top`,`right`,`bottom`,`left`].includes(e)},offset:{type:Array,validator:e=>e.length===2},expand:Boolean};function ve(){let{props:e}=f(),t=T(),n=s(C,k);if(n===k)return console.error(`QPageSticky needs to be child of QLayout`),k;let r=v(()=>{let t=e.position;return{top:t.includes(`top`),right:t.includes(`right`),bottom:t.includes(`bottom`),left:t.includes(`left`),vertical:t===`top`||t===`bottom`,horizontal:t===`left`||t===`right`}}),i=v(()=>n.header.offset),a=v(()=>n.right.offset),o=v(()=>n.footer.offset),c=v(()=>n.left.offset),l=v(()=>{let n=0,s=0,l=r.value,u=t.lang.rtl?-1:1;l.top&&i.value!==0?s=`${i.value}px`:l.bottom&&o.value!==0&&(s=`${-o.value}px`),l.left&&c.value!==0?n=`${u*c.value}px`:l.right&&a.value!==0&&(n=`${-u*a.value}px`);let d={transform:`translate(${n}, ${s})`};return e.offset&&(d.margin=`${e.offset[1]}px ${e.offset[0]}px`),l.vertical?(c.value!==0&&(d[t.lang.rtl?`right`:`left`]=`${c.value}px`),a.value!==0&&(d[t.lang.rtl?`left`:`right`]=`${a.value}px`)):l.horizontal&&(i.value!==0&&(d.top=`${i.value}px`),o.value!==0&&(d.bottom=`${o.value}px`)),d}),u=v(()=>`q-page-sticky row flex-center fixed-${e.position} q-page-sticky--${e.expand?`expand`:`shrink`}`);function d(t){let n=D(t.default);return y(`div`,{class:u.value,style:l.value},e.expand?n:[y(`div`,n)])}return{$layout:n,getStickyContent:d}}var ye=A({name:`QPageSticky`,props:_e,setup(e,{slots:t}){let{getStickyContent:n}=ve();return()=>n(t)}});function be(e){let t=new URLSearchParams;e.search&&t.set(`search`,e.search),e.createdFrom&&t.set(`createdFrom`,e.createdFrom),e.createdTo&&t.set(`createdTo`,e.createdTo),e.files&&e.files.length>0&&t.set(`files`,e.files.join(`,`));let n=t.toString();return n?`?${n}`:``}var $=R(`reviews`,()=>{let e=r([]),t=r(!1),n=r(null);async function i(r={}){t.value=!0,n.value=null;try{e.value=await U(`/api/reviews${be(r)}`)}catch(t){e.value=[],n.value=t instanceof Error?t.message:String(t)}finally{t.value=!1}}async function a(t){let n=await B(`/api/reviews`,t);return e.value=[n,...e.value],n}return{reviews:e,loading:t,error:n,fetchReviews:i,createReview:a}}),xe={class:`text-h6`},Se=S(p({__name:`ReviewCreateDialog`,props:{modelValue:{type:Boolean,required:!0},modelModifiers:{}},emits:b([`created`,`hide`],[`update:modelValue`]),setup(e,{emit:n}){let i=x(e,`modelValue`),s=n,{t:u}=ie(),d=$(),f=r(``),p=r(!1),_=r(null);function v(){f.value=``,p.value=!1,_.value=null}async function y(){p.value=!0,_.value=null;try{let e=f.value.trim(),t=await d.createReview(e?{name:e}:{});i.value=!1,v(),s(`created`,t)}catch(e){_.value=e instanceof W||e instanceof Error?e.message:String(e)}finally{p.value=!1}}function b(){v(),s(`hide`)}return(e,n)=>(t(),h(L,{modelValue:i.value,"onUpdate:modelValue":n[1]||=e=>i.value=e,onHide:b},{default:a(()=>[g(M,{class:`review-create-dialog`},{default:a(()=>[g(N,null,{default:a(()=>[l(`div`,xe,c(m(u)(`reviews.create.title`)),1)]),_:1}),g(N,{class:`q-gutter-y-md`},{default:a(()=>[g(F,{modelValue:f.value,"onUpdate:modelValue":n[0]||=e=>f.value=e,dense:``,outlined:``,label:m(u)(`reviews.create.nameLabel`),hint:m(u)(`reviews.create.nameHint`),error:!!_.value,"error-message":_.value??void 0,onKeyup:w(y,[`enter`])},null,8,[`modelValue`,`label`,`hint`,`error`,`error-message`])]),_:1}),g(P,{align:`right`},{default:a(()=>[o(g(oe,null,null,512),[[Y]]),g(ae,{loading:p.value,onClick:y},null,8,[`loading`])]),_:1})]),_:1})]),_:1},8,[`modelValue`]))}}),[[`__scopeId`,`data-v-c9b736c2`]]),Ce=p({__name:`CreateReviewFabButton`,emits:[`created`],setup(e,{emit:n}){let i=n,s=X(),c=J(),{activeReview:l}=z(s),{changedPaths:d}=z(c),f=r(!1),p=r(!1),m=r([18,18]),h=r(!1),y=v(()=>d.value.length===0||l.value!==null);function b(e){h.value=e.isFirst!==!0&&e.isFinal!==!0,m.value=[m.value[0]-(e.delta?.x??0),m.value[1]-(e.delta?.y??0)]}function x(e){s.setActiveReview(e),i(`created`,e)}return(e,n)=>(t(),u(_,null,[g(ye,{position:`bottom-right`,offset:m.value},{default:a(()=>[o(g(ge,{modelValue:p.value,"onUpdate:modelValue":n[0]||=e=>p.value=e,icon:`add`,color:`accent`,disable:y.value||h.value,onClick:n[1]||=e=>f.value=!0},null,8,[`modelValue`,`disable`]),[[G,b,void 0,{prevent:!0,mouse:!0}]])]),_:1},8,[`offset`]),g(Se,{modelValue:f.value,"onUpdate:modelValue":n[2]||=e=>f.value=e,onCreated:x,onHide:n[3]||=e=>p.value=!1},null,8,[`modelValue`])],64))}});export{X as a,se as i,$ as n,ce as r,Ce as t};
@@ -1 +1 @@
1
- import{M as e,c as t,d as n,m as r}from"./runtime-core.esm-bundler-Wxr8ezGp.js";import{C as i,t as a}from"./QBtn-Cf6WtiKt.js";var o={},s={class:`fullscreen bg-blue text-white text-center q-pa-md flex flex-center`};function c(i,o){return e(),n(`div`,s,[t(`div`,null,[o[0]||=t(`div`,{style:{"font-size":`30vh`}},`404`,-1),o[1]||=t(`div`,{class:`text-h2`,style:{opacity:`0.4`}},`Oops. Nothing here...`,-1),r(a,{class:`q-mt-xl`,color:`white`,"text-color":`blue`,unelevated:``,to:`/`,label:`Go Home`,"no-caps":``})])])}var l=i(o,[[`render`,c]]);export{l as default};
1
+ import{M as e,c as t,d as n,m as r}from"./runtime-core.esm-bundler-Db5Q9Gsb.js";import{C as i,t as a}from"./QBtn-DGiC5uLo.js";var o={},s={class:`fullscreen bg-blue text-white text-center q-pa-md flex flex-center`};function c(i,o){return e(),n(`div`,s,[t(`div`,null,[o[0]||=t(`div`,{style:{"font-size":`30vh`}},`404`,-1),o[1]||=t(`div`,{class:`text-h2`,style:{opacity:`0.4`}},`Oops. Nothing here...`,-1),r(a,{class:`q-mt-xl`,color:`white`,"text-color":`blue`,unelevated:``,to:`/`,label:`Go Home`,"no-caps":``})])])}var l=i(o,[[`render`,c]]);export{l as default};
@@ -1 +1 @@
1
- .file-explorer[data-v-1ce1b104]{height:100%}[data-v-1ce1b104] .file-explorer__panel{flex-direction:column;flex-shrink:0;min-width:0;height:100%;display:flex}[data-v-1ce1b104] .file-explorer__content{flex-direction:column;flex-wrap:no-wrap;display:flex}.file-tree-node__icon-section[data-v-43168dd7]{min-width:0!important;padding-right:6px!important}.file-tree-node__label[data-v-43168dd7]{width:100%;min-width:0}.file-tree-node__chevron[data-v-43168dd7]{transition:transform .15s}.file-tree-node__chevron--expanded[data-v-43168dd7]{transform:rotate(90deg)}.file-tree-node--selected[data-v-43168dd7]{font-weight:500}.file-list-item__icon-section[data-v-77d2a0e7]{min-width:0!important;padding-right:6px!important}.file-list-item__path-label[data-v-77d2a0e7]{width:100%;min-width:0}.file-list-item--selected[data-v-77d2a0e7],[data-v-77d2a0e7] .file-list-item--selected{font-weight:500}.file-list-item__actions[data-v-77d2a0e7]{gap:2px;flex-direction:row!important;padding:0 0 0 6px!important}.file-list-item__stats[data-v-77d2a0e7]{padding-right:0!important}.file-list-panel__scroll[data-v-704ed3e1] .q-scrollarea__content{width:100%}.active-review-bar[data-v-725a73f5]{min-height:36px}
1
+ .file-explorer[data-v-1ce1b104]{height:100%}[data-v-1ce1b104] .file-explorer__panel{flex-direction:column;flex-shrink:0;min-width:0;height:100%;display:flex}[data-v-1ce1b104] .file-explorer__content{flex-direction:column;flex-wrap:no-wrap;display:flex}.file-tree-node__icon-section[data-v-43168dd7]{min-width:0!important;padding-right:6px!important}.file-tree-node__label[data-v-43168dd7]{width:100%;min-width:0}.file-tree-node__chevron[data-v-43168dd7]{transition:transform .15s}.file-tree-node__chevron--expanded[data-v-43168dd7]{transform:rotate(90deg)}.file-tree-node--selected[data-v-43168dd7]{font-weight:500}.file-list-item__icon-section[data-v-77d2a0e7]{min-width:0!important;padding-right:6px!important}.file-list-item__path-label[data-v-77d2a0e7]{width:100%;min-width:0}.file-list-item--selected[data-v-77d2a0e7],[data-v-77d2a0e7] .file-list-item--selected{font-weight:500}.file-list-item__actions[data-v-77d2a0e7]{gap:2px;flex-direction:row!important;padding:0 0 0 6px!important}.file-list-item__stats[data-v-77d2a0e7]{padding-right:0!important}.file-list-panel__scroll[data-v-32cc9fb2] .q-scrollarea__content{width:100%}.active-review-bar[data-v-725a73f5]{min-height:36px}