dsh-plugin-orchd 0.1.0 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -0
- package/client/client.js +106 -118
- package/lib/cache.js +5 -0
- package/lib/cli-invoke.js +12 -5
- package/lib/detect.js +37 -23
- package/lib/git-times.js +240 -0
- package/lib/ideas.js +82 -57
- package/lib/master.js +61 -23
- package/lib/model.js +51 -5
- package/lib/orchd-cli.js +59 -5
- package/lib/routes.js +61 -24
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -40,6 +40,14 @@ dsh plugin add dsh-plugin-orchd
|
|
|
40
40
|
dsh plugin --profile web add github:7bder/dsh-plugin-orchd
|
|
41
41
|
```
|
|
42
42
|
|
|
43
|
+
## 截图
|
|
44
|
+
|
|
45
|
+

|
|
46
|
+
|
|
47
|
+

|
|
48
|
+
|
|
49
|
+

|
|
50
|
+
|
|
43
51
|
Desktop 的 `desktop` profile 由应用托管:普通 `dsh plugin --profile desktop ...` 会被 CLI 直接拒(
|
|
44
52
|
`profile "desktop" is managed exclusively by the Electron application`),应用自带终端里不带 `--profile`
|
|
45
53
|
的命令才改得到它。装好后**重启 DSH**——宿主侧模块不热替换:会话 header 角落出现开关,右侧栏出现「orchd 任务」页。
|
package/client/client.js
CHANGED
|
@@ -126,7 +126,7 @@ window.__ModuleLoader__.load({
|
|
|
126
126
|
// 列表与筛选(FR-5)
|
|
127
127
|
search: '搜索 ID / 名称…',
|
|
128
128
|
sortBy: '排序',
|
|
129
|
-
sortDefault: '活跃优先',
|
|
129
|
+
sortDefault: '活跃优先 · 按时间',
|
|
130
130
|
sortImportance: '重要度',
|
|
131
131
|
sortDifficulty: '难度',
|
|
132
132
|
sortModule: '模块',
|
|
@@ -200,7 +200,7 @@ window.__ModuleLoader__.load({
|
|
|
200
200
|
ideasTitle: '想法',
|
|
201
201
|
ideasArchived: '归档',
|
|
202
202
|
ideasArchivedUnit: '条',
|
|
203
|
-
ideasArchivedHint: '归档只计数,不加载全文;已立任务 /
|
|
203
|
+
ideasArchivedHint: '归档只计数,不加载全文;已立任务 / 已覆盖 / 人工 close 的条目都到这里了',
|
|
204
204
|
ideasArchivedHistorical: '归档保留原文,因此这里的 status 是历史值(可能仍写着 pending)',
|
|
205
205
|
ideasEngineUnsupported: '本项目的 orchd 引擎没有 ideas 子命令(旧版本):下面这份列表与状态是直接读 IDEAS.md 得到的,没有引擎清单可交叉核对。升级引擎后会自动启用交叉核对。',
|
|
206
206
|
ideasBadgeHint: '在等你裁决的想法数(论证中 + 待补充)',
|
|
@@ -211,12 +211,12 @@ window.__ModuleLoader__.load({
|
|
|
211
211
|
ideasOnlyInMarkdown: '仅存在于 IDEAS.md(引擎清单里没有)',
|
|
212
212
|
ideasOwnerUser: '等你裁决',
|
|
213
213
|
ideasOwnerAgent: '等 agent 拆解',
|
|
214
|
-
ideasOwnerNone: '
|
|
214
|
+
ideasOwnerNone: '无需等待',
|
|
215
215
|
ideasConverted: '已转成的任务',
|
|
216
216
|
ideasConvertedHint: '定义层里 source 指向这条想法的任务',
|
|
217
217
|
ideasConvertedTasks: '个任务',
|
|
218
218
|
ideasStatusUnknown: '未知状态',
|
|
219
|
-
|
|
219
|
+
ideasArchiveLegacyDropped: '历史已放弃(dropped)',
|
|
220
220
|
ideasUntasked: '未立任务',
|
|
221
221
|
ideasNoId: '无法对照',
|
|
222
222
|
ideasDetail: '正文',
|
|
@@ -224,15 +224,15 @@ window.__ModuleLoader__.load({
|
|
|
224
224
|
ideasExtra: '未知字段',
|
|
225
225
|
ideasDetailPath: '详档',
|
|
226
226
|
ideasCommands: '你可以执行的命令(id 已填好)',
|
|
227
|
-
ideasCommandsHint: '本面板只读:不提供 propose / confirm /
|
|
228
|
-
ideasCommandLegend: 'append = 往这条想法追加一条带时间戳的追记(不动原正文、不改状态,就是「追加记录」里那几行);confirm /
|
|
227
|
+
ideasCommandsHint: '本面板只读:不提供 propose / confirm / close 按钮,只帮你复制命令。这些命令按当前状态给出;在途任务引用期间引擎会拒绝搬走(source_live_reference)。',
|
|
228
|
+
ideasCommandLegend: 'append = 往这条想法追加一条带时间戳的追记(不动原正文、不改状态,就是「追加记录」里那几行);confirm / close = 你对这条想法的裁决(仅用户可执行;close 一律归档并留下裁决理由)。括号里的 <…> 是占位符,换成你自己要写的字。',
|
|
229
229
|
ideasPropose: '新建想法(引擎侧的正式通道)',
|
|
230
230
|
ideasProposeHint: '新建时还没有 id,所以标题里要自己声明一个(形如「标题(id: my-idea)」)。',
|
|
231
231
|
iStudy: '论证中',
|
|
232
232
|
iQuestioning: '待补充',
|
|
233
233
|
iPending: '待拆解',
|
|
234
|
-
|
|
235
|
-
|
|
234
|
+
iConverted: '已转任务',
|
|
235
|
+
iCovered: '已覆盖',
|
|
236
236
|
// ── FR-7 依赖图(P2b)──
|
|
237
237
|
graphCounts: '节点 {n} · 边 {e}',
|
|
238
238
|
graphIncludeTerminal: '含终态任务',
|
|
@@ -335,7 +335,7 @@ window.__ModuleLoader__.load({
|
|
|
335
335
|
summaryFromPlugin: 'Source: plugin',
|
|
336
336
|
search: 'Search id / name…',
|
|
337
337
|
sortBy: 'Sort',
|
|
338
|
-
sortDefault: 'Active first',
|
|
338
|
+
sortDefault: 'Active first · by time',
|
|
339
339
|
sortImportance: 'Importance',
|
|
340
340
|
sortDifficulty: 'Difficulty',
|
|
341
341
|
sortModule: 'Module',
|
|
@@ -404,7 +404,7 @@ window.__ModuleLoader__.load({
|
|
|
404
404
|
ideasTitle: 'Ideas',
|
|
405
405
|
ideasArchived: 'Archived',
|
|
406
406
|
ideasArchivedUnit: '',
|
|
407
|
-
ideasArchivedHint: 'Archived entries are counted only; entries that became tasks or were
|
|
407
|
+
ideasArchivedHint: 'Archived entries are counted only; entries that became tasks, were covered, or were closed live here',
|
|
408
408
|
ideasArchivedHistorical: 'The archive keeps the original text, so these statuses are historical',
|
|
409
409
|
ideasEngineUnsupported: "This project's orchd engine has no ideas subcommand (older build): the list and statuses below are read straight from IDEAS.md, with no engine ledger to cross-check. Cross-checking turns on automatically after an engine upgrade.",
|
|
410
410
|
ideasBadgeHint: 'Ideas waiting on you (studying + needs info)',
|
|
@@ -415,12 +415,12 @@ window.__ModuleLoader__.load({
|
|
|
415
415
|
ideasOnlyInMarkdown: 'Only present in IDEAS.md (missing from the engine list)',
|
|
416
416
|
ideasOwnerUser: 'waiting on you',
|
|
417
417
|
ideasOwnerAgent: 'waiting on an agent',
|
|
418
|
-
ideasOwnerNone: '
|
|
418
|
+
ideasOwnerNone: 'no action pending',
|
|
419
419
|
ideasConverted: 'Tasks created from this idea',
|
|
420
420
|
ideasConvertedHint: 'Definition-layer tasks whose source points at this idea',
|
|
421
421
|
ideasConvertedTasks: 'tasks',
|
|
422
422
|
ideasStatusUnknown: 'unknown status',
|
|
423
|
-
|
|
423
|
+
ideasArchiveLegacyDropped: 'historically dropped',
|
|
424
424
|
ideasUntasked: 'never tasked',
|
|
425
425
|
ideasNoId: 'no id',
|
|
426
426
|
ideasDetail: 'Body',
|
|
@@ -428,15 +428,15 @@ window.__ModuleLoader__.load({
|
|
|
428
428
|
ideasExtra: 'Unrecognised fields',
|
|
429
429
|
ideasDetailPath: 'Detail doc',
|
|
430
430
|
ideasCommands: 'Commands you can run (id already filled in)',
|
|
431
|
-
ideasCommandsHint: 'This panel is read-only: no propose / confirm /
|
|
432
|
-
ideasCommandLegend: 'append = add one timestamped note to this idea (it never touches the original body or status — these are the lines shown under "Appended notes"); confirm /
|
|
431
|
+
ideasCommandsHint: 'This panel is read-only: no propose / confirm / close buttons — it only copies commands. They match the current status; while in-flight tasks reference the entry the engine refuses to move it (source_live_reference).',
|
|
432
|
+
ideasCommandLegend: 'append = add one timestamped note to this idea (it never touches the original body or status — these are the lines shown under "Appended notes"); confirm / close = your decision on this idea (user-only; close always archives the entry with your reason). The <…> parts are placeholders — replace them with your own text.',
|
|
433
433
|
ideasPropose: 'New idea (the engine-side channel)',
|
|
434
434
|
ideasProposeHint: 'A new idea has no id yet, so declare one in the title (e.g. "Title (id: my-idea)").',
|
|
435
435
|
iStudy: 'Studying',
|
|
436
436
|
iQuestioning: 'Needs info',
|
|
437
437
|
iPending: 'Awaiting breakdown',
|
|
438
|
-
|
|
439
|
-
|
|
438
|
+
iConverted: 'Turned into tasks',
|
|
439
|
+
iCovered: 'Covered',
|
|
440
440
|
graphCounts: '{n} nodes · {e} edges',
|
|
441
441
|
graphIncludeTerminal: 'Include finished tasks',
|
|
442
442
|
graphIncludeTerminalHint: 'Also draw completed / cancelled tasks (by default only active tasks and their direct neighbours are drawn)',
|
|
@@ -771,28 +771,14 @@ window.__ModuleLoader__.load({
|
|
|
771
771
|
function interactionCss() {
|
|
772
772
|
const hover = 'var(--dsw-alias-interactive-bg-hover, rgba(127, 127, 127, 0.12))'
|
|
773
773
|
const active = 'var(--dsw-alias-interactive-bg-active, rgba(127, 127, 127, 0.2))'
|
|
774
|
-
|
|
775
|
-
// 兜底写 `rgba()` 而**不是 `#rrggbb`**:本仓库 FR-11.3 禁止前端出现裸色值 hex
|
|
776
|
-
// (lint 套件会抓,实测第 8 轮就被抓到过一次)。兜底保证变量缺失时脉冲依旧醒目。
|
|
777
|
-
const accent = 'var(--dsw-alias-state-business-primary, rgba(77, 107, 254, 1))'
|
|
774
|
+
|
|
778
775
|
return [
|
|
779
776
|
// 悬停:禁用态不亮(否则会让人以为能点)
|
|
780
777
|
`[data-orchd-toggle]:hover,[data-orchd-panel] button:hover:not(:disabled){background-color:${hover}!important}`,
|
|
781
778
|
// 按下:比悬停更深一档
|
|
782
779
|
`[data-orchd-toggle]:active,[data-orchd-panel] button:active:not(:disabled){background-color:${active}!important}`,
|
|
783
780
|
// 键盘可达:焦点环(鼠标点击不显示,避免视觉噪音)
|
|
784
|
-
'[data-orchd-toggle]:focus-visible,[data-orchd-panel] button:focus-visible{outline:2px solid var(--dsw-alias-label-secondary, currentColor);outline-offset:1px}'
|
|
785
|
-
// 标题脉冲环:点击图标时面板**本来就开着**(`openTab` 只是聚焦,没有任何可见变化),
|
|
786
|
-
// 用它提示「面板在这儿」。
|
|
787
|
-
// ⚠ 强度:第一版借的是 hover 那个 12% 的弱色 ⇒ 用户实测「太弱了」。
|
|
788
|
-
// 现在用**平台品牌强调色**(`--dsw-alias-state-business-primary`,第一方用它的地方很多),
|
|
789
|
-
// 并且:① 环从 0 扩散到 14px(辐射式「ping」,比静止细环显眼得多);
|
|
790
|
-
// ② 标题文字同时变亮成强调色 ⇒ 文字本身也在闪,不只是外圈。
|
|
791
|
-
// 仍用 `box-shadow` + `color`:都**不参与布局**,标题不会被挤动;
|
|
792
|
-
// 且是一次性动画(非状态样式),播完自动回到无环/原色。
|
|
793
|
-
`@keyframes orchd-title-flash{0%{box-shadow:0 0 0 0 ${accent}}100%{box-shadow:0 0 0 14px transparent}}`,
|
|
794
|
-
`@keyframes orchd-title-flash-text{0%,35%{color:${accent}}100%{color:inherit}}`,
|
|
795
|
-
'[data-orchd-title-flash="true"]{border-radius:999px;animation:orchd-title-flash 1s ease-out 1,orchd-title-flash-text 1s ease-out 1}'
|
|
781
|
+
'[data-orchd-toggle]:focus-visible,[data-orchd-panel] button:focus-visible{outline:2px solid var(--dsw-alias-label-secondary, currentColor);outline-offset:1px}'
|
|
796
782
|
].join('')
|
|
797
783
|
}
|
|
798
784
|
|
|
@@ -812,39 +798,8 @@ window.__ModuleLoader__.load({
|
|
|
812
798
|
}
|
|
813
799
|
}
|
|
814
800
|
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
*
|
|
818
|
-
* `mounted` 由面板组件在挂载/卸载时维护:图标被点时如果面板**本来就开着**,
|
|
819
|
-
* `openTab` 只会聚焦已有 tab、界面上没有任何变化,用户会以为「点了没反应」——
|
|
820
|
-
* 这时才在标题上闪一下指路;面板本来是关的,tab 出现本身就是反馈,不必再闪。
|
|
821
|
-
*/
|
|
822
|
-
const panelState = { mounted: false, listeners: new Set() }
|
|
823
|
-
|
|
824
|
-
function flashPanelTitle() {
|
|
825
|
-
for (const listener of [...panelState.listeners]) {
|
|
826
|
-
try {
|
|
827
|
-
listener()
|
|
828
|
-
} catch (error) {
|
|
829
|
-
console.error('orchd-tasks: title flash listener failed', error)
|
|
830
|
-
}
|
|
831
|
-
}
|
|
832
|
-
}
|
|
833
|
-
|
|
834
|
-
/**
|
|
835
|
-
* 订阅标题脉冲。返回 `flashId`:每次闪烁 +1,调用方把它当 `key` 用,
|
|
836
|
-
* 靠**重挂载**让 CSS 动画能重放(同一元素上重复加同一个属性值不会重启动画)。
|
|
837
|
-
*/
|
|
838
|
-
function useTitleFlash() {
|
|
839
|
-
const [flashId, setFlashId] = React.useState(0)
|
|
840
|
-
React.useEffect(() => {
|
|
841
|
-
const listener = () => setFlashId((value) => value + 1)
|
|
842
|
-
panelState.listeners.add(listener)
|
|
843
|
-
return () => panelState.listeners.delete(listener)
|
|
844
|
-
}, [])
|
|
845
|
-
return flashId
|
|
846
|
-
}
|
|
847
|
-
|
|
801
|
+
// 标题脉冲环已移除:入口按钮改为「打开 / 关闭」toggle(见 OrchdToggleButton),
|
|
802
|
+
// 再点有可见反馈(面板收起),不再需要「面板本来开着 ⇒ 点了没反应」的提示。
|
|
848
803
|
|
|
849
804
|
async function writeClipboard(text) {
|
|
850
805
|
if (navigator.clipboard?.writeText !== undefined) {
|
|
@@ -1449,10 +1404,25 @@ window.__ModuleLoader__.load({
|
|
|
1449
1404
|
return counts
|
|
1450
1405
|
}
|
|
1451
1406
|
|
|
1452
|
-
/**
|
|
1407
|
+
/** 默认「活跃优先 → 组内按时间」:活跃按最近动作、终态按合并时间(FR-5.4 修订)。 */ function sortRows(rows, by) {
|
|
1453
1408
|
const rank = (value) => ({ critical: 4, high: 3, normal: 2, low: 1 })[value] ?? 0
|
|
1454
1409
|
const diff = (value) => ({ high: 3, medium: 2, low: 1 })[value] ?? 0
|
|
1455
1410
|
const activeFirst = (status) => (status === 'claimed' || status === 'done' || status === 'in_review' ? 0 : status === 'completed' || status === 'cancelled' ? 2 : 1)
|
|
1411
|
+
// 时间戳来自宿主(`lastActionAt` / `mergedAt`,git 近似;取不到为 null → 排本组最后)
|
|
1412
|
+
const timeOf = (row) => {
|
|
1413
|
+
const raw = row.status === 'completed' || row.status === 'cancelled' ? row.mergedAt : row.lastActionAt
|
|
1414
|
+
if (typeof raw !== 'string' || raw === '') return null
|
|
1415
|
+
const parsed = Date.parse(raw)
|
|
1416
|
+
return Number.isFinite(parsed) ? parsed : null
|
|
1417
|
+
}
|
|
1418
|
+
const byTime = (a, b) => {
|
|
1419
|
+
const ta = timeOf(a)
|
|
1420
|
+
const tb = timeOf(b)
|
|
1421
|
+
if (ta === null && tb === null) return 0
|
|
1422
|
+
if (ta === null) return 1
|
|
1423
|
+
if (tb === null) return -1
|
|
1424
|
+
return tb - ta
|
|
1425
|
+
}
|
|
1456
1426
|
const copy = [...rows]
|
|
1457
1427
|
const byId = (a, b) => String(a.id ?? '').localeCompare(String(b.id ?? ''))
|
|
1458
1428
|
switch (by) {
|
|
@@ -1465,7 +1435,7 @@ window.__ModuleLoader__.load({
|
|
|
1465
1435
|
case 'status':
|
|
1466
1436
|
return copy.sort((a, b) => String(a.status ?? '~').localeCompare(String(b.status ?? '~')) || byId(a, b))
|
|
1467
1437
|
default:
|
|
1468
|
-
return copy.sort((a, b) => activeFirst(a.status) - activeFirst(b.status) || rank(b.importance ?? b.importanceDerived) - rank(a.importance ?? a.importanceDerived) || byId(a, b))
|
|
1438
|
+
return copy.sort((a, b) => activeFirst(a.status) - activeFirst(b.status) || byTime(a, b) || rank(b.importance ?? b.importanceDerived) - rank(a.importance ?? a.importanceDerived) || byId(a, b))
|
|
1469
1439
|
}
|
|
1470
1440
|
}
|
|
1471
1441
|
|
|
@@ -2250,26 +2220,44 @@ window.__ModuleLoader__.load({
|
|
|
2250
2220
|
|
|
2251
2221
|
// ── FR-13 想法看板 ────────────────────────────────────────────────────
|
|
2252
2222
|
/**
|
|
2253
|
-
*
|
|
2254
|
-
*
|
|
2255
|
-
* `全部 = 活跃 + 已转任务 +
|
|
2223
|
+
* 想法展示桶顺序(4 个引擎状态 + 派生桶 `converted`)。
|
|
2224
|
+
* 统计条**固定**展示这些(0 值可点),这样三分区在界面上闭合:
|
|
2225
|
+
* `全部 = 活跃 + 已转任务 + 已覆盖`(活跃 = 论证中 + 待补充 + 待拆解)。
|
|
2256
2226
|
* 集合以宿主下发的 `activeStatuses` 为准,这里只是同源兜底。
|
|
2227
|
+
*
|
|
2228
|
+
* 引擎 2026-10-23 调整:`taskified` 正名为 `covered`(撞车并入已有任务、待归档),
|
|
2229
|
+
* `dropped` 从活跃台账下线(放弃改走 `idea close --reason`)。`converted` 是
|
|
2230
|
+
* **派生桶**(任务定义层 source/additional_sources 指向该想法),不是引擎状态。
|
|
2257
2231
|
*/
|
|
2258
|
-
const IDEA_STATUSES_UI = ['study', 'questioning', 'pending', '
|
|
2232
|
+
const IDEA_STATUSES_UI = ['study', 'questioning', 'pending', 'converted', 'covered']
|
|
2259
2233
|
|
|
2260
2234
|
/**
|
|
2261
|
-
*
|
|
2235
|
+
* 想法展示桶 → i18n 键。
|
|
2262
2236
|
*
|
|
2263
|
-
* ⚠
|
|
2264
|
-
*
|
|
2265
|
-
*
|
|
2237
|
+
* ⚠ 前 4 个是**引擎定义的**(`study` / `questioning` / `pending` / `covered`),
|
|
2238
|
+
* 不得自造;`converted` 是本插件按定义层溯源派生的展示桶。引擎将来加了新状态时
|
|
2239
|
+
* 解析层会把 `status` 置 null 并保留原文,这里显示「未知状态 + 原文」,不崩、不猜。
|
|
2266
2240
|
*/
|
|
2267
2241
|
const IDEA_STATUS_I18N = {
|
|
2268
2242
|
study: 'iStudy',
|
|
2269
2243
|
questioning: 'iQuestioning',
|
|
2270
2244
|
pending: 'iPending',
|
|
2271
|
-
|
|
2272
|
-
|
|
2245
|
+
converted: 'iConverted',
|
|
2246
|
+
covered: 'iCovered'
|
|
2247
|
+
}
|
|
2248
|
+
|
|
2249
|
+
/** 条目的展示桶:派生桶优先,其次才是引擎字面状态。 */
|
|
2250
|
+
function ideaDisplayStatus(entry) {
|
|
2251
|
+
if (entry === null || entry === undefined) return null
|
|
2252
|
+
if (typeof entry.displayStatus === 'string') return entry.displayStatus
|
|
2253
|
+
if (entry.converted === true) return 'converted'
|
|
2254
|
+
return entry.status ?? null
|
|
2255
|
+
}
|
|
2256
|
+
|
|
2257
|
+
/** 展示桶文案;未知状态显式说「未知」,不猜、不崩。 */
|
|
2258
|
+
function ideaStatusText(entry, t) {
|
|
2259
|
+
const status = ideaDisplayStatus(entry)
|
|
2260
|
+
return status === null ? t('ideasStatusUnknown') : t(IDEA_STATUS_I18N[status] ?? 'ideasStatusUnknown')
|
|
2273
2261
|
}
|
|
2274
2262
|
|
|
2275
2263
|
/** 想法的「在等谁」文案(FR-13.4:**不得只给状态码**)。 */
|
|
@@ -2293,29 +2281,30 @@ window.__ModuleLoader__.load({
|
|
|
2293
2281
|
/**
|
|
2294
2282
|
* 想法列表(FR-13.1/13.3/13.4)。
|
|
2295
2283
|
*
|
|
2296
|
-
* 与任务列表用**同一套**状态条筛选组件(FR-13.6
|
|
2284
|
+
* 与任务列表用**同一套**状态条筛选组件(FR-13.6),只是标签换成想法展示桶;
|
|
2297
2285
|
* 徽章数在调用方(视图切换)显示,这里不重复。
|
|
2298
2286
|
*/
|
|
2299
2287
|
function IdeasBoard({ data, filters, setFilters, t, onOpen }) {
|
|
2300
2288
|
const entries = data?.entries ?? []
|
|
2301
2289
|
const counts = data?.counts?.byStatus ?? {}
|
|
2302
|
-
//
|
|
2303
|
-
//
|
|
2304
|
-
// 全部 = 活跃 + 已转任务 +
|
|
2290
|
+
// 统计条固定展示全部展示桶(4 个引擎状态 + 派生桶 converted,0 值可点),
|
|
2291
|
+
// 以便三分区在界面上**闭合**:
|
|
2292
|
+
// 全部 = 活跃 + 已转任务 + 已覆盖 (活跃 = 论证中 + 待补充 + 待拆解)
|
|
2305
2293
|
// 集合由宿主下发(`activeStatuses`),客户端只做兜底。
|
|
2306
2294
|
const labels = Array.isArray(data?.activeStatuses) && data.activeStatuses.length > 0
|
|
2307
2295
|
? data.activeStatuses
|
|
2308
2296
|
: IDEA_STATUSES_UI
|
|
2309
|
-
// 归档分组:与任务池对照得出(已转任务 /
|
|
2297
|
+
// 归档分组:与任务池对照得出(已转任务 / 历史已放弃 / 未立任务 / 无 id)
|
|
2310
2298
|
const archived = data?.archived ?? {}
|
|
2311
2299
|
const archivedTotal = data?.archivedCount ?? 0
|
|
2312
2300
|
|
|
2313
2301
|
const filtered = entries.filter((entry) => {
|
|
2314
2302
|
const mode = filters.mode
|
|
2303
|
+
const display = ideaDisplayStatus(entry)
|
|
2315
2304
|
if (mode === 'active') {
|
|
2316
|
-
//
|
|
2317
|
-
if (
|
|
2318
|
-
} else if (mode !== 'all' &&
|
|
2305
|
+
// 活跃 = 仍待处理:已转任务(converted)与已覆盖(covered)都不算
|
|
2306
|
+
if (display === 'converted' || display === 'covered') return false
|
|
2307
|
+
} else if (mode !== 'all' && display !== mode) {
|
|
2319
2308
|
return false
|
|
2320
2309
|
}
|
|
2321
2310
|
if ((filters.keyword ?? '') !== '') {
|
|
@@ -2327,10 +2316,10 @@ window.__ModuleLoader__.load({
|
|
|
2327
2316
|
|
|
2328
2317
|
const summaryForStrip = {
|
|
2329
2318
|
total: data?.counts?.total ?? entries.length,
|
|
2330
|
-
//
|
|
2319
|
+
// 「活跃」用宿主给的数(已扣除已转任务 / 已覆盖);拿不到时退回本地口径
|
|
2331
2320
|
active: typeof data?.active === 'number'
|
|
2332
2321
|
? data.active
|
|
2333
|
-
: entries.filter((entry) =>
|
|
2322
|
+
: entries.filter((entry) => !['converted', 'covered'].includes(ideaDisplayStatus(entry))).length,
|
|
2334
2323
|
...counts,
|
|
2335
2324
|
source: 'engine'
|
|
2336
2325
|
}
|
|
@@ -2383,8 +2372,8 @@ window.__ModuleLoader__.load({
|
|
|
2383
2372
|
archivedTotal === 0
|
|
2384
2373
|
? `${t('ideasArchived')} 0 ${t('ideasArchivedUnit')}`
|
|
2385
2374
|
: `${t('ideasArchived')} ${archivedTotal}${t('ideasArchivedUnit')}(${[
|
|
2386
|
-
`${t('ideasConverted')} ${archived.
|
|
2387
|
-
`${t('
|
|
2375
|
+
`${t('ideasConverted')} ${archived.converted ?? 0}`,
|
|
2376
|
+
`${t('ideasArchiveLegacyDropped')} ${archived.legacyDropped ?? 0}`,
|
|
2388
2377
|
`${t('ideasUntasked')} ${archived.untasked ?? 0}`,
|
|
2389
2378
|
`${t('ideasNoId')} ${archived.noId ?? 0}`
|
|
2390
2379
|
].join(' · ')})`),
|
|
@@ -2448,7 +2437,7 @@ window.__ModuleLoader__.load({
|
|
|
2448
2437
|
React.createElement('span', { key: 'm', style: styles.rowMeta }, [
|
|
2449
2438
|
React.createElement('span', { key: 's', style: { ...styles.statusTag, color: ideaOwnerToken(entry.owner) } }, [
|
|
2450
2439
|
React.createElement('span', { key: 'dd', style: styles.chipDot }),
|
|
2451
|
-
React.createElement('span', { key: 'l' }, entry
|
|
2440
|
+
React.createElement('span', { key: 'l' }, ideaStatusText(entry, t)),
|
|
2452
2441
|
React.createElement('span', { key: 'o' }, ` · ${ideaOwnerText(entry.owner, t)}`)
|
|
2453
2442
|
]),
|
|
2454
2443
|
// 已转任务:由定义层溯源推出(status 只表达摄入/裁决态,不表达「已立任务」),顺带给出任务数量
|
|
@@ -2519,7 +2508,7 @@ window.__ModuleLoader__.load({
|
|
|
2519
2508
|
style: { ...styles.statusTag, color: ideaOwnerToken(entry.owner) }
|
|
2520
2509
|
}, [
|
|
2521
2510
|
React.createElement('span', { key: 'd', style: styles.chipDot }),
|
|
2522
|
-
React.createElement('span', { key: 'l' }, entry
|
|
2511
|
+
React.createElement('span', { key: 'l' }, ideaStatusText(entry, t)),
|
|
2523
2512
|
React.createElement('span', { key: 'o' }, ` · ${ideaOwnerText(entry.owner, t)}`)
|
|
2524
2513
|
])
|
|
2525
2514
|
]),
|
|
@@ -2581,11 +2570,13 @@ window.__ModuleLoader__.load({
|
|
|
2581
2570
|
*
|
|
2582
2571
|
* 命令形态来自引擎实测(`orchd idea <c> --help`):
|
|
2583
2572
|
* `append --id <id> --notes …`(按 `- id:` 精确值定位)
|
|
2584
|
-
* `confirm
|
|
2585
|
-
* ——
|
|
2573
|
+
* `confirm --title <title>`、`close --title <title> --reason …`
|
|
2574
|
+
* —— 后两者的 `--title` 支持「裸 slug」,而裸 slug 与 `- id:` 精确相等,故直接用 id。
|
|
2586
2575
|
*
|
|
2587
|
-
*
|
|
2576
|
+
* 引擎 2026-10-23 起人工离场出口统一为 `idea close --reason`(旧 drop / archive 已下线):
|
|
2577
|
+
* study/questioning/pending/covered 全部走 close;`study` 另给 confirm(升 pending)。
|
|
2588
2578
|
* 在途任务引用期间引擎会拒绝搬走(`source_live_reference`),这是引擎的守卫,不是错误。
|
|
2579
|
+
* 判定用**字面 status**(`converted` 的条目字面仍是 pending)。
|
|
2589
2580
|
*/
|
|
2590
2581
|
function ideaCommandsFor(entry) {
|
|
2591
2582
|
const id = typeof entry.id === 'string' && entry.id !== '' ? entry.id : null
|
|
@@ -2593,9 +2584,9 @@ window.__ModuleLoader__.load({
|
|
|
2593
2584
|
const commands = [{ key: 'append', text: `python .orchd/__main__.py idea append --id ${id} --notes "<追记内容>"` }]
|
|
2594
2585
|
if (entry.status === 'study') {
|
|
2595
2586
|
commands.push({ key: 'confirm', text: `python .orchd/__main__.py idea confirm --title ${id}` })
|
|
2596
|
-
commands.push({ key: '
|
|
2597
|
-
} else if (entry.status === 'questioning' || entry.status === 'pending' || entry.status === '
|
|
2598
|
-
commands.push({ key: '
|
|
2587
|
+
commands.push({ key: 'close', text: `python .orchd/__main__.py idea close --title ${id} --reason "<裁决理由>"` })
|
|
2588
|
+
} else if (entry.status === 'questioning' || entry.status === 'pending' || entry.status === 'covered') {
|
|
2589
|
+
commands.push({ key: 'close', text: `python .orchd/__main__.py idea close --title ${id} --reason "<裁决理由>"` })
|
|
2599
2590
|
}
|
|
2600
2591
|
return commands
|
|
2601
2592
|
}
|
|
@@ -2625,13 +2616,6 @@ window.__ModuleLoader__.load({
|
|
|
2625
2616
|
// 每会话记忆(FR-12.2):视图与筛选状态按 sessionId 隔离保存,切走再切回可恢复
|
|
2626
2617
|
const sessionMemory = React.useRef(new Map())
|
|
2627
2618
|
|
|
2628
|
-
// 面板是否已挂载:图标被点时用来判断「面板本来就开着」(这时才需要在标题上闪一下指路)
|
|
2629
|
-
React.useEffect(() => {
|
|
2630
|
-
panelState.mounted = true
|
|
2631
|
-
return () => {
|
|
2632
|
-
panelState.mounted = false
|
|
2633
|
-
}
|
|
2634
|
-
}, [])
|
|
2635
2619
|
|
|
2636
2620
|
// 会话切换时清掉「当前打开的任务/想法」(视图状态按会话隔离)
|
|
2637
2621
|
React.useEffect(() => {
|
|
@@ -3094,21 +3078,17 @@ window.__ModuleLoader__.load({
|
|
|
3094
3078
|
/** tab chip 标题(sidebar.right.pane.tab.title,keyed/session)。 */
|
|
3095
3079
|
function OrchdTasksTitle(props) {
|
|
3096
3080
|
const t = typeof props?.t === 'function' ? props.t : (key) => key
|
|
3097
|
-
// 点图标时若面板本来就开着,就在标题上闪一下(见 flashPanelTitle 的注释)。
|
|
3098
|
-
// `flashId` 当 `key` 用:每次闪烁重挂载标题,CSS 动画才能重放。
|
|
3099
|
-
const flashId = useTitleFlash()
|
|
3100
3081
|
return React.createElement('span', { style: { display: 'inline-flex', alignItems: 'center', gap: 6 } }, [
|
|
3101
3082
|
React.createElement(OrchdIcon, { key: 'i', size: 14 }),
|
|
3102
|
-
React.createElement('span',
|
|
3103
|
-
key: `t-${flashId}`,
|
|
3104
|
-
'data-orchd-title-flash': flashId > 0 ? 'true' : 'false'
|
|
3105
|
-
}, t('title'))
|
|
3083
|
+
React.createElement('span', null, t('title'))
|
|
3106
3084
|
])
|
|
3107
3085
|
}
|
|
3108
3086
|
|
|
3109
3087
|
/**
|
|
3110
3088
|
* 会话 header 的开关按钮。
|
|
3111
3089
|
*
|
|
3090
|
+
* 点击 = 打开 / 关闭(面板正显示时收起右侧栏,否则打开并聚焦本面板)。
|
|
3091
|
+
*
|
|
3112
3092
|
* 席位是 conversation.session.header.utilities,kind 为 list、scope 为 session。
|
|
3113
3093
|
* 不要用 conversation.session.header.corner:那是 **single** 席位,右侧栏自带的
|
|
3114
3094
|
* 「展开」按钮已经占着它;single 在同一优先级下只允许一个占位者,重复注册会抛错
|
|
@@ -3120,13 +3100,11 @@ window.__ModuleLoader__.load({
|
|
|
3120
3100
|
type: 'button',
|
|
3121
3101
|
style: styles.iconButton,
|
|
3122
3102
|
onClick: () => {
|
|
3123
|
-
//
|
|
3124
|
-
// (面板本来是关的就不闪:tab 出现本身就是反馈)
|
|
3125
|
-
if (panelState.mounted) flashPanelTitle()
|
|
3103
|
+
// 点击只是「打开 / 关闭」:面板正显示时收起,否则打开并聚焦本面板。
|
|
3126
3104
|
try {
|
|
3127
|
-
props.
|
|
3105
|
+
props.toggle?.()
|
|
3128
3106
|
} catch (error) {
|
|
3129
|
-
console.error('orchd-tasks:
|
|
3107
|
+
console.error('orchd-tasks: toggle failed', error)
|
|
3130
3108
|
}
|
|
3131
3109
|
},
|
|
3132
3110
|
'aria-label': t('toggleLabel'),
|
|
@@ -3220,7 +3198,17 @@ window.__ModuleLoader__.load({
|
|
|
3220
3198
|
order: 30,
|
|
3221
3199
|
locale: NS,
|
|
3222
3200
|
inject: () => ({
|
|
3223
|
-
|
|
3201
|
+
// 点击 = 打开 / 关闭:面板正显示时收起,否则打开并聚焦本面板。
|
|
3202
|
+
// 用平台 sidebarRight 的真实状态(isExpanded / active),不靠本地「挂载与否」猜测。
|
|
3203
|
+
toggle: () => {
|
|
3204
|
+
const sidebar = ctx.sidebarRight
|
|
3205
|
+
const active = typeof sidebar?.active === 'function' ? sidebar.active() : undefined
|
|
3206
|
+
if (typeof sidebar?.isExpanded === 'function' && sidebar.isExpanded() && active?.kind === TAB_KIND) {
|
|
3207
|
+
sidebar.toggleExpanded()
|
|
3208
|
+
} else {
|
|
3209
|
+
sidebar.openTab(TAB_KIND)
|
|
3210
|
+
}
|
|
3211
|
+
}
|
|
3224
3212
|
})
|
|
3225
3213
|
}, OrchdToggleButton)),
|
|
3226
3214
|
'orchd-tasks: header toggle'
|
|
@@ -3237,7 +3225,7 @@ window.__ModuleLoader__.load({
|
|
|
3237
3225
|
* 必须有单测兜底。测试用 `test/client.test.mjs` 的 `vm` + 假 `window.__ModuleLoader__`
|
|
3238
3226
|
* 加载本文件再取这个对象断言。
|
|
3239
3227
|
*/
|
|
3240
|
-
exports.__testables = { applyFilters, sortRows, countByStatus, TASK_STATUS_I18N, ALL_STATUSES, styles, IDEA_STATUSES_UI, IDEA_STATUS_I18N, ideaCommandsFor, computeThumb, scrollTopForThumbTop, SCROLLBAR_WIDTH, SCROLLBAR_MIN_THUMB, ROW_HEIGHT, IDEA_ROW_HEIGHT, interactionCss, INTERACTION_CSS_TAG, layoutDependencyGraph, graphAnomalyRows, graphNodeLabel, truncateLabel, graphTextUnits, GRAPH_NODE_W, GRAPH_NODE_H, GRAPH_GAP_X, GRAPH_GAP_Y }
|
|
3228
|
+
exports.__testables = { applyFilters, sortRows, countByStatus, TASK_STATUS_I18N, ALL_STATUSES, styles, IDEA_STATUSES_UI, IDEA_STATUS_I18N, ideaCommandsFor, ideaDisplayStatus, ideaStatusText, computeThumb, scrollTopForThumbTop, SCROLLBAR_WIDTH, SCROLLBAR_MIN_THUMB, ROW_HEIGHT, IDEA_ROW_HEIGHT, interactionCss, INTERACTION_CSS_TAG, layoutDependencyGraph, graphAnomalyRows, graphNodeLabel, truncateLabel, graphTextUnits, GRAPH_NODE_W, GRAPH_NODE_H, GRAPH_GAP_X, GRAPH_GAP_Y }
|
|
3241
3229
|
return module.exports
|
|
3242
3230
|
}
|
|
3243
3231
|
})
|
package/lib/cache.js
CHANGED
|
@@ -106,6 +106,11 @@ export function createProjectCaches(config) {
|
|
|
106
106
|
review: createTtlCache({ ttlMs: 60_000, maxEntries: limit * 4 }),
|
|
107
107
|
/** IDEAS 清单(`ideas list`):变化慢,用 pollTtlMs(FR-13)。 */
|
|
108
108
|
ideas: createTtlCache({ ttlMs: config.pollTtlMs, maxEntries: limit }),
|
|
109
|
+
/**
|
|
110
|
+
* git 时间层(FR-5.4 修订):默认排序要用的「最近动作时间 / 合并时间」。
|
|
111
|
+
* 走两条只读 git 命令,变化比状态层慢,用 pollTtlMs(与 pool 同档)。
|
|
112
|
+
*/
|
|
113
|
+
gitTimes: createTtlCache({ ttlMs: config.pollTtlMs, maxEntries: limit }),
|
|
109
114
|
/** IDEAS 正文与归档计数:按文件指纹缓存(FR-13.12:归档不进默认加载路径)。 */
|
|
110
115
|
ideasFiles: createTtlCache({ ttlMs: config.pollTtlMs, maxEntries: limit * 2 }),
|
|
111
116
|
/** 定义层:TTL 视为无限,靠文件指纹失效(见 `definitionFingerprint`)。 */
|
package/lib/cli-invoke.js
CHANGED
|
@@ -30,7 +30,10 @@ export function normalizeCliResult(result) {
|
|
|
30
30
|
reason: result.outcome === 'ok' ? null : (result.reason ?? 'cli-failed'),
|
|
31
31
|
elapsedMs: result.elapsedMs,
|
|
32
32
|
argv: result.argv,
|
|
33
|
-
stderr: result.stderr ?? null
|
|
33
|
+
stderr: result.stderr ?? null,
|
|
34
|
+
// 启动器定位结果(`worktree` 或 `container-root-fallback`):出问题时能直接看出用了哪一份入口
|
|
35
|
+
launcher: result.launcher ?? null,
|
|
36
|
+
launcherSource: result.launcherSource ?? null
|
|
34
37
|
}
|
|
35
38
|
}
|
|
36
39
|
|
|
@@ -98,6 +101,8 @@ export function createCliInvoker(config, runner = runOrchdCli, logger = null) {
|
|
|
98
101
|
const result = normalizeCliResult(await runner({
|
|
99
102
|
root: ctx.root,
|
|
100
103
|
workDir: ctx.workDir ?? ctx.mainWorktree,
|
|
104
|
+
// 容器根(`resolveLayout` 给出):启动器在「工作树那份不存在」时可回退到它下面那一份
|
|
105
|
+
containerRoot: ctx.containerRoot ?? null,
|
|
101
106
|
python: ctx.python,
|
|
102
107
|
argv,
|
|
103
108
|
timeoutMs,
|
|
@@ -121,10 +126,10 @@ export function createCliInvoker(config, runner = runOrchdCli, logger = null) {
|
|
|
121
126
|
* ⚠ 引擎**只返回 3 个字段**(`title` / `id` / `status`),正文必须另读 `IDEAS.md`。
|
|
122
127
|
*/
|
|
123
128
|
ideasList: (ctx) => call(ctx, ['ideas', 'list']),
|
|
124
|
-
/** 供 `detect()` 使用的形态。`detect` 会传 `workDir`(它自己从 layout 取)。 */
|
|
125
|
-
detectRunner: async ({ root, python, workDir, command }) => {
|
|
129
|
+
/** 供 `detect()` 使用的形态。`detect` 会传 `workDir` 与 `containerRoot`(它自己从 layout 取)。 */
|
|
130
|
+
detectRunner: async ({ root, python, workDir, containerRoot, command }) => {
|
|
126
131
|
const argv = Array.isArray(command) ? command : ['status', '--all']
|
|
127
|
-
const result = await call({ root, python, workDir }, argv)
|
|
132
|
+
const result = await call({ root, python, workDir, containerRoot }, argv)
|
|
128
133
|
return {
|
|
129
134
|
timedOut: result.timedOut,
|
|
130
135
|
exitCode: result.exitCode,
|
|
@@ -132,7 +137,9 @@ export function createCliInvoker(config, runner = runOrchdCli, logger = null) {
|
|
|
132
137
|
engineError: result.engineError,
|
|
133
138
|
reason: result.reason,
|
|
134
139
|
elapsedMs: result.elapsedMs,
|
|
135
|
-
stderr: result.stderr
|
|
140
|
+
stderr: result.stderr,
|
|
141
|
+
launcher: result.launcher,
|
|
142
|
+
launcherSource: result.launcherSource
|
|
136
143
|
}
|
|
137
144
|
}
|
|
138
145
|
}
|