@cat-factory/app 0.311.3 → 0.312.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.
@@ -0,0 +1,148 @@
1
+ import { defineStore } from 'pinia'
2
+ import { computed, ref, watch } from 'vue'
3
+ import type { Block } from '~/types/domain'
4
+ import { useTaskLaneClassifier, type ClassifiedEntry } from '~/composables/useTaskLaneClassifier'
5
+ import {
6
+ assembleQueue,
7
+ QUEUE_SECTION_BY_REASON,
8
+ type QueueAssembly,
9
+ type QueueEntry,
10
+ } from '~/utils/queueSections'
11
+
12
+ /**
13
+ * Every task on the board, across every service, classified ONCE and filed into queue sections.
14
+ *
15
+ * The store-facing half of `utils/queueSections.ts`, and a STORE rather than a composable for the
16
+ * reason the swimlane README gives: a workspace-wide derivation belongs on one owner, never on each
17
+ * consumer. The queue view, the sidebar's attention counter AND every frame's swimlanes
18
+ * (`useFrameLanes`) read the one classification here, so a board pays for each task's verdict once
19
+ * per change, however many surfaces show it.
20
+ *
21
+ * It walks the service frames rather than `board.allTasks` so it holds exactly the tasks the
22
+ * frames' own swimlanes hold (direct tasks plus those inside modules): a task the canvas shows
23
+ * nowhere is not one the queue may claim needs you.
24
+ */
25
+ export const useWorkspaceQueueStore = defineStore('workspaceQueue', () => {
26
+ const board = useBoardStore()
27
+ const execution = useExecutionStore()
28
+ const workspace = useWorkspaceStore()
29
+ const settings = useWorkspaceSettingsStore()
30
+ const reviews = useReviewStage()
31
+ const { classify } = useTaskLaneClassifier()
32
+
33
+ /**
34
+ * Each task's lane verdict, keyed by id. `order` is the task's position in its OWN frame, which
35
+ * is the tiebreak the frame's lanes sort by.
36
+ */
37
+ const classified = computed(() => {
38
+ const byId = new Map<string, ClassifiedEntry>()
39
+ for (const frame of board.frames) {
40
+ board
41
+ .allTasksUnder(frame.id)
42
+ .forEach((task, order) => byId.set(task.id, classify(task, order)))
43
+ }
44
+ return byId
45
+ })
46
+
47
+ /** Narrow the queue to one service; null means all of them. Session-only, like a search box. */
48
+ const serviceFilter = ref<string | null>(null)
49
+ // A filter belongs to the board it was set on: another board's service id matches nothing there.
50
+ watch(
51
+ () => workspace.workspaceId,
52
+ () => (serviceFilter.value = null),
53
+ )
54
+
55
+ /** The service frames, for the filter. Board order, which is the order the canvas lays out. */
56
+ const services = computed(() => board.frames.map((f) => ({ id: f.id, title: f.title })))
57
+
58
+ /** The filter in force: a service deleted since it was picked narrows to nothing, so it lapses. */
59
+ const activeFilter = computed(() =>
60
+ serviceFilter.value && services.value.some((s) => s.id === serviceFilter.value)
61
+ ? serviceFilter.value
62
+ : null,
63
+ )
64
+
65
+ /** Show every service. The surfaces that send a person to the queue call it, so a filter set
66
+ * earlier cannot hide the work they were sent to see. */
67
+ function clearFilter() {
68
+ serviceFilter.value = null
69
+ }
70
+
71
+ /**
72
+ * A decision or approval on a block that is not a task in a service: an initiative's planning
73
+ * run, or a frame-level run. The swimlanes have no lane for it, but it waits on a person all the
74
+ * same, and the toolbar's decision count includes it, so the queue lists it under Needs you.
75
+ */
76
+ const parkedElsewhere = computed<QueueEntry[]>(() => {
77
+ const ids = new Set<string>()
78
+ for (const d of execution.openDecisions) ids.add(d.blockId)
79
+ for (const a of execution.openApprovals) {
80
+ if (!reviews.isBackground(a.agentKind, a.blockId)) ids.add(a.blockId)
81
+ }
82
+ const out: QueueEntry[] = []
83
+ for (const id of ids) {
84
+ if (classified.value.has(id)) continue
85
+ const block = board.getBlock(id)
86
+ if (!block) continue
87
+ out.push(parkEntry(block, out.length))
88
+ }
89
+ return out
90
+ })
91
+
92
+ function parkEntry(block: Block, order: number): QueueEntry {
93
+ const { entry } = classify(block, order)
94
+ const service = board.serviceOf(block)
95
+ return {
96
+ ...entry,
97
+ reason: execution.decisionsByBlock.has(block.id) ? 'decision' : 'approval',
98
+ service: service ? { id: service.id, title: service.title } : null,
99
+ }
100
+ }
101
+
102
+ /** Everything the queue lists, before the filter. The attention counter reads this, unfiltered. */
103
+ const entries = computed<QueueEntry[]>(() => {
104
+ const out: QueueEntry[] = []
105
+ for (const frame of board.frames) {
106
+ const service = { id: frame.id, title: frame.title }
107
+ // `classified` walks these same frames and tasks, so every one of them has a verdict.
108
+ for (const task of board.allTasksUnder(frame.id)) {
109
+ out.push({ ...classified.value.get(task.id)!.entry, service })
110
+ }
111
+ }
112
+ return out.concat(parkedElsewhere.value)
113
+ })
114
+
115
+ // `Date.now()` is read non-reactively, as the frame's Done lane does: the window is
116
+ // re-evaluated on every board or run change, which on a live board is constantly.
117
+ const queue = computed<QueueAssembly>(() =>
118
+ assembleQueue(
119
+ activeFilter.value
120
+ ? entries.value.filter((e) => e.service?.id === activeFilter.value)
121
+ : entries.value,
122
+ Date.now(),
123
+ {
124
+ maxItems: settings.settings.doneLaneMaxItems,
125
+ retentionDays: settings.settings.doneLaneRetentionDays,
126
+ },
127
+ ),
128
+ )
129
+
130
+ /**
131
+ * The ONE attention count the queue view shows: what a human must act on before an agent can
132
+ * continue, across every service and regardless of the filter. A pull request waiting to merge
133
+ * is not in it, because nothing is stalled behind it (see `utils/queueSections.ts`).
134
+ */
135
+ const needsYouCount = computed(
136
+ () => entries.value.filter((e) => QUEUE_SECTION_BY_REASON[e.reason] === 'needs_you').length,
137
+ )
138
+
139
+ return {
140
+ classified,
141
+ serviceFilter,
142
+ activeFilter,
143
+ clearFilter,
144
+ services,
145
+ queue,
146
+ needsYouCount,
147
+ }
148
+ })
@@ -0,0 +1,111 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import type { Block } from '~/types/domain'
3
+ import { LANE_BY_REASON, LANE_REASONS, type LaneReason } from '~/utils/swimlanes'
4
+ import { assembleQueue, QUEUE_SECTION_BY_REASON, type QueueEntry } from '~/utils/queueSections'
5
+ import type { DoneLaneCaps } from '~/utils/swimlanes'
6
+
7
+ // The queue reads the frame lanes' verdict and must never contradict it. The one place it
8
+ // splits differently (a PR waiting to merge gets its own column) is a regrouping INSIDE the
9
+ // frame's `needs_you` lane, never a move across lanes.
10
+
11
+ const NOW = 1_800_000_000_000
12
+ const DAY = 86_400_000
13
+ /** The caps a workspace sets for its frames' Done lane, which the queue applies too. */
14
+ const CAPS: DoneLaneCaps = { maxItems: 20, retentionDays: 14 }
15
+
16
+ function entry(id: string, reason: LaneReason, overrides: Partial<QueueEntry> = {}): QueueEntry {
17
+ return {
18
+ task: { id, title: id, status: 'ready', level: 'task' } as Block,
19
+ reason,
20
+ order: 0,
21
+ activityAt: null,
22
+ waitingSince: null,
23
+ moduleName: null,
24
+ initiativeName: null,
25
+ epicName: null,
26
+ service: { id: 'svc', title: 'Service' },
27
+ ...overrides,
28
+ }
29
+ }
30
+
31
+ describe('QUEUE_SECTION_BY_REASON', () => {
32
+ it('files every reason, and never across the lane the frame put it in', () => {
33
+ const laneOfSection = {
34
+ needs_you: 'needs_you',
35
+ ready_to_merge: 'needs_you',
36
+ in_flight: 'in_progress',
37
+ waiting: 'not_started',
38
+ done: 'done',
39
+ } as const
40
+ for (const reason of LANE_REASONS) {
41
+ expect(laneOfSection[QUEUE_SECTION_BY_REASON[reason]], reason).toBe(LANE_BY_REASON[reason])
42
+ }
43
+ })
44
+ })
45
+
46
+ describe('assembleQueue', () => {
47
+ it('puts a decision, a failure and a parked run in Needs you, and a waiting PR beside it', () => {
48
+ const { sections } = assembleQueue(
49
+ [
50
+ entry('decide', 'decision'),
51
+ entry('broken', 'failed'),
52
+ entry('parked', 'parked'),
53
+ entry('pr', 'pr_awaiting_merge'),
54
+ ],
55
+ NOW,
56
+ CAPS,
57
+ )
58
+ expect(sections.needs_you.map((e) => e.task.id).sort()).toEqual(['broken', 'decide', 'parked'])
59
+ expect(sections.ready_to_merge.map((e) => e.task.id)).toEqual(['pr'])
60
+ })
61
+
62
+ it('orders Needs you as the frame lane does: broken first, then the longest wait', () => {
63
+ const { sections } = assembleQueue(
64
+ [
65
+ entry('recent', 'decision', { waitingSince: NOW - 1000, order: 0 }),
66
+ entry('old', 'decision', { waitingSince: NOW - 5000, order: 1 }),
67
+ entry('broken', 'failed', { order: 2 }),
68
+ ],
69
+ NOW,
70
+ CAPS,
71
+ )
72
+ expect(sections.needs_you.map((e) => e.task.id)).toEqual(['broken', 'old', 'recent'])
73
+ })
74
+
75
+ it("applies the frames' Done-lane window: newest first, undated last, older ones left out", () => {
76
+ const merged = (id: string, completedAt: number | undefined) =>
77
+ entry(id, 'merged', {
78
+ task: { id, title: id, status: 'done', level: 'task', completedAt } as Block,
79
+ })
80
+ const queue = assembleQueue(
81
+ [
82
+ merged('yesterday', NOW - DAY),
83
+ merged('today', NOW - 1000),
84
+ merged('ancient', NOW - (CAPS.retentionDays! + 1) * DAY),
85
+ merged('undated', undefined),
86
+ ],
87
+ NOW,
88
+ CAPS,
89
+ )
90
+ expect(queue.sections.done.map((e) => e.task.id)).toEqual(['today', 'yesterday', 'undated'])
91
+ expect(queue.doneInWindow).toBe(3)
92
+ })
93
+
94
+ it('caps the Done list but never the count above it', () => {
95
+ const many = Array.from({ length: CAPS.maxItems + 3 }, (_, i) =>
96
+ entry(`m${i}`, 'merged', {
97
+ order: i,
98
+ task: {
99
+ id: `m${i}`,
100
+ title: '',
101
+ status: 'done',
102
+ level: 'task',
103
+ completedAt: NOW - i,
104
+ } as Block,
105
+ }),
106
+ )
107
+ const queue = assembleQueue(many, NOW, CAPS)
108
+ expect(queue.sections.done).toHaveLength(CAPS.maxItems)
109
+ expect(queue.doneInWindow).toBe(many.length)
110
+ })
111
+ })
@@ -0,0 +1,117 @@
1
+ import type { Block } from '~/types/domain'
2
+ import { sortLaneTasks, type LaneTaskEntry } from '~/utils/laneSort'
3
+ import {
4
+ selectDoneLaneTasks,
5
+ type DoneLaneCaps,
6
+ type LaneReason,
7
+ type TaskLane,
8
+ } from '~/utils/swimlanes'
9
+
10
+ // ---------------------------------------------------------------------------
11
+ // The workspace queue: the swimlane model lifted out of the service frames.
12
+ //
13
+ // A frame's lanes answer "what is the state of THIS service". The queue answers "what needs
14
+ // me, across every service", which is the question a person asks first when they open the
15
+ // app. It reads the SAME `classifyTask` verdict the frame does (see
16
+ // `useTaskLaneClassifier`), so a task never sits in one column here and another lane there.
17
+ //
18
+ // It splits the verdict differently in one place, on purpose: a pull request waiting to be
19
+ // merged is in the frame's `needs_you` lane, and here it has its own column. Answering a
20
+ // decision and landing a finished change are different jobs done at different moments (one
21
+ // unblocks an agent within minutes, the other is a review session), and a single column that
22
+ // mixed them would put "merge this" between two questions an agent is parked on.
23
+ // ---------------------------------------------------------------------------
24
+
25
+ /** The queue's sections, in reading order. */
26
+ export const QUEUE_SECTIONS = [
27
+ 'needs_you',
28
+ 'in_flight',
29
+ 'waiting',
30
+ 'ready_to_merge',
31
+ 'done',
32
+ ] as const
33
+ export type QueueSection = (typeof QUEUE_SECTIONS)[number]
34
+
35
+ /**
36
+ * Each lane reason's queue section. Exhaustive, so a new reason cannot ship without deciding
37
+ * where the queue files it.
38
+ */
39
+ export const QUEUE_SECTION_BY_REASON: Record<LaneReason, QueueSection> = {
40
+ unstarted: 'waiting',
41
+ dependencies: 'waiting',
42
+ running: 'in_flight',
43
+ background_review: 'in_flight',
44
+ decision: 'needs_you',
45
+ approval: 'needs_you',
46
+ failed: 'needs_you',
47
+ budget_paused: 'needs_you',
48
+ parked: 'needs_you',
49
+ unclassified: 'needs_you',
50
+ pr_awaiting_merge: 'ready_to_merge',
51
+ merged: 'done',
52
+ }
53
+
54
+ /**
55
+ * The frame lane whose `smart` order each section borrows. The queue adds no comparator of its
56
+ * own: an order that differed from the frame's would make the two surfaces disagree about what
57
+ * to look at first. Done takes the Done lane's own selection and order (below).
58
+ */
59
+ const SORT_LANE_BY_SECTION: Record<Exclude<QueueSection, 'done'>, TaskLane> = {
60
+ needs_you: 'needs_you',
61
+ in_flight: 'in_progress',
62
+ waiting: 'not_started',
63
+ ready_to_merge: 'needs_you',
64
+ }
65
+
66
+ /** One queue entry: the lane entry plus the service the task belongs to. */
67
+ export interface QueueEntry extends LaneTaskEntry {
68
+ /** The enclosing service frame, or null for a task the board holds outside any service. */
69
+ readonly service: Pick<Block, 'id' | 'title'> | null
70
+ }
71
+
72
+ /** The assembled queue. */
73
+ export interface QueueAssembly {
74
+ readonly sections: Record<QueueSection, QueueEntry[]>
75
+ /** Every merged task inside the retention window, before the item cap. */
76
+ readonly doneInWindow: number
77
+ }
78
+
79
+ /**
80
+ * Bucket and order classified entries into queue sections. Pure, so the ordering rules are
81
+ * testable without a Pinia instance.
82
+ *
83
+ * The Done section is the frames' Done lane over every service: the same selection
84
+ * (`selectDoneLaneTasks`) under the same workspace caps, so the queue and a frame never disagree
85
+ * about which merges are recent.
86
+ */
87
+ export function assembleQueue(
88
+ entries: readonly QueueEntry[],
89
+ now: number,
90
+ doneCaps: DoneLaneCaps,
91
+ ): QueueAssembly {
92
+ const sections = Object.fromEntries(QUEUE_SECTIONS.map((s) => [s, [] as QueueEntry[]])) as Record<
93
+ QueueSection,
94
+ QueueEntry[]
95
+ >
96
+ for (const entry of entries) sections[QUEUE_SECTION_BY_REASON[entry.reason]].push(entry)
97
+
98
+ const byId = new Map(sections.done.map((e) => [e.task.id, e]))
99
+ const done = selectDoneLaneTasks(
100
+ sections.done.map((e) => e.task),
101
+ doneCaps,
102
+ now,
103
+ )
104
+
105
+ for (const section of QUEUE_SECTIONS) {
106
+ if (section === 'done') continue
107
+ sections[section] = sortLaneTasks(
108
+ sections[section],
109
+ 'smart',
110
+ SORT_LANE_BY_SECTION[section],
111
+ ) as QueueEntry[]
112
+ }
113
+ // Newest completion first, as the frame's Done lane orders it.
114
+ sections.done = done.shown.map((task) => byId.get(task.id)!)
115
+
116
+ return { sections, doneInWindow: done.shown.length + done.hiddenByCap }
117
+ }
@@ -0,0 +1,16 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import { DEFAULT_WORK_VIEW, isWorkView } from '~/utils/workView'
3
+
4
+ describe('isWorkView', () => {
5
+ it('accepts the two views and nothing else', () => {
6
+ expect(isWorkView('queue')).toBe(true)
7
+ expect(isWorkView('board')).toBe(true)
8
+ // A retired or hand-edited persisted value is not a view, so the store falls back.
9
+ expect(isWorkView('setup')).toBe(false)
10
+ expect(isWorkView(null)).toBe(false)
11
+ })
12
+
13
+ it('lands a person who has not chosen on the queue', () => {
14
+ expect(DEFAULT_WORK_VIEW).toBe('queue')
15
+ })
16
+ })
@@ -0,0 +1,25 @@
1
+ // ---------------------------------------------------------------------------
2
+ // The two views of a board (issue #2258): the QUEUE (every task across every service, in the
3
+ // column that says what it needs) and the BOARD (the canvas, where the work is planned).
4
+ //
5
+ // They are two views of one board, not two products: the queue reads the same lane verdict the
6
+ // canvas's swimlanes do. Which one a person opens on is THEIR choice, remembered as the view they
7
+ // used last, because the jobs differ: someone who reviews all day wants the list, someone who
8
+ // plans the next service wants the map.
9
+ // ---------------------------------------------------------------------------
10
+
11
+ export const WORK_VIEWS = ['queue', 'board'] as const
12
+ export type WorkView = (typeof WORK_VIEWS)[number]
13
+
14
+ /**
15
+ * Where a person lands before they have chosen. The queue: it answers the first question a
16
+ * person opens the app with ("what needs me?"), and the canvas is one click away.
17
+ */
18
+ export const DEFAULT_WORK_VIEW: WorkView = 'queue'
19
+
20
+ export function isWorkView(value: unknown): value is WorkView {
21
+ return typeof value === 'string' && (WORK_VIEWS as readonly string[]).includes(value)
22
+ }
23
+
24
+ /** The query parameter that opens a view, so a link can say "look at the queue". */
25
+ export const WORK_VIEW_PARAM = 'view'
@@ -2773,7 +2773,9 @@
2773
2773
  "toggleUiMode": "Oberflächenmodus wechseln",
