opencode-extended-sidebar 0.2.90
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/.oesignore +17 -0
- package/CHANGELOG.md +95 -0
- package/LICENSE +21 -0
- package/README.md +235 -0
- package/oes.json +11 -0
- package/package.json +100 -0
- package/scripts/install-git-hooks.mjs +29 -0
- package/src/pware.oc.core/constants/index.ts +18 -0
- package/src/pware.oc.core/constants/pware.oc.core.constants.eventKind.ts +29 -0
- package/src/pware.oc.core/constants/pware.oc.core.constants.eventName.ts +13 -0
- package/src/pware.oc.core/constants/pware.oc.core.constants.eventType.ts +241 -0
- package/src/pware.oc.core/constants/pware.oc.core.constants.myWork.ts +55 -0
- package/src/pware.oc.core/constants/pware.oc.core.constants.partType.ts +65 -0
- package/src/pware.oc.core/constants/pware.oc.core.constants.phase.ts +58 -0
- package/src/pware.oc.core/constants/pware.oc.core.constants.pulse.ts +63 -0
- package/src/pware.oc.core/constants/pware.oc.core.constants.rowKind.ts +34 -0
- package/src/pware.oc.core/constants/pware.oc.core.constants.status.ts +74 -0
- package/src/pware.oc.core/constants/pware.oc.core.constants.toolName.ts +69 -0
- package/src/pware.oc.core/git/index.ts +7 -0
- package/src/pware.oc.core/git/pware.oc.core.git.ts +237 -0
- package/src/pware.oc.core/git/pware.oc.core.gitignore.ts +87 -0
- package/src/pware.oc.core/index.ts +18 -0
- package/src/pware.oc.core/pware.oc.core.bus.ts +42 -0
- package/src/pware.oc.core/pware.oc.core.cache.ts +48 -0
- package/src/pware.oc.core/pware.oc.core.clipboard.ts +97 -0
- package/src/pware.oc.core/pware.oc.core.debug.ts +280 -0
- package/src/pware.oc.core/pware.oc.core.events.ts +79 -0
- package/src/pware.oc.core/pware.oc.core.glyph.ts +135 -0
- package/src/pware.oc.core/pware.oc.core.layout.ts +234 -0
- package/src/pware.oc.core/pware.oc.core.oes.ts +89 -0
- package/src/pware.oc.core/pware.oc.core.paths.ts +240 -0
- package/src/pware.oc.core/pware.oc.core.preview.ts +74 -0
- package/src/pware.oc.core/pware.oc.core.pulse.ts +726 -0
- package/src/pware.oc.core/pware.oc.core.sqlite.ts +175 -0
- package/src/pware.oc.core/pware.oc.core.status.ts +214 -0
- package/src/pware.oc.core/pware.oc.core.timing.ts +63 -0
- package/src/pware.oc.core/pware.oc.core.width.ts +215 -0
- package/src/pware.oc.omo/constants/index.ts +15 -0
- package/src/pware.oc.omo/constants/pware.oc.omo.constants.backgroundTask.ts +18 -0
- package/src/pware.oc.omo/constants/pware.oc.omo.constants.boulderStatus.ts +49 -0
- package/src/pware.oc.omo/constants/pware.oc.omo.constants.docKind.ts +29 -0
- package/src/pware.oc.omo/constants/pware.oc.omo.constants.eventName.ts +11 -0
- package/src/pware.oc.omo/constants/pware.oc.omo.constants.planStatus.ts +60 -0
- package/src/pware.oc.omo/constants/pware.oc.omo.constants.reviewStatus.ts +49 -0
- package/src/pware.oc.omo/constants/pware.oc.omo.constants.startWork.ts +25 -0
- package/src/pware.oc.omo/constants/pware.oc.omo.constants.verdict.ts +38 -0
- package/src/pware.oc.omo/index.ts +7 -0
- package/src/pware.oc.omo/resolver/index.ts +19 -0
- package/src/pware.oc.omo/resolver/pware.oc.omo.resolver.approval.ts +179 -0
- package/src/pware.oc.omo/resolver/pware.oc.omo.resolver.approvalGroup.ts +116 -0
- package/src/pware.oc.omo/resolver/pware.oc.omo.resolver.approvalState.ts +34 -0
- package/src/pware.oc.omo/resolver/pware.oc.omo.resolver.boulder.ts +503 -0
- package/src/pware.oc.omo/resolver/pware.oc.omo.resolver.config.ts +40 -0
- package/src/pware.oc.omo/resolver/pware.oc.omo.resolver.doc.ts +269 -0
- package/src/pware.oc.omo/resolver/pware.oc.omo.resolver.draftFile.ts +18 -0
- package/src/pware.oc.omo/resolver/pware.oc.omo.resolver.notepadsFile.ts +18 -0
- package/src/pware.oc.omo/resolver/pware.oc.omo.resolver.plan.ts +155 -0
- package/src/pware.oc.omo/resolver/pware.oc.omo.resolver.planFile.ts +361 -0
- package/src/pware.oc.omo/resolver/pware.oc.omo.resolver.proofFile.ts +18 -0
- package/src/pware.oc.omo/resolver/pware.oc.omo.resolver.rulesFile.ts +11 -0
- package/src/pware.oc.omo/resolver/pware.oc.omo.resolver.runContinuationFile.ts +14 -0
- package/src/pware.oc.opencode/constants/index.ts +10 -0
- package/src/pware.oc.opencode/constants/pware.oc.opencode.constants.eventName.ts +15 -0
- package/src/pware.oc.opencode/constants/pware.oc.opencode.constants.fileTouch.ts +18 -0
- package/src/pware.oc.opencode/constants/pware.oc.opencode.constants.questionKind.ts +45 -0
- package/src/pware.oc.opencode/constants/pware.oc.opencode.constants.sessionStatus.ts +58 -0
- package/src/pware.oc.opencode/index.ts +8 -0
- package/src/pware.oc.opencode/pware.oc.opencode.events.ts +182 -0
- package/src/pware.oc.opencode/pware.oc.opencode.files.ts +399 -0
- package/src/pware.oc.opencode/resolver/index.ts +170 -0
- package/src/pware.oc.opencode/resolver/pware.oc.opencode.resolver.file.ts +162 -0
- package/src/pware.oc.opencode/resolver/pware.oc.opencode.resolver.question.ts +185 -0
- package/src/pware.oc.opencode/resolver/pware.oc.opencode.resolver.session.ts +211 -0
- package/src/pware.oc.opencode/resolver/pware.oc.opencode.resolver.todo.ts +26 -0
- package/src/pware.oc.opencode/resolver/pware.oc.opencode.resolver.tool.ts +236 -0
- package/src/pware.oc.perf/index.ts +14 -0
- package/src/pware.oc.perf/pware.oc.perf.asciichart.d.ts +15 -0
- package/src/pware.oc.perf/pware.oc.perf.charts.ts +284 -0
- package/src/pware.oc.perf/pware.oc.perf.reader.ts +927 -0
- package/src/pware.oc.perf/pware.oc.perf.realtime.ts +88 -0
- package/src/pware.oc.perf/pware.oc.perf.realtimeBlock.ts +118 -0
- package/src/pware.oc.perf/pware.oc.perf.realtimeCpuRam.ts +79 -0
- package/src/pware.oc.perf/pware.oc.perf.realtimeSampler.ts +71 -0
- package/src/pware.oc.perf/pware.oc.perf.realtimeTimeline.ts +214 -0
- package/src/pware.oc.perf/pware.oc.perf.self.ts +179 -0
- package/src/pware.oc.perf/pware.oc.perf.view.tsx +462 -0
- package/src/pware.oc.runtime/index.ts +13 -0
- package/src/pware.oc.runtime/pware.oc.runtime.monitor.ts +142 -0
- package/src/pware.oc.runtime/pware.oc.runtime.mywork-enrich.ts +79 -0
- package/src/pware.oc.runtime/pware.oc.runtime.mywork.ts +253 -0
- package/src/pware.oc.runtime/pware.oc.runtime.questions.ts +95 -0
- package/src/pware.oc.runtime/pware.oc.runtime.snapshotClient.ts +129 -0
- package/src/pware.oc.runtime/pware.oc.runtime.source.ts +105 -0
- package/src/pware.oc.runtime/pware.oc.runtime.worker.ts +43 -0
- package/src/pware.oc.runtime/resolver/index.ts +224 -0
- package/src/pware.oc.runtime/resolver/pware.oc.runtime.resolver.delegate.ts +146 -0
- package/src/pware.oc.ui/index.ts +12 -0
- package/src/pware.oc.ui/pware.oc.ui.chrome.tsx +339 -0
- package/src/pware.oc.ui/pware.oc.ui.glyphs.tsx +144 -0
- package/src/pware.oc.ui/pware.oc.ui.host.tsx +111 -0
- package/src/pware.oc.ui/pware.oc.ui.live.tsx +82 -0
- package/src/pware.oc.ui/pware.oc.ui.menudialogs.tsx +843 -0
- package/src/pware.oc.ui/pware.oc.ui.sections.tsx +526 -0
- package/src/pware.oc.ui/pware.oc.ui.sidebar.tsx +1629 -0
- package/src/pware.oc.ui.tsx +98 -0
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Watch boulder.json + poll SQLite stamps. Invokes onChange when fingerprint changes.
|
|
3
|
+
*/
|
|
4
|
+
import fs from "node:fs"
|
|
5
|
+
import path from "node:path"
|
|
6
|
+
import type { PwareEvent } from "../pware.oc.core/pware.oc.core.bus.js"
|
|
7
|
+
import { EV_OES_SNAPSHOT } from "../pware.oc.core/constants/pware.oc.core.constants.eventName.js"
|
|
8
|
+
import { EV_OMO_BOULDER_CHANGED } from "../pware.oc.omo/constants/pware.oc.omo.constants.eventName.js"
|
|
9
|
+
import { findBoulder } from "../pware.oc.omo/resolver/index.js"
|
|
10
|
+
import { computeFingerprint, type RuntimeSnapshot } from "./resolver/index.js"
|
|
11
|
+
import { readRuntimeSnapshotAsync } from "./pware.oc.runtime.snapshotClient.js"
|
|
12
|
+
import { getOpenCodeDbPath } from "../pware.oc.core/pware.oc.core.paths.js"
|
|
13
|
+
import { MONITOR_POLL_MS, MONITOR_WATCH_DEBOUNCE_MS } from "../pware.oc.core/pware.oc.core.timing.js"
|
|
14
|
+
import { dbg, profile } from "../pware.oc.core/pware.oc.core.debug.js"
|
|
15
|
+
|
|
16
|
+
export type MonitorOptions = {
|
|
17
|
+
sessionId: string
|
|
18
|
+
projectRoot: string | null
|
|
19
|
+
dbPath?: string
|
|
20
|
+
pollMs?: number
|
|
21
|
+
onChange?: (snap: RuntimeSnapshot) => void
|
|
22
|
+
emit?: (evt: PwareEvent) => void
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export type MonitorHandle = {
|
|
26
|
+
stop: () => void
|
|
27
|
+
refresh: () => void
|
|
28
|
+
question: (sessionId: string) => void
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function startMonitor(opts: MonitorOptions): MonitorHandle {
|
|
32
|
+
const dbPath = opts.dbPath || getOpenCodeDbPath(process.env, undefined, opts.projectRoot)
|
|
33
|
+
dbg("monitor", "start", { dbPath, sessionId: opts.sessionId, projectRoot: opts.projectRoot ?? null })
|
|
34
|
+
const pollMs = opts.pollMs ?? MONITOR_POLL_MS
|
|
35
|
+
let lastFp = ""
|
|
36
|
+
let stopped = false
|
|
37
|
+
let emitGen = 0
|
|
38
|
+
let debounce: ReturnType<typeof setTimeout> | null = null
|
|
39
|
+
let pendingQuestionHint: string | null = null
|
|
40
|
+
const watchers: fs.FSWatcher[] = []
|
|
41
|
+
|
|
42
|
+
const emit = (force = false) =>
|
|
43
|
+
profile("monitor.emit", () => {
|
|
44
|
+
if (stopped) return
|
|
45
|
+
const fp = computeFingerprint({
|
|
46
|
+
dbPath,
|
|
47
|
+
projectRoot: opts.projectRoot,
|
|
48
|
+
sessionId: opts.sessionId,
|
|
49
|
+
})
|
|
50
|
+
if (!force && fp === lastFp) return
|
|
51
|
+
dbg("monitor", "emit", { force, fp: fp.slice(0, 40) })
|
|
52
|
+
lastFp = fp
|
|
53
|
+
const gen = ++emitGen
|
|
54
|
+
const questionHint = pendingQuestionHint ?? undefined
|
|
55
|
+
pendingQuestionHint = null
|
|
56
|
+
void readRuntimeSnapshotAsync({
|
|
57
|
+
sessionId: opts.sessionId,
|
|
58
|
+
projectRoot: opts.projectRoot,
|
|
59
|
+
dbPath,
|
|
60
|
+
questionHint,
|
|
61
|
+
}).then((snapshot) => {
|
|
62
|
+
if (stopped || gen !== emitGen) return
|
|
63
|
+
opts.onChange?.(snapshot)
|
|
64
|
+
opts.emit?.({
|
|
65
|
+
type: EV_OES_SNAPSHOT,
|
|
66
|
+
ts: Date.now(),
|
|
67
|
+
data: { snapshot },
|
|
68
|
+
})
|
|
69
|
+
})
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
const schedule = () => {
|
|
73
|
+
if (stopped) return
|
|
74
|
+
if (debounce) clearTimeout(debounce)
|
|
75
|
+
debounce = setTimeout(() => {
|
|
76
|
+
debounce = null
|
|
77
|
+
emit()
|
|
78
|
+
}, MONITOR_WATCH_DEBOUNCE_MS)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Initial
|
|
82
|
+
emit(true)
|
|
83
|
+
|
|
84
|
+
const interval = setInterval(schedule, pollMs)
|
|
85
|
+
|
|
86
|
+
const root = opts.projectRoot
|
|
87
|
+
if (root) {
|
|
88
|
+
const boulderPath = findBoulder(root)
|
|
89
|
+
if (boulderPath) {
|
|
90
|
+
try {
|
|
91
|
+
watchers.push(
|
|
92
|
+
fs.watch(boulderPath, { persistent: false }, () => {
|
|
93
|
+
opts.emit?.({
|
|
94
|
+
type: EV_OMO_BOULDER_CHANGED,
|
|
95
|
+
ts: Date.now(),
|
|
96
|
+
data: {},
|
|
97
|
+
})
|
|
98
|
+
schedule()
|
|
99
|
+
}),
|
|
100
|
+
)
|
|
101
|
+
} catch {
|
|
102
|
+
// poll covers it
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// SQLite: watching the file is flaky under WAL — poll stamps instead.
|
|
108
|
+
// Still try a non-recursive watch on the data dir for companions.
|
|
109
|
+
try {
|
|
110
|
+
const dataDir = path.dirname(dbPath)
|
|
111
|
+
if (fs.existsSync(dataDir)) {
|
|
112
|
+
watchers.push(
|
|
113
|
+
fs.watch(dataDir, { persistent: false }, (event, filename) => {
|
|
114
|
+
const name = filename?.toString() || ""
|
|
115
|
+
if (!name || name.includes("opencode.db")) schedule()
|
|
116
|
+
}),
|
|
117
|
+
)
|
|
118
|
+
}
|
|
119
|
+
} catch {
|
|
120
|
+
// poll only
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return {
|
|
124
|
+
refresh: () => emit(true),
|
|
125
|
+
question: (sessionId: string) => {
|
|
126
|
+
pendingQuestionHint = sessionId
|
|
127
|
+
emit(true)
|
|
128
|
+
},
|
|
129
|
+
stop: () => {
|
|
130
|
+
stopped = true
|
|
131
|
+
if (debounce) clearTimeout(debounce)
|
|
132
|
+
clearInterval(interval)
|
|
133
|
+
for (const w of watchers) {
|
|
134
|
+
try {
|
|
135
|
+
w.close()
|
|
136
|
+
} catch {
|
|
137
|
+
// ignore
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
},
|
|
141
|
+
}
|
|
142
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* pware.oc.runtime.mywork-enrich
|
|
3
|
+
*
|
|
4
|
+
* Enriches the OMO approval queue with the writer session's activity state:
|
|
5
|
+
* builds the plan-file → writer-session index once, then per item resolves the
|
|
6
|
+
* writer via `basenameOf(item.rel)` and asks `sessionActivityState` (SQLite
|
|
7
|
+
* freshness + the omo `.omo/run-continuation` marker). Soft-fails to
|
|
8
|
+
* `sessionState: null` per item — never throws. This is the runtime layer's
|
|
9
|
+
* job: it composes omo (`.omo/run-continuation`) with opencode (SQLite session
|
|
10
|
+
* activity).
|
|
11
|
+
*/
|
|
12
|
+
import fs from "node:fs"
|
|
13
|
+
import {
|
|
14
|
+
sessionActivityState,
|
|
15
|
+
type SessionActivityState,
|
|
16
|
+
} from "../pware.oc.opencode/resolver/pware.oc.opencode.resolver.session.js"
|
|
17
|
+
import { listTodos } from "../pware.oc.opencode/resolver/pware.oc.opencode.resolver.todo.js"
|
|
18
|
+
import { planSessionIndex } from "../pware.oc.omo/resolver/index.js"
|
|
19
|
+
import { openReadonlyDb, withDbRead, type SqlDb } from "../pware.oc.core/pware.oc.core.sqlite.js"
|
|
20
|
+
import { profile } from "../pware.oc.core/pware.oc.core.debug.js"
|
|
21
|
+
import { basenameOf } from "../pware.oc.core/pware.oc.core.paths.js"
|
|
22
|
+
import { readRunContinuationState } from "../pware.oc.omo/resolver/pware.oc.omo.resolver.approvalState.js"
|
|
23
|
+
import { BACKGROUND_TASK_ACTIVE } from "../pware.oc.omo/constants/pware.oc.omo.constants.backgroundTask.js"
|
|
24
|
+
import { STATUS_COMPLETED } from "../pware.oc.core/constants/pware.oc.core.constants.status.js"
|
|
25
|
+
import type { ApprovalItem } from "../pware.oc.omo/resolver/pware.oc.omo.resolver.plan.js"
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* An approval item with its planner-session activity attached. `sessionState`
|
|
29
|
+
* is a runtime enrichment: it lives here on the runtime layer's enriched type,
|
|
30
|
+
* not on omo's base `ApprovalItem` (omo must not import the opencode session type).
|
|
31
|
+
*/
|
|
32
|
+
export type EnrichedApproval = ApprovalItem & {
|
|
33
|
+
sessionState: SessionActivityState | null
|
|
34
|
+
/** Writer session's todos all completed (and at least one present). */
|
|
35
|
+
todosDone: boolean
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function blank(items: readonly ApprovalItem[]): EnrichedApproval[] {
|
|
39
|
+
return items.map((item) => ({ ...item, sessionState: null, todosDone: false }))
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function sessionTodosDone(db: SqlDb, sessionId: string): boolean {
|
|
43
|
+
const todos = listTodos(db, sessionId)
|
|
44
|
+
return todos.length > 0 && todos.every((t) => t.status === STATUS_COMPLETED)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function enrichApprovalSessionStates(
|
|
48
|
+
items: readonly ApprovalItem[],
|
|
49
|
+
opts: { dbPath: string | null | undefined; projectRoot: string | null | undefined; now?: number },
|
|
50
|
+
): EnrichedApproval[] {
|
|
51
|
+
if (items.length === 0) return []
|
|
52
|
+
const dbPath = opts.dbPath
|
|
53
|
+
const projectRoot = opts.projectRoot
|
|
54
|
+
if (!dbPath || !projectRoot || !fs.existsSync(dbPath)) return blank(items)
|
|
55
|
+
return profile("mywork.approvals", () =>
|
|
56
|
+
withDbRead(
|
|
57
|
+
() => {
|
|
58
|
+
const db = openReadonlyDb(dbPath)
|
|
59
|
+
if (!db) return blank(items)
|
|
60
|
+
// Index once; match writers by basename across all sessions (no
|
|
61
|
+
// projectId in the opts shape — same scope as sessionForPlanFile).
|
|
62
|
+
const index = planSessionIndex(db, null, projectRoot)
|
|
63
|
+
return items.map((item) => {
|
|
64
|
+
const sessionId = index.fileWriter.get(basenameOf(item.rel))?.sessionId ?? null
|
|
65
|
+
const sessionState = sessionId
|
|
66
|
+
? sessionActivityState(db, sessionId, {
|
|
67
|
+
backgroundTaskActive:
|
|
68
|
+
readRunContinuationState(projectRoot, sessionId) === BACKGROUND_TASK_ACTIVE,
|
|
69
|
+
now: opts.now,
|
|
70
|
+
})
|
|
71
|
+
: null
|
|
72
|
+
const todosDone = sessionId ? sessionTodosDone(db, sessionId) : false
|
|
73
|
+
return { ...item, sessionState, todosDone }
|
|
74
|
+
})
|
|
75
|
+
},
|
|
76
|
+
() => blank(items),
|
|
77
|
+
),
|
|
78
|
+
)
|
|
79
|
+
}
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* pware.oc.core.mywork.resolver
|
|
3
|
+
*
|
|
4
|
+
* The "My work" queue — one list of things awaiting the user's action, from
|
|
5
|
+
* two sources: an open OpenCode `question` (core, always) and an OMO plan
|
|
6
|
+
* awaiting approval (optional, only when `.omo/` is present).
|
|
7
|
+
*/
|
|
8
|
+
import type { OpenQuestion } from "../pware.oc.opencode/resolver/pware.oc.opencode.resolver.question.js"
|
|
9
|
+
import type {
|
|
10
|
+
AgentStatus,
|
|
11
|
+
SessionActivityState,
|
|
12
|
+
} from "../pware.oc.opencode/resolver/pware.oc.opencode.resolver.session.js"
|
|
13
|
+
import type { ReviewState } from "../pware.oc.omo/resolver/pware.oc.omo.resolver.plan.js"
|
|
14
|
+
import type { EnrichedApproval } from "./pware.oc.runtime.mywork-enrich.js"
|
|
15
|
+
import {
|
|
16
|
+
MY_WORK_GROUP_DISMISSED,
|
|
17
|
+
MY_WORK_GROUP_DRAFT_DOCS,
|
|
18
|
+
MY_WORK_GROUP_DRAFTING,
|
|
19
|
+
MY_WORK_GROUP_FINISHED,
|
|
20
|
+
MY_WORK_GROUP_PINNED,
|
|
21
|
+
MY_WORK_GROUP_PLANS,
|
|
22
|
+
MY_WORK_GROUP_READY_REVIEW,
|
|
23
|
+
MY_WORK_GROUP_READY_START,
|
|
24
|
+
MY_WORK_GROUP_SESSIONS,
|
|
25
|
+
type ApprovalGroupKind,
|
|
26
|
+
} from "../pware.oc.core/constants/pware.oc.core.constants.myWork.js"
|
|
27
|
+
import { isDraftOf, resolveApprovalGroup } from "../pware.oc.omo/resolver/pware.oc.omo.resolver.approvalGroup.js"
|
|
28
|
+
import {
|
|
29
|
+
QUESTION_KIND_ERROR,
|
|
30
|
+
QUESTION_KIND_INTERRUPTED,
|
|
31
|
+
QUESTION_KIND_QUESTION,
|
|
32
|
+
type OpenQuestionKind,
|
|
33
|
+
} from "../pware.oc.opencode/constants/pware.oc.opencode.constants.questionKind.js"
|
|
34
|
+
import {
|
|
35
|
+
START_WORK_MAKE_PR,
|
|
36
|
+
START_WORK_PLAIN,
|
|
37
|
+
START_WORK_SHIP,
|
|
38
|
+
type StartWorkMode,
|
|
39
|
+
} from "../pware.oc.omo/constants/pware.oc.omo.constants.startWork.js"
|
|
40
|
+
|
|
41
|
+
export type { ApprovalGroupKind, OpenQuestionKind, StartWorkMode }
|
|
42
|
+
|
|
43
|
+
export type QuestionGroupKind = OpenQuestionKind | typeof MY_WORK_GROUP_DISMISSED
|
|
44
|
+
|
|
45
|
+
export type MyWorkItem =
|
|
46
|
+
| {
|
|
47
|
+
kind: QuestionGroupKind
|
|
48
|
+
partId: string
|
|
49
|
+
sessionId: string
|
|
50
|
+
title: string
|
|
51
|
+
startedAt: number | null
|
|
52
|
+
/** `state.error` text for interrupted/error rows; null for an open question. */
|
|
53
|
+
reason: string | null
|
|
54
|
+
}
|
|
55
|
+
| {
|
|
56
|
+
kind: ApprovalGroupKind
|
|
57
|
+
name: string
|
|
58
|
+
rel: string
|
|
59
|
+
pendingAction: string | null
|
|
60
|
+
updatedAt: number | null
|
|
61
|
+
sessionState: SessionActivityState | null
|
|
62
|
+
review: ReviewState | null
|
|
63
|
+
}
|
|
64
|
+
| {
|
|
65
|
+
kind: typeof MY_WORK_GROUP_SESSIONS
|
|
66
|
+
sessionId: string
|
|
67
|
+
title: string
|
|
68
|
+
status: AgentStatus
|
|
69
|
+
timeUpdated: number | null
|
|
70
|
+
}
|
|
71
|
+
| {
|
|
72
|
+
kind: typeof MY_WORK_GROUP_PINNED
|
|
73
|
+
sessionId: string
|
|
74
|
+
title: string
|
|
75
|
+
status: AgentStatus
|
|
76
|
+
timeUpdated: number | null
|
|
77
|
+
}
|
|
78
|
+
| {
|
|
79
|
+
kind: typeof MY_WORK_GROUP_DRAFT_DOCS
|
|
80
|
+
name: string
|
|
81
|
+
rel: string
|
|
82
|
+
updatedAt: number | null
|
|
83
|
+
}
|
|
84
|
+
| {
|
|
85
|
+
kind: typeof MY_WORK_GROUP_PLANS
|
|
86
|
+
name: string
|
|
87
|
+
rel: string
|
|
88
|
+
updatedAt: number | null
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export type MyWorkKind = MyWorkItem["kind"]
|
|
92
|
+
|
|
93
|
+
export const MY_WORK_ORDER: readonly MyWorkKind[] = [
|
|
94
|
+
MY_WORK_GROUP_PINNED,
|
|
95
|
+
QUESTION_KIND_QUESTION,
|
|
96
|
+
QUESTION_KIND_INTERRUPTED,
|
|
97
|
+
QUESTION_KIND_ERROR,
|
|
98
|
+
MY_WORK_GROUP_SESSIONS,
|
|
99
|
+
MY_WORK_GROUP_READY_REVIEW,
|
|
100
|
+
MY_WORK_GROUP_READY_START,
|
|
101
|
+
MY_WORK_GROUP_FINISHED,
|
|
102
|
+
MY_WORK_GROUP_DRAFTING,
|
|
103
|
+
MY_WORK_GROUP_DRAFT_DOCS,
|
|
104
|
+
MY_WORK_GROUP_PLANS,
|
|
105
|
+
MY_WORK_GROUP_DISMISSED,
|
|
106
|
+
]
|
|
107
|
+
|
|
108
|
+
const MY_WORK_LABELS: Record<MyWorkKind, string> = {
|
|
109
|
+
[MY_WORK_GROUP_PINNED]: "Pinned",
|
|
110
|
+
[QUESTION_KIND_QUESTION]: "Awaiting answer",
|
|
111
|
+
[QUESTION_KIND_INTERRUPTED]: "Interrupted",
|
|
112
|
+
[QUESTION_KIND_ERROR]: "Errors",
|
|
113
|
+
[MY_WORK_GROUP_SESSIONS]: "Sessions",
|
|
114
|
+
[MY_WORK_GROUP_READY_REVIEW]: "Ready to review",
|
|
115
|
+
[MY_WORK_GROUP_READY_START]: "Ready to start",
|
|
116
|
+
[MY_WORK_GROUP_FINISHED]: "Finished",
|
|
117
|
+
[MY_WORK_GROUP_DISMISSED]: "Dismissed questions",
|
|
118
|
+
[MY_WORK_GROUP_DRAFTING]: "Drafting",
|
|
119
|
+
[MY_WORK_GROUP_DRAFT_DOCS]: "Draft docs",
|
|
120
|
+
[MY_WORK_GROUP_PLANS]: "Plans",
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export function myWorkLabel(kind: MyWorkKind): string {
|
|
124
|
+
return MY_WORK_LABELS[kind]
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Why "Navigate to session" is unavailable for an approval: no writer session
|
|
129
|
+
* was found in the database, or the database itself could not be opened. Null
|
|
130
|
+
* when navigation is actionable.
|
|
131
|
+
*/
|
|
132
|
+
export function approvalContinueHint(
|
|
133
|
+
sessionId: string | null | undefined,
|
|
134
|
+
dbAvailable: boolean,
|
|
135
|
+
): string | null {
|
|
136
|
+
if (sessionId) return null
|
|
137
|
+
return dbAvailable ? "No session wrote this plan" : "Database unavailable"
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** The exact "start work" command text for a delivery mode and optional plan name. */
|
|
141
|
+
export function startWorkCommand(mode: StartWorkMode, planName?: string | null): string {
|
|
142
|
+
const name = typeof planName === "string" ? planName.trim() : ""
|
|
143
|
+
const base = name ? `start work ${name}` : "start work"
|
|
144
|
+
if (mode === START_WORK_MAKE_PR) return `${base} --make-pr`
|
|
145
|
+
if (mode === START_WORK_SHIP) return `${base} --ship`
|
|
146
|
+
return base
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export function groupMyWork(
|
|
150
|
+
items: readonly MyWorkItem[],
|
|
151
|
+
): { kind: MyWorkKind; items: MyWorkItem[] }[] {
|
|
152
|
+
const out: { kind: MyWorkKind; items: MyWorkItem[] }[] = []
|
|
153
|
+
for (const kind of MY_WORK_ORDER) {
|
|
154
|
+
const bucket = items.filter((i) => i.kind === kind)
|
|
155
|
+
if (bucket.length > 0) out.push({ kind, items: bucket })
|
|
156
|
+
}
|
|
157
|
+
return out
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** Build the question items from an open-question read (title comes from the row). */
|
|
161
|
+
export function toQuestionItems(questions: readonly OpenQuestion[]): MyWorkItem[] {
|
|
162
|
+
return questions.map((q) => ({
|
|
163
|
+
kind: q.kind,
|
|
164
|
+
partId: q.partId,
|
|
165
|
+
sessionId: q.sessionId,
|
|
166
|
+
title: q.title,
|
|
167
|
+
startedAt: q.startedAt,
|
|
168
|
+
reason: q.reason,
|
|
169
|
+
}))
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/** Build the approval items from a pending-approval read, dropping superseded plans. */
|
|
173
|
+
export function toApprovalItems(approvals: readonly EnrichedApproval[]): MyWorkItem[] {
|
|
174
|
+
const out: MyWorkItem[] = []
|
|
175
|
+
for (const a of approvals) {
|
|
176
|
+
const kind = resolveApprovalGroup(a.status, isDraftOf(a.rel), a.workState, a.todosDone)
|
|
177
|
+
if (!kind) continue
|
|
178
|
+
out.push({
|
|
179
|
+
kind,
|
|
180
|
+
name: a.name,
|
|
181
|
+
rel: a.rel,
|
|
182
|
+
pendingAction: a.pendingAction,
|
|
183
|
+
updatedAt: a.updatedAt,
|
|
184
|
+
sessionState: a.sessionState,
|
|
185
|
+
review: a.review,
|
|
186
|
+
})
|
|
187
|
+
}
|
|
188
|
+
return out
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/** Build the Draft docs items — drafts no action group covers, browsed as files. */
|
|
192
|
+
export function toDraftDocItems(
|
|
193
|
+
docs: readonly { name: string; rel: string; updatedAt: number | null }[],
|
|
194
|
+
): MyWorkItem[] {
|
|
195
|
+
return docs.map((d) => ({ kind: MY_WORK_GROUP_DRAFT_DOCS, name: d.name, rel: d.rel, updatedAt: d.updatedAt }))
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/** Build the Plans items — plan files no action group covers, browsed as files. */
|
|
199
|
+
export function toPlanItems(
|
|
200
|
+
docs: readonly { name: string; rel: string; updatedAt: number | null }[],
|
|
201
|
+
): MyWorkItem[] {
|
|
202
|
+
return docs.map((d) => ({ kind: MY_WORK_GROUP_PLANS, name: d.name, rel: d.rel, updatedAt: d.updatedAt }))
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/** Build the sessions group from recent main sessions — every session, live or idle. */
|
|
206
|
+
export function toSessionItems(
|
|
207
|
+
sessions: readonly {
|
|
208
|
+
id: string
|
|
209
|
+
title: string
|
|
210
|
+
status: AgentStatus
|
|
211
|
+
timeUpdated: number | null
|
|
212
|
+
}[],
|
|
213
|
+
): MyWorkItem[] {
|
|
214
|
+
return sessions.map((s) => ({
|
|
215
|
+
kind: MY_WORK_GROUP_SESSIONS,
|
|
216
|
+
sessionId: s.id,
|
|
217
|
+
title: s.title,
|
|
218
|
+
status: s.status,
|
|
219
|
+
timeUpdated: s.timeUpdated,
|
|
220
|
+
}))
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/** Parse the persisted dismissed-question set (a JSON array of part ids) from the kv store. */
|
|
224
|
+
export function parseDismissed(raw: string | null | undefined): ReadonlySet<string> {
|
|
225
|
+
if (!raw) return new Set()
|
|
226
|
+
try {
|
|
227
|
+
const value = JSON.parse(raw)
|
|
228
|
+
if (!Array.isArray(value)) return new Set()
|
|
229
|
+
return new Set(value.filter((x): x is string => typeof x === "string" && x.length > 0))
|
|
230
|
+
} catch {
|
|
231
|
+
return new Set()
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/** Serialize a dismissed-question id set back to the JSON string the kv store holds. */
|
|
236
|
+
export function formatDismissed(ids: ReadonlySet<string>): string {
|
|
237
|
+
return JSON.stringify([...ids])
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
export function dropDismissed(
|
|
241
|
+
items: readonly MyWorkItem[],
|
|
242
|
+
dismissed: ReadonlySet<string>,
|
|
243
|
+
): MyWorkItem[] {
|
|
244
|
+
return items.map((i) => {
|
|
245
|
+
if (!("partId" in i)) return i
|
|
246
|
+
const byId = dismissed.has(i.partId)
|
|
247
|
+
const byReason = i.kind === QUESTION_KIND_ERROR
|
|
248
|
+
&& typeof i.reason === "string"
|
|
249
|
+
&& i.reason.toLowerCase().includes("dismissed this question")
|
|
250
|
+
if (!byId && !byReason) return i
|
|
251
|
+
return { ...i, kind: MY_WORK_GROUP_DISMISSED }
|
|
252
|
+
})
|
|
253
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* pware.oc.runtime.questions
|
|
3
|
+
*
|
|
4
|
+
* In-memory, per-session cache of open questions for the "My work" queue. A
|
|
5
|
+
* later wiring task feeds it from cheap host-event hints (targeted
|
|
6
|
+
* `listSessionQuestions` on the session a `message.part.updated` just touched)
|
|
7
|
+
* and a periodic `listOpenQuestions` backstop, replacing the expensive
|
|
8
|
+
* project-wide scan that used to run on every snapshot.
|
|
9
|
+
*
|
|
10
|
+
* `createQuestionCache()` returns a fresh instance — no module-level mutable
|
|
11
|
+
* singleton — so tests build their own and the wiring task owns the one real
|
|
12
|
+
* instance. State is `bySession`, a Map keyed by `sessionId`; `get` flattens
|
|
13
|
+
* and dedupes it. The two read functions below already soft-fail to `[]`, so
|
|
14
|
+
* this cache only holds/merges their results and needs no try/catch of its own.
|
|
15
|
+
*/
|
|
16
|
+
import {
|
|
17
|
+
listOpenQuestions,
|
|
18
|
+
listSessionQuestions,
|
|
19
|
+
type OpenQuestion,
|
|
20
|
+
} from "../pware.oc.opencode/resolver/pware.oc.opencode.resolver.question.js"
|
|
21
|
+
|
|
22
|
+
export type QuestionCache = {
|
|
23
|
+
/** Project-wide open questions, deduped by partId, sorted `startedAt` DESC (nulls last). */
|
|
24
|
+
get: () => OpenQuestion[]
|
|
25
|
+
/** Full `listOpenQuestions` scan; replaces the whole map (drop gone sessions). */
|
|
26
|
+
seed: (dbPath: string, projectId: string | null) => void
|
|
27
|
+
/** One session's `listSessionQuestions`; merges in, or removes on empty. */
|
|
28
|
+
touch: (dbPath: string, projectId: string | null, sessionId: string) => void
|
|
29
|
+
/** Full `listOpenQuestions` scan — semantic alias of `seed` for the backstop call site. */
|
|
30
|
+
reconcile: (dbPath: string, projectId: string | null) => void
|
|
31
|
+
/** Drop every cached session. */
|
|
32
|
+
reset: () => void
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Flatten all per-session slices, dedupe by `partId` (a question may briefly
|
|
37
|
+
* appear in two sessions' slices — keep the first), and sort by `startedAt`
|
|
38
|
+
* descending with nulls last. Pure — never mutates its input.
|
|
39
|
+
*/
|
|
40
|
+
export function mergeQuestions(
|
|
41
|
+
bySession: ReadonlyMap<string, readonly OpenQuestion[]>,
|
|
42
|
+
): OpenQuestion[] {
|
|
43
|
+
const seen = new Set<string>()
|
|
44
|
+
const out: OpenQuestion[] = []
|
|
45
|
+
for (const list of bySession.values()) {
|
|
46
|
+
for (const q of list) {
|
|
47
|
+
if (seen.has(q.partId)) continue
|
|
48
|
+
seen.add(q.partId)
|
|
49
|
+
out.push(q)
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
out.sort((a, b) => {
|
|
53
|
+
const x = a.startedAt
|
|
54
|
+
const y = b.startedAt
|
|
55
|
+
if (x === y) return 0
|
|
56
|
+
if (x == null) return 1
|
|
57
|
+
if (y == null) return -1
|
|
58
|
+
return y - x
|
|
59
|
+
})
|
|
60
|
+
return out
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function createQuestionCache(): QuestionCache {
|
|
64
|
+
let bySession = new Map<string, OpenQuestion[]>()
|
|
65
|
+
|
|
66
|
+
function refill(dbPath: string, projectId: string | null): void {
|
|
67
|
+
const next = new Map<string, OpenQuestion[]>()
|
|
68
|
+
for (const q of listOpenQuestions({ dbPath, projectId })) {
|
|
69
|
+
const bucket = next.get(q.sessionId)
|
|
70
|
+
if (bucket) bucket.push(q)
|
|
71
|
+
else next.set(q.sessionId, [q])
|
|
72
|
+
}
|
|
73
|
+
bySession = next
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return {
|
|
77
|
+
get() {
|
|
78
|
+
return mergeQuestions(bySession)
|
|
79
|
+
},
|
|
80
|
+
seed(dbPath, projectId) {
|
|
81
|
+
refill(dbPath, projectId)
|
|
82
|
+
},
|
|
83
|
+
touch(dbPath, projectId, sessionId) {
|
|
84
|
+
const result = listSessionQuestions({ dbPath, sessionId, projectId })
|
|
85
|
+
if (result.length > 0) bySession.set(sessionId, result)
|
|
86
|
+
else bySession.delete(sessionId)
|
|
87
|
+
},
|
|
88
|
+
reconcile(dbPath, projectId) {
|
|
89
|
+
refill(dbPath, projectId)
|
|
90
|
+
},
|
|
91
|
+
reset() {
|
|
92
|
+
bySession = new Map()
|
|
93
|
+
},
|
|
94
|
+
}
|
|
95
|
+
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* pware.oc.runtime.snapshotClient
|
|
3
|
+
*
|
|
4
|
+
* Async facade over the snapshot worker. `readRuntimeSnapshotAsync` posts a
|
|
5
|
+
* snapshot request and resolves with the composed snapshot; when the worker
|
|
6
|
+
* cannot be spawned, errors, or fails to answer within the timeout it falls
|
|
7
|
+
* back to the synchronous `readRuntimeSnapshot` — so the panel never hard
|
|
8
|
+
* depends on worker availability.
|
|
9
|
+
*/
|
|
10
|
+
import { dbg } from "../pware.oc.core/pware.oc.core.debug.js"
|
|
11
|
+
import { readRuntimeSnapshot, type RuntimeSnapshot } from "./resolver/index.js"
|
|
12
|
+
|
|
13
|
+
export type SnapshotRequestOpts = {
|
|
14
|
+
sessionId: string
|
|
15
|
+
projectRoot: string | null
|
|
16
|
+
dbPath?: string
|
|
17
|
+
questionHint?: string
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
type SnapshotDoneMessage = {
|
|
21
|
+
type: "snapshot:done"
|
|
22
|
+
id: number
|
|
23
|
+
ok: boolean
|
|
24
|
+
snap?: RuntimeSnapshot
|
|
25
|
+
error?: string
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Bound on a single worker round-trip before the sync path takes over. */
|
|
29
|
+
const WORKER_TIMEOUT_MS = 4_000
|
|
30
|
+
|
|
31
|
+
type PendingEntry = {
|
|
32
|
+
opts: SnapshotRequestOpts
|
|
33
|
+
resolve: (snap: RuntimeSnapshot) => void
|
|
34
|
+
timer: ReturnType<typeof setTimeout>
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
let worker: Worker | null = null
|
|
38
|
+
let workerFailed = false
|
|
39
|
+
let nextId = 0
|
|
40
|
+
const pending = new Map<number, PendingEntry>()
|
|
41
|
+
|
|
42
|
+
function syncFallback(opts: SnapshotRequestOpts): RuntimeSnapshot {
|
|
43
|
+
return readRuntimeSnapshot(opts)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function settle(id: number, snap: RuntimeSnapshot): void {
|
|
47
|
+
const entry = pending.get(id)
|
|
48
|
+
if (!entry) return
|
|
49
|
+
pending.delete(id)
|
|
50
|
+
clearTimeout(entry.timer)
|
|
51
|
+
entry.resolve(snap)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function failAll(reason: string): void {
|
|
55
|
+
dbg("snapshot.worker", "failed", { reason })
|
|
56
|
+
workerFailed = true
|
|
57
|
+
for (const id of [...pending.keys()]) {
|
|
58
|
+
const entry = pending.get(id)
|
|
59
|
+
if (entry) settle(id, syncFallback(entry.opts))
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function ensureWorker(): Worker | null {
|
|
64
|
+
if (workerFailed) return null
|
|
65
|
+
if (worker) return worker
|
|
66
|
+
try {
|
|
67
|
+
const w = new Worker(new URL("./pware.oc.runtime.worker.ts", import.meta.url), { type: "module" })
|
|
68
|
+
w.onmessage = (event: MessageEvent<SnapshotDoneMessage>) => {
|
|
69
|
+
const msg = event.data
|
|
70
|
+
if (!msg || msg.type !== "snapshot:done") return
|
|
71
|
+
const entry = pending.get(msg.id)
|
|
72
|
+
if (!entry) return
|
|
73
|
+
settle(msg.id, msg.ok && msg.snap ? msg.snap : syncFallback(entry.opts))
|
|
74
|
+
}
|
|
75
|
+
w.onerror = (event) => {
|
|
76
|
+
const message =
|
|
77
|
+
typeof event === "object" && event !== null && "message" in event
|
|
78
|
+
? String((event as { message: unknown }).message)
|
|
79
|
+
: String(event)
|
|
80
|
+
failAll(message)
|
|
81
|
+
try {
|
|
82
|
+
w.terminate()
|
|
83
|
+
} catch {
|
|
84
|
+
// ignore
|
|
85
|
+
}
|
|
86
|
+
worker = null
|
|
87
|
+
}
|
|
88
|
+
worker = w
|
|
89
|
+
dbg("snapshot.worker", "spawned", {})
|
|
90
|
+
return w
|
|
91
|
+
} catch (err) {
|
|
92
|
+
dbg("snapshot.worker", "spawn failed", { error: err instanceof Error ? err.message : String(err) })
|
|
93
|
+
workerFailed = true
|
|
94
|
+
return null
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** Off-main-thread snapshot read, falling back to the sync path on any failure. */
|
|
99
|
+
export function readRuntimeSnapshotAsync(opts: SnapshotRequestOpts): Promise<RuntimeSnapshot> {
|
|
100
|
+
const w = ensureWorker()
|
|
101
|
+
if (!w) return Promise.resolve(syncFallback(opts))
|
|
102
|
+
const id = ++nextId
|
|
103
|
+
return new Promise<RuntimeSnapshot>((resolve) => {
|
|
104
|
+
const timer = setTimeout(() => settle(id, syncFallback(opts)), WORKER_TIMEOUT_MS)
|
|
105
|
+
pending.set(id, { opts, resolve, timer })
|
|
106
|
+
try {
|
|
107
|
+
w.postMessage({ type: "snapshot", id, opts })
|
|
108
|
+
} catch (err) {
|
|
109
|
+
dbg("snapshot.worker", "post failed", { error: err instanceof Error ? err.message : String(err) })
|
|
110
|
+
settle(id, syncFallback(opts))
|
|
111
|
+
}
|
|
112
|
+
})
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** Terminate the shared worker (idempotent). Safe to call on plugin shutdown. */
|
|
116
|
+
export function shutdownSnapshotWorker(): void {
|
|
117
|
+
if (!worker) return
|
|
118
|
+
try {
|
|
119
|
+
worker.postMessage({ type: "shutdown" })
|
|
120
|
+
} catch {
|
|
121
|
+
// ignore
|
|
122
|
+
}
|
|
123
|
+
try {
|
|
124
|
+
worker.terminate()
|
|
125
|
+
} catch {
|
|
126
|
+
// ignore
|
|
127
|
+
}
|
|
128
|
+
worker = null
|
|
129
|
+
}
|