@cat-factory/app 0.300.1 → 0.300.2
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/README.md +21 -0
- package/app/components/assistant/AssistantModal.logic.spec.ts +129 -1
- package/app/components/assistant/AssistantModal.logic.ts +83 -1
- package/app/components/assistant/AssistantModal.vue +124 -17
- package/app/components/board/AddTaskModal.vue +1 -2
- package/app/components/board/CreateInitiativeModal.vue +2 -3
- package/app/components/board/RecurringPipelineModal.vue +1 -2
- package/app/components/documents/DocumentSourceConnectModal.vue +2 -2
- package/app/components/documents/StartFromDesignModal.vue +1 -2
- package/app/components/layout/CommandBar.vue +1 -2
- package/app/components/pipeline/PipelineBuilder.vue +6 -6
- package/app/components/providers/PersonalCredentialModal.vue +3 -3
- package/app/components/tasks/BugHuntModal.vue +1 -2
- package/app/components/tasks/TaskImportModal.vue +5 -7
- package/app/components/tasks/TaskSourceConnectModal.vue +2 -2
- package/app/composables/api/assistant.ts +5 -3
- package/app/composables/useArtifactBlobs.ts +2 -1
- package/app/composables/useModalOpen.spec.ts +51 -0
- package/app/composables/useModalOpen.ts +31 -0
- package/app/stores/assistant.spec.ts +183 -0
- package/app/stores/assistant.ts +83 -7
- package/app/types/domain.ts +4 -0
- package/app/types/load-state.ts +18 -0
- package/app/utils/catalog.ts +21 -0
- package/i18n/locales/de.json +4 -0
- package/i18n/locales/en.json +7 -0
- package/i18n/locales/es.json +4 -0
- package/i18n/locales/fr.json +4 -0
- package/i18n/locales/he.json +4 -0
- package/i18n/locales/it.json +4 -0
- package/i18n/locales/ja.json +4 -0
- package/i18n/locales/pl.json +4 -0
- package/i18n/locales/tr.json +4 -0
- package/i18n/locales/uk.json +4 -0
- package/package.json +2 -2
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { reactive } from 'vue'
|
|
2
|
+
import type { LoadState } from '~/types/load-state'
|
|
2
3
|
import { useWorkspaceStore } from '~/stores/workspace'
|
|
3
4
|
|
|
4
5
|
/**
|
|
@@ -15,7 +16,7 @@ import { useWorkspaceStore } from '~/stores/workspace'
|
|
|
15
16
|
* Both the visual-confirmation gate and the test-report window use this, so neither has to
|
|
16
17
|
* own blob plumbing or depend on the other's Pinia store.
|
|
17
18
|
*/
|
|
18
|
-
export type ArtifactBlobStatus =
|
|
19
|
+
export type ArtifactBlobStatus = LoadState
|
|
19
20
|
|
|
20
21
|
export function useArtifactBlobs() {
|
|
21
22
|
const ws = useWorkspaceStore()
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest'
|
|
2
|
+
import { effectScope, nextTick, ref } from 'vue'
|
|
3
|
+
import { onModalOpen } from './useModalOpen'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Register `onModalOpen` with `open` already at `mountedOpen`, which is what a modal's setup does.
|
|
7
|
+
* That first value is the whole point: the page mounts most panels only WHILE their flag is set,
|
|
8
|
+
* so a modal's `open` is already `true` by the time its own setup runs.
|
|
9
|
+
*/
|
|
10
|
+
function register(mountedOpen: boolean) {
|
|
11
|
+
const open = ref(mountedOpen)
|
|
12
|
+
const ran = vi.fn()
|
|
13
|
+
const scope = effectScope()
|
|
14
|
+
scope.run(() => onModalOpen(open, ran))
|
|
15
|
+
return { open, ran, stop: () => scope.stop() }
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
describe('onModalOpen', () => {
|
|
19
|
+
it('runs on the render the panel MOUNTS on, when it mounts already open', () => {
|
|
20
|
+
// The bug this exists to make unrepresentable: a change-only `watch(open)` never fires for a
|
|
21
|
+
// `v-if`-mounted modal, so whatever the body seeds (a picker's default, the read that fills a
|
|
22
|
+
// catalog) never happens and the panel opens with an unselected control and a dead confirm.
|
|
23
|
+
const { ran, stop } = register(true)
|
|
24
|
+
|
|
25
|
+
expect(ran).toHaveBeenCalledTimes(1)
|
|
26
|
+
stop()
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
it('does not run for a panel that is mounted CLOSED', () => {
|
|
30
|
+
// The always-mounted half of the SPA. The same call has to be correct there, or this could not
|
|
31
|
+
// be the one thing every site uses and each author would be back to deciding per site.
|
|
32
|
+
const { ran, stop } = register(false)
|
|
33
|
+
|
|
34
|
+
expect(ran).not.toHaveBeenCalled()
|
|
35
|
+
stop()
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
it('runs again on each later open, and never on a close', async () => {
|
|
39
|
+
const { open, ran, stop } = register(false)
|
|
40
|
+
|
|
41
|
+
open.value = true
|
|
42
|
+
await nextTick()
|
|
43
|
+
open.value = false
|
|
44
|
+
await nextTick()
|
|
45
|
+
open.value = true
|
|
46
|
+
await nextTick()
|
|
47
|
+
|
|
48
|
+
expect(ran).toHaveBeenCalledTimes(2)
|
|
49
|
+
stop()
|
|
50
|
+
})
|
|
51
|
+
})
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { watch } from 'vue'
|
|
2
|
+
import type { WatchSource } from 'vue'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Run `fn` whenever a modal opens, INCLUDING the render it mounts on.
|
|
6
|
+
*
|
|
7
|
+
* This exists because `watch(open, (isOpen) => { if (isOpen) … })` is wrong in this SPA and reads
|
|
8
|
+
* as right. The page mounts most panels only WHILE their open flag is set
|
|
9
|
+
* (`<AssistantModal v-if="ui.assistantOpen" />`), so `open` is already `true` when the component
|
|
10
|
+
* sets up and a change-only watcher never fires at all: whatever the body seeds (a picker's
|
|
11
|
+
* default, the read that fills a catalog) never happens, and the panel opens with an unselected
|
|
12
|
+
* control and a confirm button that cannot be pressed. Nothing throws, which is why it survived in
|
|
13
|
+
* several modals at once.
|
|
14
|
+
*
|
|
15
|
+
* `{ immediate: true }` is the whole fix, and a shared helper is how it stops being per-site
|
|
16
|
+
* knowledge: a panel that IS always mounted passes a `false` on that first run and the body is
|
|
17
|
+
* skipped, so this is correct either way and nobody has to know which kind they are writing.
|
|
18
|
+
*
|
|
19
|
+
* Only for work done on the way IN. A watcher acting on the CLOSE edge (emitting `close`, revoking
|
|
20
|
+
* object URLs) stays a plain `watch`: it has nothing to do at mount, and running it there would
|
|
21
|
+
* announce a close that never happened.
|
|
22
|
+
*/
|
|
23
|
+
export function onModalOpen(open: WatchSource<boolean>, fn: () => void): void {
|
|
24
|
+
watch(
|
|
25
|
+
open,
|
|
26
|
+
(isOpen) => {
|
|
27
|
+
if (isOpen) fn()
|
|
28
|
+
},
|
|
29
|
+
{ immediate: true },
|
|
30
|
+
)
|
|
31
|
+
}
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'
|
|
2
|
+
import { useAssistantStore } from '~/stores/assistant'
|
|
3
|
+
import { useWorkspaceStore } from '~/stores/workspace'
|
|
4
|
+
import { ApiError } from '~/composables/api/errors'
|
|
5
|
+
import type { AssistantCapability } from '~/types/domain'
|
|
6
|
+
|
|
7
|
+
// What the modal reads to decide whether it may offer the prompt box, and whether that box may be
|
|
8
|
+
// submitted. The capability's own ANSWER is two independent bits (is a model wired, and is there
|
|
9
|
+
// anything for it to do); on their own they cannot say whether anyone has asked yet, which is a
|
|
10
|
+
// third fact the modal has to render differently from both.
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Stub `useApi` ONCE, behind a handler the test can swap. The store resolves `useApi()` at setup,
|
|
14
|
+
* so re-stubbing after `useAssistantStore()` would leave it holding the first stub for ever.
|
|
15
|
+
*
|
|
16
|
+
* The handler is given the abort signal the store attaches its deadline to, so a test can assert
|
|
17
|
+
* on what a timed-out read does to the request as well as to the state.
|
|
18
|
+
*/
|
|
19
|
+
function stubApi(): {
|
|
20
|
+
store: ReturnType<typeof useAssistantStore>
|
|
21
|
+
serve: (fn: (signal: AbortSignal) => Promise<AssistantCapability>) => void
|
|
22
|
+
} {
|
|
23
|
+
let handler: (signal: AbortSignal) => Promise<AssistantCapability> = () =>
|
|
24
|
+
Promise.resolve({ available: true, actions: ['declare-service-dependency'] })
|
|
25
|
+
vi.stubGlobal('useApi', () => ({
|
|
26
|
+
getAssistantCapability: (_ws: string, signal: AbortSignal) => handler(signal),
|
|
27
|
+
}))
|
|
28
|
+
return {
|
|
29
|
+
store: useAssistantStore(),
|
|
30
|
+
serve: (fn) => {
|
|
31
|
+
handler = fn
|
|
32
|
+
},
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const failing = (status = 503) =>
|
|
37
|
+
Promise.reject(new ApiError(status, { error: { code: 'unavailable', message: 'nope' } }))
|
|
38
|
+
|
|
39
|
+
describe('assistant store: the capability read', () => {
|
|
40
|
+
beforeEach(() => {
|
|
41
|
+
useWorkspaceStore().workspaceId = 'ws1'
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
it('starts idle, which is not the same fact as unavailable', () => {
|
|
45
|
+
const { store } = stubApi()
|
|
46
|
+
|
|
47
|
+
expect(store.capabilityRead).toBe('idle')
|
|
48
|
+
expect(store.capability).toBeNull()
|
|
49
|
+
expect(store.available).toBe(false)
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
it('holds `loading` while the read is in flight', async () => {
|
|
53
|
+
const { store, serve } = stubApi()
|
|
54
|
+
let answer!: (capability: AssistantCapability) => void
|
|
55
|
+
serve(() => new Promise((resolve) => (answer = resolve)))
|
|
56
|
+
|
|
57
|
+
const inFlight = store.loadCapability()
|
|
58
|
+
expect(store.capabilityRead).toBe('loading')
|
|
59
|
+
|
|
60
|
+
answer({ available: true, actions: ['declare-service-dependency'] })
|
|
61
|
+
await inFlight
|
|
62
|
+
|
|
63
|
+
expect(store.capabilityRead).toBe('ready')
|
|
64
|
+
expect(store.actions).toEqual(['declare-service-dependency'])
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
it('KEEPS the answer it holds while a re-read is in flight', async () => {
|
|
68
|
+
// Re-opening the modal re-reads. Clearing the answer for the duration would replace the box
|
|
69
|
+
// with a waiting state on every open, for a fact the store can already state.
|
|
70
|
+
const { store, serve } = stubApi()
|
|
71
|
+
await store.loadCapability()
|
|
72
|
+
|
|
73
|
+
serve(() => new Promise(() => {}))
|
|
74
|
+
void store.loadCapability()
|
|
75
|
+
|
|
76
|
+
expect(store.capabilityRead).toBe('loading')
|
|
77
|
+
expect(store.available).toBe(true)
|
|
78
|
+
expect(store.actions).toEqual(['declare-service-dependency'])
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
it('records a read that answered "no model" as read, so it can be told from an outage', async () => {
|
|
82
|
+
const { store, serve } = stubApi()
|
|
83
|
+
serve(() => Promise.resolve({ available: false, actions: [] }))
|
|
84
|
+
|
|
85
|
+
await store.loadCapability()
|
|
86
|
+
|
|
87
|
+
expect(store.capabilityRead).toBe('ready')
|
|
88
|
+
expect(store.available).toBe(false)
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
it('marks a FAILED read as failed and does not throw', async () => {
|
|
92
|
+
// The failure is REPORTED by the panel this state puts on screen, which is where the retry
|
|
93
|
+
// lives too. Throwing it as well would have the modal toast a second, non-dismissing copy of
|
|
94
|
+
// the same sentence over that panel, once per retry.
|
|
95
|
+
const { store, serve } = stubApi()
|
|
96
|
+
serve(() => failing())
|
|
97
|
+
|
|
98
|
+
await expect(store.loadCapability()).resolves.toBeUndefined()
|
|
99
|
+
|
|
100
|
+
expect(store.capabilityRead).toBe('error')
|
|
101
|
+
expect(store.capability).toBeNull()
|
|
102
|
+
expect(store.available).toBe(false)
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
it('drops a previous answer when a later read fails', async () => {
|
|
106
|
+
// A retry that fails must not leave the box open on the strength of the read before it: the
|
|
107
|
+
// deployment's model may have gone away with whatever took the endpoint down.
|
|
108
|
+
const { store, serve } = stubApi()
|
|
109
|
+
serve(() => Promise.resolve({ available: true, actions: ['add-service-from-repo'] }))
|
|
110
|
+
await store.loadCapability()
|
|
111
|
+
|
|
112
|
+
serve(() => failing(500))
|
|
113
|
+
await store.loadCapability()
|
|
114
|
+
|
|
115
|
+
expect(store.capability).toBeNull()
|
|
116
|
+
expect(store.actions).toEqual([])
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
it('recovers on a retry that answers', async () => {
|
|
120
|
+
const { store, serve } = stubApi()
|
|
121
|
+
serve(() => failing(502))
|
|
122
|
+
await store.loadCapability()
|
|
123
|
+
|
|
124
|
+
serve(() => Promise.resolve({ available: true, actions: ['create-task-from-issue'] }))
|
|
125
|
+
await store.loadCapability()
|
|
126
|
+
|
|
127
|
+
expect(store.capabilityRead).toBe('ready')
|
|
128
|
+
expect(store.available).toBe(true)
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
it('settles overlapping reads in the order they STARTED, not the order they answer', async () => {
|
|
132
|
+
// A slow success that lands after the fast failure which superseded it would otherwise
|
|
133
|
+
// re-offer the box on an answer older than the failure that replaced it.
|
|
134
|
+
const { store, serve } = stubApi()
|
|
135
|
+
let answerSlow!: (capability: AssistantCapability) => void
|
|
136
|
+
serve(() => new Promise((resolve) => (answerSlow = resolve)))
|
|
137
|
+
const slow = store.loadCapability()
|
|
138
|
+
|
|
139
|
+
serve(() => failing())
|
|
140
|
+
await store.loadCapability()
|
|
141
|
+
expect(store.capabilityRead).toBe('error')
|
|
142
|
+
|
|
143
|
+
answerSlow({ available: true, actions: ['declare-service-dependency'] })
|
|
144
|
+
await slow
|
|
145
|
+
|
|
146
|
+
expect(store.capabilityRead).toBe('error')
|
|
147
|
+
expect(store.capability).toBeNull()
|
|
148
|
+
})
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
describe('assistant store: the capability read deadline', () => {
|
|
152
|
+
beforeEach(() => {
|
|
153
|
+
useWorkspaceStore().workspaceId = 'ws1'
|
|
154
|
+
vi.useFakeTimers()
|
|
155
|
+
})
|
|
156
|
+
afterEach(() => {
|
|
157
|
+
vi.useRealTimers()
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
it('fails a read that never settles, and ABORTS the request it gave up on', async () => {
|
|
161
|
+
// The shared client sets no timeout, so a connection accepted and never answered would leave
|
|
162
|
+
// the modal waiting for ever: no answer, no failure, and so no retry either, since the retry
|
|
163
|
+
// is what a failed read puts on screen.
|
|
164
|
+
const { store, serve } = stubApi()
|
|
165
|
+
let aborted = false
|
|
166
|
+
serve(
|
|
167
|
+
(signal) =>
|
|
168
|
+
new Promise((_resolve, reject) => {
|
|
169
|
+
signal.addEventListener('abort', () => {
|
|
170
|
+
aborted = true
|
|
171
|
+
reject(new Error('aborted'))
|
|
172
|
+
})
|
|
173
|
+
}),
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
const read = store.loadCapability()
|
|
177
|
+
await vi.advanceTimersByTimeAsync(10_000)
|
|
178
|
+
await read
|
|
179
|
+
|
|
180
|
+
expect(aborted).toBe(true)
|
|
181
|
+
expect(store.capabilityRead).toBe('error')
|
|
182
|
+
})
|
|
183
|
+
})
|
package/app/stores/assistant.ts
CHANGED
|
@@ -1,8 +1,20 @@
|
|
|
1
1
|
import { defineStore } from 'pinia'
|
|
2
2
|
import { computed, ref } from 'vue'
|
|
3
3
|
import type { AssistantAnswer, AssistantCapability, AssistantTurn } from '~/types/domain'
|
|
4
|
+
import type { LoadState } from '~/types/load-state'
|
|
4
5
|
import { useWorkspaceStore } from '~/stores/workspace'
|
|
5
6
|
|
|
7
|
+
/**
|
|
8
|
+
* How long the capability read waits before it counts as failed.
|
|
9
|
+
*
|
|
10
|
+
* The shared client sets no timeout, so a connection that is accepted and never answered (a proxy
|
|
11
|
+
* holding it open, a wedged worker) leaves the GET pending for ever. Without a deadline that is a
|
|
12
|
+
* modal whose read never settles: no answer, no failure, and therefore no retry either, since the
|
|
13
|
+
* retry lives in what a FAILED read puts on screen. The read itself is a tiny in-memory answer on
|
|
14
|
+
* the backend, so anything past a few seconds is already a connection that is not coming back.
|
|
15
|
+
*/
|
|
16
|
+
const CAPABILITY_DEADLINE_MS = 10_000
|
|
17
|
+
|
|
6
18
|
/**
|
|
7
19
|
* In-app assistant state: what this deployment's assistant can do, and the last turn's outcome.
|
|
8
20
|
*
|
|
@@ -11,25 +23,78 @@ import { useWorkspaceStore } from '~/stores/workspace'
|
|
|
11
23
|
* live stream like any other, so the frame or task shows up without this store touching the board).
|
|
12
24
|
* Keeping a transcript would be a second, staler record of changes the board already carries.
|
|
13
25
|
*
|
|
14
|
-
* Failures are
|
|
15
|
-
* funnel (`usePipelineErrorToast`) already renders translated, copyable and with its
|
|
16
|
-
* The three OUTCOMES are the ones this store keeps, because they are answers rather
|
|
26
|
+
* Failures of a TURN are not held here: every refusal a turn can raise is a `DomainError` the
|
|
27
|
+
* shared error funnel (`usePipelineErrorToast`) already renders translated, copyable and with its
|
|
28
|
+
* request id. The three OUTCOMES are the ones this store keeps, because they are answers rather
|
|
29
|
+
* than errors. A failed capability READ is the exception, and it is state rather than a throw: see
|
|
30
|
+
* `loadCapability`.
|
|
17
31
|
*/
|
|
18
32
|
export const useAssistantStore = defineStore('assistant', () => {
|
|
19
33
|
const api = useApi()
|
|
20
34
|
const workspace = useWorkspaceStore()
|
|
21
35
|
|
|
22
36
|
const capability = ref<AssistantCapability | null>(null)
|
|
37
|
+
const capabilityRead = ref<LoadState>('idle')
|
|
23
38
|
const turn = ref<AssistantTurn | null>(null)
|
|
24
39
|
const running = ref(false)
|
|
25
40
|
|
|
26
|
-
/**
|
|
41
|
+
/**
|
|
42
|
+
* How many reads have STARTED. Compared before every write, so two overlapping reads settle in
|
|
43
|
+
* the order they were ISSUED rather than the order they answer: a slow success that lands after
|
|
44
|
+
* the fast failure that superseded it would otherwise re-offer the box on the older answer.
|
|
45
|
+
*/
|
|
46
|
+
let reads = 0
|
|
47
|
+
|
|
48
|
+
/** Whether a model is wired at all. Only meaningful once `capabilityRead` says `ready`. */
|
|
27
49
|
const available = computed(() => capability.value?.available === true)
|
|
28
50
|
const actions = computed(() => capability.value?.actions ?? [])
|
|
29
51
|
|
|
30
|
-
/**
|
|
52
|
+
/**
|
|
53
|
+
* Read what the assistant can do here. Idempotent: re-reading replaces the answer.
|
|
54
|
+
*
|
|
55
|
+
* A re-read KEEPS the answer it already has while it is in flight, so the surface stays on the
|
|
56
|
+
* fact it can already state instead of dropping back to a spinner every time the modal is
|
|
57
|
+
* re-opened. Only a failure clears it, because a deployment's model may have gone away with
|
|
58
|
+
* whatever took the endpoint down.
|
|
59
|
+
*
|
|
60
|
+
* The failure is recorded, not thrown. It is reported in place: `error` is what puts the
|
|
61
|
+
* explanation and the retry button on screen, and toasting it as well would stack a second,
|
|
62
|
+
* non-dismissing copy of the same sentence over the panel that already says it, once per retry.
|
|
63
|
+
*/
|
|
31
64
|
async function loadCapability(): Promise<void> {
|
|
32
|
-
|
|
65
|
+
const read = ++reads
|
|
66
|
+
capabilityRead.value = 'loading'
|
|
67
|
+
try {
|
|
68
|
+
const answer = await withDeadline((signal) =>
|
|
69
|
+
api.getAssistantCapability(workspace.requireId(), signal),
|
|
70
|
+
)
|
|
71
|
+
if (read !== reads) return
|
|
72
|
+
capability.value = answer
|
|
73
|
+
capabilityRead.value = 'ready'
|
|
74
|
+
} catch {
|
|
75
|
+
if (read !== reads) return
|
|
76
|
+
capability.value = null
|
|
77
|
+
capabilityRead.value = 'error'
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Read under {@link CAPABILITY_DEADLINE_MS}, ABORTING the request when it expires. */
|
|
82
|
+
async function withDeadline(
|
|
83
|
+
send: (signal: AbortSignal) => Promise<AssistantCapability>,
|
|
84
|
+
): Promise<AssistantCapability> {
|
|
85
|
+
const controller = new AbortController()
|
|
86
|
+
let timer: ReturnType<typeof setTimeout> | undefined
|
|
87
|
+
try {
|
|
88
|
+
return await new Promise<AssistantCapability>((resolve, reject) => {
|
|
89
|
+
timer = setTimeout(() => {
|
|
90
|
+
controller.abort()
|
|
91
|
+
reject(new Error(`Assistant capability read timed out after ${CAPABILITY_DEADLINE_MS}ms`))
|
|
92
|
+
}, CAPABILITY_DEADLINE_MS)
|
|
93
|
+
send(controller.signal).then(resolve, reject)
|
|
94
|
+
})
|
|
95
|
+
} finally {
|
|
96
|
+
clearTimeout(timer)
|
|
97
|
+
}
|
|
33
98
|
}
|
|
34
99
|
|
|
35
100
|
/**
|
|
@@ -65,5 +130,16 @@ export const useAssistantStore = defineStore('assistant', () => {
|
|
|
65
130
|
turn.value = null
|
|
66
131
|
}
|
|
67
132
|
|
|
68
|
-
return {
|
|
133
|
+
return {
|
|
134
|
+
capability,
|
|
135
|
+
capabilityRead,
|
|
136
|
+
turn,
|
|
137
|
+
running,
|
|
138
|
+
available,
|
|
139
|
+
actions,
|
|
140
|
+
loadCapability,
|
|
141
|
+
run,
|
|
142
|
+
answer,
|
|
143
|
+
reset,
|
|
144
|
+
}
|
|
69
145
|
})
|
package/app/types/domain.ts
CHANGED
|
@@ -125,6 +125,10 @@ import type { AgentCategory, AgentKind, AgentTier, PipelinePurpose } from '@cat-
|
|
|
125
125
|
// single source of truth lives in the contracts package.
|
|
126
126
|
export { DOC_KINDS, DOC_KIND_FIELDS } from '@cat-factory/contracts'
|
|
127
127
|
|
|
128
|
+
// The assistant's prompt cap is a runtime value too: the box states the limit and refuses a
|
|
129
|
+
// submission over it, and the wire schema holds the same number.
|
|
130
|
+
export { ASSISTANT_PROMPT_MAX } from '@cat-factory/contracts'
|
|
131
|
+
|
|
128
132
|
/** A draggable agent definition shown in the agent palette. Frontend-only. */
|
|
129
133
|
export interface AgentArchetype {
|
|
130
134
|
kind: AgentKind
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How far a read has got: the SPA's one vocabulary for load progress.
|
|
3
|
+
*
|
|
4
|
+
* Four states rather than a nullable value, because an absent value is not a single fact. "Nobody
|
|
5
|
+
* asked", "the read is in flight" and "the read failed" need three different answers on screen and
|
|
6
|
+
* only the middle one is temporary; collapsed into one `null` they all render as whatever the
|
|
7
|
+
* surface shows for "no data", which is usually the empty state and is wrong for two of them.
|
|
8
|
+
*
|
|
9
|
+
* A REFRESH is deliberately not a fifth member. A read that already has an answer keeps it, so a
|
|
10
|
+
* surface asking "what do I show" reads the value it holds and a surface asking "is something in
|
|
11
|
+
* flight" reads `loading`; a re-read that downgraded the surface to `loading` would replace a
|
|
12
|
+
* usable panel with a spinner for a fact it can already state.
|
|
13
|
+
*
|
|
14
|
+
* A status carrying a member this does not have keeps its own type (`NotificationSettingsStatus`
|
|
15
|
+
* distinguishes "the deployment does not offer this" from a failure, which is a fifth fact rather
|
|
16
|
+
* than a renaming of one of these four).
|
|
17
|
+
*/
|
|
18
|
+
export type LoadState = 'idle' | 'loading' | 'ready' | 'error'
|
package/app/utils/catalog.ts
CHANGED
|
@@ -862,6 +862,24 @@ export const SYSTEM_AGENT_META: Record<string, AgentArchetype> = {
|
|
|
862
862
|
description:
|
|
863
863
|
'Grades each completed agent step (smooth vs chaotic) after a run and recommends prompt/model improvements.',
|
|
864
864
|
},
|
|
865
|
+
// The in-app assistant routes ONE typed sentence to one action from a closed catalog. Not a
|
|
866
|
+
// pipeline step (it declares no `category`, so it is never in the palette), but it runs an LLM
|
|
867
|
+
// on every request, so it needs display metadata here and a per-workspace model in Model
|
|
868
|
+
// Configuration. Without an entry it inherits the preset's base model like any unnamed kind,
|
|
869
|
+
// which is the right default; what it lacked was the row an operator pins a different one on,
|
|
870
|
+
// and a label anywhere a spend rollup names the kind that spent it.
|
|
871
|
+
assistant: {
|
|
872
|
+
kind: 'assistant',
|
|
873
|
+
// Intermediate, not advanced like `kaizen`: the assistant is a surface a person OPENS and
|
|
874
|
+
// spends on deliberately, several times a day, where Kaizen grades in the background on its
|
|
875
|
+
// own schedule. A kind whose cost someone can feel should not sit two levels down.
|
|
876
|
+
tier: 'intermediate',
|
|
877
|
+
label: 'Assistant',
|
|
878
|
+
icon: 'i-lucide-sparkles',
|
|
879
|
+
color: '#38bdf8',
|
|
880
|
+
description:
|
|
881
|
+
'Routes a typed request to one action the platform performs on the board (declare a dependency, add a service from a repository, file a task from a tracker issue).',
|
|
882
|
+
},
|
|
865
883
|
// A polling gate (no model of its own) that watches the released PR's observability
|
|
866
884
|
// signals after merge and escalates to the on-call agent on a regression. NOT in any
|
|
867
885
|
// default pipeline and NOT a standing palette archetype — the palette surfaces it
|
|
@@ -913,6 +931,9 @@ export const MODEL_CONFIGURABLE_SYSTEM_KINDS: AgentArchetype[] = [
|
|
|
913
931
|
// The PR-review Challenge Investigator — pinnable to its own (stronger) model, separately
|
|
914
932
|
// from the reviewer that produced the findings.
|
|
915
933
|
'challenge-investigator',
|
|
934
|
+
// The in-app assistant: one inline model call per typed request, on the workspace's preset
|
|
935
|
+
// like every other kind, and pinnable away from it here.
|
|
936
|
+
'assistant',
|
|
916
937
|
].map((kind) => SYSTEM_AGENT_META[kind]!),
|
|
917
938
|
// Companions run LLMs but aren't palette-addable (they're producer toggles), so include
|
|
918
939
|
// them here to keep their per-workspace default model pinnable in the Model Defaults panel.
|
package/i18n/locales/de.json
CHANGED
|
@@ -4520,9 +4520,13 @@
|
|
|
4520
4520
|
"title": "Assistent",
|
|
4521
4521
|
"intro": "Beschreiben Sie, was erledigt werden soll, und der Assistent führt es auf dem Board aus: eine Abhängigkeit zwischen zwei Services erklären, einen Service über eine Repository-URL hinzufügen oder eine Aufgabe aus einem Tracker-Ticket anlegen.",
|
|
4522
4522
|
"unavailable": "Für diese Installation ist kein Modell konfiguriert, daher kann der Assistent keine Anfrage lesen. Richten Sie einen Modellanbieter ein, um ihn zu aktivieren.",
|
|
4523
|
+
"reading": "Es wird geprüft, was der Assistent hier tun kann…",
|
|
4524
|
+
"unreadable": "Es konnte nicht geprüft werden, was der Assistent hier tun kann, daher ist das Anfragefeld vorerst ausgeblendet. Versuche es in einem Moment erneut.",
|
|
4525
|
+
"noActions": "Für den Assistenten dieser Installation sind keine Aktionen konfiguriert, daher kann er nichts ausführen. Konfigurieren Sie seine Aktionen, um ihn zu aktivieren.",
|
|
4523
4526
|
"placeholder": "z. B. der Checkout-Service hängt vom Payments-Service ab",
|
|
4524
4527
|
"submit": "Ausführen",
|
|
4525
4528
|
"submitHint": "Strg+Enter, auf dem Mac Cmd+Enter",
|
|
4529
|
+
"tooLong": "Diese Anfrage hat {length} Zeichen, der Assistent liest höchstens {limit}. Kürze sie und starte erneut.",
|
|
4526
4530
|
"examplesTitle": "Das können Sie fragen",
|
|
4527
4531
|
"showOnBoard": "Auf dem Board zeigen",
|
|
4528
4532
|
"declined": "Keine Aktion des Assistenten passt zu dieser Anfrage. Er kann eine Abhängigkeit zwischen zwei Services erklären, einen Service über eine Repository-URL hinzufügen oder eine Aufgabe aus einem Tracker-Ticket anlegen.",
|
package/i18n/locales/en.json
CHANGED
|
@@ -5216,9 +5216,16 @@
|
|
|
5216
5216
|
"title": "Assistant",
|
|
5217
5217
|
"intro": "Describe what you want done and the assistant performs it on the board: declare a dependency between two services, add a service from a repository URL, or file a task from a tracker issue.",
|
|
5218
5218
|
"unavailable": "No model is configured on this deployment, so the assistant cannot read a request. Configure a model provider to enable it.",
|
|
5219
|
+
"reading": "Checking what the assistant can do here…",
|
|
5220
|
+
"unreadable": "Could not check what the assistant can do here, so the request box is hidden for now. Try again in a moment.",
|
|
5221
|
+
"noActions": "This deployment's assistant has no actions configured, so there is nothing it can perform. Configure its actions to enable it.",
|
|
5222
|
+
"@noActions": {
|
|
5223
|
+
"description": "Shown in place of the request box when the deployment wired a model but registered no ACTIONS for the assistant to choose from: the operations it can perform on the board, not buttons in the interface. Distinct from the no-model case above."
|
|
5224
|
+
},
|
|
5219
5225
|
"placeholder": "e.g. the checkout service depends on the payments service",
|
|
5220
5226
|
"submit": "Run",
|
|
5221
5227
|
"submitHint": "Ctrl+Enter, or Cmd+Enter on a Mac",
|
|
5228
|
+
"tooLong": "That request is {length} characters and the assistant reads up to {limit}. Shorten it and run again.",
|
|
5222
5229
|
"examplesTitle": "What you can ask",
|
|
5223
5230
|
"showOnBoard": "Show on board",
|
|
5224
5231
|
"declined": "None of the assistant's actions match that request. It can declare a dependency between two services, add a service from a repository URL, or file a task from a tracker issue.",
|
package/i18n/locales/es.json
CHANGED
|
@@ -5031,9 +5031,13 @@
|
|
|
5031
5031
|
"title": "Asistente",
|
|
5032
5032
|
"intro": "Describe lo que quieres hacer y el asistente lo ejecuta en el tablero: declarar una dependencia entre dos servicios, añadir un servicio a partir de la URL de un repositorio o crear una tarea desde una incidencia del rastreador.",
|
|
5033
5033
|
"unavailable": "Este despliegue no tiene ningún modelo configurado, así que el asistente no puede leer una petición. Configura un proveedor de modelos para habilitarlo.",
|
|
5034
|
+
"reading": "Comprobando qué puede hacer el asistente aquí…",
|
|
5035
|
+
"unreadable": "No se pudo comprobar qué puede hacer el asistente aquí, así que por ahora el cuadro de solicitud está oculto. Vuelve a intentarlo en un momento.",
|
|
5036
|
+
"noActions": "El asistente de este despliegue no tiene ninguna acción configurada, así que no puede realizar nada. Configura sus acciones para habilitarlo.",
|
|
5034
5037
|
"placeholder": "p. ej. el servicio de checkout depende del servicio de pagos",
|
|
5035
5038
|
"submit": "Ejecutar",
|
|
5036
5039
|
"submitHint": "Ctrl+Intro, o Cmd+Intro en un Mac",
|
|
5040
|
+
"tooLong": "Esa solicitud tiene {length} caracteres y el asistente lee hasta {limit}. Acórtala y vuelve a ejecutarla.",
|
|
5037
5041
|
"examplesTitle": "Qué puedes pedir",
|
|
5038
5042
|
"showOnBoard": "Ver en el tablero",
|
|
5039
5043
|
"declined": "Ninguna acción del asistente encaja con esa petición. Puede declarar una dependencia entre dos servicios, añadir un servicio a partir de la URL de un repositorio o crear una tarea desde una incidencia del rastreador.",
|
package/i18n/locales/fr.json
CHANGED
|
@@ -5031,9 +5031,13 @@
|
|
|
5031
5031
|
"title": "Assistant",
|
|
5032
5032
|
"intro": "Décrivez ce que vous voulez faire et l'assistant l'exécute sur le tableau : déclarer une dépendance entre deux services, ajouter un service à partir de l'URL d'un dépôt, ou créer une tâche à partir d'un ticket.",
|
|
5033
5033
|
"unavailable": "Aucun modèle n'est configuré sur ce déploiement, l'assistant ne peut donc pas lire une demande. Configurez un fournisseur de modèles pour l'activer.",
|
|
5034
|
+
"reading": "Vérification de ce que l'assistant peut faire ici…",
|
|
5035
|
+
"unreadable": "Impossible de vérifier ce que l'assistant peut faire ici, le champ de demande est donc masqué pour l'instant. Réessayez dans un instant.",
|
|
5036
|
+
"noActions": "Aucune action n'est configurée pour l'assistant de ce déploiement, il ne peut donc rien exécuter. Configurez ses actions pour l'activer.",
|
|
5034
5037
|
"placeholder": "ex. le service checkout dépend du service paiements",
|
|
5035
5038
|
"submit": "Exécuter",
|
|
5036
5039
|
"submitHint": "Ctrl+Entrée, ou Cmd+Entrée sur un Mac",
|
|
5040
|
+
"tooLong": "Cette demande fait {length} caractères et l'assistant en lit {limit} au maximum. Raccourcissez-la puis relancez.",
|
|
5037
5041
|
"examplesTitle": "Ce que vous pouvez demander",
|
|
5038
5042
|
"showOnBoard": "Afficher sur le tableau",
|
|
5039
5043
|
"declined": "Aucune action de l'assistant ne correspond à cette demande. Il peut déclarer une dépendance entre deux services, ajouter un service à partir de l'URL d'un dépôt, ou créer une tâche à partir d'un ticket.",
|
package/i18n/locales/he.json
CHANGED
|
@@ -5031,9 +5031,13 @@
|
|
|
5031
5031
|
"title": "עוזר",
|
|
5032
5032
|
"intro": "תארו מה צריך לקרות והעוזר יבצע זאת על הלוח: להצהיר על תלות בין שני שירותים, להוסיף שירות מכתובת מאגר או לפתוח משימה מתקלה במעקב.",
|
|
5033
5033
|
"unavailable": "לא הוגדר מודל בפריסה הזו, לכן העוזר אינו יכול לקרוא בקשה. הגדירו ספק מודלים כדי להפעיל אותו.",
|
|
5034
|
+
"reading": "בודקים מה העוזר יכול לעשות כאן…",
|
|
5035
|
+
"unreadable": "לא הצלחנו לבדוק מה העוזר יכול לעשות כאן, ולכן שדה הבקשה מוסתר בשלב זה. נסו שוב בעוד רגע.",
|
|
5036
|
+
"noActions": "לא הוגדרו פעולות לעוזר בפריסה הזו, ולכן אין לו מה לבצע. הגדירו את הפעולות שלו כדי להפעיל אותו.",
|
|
5034
5037
|
"placeholder": "למשל: שירות ה-checkout תלוי בשירות התשלומים",
|
|
5035
5038
|
"submit": "הרץ",
|
|
5036
5039
|
"submitHint": "Ctrl+Enter, וב-Mac Cmd+Enter",
|
|
5040
|
+
"tooLong": "הבקשה הזאת באורך {length} תווים, והעוזר קורא עד {limit}. קצרו אותה והפעילו שוב.",
|
|
5037
5041
|
"examplesTitle": "מה אפשר לבקש",
|
|
5038
5042
|
"showOnBoard": "הצג על הלוח",
|
|
5039
5043
|
"declined": "אף פעולה של העוזר אינה מתאימה לבקשה הזו. הוא יכול להצהיר על תלות בין שני שירותים, להוסיף שירות מכתובת מאגר או לפתוח משימה מתקלה במעקב.",
|
package/i18n/locales/it.json
CHANGED
|
@@ -4520,9 +4520,13 @@
|
|
|
4520
4520
|
"title": "Assistente",
|
|
4521
4521
|
"intro": "Descrivi cosa vuoi fare e l'assistente lo esegue sulla bacheca: dichiarare una dipendenza fra due servizi, aggiungere un servizio dall'URL di un repository o creare un'attività da una segnalazione del tracker.",
|
|
4522
4522
|
"unavailable": "Questo deployment non ha un modello configurato, quindi l'assistente non può leggere una richiesta. Configura un provider di modelli per abilitarlo.",
|
|
4523
|
+
"reading": "Verifica di ciò che l'assistente può fare qui…",
|
|
4524
|
+
"unreadable": "Non è stato possibile verificare ciò che l'assistente può fare qui, quindi per ora il campo della richiesta è nascosto. Riprova tra un momento.",
|
|
4525
|
+
"noActions": "L'assistente di questo deployment non ha azioni configurate, quindi non può eseguire nulla. Configura le sue azioni per abilitarlo.",
|
|
4523
4526
|
"placeholder": "es. il servizio checkout dipende dal servizio pagamenti",
|
|
4524
4527
|
"submit": "Esegui",
|
|
4525
4528
|
"submitHint": "Ctrl+Invio, oppure Cmd+Invio su Mac",
|
|
4529
|
+
"tooLong": "Questa richiesta ha {length} caratteri e l'assistente ne legge al massimo {limit}. Accorciala ed esegui di nuovo.",
|
|
4526
4530
|
"examplesTitle": "Cosa puoi chiedere",
|
|
4527
4531
|
"showOnBoard": "Mostra sulla bacheca",
|
|
4528
4532
|
"declined": "Nessuna azione dell'assistente corrisponde a questa richiesta. Può dichiarare una dipendenza fra due servizi, aggiungere un servizio dall'URL di un repository o creare un'attività da una segnalazione del tracker.",
|
package/i18n/locales/ja.json
CHANGED
|
@@ -5031,9 +5031,13 @@
|
|
|
5031
5031
|
"title": "アシスタント",
|
|
5032
5032
|
"intro": "やりたいことを書くと、アシスタントがボード上で実行します。サービス間の依存関係の宣言、リポジトリ URL からのサービス追加、トラッカーのイシューからのタスク作成ができます。",
|
|
5033
5033
|
"unavailable": "このデプロイメントにはモデルが設定されていないため、アシスタントは依頼を読み取れません。モデルプロバイダーを設定して有効にしてください。",
|
|
5034
|
+
"reading": "ここでアシスタントに何ができるかを確認しています…",
|
|
5035
|
+
"unreadable": "ここでアシスタントに何ができるかを確認できなかったため、入力欄はいまは表示されません。少し待ってからもう一度お試しください。",
|
|
5036
|
+
"noActions": "このデプロイメントのアシスタントにはアクションが設定されていないため、実行できる操作がありません。アクションを設定して有効にしてください。",
|
|
5034
5037
|
"placeholder": "例: checkout サービスは payments サービスに依存している",
|
|
5035
5038
|
"submit": "実行",
|
|
5036
5039
|
"submitHint": "Ctrl+Enter、Mac では Cmd+Enter",
|
|
5040
|
+
"tooLong": "このリクエストは {length} 文字ですが、アシスタントが読めるのは {limit} 文字までです。短くしてから実行してください。",
|
|
5037
5041
|
"examplesTitle": "依頼できること",
|
|
5038
5042
|
"showOnBoard": "ボードで表示",
|
|
5039
5043
|
"declined": "この依頼に合う操作はありません。アシスタントができるのは、2 つのサービス間の依存関係の宣言、リポジトリ URL からのサービス追加、トラッカーのイシューからのタスク作成です。",
|
package/i18n/locales/pl.json
CHANGED
|
@@ -5031,9 +5031,13 @@
|
|
|
5031
5031
|
"title": "Asystent",
|
|
5032
5032
|
"intro": "Opisz, co ma zostać zrobione, a asystent wykona to na tablicy: zadeklaruje zależność między dwiema usługami, doda usługę z adresu repozytorium albo utworzy zadanie ze zgłoszenia w trackerze.",
|
|
5033
5033
|
"unavailable": "W tym wdrożeniu nie skonfigurowano żadnego modelu, więc asystent nie może odczytać prośby. Skonfiguruj dostawcę modeli, aby go włączyć.",
|
|
5034
|
+
"reading": "Sprawdzamy, co asystent może tu zrobić…",
|
|
5035
|
+
"unreadable": "Nie udało się sprawdzić, co asystent może tu zrobić, więc pole żądania jest na razie ukryte. Spróbuj ponownie za chwilę.",
|
|
5036
|
+
"noActions": "Asystent w tym wdrożeniu nie ma skonfigurowanych żadnych akcji, więc nie może nic wykonać. Skonfiguruj jego akcje, aby go włączyć.",
|
|
5034
5037
|
"placeholder": "np. usługa checkout zależy od usługi płatności",
|
|
5035
5038
|
"submit": "Uruchom",
|
|
5036
5039
|
"submitHint": "Ctrl+Enter, na Macu Cmd+Enter",
|
|
5040
|
+
"tooLong": "To żądanie ma {length} znaków, a asystent czyta najwyżej {limit}. Skróć je i uruchom ponownie.",
|
|
5037
5041
|
"examplesTitle": "O co możesz poprosić",
|
|
5038
5042
|
"showOnBoard": "Pokaż na tablicy",
|
|
5039
5043
|
"declined": "Żadne działanie asystenta nie pasuje do tej prośby. Może zadeklarować zależność między dwiema usługami, dodać usługę z adresu repozytorium albo utworzyć zadanie ze zgłoszenia w trackerze.",
|
package/i18n/locales/tr.json
CHANGED
|
@@ -5031,9 +5031,13 @@
|
|
|
5031
5031
|
"title": "Asistan",
|
|
5032
5032
|
"intro": "Ne yapılmasını istediğinizi yazın, asistan bunu panoda gerçekleştirsin: iki servis arasında bağımlılık tanımlamak, bir depo adresinden servis eklemek ya da bir izleyici kaydından görev oluşturmak.",
|
|
5033
5033
|
"unavailable": "Bu kurulumda yapılandırılmış bir model yok, bu yüzden asistan isteği okuyamıyor. Etkinleştirmek için bir model sağlayıcısı yapılandırın.",
|
|
5034
|
+
"reading": "Asistanın burada neler yapabileceği denetleniyor…",
|
|
5035
|
+
"unreadable": "Asistanın burada neler yapabileceği denetlenemedi, bu yüzden istek alanı şimdilik gizli. Birazdan yeniden deneyin.",
|
|
5036
|
+
"noActions": "Bu kurulumdaki asistan için yapılandırılmış bir eylem yok, bu yüzden hiçbir şey gerçekleştiremiyor. Etkinleştirmek için eylemlerini yapılandırın.",
|
|
5034
5037
|
"placeholder": "örn. checkout servisi ödemeler servisine bağlıdır",
|
|
5035
5038
|
"submit": "Çalıştır",
|
|
5036
5039
|
"submitHint": "Ctrl+Enter, Mac'te Cmd+Enter",
|
|
5040
|
+
"tooLong": "Bu istek {length} karakter, asistan ise en fazla {limit} karakter okuyor. Kısaltıp yeniden çalıştırın.",
|
|
5037
5041
|
"examplesTitle": "Neler isteyebilirsiniz",
|
|
5038
5042
|
"showOnBoard": "Panoda göster",
|
|
5039
5043
|
"declined": "Asistanın hiçbir eylemi bu istekle eşleşmiyor. İki servis arasında bağımlılık tanımlayabilir, bir depo adresinden servis ekleyebilir ya da bir izleyici kaydından görev oluşturabilir.",
|
package/i18n/locales/uk.json
CHANGED
|
@@ -5031,9 +5031,13 @@
|
|
|
5031
5031
|
"title": "Асистент",
|
|
5032
5032
|
"intro": "Опишіть, що потрібно зробити, і асистент виконає це на дошці: оголосить залежність між двома сервісами, додасть сервіс за адресою репозиторію або створить завдання з тікета трекера.",
|
|
5033
5033
|
"unavailable": "У цьому розгортанні не налаштовано жодної моделі, тому асистент не може прочитати запит. Налаштуйте постачальника моделей, щоб увімкнути його.",
|
|
5034
|
+
"reading": "Перевіряємо, що асистент може зробити тут…",
|
|
5035
|
+
"unreadable": "Не вдалося перевірити, що асистент може зробити тут, тож поле запиту поки приховано. Спробуйте ще раз за мить.",
|
|
5036
|
+
"noActions": "Для асистента в цьому розгортанні не налаштовано жодної дії, тому він не може нічого виконати. Налаштуйте його дії, щоб увімкнути його.",
|
|
5034
5037
|
"placeholder": "напр. сервіс checkout залежить від сервісу платежів",
|
|
5035
5038
|
"submit": "Виконати",
|
|
5036
5039
|
"submitHint": "Ctrl+Enter, на Mac Cmd+Enter",
|
|
5040
|
+
"tooLong": "Цей запит містить {length} символів, а асистент читає щонайбільше {limit}. Скоротіть його та запустіть знову.",
|
|
5037
5041
|
"examplesTitle": "Про що можна попросити",
|
|
5038
5042
|
"showOnBoard": "Показати на дошці",
|
|
5039
5043
|
"declined": "Жодна дія асистента не відповідає цьому запиту. Він може оголосити залежність між двома сервісами, додати сервіс за адресою репозиторію або створити завдання з тікета трекера.",
|