2774
2774
  "chooseRole": "Rolle wechseln",
2775
2775
  "foundationalServices": "Basisdienste",
2776
- "notificationSettings": "Benachrichtigungs-Routing verwalten"
2776
+ "notificationSettings": "Benachrichtigungs-Routing verwalten",
2777
+ "queueView": "Warteschlangen-Ansicht öffnen",
2778
+ "boardView": "Board-Ansicht öffnen"
2777
2779
  },
2778
2780
  "keywords": {
2779
2781
  "newPipeline": "pipeline agents chain",
@@ -2801,7 +2803,9 @@
2801
2803
  "chooseRole": "rolle entwickler produktmanager designer persona einfachere ansicht",
2802
2804
  "tutorial": "tutorial tour einführung hilfe onboarding lernen grundlagen",
2803
2805
  "foundationalServices": "gemeinsame Fähigkeit Plattform Dienst API Vertrag OpenAPI Katalog",
2804
- "notificationSettings": "benachrichtigungen e-mail routing kanäle posteingang"
2806
+ "notificationSettings": "benachrichtigungen e-mail routing kanäle posteingang",
2807
+ "queueView": "warteschlange ansicht wartet auf dich liste",
2808
+ "boardView": "board canvas karte ansicht planen"
2805
2809
  }
