Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog.d/inbox-sse.added.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
The question inbox now updates over Server-Sent Events at `/api/inbox/events`. Fledge sends the server-rendered inbox when the question set changes, and the browser replaces the section and rebinds the answer forms without a reload.
1 change: 1 addition & 0 deletions changelog.d/issue-commands.added.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Collaborators can drive a work item from the issue thread with `/fledge status`, `/fledge plan`, `/fledge approve` and `/fledge fix`. Fledge checks the author association, dedupes commands by comment id, creates or reuses the work item, and replies with the current state; plan approval remains maintainer-only.
1 change: 1 addition & 0 deletions changelog.d/stage-chain.changed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
The delivery chain is now Fledge-driven: a successful iterate with a PR queues review when the reviewer is enabled, plan approval moves the work item to assigned, and a review outcome moves it to approved or changes_requested. The work-item table shows the current stage.
1 change: 1 addition & 0 deletions changelog.d/ui-polish.changed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Polished the repository lifecycle, provider cards, per-bot model grid, danger zone and delivery list, and updated the standalone user-view mock with the new provider, delivery and lifecycle screens.
24 changes: 24 additions & 0 deletions docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -280,6 +280,30 @@ job is retried, so a new job can continue from the last known point instead of
starting from scratch. The add/settings goal forms also offer four task
templates (bug, tests, docs, dependency) that fill the goal text.

### Issue commands and live inbox

A collaborator can drive a work item from the issue thread with `/fledge status`,
`/fledge plan`, `/fledge approve` or `/fledge fix`. The webhook checks the author
association, dedupes by comment id, creates or reuses the work item, and replies
on the issue with the work-item/job/PR state. `/fledge approve` is maintainer-only
and approves the latest completed plan.

The inbox is also available as `GET /api/inbox/events`, a per-user SSE stream.
The server reuses the cached inbox snapshot, sends the server-rendered inbox HTML
when the question signature changes, and the browser replaces the section and
rebinds the answer forms. The page keeps a hidden empty inbox so the first new
question can appear without a reload.

### Fledge-driven delivery chain

After an iterate job succeeds with a PR, Fledge queues a review job when the
repository has an enabled reviewer and no other job is active. A successful review
moves the linked work item to `approved` (score >= 80 and no follow-ups) or
`changes_requested`; follow-ups are queued on the same branch. Plan approval
moves the work item to `assigned`, and the execute action enqueues the
implementation with the work item attached. The repository work-item table shows
the stage (Scope / Implement / Review / Merge) alongside the state.

### Repository lifecycle

The repository page has an explicit lifecycle: an enabled/disabled flag, the
Expand Down
14 changes: 14 additions & 0 deletions mock/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,17 @@ What to review:
- Global settings and the mock question inbox.
- The top-right `中 / EN` switch changes the main labels; mock content stays as
written.

## Latest screens

- Repository lifecycle banner and danger zone (stop scheduling, stop FlyBot, typed-name delete).
- Pull-request delivery list with review score, cost and a merge action.
- Model providers with multiple models per provider and platform-side connection testing.
- Per-bot model selectors (Chief, Lead, Product, Test, Security, Reviewer, Dev-1, Dev-2).

## Latest screens

