duaer-spec 0.19.0 → 0.20.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.duaer/active-job.json +3 -3
- package/.duaer/specs/188-release-019/delivery.json +18 -0
- package/.duaer/specs/188-release-019/tasks.md +3 -3
- package/.duaer/specs/189-fde-portfolio/delivery.json +10 -0
- package/.duaer/specs/189-fde-portfolio/spec.md +29 -0
- package/.duaer/specs/189-fde-portfolio/tasks.md +8 -0
- package/.duaer/specs/190-live-open-no-model/delivery.json +9 -0
- package/.duaer/specs/190-live-open-no-model/spec.md +17 -0
- package/.duaer/specs/190-live-open-no-model/tasks.md +6 -0
- package/.duaer/specs/191-projects-root-pick/delivery.json +11 -0
- package/.duaer/specs/191-projects-root-pick/spec.md +14 -0
- package/.duaer/specs/191-projects-root-pick/tasks.md +6 -0
- package/.duaer/specs/192-release-020/spec.md +12 -0
- package/.duaer/specs/192-release-020/tasks.md +6 -0
- package/CHANGELOG.md +17 -0
- package/README.md +16 -12
- package/README.zh-CN.md +8 -7
- package/bin/duaer-live.mjs +58 -9
- package/bin/live-projects.mjs +34 -1
- package/docs/agent/e2e-test-plan.md +7 -1
- package/docs/agent/live-desk.md +22 -1
- package/package.json +1 -1
- package/web/live-dev/app.js +144 -27
- package/web/live-dev/delivery-status.mjs +135 -0
- package/web/live-dev/i18n.js +52 -2
- package/web/live-dev/index.html +15 -3
- package/web/live-dev/styles.css +110 -0
package/.duaer/active-job.json
CHANGED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"status": "accepted",
|
|
3
|
+
"feature": "188-release-019",
|
|
4
|
+
"acceptedAt": "2026-09-19T00:00:00.000Z",
|
|
5
|
+
"summary": "0.19.0 shipped: develop+main pushed, tag v0.19.0, GitHub Release, npm publish success.",
|
|
6
|
+
"verification": {
|
|
7
|
+
"commands": [
|
|
8
|
+
"git push origin develop main v0.19.0",
|
|
9
|
+
"gh release create v0.19.0",
|
|
10
|
+
"gh run watch npm-publish"
|
|
11
|
+
],
|
|
12
|
+
"results": "CI pass; Pages pass; npm publish success"
|
|
13
|
+
},
|
|
14
|
+
"preview": {
|
|
15
|
+
"url": "https://www.npmjs.com/package/duaer-spec/v/0.19.0",
|
|
16
|
+
"label": "npm 0.19.0"
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Tasks — 188-release-019
|
|
2
2
|
|
|
3
3
|
- [x] Brief
|
|
4
|
-
- [
|
|
5
|
-
- [
|
|
6
|
-
- [
|
|
4
|
+
- [x] Bump 0.19.0 + CHANGELOG
|
|
5
|
+
- [x] Merge to develop; promote main; push; tag; gh release
|
|
6
|
+
- [x] Watch npm publish / stamp delivery
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
{
|
|
2
|
+
"status": "accepted",
|
|
3
|
+
"feature": "189-fde-portfolio",
|
|
4
|
+
"acceptedAt": "2026-09-19T02:30:00.000Z",
|
|
5
|
+
"verification": {
|
|
6
|
+
"L0": "node --check app.js delivery-status.mjs live-projects.mjs",
|
|
7
|
+
"L1": "node --test test/delivery-status.test.mjs test/live-projects.test.mjs (9 pass)",
|
|
8
|
+
"L3": "npm run test:live (pass)"
|
|
9
|
+
}
|
|
10
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# 189 — FDE project portfolio status strip
|
|
2
|
+
|
|
3
|
+
## Goal
|
|
4
|
+
|
|
5
|
+
Operators see each project’s delivery status at a glance in the Projects drawer,
|
|
6
|
+
open deliverables in one click, and get a clear next action in the Progress
|
|
7
|
+
column — without learning Duaer phases.
|
|
8
|
+
|
|
9
|
+
## In scope
|
|
10
|
+
|
|
11
|
+
- Derive status: drafting / confirming / building / delivered / revising
|
|
12
|
+
- Enrich `GET /api/projects` project rows with status + nextAction + hasDeliverables
|
|
13
|
+
- Portfolio list: status chip + open-deliverables control
|
|
14
|
+
- Progress: stage strip + next-action line; deliverables CTA stays primary
|
|
15
|
+
- Docs (live-desk) + E2E + L0/L3
|
|
16
|
+
|
|
17
|
+
## Out of scope
|
|
18
|
+
|
|
19
|
+
- Dedicated cloud / multi-user accounts
|
|
20
|
+
- Session export/import
|
|
21
|
+
- Changing BYOC deploy hosts
|
|
22
|
+
|
|
23
|
+
## Acceptance
|
|
24
|
+
|
|
25
|
+
1. `/api/projects` includes `deliveryStatus` and `nextAction` per project when a
|
|
26
|
+
session exists (or sensible defaults when empty).
|
|
27
|
+
2. Portfolio row shows a status chip; deliverables openable when `hasDeliverables`.
|
|
28
|
+
3. Progress column shows stage summary + next-action copy for the active desk.
|
|
29
|
+
4. `npm test` (targeted) + `npm run test:live` pass.
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
# Tasks — 189-fde-portfolio
|
|
2
|
+
|
|
3
|
+
- [x] Brief + active-job
|
|
4
|
+
- [x] `delivery-status.mjs` derive status / nextAction / stages
|
|
5
|
+
- [x] Enrich `/api/projects` via live-projects / duaer-live
|
|
6
|
+
- [x] Portfolio UI chips + deliverables
|
|
7
|
+
- [x] Progress cockpit (stages + next action)
|
|
8
|
+
- [x] i18n, CSS, docs, E2E, verify, merge
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# 190 — Live starts without model gate; open desk URL
|
|
2
|
+
|
|
3
|
+
## Goal
|
|
4
|
+
|
|
5
|
+
`duaer live` must not require a configured model before serving. Always start
|
|
6
|
+
the desk, open `http://127.0.0.1:<port>` in the browser, and guide the operator
|
|
7
|
+
to Settings when the model is missing.
|
|
8
|
+
|
|
9
|
+
## Acceptance
|
|
10
|
+
|
|
11
|
+
1. `serve()` does not exit or block when `configReady` is false.
|
|
12
|
+
2. After listen, the desk URL is opened (macOS `open` / Linux `xdg-open`).
|
|
13
|
+
3. Console copy points to in-page Settings, not “must run live config first”.
|
|
14
|
+
4. UI shows the desk even when not ready; Settings opens as guidance; Settings
|
|
15
|
+
can be closed and reopened via the header control.
|
|
16
|
+
5. Chat/API that need a model still fail clearly until configured.
|
|
17
|
+
6. L0 + targeted tests / live smoke markers pass.
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
{
|
|
2
|
+
"status": "accepted",
|
|
3
|
+
"acceptedAt": "2026-09-19T02:45:00.000Z",
|
|
4
|
+
"verification": {
|
|
5
|
+
"L0": "node --check bin/duaer-live.mjs; node --check web/live-dev/app.js",
|
|
6
|
+
"L1": "node --test test/live-repo-path.test.mjs test/live-projects.test.mjs",
|
|
7
|
+
"L3": "npm run test:live",
|
|
8
|
+
"result": "pass"
|
|
9
|
+
},
|
|
10
|
+
"summary": "产品父目录 uses native folder pick (选择…/清除); no path typing."
|
|
11
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# 191 — Pick projects parent folder (no typing)
|
|
2
|
+
|
|
3
|
+
## Goal
|
|
4
|
+
|
|
5
|
+
Operators choose the **产品父目录** via the system folder dialog, not by
|
|
6
|
+
typing a path. Selection saves immediately.
|
|
7
|
+
|
|
8
|
+
## Acceptance
|
|
9
|
+
|
|
10
|
+
1. Projects drawer shows parent path as read-only display (not a free-text field).
|
|
11
|
+
2. A **选择…** control opens the native folder picker and saves `projectsRoot`.
|
|
12
|
+
3. Cancel leaves the previous value unchanged.
|
|
13
|
+
4. Optional clear control empties `projectsRoot`.
|
|
14
|
+
5. L0 + smoke/unit markers + merge to develop.
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# 192 — Release 0.20.0
|
|
2
|
+
|
|
3
|
+
## Goal
|
|
4
|
+
|
|
5
|
+
Ship 0.20.0: bump version + CHANGELOG, promote develop → main, push,
|
|
6
|
+
GitHub Release `v0.20.0`, trigger npm Trusted Publishing.
|
|
7
|
+
|
|
8
|
+
## Acceptance
|
|
9
|
+
|
|
10
|
+
1. `package.json` is `0.20.0`; CHANGELOG has `0.20.0` section (Unreleased cleared).
|
|
11
|
+
2. `develop` and `main` pushed with release commits.
|
|
12
|
+
3. GitHub Release `v0.20.0` published; npm publish workflow run succeeds or documented.
|
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,23 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 0.20.0 — 2026-09-19
|
|
6
|
+
|
|
7
|
+
### Feat: pick projects parent folder via system dialog
|
|
8
|
+
|
|
9
|
+
- 产品父目录 is read-only; **选择…** opens the native folder picker and saves
|
|
10
|
+
- **清除** empties `projectsRoot`; cancel leaves the previous path
|
|
11
|
+
|
|
12
|
+
### Fix: `duaer live` opens without model gate
|
|
13
|
+
|
|
14
|
+
- Desk starts and opens the browser even when no model is configured
|
|
15
|
+
- Settings guides model setup instead of blocking launch
|
|
16
|
+
|
|
17
|
+
### Feat: FDE portfolio status and delivery cockpit
|
|
18
|
+
|
|
19
|
+
- Project list shows delivery status chips and open-deliverables actions
|
|
20
|
+
- Progress column shows stage summary, next action, and deliverables CTA
|
|
21
|
+
|
|
5
22
|
## 0.19.0 — 2026-09-19
|
|
6
23
|
|
|
7
24
|
### Feat: modular `[模块]` structured requirement cards
|
package/README.md
CHANGED
|
@@ -61,16 +61,18 @@ Isolated from product repos. Config and Briefs live under `~/.duaer/live/`.
|
|
|
61
61
|
**Duaer-spec FDE** = Field Development Environment.
|
|
62
62
|
|
|
63
63
|
```bash
|
|
64
|
-
#
|
|
64
|
+
# Start anytime (opens the desk even if the model is not configured yet)
|
|
65
|
+
duaer live
|
|
66
|
+
|
|
67
|
+
# In the page Settings, or via CLI:
|
|
65
68
|
duaer live config --provider deepseek --api-key sk-...
|
|
66
69
|
|
|
67
70
|
# Remember a product repo for dispatch (optional; run inside that repo)
|
|
68
71
|
duaer live repo add
|
|
69
|
-
|
|
70
|
-
duaer live
|
|
71
72
|
```
|
|
72
73
|
|
|
73
|
-
|
|
74
|
+
`duaer live` opens the browser to the desk URL (default `http://127.0.0.1:8787`).
|
|
75
|
+
If the model is missing, use **Settings** in the page. The desk is **full-width**
|
|
74
76
|
with three columns: **chat** | **modules / confirm / architecture / kickoff /
|
|
75
77
|
revise** | **task progress**. Detail:
|
|
76
78
|
[`docs/agent/live-desk.md`](docs/agent/live-desk.md).
|
|
@@ -81,9 +83,10 @@ mock LLM — no paid API).
|
|
|
81
83
|
|
|
82
84
|
**Flow**
|
|
83
85
|
|
|
84
|
-
1. Open or create a **project**, then chat.
|
|
85
|
-
|
|
86
|
-
|
|
86
|
+
1. Open or create a **project**, then chat. The **Projects** drawer shows each
|
|
87
|
+
project’s **delivery status**; open **Deliverables** when the session has
|
|
88
|
+
content. Messy multi-topic talk is fine: the desk evolves **`modules[]`**
|
|
89
|
+
(tabs). Chat bubbles render **Markdown** (`**bold**`, code, links).
|
|
87
90
|
2. Per module: fill the confirm card (goal / out-of-scope / **checkable**
|
|
88
91
|
acceptance / assumptions). Vague acceptance fails the gate; validate must
|
|
89
92
|
pass before **Confirm**. Confirming a module **locks that card only** — it
|
|
@@ -104,11 +107,12 @@ mock LLM — no paid API).
|
|
|
104
107
|
- To run **DeepSeek or other models** for coding, point Claude Code at that API — see [`docs/agent/worker-models.md`](docs/agent/worker-models.md). The FDE setup page guide covers the same.
|
|
105
108
|
6. Edit the start command (must begin with `Duaer`) → kickoff creates
|
|
106
109
|
`.worktree/feat-*` and opens **Terminal** for each worker lane.
|
|
107
|
-
7.
|
|
108
|
-
progress) opens a generated HTML page of stage
|
|
109
|
-
timeline, confirmation, architecture, task pool,
|
|
110
|
-
workers update the product **README** before
|
|
111
|
-
Then open **Results** (**View result** when a
|
|
110
|
+
7. Progress shows a **next-action** stage strip, then `tasks.md` polling.
|
|
111
|
+
**View deliverables** (above progress) opens a generated HTML page of stage
|
|
112
|
+
artifacts (requirements doc timeline, confirmation, architecture, task pool,
|
|
113
|
+
delivery). On delivery, workers update the product **README** before
|
|
114
|
+
`delivery.json` is `accepted`. Then open **Results** (**View result** when a
|
|
115
|
+
page exists, otherwise **Open
|
|
112
116
|
project folder**).
|
|
113
117
|
8. If the result is not right: **Continue improving (left chat)** → confirm the
|
|
114
118
|
revise card (same validate gate) → re-confirm architecture (keep or redesign)
|
package/README.zh-CN.md
CHANGED
|
@@ -56,7 +56,10 @@ npx duaer-spec update
|
|
|
56
56
|
**Duaer-spec FDE** = Field Development Environment(现场开发)。
|
|
57
57
|
|
|
58
58
|
```bash
|
|
59
|
-
#
|
|
59
|
+
# 启动(任意目录都行,不会写入当前项目;未配模型也会打开台面)
|
|
60
|
+
duaer live
|
|
61
|
+
|
|
62
|
+
# 在打开的页面「设置」里填模型,或用 CLI:
|
|
60
63
|
duaer live config --provider deepseek --api-key sk-...
|
|
61
64
|
|
|
62
65
|
# 在产品仓登记,派工时点选(可选)
|
|
@@ -64,12 +67,10 @@ duaer live repo add
|
|
|
64
67
|
|
|
65
68
|
# 或任意 OpenAI 兼容接口
|
|
66
69
|
duaer live config --base-url https://api.openai.com/v1 --api-key sk-... --model gpt-4o-mini
|
|
67
|
-
|
|
68
|
-
# 启动(任意目录都行,不会写入当前项目)
|
|
69
|
-
duaer live
|
|
70
70
|
```
|
|
71
71
|
|
|
72
|
-
|
|
72
|
+
启动后会自动打开浏览器(默认 `http://127.0.0.1:8787`)。未配置模型时请在页面
|
|
73
|
+
**设置**里填写并保存。台面**全屏宽**,三列:
|
|
73
74
|
**对话** | **模块 / 确认 / 架构 / 开工 / 改进** | **任务进度**。细则见
|
|
74
75
|
[`docs/agent/live-desk.md`](docs/agent/live-desk.md)。
|
|
75
76
|
|
|
@@ -79,7 +80,7 @@ duaer live
|
|
|
79
80
|
|
|
80
81
|
**流程**
|
|
81
82
|
|
|
82
|
-
1.
|
|
83
|
+
1. 打开或新建**项目**,再聊天。右上角「项目」列表会显示每个项目的**交付状态**;有内容时可直接打开**交付物**。话题可以乱跳:台面会演进 **`modules[]`**(模块页签)。气泡支持 **Markdown**(加粗、代码、链接)。
|
|
83
84
|
2. **按模块**填确认卡(目标 / 不做 / **可检查验收** / 假设)。空泛验收过不了门禁;须校验通过才能点确认。确认只**锁住该模块卡**,**不会**立刻写 Brief 或开工。模块之间可随时切换。
|
|
84
85
|
3. 关心的模块都确认完 → 审 / 确认**架构**(Archify)→ **开工(kickoff)**。开工才写 Brief,建带 **`dependsOn` 的任务池**,并分派。默认 **1** 个数字员工;可选 **N** 个同 CLI 并行。共享任务给 1 号;模块任务轮询。每个 worker 独占一条 **Terminal 队列泳道**(`live-terminal` / `live-terminal/w2`…)。
|
|
85
86
|
4. 浏览 / 扫描 / 点选产品仓,或粘贴路径 / 项目名。路径不存在会自动创建;可先设**产品父目录**,再填短名在其下新建。非 git 目录会 `git init -b develop`;没有 develop/main/master 会建本地 `develop`。
|
|
@@ -88,7 +89,7 @@ duaer live
|
|
|
88
89
|
- Claude Code:`npm install -g @anthropic-ai/claude-code`(或 `curl -fsSL https://claude.ai/install.sh | bash`)
|
|
89
90
|
- 要用 **DeepSeek 等其它模型**写代码:把 Claude Code 接到对应 API,见 [`docs/agent/worker-models.zh-CN.md`](docs/agent/worker-models.zh-CN.md)。FDE「配置模型」页下方也有同样说明。
|
|
90
91
|
6. 编辑「启动命令」(须以 `Duaer` 开头)→ 开工建 `.worktree/feat-*`,并为每条 worker 泳道打开 **Terminal**。
|
|
91
|
-
7.
|
|
92
|
+
7. 右侧进度列先提示**下一步**(阶段条),并轮询 `tasks.md`。进度上方 **查看交付物** 会打开生成的 HTML 页(需求文档时间线、确认书、架构、任务池、交付等)。交付时数字员工先更新产品仓 **README**,再把 `delivery.json` 标为 `accepted`。然后出现 **结果**(有页面时 **查看结果**;否则 **打开项目目录**)。
|
|
92
93
|
8. 成品不满意:点 **继续改进(左侧对话)** → 确认改进卡(同样须校验)→ 再确认架构(保留或重画)→ 同 worktree 续派(先清残留 Agent 再入队)。进度跟本轮 `R{n}-*`。启动失败会回滚本轮 Brief Revision;状态行按泳道显示 Terminal 忙闲/队列。
|
|
93
94
|
9. 若需要上线 / 公网:在台面选 **计划托管平台**(Cloudflare / 阿里云 / AWS / GitHub Pages),说明见 [`docs/agent/deploy-targets.md`](docs/agent/deploy-targets.md)。未指定时默认 **GitHub CLI(`gh`)+ Actions**([`docs/agent/deploy-github.md`](docs/agent/deploy-github.md))
|
|
94
95
|
|
package/bin/duaer-live.mjs
CHANGED
|
@@ -50,6 +50,7 @@ import {
|
|
|
50
50
|
} from "./live-repo-path.mjs";
|
|
51
51
|
import {
|
|
52
52
|
buildProjectList,
|
|
53
|
+
enrichProjectsWithDeliveryStatus,
|
|
53
54
|
normalizeProjectKey,
|
|
54
55
|
} from "./live-projects.mjs";
|
|
55
56
|
import {
|
|
@@ -1572,11 +1573,12 @@ function discoverRepos({ max = 40 } = {}) {
|
|
|
1572
1573
|
);
|
|
1573
1574
|
}
|
|
1574
1575
|
|
|
1575
|
-
function pickFolderNative() {
|
|
1576
|
+
function pickFolderNative(promptText = "选择产品仓库") {
|
|
1577
|
+
const prompt = String(promptText || "选择产品仓库").trim() || "选择产品仓库";
|
|
1576
1578
|
if (process.platform === "darwin") {
|
|
1577
1579
|
const r = spawnSync(
|
|
1578
1580
|
"osascript",
|
|
1579
|
-
["-e",
|
|
1581
|
+
["-e", `POSIX path of (choose folder with prompt ${JSON.stringify(prompt)})`],
|
|
1580
1582
|
{ encoding: "utf8", timeout: 300000 },
|
|
1581
1583
|
);
|
|
1582
1584
|
const err = String(r.stderr || "").trim();
|
|
@@ -1602,7 +1604,7 @@ function pickFolderNative() {
|
|
|
1602
1604
|
if (process.platform === "linux") {
|
|
1603
1605
|
const r = spawnSync(
|
|
1604
1606
|
"zenity",
|
|
1605
|
-
["--file-selection", "--directory",
|
|
1607
|
+
["--file-selection", "--directory", `--title=${prompt}`],
|
|
1606
1608
|
{ encoding: "utf8", timeout: 300000 },
|
|
1607
1609
|
);
|
|
1608
1610
|
if (r.status !== 0) {
|
|
@@ -2035,6 +2037,9 @@ function listProjectsPayload() {
|
|
|
2035
2037
|
jobs,
|
|
2036
2038
|
activeProjectPath: cfg.activeProjectPath,
|
|
2037
2039
|
});
|
|
2040
|
+
enrichProjectsWithDeliveryStatus(bag, (projectPath) =>
|
|
2041
|
+
readProjectChat(liveRoot(), projectPath),
|
|
2042
|
+
);
|
|
2038
2043
|
return {
|
|
2039
2044
|
...bag,
|
|
2040
2045
|
projectsRoot: cfg.projectsRoot || "",
|
|
@@ -6425,6 +6430,31 @@ async function handleApi(req, res) {
|
|
|
6425
6430
|
return;
|
|
6426
6431
|
}
|
|
6427
6432
|
|
|
6433
|
+
if (req.method === "POST" && url.pathname === "/api/projects/pick-root") {
|
|
6434
|
+
try {
|
|
6435
|
+
const chosen = pickFolderNative("选择产品父目录");
|
|
6436
|
+
const abs = path.resolve(String(chosen || "").trim()).replace(/[\\/]+$/, "");
|
|
6437
|
+
if (!abs || !fs.existsSync(abs) || !fs.statSync(abs).isDirectory()) {
|
|
6438
|
+
throw new Error("请选择一个有效的文件夹");
|
|
6439
|
+
}
|
|
6440
|
+
const next = writeConfig({ projectsRoot: abs });
|
|
6441
|
+
send(res, 200, {
|
|
6442
|
+
ok: true,
|
|
6443
|
+
projectsRoot: abs,
|
|
6444
|
+
...publicConfig(next),
|
|
6445
|
+
...listProjectsPayload(),
|
|
6446
|
+
});
|
|
6447
|
+
} catch (err) {
|
|
6448
|
+
const code = err && err.code;
|
|
6449
|
+
send(res, 400, {
|
|
6450
|
+
error: err instanceof Error ? err.message : "pick root failed",
|
|
6451
|
+
cancelled: code === "CANCELLED",
|
|
6452
|
+
code: code || undefined,
|
|
6453
|
+
});
|
|
6454
|
+
}
|
|
6455
|
+
return;
|
|
6456
|
+
}
|
|
6457
|
+
|
|
6428
6458
|
if (req.method === "GET" && url.pathname === "/api/projects/chat") {
|
|
6429
6459
|
try {
|
|
6430
6460
|
const projectPath = String(url.searchParams.get("path") || "").trim();
|
|
@@ -6659,6 +6689,25 @@ function cmdConfig(opts) {
|
|
|
6659
6689
|
}
|
|
6660
6690
|
}
|
|
6661
6691
|
|
|
6692
|
+
function openDeskInBrowser(url) {
|
|
6693
|
+
const target = String(url || "").trim();
|
|
6694
|
+
if (!target) return;
|
|
6695
|
+
try {
|
|
6696
|
+
if (process.platform === "darwin") {
|
|
6697
|
+
spawn("open", [target], { detached: true, stdio: "ignore" }).unref();
|
|
6698
|
+
} else if (process.platform === "win32") {
|
|
6699
|
+
spawn("cmd", ["/c", "start", "", target], {
|
|
6700
|
+
detached: true,
|
|
6701
|
+
stdio: "ignore",
|
|
6702
|
+
}).unref();
|
|
6703
|
+
} else {
|
|
6704
|
+
spawn("xdg-open", [target], { detached: true, stdio: "ignore" }).unref();
|
|
6705
|
+
}
|
|
6706
|
+
} catch {
|
|
6707
|
+
/* ignore — operator can open the printed URL */
|
|
6708
|
+
}
|
|
6709
|
+
}
|
|
6710
|
+
|
|
6662
6711
|
function serve(port) {
|
|
6663
6712
|
if (!fs.existsSync(WEB_ROOT)) {
|
|
6664
6713
|
console.error("Missing web/live-dev");
|
|
@@ -6678,19 +6727,19 @@ function serve(port) {
|
|
|
6678
6727
|
});
|
|
6679
6728
|
|
|
6680
6729
|
server.listen(port, "127.0.0.1", () => {
|
|
6730
|
+
const deskUrl = `http://127.0.0.1:${port}`;
|
|
6681
6731
|
const cfg = publicConfig();
|
|
6682
|
-
console.log(`Duaer-spec FDE
|
|
6732
|
+
console.log(`Duaer-spec FDE ${deskUrl}`);
|
|
6683
6733
|
console.log(`隔离目录 ${liveRoot()}`);
|
|
6684
6734
|
console.log(`Brief 写入 ${jobsRoot()} (不会写入你当前业务仓库)`);
|
|
6685
6735
|
if (!cfg.ready) {
|
|
6686
|
-
console.log(
|
|
6687
|
-
|
|
6688
|
-
|
|
6689
|
-
或页面里点「DeepSeek」再填 Key。
|
|
6690
|
-
`);
|
|
6736
|
+
console.log(
|
|
6737
|
+
`模型尚未配置 — 已打开台面,请在页面「设置」里填写模型后保存。`,
|
|
6738
|
+
);
|
|
6691
6739
|
} else {
|
|
6692
6740
|
console.log(`模型 ${cfg.model} @ ${cfg.baseUrl}`);
|
|
6693
6741
|
}
|
|
6742
|
+
openDeskInBrowser(deskUrl);
|
|
6694
6743
|
scheduleUpdateHint();
|
|
6695
6744
|
void refreshUpdateInfo({ force: false });
|
|
6696
6745
|
// Background: every 10 days check/upgrade Cursor Agent / Claude Code CLIs
|
package/bin/live-projects.mjs
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import { basename } from "node:path";
|
|
6
|
+
import { deriveProjectDeliveryStatus } from "../web/live-dev/delivery-status.mjs";
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
9
|
* @param {string | null | undefined} p
|
|
@@ -21,6 +22,34 @@ export function normalizeProjectKey(p) {
|
|
|
21
22
|
}
|
|
22
23
|
}
|
|
23
24
|
|
|
25
|
+
/**
|
|
26
|
+
* Attach deliveryStatus / nextAction / hasDeliverables from a session reader.
|
|
27
|
+
* @param {ReturnType<typeof buildProjectList>} bag
|
|
28
|
+
* @param {(projectPath: string) => object|null|undefined} readSession
|
|
29
|
+
*/
|
|
30
|
+
export function enrichProjectsWithDeliveryStatus(bag, readSession) {
|
|
31
|
+
const projects = Array.isArray(bag?.projects) ? bag.projects : [];
|
|
32
|
+
for (const p of projects) {
|
|
33
|
+
let session = null;
|
|
34
|
+
try {
|
|
35
|
+
session = typeof readSession === "function" ? readSession(p.path) : null;
|
|
36
|
+
} catch {
|
|
37
|
+
session = null;
|
|
38
|
+
}
|
|
39
|
+
const latestJob = Array.isArray(p.jobs) && p.jobs.length ? p.jobs[0] : null;
|
|
40
|
+
const derived = deriveProjectDeliveryStatus(session, {
|
|
41
|
+
jobStatus: latestJob?.status || null,
|
|
42
|
+
deliveryAccepted:
|
|
43
|
+
String(latestJob?.status || "").toLowerCase() === "accepted",
|
|
44
|
+
});
|
|
45
|
+
p.deliveryStatus = derived.status;
|
|
46
|
+
p.nextAction = derived.nextAction;
|
|
47
|
+
p.hasDeliverables = derived.hasDeliverables;
|
|
48
|
+
p.stageSummary = derived.stages;
|
|
49
|
+
}
|
|
50
|
+
return bag;
|
|
51
|
+
}
|
|
52
|
+
|
|
24
53
|
/**
|
|
25
54
|
* Build project rows from remembered repos + jobs.
|
|
26
55
|
* @param {{
|
|
@@ -34,7 +63,7 @@ export function buildProjectList(input = {}) {
|
|
|
34
63
|
const jobs = Array.isArray(input.jobs) ? input.jobs : [];
|
|
35
64
|
const active = normalizeProjectKey(input.activeProjectPath);
|
|
36
65
|
|
|
37
|
-
/** @type {Map<string,
|
|
66
|
+
/** @type {Map<string, object>} */
|
|
38
67
|
const map = new Map();
|
|
39
68
|
|
|
40
69
|
function ensure(pathRaw, meta = {}) {
|
|
@@ -52,6 +81,10 @@ export function buildProjectList(input = {}) {
|
|
|
52
81
|
description: descHint,
|
|
53
82
|
lastUsedAt: lastUsedAt || null,
|
|
54
83
|
jobs: [],
|
|
84
|
+
deliveryStatus: "drafting",
|
|
85
|
+
nextAction: "chat",
|
|
86
|
+
hasDeliverables: false,
|
|
87
|
+
stageSummary: [],
|
|
55
88
|
};
|
|
56
89
|
map.set(key, row);
|
|
57
90
|
} else {
|
|
@@ -131,10 +131,13 @@ the table for their app.
|
|
|
131
131
|
| E2E-149 | 初版 · 需求卡 accordion body uses structured lists/paragraphs (`req-list` / `req-para`); goal prose does not shatter on `、` | `npm test` + `npm run test:live` |
|
|
132
132
|
| E2E-150 | Requirements click-to-edit then blur without changes keeps field text (no wipe) | `npm run test:live` + manual |
|
|
133
133
|
| E2E-151 | Modular `[模块]` card fields render section titles + numbered acceptance lists | `npm test` + `npm run test:live` |
|
|
134
|
+
| E2E-152 | Project portfolio shows delivery status chips + open deliverables; Progress shows stage next-action | `npm test` + `npm run test:live` |
|
|
135
|
+
| E2E-153 | `duaer live` starts without model config, opens desk URL, Settings guides setup | `npm test` + `npm run test:live` |
|
|
136
|
+
| E2E-154 | 产品父目录 is read-only; 选择… opens native folder pick and saves; 清除 empties; no path typing | `npm test` + manual |
|
|
134
137
|
| E2E-093 | Header top actions share one button chrome; GitHub control links to duaer-spec and shows live stars via `/api/github` | `npm run test:live` + manual |
|
|
135
138
|
| E2E-063 | Duaer-spec FDE brand: header/title show Duaer-spec FDE; zh secondary mark is 现场开发; en mark is Field Development Environment; setup hint / desk-model guide name Duaer | `npm test` + manual |
|
|
136
139
|
| E2E-078 | Claude Terminal launch includes `--permission-mode bypassPermissions` and stamps `hasTrustDialogAccepted` for the worktree | `npm test` + manual |
|
|
137
|
-
| E2E-077 | Missing product path auto-created; projectsRoot
|
|
140
|
+
| E2E-077 | Missing product path auto-created; projectsRoot via native folder pick (no typing); relative name creates under parent | `npm test` + manual |
|
|
138
141
|
| E2E-076 | Worker CLIs are Cursor Agent + Claude Code only; no deepseek-tui worker; tutorial docs/agent/worker-models(.zh-CN).md | `npm test` + manual |
|
|
139
142
|
| E2E-075 | DeepSeek Terminal launch uses `deepseek -w <worktree> --yolo` (not `--workspace`); job exits 0 when CLI present | `npm test` + manual |
|
|
140
143
|
| E2E-074 | Missing git: auto-install then init; worker CLIs versioned; 10-day auto-upgrade stamp under `~/.duaer/cli-tools-check.json` (opt-out `DUAER_NO_CLI_UPGRADE=1`) | `npm test` + manual |
|
|
@@ -307,3 +310,6 @@ the table for their app.
|
|
|
307
310
|
| E2E-149 | `185-req-card-structure` | structured 初版需求卡 accordion |
|
|
308
311
|
| E2E-150 | `186-req-edit-wipe` | click-edit blur must not wipe fields |
|
|
309
312
|
| E2E-151 | `187-req-card-modules` | modular `[模块]` structured sections |
|
|
313
|
+
| E2E-152 | `189-fde-portfolio` | portfolio status + delivery cockpit |
|
|
314
|
+
| E2E-153 | `190-live-open-no-model` | live opens desk without model gate |
|
|
315
|
+
| E2E-154 | `191-projects-root-pick` | projectsRoot via native folder pick |
|
package/docs/agent/live-desk.md
CHANGED
|
@@ -4,6 +4,10 @@ Field Development Environment for digital employees. Config and Briefs live unde
|
|
|
4
4
|
`~/.duaer/live/`. Product work runs in isolated worktrees inside the chosen
|
|
5
5
|
product repo.
|
|
6
6
|
|
|
7
|
+
`duaer live` always starts the desk and opens the browser URL. A missing model
|
|
8
|
+
does not block startup — the Settings drawer opens so the operator can save
|
|
9
|
+
credentials, then chat.
|
|
10
|
+
|
|
7
11
|
Also see root [`README.md`](../../README.md) (EN) / [`README.zh-CN.md`](../../README.zh-CN.md).
|
|
8
12
|
|
|
9
13
|
## Layout
|
|
@@ -14,7 +18,24 @@ Three columns:
|
|
|
14
18
|
|---|---|
|
|
15
19
|
| Chat | Project dialogue; Markdown bubbles; messy multi-topic talk |
|
|
16
20
|
| Center | Module tabs + confirm cards → architecture → kickoff / revise |
|
|
17
|
-
| Progress |
|
|
21
|
+
| Progress | Delivery cockpit (stages + next action) · **View deliverables** · `tasks.md` polling · result bar |
|
|
22
|
+
|
|
23
|
+
## Project portfolio
|
|
24
|
+
|
|
25
|
+
The **Projects** drawer lists remembered product folders. Each row shows a
|
|
26
|
+
**delivery status** chip (`drafting` / `confirming` / `building` / `delivered` /
|
|
27
|
+
`revising`) derived from the per-project session under
|
|
28
|
+
`~/.duaer/live/project-chats/`. When the session has anything to show, a short
|
|
29
|
+
**Deliverables** control opens the dossier without hunting through progress.
|
|
30
|
+
|
|
31
|
+
`GET /api/projects` includes `deliveryStatus`, `nextAction`, and
|
|
32
|
+
`hasDeliverables` on each project row.
|
|
33
|
+
|
|
34
|
+
## Delivery cockpit (Progress)
|
|
35
|
+
|
|
36
|
+
With an active project, Progress shows a stage strip (requirements →
|
|
37
|
+
architecture → kickoff → delivery → revise) and one **next action** line in
|
|
38
|
+
plain language. **View deliverables** stays the primary CTA above the strip.
|
|
18
39
|
|
|
19
40
|
## Stage deliverables page
|
|
20
41
|
|
package/package.json
CHANGED
package/web/live-dev/app.js
CHANGED
|
@@ -11,6 +11,10 @@ import {
|
|
|
11
11
|
applyDomI18n,
|
|
12
12
|
} from "./i18n.js";
|
|
13
13
|
import { structuredHtml, escapeHtml, reqEditModel, serializeReqEdit } from "./structured-html.mjs";
|
|
14
|
+
import {
|
|
15
|
+
deriveProjectDeliveryStatus,
|
|
16
|
+
stageTone,
|
|
17
|
+
} from "./delivery-status.mjs";
|
|
14
18
|
import { renderChatMarkdown } from "./chat-markdown.mjs";
|
|
15
19
|
import { extractArchitectureIr } from "./architecture-ir.mjs";
|
|
16
20
|
import {
|
|
@@ -211,7 +215,8 @@ const el = {
|
|
|
211
215
|
repoList: document.getElementById("repoList"),
|
|
212
216
|
repoPath: document.getElementById("repoPath"),
|
|
213
217
|
projectsRoot: document.getElementById("projectsRoot"),
|
|
214
|
-
|
|
218
|
+
pickProjectsRoot: document.getElementById("pickProjectsRoot"),
|
|
219
|
+
clearProjectsRoot: document.getElementById("clearProjectsRoot"),
|
|
215
220
|
projectFolder: document.getElementById("projectFolder"),
|
|
216
221
|
projectTitle: document.getElementById("projectTitle"),
|
|
217
222
|
projectDescription: document.getElementById("projectDescription"),
|
|
@@ -255,6 +260,9 @@ const el = {
|
|
|
255
260
|
dispatchErr: document.getElementById("dispatchErr"),
|
|
256
261
|
dispatchStatus: document.getElementById("dispatchStatus"),
|
|
257
262
|
openDeliverables: document.getElementById("openDeliverables"),
|
|
263
|
+
deliveryCockpit: document.getElementById("deliveryCockpit"),
|
|
264
|
+
deliveryStages: document.getElementById("deliveryStages"),
|
|
265
|
+
deliveryNextAction: document.getElementById("deliveryNextAction"),
|
|
258
266
|
progressCol: document.querySelector(".progress-col"),
|
|
259
267
|
progressEmpty: document.getElementById("progressEmpty"),
|
|
260
268
|
runTimeline: document.getElementById("runTimeline"),
|
|
@@ -589,6 +597,53 @@ function setActiveProject(path, name, description) {
|
|
|
589
597
|
function syncDeliverablesEntry() {
|
|
590
598
|
if (!el.openDeliverables) return;
|
|
591
599
|
el.openDeliverables.hidden = !state.projectPath;
|
|
600
|
+
paintDeliveryCockpit();
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
function deskSessionSnapshot() {
|
|
604
|
+
return {
|
|
605
|
+
modules: state.modules || [],
|
|
606
|
+
architecture: state.architecture || {},
|
|
607
|
+
jobId: state.jobId || null,
|
|
608
|
+
reviseCards: state.reviseCards || [],
|
|
609
|
+
reviseLocked: Boolean(state.reviseLocked),
|
|
610
|
+
revisePlanConfirmed: Boolean(state.revisePlanConfirmed),
|
|
611
|
+
lastRevision: state.lastRevision || null,
|
|
612
|
+
originalCard: state.originalCard || null,
|
|
613
|
+
};
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
function paintDeliveryCockpit(statusHint = null) {
|
|
617
|
+
if (!el.deliveryCockpit) return;
|
|
618
|
+
if (!state.projectPath) {
|
|
619
|
+
el.deliveryCockpit.hidden = true;
|
|
620
|
+
return;
|
|
621
|
+
}
|
|
622
|
+
const hint =
|
|
623
|
+
statusHint && typeof statusHint === "object" ? statusHint : {};
|
|
624
|
+
const progress = hint.progress;
|
|
625
|
+
const derived = deriveProjectDeliveryStatus(deskSessionSnapshot(), {
|
|
626
|
+
jobStatus: hint.status || hint.jobStatus || null,
|
|
627
|
+
deliveryAccepted:
|
|
628
|
+
hint.delivery?.status === "accepted" ||
|
|
629
|
+
String(hint.status || "").toLowerCase() === "accepted",
|
|
630
|
+
progressDone: progress?.done,
|
|
631
|
+
progressTotal: progress?.total,
|
|
632
|
+
});
|
|
633
|
+
el.deliveryCockpit.hidden = false;
|
|
634
|
+
if (el.deliveryStages) {
|
|
635
|
+
el.deliveryStages.replaceChildren();
|
|
636
|
+
for (const st of stageTone(derived.stages)) {
|
|
637
|
+
const li = document.createElement("li");
|
|
638
|
+
li.className = `delivery-stage is-${st.tone}`;
|
|
639
|
+
li.dataset.stage = st.id;
|
|
640
|
+
li.textContent = t(`cockpit.stage.${st.id}`);
|
|
641
|
+
el.deliveryStages.appendChild(li);
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
if (el.deliveryNextAction) {
|
|
645
|
+
el.deliveryNextAction.textContent = t(`cockpit.next.${derived.nextAction}`);
|
|
646
|
+
}
|
|
592
647
|
}
|
|
593
648
|
|
|
594
649
|
function openDeliverablesPage() {
|
|
@@ -791,6 +846,7 @@ function renderModuleTabs() {
|
|
|
791
846
|
total: String(list.length),
|
|
792
847
|
});
|
|
793
848
|
}
|
|
849
|
+
paintDeliveryCockpit();
|
|
794
850
|
}
|
|
795
851
|
|
|
796
852
|
function mergeModulesFromChatPayload(data) {
|
|
@@ -2661,10 +2717,6 @@ function syncDrawerBackdrop() {
|
|
|
2661
2717
|
function setSettingsOpen(open) {
|
|
2662
2718
|
if (!el.settingsPanel) return;
|
|
2663
2719
|
const want = Boolean(open);
|
|
2664
|
-
if (!want && !state.ready) {
|
|
2665
|
-
// First-time config: keep drawer open until saved.
|
|
2666
|
-
return;
|
|
2667
|
-
}
|
|
2668
2720
|
if (want && el.historyPanel && !el.historyPanel.hidden) {
|
|
2669
2721
|
el.historyPanel.hidden = true;
|
|
2670
2722
|
if (el.historyToggle) {
|
|
@@ -2676,7 +2728,7 @@ function setSettingsOpen(open) {
|
|
|
2676
2728
|
el.cfgOpen.setAttribute("aria-expanded", want ? "true" : "false");
|
|
2677
2729
|
}
|
|
2678
2730
|
if (el.settingsClose) {
|
|
2679
|
-
el.settingsClose.hidden =
|
|
2731
|
+
el.settingsClose.hidden = false;
|
|
2680
2732
|
}
|
|
2681
2733
|
syncDrawerBackdrop();
|
|
2682
2734
|
}
|
|
@@ -2759,10 +2811,14 @@ function showSetup(cfg) {
|
|
|
2759
2811
|
fillAwsFields(cfg);
|
|
2760
2812
|
const id = cfg?.provider || "deepseek";
|
|
2761
2813
|
applyProvider(id, { fillEmptyOnly: Boolean(cfg?.baseUrl || cfg?.model) });
|
|
2762
|
-
|
|
2763
|
-
|
|
2764
|
-
|
|
2814
|
+
// Always show the desk; open Settings so the operator can fill the model.
|
|
2815
|
+
if (el.desk) el.desk.hidden = false;
|
|
2816
|
+
state.ready = false;
|
|
2817
|
+
syncComposerEnabled();
|
|
2765
2818
|
setSettingsOpen(true);
|
|
2819
|
+
if (el.meta) {
|
|
2820
|
+
el.meta.textContent = t("setup.hintOpenSettings");
|
|
2821
|
+
}
|
|
2766
2822
|
}
|
|
2767
2823
|
|
|
2768
2824
|
function showDesk(cfg) {
|
|
@@ -4035,6 +4091,7 @@ async function renderArchitectureFromIr(ir) {
|
|
|
4035
4091
|
if (!state.architecture.fingerprint) state.architecture.fingerprint = fp;
|
|
4036
4092
|
}
|
|
4037
4093
|
syncArchitecturePanel();
|
|
4094
|
+
paintDeliveryCockpit();
|
|
4038
4095
|
schedulePersistProjectDesk();
|
|
4039
4096
|
return true;
|
|
4040
4097
|
}
|
|
@@ -4933,36 +4990,65 @@ el.architectureRetry?.addEventListener("click", () => {
|
|
|
4933
4990
|
retryArchitectureDesign();
|
|
4934
4991
|
});
|
|
4935
4992
|
|
|
4936
|
-
el.
|
|
4993
|
+
el.pickProjectsRoot?.addEventListener("click", async () => {
|
|
4937
4994
|
if (el.historyErr) el.historyErr.hidden = true;
|
|
4938
4995
|
if (el.dispatchErr) el.dispatchErr.hidden = true;
|
|
4996
|
+
if (el.pickProjectsRoot) {
|
|
4997
|
+
el.pickProjectsRoot.disabled = true;
|
|
4998
|
+
el.pickProjectsRoot.textContent = t("dispatch.pickProjectsRootBusy");
|
|
4999
|
+
}
|
|
5000
|
+
try {
|
|
5001
|
+
const res = await fetch("/api/projects/pick-root", { method: "POST" });
|
|
5002
|
+
const data = await res.json().catch(() => ({}));
|
|
5003
|
+
if (!res.ok) {
|
|
5004
|
+
if (data.cancelled) return;
|
|
5005
|
+
throw new Error(data.error || t("dispatch.projectsRootPickFail"));
|
|
5006
|
+
}
|
|
5007
|
+
if (el.projectsRoot) el.projectsRoot.value = data.projectsRoot || "";
|
|
5008
|
+
state.lastCfg = { ...(state.lastCfg || {}), ...data };
|
|
5009
|
+
if (Array.isArray(data.projects)) {
|
|
5010
|
+
state.projects = data.projects;
|
|
5011
|
+
renderProjectList();
|
|
5012
|
+
}
|
|
5013
|
+
if (el.historyErr) {
|
|
5014
|
+
el.historyErr.hidden = false;
|
|
5015
|
+
el.historyErr.textContent = t("dispatch.projectsRootSaved");
|
|
5016
|
+
}
|
|
5017
|
+
} catch (err) {
|
|
5018
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
5019
|
+
if (el.historyErr) {
|
|
5020
|
+
el.historyErr.hidden = false;
|
|
5021
|
+
el.historyErr.textContent = msg;
|
|
5022
|
+
}
|
|
5023
|
+
} finally {
|
|
5024
|
+
if (el.pickProjectsRoot) {
|
|
5025
|
+
el.pickProjectsRoot.disabled = false;
|
|
5026
|
+
el.pickProjectsRoot.textContent = t("dispatch.pickProjectsRoot");
|
|
5027
|
+
}
|
|
5028
|
+
}
|
|
5029
|
+
});
|
|
5030
|
+
|
|
5031
|
+
el.clearProjectsRoot?.addEventListener("click", async () => {
|
|
5032
|
+
if (el.historyErr) el.historyErr.hidden = true;
|
|
4939
5033
|
try {
|
|
4940
5034
|
const res = await fetch("/api/config", {
|
|
4941
5035
|
method: "POST",
|
|
4942
5036
|
headers: { "content-type": "application/json" },
|
|
4943
|
-
body: JSON.stringify({
|
|
4944
|
-
projectsRoot: (el.projectsRoot?.value || "").trim(),
|
|
4945
|
-
}),
|
|
5037
|
+
body: JSON.stringify({ projectsRoot: "" }),
|
|
4946
5038
|
});
|
|
4947
5039
|
const data = await res.json();
|
|
4948
5040
|
if (!res.ok) throw new Error(data.error || t("dispatch.projectsRootFail"));
|
|
4949
|
-
if (el.projectsRoot) el.projectsRoot.value =
|
|
5041
|
+
if (el.projectsRoot) el.projectsRoot.value = "";
|
|
4950
5042
|
state.lastCfg = { ...(state.lastCfg || {}), ...data };
|
|
4951
5043
|
if (el.historyErr) {
|
|
4952
5044
|
el.historyErr.hidden = false;
|
|
4953
|
-
el.historyErr.textContent = t("dispatch.
|
|
4954
|
-
} else if (el.dispatchStatus) {
|
|
4955
|
-
el.dispatchStatus.hidden = false;
|
|
4956
|
-
el.dispatchStatus.textContent = t("dispatch.projectsRootSaved");
|
|
5045
|
+
el.historyErr.textContent = t("dispatch.projectsRootCleared");
|
|
4957
5046
|
}
|
|
4958
5047
|
} catch (err) {
|
|
4959
5048
|
const msg = err instanceof Error ? err.message : String(err);
|
|
4960
5049
|
if (el.historyErr) {
|
|
4961
5050
|
el.historyErr.hidden = false;
|
|
4962
5051
|
el.historyErr.textContent = msg;
|
|
4963
|
-
} else if (el.dispatchErr) {
|
|
4964
|
-
el.dispatchErr.hidden = false;
|
|
4965
|
-
el.dispatchErr.textContent = msg;
|
|
4966
5052
|
}
|
|
4967
5053
|
}
|
|
4968
5054
|
});
|
|
@@ -5492,6 +5578,7 @@ function fillRunProgress(block, data) {
|
|
|
5492
5578
|
|
|
5493
5579
|
function renderProgress(data) {
|
|
5494
5580
|
if (!el.runTimeline) return;
|
|
5581
|
+
paintDeliveryCockpit(data);
|
|
5495
5582
|
const progress = data?.progress;
|
|
5496
5583
|
if (!progress) {
|
|
5497
5584
|
if (!state.activeRun) el.runTimeline.hidden = el.runTimeline.childElementCount === 0;
|
|
@@ -6865,20 +6952,22 @@ function renderProjectList() {
|
|
|
6865
6952
|
}
|
|
6866
6953
|
const active = normalizePathKey(state.projectPath);
|
|
6867
6954
|
for (const proj of state.projects) {
|
|
6955
|
+
const row = document.createElement("div");
|
|
6956
|
+
row.className = "project-row";
|
|
6957
|
+
const pressed = normalizePathKey(proj.path) === active;
|
|
6958
|
+
if (pressed) row.classList.add("is-active");
|
|
6959
|
+
|
|
6868
6960
|
const btn = document.createElement("button");
|
|
6869
6961
|
btn.type = "button";
|
|
6870
6962
|
btn.className = "history-item project-item";
|
|
6871
6963
|
btn.dataset.path = proj.path;
|
|
6872
|
-
const pressed = normalizePathKey(proj.path) === active;
|
|
6873
6964
|
btn.setAttribute("aria-pressed", pressed ? "true" : "false");
|
|
6874
6965
|
const goal = document.createElement("span");
|
|
6875
6966
|
goal.className = "history-item-goal";
|
|
6876
6967
|
goal.textContent = proj.title || proj.name || proj.path;
|
|
6877
6968
|
const st = document.createElement("span");
|
|
6878
|
-
st.className =
|
|
6879
|
-
st.textContent = t(
|
|
6880
|
-
n: String((proj.jobs || []).length),
|
|
6881
|
-
});
|
|
6969
|
+
st.className = `history-item-status project-status is-${proj.deliveryStatus || "drafting"}`;
|
|
6970
|
+
st.textContent = t(`project.status.${proj.deliveryStatus || "drafting"}`);
|
|
6882
6971
|
const meta = document.createElement("span");
|
|
6883
6972
|
meta.className = "history-item-meta";
|
|
6884
6973
|
meta.textContent = proj.description
|
|
@@ -6894,7 +6983,33 @@ function renderProjectList() {
|
|
|
6894
6983
|
requireMeta: false,
|
|
6895
6984
|
});
|
|
6896
6985
|
});
|
|
6897
|
-
|
|
6986
|
+
row.appendChild(btn);
|
|
6987
|
+
|
|
6988
|
+
if (proj.hasDeliverables) {
|
|
6989
|
+
const deliv = document.createElement("button");
|
|
6990
|
+
deliv.type = "button";
|
|
6991
|
+
deliv.className = "btn project-deliverables";
|
|
6992
|
+
deliv.textContent = t("deliverables.openShort");
|
|
6993
|
+
deliv.title = t("deliverables.openHint");
|
|
6994
|
+
deliv.addEventListener("click", (ev) => {
|
|
6995
|
+
ev.preventDefault();
|
|
6996
|
+
ev.stopPropagation();
|
|
6997
|
+
const open = async () => {
|
|
6998
|
+
if (normalizePathKey(state.projectPath) !== normalizePathKey(proj.path)) {
|
|
6999
|
+
await activateProjectPath(proj.path, {
|
|
7000
|
+
title: proj.title || proj.name,
|
|
7001
|
+
description: proj.description || "",
|
|
7002
|
+
requireMeta: false,
|
|
7003
|
+
});
|
|
7004
|
+
}
|
|
7005
|
+
openDeliverablesPage();
|
|
7006
|
+
};
|
|
7007
|
+
void open();
|
|
7008
|
+
});
|
|
7009
|
+
row.appendChild(deliv);
|
|
7010
|
+
}
|
|
7011
|
+
|
|
7012
|
+
el.projectList.appendChild(row);
|
|
6898
7013
|
}
|
|
6899
7014
|
}
|
|
6900
7015
|
|
|
@@ -7224,6 +7339,8 @@ el.projectBrowse?.addEventListener("click", async () => {
|
|
|
7224
7339
|
onLocaleChange(() => {
|
|
7225
7340
|
syncDynamicI18n();
|
|
7226
7341
|
syncReviseCardChrome();
|
|
7342
|
+
paintDeliveryCockpit();
|
|
7343
|
+
renderProjectList();
|
|
7227
7344
|
if (el.previewPanel && !el.previewPanel.hidden && state.lastStatus) {
|
|
7228
7345
|
renderPreview(state.lastStatus);
|
|
7229
7346
|
} else {
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure delivery status for FDE project portfolio + progress cockpit.
|
|
3
|
+
* Shared by live server (project list) and desk UI.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* @param {unknown} modules
|
|
8
|
+
*/
|
|
9
|
+
function modulesAllConfirmed(modules) {
|
|
10
|
+
const list = Array.isArray(modules) ? modules : [];
|
|
11
|
+
if (!list.length) return false;
|
|
12
|
+
return list.every((m) => m && m.status === "confirmed");
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* @typedef {'drafting'|'confirming'|'building'|'delivered'|'revising'} DeliveryStatus
|
|
17
|
+
* @typedef {'chat'|'confirm_module'|'confirm_arch'|'kickoff'|'wait_workers'|'view_deliverables'|'revise'} NextAction
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Derive portfolio / cockpit status from a project chat session (+ optional job).
|
|
22
|
+
* @param {object|null|undefined} session
|
|
23
|
+
* @param {{
|
|
24
|
+
* jobStatus?: string|null,
|
|
25
|
+
* deliveryAccepted?: boolean,
|
|
26
|
+
* progressDone?: number,
|
|
27
|
+
* progressTotal?: number,
|
|
28
|
+
* }} [hint]
|
|
29
|
+
*/
|
|
30
|
+
export function deriveProjectDeliveryStatus(session, hint = {}) {
|
|
31
|
+
const s = session && typeof session === "object" ? session : {};
|
|
32
|
+
const modules = Array.isArray(s.modules) ? s.modules : [];
|
|
33
|
+
const confirmed = modules.filter((m) => m && m.status === "confirmed");
|
|
34
|
+
const allReq = modulesAllConfirmed(modules);
|
|
35
|
+
const arch =
|
|
36
|
+
s.architecture && typeof s.architecture === "object" ? s.architecture : {};
|
|
37
|
+
const reviseCards = Array.isArray(s.reviseCards) ? s.reviseCards : [];
|
|
38
|
+
const hasJob = Boolean(s.jobId);
|
|
39
|
+
const jobStatus = String(hint.jobStatus || "").toLowerCase();
|
|
40
|
+
const deliveryAccepted =
|
|
41
|
+
Boolean(hint.deliveryAccepted) || jobStatus === "accepted";
|
|
42
|
+
const progressTotal = Number(hint.progressTotal) || 0;
|
|
43
|
+
const progressDone = Number(hint.progressDone) || 0;
|
|
44
|
+
const buildingIncomplete =
|
|
45
|
+
hasJob &&
|
|
46
|
+
!deliveryAccepted &&
|
|
47
|
+
progressTotal > 0 &&
|
|
48
|
+
progressDone < progressTotal;
|
|
49
|
+
|
|
50
|
+
/** @type {DeliveryStatus} */
|
|
51
|
+
let status = "drafting";
|
|
52
|
+
/** @type {NextAction} */
|
|
53
|
+
let nextAction = "chat";
|
|
54
|
+
|
|
55
|
+
if (deliveryAccepted && (reviseCards.length > 0 || s.reviseLocked)) {
|
|
56
|
+
status = "revising";
|
|
57
|
+
nextAction = s.revisePlanConfirmed ? "kickoff" : "revise";
|
|
58
|
+
} else if (deliveryAccepted) {
|
|
59
|
+
status = "delivered";
|
|
60
|
+
nextAction = "view_deliverables";
|
|
61
|
+
} else if (reviseCards.length > 0 || Number(s.lastRevision?.revision) > 0) {
|
|
62
|
+
status = "revising";
|
|
63
|
+
nextAction = "revise";
|
|
64
|
+
} else if (hasJob) {
|
|
65
|
+
status = "building";
|
|
66
|
+
nextAction = buildingIncomplete ? "wait_workers" : "view_deliverables";
|
|
67
|
+
} else if (allReq && arch.confirmed) {
|
|
68
|
+
status = "confirming";
|
|
69
|
+
nextAction = "kickoff";
|
|
70
|
+
} else if (allReq && !arch.confirmed) {
|
|
71
|
+
status = "confirming";
|
|
72
|
+
nextAction = "confirm_arch";
|
|
73
|
+
} else if (confirmed.length > 0 || modules.length > 0) {
|
|
74
|
+
status = "confirming";
|
|
75
|
+
nextAction = "confirm_module";
|
|
76
|
+
} else {
|
|
77
|
+
status = "drafting";
|
|
78
|
+
nextAction = "chat";
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const stages = [
|
|
82
|
+
{
|
|
83
|
+
id: "requirements",
|
|
84
|
+
done: allReq,
|
|
85
|
+
partial: confirmed.length > 0 && !allReq,
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
id: "architecture",
|
|
89
|
+
done: Boolean(arch.confirmed),
|
|
90
|
+
partial: Boolean(arch.url) && !arch.confirmed,
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
id: "kickoff",
|
|
94
|
+
done: hasJob,
|
|
95
|
+
partial: false,
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
id: "delivery",
|
|
99
|
+
done: deliveryAccepted,
|
|
100
|
+
partial: hasJob && !deliveryAccepted,
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
id: "revise",
|
|
104
|
+
done: reviseCards.length > 0 && deliveryAccepted,
|
|
105
|
+
partial: reviseCards.length > 0 && !deliveryAccepted,
|
|
106
|
+
},
|
|
107
|
+
];
|
|
108
|
+
|
|
109
|
+
const hasDeliverables =
|
|
110
|
+
confirmed.length > 0 ||
|
|
111
|
+
Boolean(arch.url) ||
|
|
112
|
+
hasJob ||
|
|
113
|
+
reviseCards.length > 0 ||
|
|
114
|
+
Boolean(s.originalCard?.goal);
|
|
115
|
+
|
|
116
|
+
return {
|
|
117
|
+
status,
|
|
118
|
+
nextAction,
|
|
119
|
+
hasDeliverables,
|
|
120
|
+
stages,
|
|
121
|
+
confirmedCount: confirmed.length,
|
|
122
|
+
moduleCount: modules.length,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Compact stage tokens for UI (done | partial | empty).
|
|
128
|
+
* @param {ReturnType<typeof deriveProjectDeliveryStatus>['stages']} stages
|
|
129
|
+
*/
|
|
130
|
+
export function stageTone(stages) {
|
|
131
|
+
return (Array.isArray(stages) ? stages : []).map((st) => ({
|
|
132
|
+
id: st.id,
|
|
133
|
+
tone: st.done ? "done" : st.partial ? "partial" : "empty",
|
|
134
|
+
}));
|
|
135
|
+
}
|
package/web/live-dev/i18n.js
CHANGED
|
@@ -59,11 +59,17 @@ const zhCN = {
|
|
|
59
59
|
"project.needName": "请填写项目名或路径",
|
|
60
60
|
"project.fail": "无法打开项目",
|
|
61
61
|
"project.jobCount": "{n} 条对话",
|
|
62
|
+
"project.status.drafting": "起草中",
|
|
63
|
+
"project.status.confirming": "确认中",
|
|
64
|
+
"project.status.building": "实现中",
|
|
65
|
+
"project.status.delivered": "已交付",
|
|
66
|
+
"project.status.revising": "改进中",
|
|
62
67
|
"project.unassigned": "未归入项目的对话(选项目后确认会挂上)",
|
|
63
68
|
"bot.needProject": "请先点右上角「项目」选择或新建产品目录,再开始对话。",
|
|
64
69
|
"setup.mark": "Duaer-spec FDE · model",
|
|
65
70
|
"setup.title": "配置模型",
|
|
66
71
|
"setup.hint": "先接好 Duaer 台面模型,再在台面说需求。写入 ~/.duaer/live/config.json",
|
|
72
|
+
"setup.hintOpenSettings": "请在「设置」里配置模型后保存,即可开始对话",
|
|
67
73
|
"setup.provider": "服务商",
|
|
68
74
|
"setup.providerAria": "服务商预设",
|
|
69
75
|
"setup.save": "保存并开始",
|
|
@@ -254,9 +260,15 @@ const zhCN = {
|
|
|
254
260
|
"arch.needConfirm": "请先确认架构再派工",
|
|
255
261
|
"dispatch.projectsRoot": "产品父目录",
|
|
256
262
|
"dispatch.projectsRootPh": "/Users/…/Projects",
|
|
263
|
+
"dispatch.projectsRootEmpty": "尚未选择",
|
|
264
|
+
"dispatch.pickProjectsRoot": "选择…",
|
|
265
|
+
"dispatch.clearProjectsRoot": "清除",
|
|
266
|
+
"dispatch.pickProjectsRootBusy": "选择中…",
|
|
257
267
|
"dispatch.saveProjectsRoot": "保存父目录",
|
|
258
268
|
"dispatch.projectsRootSaved": "父目录已保存",
|
|
269
|
+
"dispatch.projectsRootCleared": "已清除父目录",
|
|
259
270
|
"dispatch.projectsRootFail": "父目录保存失败",
|
|
271
|
+
"dispatch.projectsRootPickFail": "无法选择父目录",
|
|
260
272
|
"dispatch.agentLabel": "用哪个 CLI 数字员工启动",
|
|
261
273
|
"dispatch.agentHint": "检测本机 PATH 上的 Cursor Agent / Claude Code",
|
|
262
274
|
"dispatch.redetect": "重新检测",
|
|
@@ -396,9 +408,22 @@ const zhCN = {
|
|
|
396
408
|
"progress.title": "任务进度",
|
|
397
409
|
"progress.aria": "任务进度",
|
|
398
410
|
"deliverables.open": "查看交付物",
|
|
411
|
+
"deliverables.openShort": "交付物",
|
|
399
412
|
"deliverables.openHint": "新标签打开结构化交付物档案(需求文档 / 确认书 / 架构…)",
|
|
413
|
+
"cockpit.stage.requirements": "需求",
|
|
414
|
+
"cockpit.stage.architecture": "架构",
|
|
415
|
+
"cockpit.stage.kickoff": "开工",
|
|
416
|
+
"cockpit.stage.delivery": "交付",
|
|
417
|
+
"cockpit.stage.revise": "改进",
|
|
418
|
+
"cockpit.next.chat": "下一步:在对话里说清要做什么",
|
|
419
|
+
"cockpit.next.confirm_module": "下一步:确认当前模块需求卡",
|
|
420
|
+
"cockpit.next.confirm_arch": "下一步:确认系统架构",
|
|
421
|
+
"cockpit.next.kickoff": "下一步:开工派工",
|
|
422
|
+
"cockpit.next.wait_workers": "下一步:等待数字员工完成任务",
|
|
423
|
+
"cockpit.next.view_deliverables": "下一步:查看交付物或再改一版",
|
|
424
|
+
"cockpit.next.revise": "下一步:写清改进方案并派一版",
|
|
400
425
|
"progress.emptyTitle": "还没有进度",
|
|
401
|
-
"progress.empty": "
|
|
426
|
+
"progress.empty": "有项目后这里会提示下一步;派工后显示各轮任务",
|
|
402
427
|
"run.mark": "Run",
|
|
403
428
|
"run.dispatch": "派工进度",
|
|
404
429
|
"run.dispatchWorkers": "派工进度 · {n} 名员工",
|
|
@@ -546,11 +571,17 @@ const en = {
|
|
|
546
571
|
"project.needName": "Enter a project name or path",
|
|
547
572
|
"project.fail": "Could not open project",
|
|
548
573
|
"project.jobCount": "{n} chats",
|
|
574
|
+
"project.status.drafting": "Drafting",
|
|
575
|
+
"project.status.confirming": "Confirming",
|
|
576
|
+
"project.status.building": "Building",
|
|
577
|
+
"project.status.delivered": "Delivered",
|
|
578
|
+
"project.status.revising": "Revising",
|
|
549
579
|
"project.unassigned": "Unassigned chats (confirm after picking a project to attach)",
|
|
550
580
|
"bot.needProject": "Open Projects (top right) and select or create a product folder before chatting.",
|
|
551
581
|
"setup.mark": "Duaer-spec FDE · model",
|
|
552
582
|
"setup.title": "Configure model",
|
|
553
583
|
"setup.hint": "Connect a Duaer desk model first, then state the job. Saved to ~/.duaer/live/config.json",
|
|
584
|
+
"setup.hintOpenSettings": "Open Settings, save a model, then start chatting",
|
|
554
585
|
"setup.provider": "Provider",
|
|
555
586
|
"setup.providerAria": "Provider presets",
|
|
556
587
|
"setup.save": "Save and start",
|
|
@@ -715,9 +746,15 @@ const en = {
|
|
|
715
746
|
"dispatch.deploy.github-pages": "GitHub Pages",
|
|
716
747
|
"dispatch.projectsRoot": "Projects parent folder",
|
|
717
748
|
"dispatch.projectsRootPh": "/Users/…/Projects",
|
|
749
|
+
"dispatch.projectsRootEmpty": "Not set",
|
|
750
|
+
"dispatch.pickProjectsRoot": "Choose…",
|
|
751
|
+
"dispatch.clearProjectsRoot": "Clear",
|
|
752
|
+
"dispatch.pickProjectsRootBusy": "Choosing…",
|
|
718
753
|
"dispatch.saveProjectsRoot": "Save parent",
|
|
719
754
|
"dispatch.projectsRootSaved": "Parent folder saved",
|
|
755
|
+
"dispatch.projectsRootCleared": "Parent folder cleared",
|
|
720
756
|
"dispatch.projectsRootFail": "Could not save parent folder",
|
|
757
|
+
"dispatch.projectsRootPickFail": "Could not choose parent folder",
|
|
721
758
|
"arch.title": "Architecture",
|
|
722
759
|
"arch.titlePrevious": "Previous architecture",
|
|
723
760
|
"arch.titleNew": "New architecture",
|
|
@@ -899,9 +936,22 @@ const en = {
|
|
|
899
936
|
"progress.title": "Task progress",
|
|
900
937
|
"progress.aria": "Task progress",
|
|
901
938
|
"deliverables.open": "View deliverables",
|
|
939
|
+
"deliverables.openShort": "Deliverables",
|
|
902
940
|
"deliverables.openHint": "Opens a structured deliverables dossier in a new tab",
|
|
941
|
+
"cockpit.stage.requirements": "Requirements",
|
|
942
|
+
"cockpit.stage.architecture": "Architecture",
|
|
943
|
+
"cockpit.stage.kickoff": "Kickoff",
|
|
944
|
+
"cockpit.stage.delivery": "Delivery",
|
|
945
|
+
"cockpit.stage.revise": "Revise",
|
|
946
|
+
"cockpit.next.chat": "Next: describe what to build in chat",
|
|
947
|
+
"cockpit.next.confirm_module": "Next: confirm the active module card",
|
|
948
|
+
"cockpit.next.confirm_arch": "Next: confirm architecture",
|
|
949
|
+
"cockpit.next.kickoff": "Next: kick off dispatch",
|
|
950
|
+
"cockpit.next.wait_workers": "Next: wait for digital employees",
|
|
951
|
+
"cockpit.next.view_deliverables": "Next: view deliverables or revise",
|
|
952
|
+
"cockpit.next.revise": "Next: write the revise plan and dispatch",
|
|
903
953
|
"progress.emptyTitle": "No progress yet",
|
|
904
|
-
"progress.empty": "
|
|
954
|
+
"progress.empty": "With a project selected, the next step appears here; tasks show after dispatch",
|
|
905
955
|
"run.mark": "Run",
|
|
906
956
|
"run.dispatch": "Dispatch progress",
|
|
907
957
|
"run.dispatchWorkers": "Dispatch progress · {n} employees",
|
package/web/live-dev/index.html
CHANGED
|
@@ -302,7 +302,7 @@
|
|
|
302
302
|
<h2 class="card-title" data-i18n="progress.title">任务进度</h2>
|
|
303
303
|
<button
|
|
304
304
|
type="button"
|
|
305
|
-
class="btn deliverables-open"
|
|
305
|
+
class="btn deliverables-open primary"
|
|
306
306
|
id="openDeliverables"
|
|
307
307
|
data-i18n="deliverables.open"
|
|
308
308
|
data-i18n-title="deliverables.openHint"
|
|
@@ -313,6 +313,10 @@
|
|
|
313
313
|
</button>
|
|
314
314
|
<p id="dispatchStatus" class="meta progress-status" hidden></p>
|
|
315
315
|
</header>
|
|
316
|
+
<div id="deliveryCockpit" class="delivery-cockpit" hidden>
|
|
317
|
+
<ol class="delivery-stages" id="deliveryStages" aria-label="Delivery stages"></ol>
|
|
318
|
+
<p class="delivery-next" id="deliveryNextAction"></p>
|
|
319
|
+
</div>
|
|
316
320
|
<div id="progressEmpty" class="progress-empty">
|
|
317
321
|
<p class="progress-empty-title" data-i18n="progress.emptyTitle">还没有进度</p>
|
|
318
322
|
<p class="progress-empty-hint" data-i18n="progress.empty">派工后显示各轮任务与改动文件</p>
|
|
@@ -337,10 +341,18 @@
|
|
|
337
341
|
<p class="project-section-label" data-i18n="project.createTitle">新建 / 打开项目</p>
|
|
338
342
|
<label class="field">
|
|
339
343
|
<span data-i18n="dispatch.projectsRoot">产品父目录</span>
|
|
340
|
-
<input
|
|
344
|
+
<input
|
|
345
|
+
id="projectsRoot"
|
|
346
|
+
type="text"
|
|
347
|
+
readonly
|
|
348
|
+
data-i18n-placeholder="dispatch.projectsRootEmpty"
|
|
349
|
+
placeholder="尚未选择"
|
|
350
|
+
autocomplete="off"
|
|
351
|
+
/>
|
|
341
352
|
</label>
|
|
342
353
|
<div class="repo-actions repo-root-actions">
|
|
343
|
-
<button type="button" class="btn" id="
|
|
354
|
+
<button type="button" class="btn primary" id="pickProjectsRoot" data-i18n="dispatch.pickProjectsRoot">选择…</button>
|
|
355
|
+
<button type="button" class="btn" id="clearProjectsRoot" data-i18n="dispatch.clearProjectsRoot">清除</button>
|
|
344
356
|
</div>
|
|
345
357
|
<label class="field">
|
|
346
358
|
<span data-i18n="project.folderLabel">目录名或绝对路径</span>
|
package/web/live-dev/styles.css
CHANGED
|
@@ -558,6 +558,64 @@ body {
|
|
|
558
558
|
border-color: var(--lock);
|
|
559
559
|
}
|
|
560
560
|
|
|
561
|
+
.project-row {
|
|
562
|
+
display: flex;
|
|
563
|
+
flex-direction: column;
|
|
564
|
+
gap: 0.3rem;
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
.project-row.is-active .project-item {
|
|
568
|
+
border-color: var(--lock);
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
.project-row .project-item {
|
|
572
|
+
width: 100%;
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
.project-status {
|
|
576
|
+
display: inline-block;
|
|
577
|
+
margin-top: 0.15rem;
|
|
578
|
+
padding: 0.1rem 0.4rem;
|
|
579
|
+
border: 1px solid rgba(63, 86, 108, 0.65);
|
|
580
|
+
font-size: 0.68rem;
|
|
581
|
+
letter-spacing: 0.03em;
|
|
582
|
+
color: var(--mute);
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
.project-status.is-drafting {
|
|
586
|
+
color: #9fb0c0;
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
.project-status.is-confirming {
|
|
590
|
+
border-color: rgba(47, 158, 143, 0.55);
|
|
591
|
+
color: #9fe8dc;
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
.project-status.is-building {
|
|
595
|
+
border-color: rgba(212, 168, 75, 0.65);
|
|
596
|
+
color: #e8c87a;
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
.project-status.is-delivered {
|
|
600
|
+
border-color: rgba(47, 158, 143, 0.8);
|
|
601
|
+
background: rgba(47, 158, 143, 0.12);
|
|
602
|
+
color: #9fe8dc;
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
.project-status.is-revising {
|
|
606
|
+
border-color: rgba(127, 149, 168, 0.8);
|
|
607
|
+
color: #c5d4e0;
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
.project-deliverables {
|
|
611
|
+
align-self: flex-start;
|
|
612
|
+
font-size: 0.75rem;
|
|
613
|
+
padding: 0.25rem 0.55rem;
|
|
614
|
+
background: rgba(47, 158, 143, 0.1);
|
|
615
|
+
border-color: var(--lock);
|
|
616
|
+
color: #9fe8dc;
|
|
617
|
+
}
|
|
618
|
+
|
|
561
619
|
.dispatch-project-summary {
|
|
562
620
|
margin: 0 0 0.85rem;
|
|
563
621
|
padding: 0.55rem 0.65rem;
|
|
@@ -1225,6 +1283,58 @@ a.btn[hidden] {
|
|
|
1225
1283
|
display: none !important;
|
|
1226
1284
|
}
|
|
1227
1285
|
|
|
1286
|
+
.deliverables-open.primary {
|
|
1287
|
+
font-weight: 650;
|
|
1288
|
+
}
|
|
1289
|
+
|
|
1290
|
+
.delivery-cockpit {
|
|
1291
|
+
flex-shrink: 0;
|
|
1292
|
+
margin: 0.55rem 0 0.35rem;
|
|
1293
|
+
padding: 0.5rem 0.55rem;
|
|
1294
|
+
border: 1px solid rgba(63, 86, 108, 0.55);
|
|
1295
|
+
background: rgba(8, 14, 20, 0.35);
|
|
1296
|
+
}
|
|
1297
|
+
|
|
1298
|
+
.delivery-cockpit[hidden] {
|
|
1299
|
+
display: none !important;
|
|
1300
|
+
}
|
|
1301
|
+
|
|
1302
|
+
.delivery-stages {
|
|
1303
|
+
list-style: none;
|
|
1304
|
+
margin: 0;
|
|
1305
|
+
padding: 0;
|
|
1306
|
+
display: flex;
|
|
1307
|
+
flex-wrap: wrap;
|
|
1308
|
+
gap: 0.3rem;
|
|
1309
|
+
}
|
|
1310
|
+
|
|
1311
|
+
.delivery-stage {
|
|
1312
|
+
margin: 0;
|
|
1313
|
+
padding: 0.15rem 0.4rem;
|
|
1314
|
+
border: 1px solid rgba(63, 86, 108, 0.55);
|
|
1315
|
+
font-size: 0.68rem;
|
|
1316
|
+
letter-spacing: 0.02em;
|
|
1317
|
+
color: var(--mute);
|
|
1318
|
+
}
|
|
1319
|
+
|
|
1320
|
+
.delivery-stage.is-partial {
|
|
1321
|
+
border-color: rgba(212, 168, 75, 0.65);
|
|
1322
|
+
color: #e8c87a;
|
|
1323
|
+
}
|
|
1324
|
+
|
|
1325
|
+
.delivery-stage.is-done {
|
|
1326
|
+
border-color: rgba(47, 158, 143, 0.75);
|
|
1327
|
+
background: rgba(47, 158, 143, 0.14);
|
|
1328
|
+
color: #9fe8dc;
|
|
1329
|
+
}
|
|
1330
|
+
|
|
1331
|
+
.delivery-next {
|
|
1332
|
+
margin: 0.45rem 0 0;
|
|
1333
|
+
font-size: 0.82rem;
|
|
1334
|
+
line-height: 1.4;
|
|
1335
|
+
color: var(--ink);
|
|
1336
|
+
}
|
|
1337
|
+
|
|
1228
1338
|
.progress-col .card-title {
|
|
1229
1339
|
font-size: 1.2rem;
|
|
1230
1340
|
letter-spacing: 0.03em;
|