dsh-taskboard 0.6.2 → 0.6.4
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 +18 -3
- package/lib/client.js +13 -4
- package/lib/host/execution.js +160 -78
- package/lib/host/execution.js.map +1 -1
- package/lib/host/git.js +41 -13
- package/lib/host/git.js.map +1 -1
- package/lib/host/isolation.js +192 -0
- package/lib/host/isolation.js.map +1 -0
- package/lib/host/repos.js +91 -0
- package/lib/host/repos.js.map +1 -0
- package/lib/host/routes.js +205 -60
- package/lib/host/routes.js.map +1 -1
- package/lib/host/tools.js +1 -1
- package/lib/host/tools.js.map +1 -1
- package/lib/index.js +4 -0
- package/lib/index.js.map +1 -1
- package/lib/shared/api.js.map +1 -1
- package/lib/shared/protocol.js +62 -1
- package/lib/shared/protocol.js.map +1 -1
- package/package.json +5 -2
- package/src/client/api.ts +2 -1
- package/src/client/board/TaskDetail.tsx +100 -32
- package/src/client/board/TaskFormModal.tsx +7 -0
- package/src/client/controller.ts +15 -6
- package/src/client/i18n/en.ts +5 -1
- package/src/client/i18n/runtime.ts +71 -5
- package/src/client/i18n/zh.ts +5 -1
- package/src/client/index.ts +5 -2
- package/src/client/styles.ts +7 -0
- package/src/host/execution.ts +199 -115
- package/src/host/git.ts +84 -18
- package/src/host/isolation.ts +268 -0
- package/src/host/repos.ts +146 -0
- package/src/host/routes.ts +211 -73
- package/src/host/tools.ts +2 -2
- package/src/index.ts +6 -1
- package/src/shared/api.ts +29 -5
- package/src/shared/protocol.ts +124 -0
- package/src/shared/version.ts +1 -1
package/src/client/controller.ts
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
*
|
|
8
8
|
* @module dsh-taskboard/client/controller
|
|
9
9
|
*/
|
|
10
|
-
import type { ChangeEvent, DiagnosticsResponse, DiffResponse, ImportCommitResponse, ImportPreviewResponse, PromptCompletionsResponse, TaskTemplate, TaskTemplateSpec, UpdateTaskBody, WorkspaceView } from '../shared/api.ts'
|
|
10
|
+
import type { ChangeEvent, DiagnosticsResponse, DiffResponse, ImportCommitResponse, ImportPreviewResponse, MergeRepoResult, PromptCompletionsResponse, TaskTemplate, TaskTemplateSpec, UpdateTaskBody, WorkspaceView } from '../shared/api.ts'
|
|
11
11
|
import type { ChecklistItem, TaskLedger, TaskRecord, Urgency } from '../shared/protocol.ts'
|
|
12
12
|
import { emptyLedger } from '../shared/protocol.ts'
|
|
13
13
|
import type { TaskboardClient } from './api.ts'
|
|
@@ -277,6 +277,12 @@ export class BoardController {
|
|
|
277
277
|
return this.state.workspaces.find(w => w.id === workspaceId)?.gitAvailable === true
|
|
278
278
|
}
|
|
279
279
|
|
|
280
|
+
/** How many repos a task mirror of this workspace would cover (0.6.3 mirror badge). */
|
|
281
|
+
repoCount(workspaceId: string | undefined): number {
|
|
282
|
+
if (workspaceId === undefined) return 1
|
|
283
|
+
return this.state.workspaces.find(w => w.id === workspaceId)?.repoCount ?? 1
|
|
284
|
+
}
|
|
285
|
+
|
|
280
286
|
/**
|
|
281
287
|
* Install the session-jump bridge (built from the runtime sessions service
|
|
282
288
|
* by the client entry). Without it openSession reports 'unavailable'.
|
|
@@ -473,8 +479,8 @@ export class BoardController {
|
|
|
473
479
|
}
|
|
474
480
|
}
|
|
475
481
|
|
|
476
|
-
/** Diff view (0.4.0): one execution's commit or changed path;
|
|
477
|
-
async fetchDiff(taskId: string, query: { execution: string; commit?: string; path?: string }): Promise<DiffResponse | undefined> {
|
|
482
|
+
/** Diff view (0.4.0): one execution's commit or changed path; `repo` picks a mirror repo (0.6.3). */
|
|
483
|
+
async fetchDiff(taskId: string, query: { execution: string; commit?: string; path?: string; repo?: string }): Promise<DiffResponse | undefined> {
|
|
478
484
|
try {
|
|
479
485
|
return await this.client.diff(taskId, query)
|
|
480
486
|
} catch (error) {
|
|
@@ -520,13 +526,16 @@ export class BoardController {
|
|
|
520
526
|
|
|
521
527
|
/**
|
|
522
528
|
* ⇥ 合并 (detail page): merge the task branch into the main worktree.
|
|
523
|
-
* @returns the outcome; `noop` means the branch had no new commits (nothing
|
|
529
|
+
* @returns the outcome; `noop` means the branch had no new commits (nothing
|
|
530
|
+
* merged); multi-repo tasks (0.6.3) additionally carry per-repo `results`.
|
|
524
531
|
*/
|
|
525
|
-
async mergeBranch(id: string): Promise<{ ok: true; noop?: boolean } | { ok: false; error: string }> {
|
|
532
|
+
async mergeBranch(id: string): Promise<{ ok: true; noop?: boolean; results?: MergeRepoResult[] } | { ok: false; error: string }> {
|
|
526
533
|
try {
|
|
527
534
|
const value = await this.client.mergeBranch(id)
|
|
528
535
|
await this.refresh()
|
|
529
|
-
return value.noop === true
|
|
536
|
+
return value.noop === true
|
|
537
|
+
? { ok: true, noop: true }
|
|
538
|
+
: value.results !== undefined ? { ok: true, results: value.results } : { ok: true }
|
|
530
539
|
} catch (error) {
|
|
531
540
|
return { ok: false, error: error instanceof Error ? error.message : String(error) }
|
|
532
541
|
}
|
package/src/client/i18n/en.ts
CHANGED
|
@@ -166,6 +166,9 @@ export const en: TaskboardDict = {
|
|
|
166
166
|
'iso.merge.button': '⇥ Merge into main worktree',
|
|
167
167
|
'iso.merge.failed': 'Merge failed: {error}',
|
|
168
168
|
'iso.merge.noop': 'The branch has no commits ahead of the main worktree — nothing to merge (send it back to continue running, or clean it up)',
|
|
169
|
+
'iso.merge.done': 'Merged per repo (--no-ff): {summary}',
|
|
170
|
+
'iso.merge.partial': 'Some repos failed to merge: {summary}\n{error}',
|
|
171
|
+
'iso.repo.root': 'Root repo',
|
|
169
172
|
'iso.remove.wt': '🗑 Remove worktree',
|
|
170
173
|
'iso.remove.wtTitle': 'git worktree remove (refused when uncommitted changes exist)',
|
|
171
174
|
'iso.remove.wtb': '🗑 Remove worktree + branch',
|
|
@@ -290,6 +293,7 @@ export const en: TaskboardDict = {
|
|
|
290
293
|
'form.iso.noneHintNonGit': 'Not a git repository; will run in the project directory',
|
|
291
294
|
'form.iso.noneHint': 'No git; works directly in the project directory',
|
|
292
295
|
'form.iso.nonGitNote': 'This project is not a git repository; the task will run in its directory (still created with the default configuration; the runtime degrades automatically)',
|
|
296
|
+
'form.iso.mirrorNote': 'Multi-repo workspace: worktree isolation automatically mirrors all {n} repos — one task branch each, merged per repo at acceptance',
|
|
293
297
|
'form.field.checklist': 'Acceptance checklist (DoD)',
|
|
294
298
|
'form.field.checklistOptional': 'Acceptance checklist (DoD, optional)',
|
|
295
299
|
'form.check.itemPlaceholder': 'Checklist item {n} (definition of done)',
|
|
@@ -361,7 +365,7 @@ export const en: TaskboardDict = {
|
|
|
361
365
|
'set.subtitle': 'Global defaults for new tasks and session sync',
|
|
362
366
|
'set.iso.heading': 'Default execution isolation',
|
|
363
367
|
'set.iso.noneHint': 'No git; works directly in the project directory (factory default)',
|
|
364
|
-
'set.iso.worktreeHint': 'Each execution runs on its own worktree branch (task/title+ID), isolated from the others',
|
|
368
|
+
'set.iso.worktreeHint': 'Each execution runs on its own worktree branch (task/title+ID), isolated from the others; multi-repo workspaces are mirrored whole (one branch per repo)',
|
|
365
369
|
'set.iso.current': 'Currently saved default: {current}. Affects only tasks created hereafter; existing tasks keep their creation-time choice, and non-git projects still degrade to the project directory at run time.',
|
|
366
370
|
'set.sync.heading': 'Auto-sync workspace sessions',
|
|
367
371
|
'set.sync.off.name': '🚫 Sync off',
|
|
@@ -55,6 +55,24 @@ let unsubscribeService: (() => void) | undefined
|
|
|
55
55
|
let snapshot: TaskboardLocaleSnapshot = { active: detectFallbackLocale(), revision: 0 }
|
|
56
56
|
const listeners = new Set<() => void>()
|
|
57
57
|
|
|
58
|
+
// Late attach (issue #16): the taskboard client injects NOTHING, so it can
|
|
59
|
+
// activate BEFORE the locale service provides — and before the locale
|
|
60
|
+
// runtime syncs <html lang> (the server-rendered static value is "en").
|
|
61
|
+
// Until a real service attaches, a MutationObserver re-detects on every
|
|
62
|
+
// <html lang> change and a short poll re-tries ctx.get('locale').
|
|
63
|
+
let langObserver: MutationObserver | undefined
|
|
64
|
+
let retryTimer: ReturnType<typeof setInterval> | undefined
|
|
65
|
+
|
|
66
|
+
/** Stop every late-attach mechanism (a service took over, or dispose). */
|
|
67
|
+
function clearLateAttach(): void {
|
|
68
|
+
if (retryTimer !== undefined) {
|
|
69
|
+
clearInterval(retryTimer)
|
|
70
|
+
retryTimer = undefined
|
|
71
|
+
}
|
|
72
|
+
try { langObserver?.disconnect() } catch { /* observer already dead */ }
|
|
73
|
+
langObserver = undefined
|
|
74
|
+
}
|
|
75
|
+
|
|
58
76
|
function isLocaleId(value: string): value is LocaleId {
|
|
59
77
|
return value === 'zh' || value === 'en'
|
|
60
78
|
}
|
|
@@ -80,6 +98,45 @@ function detectFallbackLocale(): LocaleId {
|
|
|
80
98
|
return 'en'
|
|
81
99
|
}
|
|
82
100
|
|
|
101
|
+
/** Coerce an unknown ctx value into a usable service face, else undefined. */
|
|
102
|
+
function asLocaleService(value: unknown): LocaleServiceFace | undefined {
|
|
103
|
+
const face = value as LocaleServiceFace | null | undefined
|
|
104
|
+
if (face === null || face === undefined || typeof face.getSnapshot !== 'function' || typeof face.subscribe !== 'function') return undefined
|
|
105
|
+
return face
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Watch for the late locale activation while no service is attached
|
|
110
|
+
* (issue #16): re-detect on <html lang> changes (the locale runtime syncs
|
|
111
|
+
* it once up) and poll ctx.get('locale') briefly (inject ordering — the
|
|
112
|
+
* service may provide a moment after our apply).
|
|
113
|
+
*/
|
|
114
|
+
function startLateAttach(retryGetLocale: (() => unknown) | undefined): void {
|
|
115
|
+
clearLateAttach()
|
|
116
|
+
try {
|
|
117
|
+
if (typeof MutationObserver !== 'undefined' && typeof document !== 'undefined') {
|
|
118
|
+
langObserver = new MutationObserver(() => {
|
|
119
|
+
if (service !== undefined) return // a real service owns the state
|
|
120
|
+
const next = detectFallbackLocale()
|
|
121
|
+
if (next !== snapshot.active) publish({ active: next, revision: snapshot.revision + 1 })
|
|
122
|
+
})
|
|
123
|
+
langObserver.observe(document.documentElement, { attributes: true, attributeFilter: ['lang'] })
|
|
124
|
+
}
|
|
125
|
+
} catch { /* no DOM — the poll below still covers service-side activation */ }
|
|
126
|
+
if (retryGetLocale === undefined) return
|
|
127
|
+
let tries = 0
|
|
128
|
+
retryTimer = setInterval(() => {
|
|
129
|
+
tries += 1
|
|
130
|
+
let face: LocaleServiceFace | undefined
|
|
131
|
+
try { face = asLocaleService(retryGetLocale()) } catch { face = undefined }
|
|
132
|
+
if (face !== undefined) {
|
|
133
|
+
initI18n(face) // attaches the service and tears the late attach down
|
|
134
|
+
return
|
|
135
|
+
}
|
|
136
|
+
if (tries >= 8) clearLateAttach() // ~2s at 250ms — enough for boot ordering
|
|
137
|
+
}, 250)
|
|
138
|
+
}
|
|
139
|
+
|
|
83
140
|
function publish(next: TaskboardLocaleSnapshot): void {
|
|
84
141
|
if (next.active === snapshot.active && next.revision === snapshot.revision) return
|
|
85
142
|
snapshot = next
|
|
@@ -89,18 +146,26 @@ function publish(next: TaskboardLocaleSnapshot): void {
|
|
|
89
146
|
/**
|
|
90
147
|
* Attach the DSH locale service (call from the client entry's apply with
|
|
91
148
|
* ctx.get('locale')). Absent/malformed services are ignored — the fallback
|
|
92
|
-
* detection stays in charge
|
|
149
|
+
* detection stays in charge, and (issue #16) a late attach keeps watching:
|
|
150
|
+
* the locale service can provide AFTER our activation (inject ordering), so
|
|
151
|
+
* pass `retryGetLocale` to re-poll ctx.get('locale') for ~2s and pick the
|
|
152
|
+
* service up the moment it exists; <html lang> changes re-run detection in
|
|
153
|
+
* the meantime.
|
|
93
154
|
* @param localeService - the ctx 'locale' service, when provided.
|
|
155
|
+
* @param retryGetLocale - re-lookup for the service (the client entry passes
|
|
156
|
+
* `() => ctx.get?.('locale')`); polled only while no service is attached.
|
|
94
157
|
*/
|
|
95
|
-
export function initI18n(localeService: unknown): void {
|
|
96
|
-
const face = localeService
|
|
97
|
-
if (face ===
|
|
158
|
+
export function initI18n(localeService: unknown, retryGetLocale?: () => unknown): void {
|
|
159
|
+
const face = asLocaleService(localeService)
|
|
160
|
+
if (face === undefined) {
|
|
98
161
|
// No usable service: resolve the fallback NOW so a caller that inits
|
|
99
162
|
// after the DOM is up gets the current detection, never a stale
|
|
100
|
-
// module-load snapshot.
|
|
163
|
+
// module-load snapshot — then keep watching for the late activation.
|
|
101
164
|
publish({ active: detectFallbackLocale(), revision: 0 })
|
|
165
|
+
startLateAttach(retryGetLocale)
|
|
102
166
|
return
|
|
103
167
|
}
|
|
168
|
+
clearLateAttach()
|
|
104
169
|
service = face
|
|
105
170
|
const sync = (): void => {
|
|
106
171
|
try {
|
|
@@ -115,6 +180,7 @@ export function initI18n(localeService: unknown): void {
|
|
|
115
180
|
|
|
116
181
|
/** Detach the service and return to fallback detection (tests, dispose). */
|
|
117
182
|
export function disposeI18n(): void {
|
|
183
|
+
clearLateAttach()
|
|
118
184
|
try { unsubscribeService?.() } catch { /* source already gone */ }
|
|
119
185
|
unsubscribeService = undefined
|
|
120
186
|
service = undefined
|
package/src/client/i18n/zh.ts
CHANGED
|
@@ -168,6 +168,9 @@ export const zh = {
|
|
|
168
168
|
'iso.merge.button': '⇥ 合并到主工作区',
|
|
169
169
|
'iso.merge.failed': '合并失败:{error}',
|
|
170
170
|
'iso.merge.noop': '该分支没有领先主工作区的新提交,无需合并(可退回续跑或直接清理)',
|
|
171
|
+
'iso.merge.done': '已按仓库合并(--no-ff):{summary}',
|
|
172
|
+
'iso.merge.partial': '部分仓库合并失败:{summary}\n{error}',
|
|
173
|
+
'iso.repo.root': '根仓库',
|
|
171
174
|
'iso.remove.wt': '🗑 删除 worktree',
|
|
172
175
|
'iso.remove.wtTitle': 'git worktree remove(有未提交修改时拒绝)',
|
|
173
176
|
'iso.remove.wtb': '🗑 删 worktree + 分支',
|
|
@@ -292,6 +295,7 @@ export const zh = {
|
|
|
292
295
|
'form.iso.noneHintNonGit': '当前项目非 git 仓库,将在原目录执行',
|
|
293
296
|
'form.iso.noneHint': '不使用 git,直接在项目目录工作',
|
|
294
297
|
'form.iso.nonGitNote': '当前项目非 git 仓库,将在原目录执行(任务仍按默认配置创建,运行时自动降级)',
|
|
298
|
+
'form.iso.mirrorNote': '多仓库工作区:Worktree 隔离将自动整区镜像 {n} 个仓库——每仓库独立任务分支,验收按仓库合并',
|
|
295
299
|
'form.field.checklist': '验收清单(DoD)',
|
|
296
300
|
'form.field.checklistOptional': '验收清单(DoD,可选)',
|
|
297
301
|
'form.check.itemPlaceholder': '验收项 {n}(完成标准)',
|
|
@@ -363,7 +367,7 @@ export const zh = {
|
|
|
363
367
|
'set.subtitle': '新建任务与会话同步的全局默认值',
|
|
364
368
|
'set.iso.heading': '默认执行隔离',
|
|
365
369
|
'set.iso.noneHint': '不使用 git,直接在项目目录工作(出厂默认)',
|
|
366
|
-
'set.iso.worktreeHint': '每次执行在独立 worktree 分支上进行(task/标题+ID
|
|
370
|
+
'set.iso.worktreeHint': '每次执行在独立 worktree 分支上进行(task/标题+ID),互不污染;多仓库工作区自动整区镜像(每仓库独立分支)',
|
|
367
371
|
'set.iso.current': '当前保存的默认:{current}。仅影响之后新建的任务;已有任务保持创建时的选择,非 git 项目运行时仍自动降级原目录。',
|
|
368
372
|
'set.sync.heading': '自动同步工作区会话',
|
|
369
373
|
'set.sync.off.name': '🚫 关闭同步',
|
package/src/client/index.ts
CHANGED
|
@@ -70,8 +70,11 @@ export function apply(ctx: ClientContextFace): void {
|
|
|
70
70
|
injectStyles()
|
|
71
71
|
// Locale source (设置 → 通用设置 → 语言): soft-attached — absent on
|
|
72
72
|
// compositions without the DSH locale plugin, where the fallback
|
|
73
|
-
// (<html lang> / navigator) takes over. Never a hard inject.
|
|
74
|
-
|
|
73
|
+
// (<html lang> / navigator) takes over. Never a hard inject. The getter
|
|
74
|
+
// rides along (issue #16): our client bundle activates with zero service
|
|
75
|
+
// deps, potentially BEFORE the locale service provides — the runtime
|
|
76
|
+
// re-polls it for ~2s and watches <html lang> until it does.
|
|
77
|
+
initI18n(ctx.get?.('locale'), () => ctx.get?.('locale'))
|
|
75
78
|
const client = createClient()
|
|
76
79
|
const controller = new BoardController(client)
|
|
77
80
|
|
package/src/client/styles.ts
CHANGED
|
@@ -622,6 +622,13 @@ button.dsh-atb-chip2.dsh-atb-chip-btn:hover {
|
|
|
622
622
|
.dsh-atb-isolation-note { display: block; margin-top: 6px; font-size: 11px; color: var(--dsw-alias-label-tertiary, gray); }
|
|
623
623
|
.dsh-atb-mode-picker[data-disabled="true"] .dsh-atb-mode-opt { cursor: not-allowed; opacity: .55; }
|
|
624
624
|
.dsh-atb-iso-none { font-size: 12.5px; color: var(--dsw-alias-label-secondary, inherit); }
|
|
625
|
+
.dsh-atb-iso-repo[data-multi="true"] { padding: 6px 0 2px; border-top: 1px dashed var(--dsw-alias-border-secondary, #ddd2); }
|
|
626
|
+
.dsh-atb-iso-repo[data-multi="true"]:first-of-type { border-top: none; padding-top: 0; }
|
|
627
|
+
.dsh-atb-iso-repohead { display: flex; flex-wrap: wrap; gap: 6px 12px; margin-bottom: 6px; align-items: baseline; }
|
|
628
|
+
.dsh-atb-iso-repohead code {
|
|
629
|
+
font-family: ui-monospace, Consolas, monospace; font-size: 11.5px; font-weight: 600;
|
|
630
|
+
color: var(--dsw-alias-state-business-primary, #3e63dd);
|
|
631
|
+
}
|
|
625
632
|
.dsh-atb-iso-facts { display: flex; flex-wrap: wrap; gap: 6px 12px; margin-bottom: 8px; }
|
|
626
633
|
.dsh-atb-iso-fact { font-size: 11.5px; color: var(--dsw-alias-label-secondary, inherit); }
|
|
627
634
|
.dsh-atb-iso-fact b { font-weight: 600; color: var(--dsw-alias-state-business-primary, #3e63dd); }
|