- Repository lifecycle banner and danger zone (stop scheduling, stop FlyBot, typed-name delete).
- Pull-request delivery list with review score, cost and a merge action.
- Model providers with multiple models per provider and platform-side connection testing.
- Per-bot model selectors (Chief, Lead, Product, Test, Security, Reviewer, Dev-1, Dev-2).
14 changes: 11 additions & 3 deletions mock/fledge-ui.html
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,7 @@
.nav a { padding: 8px 0; }
.account { display: none; }
}
.provider-card{border:1px solid var(--line);border-radius:12px;padding:12px;margin-bottom:12px;background:rgba(255,255,255,.02)}.provider-card ul{margin:8px 0;padding-left:18px}.bot-models{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:8px}#danger{border-color:rgba(240,116,116,.35);background:linear-gradient(180deg,rgba(240,116,116,.06),transparent 70%)}
</style>
</head>
<body>
Expand Down Expand Up @@ -558,6 +559,8 @@ <h2>${esc(t('选择你在这个仓库的角色', 'Choose your role for this repo
<label class="check"><input type="checkbox" name="planRequired" ${repo.planRequired ? 'checked' : ''}> ${esc(t('先出计划,批准后执行', 'Plan first; execute only after approval'))}</label>
<div class="row"><label style="flex:1">${esc(t('允许修改路径', 'Allowed paths'))} <input name="allowPaths" value="${esc(repo.allowPaths || '')}" placeholder="src/, docs/" /></label><label style="flex:1">${esc(t('禁止修改路径', 'Forbidden paths'))} <input name="denyPaths" value="${esc(repo.denyPaths || '')}" placeholder=".github/, deploy/" /></label></div>
<div class="row"><label>${esc(t('合并方式', 'Merge method'))} <select name="mergeMethod">${['squash', 'merge', 'rebase'].map((method) => `<option value="${method}" ${(repo.mergeMethod || 'squash') === method ? 'selected' : ''}>${method}</option>`).join('')}</select></label><label class="check"><input type="checkbox" name="requireProtection" ${repo.requireProtection !== false ? 'checked' : ''}> ${esc(t('自动合并前要求 branch protection', 'Require branch protection before auto-merge'))}</label></div>
<div class="row bot-models">${['Chief', 'Lead', 'Product', 'Test', 'Security', 'Reviewer', 'Dev-1', 'Dev-2'].map((role) => `<label>${esc(role)} <select><option>${esc(t('跟随默认', 'Follow default'))}</option><option>DeepSeek · deepseek-chat</option></select></label>`).join('')}</div>
<p class="fine">${esc(t('每个 bot 可以单独选择模型;不选则跟随默认。应用模型后 Fledge 会通过 DSH 分配。', 'Each bot can choose a model; empty follows the default. Applying models assigns them through DSH.'))}</p>
<p class="fine">${esc(t('Branch protection', 'Branch protection'))}: <code>${esc(repo.protectionState || 'unknown')}</code>${repo.protectedAt ? ` · ${esc(repo.protectedAt)}` : ''}</p>
<label class="check ${canMaintain ? '' : 'disabled'}"><input type="checkbox" name="autoMerge" ${repo.autoMerge ? 'checked' : ''} ${canMaintain ? '' : 'disabled'}> ${esc(t('允许 maintainer 自动合并 PR', 'Allow maintainer auto-merge'))}</label>
<label class="check ${canReview ? '' : 'disabled'}"><input type="checkbox" name="review" ${repo.review ? 'checked' : ''} ${canReview ? '' : 'disabled'}> ${esc(t('启用 PR 审核', 'Enable PR reviewer'))}</label>
Expand All @@ -570,14 +573,19 @@ <h2>${esc(t('连接', 'Connection'))}</h2>
<p class="muted">${esc(t('GitHub token 已加密保存;页面不会显示明文。如果仓库或 token 变了,删除后重新添加。', 'The GitHub token is encrypted and never rendered in plain text. If the repository or token changes, delete and add it again.'))}</p>
<p class="fine">${esc(t('审核最近运行', 'Last review'))} ${esc(repo.review ? t('已启用', 'Enabled') : t('未启用', 'Disabled'))}</p>
<div class="row" style="margin-bottom:12px"><button class="btn ghost small" data-action="toast" data-message="${esc(t('上游 / fork / 权限已重新检测(mock)。', 'Upstream / fork / permissions refreshed (mock).'))}">${esc(t('重新检测上游 / fork', 'Refresh upstream / fork'))}</button></div>
<button class="btn danger small" data-action="toast" data-message="${esc(t('删除仓库需要二次确认(mock)。', 'Deleting a repository needs confirmation (mock).'))}">${esc(t('删除仓库', 'Delete repository'))}</button>
<p class="fine">${esc(t('删除仓库连接请见危险操作区。', 'See the danger zone to delete the repository connection.'))}</p>
<p class="fine" style="margin-top:12px">${esc(t('平台只通过 GitHub API 读取元数据、创建 fork 和分支/PR;不会 clone 到公共磁盘。', 'The platform only uses the GitHub API to read metadata, create forks and open branches/PRs; it never clones to public disk.'))}</p>
</section>`;
const lifecycleCard = repo.enabled === false || repo.instance !== 'ready'
? `<section class="card" id="lifecycle"><strong>${esc(t('当前状态', 'Current state'))}</strong><ul>${repo.enabled === false ? `<li>${esc(t('仓库已停用:不会自动或手动运行任务。', 'Repository disabled: no jobs run automatically or manually.'))}</li>` : ''}${repo.instance !== 'ready' ? `<li>${esc(t('FlyBot 未就绪:任务不会运行。', 'FlyBot is not ready: jobs will not run.'))}</li>` : ''}</ul><div class="row"><button class="btn ghost" data-action="toast" data-message="${esc(t('仓库已启用(mock)。', 'Repository enabled (mock).'))}">${esc(t('启用仓库', 'Enable repository'))}</button><button class="btn ghost" data-action="toast" data-message="${esc(t('FlyBot 正在启动(mock)。', 'FlyBot starting (mock).'))}">${esc(t('启动 FlyBot', 'Start FlyBot'))}</button></div></section>`
: '';
const deliveryCard = `<section class="card" id="delivery"><h2>${esc(t('待处理 PR', 'Pull requests to decide'))}</h2><p class="muted">${esc(t('合并前会重新检查 PR 状态、checks、commit status、reviews 和 branch protection。', 'Merging re-checks the PR state, checks, commit status, reviews and branch protection.'))}</p><div class="table-scroll"><table><thead><tr><th>PR</th><th>${esc(t('状态', 'Status'))}</th><th>${esc(t('审核分', 'Review'))}</th><th>${esc(t('成本', 'Cost'))}</th><th></th></tr></thead><tbody><tr><td><a data-action="toast" data-message="${esc(t('打开 PR(mock)。', 'Open PR (mock).'))}">#21</a></td><td>${statusBadge('ready')}</td><td>93</td><td>¥5.97</td><td><button class="btn ghost small" data-action="toast" data-message="${esc(t('PR 已合并(mock)。', 'PR merged (mock).'))}">${esc(t('合并', 'Merge'))}</button></td></tr><tr><td><a data-action="toast" data-message="${esc(t('打开 PR(mock)。', 'Open PR (mock).'))}">#19</a></td><td>${statusBadge('ready')}</td><td>91</td><td>¥8.79</td><td><button class="btn ghost small" data-action="toast" data-message="${esc(t('PR 已合并(mock)。', 'PR merged (mock).'))}">${esc(t('合并', 'Merge'))}</button></td></tr></tbody></table></div></section>`;
const dangerCard = `<section class="card" id="danger"><h2>${esc(t('危险操作', 'Danger zone'))}</h2><p class="muted">${esc(t('这里的操作只影响 Fledge 连接,不会删除 GitHub 仓库。', 'These actions affect the Fledge connection only; GitHub is not deleted.'))}</p><div class="row"><button class="btn ghost" data-action="toast" data-message="${esc(t('调度已停止(mock)。', 'Scheduling stopped (mock).'))}">${esc(t('停止调度', 'Stop scheduling'))}</button><button class="btn ghost" data-action="toast" data-message="${esc(t('FlyBot 已停止(mock)。', 'FlyBot stopped (mock).'))}">${esc(t('停止 FlyBot', 'Stop FlyBot'))}</button></div><div class="field"><label>${esc(t('输入仓库名确认', 'Type the repository name to confirm'))} <code>${esc(repo.name)}</code></label><input placeholder="${esc(repo.name)}" /></div><button class="btn danger" data-action="toast" data-message="${esc(t('仓库连接已删除(mock)。', 'Repository connection deleted (mock).'))}">${esc(t('删除仓库连接', 'Delete repository connection'))}</button></section>`;
const jobsCard = `<section class="card">
<h2>${esc(t('任务', 'Jobs'))}</h2>
<div class="table-scroll"><table><thead><tr><th>${esc(t('任务', 'Job'))}</th><th>${esc(t('状态', 'Status'))}</th><th>PR / Issue</th><th>${esc(t('摘要', 'Summary'))}</th></tr></thead><tbody>${jobRows}</tbody></table></div>
</section>`;
return `${repoHeader(repo)}${roleCard(repo)}${qHtml}${chiefCard}${instanceCard}${workCard}<div class="grid2">${settingsCard}${connectionCard}</div>${jobsCard}`;
return `${repoHeader(repo)}${roleCard(repo)}${qHtml}${chiefCard}${instanceCard}${lifecycleCard}${deliveryCard}${workCard}<div class="grid2">${settingsCard}${connectionCard}</div>${dangerCard}${jobsCard}`;
}

function newRepoScreen() {
Expand Down Expand Up @@ -636,7 +644,7 @@ <h2>${esc(t('选择要接入的仓库', 'Choose repositories'))}</h2>
function settingsScreen() {
return `<div class="eyebrow">${esc(t('设置', 'SETTINGS'))}</div><h1>${esc(t('设置', 'Settings'))}</h1><p class="muted">${esc(t('账号、模型、通知和 API token(mock)。', 'Account, model, notifications and API tokens (mock).'))}</p>
<div class="grid2">
<section class="card"><h2>${esc(t('自己的模型', 'Your model'))}</h2><div class="field"><label>Base URL</label><input value="https://api.example.com/v1" /></div><div class="field"><label>API Key</label><input type="password" value="sk-mock" /></div><div class="field"><label>Model</label><input value="gpt-4o-mini" /></div><div class="row"><button class="btn" data-action="toast" data-message="${esc(t('模型设置已保存(mock)。', 'Model settings saved (mock).'))}">${esc(t('保存并测试', 'Save and test'))}</button><button class="btn ghost" data-action="toast" data-message="${esc(t('已清除,改回平台额度(mock)。', 'Cleared; using platform quota (mock).'))}">${esc(t('清除', 'Clear'))}</button></div></section>
<section class="card"><h2>${esc(t('模型提供方', 'Model providers'))}</h2><p class="muted">${esc(t('添加提供方(Base URL + API Key),再在下面添加多个模型。添加模型时 Fledge 会测试;失败不会保存。', 'Add a provider (Base URL + API key), then add models. Adding a model runs a Fledge test; failure is not saved.'))}</p><div class="provider-card"><div class="row between"><strong>DeepSeek</strong><span class="fine">https://api.deepseek.com/v1</span></div><ul><li>deepseek-chat <button class="btn danger small" data-action="toast" data-message="${esc(t('模型已删除(mock)。', 'Model deleted (mock).'))}">${esc(t('删除', 'Delete'))}</button></li><li>deepseek-reasoner <button class="btn danger small" data-action="toast" data-message="${esc(t('模型已删除(mock)。', 'Model deleted (mock).'))}">${esc(t('删除', 'Delete'))}</button></li></ul><div class="row"><input placeholder="model id" /><button class="btn small" data-action="toast" data-message="${esc(t('模型已通过测试并保存(mock)。', 'Model passed the test and was saved (mock).'))}">${esc(t('添加模型并测试', 'Add model and test'))}</button></div></div><div class="field"><label>${esc(t('名称', 'Label'))}</label><input placeholder="Provider name" /></div><div class="field"><label>Base URL</label><input placeholder="https://api.example.com/v1" /></div><div class="field"><label>API Key</label><input type="password" value="" /></div><label class="check"><input type="checkbox"> ${esc(t('允许 HTTP / 内网地址(Fledge 必须能访问)', 'Allow HTTP / private address (Fledge must reach it)'))}</label><button class="btn" data-action="toast" data-message="${esc(t('提供方已添加(mock)。', 'Provider added (mock).'))}">${esc(t('添加提供方', 'Add provider'))}</button></section>
<section class="card"><h2>${esc(t('通知', 'Notifications'))}</h2><div class="field"><label>${esc(t('通知邮箱', 'Notification email'))}</label><input value="h***@gmail.com" /></div><label class="check"><input type="checkbox" checked> ${esc(t('任务需要回答、完成或失败时发邮件', 'Email me when a job needs an answer, finishes or fails'))}</label><label class="check"><input type="checkbox" checked> ${esc(t('每日摘要', 'Daily digest'))}</label><button class="btn" data-action="toast" data-message="${esc(t('通知设置已保存(mock)。', 'Notification settings saved (mock).'))}">${esc(t('保存', 'Save'))}</button></section>
</div>
<section class="card"><h2>API tokens</h2><div class="list"><div class="list-row"><div class="list-main"><strong><code>flg_7f2a…</code></strong><small>flybot-cli · 最近使用 2026-11-03 10:12</small></div><button class="btn danger small" data-action="toast" data-message="${esc(t('token 已撤销(mock)。', 'Token revoked (mock).'))}">${esc(t('撤销', 'Revoke'))}</button></div></div><button class="btn ghost" style="margin-top:12px" data-action="toast" data-message="${esc(t('已创建新 token(mock)。', 'New token created (mock).'))}">${esc(t('创建 token', 'Create token'))}</button></section>`;
Expand Down
27 changes: 27 additions & 0 deletions public/app.css
Original file line number Diff line number Diff line change
Expand Up @@ -416,3 +416,30 @@ a.list-row:hover strong { color: var(--accent); }
.info-tip { display: inline-flex; align-items: center; justify-content: center; width: 15px; height: 15px; margin-left: 4px; border: 1px solid var(--line); border-radius: 50%; color: var(--muted-2); font-size: 10px; line-height: 1; cursor: help; position: relative; vertical-align: middle; }
.info-tip::after { content: attr(data-tip); position: absolute; left: 0; top: 130%; z-index: 60; width: 270px; max-width: min(270px, calc(100vw - 48px)); padding: 8px 10px; border: 1px solid var(--line); border-radius: 8px; background: #0b121b; color: #dbe6f3; font-size: 11.5px; font-weight: 400; line-height: 1.45; white-space: normal; overflow-wrap: anywhere; opacity: 0; pointer-events: none; transform: translateY(-3px); transition: opacity .12s ease; box-shadow: 0 8px 24px rgba(0, 0, 0, .35); }
.info-tip:hover::after, .info-tip:focus::after { opacity: 1; }

.provider-card {
border: 1px solid var(--line);
border-radius: 12px;
padding: 12px;
margin-bottom: 12px;
background: rgba(255, 255, 255, 0.02);
}
.provider-card ul {
margin: 8px 0;
padding-left: 18px;
}
.provider-card li {
margin: 4px 0;
}
.bot-models {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
gap: 8px;
}
#danger {
border-color: rgba(240, 116, 116, 0.35);
background: linear-gradient(180deg, rgba(240, 116, 116, 0.06), transparent 70%);
}
.inbox[hidden] {
display: none;
}
Loading
Loading