@cat-factory/app 0.147.2 → 0.147.4
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/app/components/layout/NotificationsInbox.vue +5 -0
- package/app/components/prReview/PrReviewPhaseBadge.vue +2 -2
- package/app/components/prReview/PrReviewWindow.vue +19 -15
- package/app/components/slack/SlackPanel.vue +2 -0
- package/app/stores/personalSubscriptions.spec.ts +57 -6
- package/app/stores/personalSubscriptions.ts +72 -11
- package/app/utils/prReviewProgress.spec.ts +17 -15
- package/app/utils/prReviewProgress.ts +26 -15
- package/i18n/locales/de.json +6 -5
- package/i18n/locales/en.json +6 -5
- package/i18n/locales/es.json +6 -5
- package/i18n/locales/fr.json +6 -5
- package/i18n/locales/he.json +6 -5
- package/i18n/locales/it.json +6 -5
- package/i18n/locales/ja.json +6 -5
- package/i18n/locales/pl.json +6 -5
- package/i18n/locales/tr.json +6 -5
- package/i18n/locales/uk.json +6 -5
- package/package.json +2 -2
|
@@ -77,6 +77,10 @@ const META: Record<Notification['type'], { icon: string; color: Accent }> = {
|
|
|
77
77
|
// Runs were paused by the spend safeguard. Workspace-scoped (no block to reveal); "act" just
|
|
78
78
|
// marks it read (the human raises the budget then resumes from the spend panel).
|
|
79
79
|
budget_paused: { icon: 'i-lucide-wallet', color: 'warning' },
|
|
80
|
+
// Stored credentials could not be decrypted (the ENCRYPTION_KEY changed since they were
|
|
81
|
+
// sealed). Not block-scoped; "act" drops the listed stale ciphertexts so they can be re-entered
|
|
82
|
+
// (or restore the previous key to recover them instead).
|
|
83
|
+
key_drift: { icon: 'i-lucide-key-round', color: 'error' },
|
|
80
84
|
}
|
|
81
85
|
|
|
82
86
|
// Per-type primary-action label. An exhaustive Record keyed off the notification
|
|
@@ -100,6 +104,7 @@ const ACTION_KEYS: Record<Notification['type'], string> = {
|
|
|
100
104
|
initiative: 'layout.notifications.action.initiative',
|
|
101
105
|
platform_health: 'layout.notifications.action.platform_health',
|
|
102
106
|
budget_paused: 'layout.notifications.action.budget_paused',
|
|
107
|
+
key_drift: 'layout.notifications.action.key_drift',
|
|
103
108
|
}
|
|
104
109
|
|
|
105
110
|
/** The localized primary-action label for a notification (te()-guarded against a
|
|
@@ -23,7 +23,7 @@ const phase = computed(() => prReviewPhase(props.step.prReview, props.step.subta
|
|
|
23
23
|
// working (suppressed on a failed run). `awaiting` is the parked "findings ready" state — a
|
|
24
24
|
// steady amber prompt, not a spinner.
|
|
25
25
|
const PHASE_META: Record<PrReviewPhaseKind, { icon: string; spin: boolean; class: string }> = {
|
|
26
|
-
|
|
26
|
+
planning: { icon: 'i-lucide-loader-circle', spin: true, class: 'text-indigo-300' },
|
|
27
27
|
reviewing: { icon: 'i-lucide-loader-circle', spin: true, class: 'text-indigo-300' },
|
|
28
28
|
awaiting: { icon: 'i-lucide-clipboard-check', spin: false, class: 'text-amber-300' },
|
|
29
29
|
challenging: { icon: 'i-lucide-gavel', spin: true, class: 'text-indigo-300' },
|
|
@@ -35,7 +35,7 @@ const PHASE_META: Record<PrReviewPhaseKind, { icon: string; spin: boolean; class
|
|
|
35
35
|
// label fails the typecheck (the sanctioned dynamic enum→key guard — tier 1 can't see a
|
|
36
36
|
// runtime-built key), matching the `CHUNK_STATUS_KEY` pattern in PrReviewWindow.vue.
|
|
37
37
|
const PHASE_LABEL_KEY: Record<PrReviewPhaseKind, string> = {
|
|
38
|
-
|
|
38
|
+
planning: 'prReview.phase.planning',
|
|
39
39
|
reviewing: 'prReview.phase.reviewing',
|
|
40
40
|
awaiting: 'prReview.phase.awaiting',
|
|
41
41
|
challenging: 'prReview.phase.challenging',
|
|
@@ -21,7 +21,7 @@ import type {
|
|
|
21
21
|
StepSubtasks,
|
|
22
22
|
} from '~/types/execution'
|
|
23
23
|
import { subtaskIconClass } from '~/utils/pipelineRender'
|
|
24
|
-
import { activeChunkLabels, chunkReviewPercent,
|
|
24
|
+
import { activeChunkLabels, chunkReviewPercent, hasNoSlicePlan } from '~/utils/prReviewProgress'
|
|
25
25
|
import ResultWindowShell from '~/components/panels/ResultWindowShell.vue'
|
|
26
26
|
import StepRunMeta from '~/components/panels/StepRunMeta.vue'
|
|
27
27
|
|
|
@@ -55,15 +55,18 @@ const awaiting = computed(() => status.value === 'awaiting_selection')
|
|
|
55
55
|
const challenging = computed(() => status.value === 'challenging')
|
|
56
56
|
// The reviewer's live todo list while it works, streamed onto the step. Its entries are the
|
|
57
57
|
// cohesive slices/chunks the agent grouped the diff into (plus a final "aggregate" step). The
|
|
58
|
-
// derivation of the
|
|
59
|
-
// (pure + unit-tested); see that module for the
|
|
58
|
+
// derivation of the `reviewing`-phase sub-states from it lives in `~/utils/prReviewProgress`
|
|
59
|
+
// (pure + unit-tested); see that module for the has-a-plan signal.
|
|
60
60
|
const subtasks = computed<StepSubtasks | null>(() => step.value?.subtasks ?? null)
|
|
61
61
|
|
|
62
62
|
/**
|
|
63
|
-
*
|
|
64
|
-
*
|
|
63
|
+
* Planning sub-phase: no per-slice todo plan has been reported yet. This is NEUTRAL — the
|
|
64
|
+
* reviewer may still be grouping the diff, OR it may be reviewing via parallel subagents that
|
|
65
|
+
* never write a parent todo plan (ADR 0026 D2.2). Either way we must NOT claim a specific
|
|
66
|
+
* "slicing" phase; we show a neutral "reviewing…" state and switch to the per-slice list the
|
|
67
|
+
* moment a plan exists.
|
|
65
68
|
*/
|
|
66
|
-
const
|
|
69
|
+
const planning = computed(() => status.value === 'reviewing' && hasNoSlicePlan(subtasks.value))
|
|
67
70
|
|
|
68
71
|
/** Chunk-review completion, clamped 0..100 for the progress bar. */
|
|
69
72
|
const chunkPercent = computed(() => chunkReviewPercent(subtasks.value))
|
|
@@ -256,28 +259,29 @@ async function onDismiss(id: string): Promise<void> {
|
|
|
256
259
|
|
|
257
260
|
<div class="flex min-h-0 flex-1">
|
|
258
261
|
<div class="min-w-0 flex-1 overflow-y-auto px-5 py-4">
|
|
259
|
-
<!-- Reviewing: the read-only reviewer is still working.
|
|
260
|
-
|
|
261
|
-
|
|
262
|
+
<!-- Reviewing: the read-only reviewer is still working. Two sub-states, told apart by
|
|
263
|
+
whether a per-slice todo plan exists — a NEUTRAL "planning" state (no plan reported yet;
|
|
264
|
+
may be grouping OR reviewing via subagents that write no parent plan) vs the per-slice
|
|
265
|
+
list. The copy never asserts a specific "slicing" phase from an empty todo list. -->
|
|
262
266
|
<div
|
|
263
267
|
v-if="status === 'reviewing'"
|
|
264
268
|
data-testid="pr-review-reviewing"
|
|
265
269
|
class="flex h-full flex-col"
|
|
266
270
|
>
|
|
267
|
-
<!--
|
|
271
|
+
<!-- PLANNING: no per-slice plan yet — a neutral "reviewing…" state, not a "slicing" claim. -->
|
|
268
272
|
<div
|
|
269
|
-
v-if="
|
|
270
|
-
data-testid="pr-review-
|
|
273
|
+
v-if="planning"
|
|
274
|
+
data-testid="pr-review-planning"
|
|
271
275
|
class="flex h-full flex-col items-center justify-center gap-2 py-10 text-center text-slate-400"
|
|
272
276
|
>
|
|
273
277
|
<UIcon name="i-lucide-loader-circle" class="h-8 w-8 animate-spin opacity-60" />
|
|
274
|
-
<p class="text-sm text-slate-200">{{ t('prReview.reviewing.
|
|
278
|
+
<p class="text-sm text-slate-200">{{ t('prReview.reviewing.planning.title') }}</p>
|
|
275
279
|
<p class="max-w-sm text-[11px] text-slate-500">
|
|
276
|
-
{{ t('prReview.reviewing.
|
|
280
|
+
{{ t('prReview.reviewing.planning.hint') }}
|
|
277
281
|
</p>
|
|
278
282
|
</div>
|
|
279
283
|
|
|
280
|
-
<!-- REVIEWING:
|
|
284
|
+
<!-- REVIEWING: a per-slice plan exists — show every slice with its status + which are active now. -->
|
|
281
285
|
<div v-else data-testid="pr-review-reviewing-chunks" class="py-2">
|
|
282
286
|
<div class="mb-1 flex items-center gap-2 text-sm text-slate-200">
|
|
283
287
|
<UIcon
|
|
@@ -70,6 +70,8 @@ const routes = reactive<Record<NotificationType, SlackRoute>>({
|
|
|
70
70
|
initiative: { enabled: false, channel: '' },
|
|
71
71
|
platform_health: { enabled: false, channel: '' },
|
|
72
72
|
budget_paused: { enabled: false, channel: '' },
|
|
73
|
+
// In-app only (not in ROUTABLE), but the map is exhaustive over the type.
|
|
74
|
+
key_drift: { enabled: false, channel: '' },
|
|
73
75
|
})
|
|
74
76
|
const mentionsEnabled = ref(false)
|
|
75
77
|
// Editable member rows carry a client-only stable `uid` (see `slackMemberMapping`) so
|
|
@@ -1,17 +1,22 @@
|
|
|
1
1
|
import { describe, it, expect, beforeEach, vi } from 'vitest'
|
|
2
2
|
import { ApiError } from '~/composables/api/errors'
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
personalPasswordCacheKey,
|
|
5
|
+
usePersonalSubscriptionsStore,
|
|
6
|
+
} from '~/stores/personalSubscriptions'
|
|
4
7
|
|
|
5
|
-
// The
|
|
6
|
-
//
|
|
7
|
-
const CACHE_KEY = '
|
|
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(
|
|
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
|
|
16
|
-
//
|
|
17
|
-
//
|
|
18
|
-
//
|
|
19
|
-
//
|
|
20
|
-
//
|
|
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
|
-
|
|
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
|
|
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(
|
|
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
|
-
|
|
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(
|
|
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
|
}
|
|
@@ -3,7 +3,7 @@ import type { PrReviewStepState, StepSubtasks } from '~/types/execution'
|
|
|
3
3
|
import {
|
|
4
4
|
activeChunkLabels,
|
|
5
5
|
chunkReviewPercent,
|
|
6
|
-
|
|
6
|
+
hasNoSlicePlan,
|
|
7
7
|
prReviewPhase,
|
|
8
8
|
} from './prReviewProgress'
|
|
9
9
|
|
|
@@ -14,18 +14,19 @@ const subtasks = (over: Partial<StepSubtasks>): StepSubtasks => ({
|
|
|
14
14
|
...over,
|
|
15
15
|
})
|
|
16
16
|
|
|
17
|
-
describe('
|
|
18
|
-
it('
|
|
19
|
-
// No plan
|
|
20
|
-
|
|
21
|
-
expect(
|
|
22
|
-
expect(
|
|
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)
|
|
23
24
|
})
|
|
24
25
|
|
|
25
|
-
it('
|
|
26
|
-
expect(
|
|
27
|
-
// A single-
|
|
28
|
-
expect(
|
|
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)
|
|
29
30
|
})
|
|
30
31
|
})
|
|
31
32
|
|
|
@@ -89,15 +90,16 @@ describe('prReviewPhase', () => {
|
|
|
89
90
|
expect(prReviewPhase(state('skipped'), null)).toBeNull()
|
|
90
91
|
})
|
|
91
92
|
|
|
92
|
-
it('is
|
|
93
|
-
// No plan
|
|
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.
|
|
94
96
|
expect(prReviewPhase(state('reviewing'), null)).toEqual({
|
|
95
|
-
kind: '
|
|
97
|
+
kind: 'planning',
|
|
96
98
|
completed: 0,
|
|
97
99
|
total: 0,
|
|
98
100
|
})
|
|
99
101
|
expect(prReviewPhase(state('reviewing'), subtasks({ total: 0 }))).toEqual({
|
|
100
|
-
kind: '
|
|
102
|
+
kind: 'planning',
|
|
101
103
|
completed: 0,
|
|
102
104
|
total: 0,
|
|
103
105
|
})
|
|
@@ -3,14 +3,21 @@ import type { PrReviewStepState, StepSubtasks } from '~/types/execution'
|
|
|
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
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
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
|
-
/**
|
|
13
|
-
|
|
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
|
|
|
@@ -29,13 +36,16 @@ export function activeChunkLabels(subtasks: StepSubtasks | null | undefined): st
|
|
|
29
36
|
|
|
30
37
|
/**
|
|
31
38
|
* The at-a-glance phase of a `pr-reviewer` step, collapsing its `prReview.status` (+ the
|
|
32
|
-
*
|
|
33
|
-
*
|
|
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.
|
|
34
44
|
* Returns `null` for the terminal / passed-through states (`done`/`skipped`) and when there is
|
|
35
45
|
* no live review — those have no in-flight phase to show.
|
|
36
46
|
*/
|
|
37
47
|
export type PrReviewPhaseKind =
|
|
38
|
-
| '
|
|
48
|
+
| 'planning'
|
|
39
49
|
| 'reviewing'
|
|
40
50
|
| 'awaiting'
|
|
41
51
|
| 'challenging'
|
|
@@ -44,9 +54,9 @@ export type PrReviewPhaseKind =
|
|
|
44
54
|
|
|
45
55
|
export interface PrReviewPhase {
|
|
46
56
|
kind: PrReviewPhaseKind
|
|
47
|
-
/** Slices whose review is finished (0 while
|
|
57
|
+
/** Slices whose review is finished (0 while planning). */
|
|
48
58
|
completed: number
|
|
49
|
-
/** Total slices the reviewer grouped the diff into (0 while
|
|
59
|
+
/** Total slices the reviewer grouped the diff into (0 while planning). */
|
|
50
60
|
total: number
|
|
51
61
|
}
|
|
52
62
|
|
|
@@ -60,9 +70,10 @@ export function prReviewPhase(
|
|
|
60
70
|
const total = subtasks?.total ?? 0
|
|
61
71
|
switch (status) {
|
|
62
72
|
case 'reviewing':
|
|
63
|
-
// No
|
|
64
|
-
|
|
65
|
-
|
|
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 }
|
|
66
77
|
: { kind: 'reviewing', completed, total }
|
|
67
78
|
case 'awaiting_selection':
|
|
68
79
|
return { kind: 'awaiting', completed, total }
|
package/i18n/locales/de.json
CHANGED
|
@@ -1745,7 +1745,8 @@
|
|
|
1745
1745
|
"markRead": "Als gelesen markieren",
|
|
1746
1746
|
"fork_decision_pending": "Als gelesen markieren",
|
|
1747
1747
|
"pr_review_ready": "Als gelesen markieren",
|
|
1748
|
-
"budget_paused": "Als gelesen markieren"
|
|
1748
|
+
"budget_paused": "Als gelesen markieren",
|
|
1749
|
+
"key_drift": "Veraltete Zugangsdaten entfernen"
|
|
1749
1750
|
}
|
|
1750
1751
|
},
|
|
1751
1752
|
"aiProvidersBanner": {
|
|
@@ -4943,9 +4944,9 @@
|
|
|
4943
4944
|
"suggestedFix": "Lösungsvorschlag:",
|
|
4944
4945
|
"line": "Zeile {line}",
|
|
4945
4946
|
"reviewing": {
|
|
4946
|
-
"
|
|
4947
|
-
"title": "
|
|
4948
|
-
"hint": "
|
|
4947
|
+
"planning": {
|
|
4948
|
+
"title": "Pull Request wird geprüft…",
|
|
4949
|
+
"hint": "Der Prüfer arbeitet den Diff durch. Der Fortschritt pro Abschnitt erscheint hier, sobald die Änderungen in Abschnitte gruppiert werden."
|
|
4949
4950
|
},
|
|
4950
4951
|
"reviewingChunks": {
|
|
4951
4952
|
"title": "Abschnitte werden geprüft…",
|
|
@@ -4962,7 +4963,7 @@
|
|
|
4962
4963
|
}
|
|
4963
4964
|
},
|
|
4964
4965
|
"phase": {
|
|
4965
|
-
"
|
|
4966
|
+
"planning": "Wird geprüft…",
|
|
4966
4967
|
"reviewing": "Prüfe {completed}/{total} Abschnitte",
|
|
4967
4968
|
"awaiting": "Ergebnisse bereit",
|
|
4968
4969
|
"challenging": "Wird untersucht…",
|
package/i18n/locales/en.json
CHANGED
|
@@ -1725,7 +1725,8 @@
|
|
|
1725
1725
|
"markRead": "Mark read",
|
|
1726
1726
|
"fork_decision_pending": "Mark read",
|
|
1727
1727
|
"pr_review_ready": "Mark read",
|
|
1728
|
-
"budget_paused": "Mark read"
|
|
1728
|
+
"budget_paused": "Mark read",
|
|
1729
|
+
"key_drift": "Drop stale credentials"
|
|
1729
1730
|
}
|
|
1730
1731
|
},
|
|
1731
1732
|
"aiProvidersBanner": {
|
|
@@ -5072,9 +5073,9 @@
|
|
|
5072
5073
|
"suggestedFix": "Suggested fix:",
|
|
5073
5074
|
"line": "line {line}",
|
|
5074
5075
|
"reviewing": {
|
|
5075
|
-
"
|
|
5076
|
-
"title": "
|
|
5077
|
-
"hint": "
|
|
5076
|
+
"planning": {
|
|
5077
|
+
"title": "Reviewing the pull request…",
|
|
5078
|
+
"hint": "The reviewer is working through the diff. Per-slice progress appears here once it groups the changes into slices."
|
|
5078
5079
|
},
|
|
5079
5080
|
"reviewingChunks": {
|
|
5080
5081
|
"title": "Reviewing the chunks…",
|
|
@@ -5091,7 +5092,7 @@
|
|
|
5091
5092
|
}
|
|
5092
5093
|
},
|
|
5093
5094
|
"phase": {
|
|
5094
|
-
"
|
|
5095
|
+
"planning": "Reviewing…",
|
|
5095
5096
|
"reviewing": "Reviewing {completed}/{total} slices",
|
|
5096
5097
|
"awaiting": "Findings ready",
|
|
5097
5098
|
"challenging": "Investigating…",
|
package/i18n/locales/es.json
CHANGED
|
@@ -1656,7 +1656,8 @@
|
|
|
1656
1656
|
"markRead": "Marcar como leída",
|
|
1657
1657
|
"fork_decision_pending": "Marcar como leído",
|
|
1658
1658
|
"pr_review_ready": "Marcar como leída",
|
|
1659
|
-
"budget_paused": "Marcar como leída"
|
|
1659
|
+
"budget_paused": "Marcar como leída",
|
|
1660
|
+
"key_drift": "Descartar credenciales obsoletas"
|
|
1660
1661
|
},
|
|
1661
1662
|
"toast": {
|
|
1662
1663
|
"acted": "Marcado como resuelto",
|
|
@@ -4931,9 +4932,9 @@
|
|
|
4931
4932
|
"suggestedFix": "Corrección sugerida:",
|
|
4932
4933
|
"line": "línea {line}",
|
|
4933
4934
|
"reviewing": {
|
|
4934
|
-
"
|
|
4935
|
-
"title": "
|
|
4936
|
-
"hint": "
|
|
4935
|
+
"planning": {
|
|
4936
|
+
"title": "Revisando la pull request…",
|
|
4937
|
+
"hint": "El revisor está analizando el diff. El progreso por sección aparecerá aquí cuando agrupe los cambios en secciones."
|
|
4937
4938
|
},
|
|
4938
4939
|
"reviewingChunks": {
|
|
4939
4940
|
"title": "Revisando los fragmentos…",
|
|
@@ -4950,7 +4951,7 @@
|
|
|
4950
4951
|
}
|
|
4951
4952
|
},
|
|
4952
4953
|
"phase": {
|
|
4953
|
-
"
|
|
4954
|
+
"planning": "Revisando…",
|
|
4954
4955
|
"reviewing": "Revisando {completed}/{total} secciones",
|
|
4955
4956
|
"awaiting": "Resultados listos",
|
|
4956
4957
|
"challenging": "Investigando…",
|
package/i18n/locales/fr.json
CHANGED
|
@@ -1656,7 +1656,8 @@
|
|
|
1656
1656
|
"markRead": "Marquer comme lu",
|
|
1657
1657
|
"fork_decision_pending": "Marquer comme lu",
|
|
1658
1658
|
"pr_review_ready": "Marquer comme lu",
|
|
1659
|
-
"budget_paused": "Marquer comme lu"
|
|
1659
|
+
"budget_paused": "Marquer comme lu",
|
|
1660
|
+
"key_drift": "Supprimer les identifiants obsolètes"
|
|
1660
1661
|
},
|
|
1661
1662
|
"toast": {
|
|
1662
1663
|
"acted": "Marqué comme traité",
|
|
@@ -4931,9 +4932,9 @@
|
|
|
4931
4932
|
"suggestedFix": "Correction suggérée :",
|
|
4932
4933
|
"line": "ligne {line}",
|
|
4933
4934
|
"reviewing": {
|
|
4934
|
-
"
|
|
4935
|
-
"title": "
|
|
4936
|
-
"hint": "
|
|
4935
|
+
"planning": {
|
|
4936
|
+
"title": "Révision de la pull request…",
|
|
4937
|
+
"hint": "Le relecteur parcourt le diff. La progression par section apparaîtra ici une fois les modifications regroupées en sections."
|
|
4937
4938
|
},
|
|
4938
4939
|
"reviewingChunks": {
|
|
4939
4940
|
"title": "Revue des sections…",
|
|
@@ -4950,7 +4951,7 @@
|
|
|
4950
4951
|
}
|
|
4951
4952
|
},
|
|
4952
4953
|
"phase": {
|
|
4953
|
-
"
|
|
4954
|
+
"planning": "Révision…",
|
|
4954
4955
|
"reviewing": "Révision {completed}/{total} sections",
|
|
4955
4956
|
"awaiting": "Résultats prêts",
|
|
4956
4957
|
"challenging": "Analyse en cours…",
|
package/i18n/locales/he.json
CHANGED
|
@@ -1656,7 +1656,8 @@
|
|
|
1656
1656
|
"markRead": "סמן כנקרא",
|
|
1657
1657
|
"fork_decision_pending": "סמן כנקרא",
|
|
1658
1658
|
"pr_review_ready": "סמן כנקרא",
|
|
1659
|
-
"budget_paused": "סמן כנקרא"
|
|
1659
|
+
"budget_paused": "סמן כנקרא",
|
|
1660
|
+
"key_drift": "מחק אישורים מיושנים"
|
|
1660
1661
|
},
|
|
1661
1662
|
"toast": {
|
|
1662
1663
|
"acted": "סומן כטופל",
|
|
@@ -4942,9 +4943,9 @@
|
|
|
4942
4943
|
"suggestedFix": "תיקון מוצע:",
|
|
4943
4944
|
"line": "שורה {line}",
|
|
4944
4945
|
"reviewing": {
|
|
4945
|
-
"
|
|
4946
|
-
"title": "
|
|
4947
|
-
"hint": "
|
|
4946
|
+
"planning": {
|
|
4947
|
+
"title": "בודק את בקשת המשיכה…",
|
|
4948
|
+
"hint": "הבודק עובר על ההבדלים. ההתקדמות לפי מקטע תופיע כאן לאחר שהשינויים יקובצו למקטעים."
|
|
4948
4949
|
},
|
|
4949
4950
|
"reviewingChunks": {
|
|
4950
4951
|
"title": "בודק את המקטעים…",
|
|
@@ -4961,7 +4962,7 @@
|
|
|
4961
4962
|
}
|
|
4962
4963
|
},
|
|
4963
4964
|
"phase": {
|
|
4964
|
-
"
|
|
4965
|
+
"planning": "בודק…",
|
|
4965
4966
|
"reviewing": "בודק {completed}/{total} מקטעים",
|
|
4966
4967
|
"awaiting": "הממצאים מוכנים",
|
|
4967
4968
|
"challenging": "חוקר…",
|
package/i18n/locales/it.json
CHANGED
|
@@ -1745,7 +1745,8 @@
|
|
|
1745
1745
|
"markRead": "Segna come letto",
|
|
1746
1746
|
"fork_decision_pending": "Segna come letto",
|
|
1747
1747
|
"pr_review_ready": "Segna come letto",
|
|
1748
|
-
"budget_paused": "Segna come letto"
|
|
1748
|
+
"budget_paused": "Segna come letto",
|
|
1749
|
+
"key_drift": "Elimina credenziali obsolete"
|
|
1749
1750
|
}
|
|
1750
1751
|
},
|
|
1751
1752
|
"aiProvidersBanner": {
|
|
@@ -4943,9 +4944,9 @@
|
|
|
4943
4944
|
"suggestedFix": "Correzione suggerita:",
|
|
4944
4945
|
"line": "riga {line}",
|
|
4945
4946
|
"reviewing": {
|
|
4946
|
-
"
|
|
4947
|
-
"title": "
|
|
4948
|
-
"hint": "
|
|
4947
|
+
"planning": {
|
|
4948
|
+
"title": "Revisione della pull request…",
|
|
4949
|
+
"hint": "Il revisore sta esaminando il diff. L'avanzamento per sezione comparirà qui una volta raggruppate le modifiche in sezioni."
|
|
4949
4950
|
},
|
|
4950
4951
|
"reviewingChunks": {
|
|
4951
4952
|
"title": "Revisione dei blocchi…",
|
|
@@ -4962,7 +4963,7 @@
|
|
|
4962
4963
|
}
|
|
4963
4964
|
},
|
|
4964
4965
|
"phase": {
|
|
4965
|
-
"
|
|
4966
|
+
"planning": "Revisione…",
|
|
4966
4967
|
"reviewing": "Revisione {completed}/{total} sezioni",
|
|
4967
4968
|
"awaiting": "Risultati pronti",
|
|
4968
4969
|
"challenging": "Analisi in corso…",
|
package/i18n/locales/ja.json
CHANGED
|
@@ -1656,7 +1656,8 @@
|
|
|
1656
1656
|
"markRead": "既読にする",
|
|
1657
1657
|
"fork_decision_pending": "既読にする",
|
|
1658
1658
|
"pr_review_ready": "既読にする",
|
|
1659
|
-
"budget_paused": "既読にする"
|
|
1659
|
+
"budget_paused": "既読にする",
|
|
1660
|
+
"key_drift": "古い認証情報を削除"
|
|
1660
1661
|
},
|
|
1661
1662
|
"toast": {
|
|
1662
1663
|
"acted": "対応済みにしました",
|
|
@@ -4943,9 +4944,9 @@
|
|
|
4943
4944
|
"suggestedFix": "修正案:",
|
|
4944
4945
|
"line": "{line}行目",
|
|
4945
4946
|
"reviewing": {
|
|
4946
|
-
"
|
|
4947
|
-
"title": "
|
|
4948
|
-
"hint": "
|
|
4947
|
+
"planning": {
|
|
4948
|
+
"title": "プルリクエストをレビュー中…",
|
|
4949
|
+
"hint": "レビュー担当者が差分を確認しています。変更が区分にグループ化されると、区分ごとの進捗がここに表示されます。"
|
|
4949
4950
|
},
|
|
4950
4951
|
"reviewingChunks": {
|
|
4951
4952
|
"title": "チャンクをレビュー中…",
|
|
@@ -4962,7 +4963,7 @@
|
|
|
4962
4963
|
}
|
|
4963
4964
|
},
|
|
4964
4965
|
"phase": {
|
|
4965
|
-
"
|
|
4966
|
+
"planning": "レビュー中…",
|
|
4966
4967
|
"reviewing": "レビュー中 {completed}/{total} 区分",
|
|
4967
4968
|
"awaiting": "指摘の準備完了",
|
|
4968
4969
|
"challenging": "調査中…",
|
package/i18n/locales/pl.json
CHANGED
|
@@ -1656,7 +1656,8 @@
|
|
|
1656
1656
|
"markRead": "Oznacz jako przeczytane",
|
|
1657
1657
|
"fork_decision_pending": "Oznacz jako przeczytane",
|
|
1658
1658
|
"pr_review_ready": "Oznacz jako przeczytane",
|
|
1659
|
-
"budget_paused": "Oznacz jako przeczytane"
|
|
1659
|
+
"budget_paused": "Oznacz jako przeczytane",
|
|
1660
|
+
"key_drift": "Usuń nieaktualne poświadczenia"
|
|
1660
1661
|
},
|
|
1661
1662
|
"toast": {
|
|
1662
1663
|
"acted": "Oznaczono jako obsłużone",
|
|
@@ -4931,9 +4932,9 @@
|
|
|
4931
4932
|
"suggestedFix": "Sugerowana poprawka:",
|
|
4932
4933
|
"line": "wiersz {line}",
|
|
4933
4934
|
"reviewing": {
|
|
4934
|
-
"
|
|
4935
|
-
"title": "
|
|
4936
|
-
"hint": "
|
|
4935
|
+
"planning": {
|
|
4936
|
+
"title": "Przeglądanie pull requesta…",
|
|
4937
|
+
"hint": "Recenzent analizuje zmiany. Postęp poszczególnych fragmentów pojawi się tutaj po pogrupowaniu zmian we fragmenty."
|
|
4937
4938
|
},
|
|
4938
4939
|
"reviewingChunks": {
|
|
4939
4940
|
"title": "Przeglądanie fragmentów…",
|
|
@@ -4950,7 +4951,7 @@
|
|
|
4950
4951
|
}
|
|
4951
4952
|
},
|
|
4952
4953
|
"phase": {
|
|
4953
|
-
"
|
|
4954
|
+
"planning": "Przegląd…",
|
|
4954
4955
|
"reviewing": "Przegląd {completed}/{total} fragmentów",
|
|
4955
4956
|
"awaiting": "Wyniki gotowe",
|
|
4956
4957
|
"challenging": "Analizowanie…",
|
package/i18n/locales/tr.json
CHANGED
|
@@ -1656,7 +1656,8 @@
|
|
|
1656
1656
|
"markRead": "Okundu işaretle",
|
|
1657
1657
|
"fork_decision_pending": "Okundu olarak işaretle",
|
|
1658
1658
|
"pr_review_ready": "Okundu işaretle",
|
|
1659
|
-
"budget_paused": "Okundu işaretle"
|
|
1659
|
+
"budget_paused": "Okundu işaretle",
|
|
1660
|
+
"key_drift": "Eski kimlik bilgilerini kaldır"
|
|
1660
1661
|
},
|
|
1661
1662
|
"toast": {
|
|
1662
1663
|
"acted": "İşlendi olarak işaretlendi",
|
|
@@ -4943,9 +4944,9 @@
|
|
|
4943
4944
|
"suggestedFix": "Önerilen düzeltme:",
|
|
4944
4945
|
"line": "satır {line}",
|
|
4945
4946
|
"reviewing": {
|
|
4946
|
-
"
|
|
4947
|
-
"title": "
|
|
4948
|
-
"hint": "
|
|
4947
|
+
"planning": {
|
|
4948
|
+
"title": "Pull request inceleniyor…",
|
|
4949
|
+
"hint": "İnceleyici farkı gözden geçiriyor. Değişiklikler bölümlere gruplandığında bölüm bazlı ilerleme burada görünecek."
|
|
4949
4950
|
},
|
|
4950
4951
|
"reviewingChunks": {
|
|
4951
4952
|
"title": "Parçalar inceleniyor…",
|
|
@@ -4962,7 +4963,7 @@
|
|
|
4962
4963
|
}
|
|
4963
4964
|
},
|
|
4964
4965
|
"phase": {
|
|
4965
|
-
"
|
|
4966
|
+
"planning": "İnceleniyor…",
|
|
4966
4967
|
"reviewing": "İnceleniyor {completed}/{total} bölüm",
|
|
4967
4968
|
"awaiting": "Bulgular hazır",
|
|
4968
4969
|
"challenging": "Araştırılıyor…",
|
package/i18n/locales/uk.json
CHANGED
|
@@ -1656,7 +1656,8 @@
|
|
|
1656
1656
|
"markRead": "Позначити прочитаним",
|
|
1657
1657
|
"fork_decision_pending": "Позначити прочитаним",
|
|
1658
1658
|
"pr_review_ready": "Позначити прочитаним",
|
|
1659
|
-
"budget_paused": "Позначити прочитаним"
|
|
1659
|
+
"budget_paused": "Позначити прочитаним",
|
|
1660
|
+
"key_drift": "Видалити застарілі облікові дані"
|
|
1660
1661
|
},
|
|
1661
1662
|
"toast": {
|
|
1662
1663
|
"acted": "Позначено як опрацьоване",
|
|
@@ -4931,9 +4932,9 @@
|
|
|
4931
4932
|
"suggestedFix": "Пропоноване виправлення:",
|
|
4932
4933
|
"line": "рядок {line}",
|
|
4933
4934
|
"reviewing": {
|
|
4934
|
-
"
|
|
4935
|
-
"title": "
|
|
4936
|
-
"hint": "
|
|
4935
|
+
"planning": {
|
|
4936
|
+
"title": "Перевірка pull request…",
|
|
4937
|
+
"hint": "Рецензент переглядає зміни. Прогрес за фрагментами з'явиться тут після групування змін у фрагменти."
|
|
4937
4938
|
},
|
|
4938
4939
|
"reviewingChunks": {
|
|
4939
4940
|
"title": "Огляд фрагментів…",
|
|
@@ -4950,7 +4951,7 @@
|
|
|
4950
4951
|
}
|
|
4951
4952
|
},
|
|
4952
4953
|
"phase": {
|
|
4953
|
-
"
|
|
4954
|
+
"planning": "Перевірка…",
|
|
4954
4955
|
"reviewing": "Перевірка {completed}/{total} фрагментів",
|
|
4955
4956
|
"awaiting": "Результати готові",
|
|
4956
4957
|
"challenging": "Дослідження…",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cat-factory/app",
|
|
3
|
-
"version": "0.147.
|
|
3
|
+
"version": "0.147.4",
|
|
4
4
|
"description": "Reusable Nuxt layer for the Agent Architecture Board SPA (components, stores, composables, pages). Consume it from a thin deployment app via `extends: ['@cat-factory/app']` and point it at your backend with NUXT_PUBLIC_API_BASE. See deploy/frontend for an example.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
"valibot": "^1.4.2",
|
|
41
41
|
"vue": "3.5.40",
|
|
42
42
|
"wretch": "^3.0.9",
|
|
43
|
-
"@cat-factory/contracts": "0.154.
|
|
43
|
+
"@cat-factory/contracts": "0.154.1"
|
|
44
44
|
},
|
|
45
45
|
"devDependencies": {
|
|
46
46
|
"@toad-contracts/testing": "0.3.2",
|