@cat-factory/app 0.147.1 → 0.147.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.
@@ -1,17 +1,22 @@
1
1
  import { describe, it, expect, beforeEach, vi } from 'vitest'
2
2
  import { ApiError } from '~/composables/api/errors'
3
- import { usePersonalSubscriptionsStore } from '~/stores/personalSubscriptions'
3
+ import {
4
+ personalPasswordCacheKey,
5
+ usePersonalSubscriptionsStore,
6
+ } from '~/stores/personalSubscriptions'
4
7
 
5
- // The single localStorage key the store caches the personal password under (private to the
6
- // store; hard-coded here so the buffer/expiry semantics can be asserted directly).
7
- const CACHE_KEY = 'cf.personal-pw'
8
+ // The scoped localStorage key the store caches under, for the stubbed apiBase ('') + user
9
+ // (null 'anon') the default test setup provides (see test/setup.ts). ADR 0026 D7.
10
+ const CACHE_KEY = personalPasswordCacheKey('', null)
11
+ /** The retired pre-scoping global key. */
12
+ const LEGACY_CACHE_KEY = 'cf.personal-pw'
8
13
  const HOUR = 60 * 60 * 1000
9
14
  /** Mirrors PASSWORD_EXPIRY_BUFFER_MS in the store — the runway a key must have to be ridden. */
10
15
  const BUFFER_MS = 8 * HOUR
11
16
 
12
17
  /** Write a cache entry with an explicit remaining lifetime (positive = valid, negative = past). */
13
- function seedCache(password: string, msFromNow: number) {
14
- localStorage.setItem(CACHE_KEY, JSON.stringify({ password, expiresAt: Date.now() + msFromNow }))
18
+ function seedCache(password: string, msFromNow: number, key = CACHE_KEY) {
19
+ localStorage.setItem(key, JSON.stringify({ password, expiresAt: Date.now() + msFromNow }))
15
20
  }
16
21
 
17
22
  /** A 428 credential_required error shaped like the server envelope the store parses. */
@@ -61,6 +66,52 @@ describe('personal-password cache expiry buffer', () => {
61
66
  })
62
67
  })
63
68
 