2806
2810
  },
2807
2811
  "shortcuts": {
@@ -8115,6 +8119,10 @@
8115
8119
  "title": "Das Board",
8116
8120
  "body": "Services, Module und Aufgaben liegen auf dieser Fläche. Ziehe zum Verschieben, scrolle zum Zoomen und klicke auf eine Karte, um Details zu sehen."
8117
8121
  },
8122
+ "views": {
8123
+ "title": "Zwei Ansichten",
8124
+ "body": "Das Board hat eine zweite Ansicht. Die Warteschlange zeigt, was über alle Services hinweg auf dich wartet. Nach der Tour wechselst du hier zwischen der Warteschlange und dieser Arbeitsfläche. Die App öffnet die Ansicht, die du zuletzt genutzt hast."
8125
+ },
8118
8126
  "sidebar": {
8119
8127
  "title": "Die Seitenleiste",
8120
8128
  "body": "Alles Weitere findest du hier: Pipelines erstellen, Repositories und Integrationen verbinden, Workspace-Einstellungen."
@@ -8734,5 +8742,56 @@
8734
8742
  "unsupportedVersion": "Dieses Design stammt aus einem neueren Editor-Format, als diese App versteht."
8735
8743
  }
8736
8744
  }
8745
+ },
8746
+ "queue": {
8747
+ "title": "Warteschlange",
8748
+ "summary": "{needsYou} warten auf dich · {ready} bereit zum Mergen",
8749
+ "filter": {
8750
+ "allServices": "Alle Services",
8751
+ "service": "Nach Service filtern"
8752
+ },
8753
+ "columns": {
8754
+ "needsYou": {
8755
+ "label": "Wartet auf dich",
8756
+ "empty": "Nichts wartet auf dich."
8757
+ },
8758
+ "inFlight": {
8759
+ "label": "In Arbeit",
8760
+ "empty": "Derzeit arbeitet kein Agent."
8761
+ },
8762
+ "readyToMerge": {
8763
+ "label": "Bereit zum Mergen",
8764
+ "empty": "Kein Pull Request wartet auf den Merge."
8765
+ }
8766
+ },
8767
+ "waiting": {
8768
+ "label": "Nicht begonnen ({count})",
8769
+ "empty": "Nichts wartet auf den Start."
8770
+ },
8771
+ "done": {
8772
+ "count": "Seit {date} gemergt: {count}",
8773
+ "countAll": "Gemergt: {count}",
8774
+ "show": "Anzeigen",
8775
+ "hide": "Ausblenden",
8776
+ "capped": "Zeigt die neuesten {shown} von {total}."
8777
+ },
8778
+ "card": {
8779
+ "waited": "wartete {duration}",
8780
+ "active": "vor {duration} aktiv",
8781
+ "merged": "vor {duration} gemergt",
8782
+ "step": "Schritt {current} von {total}",
8783
+ "showOnBoard": "Auf dem Board zeigen",
8784
+ "answer": "Antworten",
8785
+ "budgetPaused": "Das Ausgabenlimit hat diesen Durchlauf angehalten. Er läuft weiter, sobald das Limit erhöht wird.",
8786
+ "parked": "Der Durchlauf wartet auf einen Menschen. Öffne die Aufgabe, um zu sehen, was fehlt.",
8787
+ "waitingOn": "Wartet auf {deps}",
8788
+ "pr": "Pull Request #{number}"
8789
+ }
8790
+ },
8791
+ "views": {
8792
+ "title": "Ansichten",
8793
+ "queue": "Warteschlange",
8794
+ "board": "Board",
8795
+ "openQueue": "Warteschlangen-Ansicht öffnen"
8737
8796
  }
