Skip to content

Commit 7f98a43

Browse files
committed
feat(board): a read-only Delivery view over the repository GitHub work (#1173)
The task dialog gains a Delivery tab beside Tasks. It is one snapshot of the repository, not of one Bot: issues by state label (agent-working, agent-ready, otherwise original), pull requests in review, and pull requests merged since the Asia/Shanghai day start in done today. The host module only reads: every request is a GET, the token stays in the process, and the payload carries no credential. Each URL is cached with its ETag and revalidated with If-None-Match, a 304 reuses the cached body, and a stale snapshot is served while a refresh runs. It installs no timer; the browser asks, so nothing refreshes with nobody looking. A pull request card carries the verdict badge of a Verdict: naming its own tip (an older tip is ignored and CI speaks), and the header reports today against the last 7 days, medians, open counts and CI as counted of total rather than pretending the capped list is complete. The Delivery view is repository-wide, so the per-Bot rail is not rendered. Tests: core/test/delivery-board.test.js (GET only, no token in the payload, columns and issue/PR linkage, badges by tip, Shanghai boundary and medians, cache and 304, no timer), core/test/delivery-view.test.js (labels, header rows, board render, honest empty state), and core/test/kanban.test.js (Delivery keeps no rail; Tasks is unchanged).
1 parent b15102a commit 7f98a43

27 files changed

Lines changed: 6441 additions & 4053 deletions
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
The task board dialog gains a **Delivery** view (#1173): one read-only snapshot of the repository's GitHub issues, pull requests and CI runs, in five columns — original issue, ready to start, in progress, in review, and done today — with a verdict/CI badge per pull request and a header that reports today against the last 7 days. It is repository-wide, so the per-Bot rail does not appear, and it never writes to GitHub: the host serves a cached snapshot built only from `GET` requests, and the token stays host-side. The view replaces the board with one honest line when no GitHub connection is set up.

‎core/client.js‎

Lines changed: 2052 additions & 1786 deletions
Large diffs are not rendered by default.

‎core/index.js‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ import * as update from './src/host/update.js'
4141
import * as dshUpdate from './src/host/dsh-update.js'
4242
import * as hooks from './src/host/hooks.js'
4343
import * as remoteChat from './src/host/remote-chat.js'
44+
import * as deliveryBoard from './src/host/delivery-board.js'
4445
import * as channel from './src/host/channel.js'
4546

4647
export { COLORS } from './src/host/constants.js'
@@ -52,7 +53,7 @@ export const inject = ['llm', 'tools', 'systemPrompt', 'sessionController', 'ses
5253
// the functions other modules call; they call them through `rt` at run time, never during
5354
// install. Listeners run and tools are listed in registration order, so this order holds:
5455
// usage meters model requests after every listener that rewrites them.
55-
const MODULES = [diagnostics, store, harness, memberAuth, authz, models, permissions, turns, delivery, roster, folders, agents, prompt, timeContext, tools, decisions, chatIndex, groups, roomMembership, reach, tasks, taskDrive, approvals, resume, memory, parts, images, secrets, connectors, connectorAccess, capabilities, schedules, automationDefault, browser, usage, update, dshUpdate, hooks, remoteChat, channel]
56+
const MODULES = [diagnostics, store, harness, memberAuth, authz, models, permissions, turns, delivery, roster, folders, agents, prompt, timeContext, tools, decisions, chatIndex, groups, roomMembership, reach, tasks, taskDrive, approvals, resume, memory, parts, images, secrets, connectors, connectorAccess, capabilities, schedules, automationDefault, browser, usage, update, dshUpdate, hooks, remoteChat, deliveryBoard, channel]
5657

5758
export function apply(ctx, config = {}) {
5859
const rt = createRuntime(ctx, config)

‎core/src/client/delivery-view.js‎

Lines changed: 197 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,197 @@
1+
// Delivery board (#1173): the repository's GitHub issues and pull requests by delivery stage,
2+
// in the same dialog as the per-Bot Tasks board. This is a view only — nothing here writes to
3+
// GitHub, and the snapshot it renders is built host-side (src/host/delivery-board.js).
4+
import { createElement as h, useEffect, useState } from 'react'
5+
import { t } from './i18n.js'
6+
7+
const POLL_MS = 60_000
8+
9+
export const DELIVERY_COLUMNS = [
10+
{ id: 'raw', label: 'Original issue' },
11+
{ id: 'ready', label: 'Ready to start' },
12+
{ id: 'working', label: 'In progress' },
13+
{ id: 'review', label: 'In review' },
14+
{ id: 'done-today', label: 'Done today' },
15+
]
16+
17+
const BADGES = {
18+
inc: 'INC · rework',
19+
'pass-enqueue': 'PASS · waiting for enqueue',
20+
'pass-merge': 'PASS · waiting for merge',
21+
'ci-red': 'CI red',
22+
'ci-running': 'CI running',
23+
'no-verdict': 'No verdict',
24+
}
25+
26+
export const badgeLabel = (badge) => (badge ? t(BADGES[badge] ?? badge) : '')
27+
28+
export function duration(ms) {
29+
if (!Number.isFinite(ms) || ms < 0) return '—'
30+
const hours = ms / 3_600_000
31+
if (hours < 1) return `${Math.max(1, Math.round(ms / 60_000))}m`
32+
if (hours < 48) return `${Math.round(hours)}h`
33+
return `${Math.round(hours / 24)}d`
34+
}
35+
36+
// The header rows the dialog renders; kept pure so the numbers can be checked without React.
37+
export function metricRows(snapshot) {
38+
const today = snapshot?.today ?? {}
39+
const week = snapshot?.week ?? {}
40+
const ci = today.ci ?? {}
41+
const count = (value) => (value === null || value === undefined ? '—' : String(value))
42+
const rows = [
43+
{ label: t('New issues'), key: 'newIssues' },
44+
{ label: t('Closed issues'), key: 'closedIssues' },
45+
{ label: t('New pull requests'), key: 'newPullRequests' },
46+
{ label: t('Merged pull requests'), key: 'mergedPullRequests' },
47+
{ label: t('Median issue close'), key: 'medianIssueCloseMs', format: duration },
48+
{ label: t('Median PR merge'), key: 'medianPullMergeMs', format: duration },
49+
].map(row => {
50+
const format = row.format ?? count
51+
return { label: row.label, today: format(today[row.key]), week: format(week[row.key]) }
52+
})
53+
return {
54+
dayStart: snapshot?.dayStart ?? null,
55+
rows,
56+
ci: {
57+
failed: ci.failed ?? 0,
58+
total: ci.total ?? 0,
59+
counted: ci.counted ?? 0,
60+
caveat: Number.isFinite(ci.total) && Number.isFinite(ci.counted) && ci.counted < ci.total,
61+
superseded: ci.superseded ?? 0,
62+
},
63+
open: snapshot?.open ?? {},
64+
}
65+
}
66+
67+
export function useDeliveryBoard(actions) {
68+
const [snapshot, setSnapshot] = useState(null)
69+
const [message, setMessage] = useState('')
70+
useEffect(() => {
71+
let stopped = false
72+
const load = () => {
73+
const job = Promise.resolve(actions?.deliveryBoard?.())
74+
void job.then((next) => {
75+
if (stopped || !next || typeof next !== 'object') return
76+
if (next.ok === false) {
77+
setMessage(next.reason === 'no-github' ? t('No GitHub connection yet; the board fills in once one is added.') : t('The delivery board is not ready yet.'))
78+
return
79+
}
80+
setSnapshot(next)
81+
setMessage(next.error ? t('Showing the last snapshot from {time}: {error}', { time: timeOf(next.asOf), error: next.error }) : '')
82+
}).catch((error) => { if (!stopped) setMessage(String(error?.message ?? error)) })
83+
}
84+
load()
85+
const timer = setInterval(load, POLL_MS)
86+
return () => { stopped = true; clearInterval(timer) }
87+
}, [actions])
88+
return { snapshot, message }
89+
}
90+
91+
const timeOf = (iso) => {
92+
const at = Date.parse(iso ?? '')
93+
if (!Number.isFinite(at)) return '—'
94+
return new Date(at).toLocaleString(undefined, { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' })
95+
}
96+
97+
function Label({ name }) {
98+
return h('span', { className: 'bt-del-label', 'data-label': name }, name)
99+
}
100+
101+
export function DeliveryCard({ card }) {
102+
const badge = badgeLabel(card?.badge)
103+
return h('article', { className: 'bt-del-card', 'data-kind': card?.kind, 'data-number': card?.number, 'data-badge': card?.badge ?? undefined },
104+
h('span', { className: 'bt-del-card-top' },
105+
h('span', { className: 'bt-del-id' }, `#${card?.number ?? '?'}`),
106+
badge ? h('span', { className: 'bt-del-badge', 'data-badge': card?.badge }, badge) : null),
107+
h('span', { className: 'bt-del-title' }, card?.title ?? ''),
108+
card?.labels?.length ? h('span', { className: 'bt-del-labels' }, card.labels.map(name => h(Label, { key: name, name }))) : null,
109+
card?.owner ? h('span', { className: 'bt-del-owner', 'data-kind': card.ownerKind ?? 'human' }, card.owner) : null,
110+
card?.linked ? h('span', { className: 'bt-del-linked' }, t('linked to #{number}', { number: card.linked })) : null,
111+
card?.sha ? h('span', { className: 'bt-del-sha' }, card.sha) : null,
112+
card?.url ? h('a', { className: 'bt-del-open', href: card.url, target: '_blank', rel: 'noreferrer' }, t('Open')) : null)
113+
}
114+
115+
export function DeliveryHeader({ snapshot }) {
116+
if (!snapshot) return null
117+
const { rows, ci, open } = metricRows(snapshot)
118+
return h('header', { className: 'bt-del-head' },
119+
h('div', { className: 'bt-del-metrics' },
120+
h('table', { className: 'bt-del-table' },
121+
h('thead', null, h('tr', null, h('th', null, ''), h('th', null, t('Today')), h('th', null, t('7 days')))),
122+
h('tbody', null, rows.map(row => h('tr', { key: row.label },
123+
h('th', { scope: 'row' }, row.label),
124+
h('td', null, row.today),
125+
h('td', null, row.week))))),
126+
h('p', { className: 'bt-del-ci' }, t('CI failed {failed} of {total}', { failed: ci.failed, total: ci.total }),
127+
ci.caveat ? ` · ${t('counted {counted} of {total}', { counted: ci.counted, total: ci.total })}` : '',
128+
ci.superseded ? ` · ${t('{count} superseded', { count: ci.superseded })}` : ''),
129+
h('p', { className: 'bt-del-open' }, t('Open: {issues} issues ({needs} need a human), {prs} pull requests', {
130+
issues: open.issues ?? 0, needs: open.needsHuman ?? 0, prs: open.pullRequests ?? 0,
131+
}), open.medianIssueAgeMs !== null && open.medianIssueAgeMs !== undefined
132+
? ` · ${t('median open age {age}', { age: duration(open.medianIssueAgeMs) })}` : '')),
133+
h('p', { className: 'bt-del-asof' }, t('as of {time}', { time: timeOf(snapshot.asOf) }),
134+
snapshot.refreshing ? ` · ${t('refreshing…')}` : ''))
135+
}
136+
137+
export function DeliveryBoard({ snapshot, message }) {
138+
if (!snapshot) {
139+
return h('div', { className: 'bt-del-body', 'data-view': 'delivery' },
140+
message
141+
? h('p', { className: 'bt-board-error', role: 'alert' }, message)
142+
: h('p', { className: 'bt-board-empty' }, t('No snapshot yet.')))
143+
}
144+
const columns = snapshot.columns ?? {}
145+
return h('div', { className: 'bt-del-body', 'data-view': 'delivery' },
146+
message ? h('p', { className: 'bt-board-error', role: 'alert' }, message) : null,
147+
h(DeliveryHeader, { snapshot: snapshot.metrics ? { ...snapshot.metrics, asOf: snapshot.asOf, refreshing: snapshot.refreshing } : null }),
148+
h('div', { className: 'bt-del-columns' },
149+
DELIVERY_COLUMNS.map(column => h('section', { key: column.id, className: 'bt-del-column', 'data-column': column.id },
150+
h('h3', { className: 'bt-board-column-head' },
151+
h('span', null, t(column.label)),
152+
h('span', { className: 'bt-board-count' }, String((columns[column.id] ?? []).length))),
153+
(columns[column.id] ?? []).length === 0
154+
? h('p', { className: 'bt-board-empty' }, t('Nothing here.'))
155+
: (columns[column.id] ?? []).map(card => h(DeliveryCard, { key: `${card.kind}-${card.number}`, card }))))))
156+
}
157+
158+
export function DeliveryView({ actions }) {
159+
const { snapshot, message } = useDeliveryBoard(actions)
160+
return h(DeliveryBoard, { snapshot, message })
161+
}
162+
163+
export const DELIVERY_CSS = `
164+
.bt-del-body{flex:1 1 auto;min-height:0;display:flex;flex-direction:column;gap:10px;padding:12px 14px 16px}
165+
.bt-del-head{display:flex;flex-direction:column;gap:6px;border-bottom:.5px solid var(--bt-line);padding-bottom:8px}
166+
.bt-del-metrics{display:flex;flex-wrap:wrap;align-items:flex-start;gap:16px}
167+
.bt-del-table{border-collapse:collapse;font-size:12px;line-height:16px;font-variant-numeric:tabular-nums}
168+
.bt-del-table th,.bt-del-table td{padding:2px 8px;text-align:right;white-space:nowrap}
169+
.bt-del-table th[scope=row]{text-align:left;font-weight:500;color:var(--bt-ink-2)}
170+
.bt-del-table thead th{font-weight:600;color:var(--bt-ink-3)}
171+
.bt-del-ci,.bt-del-open{margin:0;font-size:12px;line-height:16px;color:var(--bt-ink-2)}
172+
.bt-del-asof{margin:0;font-size:11.5px;line-height:15px;color:var(--bt-ink-3)}
173+
.bt-del-columns{flex:1 1 auto;min-height:0;display:flex;gap:10px;overflow:auto}
174+
.bt-del-column{flex:1 1 0;min-width:200px;display:flex;flex-direction:column;gap:8px;padding:8px;border-radius:14px;background:var(--bt-hover);overflow:auto}
175+
.bt-del-card{display:flex;flex-direction:column;gap:5px;padding:10px;border:.5px solid var(--bt-line-2);border-radius:12px;background:var(--bt-card,var(--bt-main))}
176+
.bt-del-card-top{display:flex;align-items:center;gap:6px}
177+
.bt-del-id{font-size:12px;line-height:16px;color:var(--bt-ink-3);font-variant-numeric:tabular-nums}
178+
.bt-del-badge{margin-left:auto;font-size:10.5px;line-height:16px;padding:0 6px;border-radius:999px;background:var(--bt-hover);color:var(--bt-ink-2)}
179+
.bt-del-badge[data-badge=inc]{background:rgba(224,33,53,.12);color:#c21d2e}
180+
.bt-del-badge[data-badge=ci-red]{background:rgba(224,33,53,.12);color:#c21d2e}
181+
.bt-del-badge[data-badge=pass-enqueue],.bt-del-badge[data-badge=pass-merge]{background:color-mix(in srgb,var(--bt-green) 16%,transparent);color:color-mix(in srgb,var(--bt-green) 70%,var(--bt-ink))}
182+
.bt-del-title{font-size:13.5px;line-height:19px;color:var(--bt-ink)}
183+
.bt-del-labels{display:flex;flex-wrap:wrap;gap:4px}
184+
.bt-del-label{font-size:10.5px;line-height:16px;padding:0 6px;border-radius:999px;background:var(--bt-hover);color:var(--bt-ink-2)}
185+
.bt-del-owner{font-size:12px;line-height:16px;color:var(--bt-ink-2)}
186+
.bt-del-owner[data-kind=human]{font-weight:600;color:var(--bt-ink)}
187+
.bt-del-linked,.bt-del-sha{font-size:11.5px;line-height:15px;color:var(--bt-ink-3)}
188+
.bt-del-open{align-self:flex-start;font-size:12px;line-height:16px;color:var(--bt-accent)}
189+
@media (max-width:800px){
190+
.bt-del-columns{overflow:auto}
191+
.bt-del-column{min-width:220px}
192+
}
193+
@media (max-width:640px){
194+
.bt-del-columns{flex-direction:column}
195+
.bt-del-column{min-width:0}
196+
}
197+
`

‎core/src/client/i18n.js‎

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1274,6 +1274,39 @@ const HOST_ZH = {
12741274
'This DeepSeek Harness cannot list folders': '这个 DeepSeek Harness 不能列出文件夹',
12751275
'No FlyBot core update is available': '没有可用的 FlyBot 核心更新',
12761276
'saved by a newer FlyBot; update FlyBot': '由更新的 FlyBot 保存;请更新 FlyBot',
1277+
// #1173: the delivery board — one repository-wide snapshot beside the per-Bot task board.
1278+
'Delivery': '交付',
1279+
'Board view': '看板视图',
1280+
'Original issue': '原始单',
1281+
'Ready to start': '待开工',
1282+
'In review': '审查中',
1283+
'Done today': '今天完成',
1284+
'INC · rework': '打回 · 返工',
1285+
'PASS · waiting for enqueue': '通过 · 待入队',
1286+
'PASS · waiting for merge': '通过 · 待合入',
1287+
'CI red': 'CI 红',
1288+
'CI running': 'CI 跑着',
1289+
'No verdict': '还没结论',
1290+
'New issues': '新单',
1291+
'Closed issues': '关单',
1292+
'New pull requests': '新 PR',
1293+
'Merged pull requests': '合入 PR',
1294+
'Median issue close': '关单中位',
1295+
'Median PR merge': '合入中位',
1296+
'Today': '今天',
1297+
'No GitHub connection yet; the board fills in once one is added.': '还没接 GitHub,接上后这里就有数了。',
1298+
'The delivery board is not ready yet.': '交付看板还没准备好。',
1299+
'Showing the last snapshot from {time}: {error}': '显示 {time} 的快照:{error}',
1300+
'linked to #{number}': '关联 #{number}',
1301+
'CI failed {failed} of {total}': 'CI 红 {failed} / 共 {total}',
1302+
'counted {counted} of {total}': '已统计 {counted} / 共 {total}',
1303+
'{count} superseded': '{count} 条被取代',
1304+
'Open: {issues} issues ({needs} need a human), {prs} pull requests': '开着:{issues} 张单({needs} 张等人),{prs} 个 PR',
1305+
'median open age {age}': '开着的单中位 {age}',
1306+
'as of {time}': '截至 {time}',
1307+
'refreshing…': '刷新中…',
1308+
'No snapshot yet.': '还没有快照。',
1309+
'Nothing here.': '这里还没有。',
12771310
}
12781311
Object.assign(ZH, HOST_ZH)
12791312

‎core/src/client/index.js‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -608,6 +608,7 @@ export function apply(ctx) {
608608
setBotConnector: (botId, connector, enabled) => after(call('bot-connectors', { botId, connector, enabled })),
609609
setBotCapability: (botId, name, attached) => after(call('bot-capability', { botId, name, attached })),
610610
tasks: (payload) => call('tasks', payload ?? {}),
611+
deliveryBoard: (payload) => call('delivery-board', payload ?? {}),
611612
taskUpdate: (payload) => call('task-update', payload),
612613
taskCreate: (payload) => call('task-create', payload),
613614
schedules: () => call('schedules'),

‎core/src/client/kanban.js‎

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { createElement as h, useCallback, useEffect, useState } from 'react'
55
import { columnOf, priorityOf, progressOf } from '../shared/task-board.js'
66
import { CloseIcon } from './icons.js'
77
import { useEscape } from './overlay-hooks.js'
8+
import { DeliveryView, DELIVERY_CSS } from './delivery-view.js'
89
import { t } from './i18n.js'
910

1011
const POLL_MS = 15_000
@@ -315,6 +316,9 @@ function scopeName(roster, scope) {
315316
}
316317

317318
export function KanbanDialog({ roster, actions, leaving = false }) {
319+
// Two views on one surface: the per-Bot task board (#1058) and the repository's delivery
320+
// board (#1173). Delivery is repo-wide, so its rail would be a lie — it is not rendered.
321+
const [view, setView] = useState('tasks')
318322
const rail = boardRail(roster)
319323
const [scope, setScope] = useState(() => (rail.bots[0] ? { kind: 'bot', botId: rail.bots[0].id } : rail.groups[0] ? { kind: 'group', roomId: rail.groups[0].id } : null))
320324
const [dragging, setDragging] = useState(null)
@@ -357,18 +361,25 @@ export function KanbanDialog({ roster, actions, leaving = false }) {
357361
h('div', { className: 'bt-scrim', onClick: close }),
358362
h('div', { className: 'bt-mem bt-board', role: 'dialog', 'aria-modal': true, 'aria-labelledby': 'bt-board-heading' },
359363
h('div', { className: 'bt-mem-head' },
360-
h('h2', { id: 'bt-board-heading' }, t('Tasks')),
361-
h('span', { className: 'bt-mem-status' }, name),
364+
h('h2', { id: 'bt-board-heading' }, view === 'delivery' ? t('Delivery') : t('Tasks')),
365+
h('div', { className: 'bt-board-views', role: 'tablist', 'aria-label': t('Board view') },
366+
h('button', { type: 'button', role: 'tab', className: 'bt-soft bt-soft-sm', 'data-view': 'tasks', 'aria-selected': view === 'tasks' ? 'true' : 'false', onClick: () => setView('tasks') }, t('Tasks')),
367+
h('button', { type: 'button', role: 'tab', className: 'bt-soft bt-soft-sm', 'data-view': 'delivery', 'aria-selected': view === 'delivery' ? 'true' : 'false', onClick: () => setView('delivery') }, t('Delivery'))),
368+
view === 'tasks' ? h('span', { className: 'bt-mem-status' }, name) : null,
362369
h('span', { className: 'bt-mem-head-gap' }),
363370
h('button', { type: 'button', className: 'bt-icon-btn', 'aria-label': t('Close tasks'), autoFocus: true, onClick: close }, h(CloseIcon))),
364-
h(BoardView, {
365-
roster, tasks, rail, scope, counts, dragging, error, draft,
366-
onDraft: setDraft, onSelect: setScope, onMove: move, onDrag: setDragging, onCreate: create,
367-
})))
371+
view === 'delivery'
372+
? h(DeliveryView, { actions })
373+
: h(BoardView, {
374+
roster, tasks, rail, scope, counts, dragging, error, draft,
375+
onDraft: setDraft, onSelect: setScope, onMove: move, onDrag: setDragging, onCreate: create,
376+
})))
368377
}
369378

370379
export const KANBAN_CSS = `
371380
.bt-board-row{min-height:40px;padding:6px 8px;gap:10px}
381+
.bt-board-views{display:flex;gap:6px;align-items:center}
382+
.bt-board-views .bt-soft[aria-selected=true]{background:var(--bt-active,var(--bt-hover));color:var(--bt-ink)}
372383
.bt-board-mark{flex:none;width:36px;height:28px;display:inline-flex;align-items:center;justify-content:center;color:var(--bt-ink-2)}
373384
.bt-board-mark svg{width:18px;height:18px}
374385
.bt-board-label{flex:1 1 auto;min-width:0;font-size:14px;line-height:20px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
@@ -428,4 +439,4 @@ export const KANBAN_CSS = `
428439
.bt-board-columns{flex-direction:column}
429440
.bt-board-column{min-width:0}
430441
}
431-
`
442+
` + DELIVERY_CSS

‎core/src/host/authz.js‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -398,7 +398,8 @@ export function install(rt) {
398398
// Anything not in the table and not a known auth/read endpoint is denied.
399399
if (NO_ACTOR_ALLOWED.has(ep) || ep.startsWith('auth-')) return ALLOW
400400
if (ep === 'state' || ep === 'models' || ep === 'efforts' || ep === 'repo-stars' || ep === 'update-check'
401-
|| ep === 'diagnostics' || ep === 'diagnostics-log' || ep === 'usage') return ALLOW
401+
|| ep === 'diagnostics' || ep === 'diagnostics-log' || ep === 'usage'
402+
|| ep === 'delivery-board') return ALLOW
402403

403404
return deny('bot/not-found', `Unknown endpoint ${ep}`)
404405
}

‎core/src/host/channel.js‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -639,6 +639,9 @@ export function install(rt) {
639639
case 'task-answer-input':
640640
await rt.load()
641641
return ok(await rt.taskEndpoint(endpoint, body, actor))
642+
case 'delivery-board':
643+
await rt.load()
644+
return ok(await rt.deliveryBoard(body))
642645
case 'share-brief':
643646
await rt.load()
644647
await rt.setBriefShare(String(body.id ?? ''), String(body.botId ?? ''), body.share === true, actor)

0 commit comments

Comments
 (0)