69
+ // ADR 0026 D7: the cache is scoped per installation (apiBase) + user, and the retired global
70
+ // `cf.personal-pw` key is purged on sight and never reused.
71
+ describe('per-installation + per-user cache scoping', () => {
72
+ beforeEach(() => {
73
+ localStorage.clear()
74
+ })
75
+
76
+ it('derives distinct keys per installation and per user', () => {
77
+ // Same origin, different installations (apiBase) → different keys.
78
+ expect(personalPasswordCacheKey('https://a.example', 'usr_1')).not.toBe(
79
+ personalPasswordCacheKey('https://b.example', 'usr_1'),
80
+ )
81
+ // Same installation, different users → different keys.
82
+ expect(personalPasswordCacheKey('https://a.example', 'usr_1')).not.toBe(
83
+ personalPasswordCacheKey('https://a.example', 'usr_2'),
84
+ )
85
+ // A missing user id collapses to a stable `anon` segment (auth disabled).
86
+ expect(personalPasswordCacheKey('https://a.example', null)).toBe(
87
+ personalPasswordCacheKey('https://a.example', undefined),
88
+ )
89
+ })
90
+
91
+ it('does NOT read another installation/user cache entry', () => {
92
+ const store = usePersonalSubscriptionsStore()
93
+ // A healthy entry belonging to a DIFFERENT scope must be invisible to this store.
94
+ seedCache('other-secret', 40 * HOUR, personalPasswordCacheKey('https://other', 'usr_x'))
95
+ expect(store.getCachedPassword()).toBeUndefined()
96
+ })
97
+
98
+ it('purges the retired global `cf.personal-pw` key on read and never reuses it', () => {
99
+ const store = usePersonalSubscriptionsStore()
100
+ seedCache('legacy-secret', 40 * HOUR, LEGACY_CACHE_KEY)
101
+ // The legacy value is not offered (the scoped key is absent)…
102
+ expect(store.getCachedPassword()).toBeUndefined()
103
+ // …and the retired global key is removed so it can't be reused across installs/users.
104
+ expect(localStorage.getItem(LEGACY_CACHE_KEY)).toBeNull()
105
+ })
106
+
107
+ it('round-trips a password under the scoped key', () => {
108
+ const store = usePersonalSubscriptionsStore()
109
+ seedCache('mine', 40 * HOUR)
110
+ expect(store.getCachedPassword()).toBe('mine')
111
+ expect(localStorage.getItem(CACHE_KEY)).not.toBeNull()
112
+ })
113
+ })
114
+
64
115
  describe('withCredential early re-entry', () => {
65
116
  beforeEach(() => {
66
117
  localStorage.clear()
@@ -12,12 +12,22 @@ import type {
12
12
  // a personal PASSWORD. The token itself is double-encrypted server-side and never returned;
13
13
  // this store only carries metadata + the renewal warning.
14
14
  //
15
- // To keep the password mostly transparent, it is cached CLIENT-SIDE in localStorage under a
16
- // SINGLE key with a TTL: the backend gate applies one password to ALL of a run's
17
- // individual-usage vendors, so there's no point keying the cache per vendor. A task
18
- // start/retry (and each interaction with a live run) rides along with the cached password —
19
- // sent as the `X-Personal-Password` header — and the user is only re-prompted once it
20
- // expires (or is wrong).
15
+ // To keep the password mostly transparent, it is cached CLIENT-SIDE in localStorage with a
16
+ // TTL: the backend gate applies one password to ALL of a run's individual-usage vendors, so
17
+ // there's no point keying the cache per vendor. A task start/retry (and each interaction with a
18
+ // live run) rides along with the cached password — sent as the `X-Personal-Password` header —
19
+ // and the user is only re-prompted once it expires (or is wrong).
20
+ //
21
+ // The cache key is scoped PER INSTALLATION and PER USER (ADR 0026 D7): `cf.personal-pw:<hash of
22
+ // the configured API base>:<user id>`. The bare `cf.personal-pw` it replaced was keyed only by
23
+ // browser origin, so on a shared origin (two local installs on localhost, or one hosted origin
24
+ // fronting several deployments) one installation's cached password was offered to another as
25
+ // `X-Personal-Password`, and a second signed-in user on a shared browser profile inherited the
26
+ // first user's password. Keying on the API base (distinct per installation even when the origin
27
+ // is shared) and the user id closes both: another installation or user sees no cache and is
28
+ // challenged normally. Per-workspace scoping is NOT added — the password is a per-user secret
29
+ // (the backend applies one to all of a run's individual-usage vendors), so it would only add
30
+ // redundant prompts.
21
31
  //
22
32
  // Every gated action (start / retry / confirm) re-validates the cache against an 8h EXPIRY
23
33
  // BUFFER: a key with less than that runway left is withheld (treated as absent) so the
@@ -43,7 +53,37 @@ const PASSWORD_TTL_MS = 40 * 60 * 60 * 1000
43
53
  * EARLY — a buffer wide enough that a pipeline kicked off now won't outlive the key (8h).
44
54
  */
45
55
  const PASSWORD_EXPIRY_BUFFER_MS = 8 * 60 * 60 * 1000
46
- const CACHE_KEY = 'cf.personal-pw'
56
+
57
+ /** Prefix for the per-installation + per-user cache key (see the file header). */
58
+ const CACHE_KEY_PREFIX = 'cf.personal-pw'
59
+ /**
60
+ * The pre-scoping GLOBAL key (origin-only), retired by ADR 0026 D7. It is purged on sight and
61
+ * its value is NEVER reused — migrating it would re-introduce exactly the cross-installation /
62
+ * cross-user reuse the scoping removes, so the affected user is simply re-challenged once.
63
+ */
64
+ const LEGACY_CACHE_KEY = 'cf.personal-pw'
65
+
66
+ /** A short, stable non-cryptographic hash (FNV-1a → base36) of the API base, for the key scope. */
67
+ function scopeHash(input: string): string {
68
+ let h = 0x811c9dc5
69
+ for (let i = 0; i < input.length; i++) {
70
+ h ^= input.charCodeAt(i)
71
+ h = Math.imul(h, 0x01000193)
72
+ }
73
+ return (h >>> 0).toString(36)
74
+ }
75
+
76
+ /**
77
+ * The localStorage key the personal password is cached under, scoped per installation (the
78
+ * configured `apiBase`, hashed) and per user (`userId`, or `anon` when auth is off / nobody is
79
+ * signed in). Exported for the store's unit test to seed/read the exact key the store uses.
80
+ */
81
+ export function personalPasswordCacheKey(
82
+ apiBase: string,
83
+ userId: string | null | undefined,
84
+ ): string {
85
+ return `${CACHE_KEY_PREFIX}:${scopeHash(apiBase)}:${userId ?? 'anon'}`
86
+ }
47
87
 
48
88
  /** A credential prompt the UI must satisfy (set when the server replies 428). */
49
89
  export interface PendingCredential {
@@ -71,6 +111,14 @@ export function parseCredentialError(
71
111
 
72
112
  export const usePersonalSubscriptionsStore = defineStore('personalSubscriptions', () => {
73
113
  const api = useApi()
114
+ const auth = useAuthStore()
115
+ // The configured API base is a static per-installation fact, so hash it once at setup; the user
116
+ // id is dynamic, so it is read lazily off the auth store when the key is computed.
117
+ const apiBase = String(useRuntimeConfig().public.apiBase ?? '')
118
+ /** The current per-installation + per-user cache key. */
119
+ function cacheKey(): string {
120
+ return personalPasswordCacheKey(apiBase, auth.user?.id)
121
+ }
74
122
  const subscriptions = ref<PersonalSubscriptionStatus[]>([])
75
123
  const loading = ref(false)
76
124
  /** When set, a credential modal should open to satisfy the pending prompt. */
@@ -119,12 +167,15 @@ export const usePersonalSubscriptionsStore = defineStore('personalSubscriptions'
119
167
  */
120
168
  function getCachedPassword(bufferMs = 0): string | undefined {
121
169
  if (typeof localStorage === 'undefined') return undefined
170
+ // Retire the pre-scoping global key on sight (never reused — see LEGACY_CACHE_KEY).
171
+ purgeLegacyCache()
122
172
  try {
123
- const raw = localStorage.getItem(CACHE_KEY)
173
+ const key = cacheKey()
174
+ const raw = localStorage.getItem(key)
124
175
  if (!raw) return undefined
125
176
  const { password, expiresAt } = JSON.parse(raw) as { password: string; expiresAt: number }
126
177
  if (Date.now() > expiresAt) {
127
- localStorage.removeItem(CACHE_KEY)
178
+ localStorage.removeItem(key)
128
179
  return undefined
129
180
  }
130
181
  // Within the buffer the key is still valid — keep it, but withhold it so the action
@@ -140,7 +191,7 @@ export const usePersonalSubscriptionsStore = defineStore('personalSubscriptions'
140
191
  if (typeof localStorage === 'undefined') return
141
192
  try {
142
193
  localStorage.setItem(
143
- CACHE_KEY,
194
+ cacheKey(),
144
195
  JSON.stringify({ password, expiresAt: Date.now() + PASSWORD_TTL_MS }),
145
196
  )
146
197
  } catch {
@@ -151,7 +202,17 @@ export const usePersonalSubscriptionsStore = defineStore('personalSubscriptions'
151
202
  function clearCachedPassword() {
152
203
  if (typeof localStorage === 'undefined') return
153
204
  try {
154
- localStorage.removeItem(CACHE_KEY)
205
+ localStorage.removeItem(cacheKey())
206
+ } catch {
207
+ // best-effort
208
+ }
209
+ }
210
+
211
+ /** Remove the retired global `cf.personal-pw` key so its value can never be offered again. */
212
+ function purgeLegacyCache() {
213
+ if (typeof localStorage === 'undefined') return
214
+ try {
215
+ localStorage.removeItem(LEGACY_CACHE_KEY)
155
216
  } catch {
156
217
  // best-effort
157
218
  }
@@ -1,6 +1,11 @@
1
1
  import { describe, it, expect } from 'vitest'
2
- import type { StepSubtasks } from '~/types/execution'
3
- import { activeChunkLabels, chunkReviewPercent, isSlicingChunks } from './prReviewProgress'
2
+ import type { PrReviewStepState, StepSubtasks } from '~/types/execution'
3
+ import {
4
+ activeChunkLabels,
5
+ chunkReviewPercent,
6
+ hasNoSlicePlan,
7
+ prReviewPhase,
8
+ } from './prReviewProgress'
4
9
 
5
10
  const subtasks = (over: Partial<StepSubtasks>): StepSubtasks => ({
6
11
  completed: 0,
@@ -9,18 +14,19 @@ const subtasks = (over: Partial<StepSubtasks>): StepSubtasks => ({
9
14
  ...over,
10
15
  })
11
16
 
12
- describe('isSlicingChunks', () => {
13
- it('is slicing when there is no todo list yet (null/undefined or empty)', () => {
14
- // No plan committed the reviewer is still grouping the diff into chunks.
15
- expect(isSlicingChunks(null)).toBe(true)
16
- expect(isSlicingChunks(undefined)).toBe(true)
17
- expect(isSlicingChunks(subtasks({ total: 0 }))).toBe(true)
17
+ describe('hasNoSlicePlan', () => {
18
+ it('has no plan when there is no todo list yet (null/undefined or empty)', () => {
19
+ // No per-slice plan reported a NEUTRAL signal (grouping, OR a subagent review that never
20
+ // writes a parent plan), NOT proof of "slicing".
21
+ expect(hasNoSlicePlan(null)).toBe(true)
22
+ expect(hasNoSlicePlan(undefined)).toBe(true)
23
+ expect(hasNoSlicePlan(subtasks({ total: 0 }))).toBe(true)
18
24
  })
19
25
 
20
- it('is not slicing once the todo list exists (slicing is done)', () => {
21
- expect(isSlicingChunks(subtasks({ total: 3, completed: 1 }))).toBe(false)
22
- // A single-chunk plan still counts as sliced — a plan of size 1 is a committed plan.
23
- expect(isSlicingChunks(subtasks({ total: 1 }))).toBe(false)
26
+ it('has a plan once the todo list exists', () => {
27
+ expect(hasNoSlicePlan(subtasks({ total: 3, completed: 1 }))).toBe(false)
28
+ // A single-slice plan still counts — a plan of size 1 is a committed plan.
29
+ expect(hasNoSlicePlan(subtasks({ total: 1 }))).toBe(false)
24
30
  })
25
31
  })
26
32
 
@@ -71,3 +77,48 @@ describe('activeChunkLabels', () => {
71
77
  ).toEqual([])
72
78
  })
73
79
  })
80
+
81
+ // `prReviewPhase` reads only `status`; the other PrReviewStepState fields are irrelevant here.
82
+ const state = (status: PrReviewStepState['status']): PrReviewStepState =>
83
+ ({ status }) as PrReviewStepState
84
+
85
+ describe('prReviewPhase', () => {
86
+ it('is null with no live review or a terminal/passed-through status', () => {
87
+ expect(prReviewPhase(null, null)).toBeNull()
88
+ expect(prReviewPhase(undefined, subtasks({ total: 3, completed: 3 }))).toBeNull()
89
+ expect(prReviewPhase(state('done'), subtasks({ total: 3, completed: 3 }))).toBeNull()
90
+ expect(prReviewPhase(state('skipped'), null)).toBeNull()
91
+ })
92
+
93
+ it('is the neutral `planning` kind while reviewing with no todo list yet (counts zeroed)', () => {
94
+ // No plan reported → surface a neutral "reviewing, planning" phase (NOT "slicing") and don't
95
+ // leak a misleading 0/0 slice count.
96
+ expect(prReviewPhase(state('reviewing'), null)).toEqual({
97
+ kind: 'planning',
98
+ completed: 0,
99
+ total: 0,
100
+ })
101
+ expect(prReviewPhase(state('reviewing'), subtasks({ total: 0 }))).toEqual({
102
+ kind: 'planning',
103
+ completed: 0,
104
+ total: 0,
105
+ })
106
+ })
107
+
108
+ it('is reviewing with the slice counts once the todo list exists', () => {
109
+ expect(prReviewPhase(state('reviewing'), subtasks({ total: 4, completed: 1 }))).toEqual({
110
+ kind: 'reviewing',
111
+ completed: 1,
112
+ total: 4,
113
+ })
114
+ })
115
+
116
+ it('maps the parked / resolving statuses to their phase', () => {
117
+ expect(
118
+ prReviewPhase(state('awaiting_selection'), subtasks({ total: 4, completed: 4 }))?.kind,
119
+ ).toBe('awaiting')
120
+ expect(prReviewPhase(state('challenging'), null)?.kind).toBe('challenging')
121
+ expect(prReviewPhase(state('fixing'), null)?.kind).toBe('fixing')
122
+ expect(prReviewPhase(state('posting'), null)?.kind).toBe('posting')
123
+ })
124
+ })
@@ -1,16 +1,23 @@
1
- import type { StepSubtasks } from '~/types/execution'
1
+ import type { PrReviewStepState, StepSubtasks } from '~/types/execution'
2
2
 
3
3
  // Pure derivation of the PR deep-reviewer's `reviewing`-phase progress, factored out of
4
4
  // `PrReviewWindow.vue` so it is unit-testable independently of the Vue component.
5
5
  //
6
- // The reviewer maintains a per-slice todo list (`step.subtasks`) once it has grouped the diff
7
- // into cohesive chunks. The PRESENCE of that list is the signal that slicing is done, so the two
8
- // `reviewing` sub-phases are told apart by it:
9
- // - no todo list yet (`total === 0`) still SLICING the diff into chunks (no plan committed).
10
- // - todo list present (`total > 0`) slicing DONE, working through the chunks.
6
+ // When the reviewer maintains a per-slice todo list (`step.subtasks`) one entry per cohesive
7
+ // chunk of the diff the UI shows per-slice progress. But that list is NOT always present: the
8
+ // reviewer often reviews via parallel general-purpose subagents, which never write a parent-level
9
+ // TodoWrite plan, so `subtasks` stays empty for the whole review (ADR 0026 P2/D2.2). So an empty
10
+ // todo list is NOT proof the reviewer is "still slicing" it only means no per-slice plan has
11
+ // been reported yet. We therefore surface a NEUTRAL "reviewing, planning slices" state and switch
12
+ // to per-slice status the moment a plan exists — the UI never asserts a specific "slicing" phase
13
+ // purely because the parent stream emitted no todo list.
11
14
 
12
- /** True while the reviewer is still grouping the diff into chunks (it has not committed a plan). */
13
- export function isSlicingChunks(subtasks: StepSubtasks | null | undefined): boolean {
15
+ /**
16
+ * True while NO per-slice todo plan has been reported (`total <= 0`). This is a neutral
17
+ * "no plan yet" signal — the reviewer may be grouping the diff OR reviewing via subagents that
18
+ * don't write a parent plan — NOT an assertion that it is "still slicing" (see the note above).
19
+ */
20
+ export function hasNoSlicePlan(subtasks: StepSubtasks | null | undefined): boolean {
14
21
  return (subtasks?.total ?? 0) <= 0
15
22
  }
16
23
 
@@ -26,3 +33,58 @@ export function chunkReviewPercent(subtasks: StepSubtasks | null | undefined): n
26
33
  export function activeChunkLabels(subtasks: StepSubtasks | null | undefined): string[] {
27
34
  return subtasks?.items?.filter((i) => i.status === 'in_progress').map((i) => i.label) ?? []
28
35
  }
36
+
37
+ /**
38
+ * The at-a-glance phase of a `pr-reviewer` step, collapsing its `prReview.status` (+ the
39
+ * has-a-plan signal from the todo list) into a single kind the board surfaces label without
40
+ * re-deriving. `completed`/`total` carry the slice counts for the `reviewing` kind. The
41
+ * `planning` kind is the NEUTRAL "reviewing, no per-slice plan reported yet" state (see
42
+ * {@link hasNoSlicePlan}) — it deliberately does NOT claim a specific "slicing" phase, since an
43
+ * empty todo list is the normal shape of a subagent-driven review, not proof of slicing.
44
+ * Returns `null` for the terminal / passed-through states (`done`/`skipped`) and when there is
45
+ * no live review — those have no in-flight phase to show.
46
+ */
47
+ export type PrReviewPhaseKind =
48
+ | 'planning'
49
+ | 'reviewing'
50
+ | 'awaiting'
51
+ | 'challenging'
52
+ | 'fixing'
53
+ | 'posting'
54
+
55
+ export interface PrReviewPhase {
56
+ kind: PrReviewPhaseKind
57
+ /** Slices whose review is finished (0 while planning). */
58
+ completed: number
59
+ /** Total slices the reviewer grouped the diff into (0 while planning). */
60
+ total: number
61
+ }
62
+
63
+ export function prReviewPhase(
64
+ state: PrReviewStepState | null | undefined,
65
+ subtasks: StepSubtasks | null | undefined,
66
+ ): PrReviewPhase | null {
67
+ const status = state?.status
68
+ if (!status) return null
69
+ const completed = subtasks?.completed ?? 0
70
+ const total = subtasks?.total ?? 0
71
+ switch (status) {
72
+ case 'reviewing':
73
+ // No per-slice plan reported yet ⇒ neutral "planning" (don't claim "slicing"); once a plan
74
+ // exists, work through the chunks with live counts.
75
+ return hasNoSlicePlan(subtasks)
76
+ ? { kind: 'planning', completed: 0, total: 0 }
77
+ : { kind: 'reviewing', completed, total }
78
+ case 'awaiting_selection':
79
+ return { kind: 'awaiting', completed, total }
80
+ case 'challenging':
81
+ return { kind: 'challenging', completed, total }
82
+ case 'fixing':
83
+ return { kind: 'fixing', completed, total }
84
+ case 'posting':
85
+ return { kind: 'posting', completed, total }
86
+ default:
87
+ // done / skipped — no live phase to surface.
88
+ return null
89
+ }
90
+ }
@@ -4943,9 +4943,9 @@
4943
4943
  "suggestedFix": "Lösungsvorschlag:",
4944
4944
  "line": "Zeile {line}",
4945
4945
  "reviewing": {
4946
- "slicing": {
4947
- "title": "Diff wird in Abschnitte zerlegt…",
4948
- "hint": "Die geänderten Dateien werden in zusammenhängende Abschnitte gruppiert, damit jeder einzeln geprüft werden kann. Die Abschnittsliste erscheint hier, sobald die Zerlegung abgeschlossen ist."
4946
+ "planning": {
4947
+ "title": "Pull Request wird geprüft…",
4948
+ "hint": "Der Prüfer arbeitet den Diff durch. Der Fortschritt pro Abschnitt erscheint hier, sobald die Änderungen in Abschnitte gruppiert werden."
4949
4949
  },
4950
4950
  "reviewingChunks": {
4951
4951
  "title": "Abschnitte werden geprüft…",
@@ -4961,6 +4961,14 @@
4961
4961
  "pending": "In Warteschlange"
4962
4962
  }
4963
4963
  },
4964
+ "phase": {
4965
+ "planning": "Wird geprüft…",
4966
+ "reviewing": "Prüfe {completed}/{total} Abschnitte",
4967
+ "awaiting": "Ergebnisse bereit",
4968
+ "challenging": "Wird untersucht…",
4969
+ "fixing": "Wird behoben…",
4970
+ "posting": "Wird veröffentlicht…"
4971
+ },
4964
4972
  "challenge": {
4965
4973
  "action": "Anfechten",
4966
4974
  "reChallenge": "Erneut anfechten",
@@ -5072,9 +5072,9 @@
5072
5072
  "suggestedFix": "Suggested fix:",
5073
5073
  "line": "line {line}",
5074
5074
  "reviewing": {
5075
- "slicing": {
5076
- "title": "Slicing the diff into chunks…",
5077
- "hint": "Grouping the changed files into cohesive chunks so each can be reviewed on its own. The chunk list appears here once slicing is done."
5075
+ "planning": {
5076
+ "title": "Reviewing the pull request…",
5077
+ "hint": "The reviewer is working through the diff. Per-slice progress appears here once it groups the changes into slices."
5078
5078
  },
5079
5079
  "reviewingChunks": {
5080
5080
  "title": "Reviewing the chunks…",
@@ -5090,6 +5090,14 @@
5090
5090
  "pending": "Queued"
5091
5091
  }
5092
5092
  },
5093
+ "phase": {
5094
+ "planning": "Reviewing…",
5095
+ "reviewing": "Reviewing {completed}/{total} slices",
5096
+ "awaiting": "Findings ready",
5097
+ "challenging": "Investigating…",
5098
+ "fixing": "Fixing…",
5099
+ "posting": "Posting…"
5100
+ },
5093
5101
  "challenge": {
5094
5102
  "action": "Challenge",
5095
5103
  "reChallenge": "Challenge again",
@@ -4931,9 +4931,9 @@
4931
4931
  "suggestedFix": "Corrección sugerida:",
4932
4932
  "line": "línea {line}",
4933
4933
  "reviewing": {
4934
- "slicing": {
4935
- "title": "Dividiendo el diff en fragmentos…",
4936
- "hint": "Agrupando los archivos modificados en fragmentos coherentes para revisar cada uno por separado. La lista de fragmentos aparecerá aquí cuando termine la división."
4934
+ "planning": {
4935
+ "title": "Revisando la pull request…",
4936
+ "hint": "El revisor está analizando el diff. El progreso por sección aparecerá aquí cuando agrupe los cambios en secciones."
4937
4937
  },
4938
4938
  "reviewingChunks": {
4939
4939
  "title": "Revisando los fragmentos…",
@@ -4949,6 +4949,14 @@
4949
4949
  "pending": "En cola"
4950
4950
  }
4951
4951
  },
4952
+ "phase": {
4953
+ "planning": "Revisando…",
4954
+ "reviewing": "Revisando {completed}/{total} secciones",
4955
+ "awaiting": "Resultados listos",
4956
+ "challenging": "Investigando…",
4957
+ "fixing": "Corrigiendo…",
4958
+ "posting": "Publicando…"
4959
+ },
4952
4960
  "challenge": {
4953
4961
  "action": "Cuestionar",
4954
4962
  "reChallenge": "Cuestionar de nuevo",
@@ -4931,9 +4931,9 @@
4931
4931
  "suggestedFix": "Correction suggérée :",
4932
4932
  "line": "ligne {line}",
4933
4933
  "reviewing": {
4934
- "slicing": {
4935
- "title": "Découpage du diff en sections…",
4936
- "hint": "Regroupement des fichiers modifiés en sections cohérentes afin de pouvoir examiner chacune séparément. La liste des sections apparaîtra ici une fois le découpage terminé."
4934
+ "planning": {
4935
+ "title": "Révision de la pull request…",
4936
+ "hint": "Le relecteur parcourt le diff. La progression par section apparaîtra ici une fois les modifications regroupées en sections."
4937
4937
  },
4938
4938
  "reviewingChunks": {
4939
4939
  "title": "Revue des sections…",
@@ -4949,6 +4949,14 @@
4949
4949
  "pending": "En attente"
4950
4950
  }
4951
4951
  },
4952
+ "phase": {
4953
+ "planning": "Révision…",
4954
+ "reviewing": "Révision {completed}/{total} sections",
4955
+ "awaiting": "Résultats prêts",
4956
+ "challenging": "Analyse en cours…",
4957
+ "fixing": "Correction…",
4958
+ "posting": "Publication…"
4959
+ },
4952
4960
  "challenge": {
4953
4961
  "action": "Contester",
4954
4962
  "reChallenge": "Contester à nouveau",
@@ -4942,9 +4942,9 @@
4942
4942
  "suggestedFix": "תיקון מוצע:",
4943
4943
  "line": "שורה {line}",
4944
4944
  "reviewing": {
4945
- "slicing": {
4946
- "title": "מחלק את ההבדלים למקטעים…",
4947
- "hint": "מקבץ את הקבצים שהשתנו למקטעים לכידים כדי לבדוק כל אחד בנפרד. רשימת המקטעים תופיע כאן לאחר סיום החלוקה."
4945
+ "planning": {
4946
+ "title": "בודק את בקשת המשיכה…",
4947
+ "hint": "הבודק עובר על ההבדלים. ההתקדמות לפי מקטע תופיע כאן לאחר שהשינויים יקובצו למקטעים."
4948
4948
  },
4949
4949
  "reviewingChunks": {
4950
4950
  "title": "בודק את המקטעים…",
@@ -4960,6 +4960,14 @@
4960
4960
  "pending": "בתור"
4961
4961
  }
4962
4962
  },
4963
+ "phase": {
4964
+ "planning": "בודק…",
4965
+ "reviewing": "בודק {completed}/{total} מקטעים",
4966
+ "awaiting": "הממצאים מוכנים",
4967
+ "challenging": "חוקר…",
4968
+ "fixing": "מתקן…",
4969
+ "posting": "מפרסם…"
4970
+ },
4963
4971
  "challenge": {
4964
4972
  "action": "ערער",
4965
4973
  "reChallenge": "ערער שוב",
@@ -4943,9 +4943,9 @@
4943
4943
  "suggestedFix": "Correzione suggerita:",
4944
4944
  "line": "riga {line}",
4945
4945
  "reviewing": {
4946
- "slicing": {
4947
- "title": "Suddivisione del diff in blocchi…",
4948
- "hint": "Raggruppamento dei file modificati in blocchi coerenti per esaminare ciascuno separatamente. L'elenco dei blocchi comparirà qui al termine della suddivisione."
4946
+ "planning": {
4947
+ "title": "Revisione della pull request…",
4948
+ "hint": "Il revisore sta esaminando il diff. L'avanzamento per sezione comparirà qui una volta raggruppate le modifiche in sezioni."
4949
4949
  },
4950
4950
  "reviewingChunks": {
4951
4951
  "title": "Revisione dei blocchi…",
@@ -4961,6 +4961,14 @@
4961
4961
  "pending": "In coda"
4962
4962
  }
4963
4963
  },
4964
+ "phase": {
4965
+ "planning": "Revisione…",
4966
+ "reviewing": "Revisione {completed}/{total} sezioni",
4967
+ "awaiting": "Risultati pronti",
4968
+ "challenging": "Analisi in corso…",
4969
+ "fixing": "Correzione…",
4970
+ "posting": "Pubblicazione…"
4971
+ },
4964
4972
  "challenge": {
4965
4973
  "action": "Contesta",
4966
4974
  "reChallenge": "Contesta di nuovo",
@@ -4943,9 +4943,9 @@
4943
4943
  "suggestedFix": "修正案:",
4944
4944
  "line": "{line}行目",
4945
4945
  "reviewing": {
4946
- "slicing": {
4947
- "title": "差分をチャンクに分割中…",
4948
- "hint": "変更されたファイルをまとまりのあるチャンクにグループ化し、各チャンクを個別にレビューできるようにしています。分割が完了すると、ここにチャンク一覧が表示されます。"
4946
+ "planning": {
4947
+ "title": "プルリクエストをレビュー中…",
4948
+ "hint": "レビュー担当者が差分を確認しています。変更が区分にグループ化されると、区分ごとの進捗がここに表示されます。"
4949
4949
  },
4950
4950
  "reviewingChunks": {
4951
4951
  "title": "チャンクをレビュー中…",
@@ -4961,6 +4961,14 @@
4961
4961
  "pending": "待機中"
4962
4962
  }
4963
4963
  },
4964
+ "phase": {
4965
+ "planning": "レビュー中…",
4966
+ "reviewing": "レビュー中 {completed}/{total} 区分",
4967
+ "awaiting": "指摘の準備完了",
4968
+ "challenging": "調査中…",
4969
+ "fixing": "修正中…",
4970
+ "posting": "投稿中…"
4971
+ },
4964
4972
  "challenge": {
4965
4973
  "action": "異議を唱える",
4966
4974
  "reChallenge": "再度異議を唱える",
@@ -4931,9 +4931,9 @@
4931
4931
  "suggestedFix": "Sugerowana poprawka:",
4932
4932
  "line": "wiersz {line}",
4933
4933
  "reviewing": {
4934
- "slicing": {
4935
- "title": "Dzielenie zmian na fragmenty…",
4936
- "hint": "Grupowanie zmienionych plików w spójne fragmenty, aby każdy można było przejrzeć osobno. Lista fragmentów pojawi się tutaj po zakończeniu podziału."
4934
+ "planning": {
4935
+ "title": "Przeglądanie pull requesta…",
4936
+ "hint": "Recenzent analizuje zmiany. Postęp poszczególnych fragmentów pojawi się tutaj po pogrupowaniu zmian we fragmenty."
4937
4937
  },
4938
4938
  "reviewingChunks": {
4939
4939
  "title": "Przeglądanie fragmentów…",
@@ -4949,6 +4949,14 @@
4949
4949
  "pending": "W kolejce"
4950
4950
  }
4951
4951
  },
4952
+ "phase": {
4953
+ "planning": "Przegląd…",
4954
+ "reviewing": "Przegląd {completed}/{total} fragmentów",
4955
+ "awaiting": "Wyniki gotowe",
4956
+ "challenging": "Analizowanie…",
4957
+ "fixing": "Naprawianie…",
4958
+ "posting": "Publikowanie…"
4959
+ },
4952
4960
  "challenge": {
4953
4961
  "action": "Zakwestionuj",
4954
4962
  "reChallenge": "Zakwestionuj ponownie",