@cat-factory/app 0.276.0 → 0.277.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/app/components/binaryCandidates/BinaryCandidatesWindow.vue +13 -1
- package/app/components/binaryOutput/BinaryOutputReport.vue +15 -1
- package/app/components/binaryOutput/StoredAssetView.vue +134 -0
- package/app/components/board/AddTaskModal.vue +9 -4
- package/app/components/palettes/PipelinePurposeSelect.vue +1 -0
- package/app/components/settings/WorkspaceSettingsPanel.vue +2 -1
- package/app/composables/api/visualConfirm.ts +11 -2
- package/app/composables/useArtifactBlobs.spec.ts +76 -0
- package/app/composables/useArtifactBlobs.ts +25 -4
- package/app/utils/binaryCandidates.ts +19 -1
- package/app/utils/binaryOutput.ts +13 -0
- package/app/utils/catalog.ts +6 -0
- package/i18n/locales/de.json +13 -2
- package/i18n/locales/en.json +13 -2
- package/i18n/locales/es.json +13 -2
- package/i18n/locales/fr.json +13 -2
- package/i18n/locales/he.json +13 -2
- package/i18n/locales/it.json +13 -2
- package/i18n/locales/ja.json +13 -2
- package/i18n/locales/pl.json +13 -2
- package/i18n/locales/tr.json +13 -2
- package/i18n/locales/uk.json +13 -2
- package/package.json +2 -2
|
@@ -26,6 +26,7 @@ import {
|
|
|
26
26
|
binaryCandidateView,
|
|
27
27
|
} from '~/utils/binaryCandidates'
|
|
28
28
|
import ResultWindowShell from '~/components/panels/ResultWindowShell.vue'
|
|
29
|
+
import StoredAssetView from '~/components/binaryOutput/StoredAssetView.vue'
|
|
29
30
|
|
|
30
31
|
const execution = useExecutionStore()
|
|
31
32
|
const board = useBoardStore()
|
|
@@ -194,8 +195,19 @@ async function onKeep() {
|
|
|
194
195
|
data-testid="binary-candidate-card"
|
|
195
196
|
@click="toggle(row.id)"
|
|
196
197
|
>
|
|
198
|
+
<!-- Staged through the platform's OWN asset storage: we hold the bytes, so the card
|
|
199
|
+
renders them (and offers to open or save one) rather than waiting for a public
|
|
200
|
+
link the shipped storage never issues. Checked first because a candidate can
|
|
201
|
+
carry both, and ours is the one that is definitely still there. -->
|
|
202
|
+
<StoredAssetView
|
|
203
|
+
v-if="row.assetId"
|
|
204
|
+
:asset-id="row.assetId"
|
|
205
|
+
:content-type="row.contentType"
|
|
206
|
+
:label="row.label ?? row.subject"
|
|
207
|
+
class="mb-2"
|
|
208
|
+
/>
|
|
197
209
|
<img
|
|
198
|
-
v-if="row.previewUrl"
|
|
210
|
+
v-else-if="row.previewUrl"
|
|
199
211
|
:src="row.previewUrl"
|
|
200
212
|
:alt="row.label ?? row.id"
|
|
201
213
|
class="mb-2 max-h-56 w-full rounded object-contain"
|
|
@@ -16,6 +16,7 @@ import type { PipelineStep } from '~/types/execution'
|
|
|
16
16
|
import { BINARY_OUTPUT_STATE_KEYS, binaryOutputView } from '~/utils/binaryOutput'
|
|
17
17
|
import { binaryCandidateView } from '~/utils/binaryCandidates'
|
|
18
18
|
import CopyButton from '~/components/common/CopyButton.vue'
|
|
19
|
+
import StoredAssetView from '~/components/binaryOutput/StoredAssetView.vue'
|
|
19
20
|
|
|
20
21
|
// Two callers, one renderer — the same split `StepEffortReport` makes: the generic step-detail
|
|
21
22
|
// panel drops it in as a `card` (its own heading + border, among the other detail sections),
|
|
@@ -135,7 +136,13 @@ const state = computed(() => {
|
|
|
135
136
|
|
|
136
137
|
<!-- The artifacts. `location` is the service's OWN addressing — an object key, a path, a
|
|
137
138
|
URL — recorded verbatim and never interpreted, so it renders as copyable text and
|
|
138
|
-
never as a link.
|
|
139
|
+
never as a link.
|
|
140
|
+
The ONE exception is an artifact stored through the platform's own asset storage, where
|
|
141
|
+
the location is an id for bytes WE hold: there the row also shows the thing itself, and
|
|
142
|
+
offers to open or save it. That is not a reinterpretation of the location, it is the
|
|
143
|
+
join `binaryOutputView` already made (`row.assetId` is null unless the service is ours
|
|
144
|
+
AND the location is a well-formed id), and it is the difference between a report that
|
|
145
|
+
says where the work went and one that shows it. -->
|
|
139
146
|
<ul v-if="view.rows.length" class="space-y-1.5">
|
|
140
147
|
<li
|
|
141
148
|
v-for="(row, i) in view.rows"
|
|
@@ -144,6 +151,13 @@ const state = computed(() => {
|
|
|
144
151
|
data-testid="binary-output-artifact"
|
|
145
152
|
>
|
|
146
153
|
<CopyButton :text="row.location" class="absolute end-1 top-1" />
|
|
154
|
+
<StoredAssetView
|
|
155
|
+
v-if="row.assetId"
|
|
156
|
+
:asset-id="row.assetId"
|
|
157
|
+
:content-type="row.contentType"
|
|
158
|
+
:label="row.entity ?? row.description"
|
|
159
|
+
class="mb-2 pe-8"
|
|
160
|
+
/>
|
|
147
161
|
<code class="block break-all pe-8 font-mono text-[11px] text-slate-200">{{
|
|
148
162
|
row.location
|
|
149
163
|
}}</code>
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
// A stored artifact the PLATFORM holds the bytes for: what it looks like, and the two things a
|
|
3
|
+
// person wants to do with it afterwards.
|
|
4
|
+
//
|
|
5
|
+
// This is the half of the media flow that only works because the shipped asset storage is our
|
|
6
|
+
// own. An artifact in an org's private bucket is a location string and, at best, whatever preview
|
|
7
|
+
// link that service chose to issue; one stored here is bytes we can serve, so the run's report can
|
|
8
|
+
// show the picture, open it full size, and hand the file over to be saved elsewhere.
|
|
9
|
+
//
|
|
10
|
+
// The bytes are behind the workspace's authenticated blob route, so an `<img src>` cannot point at
|
|
11
|
+
// them: they are fetched as a blob and turned into an object URL (`useArtifactBlobs`, the same
|
|
12
|
+
// composable the visual-confirmation gate uses). That is also why the DOWNLOAD is an `<a download>`
|
|
13
|
+
// over the object URL rather than a link to the API: a plain link would open an authenticated
|
|
14
|
+
// request the browser has no session header for.
|
|
15
|
+
//
|
|
16
|
+
// Whether the artifact renders as a picture is `rendersInlineAsImage` over the media type the
|
|
17
|
+
// SERVER SERVED, not the one the producing agent declared. The declaration is optional and
|
|
18
|
+
// model-authored: a step that stores a PNG and says nothing about it would render as a generic
|
|
19
|
+
// file, and one that mislabels a bundle as an image would render a broken `<img>` reporting
|
|
20
|
+
// itself as loaded, with no error affordance because the fetch genuinely succeeded. The served
|
|
21
|
+
// type is the same judgement the server already made when it decided whether to answer inline or
|
|
22
|
+
// as an attachment, so a row and the response behind it can no longer disagree. The declared type
|
|
23
|
+
// stays as the LABEL on a non-image row, which is what it is good for: it is the agent's own
|
|
24
|
+
// account of the file, where the served type only ever says image-or-not.
|
|
25
|
+
//
|
|
26
|
+
// Everything that is not a picture (a 3D model, an audio file, a PDF) is a legitimate outcome and
|
|
27
|
+
// NOT a failure: it renders as a named file with the same two actions, because the point of the
|
|
28
|
+
// surface is the file, not the thumbnail.
|
|
29
|
+
import { computed, onUnmounted, watch } from 'vue'
|
|
30
|
+
import { rendersInlineAsImage } from '@cat-factory/contracts'
|
|
31
|
+
import { useArtifactBlobs } from '~/composables/useArtifactBlobs'
|
|
32
|
+
|
|
33
|
+
const props = withDefaults(
|
|
34
|
+
defineProps<{
|
|
35
|
+
/** The platform artifact id (`art_…`) resolved off the row's service + location. */
|
|
36
|
+
assetId: string
|
|
37
|
+
/** The media type the agent reported. Shown as the label on a non-image row; see above. */
|
|
38
|
+
contentType?: string | undefined
|
|
39
|
+
/** What to call the file a person saves, and the image's alt text. */
|
|
40
|
+
label?: string | undefined
|
|
41
|
+
/** Cap on the rendered preview's height, so a report row and a comparison card can differ. */
|
|
42
|
+
previewClass?: string
|
|
43
|
+
}>(),
|
|
44
|
+
{ contentType: undefined, label: undefined, previewClass: 'max-h-56' },
|
|
45
|
+
)
|
|
46
|
+
const { t } = useI18n()
|
|
47
|
+
|
|
48
|
+
const blobs = useArtifactBlobs()
|
|
49
|
+
onUnmounted(() => blobs.revokeAll())
|
|
50
|
+
|
|
51
|
+
// Re-resolve when the id changes rather than only on mount: one of these components is reused
|
|
52
|
+
// across the rows of a report that grows over a live run.
|
|
53
|
+
watch(
|
|
54
|
+
() => props.assetId,
|
|
55
|
+
(id) => void blobs.resolve(id),
|
|
56
|
+
{ immediate: true },
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
const url = computed(() => blobs.urlFor(props.assetId))
|
|
60
|
+
const status = computed(() => blobs.statusFor(props.assetId))
|
|
61
|
+
const isImage = computed(() => rendersInlineAsImage(blobs.typeFor(props.assetId)))
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* The name the saved file takes. Derived from the agent's own label where there is one, because
|
|
65
|
+
* `art_01H…` tells the person who saves it nothing about what it is; sanitised to the characters
|
|
66
|
+
* a filename may safely carry, since the label is model-authored text going into a download.
|
|
67
|
+
*/
|
|
68
|
+
const filename = computed(() => {
|
|
69
|
+
const base = (props.label ?? '')
|
|
70
|
+
.trim()
|
|
71
|
+
.replace(/[^A-Za-z0-9._-]+/g, '-')
|
|
72
|
+
.replace(/^-+|-+$/g, '')
|
|
73
|
+
return base ? base.slice(0, 80) : props.assetId
|
|
74
|
+
})
|
|
75
|
+
</script>
|
|
76
|
+
|
|
77
|
+
<template>
|
|
78
|
+
<div class="space-y-1.5" data-testid="stored-asset">
|
|
79
|
+
<img
|
|
80
|
+
v-if="isImage && url"
|
|
81
|
+
:src="url"
|
|
82
|
+
:alt="label ?? assetId"
|
|
83
|
+
class="w-full rounded object-contain"
|
|
84
|
+
:class="previewClass"
|
|
85
|
+
data-testid="stored-asset-preview"
|
|
86
|
+
/>
|
|
87
|
+
<!-- Not a picture, and that is an ordinary outcome for a media step: a mesh, a sound and a
|
|
88
|
+
PDF are all deliverables, so the row states what it is rather than showing a broken
|
|
89
|
+
frame. -->
|
|
90
|
+
<p
|
|
91
|
+
v-else-if="!isImage && url"
|
|
92
|
+
class="flex items-center gap-1.5 rounded bg-slate-800/60 px-2 py-1.5 text-[11px] text-slate-300"
|
|
93
|
+
data-testid="stored-asset-file"
|
|
94
|
+
>
|
|
95
|
+
<UIcon name="i-lucide-file" class="h-3.5 w-3.5 shrink-0" />
|
|
96
|
+
<span class="truncate">{{ contentType ?? t('binaryOutput.asset.unknownType') }}</span>
|
|
97
|
+
</p>
|
|
98
|
+
<p
|
|
99
|
+
v-else-if="status === 'error'"
|
|
100
|
+
class="flex items-center gap-1.5 text-[11px] text-amber-300"
|
|
101
|
+
data-testid="stored-asset-error"
|
|
102
|
+
>
|
|
103
|
+
<span>{{ t('binaryOutput.asset.loadFailed') }}</span>
|
|
104
|
+
<UButton size="xs" variant="link" @click.stop="blobs.retry(assetId)">
|
|
105
|
+
{{ t('binaryOutput.asset.retry') }}
|
|
106
|
+
</UButton>
|
|
107
|
+
</p>
|
|
108
|
+
<p v-else class="text-[11px] text-slate-500" data-testid="stored-asset-loading">
|
|
109
|
+
{{ t('binaryOutput.asset.loading') }}
|
|
110
|
+
</p>
|
|
111
|
+
|
|
112
|
+
<!-- The two things a person does with a delivered asset. Both hang off the object URL, so
|
|
113
|
+
they appear only once the bytes are in hand. -->
|
|
114
|
+
<div v-if="url" class="flex items-center gap-3 text-[11px]">
|
|
115
|
+
<a
|
|
116
|
+
:href="url"
|
|
117
|
+
target="_blank"
|
|
118
|
+
rel="noopener"
|
|
119
|
+
class="text-sky-300 hover:underline"
|
|
120
|
+
data-testid="stored-asset-open"
|
|
121
|
+
@click.stop
|
|
122
|
+
>{{ t('binaryOutput.asset.open') }}</a
|
|
123
|
+
>
|
|
124
|
+
<a
|
|
125
|
+
:href="url"
|
|
126
|
+
:download="filename"
|
|
127
|
+
class="text-sky-300 hover:underline"
|
|
128
|
+
data-testid="stored-asset-download"
|
|
129
|
+
@click.stop
|
|
130
|
+
>{{ t('binaryOutput.asset.download') }}</a
|
|
131
|
+
>
|
|
132
|
+
</div>
|
|
133
|
+
</div>
|
|
134
|
+
</template>
|
|
@@ -104,6 +104,7 @@ const TASK_TYPES = computed<{ value: TaskTypeChoice; label: string; icon: string
|
|
|
104
104
|
icon: 'i-lucide-clipboard-check',
|
|
105
105
|
},
|
|
106
106
|
{ value: 'ralph', label: t('board.addTask.types.ralph'), icon: 'i-lucide-infinity' },
|
|
107
|
+
{ value: 'media', label: t('board.addTask.types.media'), icon: 'i-lucide-image-plus' },
|
|
107
108
|
{ value: 'recurring', label: t('board.addTask.types.recurring'), icon: 'i-lucide-repeat' },
|
|
108
109
|
]
|
|
109
110
|
// A document repository only accepts document/spike tasks (see BoardService.addTask).
|
|
@@ -400,16 +401,20 @@ const selectablePipelines = computed(() =>
|
|
|
400
401
|
// must appear in the form BEFORE creation:
|
|
401
402
|
// - `ralph` needs its preset so the per-task validation command + iteration budget the `ralph`
|
|
402
403
|
// agent contributes surface for editing ("choose at run time" would be a dead end);
|
|
403
|
-
// - a `document` task defaults to `pl_document
|
|
404
|
-
// purpose-narrowed picker (the `purpose` gate hides every
|
|
405
|
-
// is never rendered empty.
|
|
404
|
+
// - a `document` task defaults to `pl_document`, a `review` task to `pl_review` and a `media`
|
|
405
|
+
// task to `pl_media` so their purpose-narrowed picker (the `purpose` gate hides every pipeline
|
|
406
|
+
// of another purpose) is never rendered empty.
|
|
406
407
|
// The other typed default (spike) carries no up-front config and doesn't narrow its picker, so the
|
|
407
408
|
// modal leaves `pipelineId` unset and `BoardService` applies the backend type-default at creation.
|
|
408
|
-
// Keep these ids in step with the backend helper.
|
|
409
|
+
// Keep these ids in step with the backend helper. The test for membership is the NARROWING, not the
|
|
410
|
+
// config: a type whose picker `pipelineAllowedForTaskType` reduces to one purpose has to name its
|
|
411
|
+
// default here, because every fallback below is a BUILD-purpose pipeline the narrowed picker then
|
|
412
|
+
// rejects, leaving the form open on an empty selection.
|
|
409
413
|
const DEFAULT_PIPELINE_FOR_TYPE: Partial<Record<TaskTypeChoice, string>> = {
|
|
410
414
|
ralph: 'pl_ralph',
|
|
411
415
|
document: 'pl_document',
|
|
412
416
|
review: 'pl_review',
|
|
417
|
+
media: 'pl_media',
|
|
413
418
|
}
|
|
414
419
|
/**
|
|
415
420
|
* The pipeline a task type opens with: a custom type's registered `defaultPipelineId`, else the
|
|
@@ -30,6 +30,7 @@ const PURPOSE_LABELS = computed<Record<PipelinePurpose, string>>(() => ({
|
|
|
30
30
|
review: t('pipeline.builder.purposeOption.review'),
|
|
31
31
|
research: t('pipeline.builder.purposeOption.research'),
|
|
32
32
|
planning: t('pipeline.builder.purposeOption.planning'),
|
|
33
|
+
media: t('pipeline.builder.purposeOption.media'),
|
|
33
34
|
}))
|
|
34
35
|
|
|
35
36
|
// The button text. A stored purpose this build has no label for is NAMED as unrecognised and
|
|
@@ -148,7 +148,7 @@ const tabsUi = {
|
|
|
148
148
|
// so the config surface pins the built-in set it renders inputs for — a custom type buckets on its
|
|
149
149
|
// own id server-side (`RunAdmission`) and is not configured here. Keying the records below off this
|
|
150
150
|
// finite union (not the open `CreateTaskType`) keeps them exhaustive + undefined-free.
|
|
151
|
-
type LimitTaskType = 'feature' | 'bug' | 'document' | 'spike' | 'review' | 'ralph'
|
|
151
|
+
type LimitTaskType = 'feature' | 'bug' | 'document' | 'spike' | 'review' | 'ralph' | 'media'
|
|
152
152
|
const TASK_TYPES: LimitTaskType[] = ['feature', 'bug', 'document', 'spike']
|
|
153
153
|
|
|
154
154
|
// Per-task-type label for the "Max {type} tasks" inputs. An exhaustive Record keyed off
|
|
@@ -161,6 +161,7 @@ const TASK_TYPE_KEYS: Record<LimitTaskType, string> = {
|
|
|
161
161
|
spike: 'settings.workspaceSettings.taskTypes.spike',
|
|
162
162
|
review: 'settings.workspaceSettings.taskTypes.review',
|
|
163
163
|
ralph: 'settings.workspaceSettings.taskTypes.ralph',
|
|
164
|
+
media: 'settings.workspaceSettings.taskTypes.media',
|
|
164
165
|
}
|
|
165
166
|
|
|
166
167
|
const MODES = computed<{ value: TaskLimitMode; label: string }[]>(() => [
|
|
@@ -49,12 +49,21 @@ export function visualConfirmApi({ send, ws, http }: ApiContext) {
|
|
|
49
49
|
},
|
|
50
50
|
|
|
51
51
|
// Fetch a stored artifact's bytes and turn them into an object URL for an <img>.
|
|
52
|
-
|
|
52
|
+
//
|
|
53
|
+
// The blob's own `type` comes back beside the URL because it is the media type the SERVER
|
|
54
|
+
// decided to serve these bytes as, which is the only one a caller may render from. What the
|
|
55
|
+
// producing agent declared is a claim about a file; this is what the response actually is,
|
|
56
|
+
// after `blobResponseHeaders` has clamped anything outside the inline-image list down to
|
|
57
|
+
// `application/octet-stream`.
|
|
58
|
+
fetchArtifactBlob: async (
|
|
59
|
+
workspaceId: string,
|
|
60
|
+
artifactId: string,
|
|
61
|
+
): Promise<{ url: string; contentType: string }> => {
|
|
53
62
|
const blob: Blob = await http(
|
|
54
63
|
`${ws(workspaceId)}/artifacts/${encodeURIComponent(artifactId)}/blob`,
|
|
55
64
|
{ method: 'GET', responseType: 'blob' },
|
|
56
65
|
)
|
|
57
|
-
return URL.createObjectURL(blob)
|
|
66
|
+
return { url: URL.createObjectURL(blob), contentType: blob.type }
|
|
58
67
|
},
|
|
59
68
|
}
|
|
60
69
|
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest'
|
|
2
|
+
import { useArtifactBlobs } from '~/composables/useArtifactBlobs'
|
|
3
|
+
import { useWorkspaceStore } from '~/stores/workspace'
|
|
4
|
+
|
|
5
|
+
// What a resolved artifact reports about ITSELF, and why the answer is not the declaration that
|
|
6
|
+
// came with it.
|
|
7
|
+
//
|
|
8
|
+
// A stored asset carries two media types: the one the producing agent declared (optional,
|
|
9
|
+
// model-authored, a claim about a file) and the one the server SERVES the bytes as, after
|
|
10
|
+
// `blobResponseHeaders` has clamped anything outside the inline-image list down to
|
|
11
|
+
// `application/octet-stream`. Only the second is a fact about the response, so it is the one a
|
|
12
|
+
// surface may decide picture-versus-file from. Deciding from the declaration is wrong in both
|
|
13
|
+
// directions: an undeclared PNG renders as a generic file, and a mis-declared bundle renders as a
|
|
14
|
+
// broken `<img>` that reports itself as loaded, because the fetch genuinely succeeded.
|
|
15
|
+
|
|
16
|
+
function stubApi(contentType: string): { calls: number } {
|
|
17
|
+
const state = { calls: 0 }
|
|
18
|
+
vi.stubGlobal('useApi', () => ({
|
|
19
|
+
fetchArtifactBlob: async (_ws: string, id: string) => {
|
|
20
|
+
state.calls += 1
|
|
21
|
+
return { url: `blob:${id}`, contentType }
|
|
22
|
+
},
|
|
23
|
+
}))
|
|
24
|
+
return state
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function blobs(contentType: string) {
|
|
28
|
+
const calls = stubApi(contentType)
|
|
29
|
+
useWorkspaceStore().workspaceId = 'ws_1'
|
|
30
|
+
return { blobs: useArtifactBlobs(), calls }
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
describe('useArtifactBlobs', () => {
|
|
34
|
+
it('records the media type the server served, beside the URL', async () => {
|
|
35
|
+
const { blobs: cache } = blobs('image/png')
|
|
36
|
+
await cache.resolve('art_1')
|
|
37
|
+
expect(cache.urlFor('art_1')).toBe('blob:art_1')
|
|
38
|
+
expect(cache.typeFor('art_1')).toBe('image/png')
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
it('reports the clamped type for bytes the server refused to serve inline', async () => {
|
|
42
|
+
// A GLB, a zip, a PDF, or an image type the allow-list does not carry: the response is an
|
|
43
|
+
// attachment, and a row that pointed an `<img>` at it would render a broken frame with the
|
|
44
|
+
// load reported as successful.
|
|
45
|
+
const { blobs: cache } = blobs('application/octet-stream')
|
|
46
|
+
await cache.resolve('art_1')
|
|
47
|
+
expect(cache.typeFor('art_1')).toBe('application/octet-stream')
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
it('knows nothing about a type until the bytes are in hand', async () => {
|
|
51
|
+
// The loading state has no answer to give, which is what keeps a surface from committing to
|
|
52
|
+
// picture-or-file before the response says which.
|
|
53
|
+
const { blobs: cache } = blobs('image/png')
|
|
54
|
+
expect(cache.typeFor('art_1')).toBeUndefined()
|
|
55
|
+
await cache.resolve('art_1')
|
|
56
|
+
expect(cache.typeFor('art_1')).toBe('image/png')
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
it('drops the recorded type on a retry, so a re-fetch cannot be read through the old one', async () => {
|
|
60
|
+
const { blobs: cache } = blobs('image/png')
|
|
61
|
+
await cache.resolve('art_1')
|
|
62
|
+
const pending = cache.retry('art_1')
|
|
63
|
+
expect(cache.typeFor('art_1')).toBeUndefined()
|
|
64
|
+
await pending
|
|
65
|
+
expect(cache.typeFor('art_1')).toBe('image/png')
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
it('forgets every type when the owning component unmounts', async () => {
|
|
69
|
+
// The cache is per component and `revokeAll` is what releases the bytes; a type left behind
|
|
70
|
+
// would outlive the URL it describes.
|
|
71
|
+
const { blobs: cache } = blobs('image/png')
|
|
72
|
+
await cache.resolve('art_1')
|
|
73
|
+
cache.revokeAll()
|
|
74
|
+
expect(cache.typeFor('art_1')).toBeUndefined()
|
|
75
|
+
})
|
|
76
|
+
})
|
|
@@ -23,6 +23,17 @@ export function useArtifactBlobs() {
|
|
|
23
23
|
|
|
24
24
|
/** artifactId → object URL (reactive so templates re-render when a blob resolves). */
|
|
25
25
|
const urls = reactive<Record<string, string>>({})
|
|
26
|
+
/**
|
|
27
|
+
* artifactId → the media type the SERVER served these bytes as.
|
|
28
|
+
*
|
|
29
|
+
* The one a surface may decide from. A stored artifact also carries a DECLARED content type,
|
|
30
|
+
* which for an asset is the agent's own claim about the file it uploaded and is optional; the
|
|
31
|
+
* serve path meanwhile clamps anything outside the inline-image list to
|
|
32
|
+
* `application/octet-stream`. So a row deciding picture-versus-file from the declaration gets it
|
|
33
|
+
* wrong in both directions: an undeclared PNG renders as a generic file, and a mis-declared
|
|
34
|
+
* bundle renders as a broken `<img>` with the load reported as successful.
|
|
35
|
+
*/
|
|
36
|
+
const types = reactive<Record<string, string>>({})
|
|
26
37
|
/** artifactId → fetch status, drives loading / error / retry affordances. */
|
|
27
38
|
const status = reactive<Record<string, ArtifactBlobStatus>>({})
|
|
28
39
|
/** In-flight promises, so concurrent `resolve(id)` calls share one fetch + one blob. */
|
|
@@ -43,6 +54,11 @@ export function useArtifactBlobs() {
|
|
|
43
54
|
return id ? (status[id] ?? 'idle') : 'idle'
|
|
44
55
|
}
|
|
45
56
|
|
|
57
|
+
/** The media type the server served this artifact as, once its bytes are in hand. */
|
|
58
|
+
function typeFor(id: string | null | undefined): string | undefined {
|
|
59
|
+
return id ? types[id] : undefined
|
|
60
|
+
}
|
|
61
|
+
|
|
46
62
|
/** Resolve an artifact to an object URL (cached + deduped). Returns null on failure. */
|
|
47
63
|
function resolve(id: string | null | undefined): Promise<string | null> {
|
|
48
64
|
if (!id || disposed) return Promise.resolve(null)
|
|
@@ -53,19 +69,22 @@ export function useArtifactBlobs() {
|
|
|
53
69
|
|
|
54
70
|
status[id] = 'loading'
|
|
55
71
|
const p = api
|
|
56
|
-
.
|
|
57
|
-
.then((url) => {
|
|
72
|
+
.fetchArtifactBlob(ws.requireId(), id)
|
|
73
|
+
.then(({ url, contentType }) => {
|
|
58
74
|
// The owner unmounted while this was in flight: revoke the freshly-minted URL
|
|
59
75
|
// instead of stranding it in the cleared cache.
|
|
60
76
|
if (disposed) {
|
|
61
77
|
try {
|
|
62
78
|
URL.revokeObjectURL(url)
|
|
63
79
|
} catch {
|
|
64
|
-
// Already revoked / unsupported environment
|
|
80
|
+
// Already revoked / unsupported environment, nothing to do.
|
|
65
81
|
}
|
|
66
82
|
return null
|
|
67
83
|
}
|
|
68
84
|
urls[id] = url
|
|
85
|
+
// Written BEFORE the status flips to `ready`, so a watcher reacting to the status never
|
|
86
|
+
// reads a resolved artifact whose served type has not landed yet.
|
|
87
|
+
if (contentType) types[id] = contentType
|
|
69
88
|
status[id] = 'ready'
|
|
70
89
|
return url
|
|
71
90
|
})
|
|
@@ -91,6 +110,7 @@ export function useArtifactBlobs() {
|
|
|
91
110
|
}
|
|
92
111
|
}
|
|
93
112
|
delete urls[id]
|
|
113
|
+
delete types[id]
|
|
94
114
|
status[id] = 'idle'
|
|
95
115
|
inFlight.delete(id)
|
|
96
116
|
return resolve(id)
|
|
@@ -110,11 +130,12 @@ export function useArtifactBlobs() {
|
|
|
110
130
|
}
|
|
111
131
|
}
|
|
112
132
|
for (const k of Object.keys(urls)) delete urls[k]
|
|
133
|
+
for (const k of Object.keys(types)) delete types[k]
|
|
113
134
|
for (const k of Object.keys(status)) delete status[k]
|
|
114
135
|
inFlight.clear()
|
|
115
136
|
}
|
|
116
137
|
|
|
117
|
-
return { urls, status, urlFor, statusFor, resolve, retry, revokeAll }
|
|
138
|
+
return { urls, types, status, urlFor, statusFor, typeFor, resolve, retry, revokeAll }
|
|
118
139
|
}
|
|
119
140
|
|
|
120
141
|
export type ArtifactBlobs = ReturnType<typeof useArtifactBlobs>
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { platformAssetIdOf } from '@cat-factory/contracts'
|
|
1
2
|
import type { BinaryCandidate, BinaryCandidateStepState, PipelineStep } from '~/types/execution'
|
|
2
3
|
|
|
3
4
|
// ---------------------------------------------------------------------------
|
|
@@ -23,6 +24,17 @@ export interface BinaryCandidateRow extends BinaryCandidate {
|
|
|
23
24
|
kept: boolean
|
|
24
25
|
/** The id it is to be stored under, when the person who kept it assigned one. */
|
|
25
26
|
storeAs?: string
|
|
27
|
+
/**
|
|
28
|
+
* The platform's own artifact id, when this candidate was staged through the platform's asset
|
|
29
|
+
* storage rather than an org's own service.
|
|
30
|
+
*
|
|
31
|
+
* It is the OTHER way a candidate gets a preview, and the one that works on a private estate:
|
|
32
|
+
* `previewUrl` needs the storage service to have issued a public link, which ours never does
|
|
33
|
+
* (its bytes are behind the workspace's own authenticated blob route). Without this a
|
|
34
|
+
* deployment using the shipped storage would compare candidates it could not see, which is the
|
|
35
|
+
* one thing this window exists to make possible.
|
|
36
|
+
*/
|
|
37
|
+
assetId: string | null
|
|
26
38
|
}
|
|
27
39
|
|
|
28
40
|
/** The candidates for one subject, which is the unit a person compares. */
|
|
@@ -81,6 +93,7 @@ export function binaryCandidateView(
|
|
|
81
93
|
const choice = kept.get(candidate.id)
|
|
82
94
|
group.rows.push({
|
|
83
95
|
...candidate,
|
|
96
|
+
assetId: platformAssetIdOf(candidate),
|
|
84
97
|
kept: choice !== undefined,
|
|
85
98
|
...(choice?.storeAs ? { storeAs: choice.storeAs } : {}),
|
|
86
99
|
})
|
|
@@ -91,7 +104,12 @@ export function binaryCandidateView(
|
|
|
91
104
|
awaiting: state.status === 'awaiting_choice',
|
|
92
105
|
multiSelect: state.multiSelect === true,
|
|
93
106
|
automatic: state.choice?.automatic === true,
|
|
94
|
-
|
|
107
|
+
// Counts a candidate with NEITHER kind of preview: a service-issued link, or bytes the
|
|
108
|
+
// platform holds itself. Reading only `previewUrl` here would report every candidate of every
|
|
109
|
+
// run on the shipped storage as unviewable while the window was rendering all of them.
|
|
110
|
+
withoutPreview: state.candidates.filter(
|
|
111
|
+
(candidate) => !candidate.previewUrl && !platformAssetIdOf(candidate),
|
|
112
|
+
).length,
|
|
95
113
|
}
|
|
96
114
|
}
|
|
97
115
|
|
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
isHarnessTransport,
|
|
10
10
|
modalityCarriesPixelDimensions,
|
|
11
11
|
normalizeMediaType,
|
|
12
|
+
platformAssetIdOf,
|
|
12
13
|
requiredBinaryCapabilities,
|
|
13
14
|
} from '@cat-factory/contracts'
|
|
14
15
|
import type {
|
|
@@ -103,6 +104,17 @@ export interface BinaryOutputRow extends BinaryOutputArtifact {
|
|
|
103
104
|
* instead of letting an absent measurement read as a passing one.
|
|
104
105
|
*/
|
|
105
106
|
missized: boolean
|
|
107
|
+
/**
|
|
108
|
+
* The platform's own artifact id, when THIS deployment is what holds the bytes: the row's
|
|
109
|
+
* service is the platform asset store and its `location` is a well-formed artifact id.
|
|
110
|
+
*
|
|
111
|
+
* It is what turns a location string into a picture the reader can look at and a file they can
|
|
112
|
+
* save, which is the whole difference between an asset in our storage and one in an org's
|
|
113
|
+
* private bucket. Null covers BOTH "stored elsewhere" and "stored here, but the location is not
|
|
114
|
+
* an id". A model's location is prose, and a paraphrased one costs the row its preview and
|
|
115
|
+
* never its record.
|
|
116
|
+
*/
|
|
117
|
+
assetId: string | null
|
|
106
118
|
}
|
|
107
119
|
|
|
108
120
|
/**
|
|
@@ -301,6 +313,7 @@ export function binaryOutputView(step: PipelineStep | null | undefined): BinaryO
|
|
|
301
313
|
unknown: unknown.has(artifact.service),
|
|
302
314
|
// An UNATTRIBUTED row (no `generator` claimed) is not unknown — see the field's own note.
|
|
303
315
|
generatorUnknown: artifact.generator !== undefined && unknownGenerators.has(artifact.generator),
|
|
316
|
+
assetId: platformAssetIdOf(artifact),
|
|
304
317
|
// An UNMEASURED row is not missized either: absent dimensions are counted by
|
|
305
318
|
// `sizeUnreported`, never folded in here.
|
|
306
319
|
missized:
|
package/app/utils/catalog.ts
CHANGED
|
@@ -959,6 +959,12 @@ export const TASK_TYPE_META: Record<string, TaskTypeMeta> = {
|
|
|
959
959
|
color: '#a78bfa',
|
|
960
960
|
labelKey: 'board.addTask.types.ralph',
|
|
961
961
|
},
|
|
962
|
+
media: {
|
|
963
|
+
taskType: 'media',
|
|
964
|
+
icon: 'i-lucide-image-plus',
|
|
965
|
+
color: '#f472b6',
|
|
966
|
+
labelKey: 'board.addTask.types.media',
|
|
967
|
+
},
|
|
962
968
|
recurring: {
|
|
963
969
|
taskType: 'recurring',
|
|
964
970
|
icon: 'i-lucide-repeat',
|
package/i18n/locales/de.json
CHANGED
|
@@ -1077,7 +1077,8 @@
|
|
|
1077
1077
|
"document": "Dokument",
|
|
1078
1078
|
"spike": "Spike",
|
|
1079
1079
|
"review": "Review",
|
|
1080
|
-
"ralph": "Ralph-Schleife"
|
|
1080
|
+
"ralph": "Ralph-Schleife",
|
|
1081
|
+
"media": "Medien"
|
|
1081
1082
|
},
|
|
1082
1083
|
"reviewFriction": {
|
|
1083
1084
|
"heading": "Review-Rückstau-Bremse",
|
|
@@ -2982,6 +2983,7 @@
|
|
|
2982
2983
|
"bug": "Bug",
|
|
2983
2984
|
"document": "Dokument",
|
|
2984
2985
|
"spike": "Spike",
|
|
2986
|
+
"media": "Medien",
|
|
2985
2987
|
"recurring": "Wiederkehrend",
|
|
2986
2988
|
"review": "Review",
|
|
2987
2989
|
"ralph": "Ralph-Schleife"
|
|
@@ -4498,7 +4500,8 @@
|
|
|
4498
4500
|
"document": "Dokumentation",
|
|
4499
4501
|
"review": "Review",
|
|
4500
4502
|
"research": "Recherche",
|
|
4501
|
-
"planning": "Planung"
|
|
4503
|
+
"planning": "Planung",
|
|
4504
|
+
"media": "Medien"
|
|
4502
4505
|
},
|
|
4503
4506
|
"strategyOption": {
|
|
4504
4507
|
"specialist-panel": "Spezialisten-Panel",
|
|
@@ -5396,6 +5399,14 @@
|
|
|
5396
5399
|
"automatic": "{total} Kandidat wurde erzeugt und ohne Prüfung automatisch behalten.",
|
|
5397
5400
|
"chosen": "{kept} von {total} erzeugten Kandidaten wurden behalten.",
|
|
5398
5401
|
"awaiting": "{total} Kandidaten wurden erzeugt und warten auf den Vergleich."
|
|
5402
|
+
},
|
|
5403
|
+
"asset": {
|
|
5404
|
+
"loading": "Datei wird geladen …",
|
|
5405
|
+
"loadFailed": "Die gespeicherte Datei konnte nicht geladen werden.",
|
|
5406
|
+
"retry": "Erneut versuchen",
|
|
5407
|
+
"open": "Öffnen",
|
|
5408
|
+
"download": "Kopie speichern",
|
|
5409
|
+
"unknownType": "Gespeicherte Datei"
|
|
5399
5410
|
}
|
|
5400
5411
|
},
|
|
5401
5412
|
"brainstorm": {
|
package/i18n/locales/en.json
CHANGED
|
@@ -256,6 +256,7 @@
|
|
|
256
256
|
"bug": "Bug",
|
|
257
257
|
"document": "Document",
|
|
258
258
|
"spike": "Spike",
|
|
259
|
+
"media": "Media",
|
|
259
260
|
"recurring": "Recurring",
|
|
260
261
|
"review": "Review",
|
|
261
262
|
"ralph": "Ralph loop"
|
|
@@ -3929,7 +3930,8 @@
|
|
|
3929
3930
|
"document": "document",
|
|
3930
3931
|
"spike": "spike",
|
|
3931
3932
|
"review": "review",
|
|
3932
|
-
"ralph": "Ralph loop"
|
|
3933
|
+
"ralph": "Ralph loop",
|
|
3934
|
+
"media": "media"
|
|
3933
3935
|
},
|
|
3934
3936
|
"reviewFriction": {
|
|
3935
3937
|
"heading": "Review-debt friction",
|
|
@@ -5132,7 +5134,8 @@
|
|
|
5132
5134
|
"document": "Documentation",
|
|
5133
5135
|
"review": "Review",
|
|
5134
5136
|
"research": "Research",
|
|
5135
|
-
"planning": "Planning"
|
|
5137
|
+
"planning": "Planning",
|
|
5138
|
+
"media": "Media"
|
|
5136
5139
|
},
|
|
5137
5140
|
"strategyOption": {
|
|
5138
5141
|
"specialist-panel": "Specialist panel",
|
|
@@ -6999,6 +7002,14 @@
|
|
|
6999
7002
|
"automatic": "{total} candidate was generated and kept automatically, without review.",
|
|
7000
7003
|
"chosen": "{kept} of {total} generated candidates were kept.",
|
|
7001
7004
|
"awaiting": "{total} candidates were generated and are waiting to be compared."
|
|
7005
|
+
},
|
|
7006
|
+
"asset": {
|
|
7007
|
+
"loading": "Loading the file…",
|
|
7008
|
+
"loadFailed": "The stored file could not be loaded.",
|
|
7009
|
+
"retry": "Try again",
|
|
7010
|
+
"open": "Open",
|
|
7011
|
+
"download": "Save a copy",
|
|
7012
|
+
"unknownType": "Stored file"
|
|
7002
7013
|
}
|
|
7003
7014
|
},
|
|
7004
7015
|
"sandbox": {
|
package/i18n/locales/es.json
CHANGED
|
@@ -220,6 +220,7 @@
|
|
|
220
220
|
"bug": "Error",
|
|
221
221
|
"document": "Documento",
|
|
222
222
|
"spike": "Spike",
|
|
223
|
+
"media": "Medios",
|
|
223
224
|
"recurring": "Recurrente",
|
|
224
225
|
"review": "Revisión",
|
|
225
226
|
"ralph": "Bucle Ralph"
|
|
@@ -3644,7 +3645,8 @@
|
|
|
3644
3645
|
"document": "documento",
|
|
3645
3646
|
"spike": "spike",
|
|
3646
3647
|
"review": "revisión",
|
|
3647
|
-
"ralph": "Bucle Ralph"
|
|
3648
|
+
"ralph": "Bucle Ralph",
|
|
3649
|
+
"media": "medios"
|
|
3648
3650
|
},
|
|
3649
3651
|
"reviewFriction": {
|
|
3650
3652
|
"heading": "Fricción por revisión pendiente",
|
|
@@ -4962,7 +4964,8 @@
|
|
|
4962
4964
|
"document": "Documentación",
|
|
4963
4965
|
"review": "Revisión",
|
|
4964
4966
|
"research": "Investigación",
|
|
4965
|
-
"planning": "Planificación"
|
|
4967
|
+
"planning": "Planificación",
|
|
4968
|
+
"media": "Medios"
|
|
4966
4969
|
},
|
|
4967
4970
|
"strategyOption": {
|
|
4968
4971
|
"specialist-panel": "Panel de especialistas",
|
|
@@ -6688,6 +6691,14 @@
|
|
|
6688
6691
|
"automatic": "Se generó {total} candidato y se conservó automáticamente, sin revisión.",
|
|
6689
6692
|
"chosen": "Se conservaron {kept} de {total} candidatos generados.",
|
|
6690
6693
|
"awaiting": "Se generaron {total} candidatos y están a la espera de compararse."
|
|
6694
|
+
},
|
|
6695
|
+
"asset": {
|
|
6696
|
+
"loading": "Cargando el archivo…",
|
|
6697
|
+
"loadFailed": "No se pudo cargar el archivo almacenado.",
|
|
6698
|
+
"retry": "Reintentar",
|
|
6699
|
+
"open": "Abrir",
|
|
6700
|
+
"download": "Guardar una copia",
|
|
6701
|
+
"unknownType": "Archivo almacenado"
|
|
6691
6702
|
}
|
|
6692
6703
|
},
|
|
6693
6704
|
"sandbox": {
|
package/i18n/locales/fr.json
CHANGED
|
@@ -220,6 +220,7 @@
|
|
|
220
220
|
"bug": "Bug",
|
|
221
221
|
"document": "Document",
|
|
222
222
|
"spike": "Spike",
|
|
223
|
+
"media": "Média",
|
|
223
224
|
"recurring": "Récurrent",
|
|
224
225
|
"review": "Revue",
|
|
225
226
|
"ralph": "Boucle Ralph"
|
|
@@ -3644,7 +3645,8 @@
|
|
|
3644
3645
|
"document": "document",
|
|
3645
3646
|
"spike": "spike",
|
|
3646
3647
|
"review": "revue",
|
|
3647
|
-
"ralph": "Boucle Ralph"
|
|
3648
|
+
"ralph": "Boucle Ralph",
|
|
3649
|
+
"media": "média"
|
|
3648
3650
|
},
|
|
3649
3651
|
"reviewFriction": {
|
|
3650
3652
|
"heading": "Friction de dette de revue",
|
|
@@ -4962,7 +4964,8 @@
|
|
|
4962
4964
|
"document": "Documentation",
|
|
4963
4965
|
"review": "Revue",
|
|
4964
4966
|
"research": "Recherche",
|
|
4965
|
-
"planning": "Planification"
|
|
4967
|
+
"planning": "Planification",
|
|
4968
|
+
"media": "Média"
|
|
4966
4969
|
},
|
|
4967
4970
|
"strategyOption": {
|
|
4968
4971
|
"specialist-panel": "Panel de spécialistes",
|
|
@@ -6688,6 +6691,14 @@
|
|
|
6688
6691
|
"automatic": "{total} candidat a été généré et conservé automatiquement, sans examen.",
|
|
6689
6692
|
"chosen": "{kept} des {total} candidats générés ont été conservés.",
|
|
6690
6693
|
"awaiting": "{total} candidats ont été générés et attendent d’être comparés."
|
|
6694
|
+
},
|
|
6695
|
+
"asset": {
|
|
6696
|
+
"loading": "Chargement du fichier…",
|
|
6697
|
+
"loadFailed": "Le fichier stocké n’a pas pu être chargé.",
|
|
6698
|
+
"retry": "Réessayer",
|
|
6699
|
+
"open": "Ouvrir",
|
|
6700
|
+
"download": "Enregistrer une copie",
|
|
6701
|
+
"unknownType": "Fichier stocké"
|
|
6691
6702
|
}
|
|
6692
6703
|
},
|
|
6693
6704
|
"sandbox": {
|
package/i18n/locales/he.json
CHANGED
|
@@ -220,6 +220,7 @@
|
|
|
220
220
|
"bug": "באג",
|
|
221
221
|
"document": "מסמך",
|
|
222
222
|
"spike": "ספייק",
|
|
223
|
+
"media": "מדיה",
|
|
223
224
|
"recurring": "מחזורי",
|
|
224
225
|
"review": "סקירה",
|
|
225
226
|
"ralph": "לולאת Ralph"
|
|
@@ -3786,7 +3787,8 @@
|
|
|
3786
3787
|
"document": "מסמך",
|
|
3787
3788
|
"spike": "חקירה",
|
|
3788
3789
|
"review": "סקירה",
|
|
3789
|
-
"ralph": "לולאת Ralph"
|
|
3790
|
+
"ralph": "לולאת Ralph",
|
|
3791
|
+
"media": "מדיה"
|
|
3790
3792
|
},
|
|
3791
3793
|
"reviewFriction": {
|
|
3792
3794
|
"heading": "חיכוך חוב סקירה",
|
|
@@ -4962,7 +4964,8 @@
|
|
|
4962
4964
|
"document": "תיעוד",
|
|
4963
4965
|
"review": "סקירה",
|
|
4964
4966
|
"research": "מחקר",
|
|
4965
|
-
"planning": "תכנון"
|
|
4967
|
+
"planning": "תכנון",
|
|
4968
|
+
"media": "מדיה"
|
|
4966
4969
|
},
|
|
4967
4970
|
"strategyOption": {
|
|
4968
4971
|
"specialist-panel": "פאנל מומחים",
|
|
@@ -6688,6 +6691,14 @@
|
|
|
6688
6691
|
"automatic": "נוצר {total} מועמד ונשמר אוטומטית, ללא בדיקה.",
|
|
6689
6692
|
"chosen": "נשמרו {kept} מתוך {total} מועמדים שנוצרו.",
|
|
6690
6693
|
"awaiting": "נוצרו {total} מועמדים והם ממתינים להשוואה."
|
|
6694
|
+
},
|
|
6695
|
+
"asset": {
|
|
6696
|
+
"loading": "טוען את הקובץ…",
|
|
6697
|
+
"loadFailed": "לא ניתן היה לטעון את הקובץ השמור.",
|
|
6698
|
+
"retry": "נסה שוב",
|
|
6699
|
+
"open": "פתיחה",
|
|
6700
|
+
"download": "שמירת עותק",
|
|
6701
|
+
"unknownType": "קובץ שמור"
|
|
6691
6702
|
}
|
|
6692
6703
|
},
|
|
6693
6704
|
"sandbox": {
|
package/i18n/locales/it.json
CHANGED
|
@@ -1077,7 +1077,8 @@
|
|
|
1077
1077
|
"document": "documento",
|
|
1078
1078
|
"spike": "spike",
|
|
1079
1079
|
"review": "revisione",
|
|
1080
|
-
"ralph": "Ciclo Ralph"
|
|
1080
|
+
"ralph": "Ciclo Ralph",
|
|
1081
|
+
"media": "media"
|
|
1081
1082
|
},
|
|
1082
1083
|
"reviewFriction": {
|
|
1083
1084
|
"heading": "Attrito per revisioni in sospeso",
|
|
@@ -2982,6 +2983,7 @@
|
|
|
2982
2983
|
"bug": "Bug",
|
|
2983
2984
|
"document": "Documento",
|
|
2984
2985
|
"spike": "Spike",
|
|
2986
|
+
"media": "Media",
|
|
2985
2987
|
"recurring": "Ricorrente",
|
|
2986
2988
|
"review": "Revisione",
|
|
2987
2989
|
"ralph": "Ciclo Ralph"
|
|
@@ -4498,7 +4500,8 @@
|
|
|
4498
4500
|
"document": "Documentazione",
|
|
4499
4501
|
"review": "Revisione",
|
|
4500
4502
|
"research": "Ricerca",
|
|
4501
|
-
"planning": "Pianificazione"
|
|
4503
|
+
"planning": "Pianificazione",
|
|
4504
|
+
"media": "Media"
|
|
4502
4505
|
},
|
|
4503
4506
|
"strategyOption": {
|
|
4504
4507
|
"specialist-panel": "Panel di specialisti",
|
|
@@ -5396,6 +5399,14 @@
|
|
|
5396
5399
|
"automatic": "È stato generato {total} candidato e tenuto automaticamente, senza revisione.",
|
|
5397
5400
|
"chosen": "Sono stati tenuti {kept} dei {total} candidati generati.",
|
|
5398
5401
|
"awaiting": "Sono stati generati {total} candidati e attendono di essere confrontati."
|
|
5402
|
+
},
|
|
5403
|
+
"asset": {
|
|
5404
|
+
"loading": "Caricamento del file…",
|
|
5405
|
+
"loadFailed": "Non è stato possibile caricare il file archiviato.",
|
|
5406
|
+
"retry": "Riprova",
|
|
5407
|
+
"open": "Apri",
|
|
5408
|
+
"download": "Salva una copia",
|
|
5409
|
+
"unknownType": "File archiviato"
|
|
5399
5410
|
}
|
|
5400
5411
|
},
|
|
5401
5412
|
"brainstorm": {
|
package/i18n/locales/ja.json
CHANGED
|
@@ -220,6 +220,7 @@
|
|
|
220
220
|
"bug": "バグ",
|
|
221
221
|
"document": "ドキュメント",
|
|
222
222
|
"spike": "スパイク",
|
|
223
|
+
"media": "メディア",
|
|
223
224
|
"recurring": "繰り返し",
|
|
224
225
|
"review": "レビュー",
|
|
225
226
|
"ralph": "Ralph ループ"
|
|
@@ -3786,7 +3787,8 @@
|
|
|
3786
3787
|
"document": "ドキュメント",
|
|
3787
3788
|
"spike": "スパイク",
|
|
3788
3789
|
"review": "レビュー",
|
|
3789
|
-
"ralph": "Ralph ループ"
|
|
3790
|
+
"ralph": "Ralph ループ",
|
|
3791
|
+
"media": "メディア"
|
|
3790
3792
|
},
|
|
3791
3793
|
"reviewFriction": {
|
|
3792
3794
|
"heading": "レビュー滞留の摩擦",
|
|
@@ -4962,7 +4964,8 @@
|
|
|
4962
4964
|
"document": "ドキュメント",
|
|
4963
4965
|
"review": "レビュー",
|
|
4964
4966
|
"research": "調査",
|
|
4965
|
-
"planning": "計画"
|
|
4967
|
+
"planning": "計画",
|
|
4968
|
+
"media": "メディア"
|
|
4966
4969
|
},
|
|
4967
4970
|
"strategyOption": {
|
|
4968
4971
|
"specialist-panel": "専門家パネル",
|
|
@@ -6688,6 +6691,14 @@
|
|
|
6688
6691
|
"automatic": "候補が {total} 件生成され、確認なしで自動的に採用されました。",
|
|
6689
6692
|
"chosen": "生成された {total} 件の候補のうち {kept} 件が採用されました。",
|
|
6690
6693
|
"awaiting": "{total} 件の候補が生成され、比較を待っています。"
|
|
6694
|
+
},
|
|
6695
|
+
"asset": {
|
|
6696
|
+
"loading": "ファイルを読み込んでいます…",
|
|
6697
|
+
"loadFailed": "保存されたファイルを読み込めませんでした。",
|
|
6698
|
+
"retry": "再試行",
|
|
6699
|
+
"open": "開く",
|
|
6700
|
+
"download": "コピーを保存",
|
|
6701
|
+
"unknownType": "保存されたファイル"
|
|
6691
6702
|
}
|
|
6692
6703
|
},
|
|
6693
6704
|
"sandbox": {
|
package/i18n/locales/pl.json
CHANGED
|
@@ -220,6 +220,7 @@
|
|
|
220
220
|
"bug": "Błąd",
|
|
221
221
|
"document": "Dokument",
|
|
222
222
|
"spike": "Spike",
|
|
223
|
+
"media": "Multimedia",
|
|
223
224
|
"recurring": "Cykliczne",
|
|
224
225
|
"review": "Przegląd",
|
|
225
226
|
"ralph": "Pętla Ralph"
|
|
@@ -3644,7 +3645,8 @@
|
|
|
3644
3645
|
"document": "dokument",
|
|
3645
3646
|
"spike": "spike",
|
|
3646
3647
|
"review": "przegląd",
|
|
3647
|
-
"ralph": "Pętla Ralph"
|
|
3648
|
+
"ralph": "Pętla Ralph",
|
|
3649
|
+
"media": "multimedia"
|
|
3648
3650
|
},
|
|
3649
3651
|
"reviewFriction": {
|
|
3650
3652
|
"heading": "Hamulec długu przeglądów",
|
|
@@ -4962,7 +4964,8 @@
|
|
|
4962
4964
|
"document": "Dokumentacja",
|
|
4963
4965
|
"review": "Przegląd",
|
|
4964
4966
|
"research": "Badania",
|
|
4965
|
-
"planning": "Planowanie"
|
|
4967
|
+
"planning": "Planowanie",
|
|
4968
|
+
"media": "Multimedia"
|
|
4966
4969
|
},
|
|
4967
4970
|
"strategyOption": {
|
|
4968
4971
|
"specialist-panel": "Panel specjalistów",
|
|
@@ -6688,6 +6691,14 @@
|
|
|
6688
6691
|
"automatic": "Wygenerowano {total} kandydata i zachowano go automatycznie, bez przeglądu.",
|
|
6689
6692
|
"chosen": "Zachowano {kept} z {total} wygenerowanych kandydatów.",
|
|
6690
6693
|
"awaiting": "Wygenerowano {total} kandydatów i czekają na porównanie."
|
|
6694
|
+
},
|
|
6695
|
+
"asset": {
|
|
6696
|
+
"loading": "Wczytywanie pliku…",
|
|
6697
|
+
"loadFailed": "Nie udało się wczytać zapisanego pliku.",
|
|
6698
|
+
"retry": "Spróbuj ponownie",
|
|
6699
|
+
"open": "Otwórz",
|
|
6700
|
+
"download": "Zapisz kopię",
|
|
6701
|
+
"unknownType": "Zapisany plik"
|
|
6691
6702
|
}
|
|
6692
6703
|
},
|
|
6693
6704
|
"sandbox": {
|
package/i18n/locales/tr.json
CHANGED
|
@@ -220,6 +220,7 @@
|
|
|
220
220
|
"bug": "Hata",
|
|
221
221
|
"document": "Belge",
|
|
222
222
|
"spike": "Spike",
|
|
223
|
+
"media": "Medya",
|
|
223
224
|
"recurring": "Yinelenen",
|
|
224
225
|
"review": "İnceleme",
|
|
225
226
|
"ralph": "Ralph döngüsü"
|
|
@@ -3786,7 +3787,8 @@
|
|
|
3786
3787
|
"document": "belge",
|
|
3787
3788
|
"spike": "inceleme",
|
|
3788
3789
|
"review": "inceleme",
|
|
3789
|
-
"ralph": "Ralph döngüsü"
|
|
3790
|
+
"ralph": "Ralph döngüsü",
|
|
3791
|
+
"media": "medya"
|
|
3790
3792
|
},
|
|
3791
3793
|
"reviewFriction": {
|
|
3792
3794
|
"heading": "İnceleme borcu sürtünmesi",
|
|
@@ -4962,7 +4964,8 @@
|
|
|
4962
4964
|
"document": "Dokümantasyon",
|
|
4963
4965
|
"review": "İnceleme",
|
|
4964
4966
|
"research": "Araştırma",
|
|
4965
|
-
"planning": "Planlama"
|
|
4967
|
+
"planning": "Planlama",
|
|
4968
|
+
"media": "Medya"
|
|
4966
4969
|
},
|
|
4967
4970
|
"strategyOption": {
|
|
4968
4971
|
"specialist-panel": "Uzman paneli",
|
|
@@ -6688,6 +6691,14 @@
|
|
|
6688
6691
|
"automatic": "{total} aday üretildi ve inceleme olmadan otomatik olarak tutuldu.",
|
|
6689
6692
|
"chosen": "Üretilen {total} adayın {kept} tanesi tutuldu.",
|
|
6690
6693
|
"awaiting": "{total} aday üretildi ve karşılaştırılmayı bekliyor."
|
|
6694
|
+
},
|
|
6695
|
+
"asset": {
|
|
6696
|
+
"loading": "Dosya yükleniyor…",
|
|
6697
|
+
"loadFailed": "Saklanan dosya yüklenemedi.",
|
|
6698
|
+
"retry": "Yeniden dene",
|
|
6699
|
+
"open": "Aç",
|
|
6700
|
+
"download": "Kopyasını kaydet",
|
|
6701
|
+
"unknownType": "Saklanan dosya"
|
|
6691
6702
|
}
|
|
6692
6703
|
},
|
|
6693
6704
|
"sandbox": {
|
package/i18n/locales/uk.json
CHANGED
|
@@ -220,6 +220,7 @@
|
|
|
220
220
|
"bug": "Помилка",
|
|
221
221
|
"document": "Документ",
|
|
222
222
|
"spike": "Spike",
|
|
223
|
+
"media": "Медіа",
|
|
223
224
|
"recurring": "Періодичне",
|
|
224
225
|
"review": "Огляд",
|
|
225
226
|
"ralph": "Цикл Ralph"
|
|
@@ -3644,7 +3645,8 @@
|
|
|
3644
3645
|
"document": "документ",
|
|
3645
3646
|
"spike": "spike",
|
|
3646
3647
|
"review": "огляд",
|
|
3647
|
-
"ralph": "Цикл Ralph"
|
|
3648
|
+
"ralph": "Цикл Ralph",
|
|
3649
|
+
"media": "медіа"
|
|
3648
3650
|
},
|
|
3649
3651
|
"reviewFriction": {
|
|
3650
3652
|
"heading": "Тертя через борг рев’ю",
|
|
@@ -4962,7 +4964,8 @@
|
|
|
4962
4964
|
"document": "Документація",
|
|
4963
4965
|
"review": "Рецензування",
|
|
4964
4966
|
"research": "Дослідження",
|
|
4965
|
-
"planning": "Планування"
|
|
4967
|
+
"planning": "Планування",
|
|
4968
|
+
"media": "Медіа"
|
|
4966
4969
|
},
|
|
4967
4970
|
"strategyOption": {
|
|
4968
4971
|
"specialist-panel": "Панель фахівців",
|
|
@@ -6688,6 +6691,14 @@
|
|
|
6688
6691
|
"automatic": "Створено {total} кандидата й залишено автоматично, без перегляду.",
|
|
6689
6692
|
"chosen": "Залишено {kept} із {total} створених кандидатів.",
|
|
6690
6693
|
"awaiting": "Створено {total} кандидатів, які очікують на порівняння."
|
|
6694
|
+
},
|
|
6695
|
+
"asset": {
|
|
6696
|
+
"loading": "Завантаження файлу…",
|
|
6697
|
+
"loadFailed": "Не вдалося завантажити збережений файл.",
|
|
6698
|
+
"retry": "Спробувати ще раз",
|
|
6699
|
+
"open": "Відкрити",
|
|
6700
|
+
"download": "Зберегти копію",
|
|
6701
|
+
"unknownType": "Збережений файл"
|
|
6691
6702
|
}
|
|
6692
6703
|
},
|
|
6693
6704
|
"sandbox": {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cat-factory/app",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.277.0",
|
|
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.41",
|
|
42
42
|
"wretch": "^3.0.9",
|
|
43
|
-
"@cat-factory/contracts": "0.
|
|
43
|
+
"@cat-factory/contracts": "0.316.0"
|
|
44
44
|
},
|
|
45
45
|
"devDependencies": {
|
|
46
46
|
"@toad-contracts/testing": "0.3.2",
|