@cat-factory/app 0.195.0 → 0.195.1
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/initiative/InitiativePlanDecision.vue +134 -0
- package/app/components/initiative/InitiativePlanNotice.vue +69 -0
- package/app/components/initiative/InitiativePlanReview.vue +278 -254
- package/app/components/initiative/InitiativeTrackerWindow.vue +72 -25
- package/app/utils/initiative.spec.ts +43 -0
- package/app/utils/initiative.ts +23 -0
- package/i18n/locales/de.json +3 -2
- package/i18n/locales/en.json +3 -2
- package/i18n/locales/es.json +3 -2
- package/i18n/locales/fr.json +3 -2
- package/i18n/locales/he.json +3 -2
- package/i18n/locales/it.json +3 -2
- package/i18n/locales/ja.json +3 -2
- package/i18n/locales/pl.json +3 -2
- package/i18n/locales/tr.json +3 -2
- package/i18n/locales/uk.json +3 -2
- package/package.json +1 -1
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
// The plan gate's DECISION half: the overall-feedback field plus the two commands that resolve the
|
|
3
|
+
// park — accept the plan, or send it back to the planner.
|
|
4
|
+
//
|
|
5
|
+
// It is its own component because the gate has two surfaces: the full document review
|
|
6
|
+
// (`InitiativePlanReview`, which owns the tracker window while a rendered plan is parked) and the
|
|
7
|
+
// compact notice a gate with no rendered plan falls back to (`InitiativePlanNotice`). Both must
|
|
8
|
+
// word the commands identically, gate them on the same RBAC fact, and refuse an empty send-back
|
|
9
|
+
// the same way — so the state machine and its markup live here once rather than in each.
|
|
10
|
+
//
|
|
11
|
+
// Deliberately NOT offered: "approve with corrections". The plan was ingested into the
|
|
12
|
+
// `initiatives` entity before this gate was raised, so the document is a VIEW of committed state —
|
|
13
|
+
// an edit typed over it would reach nothing, and the engine refuses it outright
|
|
14
|
+
// (`outputIsRendered` → 422). Requesting changes is the route for a correction, which is why an
|
|
15
|
+
// anchored comment is worth having: it quotes the planner's own text back to it on the re-plan.
|
|
16
|
+
import { computed, ref, watch } from 'vue'
|
|
17
|
+
import type { RequestStepChangesInput } from '@cat-factory/contracts'
|
|
18
|
+
|
|
19
|
+
const props = defineProps<{
|
|
20
|
+
/** The parked gate being resolved. */
|
|
21
|
+
approvalId: string
|
|
22
|
+
/** The run the gate belongs to, for the approve / request-changes commands. */
|
|
23
|
+
instanceId: string
|
|
24
|
+
/** Whether the viewer may resolve runs at all (RBAC); false renders the actions disabled. */
|
|
25
|
+
canExecute: boolean
|
|
26
|
+
/**
|
|
27
|
+
* The anchored per-block comments to send with a send-back, from the surface that offers
|
|
28
|
+
* commenting. Absent on the notice surface, which has no document to anchor to.
|
|
29
|
+
*/
|
|
30
|
+
comments?: RequestStepChangesInput['comments']
|
|
31
|
+
}>()
|
|
32
|
+
|
|
33
|
+
/** A send-back succeeded: the surface drops its anchored drafts (the feedback is cleared here). */
|
|
34
|
+
const emit = defineEmits<{ sent: [] }>()
|
|
35
|
+
|
|
36
|
+
const execution = useExecutionStore()
|
|
37
|
+
const { t } = useI18n()
|
|
38
|
+
|
|
39
|
+
const feedback = ref('')
|
|
40
|
+
const submitting = ref(false)
|
|
41
|
+
|
|
42
|
+
/** Changes can only be requested with something to act on — an empty send would re-plan blind. */
|
|
43
|
+
const canRequestChanges = computed(
|
|
44
|
+
() => !!feedback.value.trim() || (props.comments?.length ?? 0) > 0,
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
/** A fresh gate (a re-plan parked again) reviews the new plan clean, drafts dropped. */
|
|
48
|
+
watch(
|
|
49
|
+
() => props.approvalId,
|
|
50
|
+
() => {
|
|
51
|
+
feedback.value = ''
|
|
52
|
+
},
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Accept the plan: the run advances to the committer, which persists it and arms the execution
|
|
57
|
+
* loop. The window stays open — the review disappears with the approval (live) and the tracker it
|
|
58
|
+
* gives the window back to is where the plan then executes.
|
|
59
|
+
*/
|
|
60
|
+
async function approve() {
|
|
61
|
+
if (submitting.value || !props.canExecute) return
|
|
62
|
+
submitting.value = true
|
|
63
|
+
try {
|
|
64
|
+
await execution.approveStep(props.instanceId, props.approvalId)
|
|
65
|
+
} finally {
|
|
66
|
+
submitting.value = false
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Send the plan back: the planner re-plans from the feedback + the anchored comments. */
|
|
71
|
+
async function requestChanges() {
|
|
72
|
+
if (submitting.value || !canRequestChanges.value || !props.canExecute) return
|
|
73
|
+
submitting.value = true
|
|
74
|
+
try {
|
|
75
|
+
const ok = await execution.requestStepChanges(props.instanceId, props.approvalId, {
|
|
76
|
+
feedback: feedback.value.trim() || undefined,
|
|
77
|
+
comments: props.comments,
|
|
78
|
+
})
|
|
79
|
+
if (ok) {
|
|
80
|
+
feedback.value = ''
|
|
81
|
+
emit('sent')
|
|
82
|
+
}
|
|
83
|
+
} finally {
|
|
84
|
+
submitting.value = false
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const disabledTitle = computed(() => (props.canExecute ? undefined : t('access.noRunExecute')))
|
|
89
|
+
</script>
|
|
90
|
+
|
|
91
|
+
<template>
|
|
92
|
+
<div>
|
|
93
|
+
<UTextarea
|
|
94
|
+
v-model="feedback"
|
|
95
|
+
data-testid="initiative-plan-feedback"
|
|
96
|
+
:rows="2"
|
|
97
|
+
autoresize
|
|
98
|
+
size="sm"
|
|
99
|
+
class="w-full"
|
|
100
|
+
:placeholder="t('initiative.planReview.feedbackPlaceholder')"
|
|
101
|
+
/>
|
|
102
|
+
<div class="mt-2 flex flex-wrap items-center gap-2">
|
|
103
|
+
<UButton
|
|
104
|
+
color="primary"
|
|
105
|
+
size="xs"
|
|
106
|
+
icon="i-lucide-check"
|
|
107
|
+
data-testid="initiative-plan-approve"
|
|
108
|
+
:loading="submitting"
|
|
109
|
+
:disabled="!canExecute"
|
|
110
|
+
:title="disabledTitle"
|
|
111
|
+
@click="approve"
|
|
112
|
+
>
|
|
113
|
+
{{ t('initiative.planReview.approve') }}
|
|
114
|
+
</UButton>
|
|
115
|
+
<UButton
|
|
116
|
+
color="warning"
|
|
117
|
+
variant="soft"
|
|
118
|
+
size="xs"
|
|
119
|
+
icon="i-lucide-rotate-ccw"
|
|
120
|
+
data-testid="initiative-plan-send-back"
|
|
121
|
+
:loading="submitting"
|
|
122
|
+
:disabled="!canRequestChanges || !canExecute"
|
|
123
|
+
:title="
|
|
124
|
+
canExecute && !canRequestChanges
|
|
125
|
+
? t('initiative.planReview.needsFeedback')
|
|
126
|
+
: disabledTitle
|
|
127
|
+
"
|
|
128
|
+
@click="requestChanges"
|
|
129
|
+
>
|
|
130
|
+
{{ t('initiative.planReview.sendBack') }}
|
|
131
|
+
</UButton>
|
|
132
|
+
</div>
|
|
133
|
+
</div>
|
|
134
|
+
</template>
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
// The plan gate with NO document to review: the compact rail that sits above the tracker's own
|
|
3
|
+
// sections, states that there is nothing to navigate, and still resolves the park.
|
|
4
|
+
//
|
|
5
|
+
// It is NOT a legacy shim for gates parked before the engine rendered plans. `outputIsRendered` is a
|
|
6
|
+
// fact about the STEP, and a plan gate can park without it today: the planner's post-completion
|
|
7
|
+
// resolver authors the rendering only once it has INGESTED the plan, so a run that reaches the gate
|
|
8
|
+
// without an ingest parks on the planner's transcript summary instead — a perfectly non-empty string
|
|
9
|
+
// that `planReviewDocument` correctly reads as no document (dressing one sentence up under a table
|
|
10
|
+
// of contents is the failure the whole review surface exists to end). Without this surface such a
|
|
11
|
+
// gate would have no resolving surface at all, which is the exact bug the plan-review e2e spec was
|
|
12
|
+
// written for.
|
|
13
|
+
//
|
|
14
|
+
// So it is deliberately NOT a takeover: unlike `InitiativePlanReview`, which replaces the tracker
|
|
15
|
+
// because the document repeats it, this one wants the tracker underneath — there the sections ARE
|
|
16
|
+
// the only rendering of the plan there is. Which is why it has to be TOLD whether they are actually
|
|
17
|
+
// on screen: it renders above the entity branch, because a gate can be parked while the entity is
|
|
18
|
+
// still loading, and pointing a reviewer at sections the window is not showing is worse than saying
|
|
19
|
+
// nothing.
|
|
20
|
+
import type { StepApproval } from '~/types/execution'
|
|
21
|
+
import InitiativePlanDecision from '~/components/initiative/InitiativePlanDecision.vue'
|
|
22
|
+
|
|
23
|
+
defineProps<{
|
|
24
|
+
/** The parked gate. */
|
|
25
|
+
approval: StepApproval
|
|
26
|
+
/** The run the gate belongs to, for the approve / request-changes commands. */
|
|
27
|
+
instanceId: string
|
|
28
|
+
/** Whether the viewer may resolve runs at all (RBAC); false renders the actions disabled. */
|
|
29
|
+
canExecute: boolean
|
|
30
|
+
/**
|
|
31
|
+
* Whether the tracker's own goal / phases / policy sections are rendered below this notice — i.e.
|
|
32
|
+
* whether the initiative entity has loaded. Only then may the notice point at them as the plan.
|
|
33
|
+
*/
|
|
34
|
+
hasSections: boolean
|
|
35
|
+
}>()
|
|
36
|
+
|
|
37
|
+
const { t } = useI18n()
|
|
38
|
+
</script>
|
|
39
|
+
|
|
40
|
+
<template>
|
|
41
|
+
<section
|
|
42
|
+
class="mb-4 rounded-lg border border-amber-500/40 bg-amber-500/10 p-3.5"
|
|
43
|
+
data-testid="initiative-plan-notice"
|
|
44
|
+
>
|
|
45
|
+
<header class="flex items-start gap-2.5">
|
|
46
|
+
<UIcon name="i-lucide-clipboard-check" class="mt-0.5 h-4 w-4 shrink-0 text-amber-300" />
|
|
47
|
+
<div class="min-w-0 flex-1">
|
|
48
|
+
<h3 class="text-[13px] font-semibold text-amber-200">
|
|
49
|
+
{{ t('initiative.planReview.title') }}
|
|
50
|
+
</h3>
|
|
51
|
+
<p class="mt-0.5 text-[12px] leading-relaxed text-amber-100/80">
|
|
52
|
+
{{ t('initiative.planReview.body') }}
|
|
53
|
+
</p>
|
|
54
|
+
<p class="mt-2 text-[12px] leading-relaxed text-amber-100/70">
|
|
55
|
+
{{ t('initiative.planReview.noDocument') }}
|
|
56
|
+
<template v-if="hasSections">
|
|
57
|
+
{{ t('initiative.planReview.noDocumentSections') }}
|
|
58
|
+
</template>
|
|
59
|
+
</p>
|
|
60
|
+
</div>
|
|
61
|
+
</header>
|
|
62
|
+
<InitiativePlanDecision
|
|
63
|
+
class="mt-3"
|
|
64
|
+
:approval-id="approval.id"
|
|
65
|
+
:instance-id="instanceId"
|
|
66
|
+
:can-execute="canExecute"
|
|
67
|
+
/>
|
|
68
|
+
</section>
|
|
69
|
+
</template>
|
|
@@ -1,79 +1,76 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
// The planner's human gate, reviewed on the PLAN — the tracker window
|
|
2
|
+
// The planner's human gate, reviewed on the PLAN — the surface the tracker window hands its WHOLE
|
|
3
|
+
// body to while a rendered plan is parked.
|
|
3
4
|
//
|
|
4
|
-
// The rail this replaces could approve or send back, but it judged nothing: the plan was a wall
|
|
5
|
-
//
|
|
6
|
-
//
|
|
5
|
+
// The rail this replaces could approve or send back, but it judged nothing: the plan was a wall of
|
|
6
|
+
// structured sections above it, with no way to navigate a long one and no way to say WHICH part
|
|
7
|
+
// needed changing. So it renders the plan as the document it is — the engine puts a markdown
|
|
7
8
|
// rendering of the INGESTED plan on the gate's proposal (`renderInitiativePlanForReview`, authored
|
|
8
|
-
// by the planner's step resolver,
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
// that reader rather than re-implemented: `useStepProse` for the outline/collapse/scroll-spy,
|
|
12
|
-
// `useProseComments` for the anchoring, and the global `.reader-prose` sheet for the presentation,
|
|
13
|
-
// so the surfaces cannot drift.
|
|
9
|
+
// by the planner's step resolver, the only thing that knows what ingest committed) — and gives it
|
|
10
|
+
// the same three tools the step reader gives the architect's prose: an outline to navigate by,
|
|
11
|
+
// click-to-comment on any block, and overall feedback.
|
|
14
12
|
//
|
|
15
|
-
//
|
|
16
|
-
//
|
|
17
|
-
//
|
|
18
|
-
//
|
|
19
|
-
//
|
|
20
|
-
|
|
13
|
+
// The LAYOUT is the step reader's too, and for the same reasons (`AgentStepDetail.vue`). The first
|
|
14
|
+
// cut of this surface was a card INSIDE the tracker's scrolling column: outline and document split
|
|
15
|
+
// that column's width between them, the document capped at a 20rem window, and the tracker's own
|
|
16
|
+
// goal / phases / policy / logs sections — the very same plan, since the render reads the ingested
|
|
17
|
+
// entity — repeated underneath. Reviewers scrolled a letterbox while a second copy of what they
|
|
18
|
+
// were reading sat below it. Now the outline is a sidebar OUTSIDE the document, the document takes
|
|
19
|
+
// the full height of the window, the commands sit in an end-side rail, and the duplicate is gone
|
|
20
|
+
// because the window renders this INSTEAD of the tracker while the gate is parked (the tracker's
|
|
21
|
+
// remaining panels — PR links, curation, checkpoints, follow-ups — are all execution-time state
|
|
22
|
+
// that cannot exist yet at plan time).
|
|
23
|
+
//
|
|
24
|
+
// Everything under the layout is shared with that reader rather than re-implemented: `useStepProse`
|
|
25
|
+
// for the outline/collapse/scroll-spy, `useProseComments` for the anchoring, `InitiativePlanDecision`
|
|
26
|
+
// for the two commands, and the global `.reader-prose` sheet for the presentation — so the surfaces
|
|
27
|
+
// cannot drift.
|
|
28
|
+
import { ref, watch } from 'vue'
|
|
21
29
|
import type { StepApproval } from '~/types/execution'
|
|
22
30
|
import { useStepProse } from '~/composables/useStepProse'
|
|
23
31
|
import { useProseComments } from '~/composables/useProseComments'
|
|
32
|
+
import InitiativePlanDecision from '~/components/initiative/InitiativePlanDecision.vue'
|
|
24
33
|
|
|
25
34
|
const props = defineProps<{
|
|
26
|
-
/** The parked gate
|
|
35
|
+
/** The parked gate under review. */
|
|
27
36
|
approval: StepApproval
|
|
28
37
|
/** The run the gate belongs to, for the approve / request-changes commands. */
|
|
29
38
|
instanceId: string
|
|
30
39
|
/** Whether the viewer may resolve runs at all (RBAC); false renders the actions disabled. */
|
|
31
40
|
canExecute: boolean
|
|
32
41
|
/**
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
* plans parks on the planner's transcript summary, which is a perfectly non-empty string —
|
|
36
|
-
* so an emptiness check would show that one sentence under a table of contents as if it were
|
|
37
|
-
* the plan, which is the failure this whole surface exists to end.
|
|
42
|
+
* The rendered plan under review, resolved by the host through `planReviewDocument` — the same
|
|
43
|
+
* value that decided to mount THIS surface rather than the notice, so it is non-empty here.
|
|
38
44
|
*/
|
|
39
|
-
|
|
45
|
+
planDocument: string
|
|
40
46
|
}>()
|
|
41
47
|
|
|
42
|
-
const execution = useExecutionStore()
|
|
43
48
|
const { t } = useI18n()
|
|
44
49
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
* planner's transcript summary) reads as no document, and the `noDocument` notice says so rather
|
|
50
|
-
* than dressing a stray sentence up as the plan.
|
|
51
|
-
*/
|
|
52
|
-
const planDocument = computed(() => (props.outputIsRendered ? (props.approval.proposal ?? '') : ''))
|
|
53
|
-
|
|
54
|
-
// The outline + collapse + scroll-spy, exactly as the step reader resolves them — minus its
|
|
55
|
-
// lead anchor: the reader renders a details card ahead of the prose and this rail renders the
|
|
56
|
-
// document alone, and the spy stops at the first anchor it cannot measure.
|
|
57
|
-
const prose = useStepProse(() => planDocument.value, { leadAnchorId: null })
|
|
50
|
+
// The outline + collapse + scroll-spy, exactly as the step reader resolves them — minus its lead
|
|
51
|
+
// anchor: the reader renders a details card ahead of the prose, while here the run details sit in
|
|
52
|
+
// the sidebar (never scrolled past), and the spy stops at the first anchor it cannot measure.
|
|
53
|
+
const prose = useStepProse(() => props.planDocument, { leadAnchorId: null })
|
|
58
54
|
const {
|
|
59
55
|
outline,
|
|
60
56
|
tocSections,
|
|
61
|
-
hasOutput,
|
|
62
57
|
collapsed,
|
|
63
58
|
activeId,
|
|
64
|
-
// The
|
|
65
|
-
//
|
|
59
|
+
// The scroll container, bound straight through so the shared scroll-spy and the comment-highlight
|
|
60
|
+
// sync read the same element the template scrolls.
|
|
66
61
|
scrollEl,
|
|
67
62
|
sectionEls,
|
|
68
63
|
toggle,
|
|
64
|
+
setAll,
|
|
65
|
+
allCollapsed,
|
|
69
66
|
goTo,
|
|
70
67
|
onScroll,
|
|
71
68
|
} = prose
|
|
72
69
|
|
|
73
70
|
/**
|
|
74
|
-
* Per-block comment drafts over the plan, anchored to its source lines. Commenting follows the
|
|
75
|
-
*
|
|
76
|
-
*
|
|
71
|
+
* Per-block comment drafts over the plan, anchored to its source lines. Commenting follows the same
|
|
72
|
+
* RBAC gate as the commands: a viewer who cannot resolve the run cannot send the comments anywhere,
|
|
73
|
+
* so offering the composer would only invite work the Send-back button then refuses.
|
|
77
74
|
*/
|
|
78
75
|
const {
|
|
79
76
|
comments: planComments,
|
|
@@ -86,266 +83,293 @@ const {
|
|
|
86
83
|
removeComment,
|
|
87
84
|
reset: resetComments,
|
|
88
85
|
} = useProseComments({
|
|
89
|
-
output: () => planDocument
|
|
86
|
+
output: () => props.planDocument,
|
|
90
87
|
root: () => scrollEl.value,
|
|
91
88
|
enabled: () => props.canExecute,
|
|
92
89
|
})
|
|
93
90
|
|
|
94
|
-
|
|
95
|
-
const submitting = ref(false)
|
|
96
|
-
|
|
97
|
-
/** Changes can only be requested with something to act on — an empty send would re-plan blind. */
|
|
98
|
-
const canRequestChanges = computed(() => !!feedback.value.trim() || planComments.value.length > 0)
|
|
99
|
-
|
|
100
|
-
/** A fresh gate (a re-plan parked again) drops the drafts rather than carrying them over. */
|
|
91
|
+
/** A fresh gate (a re-plan parked again) reviews the new plan clean, from the top. */
|
|
101
92
|
watch(
|
|
102
93
|
() => props.approval.id,
|
|
103
94
|
() => {
|
|
104
95
|
resetComments()
|
|
105
|
-
feedback.value = ''
|
|
106
96
|
prose.reset()
|
|
107
97
|
},
|
|
108
98
|
)
|
|
109
99
|
|
|
110
|
-
/**
|
|
111
|
-
|
|
112
|
-
* loop. The window stays open — the rail disappears with the approval (live) and the tracker is
|
|
113
|
-
* where the plan then executes.
|
|
114
|
-
*/
|
|
115
|
-
async function approve() {
|
|
116
|
-
if (submitting.value || !props.canExecute) return
|
|
117
|
-
submitting.value = true
|
|
118
|
-
try {
|
|
119
|
-
await execution.approveStep(props.instanceId, props.approval.id)
|
|
120
|
-
} finally {
|
|
121
|
-
submitting.value = false
|
|
122
|
-
}
|
|
123
|
-
}
|
|
100
|
+
/** Whether the sidebar's run-details stack is expanded (it is, until a reviewer wants the outline). */
|
|
101
|
+
const runDetailsOpen = ref(true)
|
|
124
102
|
|
|
125
|
-
|
|
126
|
-
async function
|
|
127
|
-
|
|
128
|
-
submitting.value = true
|
|
129
|
-
try {
|
|
130
|
-
const ok = await execution.requestStepChanges(props.instanceId, props.approval.id, {
|
|
131
|
-
feedback: feedback.value.trim() || undefined,
|
|
132
|
-
comments: wireComments.value,
|
|
133
|
-
})
|
|
134
|
-
if (ok) {
|
|
135
|
-
feedback.value = ''
|
|
136
|
-
resetComments()
|
|
137
|
-
}
|
|
138
|
-
} finally {
|
|
139
|
-
submitting.value = false
|
|
140
|
-
}
|
|
103
|
+
const { copy } = useCopyToClipboard()
|
|
104
|
+
async function copyPlan() {
|
|
105
|
+
await copy(props.planDocument)
|
|
141
106
|
}
|
|
142
|
-
|
|
143
|
-
const disabledTitle = computed(() => (props.canExecute ? undefined : t('access.noRunExecute')))
|
|
144
107
|
</script>
|
|
145
108
|
|
|
146
109
|
<template>
|
|
147
|
-
<
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
<h3 class="text-[13px] font-semibold text-amber-200">
|
|
155
|
-
{{ t('initiative.planReview.title') }}
|
|
156
|
-
</h3>
|
|
157
|
-
<p class="mt-0.5 text-[12px] leading-relaxed text-amber-100/80">
|
|
158
|
-
{{ t('initiative.planReview.body') }}
|
|
159
|
-
</p>
|
|
160
|
-
</div>
|
|
161
|
-
</header>
|
|
110
|
+
<div class="flex min-h-0 flex-1 flex-col lg:flex-row" data-testid="initiative-plan-review">
|
|
111
|
+
<!-- Navigation column: the outline OUTSIDE the document rather than splitting its width — a
|
|
112
|
+
sidebar of the window, as the step reader's is. Narrower than the reader's (`w-52` against
|
|
113
|
+
its `w-72`) and held back to `lg` rather than its `md`, because this is a THREE-column
|
|
114
|
+
layout: at 768px the document would be left ~240px between the outline and the review rail,
|
|
115
|
+
which reads worse than no outline at all. Below `lg` the whole column goes; the document and
|
|
116
|
+
the commands are what a narrow screen needs.
|
|
162
117
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
118
|
+
Its presence tracks its two contents INDEPENDENTLY rather than the outline alone. Gating the
|
|
119
|
+
run details on `outline.hasToc` would make whether this window still reports its model / run
|
|
120
|
+
id / token spend depend on whether the plan renderer happened to emit a heading — a fact
|
|
121
|
+
owned by `renderInitiativePlanForReview`, in another package, with nothing pinning it. The
|
|
122
|
+
step reader keeps the same document-level affordances out of its own `hasToc` guard for that
|
|
123
|
+
reason, in a main-column header this surface does not have. -->
|
|
124
|
+
<aside
|
|
125
|
+
v-if="outline.hasToc || $slots['run-details']"
|
|
126
|
+
class="hidden w-52 shrink-0 flex-col border-e border-slate-800 bg-slate-900/60 lg:flex"
|
|
127
|
+
>
|
|
128
|
+
<div class="flex items-center gap-0.5 border-b border-slate-800 px-3 py-2">
|
|
129
|
+
<span
|
|
130
|
+
v-if="outline.hasToc"
|
|
131
|
+
class="min-w-0 flex-1 truncate text-[11px] font-semibold uppercase tracking-wide text-slate-500"
|
|
132
|
+
>
|
|
133
|
+
{{ t('panels.stepDetail.contents') }}
|
|
134
|
+
</span>
|
|
135
|
+
<span v-else class="flex-1" />
|
|
136
|
+
<!-- Collapse-all tracks the outline: with no headings the only section is the untitled
|
|
137
|
+
preamble, which renders no toggle of its own, so collapsing it would hide the whole
|
|
138
|
+
plan with nothing on screen to bring it back. Copying it does not — that is about the
|
|
139
|
+
document, which exists either way. -->
|
|
140
|
+
<UButton
|
|
141
|
+
v-if="outline.hasToc"
|
|
142
|
+
:icon="allCollapsed ? 'i-lucide-unfold-vertical' : 'i-lucide-fold-vertical'"
|
|
143
|
+
color="neutral"
|
|
144
|
+
variant="ghost"
|
|
145
|
+
size="xs"
|
|
146
|
+
:title="
|
|
147
|
+
allCollapsed ? t('panels.stepDetail.expandAll') : t('panels.stepDetail.collapseAll')
|
|
148
|
+
"
|
|
149
|
+
@click="setAll(!allCollapsed)"
|
|
150
|
+
/>
|
|
151
|
+
<UButton
|
|
152
|
+
icon="i-lucide-copy"
|
|
153
|
+
color="neutral"
|
|
154
|
+
variant="ghost"
|
|
155
|
+
size="xs"
|
|
156
|
+
:title="t('panels.stepDetail.copyRawOutput')"
|
|
157
|
+
@click="copyPlan"
|
|
158
|
+
/>
|
|
159
|
+
</div>
|
|
170
160
|
<nav
|
|
171
161
|
v-if="outline.hasToc"
|
|
172
162
|
data-testid="initiative-plan-toc"
|
|
173
|
-
|
|
163
|
+
:aria-label="t('panels.stepDetail.contents')"
|
|
164
|
+
class="flex-1 space-y-0.5 overflow-y-auto px-2 py-2"
|
|
174
165
|
>
|
|
175
166
|
<button
|
|
176
167
|
v-for="s in tocSections"
|
|
177
168
|
:key="s.id"
|
|
178
|
-
class="block w-full truncate rounded px-
|
|
169
|
+
class="block w-full truncate rounded-md px-2 py-1 text-start text-[12px] transition"
|
|
179
170
|
:class="
|
|
180
171
|
activeId === s.id
|
|
181
|
-
? 'bg-amber-500/
|
|
182
|
-
: 'text-
|
|
172
|
+
? 'bg-amber-500/15 font-medium text-amber-100'
|
|
173
|
+
: 'text-slate-400 hover:bg-slate-800/60 hover:text-slate-200'
|
|
183
174
|
"
|
|
184
|
-
:style="{ paddingLeft: `${(s.depth - outline.minDepth) * 0.7 + 0.
|
|
175
|
+
:style="{ paddingLeft: `${(s.depth - outline.minDepth) * 0.7 + 0.5}rem` }"
|
|
185
176
|
:title="s.title"
|
|
186
177
|
@click="goTo(s.id)"
|
|
187
178
|
>
|
|
188
179
|
{{ s.title }}
|
|
189
180
|
</button>
|
|
190
181
|
</nav>
|
|
191
|
-
|
|
182
|
+
<!-- Run details (the model, the run id, the token telemetry). Filled by the host, which
|
|
183
|
+
already resolves the bundle through `useResultViewRunMeta`; it keeps its home in a
|
|
184
|
+
sidebar here rather than disappearing for the duration of the review. Open, but
|
|
185
|
+
collapsible: it is a stack of seven labelled fields, and a reviewer navigating a long
|
|
186
|
+
plan should be able to give the outline the whole column. With no outline above it there
|
|
187
|
+
is no column to give back, so it takes the space instead of leaving 55% of it empty. -->
|
|
192
188
|
<div
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
class="
|
|
196
|
-
|
|
189
|
+
v-if="$slots['run-details']"
|
|
190
|
+
class="flex flex-col"
|
|
191
|
+
:class="
|
|
192
|
+
outline.hasToc ? 'max-h-[45%] shrink-0 border-t border-slate-800' : 'min-h-0 flex-1'
|
|
193
|
+
"
|
|
197
194
|
>
|
|
198
|
-
<
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
:
|
|
203
|
-
|
|
195
|
+
<button
|
|
196
|
+
type="button"
|
|
197
|
+
data-testid="initiative-plan-run-meta-toggle"
|
|
198
|
+
class="flex shrink-0 items-center gap-1.5 px-3 py-2 text-start transition hover:bg-slate-800/40"
|
|
199
|
+
:aria-expanded="runDetailsOpen"
|
|
200
|
+
@click="runDetailsOpen = !runDetailsOpen"
|
|
204
201
|
>
|
|
205
|
-
<
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
name="i-lucide-chevron-right"
|
|
212
|
-
class="h-3.5 w-3.5 shrink-0 text-slate-500 transition-transform"
|
|
213
|
-
:class="collapsed[s.id] ? '' : 'rotate-90'"
|
|
214
|
-
/>
|
|
215
|
-
<span
|
|
216
|
-
class="font-semibold text-slate-100"
|
|
217
|
-
:class="s.depth <= 1 ? 'text-sm' : 'text-[13px]'"
|
|
218
|
-
v-html="s.titleHtml"
|
|
219
|
-
/>
|
|
220
|
-
</button>
|
|
221
|
-
<!-- `review-mode` carries the click-to-comment affordance, so it tracks the same RBAC
|
|
222
|
-
gate the composer does — a viewer gets the document, not hover targets that lead
|
|
223
|
-
nowhere. -->
|
|
224
|
-
<!-- eslint-disable-next-line vue/no-v-html -->
|
|
225
|
-
<div
|
|
226
|
-
v-show="!collapsed[s.id]"
|
|
227
|
-
class="reader-prose mt-0.5 text-[12px] leading-relaxed text-slate-300"
|
|
228
|
-
:class="[s.depth > 0 ? 'ps-5' : '', canExecute ? 'review-mode' : '']"
|
|
229
|
-
@click="onProseClick"
|
|
230
|
-
v-html="s.bodyHtml"
|
|
202
|
+
<span class="flex-1 text-[11px] font-semibold uppercase tracking-wide text-slate-500">
|
|
203
|
+
{{ t('panels.stepDetail.details') }}
|
|
204
|
+
</span>
|
|
205
|
+
<UIcon
|
|
206
|
+
:name="runDetailsOpen ? 'i-lucide-chevron-down' : 'i-lucide-chevron-up'"
|
|
207
|
+
class="h-3.5 w-3.5 shrink-0 text-slate-500"
|
|
231
208
|
/>
|
|
232
|
-
</
|
|
209
|
+
</button>
|
|
210
|
+
<div
|
|
211
|
+
v-if="runDetailsOpen"
|
|
212
|
+
data-testid="initiative-plan-run-meta"
|
|
213
|
+
class="min-h-0 flex-1 overflow-y-auto px-3 pb-3"
|
|
214
|
+
>
|
|
215
|
+
<slot name="run-details" />
|
|
216
|
+
</div>
|
|
233
217
|
</div>
|
|
234
|
-
</
|
|
235
|
-
<p v-else class="mt-2 px-3.5 text-[12px] text-amber-100/70">
|
|
236
|
-
{{ t('initiative.planReview.noDocument') }}
|
|
237
|
-
</p>
|
|
218
|
+
</aside>
|
|
238
219
|
|
|
239
|
-
<!--
|
|
240
|
-
<div
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
220
|
+
<!-- The plan itself, with the full height of the window to be read in. -->
|
|
221
|
+
<div
|
|
222
|
+
ref="scrollEl"
|
|
223
|
+
data-testid="initiative-plan-document"
|
|
224
|
+
class="min-h-0 min-w-0 flex-1 overflow-y-auto px-5 py-4"
|
|
225
|
+
@scroll="onScroll"
|
|
226
|
+
>
|
|
227
|
+
<!-- No `max-w-*` reading measure here: with the outline and the review rail both taking a
|
|
228
|
+
fixed column out of the shell's `5xl`, this one is ~490px wide at every size that renders
|
|
229
|
+
it, so a cap would only ever be dead markup. -->
|
|
230
|
+
<section
|
|
231
|
+
v-for="s in outline.sections"
|
|
232
|
+
:id="s.id"
|
|
233
|
+
:key="s.id"
|
|
234
|
+
:ref="(el) => (sectionEls[s.id] = el as HTMLElement | null)"
|
|
235
|
+
class="scroll-mt-2"
|
|
236
|
+
>
|
|
237
|
+
<button
|
|
238
|
+
v-if="s.depth > 0"
|
|
239
|
+
class="group flex w-full items-center gap-1.5 rounded py-0.5 text-start transition hover:text-white"
|
|
240
|
+
:aria-expanded="!collapsed[s.id]"
|
|
241
|
+
@click="toggle(s.id)"
|
|
242
|
+
>
|
|
243
|
+
<UIcon
|
|
244
|
+
name="i-lucide-chevron-right"
|
|
245
|
+
class="h-3.5 w-3.5 shrink-0 text-slate-500 transition-transform group-hover:text-slate-300"
|
|
246
|
+
:class="collapsed[s.id] ? '' : 'rotate-90'"
|
|
247
|
+
/>
|
|
248
|
+
<span
|
|
249
|
+
class="font-semibold text-slate-100"
|
|
250
|
+
:class="s.depth <= 1 ? 'text-base' : s.depth === 2 ? 'text-sm' : 'text-[13px]'"
|
|
251
|
+
v-html="s.titleHtml"
|
|
252
|
+
/>
|
|
253
|
+
</button>
|
|
254
|
+
<!-- `review-mode` carries the click-to-comment affordance, so it tracks the same RBAC
|
|
255
|
+
gate the composer does — a viewer gets the document, not hover targets that lead
|
|
256
|
+
nowhere. -->
|
|
257
|
+
<!-- eslint-disable-next-line vue/no-v-html -->
|
|
258
|
+
<div
|
|
259
|
+
v-show="!collapsed[s.id]"
|
|
260
|
+
class="reader-prose mt-0.5 text-[13px] leading-relaxed text-slate-300"
|
|
261
|
+
:class="[s.depth > 0 ? 'ps-5' : '', canExecute ? 'review-mode' : '']"
|
|
262
|
+
@click="onProseClick"
|
|
263
|
+
v-html="s.bodyHtml"
|
|
256
264
|
/>
|
|
257
|
-
|
|
258
|
-
<UButton color="neutral" variant="ghost" size="xs" @click="cancelDraft">
|
|
259
|
-
{{ t('common.cancel') }}
|
|
260
|
-
</UButton>
|
|
261
|
-
<UButton
|
|
262
|
-
color="primary"
|
|
263
|
-
size="xs"
|
|
264
|
-
data-testid="initiative-plan-comment-add"
|
|
265
|
-
:disabled="!draftBody.trim()"
|
|
266
|
-
@click="addDraftComment"
|
|
267
|
-
>
|
|
268
|
-
{{ t('panels.stepDetail.addComment') }}
|
|
269
|
-
</UButton>
|
|
270
|
-
</div>
|
|
271
|
-
</div>
|
|
265
|
+
</section>
|
|
272
266
|
</div>
|
|
273
267
|
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
268
|
+
<!-- Review rail: what the human is being asked, the anchored comments so far, and the two
|
|
269
|
+
commands — the step reader's end-side rail. Below `lg` it drops under the document, capped
|
|
270
|
+
so a long comment list can't crowd the plan off the screen. -->
|
|
271
|
+
<aside
|
|
272
|
+
:aria-label="t('initiative.planReview.title')"
|
|
273
|
+
class="flex max-h-[55%] w-full shrink-0 flex-col border-t border-slate-800 bg-slate-900/60 lg:max-h-none lg:w-72 lg:border-s lg:border-t-0"
|
|
274
|
+
>
|
|
275
|
+
<div class="border-b border-slate-800 px-4 py-3">
|
|
276
|
+
<!-- A HEADING, not a styled div: this rail is what the window is now for, so the surface
|
|
277
|
+
that asks the human for a decision has to be reachable as one. -->
|
|
278
|
+
<h3
|
|
279
|
+
class="flex items-center gap-1.5 text-[11px] font-semibold uppercase tracking-wide text-amber-400"
|
|
280
|
+
>
|
|
281
|
+
<UIcon name="i-lucide-clipboard-check" class="h-3.5 w-3.5 shrink-0" />
|
|
282
|
+
{{ t('initiative.planReview.title') }}
|
|
283
|
+
</h3>
|
|
284
|
+
<p class="mt-1 text-[12px] leading-relaxed text-slate-400">
|
|
285
|
+
{{ t('initiative.planReview.body') }}
|
|
286
|
+
</p>
|
|
287
|
+
</div>
|
|
288
|
+
|
|
289
|
+
<div class="flex-1 space-y-3 overflow-y-auto overscroll-contain px-4 py-3">
|
|
290
|
+
<!-- Composer for the block just clicked. -->
|
|
291
|
+
<div
|
|
292
|
+
v-if="draftTarget"
|
|
293
|
+
data-testid="initiative-plan-composer"
|
|
294
|
+
class="rounded-lg border border-indigo-500/40 bg-indigo-500/5 p-2.5"
|
|
295
|
+
>
|
|
296
|
+
<div class="mb-1 text-[10px] uppercase tracking-wide text-indigo-300">
|
|
297
|
+
{{ t('panels.stepDetail.commentingOn') }}
|
|
298
|
+
</div>
|
|
299
|
+
<pre
|
|
300
|
+
class="mb-2 max-h-20 overflow-auto whitespace-pre-wrap rounded bg-slate-950/60 p-1.5 text-[11px] text-slate-300"
|
|
301
|
+
>{{ draftTarget.quotedSource }}</pre>
|
|
302
|
+
<UTextarea
|
|
303
|
+
v-model="draftBody"
|
|
304
|
+
data-testid="initiative-plan-comment-body"
|
|
305
|
+
:rows="2"
|
|
306
|
+
autoresize
|
|
307
|
+
size="sm"
|
|
308
|
+
class="w-full"
|
|
309
|
+
:placeholder="t('panels.stepDetail.commentPlaceholder')"
|
|
310
|
+
/>
|
|
311
|
+
<div class="mt-2 flex justify-end gap-2">
|
|
312
|
+
<UButton color="neutral" variant="ghost" size="xs" @click="cancelDraft">
|
|
313
|
+
{{ t('common.cancel') }}
|
|
314
|
+
</UButton>
|
|
315
|
+
<UButton
|
|
316
|
+
color="primary"
|
|
317
|
+
size="xs"
|
|
318
|
+
data-testid="initiative-plan-comment-add"
|
|
319
|
+
:disabled="!draftBody.trim()"
|
|
320
|
+
@click="addDraftComment"
|
|
321
|
+
>
|
|
322
|
+
{{ t('panels.stepDetail.addComment') }}
|
|
323
|
+
</UButton>
|
|
284
324
|
</div>
|
|
285
|
-
<button
|
|
286
|
-
class="text-slate-500 transition hover:text-rose-400"
|
|
287
|
-
:title="t('panels.stepDetail.removeComment')"
|
|
288
|
-
@click="removeComment(idx)"
|
|
289
|
-
>
|
|
290
|
-
<UIcon name="i-lucide-x" class="h-3.5 w-3.5" />
|
|
291
|
-
</button>
|
|
292
325
|
</div>
|
|
293
|
-
<pre
|
|
294
|
-
class="mb-1 max-h-16 overflow-auto whitespace-pre-wrap rounded bg-slate-950/50 p-1.5 text-[10px] text-slate-400"
|
|
295
|
-
>{{ c.quotedSource }}</pre>
|
|
296
|
-
<p class="text-[12px] text-slate-200">{{ c.body }}</p>
|
|
297
|
-
</li>
|
|
298
|
-
</ul>
|
|
299
326
|
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
data-testid="initiative-plan-feedback"
|
|
307
|
-
:rows="2"
|
|
308
|
-
autoresize
|
|
309
|
-
size="sm"
|
|
310
|
-
class="w-full"
|
|
311
|
-
:placeholder="t('initiative.planReview.feedbackPlaceholder')"
|
|
312
|
-
/>
|
|
313
|
-
<div class="mt-2 flex flex-wrap items-center gap-2">
|
|
314
|
-
<UButton
|
|
315
|
-
color="primary"
|
|
316
|
-
size="xs"
|
|
317
|
-
icon="i-lucide-check"
|
|
318
|
-
data-testid="initiative-plan-approve"
|
|
319
|
-
:loading="submitting"
|
|
320
|
-
:disabled="!canExecute"
|
|
321
|
-
:title="disabledTitle"
|
|
322
|
-
@click="approve"
|
|
327
|
+
<!-- The anchored comments so far. -->
|
|
328
|
+
<div
|
|
329
|
+
v-for="(c, idx) in planComments"
|
|
330
|
+
:key="idx"
|
|
331
|
+
data-testid="initiative-plan-comment"
|
|
332
|
+
class="rounded-lg border border-slate-800 bg-slate-900/50 p-2.5"
|
|
323
333
|
>
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
334
|
+
<div class="mb-1 flex items-start justify-between gap-2">
|
|
335
|
+
<div class="text-[10px] uppercase tracking-wide text-slate-500">
|
|
336
|
+
{{ t('panels.stepDetail.commentN', { number: idx + 1 }) }}
|
|
337
|
+
</div>
|
|
338
|
+
<button
|
|
339
|
+
class="text-slate-500 transition hover:text-rose-400"
|
|
340
|
+
:title="t('panels.stepDetail.removeComment')"
|
|
341
|
+
@click="removeComment(idx)"
|
|
342
|
+
>
|
|
343
|
+
<UIcon name="i-lucide-x" class="h-3.5 w-3.5" />
|
|
344
|
+
</button>
|
|
345
|
+
</div>
|
|
346
|
+
<pre
|
|
347
|
+
class="mb-1 max-h-16 overflow-auto whitespace-pre-wrap rounded bg-slate-950/50 p-1.5 text-[10px] text-slate-400"
|
|
348
|
+
>{{ c.quotedSource }}</pre>
|
|
349
|
+
<p class="text-[12px] text-slate-200">{{ c.body }}</p>
|
|
350
|
+
</div>
|
|
351
|
+
|
|
352
|
+
<!-- Only worth saying where clicking a block does something (the RBAC to act on it) and
|
|
353
|
+
where nothing has been said yet. -->
|
|
354
|
+
<p
|
|
355
|
+
v-if="canExecute && !draftTarget && !planComments.length"
|
|
356
|
+
class="text-[11px] leading-relaxed text-slate-400"
|
|
340
357
|
>
|
|
341
|
-
{{ t('initiative.planReview.sendBack') }}
|
|
342
|
-
</UButton>
|
|
343
|
-
<!-- Only worth saying where clicking a block does something (a document + the RBAC to
|
|
344
|
-
act on it). -->
|
|
345
|
-
<p v-if="hasOutput && canExecute" class="text-[10px] text-amber-100/60">
|
|
346
358
|
{{ t('initiative.planReview.commentHint') }}
|
|
347
359
|
</p>
|
|
348
360
|
</div>
|
|
349
|
-
|
|
350
|
-
|
|
361
|
+
|
|
362
|
+
<!-- Overall feedback + the two commands. Feedback stays visible rather than hiding behind a
|
|
363
|
+
"request changes" step: with per-block comments in play the human is already composing a
|
|
364
|
+
review, and a hidden field reads as "there is nothing more to say". -->
|
|
365
|
+
<InitiativePlanDecision
|
|
366
|
+
class="border-t border-slate-800 px-4 pt-3 pb-[calc(0.75rem+env(safe-area-inset-bottom))]"
|
|
367
|
+
:approval-id="approval.id"
|
|
368
|
+
:instance-id="instanceId"
|
|
369
|
+
:can-execute="canExecute"
|
|
370
|
+
:comments="wireComments"
|
|
371
|
+
@sent="resetComments"
|
|
372
|
+
/>
|
|
373
|
+
</aside>
|
|
374
|
+
</div>
|
|
351
375
|
</template>
|
|
@@ -23,10 +23,12 @@ import {
|
|
|
23
23
|
INITIATIVE_STATUS_LABEL_KEYS,
|
|
24
24
|
initiativeProgress,
|
|
25
25
|
pendingCheckpointPhase,
|
|
26
|
+
planReviewDocument,
|
|
26
27
|
} from '~/utils/initiative'
|
|
27
28
|
import ResultWindowShell from '~/components/panels/ResultWindowShell.vue'
|
|
28
29
|
import StepRunMeta from '~/components/panels/StepRunMeta.vue'
|
|
29
30
|
import InitiativePlanReview from '~/components/initiative/InitiativePlanReview.vue'
|
|
31
|
+
import InitiativePlanNotice from '~/components/initiative/InitiativePlanNotice.vue'
|
|
30
32
|
|
|
31
33
|
const board = useBoardStore()
|
|
32
34
|
const initiatives = useInitiativesStore()
|
|
@@ -61,6 +63,24 @@ const {
|
|
|
61
63
|
stepIndex: () => stepIndex.value,
|
|
62
64
|
})
|
|
63
65
|
|
|
66
|
+
/**
|
|
67
|
+
* The `StepRunMeta` prop bundle, or null when no step speaks for this window. Bound as one object
|
|
68
|
+
* because it has two homes — the tracker's end-side column, and the plan review's sidebar while the
|
|
69
|
+
* review owns the window — and the run details must read identically in both.
|
|
70
|
+
*/
|
|
71
|
+
const runMeta = computed(() =>
|
|
72
|
+
metaStep.value
|
|
73
|
+
? {
|
|
74
|
+
step: metaStep.value,
|
|
75
|
+
instanceId: runId.value,
|
|
76
|
+
stepNumber: position.value,
|
|
77
|
+
totalSteps: totalSteps.value,
|
|
78
|
+
runFailed: runFailed.value,
|
|
79
|
+
failureAt: failureAt.value,
|
|
80
|
+
}
|
|
81
|
+
: null,
|
|
82
|
+
)
|
|
83
|
+
|
|
64
84
|
const phases = computed(() => initiative.value?.phases ?? [])
|
|
65
85
|
function itemsOf(phaseId: string): InitiativeItem[] {
|
|
66
86
|
return (initiative.value?.items ?? []).filter((i) => i.phaseId === phaseId)
|
|
@@ -103,9 +123,19 @@ async function checkpointControl(action: 'resume' | 'cancel') {
|
|
|
103
123
|
// ---- Plan review: the planner step's human gate, resolved right here -----------------------
|
|
104
124
|
// Derived from the BLOCK (via the shared planning composable), not from this window's own
|
|
105
125
|
// `stepIndex`: the card / inspector open the tracker with no step, and that is the entry point a
|
|
106
|
-
// human parked on the gate actually uses. So the
|
|
126
|
+
// human parked on the gate actually uses. So the review appears on every route into the window.
|
|
107
127
|
const { planApproval } = useInitiativePlanning(() => blockId.value ?? '')
|
|
108
128
|
|
|
129
|
+
/**
|
|
130
|
+
* The plan document the parked gate offers, or `''` — which is also the layout decision. A rendered
|
|
131
|
+
* plan is a REPLACEMENT for the tracker body rather than a card above it: the render reads the
|
|
132
|
+
* ingested entity, so the sections below would be a second copy of what the reviewer is reading,
|
|
133
|
+
* and everything the tracker adds on top (PR links, item curation, checkpoints, follow-ups) is
|
|
134
|
+
* execution-time state that cannot exist until the plan is committed. With no document, the tracker
|
|
135
|
+
* body IS the plan, so the gate takes the compact notice above it instead.
|
|
136
|
+
*/
|
|
137
|
+
const planDocument = computed(() => planReviewDocument(planApproval.value))
|
|
138
|
+
|
|
109
139
|
const policyRules = computed(() => initiative.value?.policy?.rules ?? [])
|
|
110
140
|
function ruleAxes(rule: { minComplexity?: number; minRisk?: number; minImpact?: number }): string {
|
|
111
141
|
const axes = [
|
|
@@ -236,30 +266,54 @@ async function savePolicy() {
|
|
|
236
266
|
</UBadge>
|
|
237
267
|
</template>
|
|
238
268
|
|
|
239
|
-
|
|
269
|
+
<!-- The planner's human gate, with the plan rendered as a document. This window is where the
|
|
270
|
+
park ROUTES (the planner's archetype declares this result view), so it is the only surface
|
|
271
|
+
that can resolve it — and while it is parked the review OWNS the window: an outline sidebar,
|
|
272
|
+
the plan at full height, per-block commenting and the commands in an end-side rail, the same
|
|
273
|
+
tools and the same shape the step reader gives the architect's prose. The tracker body it
|
|
274
|
+
replaces would only repeat the plan (see `planDocument`). -->
|
|
275
|
+
<InitiativePlanReview
|
|
276
|
+
v-if="planApproval && planDocument"
|
|
277
|
+
:approval="planApproval.approval"
|
|
278
|
+
:instance-id="planApproval.instanceId"
|
|
279
|
+
:can-execute="access.canExecuteRuns.value"
|
|
280
|
+
:plan-document="planDocument"
|
|
281
|
+
>
|
|
282
|
+
<template v-if="runMeta" #run-details>
|
|
283
|
+
<StepRunMeta v-bind="runMeta" />
|
|
284
|
+
</template>
|
|
285
|
+
</InitiativePlanReview>
|
|
286
|
+
|
|
287
|
+
<div v-else class="flex min-h-0 flex-1">
|
|
240
288
|
<div class="min-w-0 flex-1 overflow-y-auto px-5 py-4">
|
|
241
|
-
<!--
|
|
289
|
+
<!-- A parked gate whose step rendered no plan: the commands, plus a notice pointing at the
|
|
290
|
+
sections below — which in that case are the only rendering of the plan there is.
|
|
291
|
+
Deliberately OUTSIDE the entity branch: the gate lives on the RUN, so it is parked
|
|
292
|
+
before `initiatives.load()` has resolved (and stays parked if it fails), and a window
|
|
293
|
+
that answered such a gate with the empty state alone would leave it unresolvable from
|
|
294
|
+
the UI. `hasSections` is what keeps the notice honest about whether the sections it
|
|
295
|
+
points at are actually rendered underneath. -->
|
|
296
|
+
<InitiativePlanNotice
|
|
297
|
+
v-if="planApproval"
|
|
298
|
+
:approval="planApproval.approval"
|
|
299
|
+
:instance-id="planApproval.instanceId"
|
|
300
|
+
:can-execute="access.canExecuteRuns.value"
|
|
301
|
+
:has-sections="!!initiative"
|
|
302
|
+
/>
|
|
303
|
+
|
|
304
|
+
<!-- No entity yet (module unwired / still creating). Centred in the column when it is the
|
|
305
|
+
only thing in it; merely inset when the notice above it means `h-full` would overflow
|
|
306
|
+
the scroller by the notice's own height. -->
|
|
242
307
|
<div
|
|
243
308
|
v-if="!initiative"
|
|
244
|
-
class="flex
|
|
309
|
+
class="flex flex-col items-center justify-center gap-2 text-center text-slate-400"
|
|
310
|
+
:class="planApproval ? 'py-16' : 'h-full'"
|
|
245
311
|
>
|
|
246
312
|
<UIcon name="i-lucide-milestone" class="h-8 w-8 opacity-40" />
|
|
247
313
|
<p class="text-sm">{{ t('initiative.tracker.empty') }}</p>
|
|
248
314
|
</div>
|
|
249
315
|
|
|
250
316
|
<template v-else>
|
|
251
|
-
<!-- The planner's human gate. This window is where the park ROUTES (the planner's
|
|
252
|
-
archetype declares this result view), so it is the only surface that can resolve
|
|
253
|
-
it — and the plan it judges is rendered as a navigable document with per-block
|
|
254
|
-
commenting, the same tools the step reader gives the architect's prose. -->
|
|
255
|
-
<InitiativePlanReview
|
|
256
|
-
v-if="planApproval"
|
|
257
|
-
:approval="planApproval.approval"
|
|
258
|
-
:instance-id="planApproval.instanceId"
|
|
259
|
-
:can-execute="access.canExecuteRuns.value"
|
|
260
|
-
:output-is-rendered="planApproval.outputIsRendered"
|
|
261
|
-
/>
|
|
262
|
-
|
|
263
317
|
<!-- Paused at a phase checkpoint (D2): a completed checkpoint phase is awaiting
|
|
264
318
|
review before the next phase spawns. Read the phase's artifacts/PRs below,
|
|
265
319
|
then resume (continue) or cancel (stop) the initiative right here. -->
|
|
@@ -646,18 +700,11 @@ async function savePolicy() {
|
|
|
646
700
|
through `useResultViewRunMeta`, so it is present on the card / inspector entry point
|
|
647
701
|
too — where this window carries no step index of its own. -->
|
|
648
702
|
<aside
|
|
649
|
-
v-if="
|
|
703
|
+
v-if="runMeta"
|
|
650
704
|
data-testid="initiative-tracker-run-meta"
|
|
651
705
|
class="hidden w-60 shrink-0 flex-col gap-4 overflow-y-auto border-s border-slate-800 bg-slate-900/50 px-4 py-4 lg:flex"
|
|
652
706
|
>
|
|
653
|
-
<StepRunMeta
|
|
654
|
-
:step="metaStep"
|
|
655
|
-
:instance-id="runId"
|
|
656
|
-
:step-number="position"
|
|
657
|
-
:total-steps="totalSteps"
|
|
658
|
-
:run-failed="runFailed"
|
|
659
|
-
:failure-at="failureAt"
|
|
660
|
-
/>
|
|
707
|
+
<StepRunMeta v-bind="runMeta" />
|
|
661
708
|
</aside>
|
|
662
709
|
</div>
|
|
663
710
|
</ResultWindowShell>
|
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
isPendingQuestion,
|
|
10
10
|
orderInterviewQuestions,
|
|
11
11
|
pendingCheckpointPhase,
|
|
12
|
+
planReviewDocument,
|
|
12
13
|
selectPlanApproval,
|
|
13
14
|
} from './initiative'
|
|
14
15
|
|
|
@@ -205,6 +206,48 @@ describe('selectPlanApproval', () => {
|
|
|
205
206
|
})
|
|
206
207
|
})
|
|
207
208
|
|
|
209
|
+
// Which shape the plan gate takes in the tracker window: a document review that OWNS the window, or
|
|
210
|
+
// the compact notice above the tracker's own sections. Both the window's layout and the review
|
|
211
|
+
// surface read this one value, so these pin the cases where "there is a plan to read" is not the
|
|
212
|
+
// same as "the proposal is non-empty".
|
|
213
|
+
|
|
214
|
+
describe('planReviewDocument', () => {
|
|
215
|
+
const gate = (proposal: string | null | undefined, outputIsRendered: boolean) => ({
|
|
216
|
+
approval: { proposal },
|
|
217
|
+
outputIsRendered,
|
|
218
|
+
})
|
|
219
|
+
|
|
220
|
+
it('is the proposal when the step says it IS the plan rendering', () => {
|
|
221
|
+
expect(planReviewDocument(gate('# Initiative plan\n\n## Goal', true))).toBe(
|
|
222
|
+
'# Initiative plan\n\n## Goal',
|
|
223
|
+
)
|
|
224
|
+
})
|
|
225
|
+
|
|
226
|
+
it('returns the proposal verbatim, so comment anchors stay on the lines they quote', () => {
|
|
227
|
+
// Anchoring is by SOURCE LINE, so trimming a leading newline would shift every anchor up one.
|
|
228
|
+
expect(planReviewDocument(gate('\n# Initiative plan\n', true))).toBe('\n# Initiative plan\n')
|
|
229
|
+
})
|
|
230
|
+
|
|
231
|
+
it('reads an un-rendered proposal as no document, however substantial it looks', () => {
|
|
232
|
+
// The planner's transcript summary: a perfectly non-empty string that is not the plan. Showing
|
|
233
|
+
// it under a table of contents is the failure the rendered review exists to end.
|
|
234
|
+
expect(
|
|
235
|
+
planReviewDocument(gate('I drafted a three-phase plan and stopped for review.', false)),
|
|
236
|
+
).toBe('')
|
|
237
|
+
})
|
|
238
|
+
|
|
239
|
+
it('reads a rendered but blank proposal as no document', () => {
|
|
240
|
+
expect(planReviewDocument(gate(' \n ', true))).toBe('')
|
|
241
|
+
expect(planReviewDocument(gate(null, true))).toBe('')
|
|
242
|
+
expect(planReviewDocument(gate(undefined, true))).toBe('')
|
|
243
|
+
})
|
|
244
|
+
|
|
245
|
+
it('has no document when nothing is parked', () => {
|
|
246
|
+
expect(planReviewDocument(null)).toBe('')
|
|
247
|
+
expect(planReviewDocument(undefined)).toBe('')
|
|
248
|
+
})
|
|
249
|
+
})
|
|
250
|
+
|
|
208
251
|
/**
|
|
209
252
|
* These tables are the reason the initiative card and the inspector word one park identically,
|
|
210
253
|
* and they are exactly the shape both i18n drift guards are blind to: the typed-key check and
|
package/app/utils/initiative.ts
CHANGED
|
@@ -103,6 +103,29 @@ export function selectPlanApproval<A extends { agentKind: string }>(
|
|
|
103
103
|
return approvals.find((a) => resultViewOf(a.agentKind) !== INTERVIEW_GATE_RESULT_VIEW)
|
|
104
104
|
}
|
|
105
105
|
|
|
106
|
+
/**
|
|
107
|
+
* The plan DOCUMENT a parked gate offers for review — its proposal, but only once the step says
|
|
108
|
+
* that proposal IS the plan rendering (`outputIsRendered`); `''` otherwise.
|
|
109
|
+
*
|
|
110
|
+
* A step that rendered nothing parks on the planner's transcript SUMMARY, which is a perfectly
|
|
111
|
+
* non-empty string, so an emptiness check alone would present one sentence under a table of
|
|
112
|
+
* contents as though it were the plan. `''` is what routes such a gate to the compact notice
|
|
113
|
+
* instead — and the SAME value decides the tracker window's layout (a document review takes the
|
|
114
|
+
* whole window; a notice sits above the tracker's own sections), so the surface and its host can
|
|
115
|
+
* never disagree about which shape is on screen.
|
|
116
|
+
*
|
|
117
|
+
* Rendered-but-blank counts as no document. The proposal comes back VERBATIM, never trimmed: the
|
|
118
|
+
* review anchors comments to source LINE numbers, so dropping a leading newline would shift every
|
|
119
|
+
* anchor off the block it quotes.
|
|
120
|
+
*/
|
|
121
|
+
export function planReviewDocument(
|
|
122
|
+
gate: { approval: { proposal?: string | null }; outputIsRendered: boolean } | null | undefined,
|
|
123
|
+
): string {
|
|
124
|
+
if (!gate?.outputIsRendered) return ''
|
|
125
|
+
const proposal = gate.approval.proposal ?? ''
|
|
126
|
+
return proposal.trim() ? proposal : ''
|
|
127
|
+
}
|
|
128
|
+
|
|
106
129
|
/** Follow-up triage status → i18n label key. Exhaustive so a new status fails the build. */
|
|
107
130
|
export const INITIATIVE_FOLLOWUP_STATUS_LABEL_KEYS: Record<InitiativeFollowUp['status'], string> = {
|
|
108
131
|
open: 'initiative.followUpStatus.open',
|
package/i18n/locales/de.json
CHANGED
|
@@ -4426,11 +4426,12 @@
|
|
|
4426
4426
|
},
|
|
4427
4427
|
"planReview": {
|
|
4428
4428
|
"title": "Dieser Plan wartet auf dich",
|
|
4429
|
-
"body": "Der Planner hat
|
|
4429
|
+
"body": "Der Planner hat diese Phasen und Aufgaben entworfen. Gib sie frei, um den Plan zu committen und die Arbeit zu starten, oder schicke den Plan mit deinen Änderungswünschen zurück.",
|
|
4430
4430
|
"approve": "Plan freigeben",
|
|
4431
4431
|
"feedbackPlaceholder": "Was soll der Planner ändern? Umfang, Reihenfolge der Phasen, fehlende Arbeit, eine Aufgabe, die woanders hingehört …",
|
|
4432
4432
|
"sendBack": "An den Planner zurückschicken",
|
|
4433
|
-
"noDocument": "Dieser
|
|
4433
|
+
"noDocument": "Dieser Planungsschritt hat kein Plandokument zur Prüfung hinterlassen, es gibt hier also nichts zu navigieren.",
|
|
4434
|
+
"noDocumentSections": "Der Plan sind die Abschnitte unten.",
|
|
4434
4435
|
"needsFeedback": "Füge zuerst einen Kommentar oder eine Rückmeldung hinzu — der Planer plant damit neu.",
|
|
4435
4436
|
"commentHint": "Klicke auf einen Teil des Plans, um ihn zu kommentieren."
|
|
4436
4437
|
},
|
package/i18n/locales/en.json
CHANGED
|
@@ -5645,11 +5645,12 @@
|
|
|
5645
5645
|
},
|
|
5646
5646
|
"planReview": {
|
|
5647
5647
|
"title": "This plan is waiting for you",
|
|
5648
|
-
"body": "The planner drafted
|
|
5648
|
+
"body": "The planner drafted these phases and items. Approve them to commit the plan and start the work, or send the plan back with what to change.",
|
|
5649
5649
|
"approve": "Approve plan",
|
|
5650
5650
|
"feedbackPlaceholder": "What should the planner change? Scope, phase order, missing work, an item that belongs elsewhere…",
|
|
5651
5651
|
"sendBack": "Send back to the planner",
|
|
5652
|
-
"noDocument": "This
|
|
5652
|
+
"noDocument": "This planning step left no plan document to review, so there is nothing to navigate here.",
|
|
5653
|
+
"noDocumentSections": "The sections below are the plan.",
|
|
5653
5654
|
"needsFeedback": "Add a comment or some feedback first — the planner re-plans from it.",
|
|
5654
5655
|
"commentHint": "Click any part of the plan to comment on it."
|
|
5655
5656
|
},
|
package/i18n/locales/es.json
CHANGED
|
@@ -5453,11 +5453,12 @@
|
|
|
5453
5453
|
},
|
|
5454
5454
|
"planReview": {
|
|
5455
5455
|
"title": "Este plan te está esperando",
|
|
5456
|
-
"body": "El planificador redactó
|
|
5456
|
+
"body": "El planificador redactó estas fases y estos elementos. Apruébalos para confirmar el plan y empezar el trabajo, o devuelve el plan indicando qué cambiar.",
|
|
5457
5457
|
"approve": "Aprobar plan",
|
|
5458
5458
|
"feedbackPlaceholder": "¿Qué debería cambiar el planificador? Alcance, orden de las fases, trabajo que falta, un elemento que va en otro sitio…",
|
|
5459
5459
|
"sendBack": "Devolver al planificador",
|
|
5460
|
-
"noDocument": "Este
|
|
5460
|
+
"noDocument": "Este paso de planificación no dejó ningún documento del plan para revisar, así que aquí no hay nada que recorrer.",
|
|
5461
|
+
"noDocumentSections": "Las secciones de abajo son el plan.",
|
|
5461
5462
|
"needsFeedback": "Añade primero un comentario o algún comentario general: el planificador replanifica a partir de ello.",
|
|
5462
5463
|
"commentHint": "Haz clic en cualquier parte del plan para comentarla."
|
|
5463
5464
|
},
|
package/i18n/locales/fr.json
CHANGED
|
@@ -5453,11 +5453,12 @@
|
|
|
5453
5453
|
},
|
|
5454
5454
|
"planReview": {
|
|
5455
5455
|
"title": "Ce plan vous attend",
|
|
5456
|
-
"body": "Le planificateur a rédigé
|
|
5456
|
+
"body": "Le planificateur a rédigé ces phases et ces éléments. Approuvez-les pour valider le plan et lancer le travail, ou renvoyez le plan en indiquant ce qu'il faut changer.",
|
|
5457
5457
|
"approve": "Approuver le plan",
|
|
5458
5458
|
"feedbackPlaceholder": "Que doit changer le planificateur ? Périmètre, ordre des phases, travail manquant, un élément qui a sa place ailleurs…",
|
|
5459
5459
|
"sendBack": "Renvoyer au planificateur",
|
|
5460
|
-
"noDocument": "
|
|
5460
|
+
"noDocument": "Cette étape de planification n'a laissé aucun document de plan à relire, il n'y a donc rien à parcourir ici.",
|
|
5461
|
+
"noDocumentSections": "Les sections ci-dessous sont le plan.",
|
|
5461
5462
|
"needsFeedback": "Ajoutez d'abord un commentaire ou un retour : le planificateur s'en sert pour replanifier.",
|
|
5462
5463
|
"commentHint": "Cliquez sur une partie du plan pour la commenter."
|
|
5463
5464
|
},
|
package/i18n/locales/he.json
CHANGED
|
@@ -5464,11 +5464,12 @@
|
|
|
5464
5464
|
},
|
|
5465
5465
|
"planReview": {
|
|
5466
5466
|
"title": "התוכנית הזו ממתינה לך",
|
|
5467
|
-
"body": "המתכנן ניסח את השלבים והפריטים
|
|
5467
|
+
"body": "המתכנן ניסח את השלבים והפריטים האלה. אשרו אותם כדי לשמור את התוכנית ולהתחיל בעבודה, או החזירו את התוכנית עם מה שצריך לשנות.",
|
|
5468
5468
|
"approve": "אישור התוכנית",
|
|
5469
5469
|
"feedbackPlaceholder": "מה המתכנן צריך לשנות? היקף, סדר השלבים, עבודה חסרה, פריט ששייך למקום אחר…",
|
|
5470
5470
|
"sendBack": "החזרה למתכנן",
|
|
5471
|
-
"noDocument": "
|
|
5471
|
+
"noDocument": "שלב התכנון הזה לא השאיר מסמך תוכנית לבדיקה, ולכן אין כאן במה לנווט.",
|
|
5472
|
+
"noDocumentSections": "הסעיפים שלמטה הם התוכנית.",
|
|
5472
5473
|
"needsFeedback": "הוסיפו תחילה הערה או משוב — המתכנן מתכנן מחדש על סמך זה.",
|
|
5473
5474
|
"commentHint": "לחצו על כל חלק בתוכנית כדי להעיר עליו."
|
|
5474
5475
|
},
|
package/i18n/locales/it.json
CHANGED
|
@@ -4426,11 +4426,12 @@
|
|
|
4426
4426
|
},
|
|
4427
4427
|
"planReview": {
|
|
4428
4428
|
"title": "Questo piano ti sta aspettando",
|
|
4429
|
-
"body": "Il planner ha redatto
|
|
4429
|
+
"body": "Il planner ha redatto queste fasi e questi elementi. Approvali per confermare il piano e avviare il lavoro, oppure rimanda indietro il piano indicando cosa cambiare.",
|
|
4430
4430
|
"approve": "Approva il piano",
|
|
4431
4431
|
"feedbackPlaceholder": "Cosa deve cambiare il planner? Ambito, ordine delle fasi, lavoro mancante, un elemento che sta altrove…",
|
|
4432
4432
|
"sendBack": "Rimanda al planner",
|
|
4433
|
-
"noDocument": "Questo
|
|
4433
|
+
"noDocument": "Questo passaggio di pianificazione non ha lasciato alcun documento del piano da revisionare, quindi qui non c'è nulla da percorrere.",
|
|
4434
|
+
"noDocumentSections": "Le sezioni qui sotto sono il piano.",
|
|
4434
4435
|
"needsFeedback": "Aggiungi prima un commento o un riscontro: il pianificatore ripianifica a partire da questo.",
|
|
4435
4436
|
"commentHint": "Fai clic su una parte del piano per commentarla."
|
|
4436
4437
|
},
|
package/i18n/locales/ja.json
CHANGED
|
@@ -5465,11 +5465,12 @@
|
|
|
5465
5465
|
},
|
|
5466
5466
|
"planReview": {
|
|
5467
5467
|
"title": "この計画が承認を待っています",
|
|
5468
|
-
"body": "
|
|
5468
|
+
"body": "プランナーがこれらのフェーズと項目を起草しました。承認すると計画がコミットされ、作業が始まります。変更したい点がある場合は、その内容を添えて計画を差し戻してください。",
|
|
5469
5469
|
"approve": "計画を承認",
|
|
5470
5470
|
"feedbackPlaceholder": "プランナーに変更してほしい点は何ですか。範囲、フェーズの順序、不足している作業、別の場所に属する項目など…",
|
|
5471
5471
|
"sendBack": "プランナーに差し戻す",
|
|
5472
|
-
"noDocument": "
|
|
5472
|
+
"noDocument": "この計画ステップはレビュー用の計画ドキュメントを残さなかったため、ここにはたどれるものがありません。",
|
|
5473
|
+
"noDocumentSections": "下のセクションが計画そのものです。",
|
|
5473
5474
|
"needsFeedback": "まずコメントかフィードバックを入力してください。プランナーはそれをもとに計画し直します。",
|
|
5474
5475
|
"commentHint": "計画の任意の箇所をクリックするとコメントできます。"
|
|
5475
5476
|
},
|
package/i18n/locales/pl.json
CHANGED
|
@@ -5453,11 +5453,12 @@
|
|
|
5453
5453
|
},
|
|
5454
5454
|
"planReview": {
|
|
5455
5455
|
"title": "Ten plan czeka na Ciebie",
|
|
5456
|
-
"body": "Planer przygotował
|
|
5456
|
+
"body": "Planer przygotował te fazy i elementy. Zatwierdź je, aby zapisać plan i rozpocząć pracę, albo odeślij plan z informacją, co zmienić.",
|
|
5457
5457
|
"approve": "Zatwierdź plan",
|
|
5458
5458
|
"feedbackPlaceholder": "Co planer ma zmienić? Zakres, kolejność faz, brakująca praca, element, który pasuje gdzie indziej…",
|
|
5459
5459
|
"sendBack": "Odeślij do planera",
|
|
5460
|
-
"noDocument": "Ten
|
|
5460
|
+
"noDocument": "Ten krok planowania nie pozostawił dokumentu planu do przeglądu, więc nie ma tu po czym nawigować.",
|
|
5461
|
+
"noDocumentSections": "Plan stanowią sekcje poniżej.",
|
|
5461
5462
|
"needsFeedback": "Dodaj najpierw komentarz lub uwagi — planista na ich podstawie planuje ponownie.",
|
|
5462
5463
|
"commentHint": "Kliknij dowolny fragment planu, aby go skomentować."
|
|
5463
5464
|
},
|
package/i18n/locales/tr.json
CHANGED
|
@@ -5465,11 +5465,12 @@
|
|
|
5465
5465
|
},
|
|
5466
5466
|
"planReview": {
|
|
5467
5467
|
"title": "Bu plan sizi bekliyor",
|
|
5468
|
-
"body": "Planlayıcı
|
|
5468
|
+
"body": "Planlayıcı bu aşamaları ve maddeleri hazırladı. Planı kaydedip işe başlamak için onaylayın ya da neyin değişmesi gerektiğini yazarak planı geri gönderin.",
|
|
5469
5469
|
"approve": "Planı onayla",
|
|
5470
5470
|
"feedbackPlaceholder": "Planlayıcı neyi değiştirmeli? Kapsam, aşama sırası, eksik iş, başka yere ait bir madde…",
|
|
5471
5471
|
"sendBack": "Planlayıcıya geri gönder",
|
|
5472
|
-
"noDocument": "Bu
|
|
5472
|
+
"noDocument": "Bu planlama adımı incelenecek bir plan belgesi bırakmadı, bu yüzden burada gezinilecek bir şey yok.",
|
|
5473
|
+
"noDocumentSections": "Plan aşağıdaki bölümlerdir.",
|
|
5473
5474
|
"needsFeedback": "Önce bir yorum veya geri bildirim ekleyin — planlayıcı yeniden planlamak için bunu kullanır.",
|
|
5474
5475
|
"commentHint": "Yorum yapmak için planın herhangi bir bölümüne tıklayın."
|
|
5475
5476
|
},
|
package/i18n/locales/uk.json
CHANGED
|
@@ -5453,11 +5453,12 @@
|
|
|
5453
5453
|
},
|
|
5454
5454
|
"planReview": {
|
|
5455
5455
|
"title": "Цей план чекає на вас",
|
|
5456
|
-
"body": "Планувальник підготував фази та
|
|
5456
|
+
"body": "Планувальник підготував ці фази та елементи. Затвердіть їх, щоб зафіксувати план і розпочати роботу, або поверніть план із зазначенням, що змінити.",
|
|
5457
5457
|
"approve": "Затвердити план",
|
|
5458
5458
|
"feedbackPlaceholder": "Що має змінити планувальник? Обсяг, порядок фаз, пропущена робота, елемент, якому місце деінде…",
|
|
5459
5459
|
"sendBack": "Повернути планувальнику",
|
|
5460
|
-
"noDocument": "Цей
|
|
5460
|
+
"noDocument": "Цей крок планування не залишив документа плану для перегляду, тож тут немає чого оглядати.",
|
|
5461
|
+
"noDocumentSections": "План становлять розділи нижче.",
|
|
5461
5462
|
"needsFeedback": "Спершу додайте коментар або відгук — планувальник переплановує на його основі.",
|
|
5462
5463
|
"commentHint": "Клацніть будь-яку частину плану, щоб залишити коментар."
|
|
5463
5464
|
},
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cat-factory/app",
|
|
3
|
-
"version": "0.195.
|
|
3
|
+
"version": "0.195.1",
|
|
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",
|