8738
8797
  }
@@ -2800,7 +2800,9 @@
2800
2800
  "toggleUiMode": "Switch interface mode",
2801
2801
  "chooseRole": "Change your role",
2802
2802
  "foundationalServices": "Foundational services",
2803
- "notificationSettings": "Manage notification routing"
2803
+ "notificationSettings": "Manage notification routing",
2804
+ "queueView": "Open the queue view",
2805
+ "boardView": "Open the board view"
2804
2806
  },
2805
2807
  "keywords": {
2806
2808
  "newPipeline": "pipeline agents chain",
@@ -2828,7 +2830,9 @@
2828
2830
  "chooseRole": "role engineer product manager designer persona simpler view",
2829
2831
  "tutorial": "tutorial onboarding tour guide help learn basics",
2830
2832
  "foundationalServices": "shared capability platform service api contract openapi catalog",
2831
- "notificationSettings": "notifications email routing channels inbox"
2833
+ "notificationSettings": "notifications email routing channels inbox",
2834
+ "queueView": "queue view needs you inbox list waiting",
2835
+ "boardView": "board canvas map view plan"
2832
2836
  }
2833
2837
  },
2834
2838
  "shortcuts": {
@@ -8434,6 +8438,10 @@
8434
8438
  "title": "The board",
8435
8439
  "body": "Services, modules, and tasks live on this canvas. Drag to pan, scroll to zoom, and click any card to inspect it."
8436
8440
  },
8441
+ "views": {
8442
+ "title": "Two views",
8443
+ "body": "The board has a second view. The queue lists what needs you across every service. Once the tour is over, switch between the queue and this canvas here. The app opens on the view you used last."
8444
+ },
8437
8445
  "sidebar": {
8438
8446
  "title": "The sidebar",
8439
8447
  "body": "Everything else lives here: building pipelines, connecting repositories and integrations, and workspace settings."
@@ -9065,5 +9073,56 @@
9065
9073
  "unsupportedVersion": "This theme was made with a newer editor format than this app understands."
9066
9074
  }
9067
9075
  }
9076
+ },
9077
+ "queue": {
9078
+ "title": "Queue",
9079
+ "summary": "{needsYou} need you · {ready} ready to merge",
9080
+ "filter": {
9081
+ "allServices": "All services",
9082
+ "service": "Filter by service"
9083
+ },
9084
+ "columns": {
9085
+ "needsYou": {
9086
+ "label": "Needs you",
9087
+ "empty": "Nothing waits on you."
9088
+ },
9089
+ "inFlight": {
9090
+ "label": "In flight",
9091
+ "empty": "No agent is working now."
9092
+ },
9093
+ "readyToMerge": {
9094
+ "label": "Ready to merge",
9095
+ "empty": "No pull request waits to merge."
9096
+ }
9097
+ },
9098
+ "waiting": {
9099
+ "label": "Not started ({count})",
9100
+ "empty": "Nothing waits to start."
9101
+ },
9102
+ "done": {
9103
+ "count": "Merged since {date}: {count}",
9104
+ "countAll": "Merged: {count}",
9105
+ "show": "Show",
9106
+ "hide": "Hide",
9107
+ "capped": "Shows the newest {shown} of {total}."
9108
+ },
9109
+ "card": {
9110
+ "waited": "waited {duration}",
9111
+ "active": "active {duration} ago",
9112
+ "merged": "merged {duration} ago",
9113
+ "step": "step {current} of {total}",
9114
+ "showOnBoard": "Show on the board",
9115
+ "answer": "Answer",
9116
+ "budgetPaused": "The spend limit paused this run. It continues when the limit goes up.",
9117
+ "parked": "The run waits for a person. Open the task to see what it needs.",
9118
+ "waitingOn": "Waits for {deps}",
9119
+ "pr": "Pull request #{number}"
9120
+ }
9121
+ },
9122
+ "views": {
9123
+ "title": "Views",
9124
+ "queue": "Queue",
9125
+ "board": "Board",
9126
+ "openQueue": "Open the queue view"
9068
9127
  }
9069
9128
  }