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,170 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* pware.oc.core.opencode.resolver
|
|
3
|
+
*
|
|
4
|
+
* Aggregate of the opencode SQLite resolvers: readDbSnapshot composes the
|
|
5
|
+
* session graph + tools + files for the panel; readProjectFeed rolls the
|
|
6
|
+
* Sessions-tab feed across a set of sessions. Re-exports the entity resolvers.
|
|
7
|
+
*/
|
|
8
|
+
import fs from "node:fs"
|
|
9
|
+
import type { FileFilter, FileView } from "../pware.oc.opencode.files.js"
|
|
10
|
+
import { profile } from "../../pware.oc.core/pware.oc.core.debug.js"
|
|
11
|
+
import { getOes } from "../../pware.oc.core/pware.oc.core.oes.js"
|
|
12
|
+
import { openReadonlyDb, withDbRead } from "../../pware.oc.core/pware.oc.core.sqlite.js"
|
|
13
|
+
import {
|
|
14
|
+
getSessionById,
|
|
15
|
+
getSessionsByIds,
|
|
16
|
+
listChildSessions,
|
|
17
|
+
listRecentMainSessions,
|
|
18
|
+
toSessionView,
|
|
19
|
+
type SessionView,
|
|
20
|
+
} from "./pware.oc.opencode.resolver.session.js"
|
|
21
|
+
import { listRecentToolEvents, listToolEvents, type ToolView } from "./pware.oc.opencode.resolver.tool.js"
|
|
22
|
+
import { listSessionFiles, listRecentSessionFiles } from "./pware.oc.opencode.resolver.file.js"
|
|
23
|
+
import type { TodoRow } from "./pware.oc.opencode.resolver.todo.js"
|
|
24
|
+
|
|
25
|
+
export * from "./pware.oc.opencode.resolver.session.js"
|
|
26
|
+
export * from "./pware.oc.opencode.resolver.todo.js"
|
|
27
|
+
export * from "./pware.oc.opencode.resolver.tool.js"
|
|
28
|
+
export * from "./pware.oc.opencode.resolver.file.js"
|
|
29
|
+
export * from "./pware.oc.opencode.resolver.question.js"
|
|
30
|
+
|
|
31
|
+
export type DbSnapshot = {
|
|
32
|
+
present: boolean
|
|
33
|
+
dbPath: string
|
|
34
|
+
/** Project of the current session — scopes project-wide reads (questions). */
|
|
35
|
+
projectId: string | null
|
|
36
|
+
current: SessionView | null
|
|
37
|
+
/** Orchestrator: parent if current is a child, otherwise current. */
|
|
38
|
+
main: SessionView | null
|
|
39
|
+
parent: SessionView | null
|
|
40
|
+
children: SessionView[]
|
|
41
|
+
siblings: SessionView[]
|
|
42
|
+
/** Sessions keyed by id — current, main, and any looked-up delegates. */
|
|
43
|
+
byId: Record<string, SessionView>
|
|
44
|
+
/** Recent main (parent_id null) sessions in this project — the `sessionFetch` window. */
|
|
45
|
+
recent: SessionView[]
|
|
46
|
+
todos: TodoRow[]
|
|
47
|
+
/** Current session tool parts — name + status only, no args/outputs. */
|
|
48
|
+
tools: ToolView[]
|
|
49
|
+
/** Basenames + optional +/- from edit/write parts. No paths, no bodies. */
|
|
50
|
+
files: FileView[]
|
|
51
|
+
error: string | null
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function emptyDb(dbPath: string, error: string | null = null): DbSnapshot {
|
|
55
|
+
return {
|
|
56
|
+
present: false,
|
|
57
|
+
dbPath,
|
|
58
|
+
projectId: null,
|
|
59
|
+
current: null,
|
|
60
|
+
main: null,
|
|
61
|
+
parent: null,
|
|
62
|
+
children: [],
|
|
63
|
+
siblings: [],
|
|
64
|
+
byId: {},
|
|
65
|
+
recent: [],
|
|
66
|
+
todos: [],
|
|
67
|
+
tools: [],
|
|
68
|
+
files: [],
|
|
69
|
+
error,
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function readDbSnapshot(opts: {
|
|
74
|
+
dbPath: string
|
|
75
|
+
sessionId: string
|
|
76
|
+
extraIds?: string[]
|
|
77
|
+
projectRoot?: string | null
|
|
78
|
+
}): DbSnapshot {
|
|
79
|
+
if (!opts.dbPath || !fs.existsSync(opts.dbPath)) {
|
|
80
|
+
return emptyDb(opts.dbPath, "db missing")
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const run = (): DbSnapshot => {
|
|
84
|
+
const db = openReadonlyDb(opts.dbPath)
|
|
85
|
+
if (!db) return emptyDb(opts.dbPath, "sqlite unavailable")
|
|
86
|
+
|
|
87
|
+
const now = Date.now()
|
|
88
|
+
const row = getSessionById(db, opts.sessionId)
|
|
89
|
+
if (!row) {
|
|
90
|
+
return { ...emptyDb(opts.dbPath, "session not in db yet"), present: true }
|
|
91
|
+
}
|
|
92
|
+
const current = toSessionView(row, now)
|
|
93
|
+
let parent: SessionView | null = null
|
|
94
|
+
const children = listChildSessions(db, row.id).map((r) => toSessionView(r, now))
|
|
95
|
+
if (row.parent_id) {
|
|
96
|
+
const p = getSessionById(db, row.parent_id)
|
|
97
|
+
if (p) parent = toSessionView(p, now)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const main = parent ?? current
|
|
101
|
+
const extra = getSessionsByIds(db, opts.extraIds ?? []).map((r) =>
|
|
102
|
+
toSessionView(r, now),
|
|
103
|
+
)
|
|
104
|
+
const byId: Record<string, SessionView> = {}
|
|
105
|
+
const oes = getOes(opts.projectRoot)
|
|
106
|
+
const recent = listRecentMainSessions(db, {
|
|
107
|
+
projectId: row.project_id,
|
|
108
|
+
limit: oes.sessionFetch,
|
|
109
|
+
}).map((r) => toSessionView(r, now))
|
|
110
|
+
for (const v of [current, parent, main, ...children, ...extra, ...recent]) {
|
|
111
|
+
if (v) byId[v.id] = v
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return {
|
|
115
|
+
present: true,
|
|
116
|
+
dbPath: opts.dbPath,
|
|
117
|
+
projectId: row.project_id,
|
|
118
|
+
current,
|
|
119
|
+
main,
|
|
120
|
+
parent,
|
|
121
|
+
children,
|
|
122
|
+
siblings: [],
|
|
123
|
+
byId,
|
|
124
|
+
recent,
|
|
125
|
+
todos: [],
|
|
126
|
+
tools: listToolEvents(db, row.id, oes.toolFetch),
|
|
127
|
+
files: listSessionFiles(db, row.id, {
|
|
128
|
+
skipGitignore: oes.skipGitignore,
|
|
129
|
+
projectRoot: opts.projectRoot,
|
|
130
|
+
}),
|
|
131
|
+
error: null,
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return profile("db.snapshot", () =>
|
|
136
|
+
withDbRead(run, (e) =>
|
|
137
|
+
emptyDb(opts.dbPath, e instanceof Error ? e.message : "db read failed"),
|
|
138
|
+
),
|
|
139
|
+
)
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export type ProjectFeed = {
|
|
143
|
+
tools: ToolView[]
|
|
144
|
+
files: FileView[]
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export function emptyProjectFeed(): ProjectFeed {
|
|
148
|
+
return { tools: [], files: [] }
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** Lazy read — callers gate on the Sessions tab so the queries never run elsewhere. */
|
|
152
|
+
export function readProjectFeed(opts: {
|
|
153
|
+
dbPath: string
|
|
154
|
+
sessionIds: string[]
|
|
155
|
+
toolLimit: number
|
|
156
|
+
filter?: FileFilter
|
|
157
|
+
}): ProjectFeed {
|
|
158
|
+
const empty = emptyProjectFeed()
|
|
159
|
+
if (!opts.dbPath || opts.sessionIds.length === 0 || !fs.existsSync(opts.dbPath)) return empty
|
|
160
|
+
return profile("db.feed", () =>
|
|
161
|
+
withDbRead(() => {
|
|
162
|
+
const db = openReadonlyDb(opts.dbPath)
|
|
163
|
+
if (!db) return empty
|
|
164
|
+
return {
|
|
165
|
+
tools: listRecentToolEvents(db, opts.sessionIds, opts.toolLimit),
|
|
166
|
+
files: listRecentSessionFiles(db, opts.sessionIds, opts.filter),
|
|
167
|
+
}
|
|
168
|
+
}, () => empty),
|
|
169
|
+
)
|
|
170
|
+
}
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* pware.oc.core.opencode.resolver.file
|
|
3
|
+
*
|
|
4
|
+
* File-touch parts → FileView (basename + optional +/- diff stats). Patch
|
|
5
|
+
* parts and edit/write/read tool parts count; never bodies or paths in the
|
|
6
|
+
* panel. Feeds the Current and Sessions tabs.
|
|
7
|
+
*/
|
|
8
|
+
import { fileHitFromExtracted, filesFromPatchJson, type FileFilter, type FileView } from "../pware.oc.opencode.files.js"
|
|
9
|
+
import { str } from "../../pware.oc.core/pware.oc.core.paths.js"
|
|
10
|
+
import { toEpochMs } from "../../pware.oc.core/pware.oc.core.pulse.js"
|
|
11
|
+
import { uniqueIds, type SqlDb } from "../../pware.oc.core/pware.oc.core.sqlite.js"
|
|
12
|
+
import { PART_TYPE_PATCH } from "../../pware.oc.core/constants/pware.oc.core.constants.partType.js"
|
|
13
|
+
|
|
14
|
+
const FILE_SCAN = 80
|
|
15
|
+
|
|
16
|
+
type SessionFileRow = {
|
|
17
|
+
time_updated: number
|
|
18
|
+
time_created: number
|
|
19
|
+
kind: string | null
|
|
20
|
+
tool: string | null
|
|
21
|
+
filePath: string | null
|
|
22
|
+
filePath2: string | null
|
|
23
|
+
filePath3: string | null
|
|
24
|
+
addMeta: number | null
|
|
25
|
+
addMeta2: number | null
|
|
26
|
+
addTop: number | null
|
|
27
|
+
delMeta: number | null
|
|
28
|
+
delMeta2: number | null
|
|
29
|
+
delTop: number | null
|
|
30
|
+
files: string | null
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Shared file-part column list — all columns from `part.data` via json_extract. */
|
|
34
|
+
function fileColumns(): string {
|
|
35
|
+
return [
|
|
36
|
+
`time_created`,
|
|
37
|
+
`time_updated`,
|
|
38
|
+
`json_extract(data,'$.type') AS kind`,
|
|
39
|
+
`json_extract(data,'$.tool') AS tool`,
|
|
40
|
+
`json_extract(data,'$.state.input.filePath') AS filePath`,
|
|
41
|
+
`json_extract(data,'$.input.filePath') AS filePath2`,
|
|
42
|
+
`json_extract(data,'$.filePath') AS filePath3`,
|
|
43
|
+
`json_extract(data,'$.state.metadata.additions') AS addMeta`,
|
|
44
|
+
`json_extract(data,'$.metadata.additions') AS addMeta2`,
|
|
45
|
+
`json_extract(data,'$.additions') AS addTop`,
|
|
46
|
+
`json_extract(data,'$.state.metadata.deletions') AS delMeta`,
|
|
47
|
+
`json_extract(data,'$.metadata.deletions') AS delMeta2`,
|
|
48
|
+
`json_extract(data,'$.deletions') AS delTop`,
|
|
49
|
+
`json_extract(data,'$.files') AS files`,
|
|
50
|
+
].join(",\n ")
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Patch parts + edit/write/read tool parts — what counts as a file touch. */
|
|
54
|
+
function fileWhere(): string {
|
|
55
|
+
return `(
|
|
56
|
+
json_extract(data,'$.type') = 'patch'
|
|
57
|
+
OR (
|
|
58
|
+
json_extract(data,'$.type') = 'tool'
|
|
59
|
+
AND json_extract(data,'$.tool') IN ('edit','write','multiedit','read','delete','apply_edit','applyedit','remove','view','read_file','readfile')
|
|
60
|
+
)
|
|
61
|
+
)`
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Patch `files[]` + edit metadata +/- , aggregated by path. No bodies. */
|
|
65
|
+
function fileViewsFromRows(rows: readonly SessionFileRow[], filter?: FileFilter): FileView[] {
|
|
66
|
+
const byId = new Map<string, FileView>()
|
|
67
|
+
const add = (f: FileView) => {
|
|
68
|
+
const prev = byId.get(f.id)
|
|
69
|
+
byId.set(f.id, {
|
|
70
|
+
id: f.id,
|
|
71
|
+
name: f.name,
|
|
72
|
+
additions: (prev?.additions ?? 0) + f.additions,
|
|
73
|
+
deletions: (prev?.deletions ?? 0) + f.deletions,
|
|
74
|
+
at: Math.max(f.at, prev?.at ?? 0),
|
|
75
|
+
touch: prev?.touch === "write" || f.touch === "write" ? "write" : "read",
|
|
76
|
+
letter: f.letter ?? prev?.letter ?? null,
|
|
77
|
+
})
|
|
78
|
+
}
|
|
79
|
+
for (const row of rows) {
|
|
80
|
+
const at = toEpochMs(row.time_updated) ?? toEpochMs(row.time_created) ?? 0
|
|
81
|
+
if (str(row.kind) === PART_TYPE_PATCH) {
|
|
82
|
+
for (const f of filesFromPatchJson(row.files, at, filter)) add(f)
|
|
83
|
+
continue
|
|
84
|
+
}
|
|
85
|
+
const hit = fileHitFromExtracted({
|
|
86
|
+
tool: str(row.tool),
|
|
87
|
+
filePath: str(row.filePath) || str(row.filePath2) || str(row.filePath3),
|
|
88
|
+
additions: row.addMeta ?? row.addMeta2 ?? row.addTop,
|
|
89
|
+
deletions: row.delMeta ?? row.delMeta2 ?? row.delTop,
|
|
90
|
+
at,
|
|
91
|
+
filter,
|
|
92
|
+
})
|
|
93
|
+
if (hit) add(hit)
|
|
94
|
+
}
|
|
95
|
+
return [...byId.values()].sort((a, b) => b.at - a.at)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function listFileRows(db: SqlDb, where: string, params: string[]): SessionFileRow[] {
|
|
99
|
+
const candidateRows = db.all<{ id: string }>(
|
|
100
|
+
`SELECT id
|
|
101
|
+
FROM part
|
|
102
|
+
WHERE ${where}
|
|
103
|
+
ORDER BY time_updated DESC
|
|
104
|
+
LIMIT ${FILE_SCAN}`,
|
|
105
|
+
...params,
|
|
106
|
+
)
|
|
107
|
+
const ids = candidateRows.map((r) => String(r.id))
|
|
108
|
+
if (ids.length === 0) return []
|
|
109
|
+
const saturated = ids.length === FILE_SCAN
|
|
110
|
+
|
|
111
|
+
const placeholders = ids.map(() => "?").join(",")
|
|
112
|
+
const rows = db.all<SessionFileRow>(
|
|
113
|
+
`SELECT ${fileColumns()}
|
|
114
|
+
FROM part
|
|
115
|
+
WHERE id IN (${placeholders})
|
|
116
|
+
AND ${fileWhere()}
|
|
117
|
+
ORDER BY time_updated DESC`,
|
|
118
|
+
...ids,
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
if (saturated && rows.length < FILE_SCAN) {
|
|
122
|
+
return db.all<SessionFileRow>(
|
|
123
|
+
`SELECT ${fileColumns()}
|
|
124
|
+
FROM part
|
|
125
|
+
WHERE ${where}
|
|
126
|
+
AND ${fileWhere()}
|
|
127
|
+
ORDER BY time_updated DESC
|
|
128
|
+
LIMIT ${FILE_SCAN}`,
|
|
129
|
+
...params,
|
|
130
|
+
)
|
|
131
|
+
}
|
|
132
|
+
return rows
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** Patch `files[]` + edit metadata +/- for one session. No bodies. */
|
|
136
|
+
export function listSessionFiles(db: SqlDb, sessionId: string, filter?: FileFilter): FileView[] {
|
|
137
|
+
let rows: SessionFileRow[] = []
|
|
138
|
+
try {
|
|
139
|
+
rows = listFileRows(db, "session_id = ?", [sessionId])
|
|
140
|
+
} catch {
|
|
141
|
+
return []
|
|
142
|
+
}
|
|
143
|
+
return fileViewsFromRows(rows, filter)
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** Files touched by any of the given sessions, aggregated — the Sessions tab feed. */
|
|
147
|
+
export function listRecentSessionFiles(
|
|
148
|
+
db: SqlDb,
|
|
149
|
+
sessionIds: string[],
|
|
150
|
+
filter?: FileFilter,
|
|
151
|
+
): FileView[] {
|
|
152
|
+
const clean = uniqueIds(sessionIds)
|
|
153
|
+
if (clean.length === 0) return []
|
|
154
|
+
const placeholders = clean.map(() => "?").join(",")
|
|
155
|
+
let rows: SessionFileRow[] = []
|
|
156
|
+
try {
|
|
157
|
+
rows = listFileRows(db, `session_id IN (${placeholders})`, clean)
|
|
158
|
+
} catch {
|
|
159
|
+
return []
|
|
160
|
+
}
|
|
161
|
+
return fileViewsFromRows(rows, filter)
|
|
162
|
+
}
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* pware.oc.core.opencode.resolver.question
|
|
3
|
+
*
|
|
4
|
+
* Open `question` tool parts across a project — the "waiting on my answer"
|
|
5
|
+
* queue. A question is open while its state has no end time and its status is
|
|
6
|
+
* running or pending, or while it is a still-unanswered error/interrupted
|
|
7
|
+
* part. Scanned **project-wide** (every non-archived session of the project),
|
|
8
|
+
* not just the current+recent window, so an open question keeps showing after
|
|
9
|
+
* a restart or a session switch. Lazy read, gated on the My work tab;
|
|
10
|
+
* soft-fails to [] on a missing/locked DB. The question text and options stay
|
|
11
|
+
* in the `part.data` blob — never read, never shown.
|
|
12
|
+
*/
|
|
13
|
+
import fs from "node:fs"
|
|
14
|
+
import { str } from "../../pware.oc.core/pware.oc.core.paths.js"
|
|
15
|
+
import { toEpochMs } from "../../pware.oc.core/pware.oc.core.pulse.js"
|
|
16
|
+
import { openReadonlyDb, withDbRead } from "../../pware.oc.core/pware.oc.core.sqlite.js"
|
|
17
|
+
import { toToolStatus } from "../../pware.oc.core/pware.oc.core.status.js"
|
|
18
|
+
import { PART_TYPE_TOOL } from "../../pware.oc.core/constants/pware.oc.core.constants.partType.js"
|
|
19
|
+
import {
|
|
20
|
+
TOOL_STATUS_COMPLETED,
|
|
21
|
+
TOOL_STATUS_ERROR,
|
|
22
|
+
TOOL_STATUS_PENDING,
|
|
23
|
+
TOOL_STATUS_RUNNING,
|
|
24
|
+
} from "../../pware.oc.core/constants/pware.oc.core.constants.status.js"
|
|
25
|
+
import { TOOL_QUESTION } from "../../pware.oc.core/constants/pware.oc.core.constants.toolName.js"
|
|
26
|
+
import {
|
|
27
|
+
QUESTION_KIND_ERROR,
|
|
28
|
+
QUESTION_KIND_INTERRUPTED,
|
|
29
|
+
QUESTION_KIND_QUESTION,
|
|
30
|
+
type OpenQuestionKind,
|
|
31
|
+
} from "../constants/pware.oc.opencode.constants.questionKind.js"
|
|
32
|
+
|
|
33
|
+
export type { OpenQuestionKind }
|
|
34
|
+
|
|
35
|
+
export type OpenQuestion = {
|
|
36
|
+
partId: string
|
|
37
|
+
sessionId: string
|
|
38
|
+
/** The session's own title — the question's session may sit outside the recent window. */
|
|
39
|
+
title: string
|
|
40
|
+
startedAt: number | null
|
|
41
|
+
kind: OpenQuestionKind
|
|
42
|
+
/** `state.error` text for interrupted/error parts; null for an open question. */
|
|
43
|
+
reason: string | null
|
|
44
|
+
/**
|
|
45
|
+
* True when the part has a terminal end time. An ended plain error is kept in
|
|
46
|
+
* the queue for the Errors group, but it is no longer "live" — the tab light
|
|
47
|
+
* must not treat it as something happening right now. Absent = still live.
|
|
48
|
+
*/
|
|
49
|
+
ended?: boolean
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Raw SELECT row for an open-question scan. Metadata only — never part.data text. */
|
|
53
|
+
export type OpenQuestionRow = {
|
|
54
|
+
id: string
|
|
55
|
+
session_id: string
|
|
56
|
+
title: string | null
|
|
57
|
+
time_created: number
|
|
58
|
+
status: string | null
|
|
59
|
+
tstart: number | null
|
|
60
|
+
tend: number | null
|
|
61
|
+
error: string | null
|
|
62
|
+
interrupted: number | null
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Classify one SELECT row into an open question, or null when the part is not
|
|
67
|
+
* part of the queue (resolved / completed / a still-terminated interrupt).
|
|
68
|
+
* Pure — the same row always maps to the same answer.
|
|
69
|
+
*/
|
|
70
|
+
export function classifyQuestionRow(row: OpenQuestionRow): OpenQuestion | null {
|
|
71
|
+
const start = toEpochMs(row.tstart)
|
|
72
|
+
const end = toEpochMs(row.tend)
|
|
73
|
+
const status = toToolStatus(
|
|
74
|
+
str(row.status) || (end != null ? TOOL_STATUS_COMPLETED : start != null ? TOOL_STATUS_RUNNING : null),
|
|
75
|
+
)
|
|
76
|
+
if (status !== TOOL_STATUS_RUNNING && status !== TOOL_STATUS_PENDING && status !== TOOL_STATUS_ERROR) return null
|
|
77
|
+
const interrupted = row.interrupted === 1
|
|
78
|
+
// An interrupted question that terminated (has an end time) is resolved:
|
|
79
|
+
// the abort closed the tool and no answer is pending. Only a still-open
|
|
80
|
+
// interrupted part (no end time) stays "your call".
|
|
81
|
+
if (interrupted && end != null) return null
|
|
82
|
+
const reason = str(row.error) || null
|
|
83
|
+
const kind: OpenQuestionKind =
|
|
84
|
+
status === TOOL_STATUS_ERROR
|
|
85
|
+
? interrupted
|
|
86
|
+
? QUESTION_KIND_INTERRUPTED
|
|
87
|
+
: QUESTION_KIND_ERROR
|
|
88
|
+
: QUESTION_KIND_QUESTION
|
|
89
|
+
return {
|
|
90
|
+
partId: row.id,
|
|
91
|
+
sessionId: row.session_id,
|
|
92
|
+
title: str(row.title) || "untitled",
|
|
93
|
+
startedAt: start ?? toEpochMs(row.time_created),
|
|
94
|
+
kind,
|
|
95
|
+
reason: kind === QUESTION_KIND_QUESTION ? null : reason,
|
|
96
|
+
ended: end != null,
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function listOpenQuestions(opts: {
|
|
101
|
+
dbPath: string
|
|
102
|
+
projectId: string | null
|
|
103
|
+
}): OpenQuestion[] {
|
|
104
|
+
if (!opts.dbPath || !opts.projectId || !fs.existsSync(opts.dbPath)) return []
|
|
105
|
+
return withDbRead(() => {
|
|
106
|
+
const db = openReadonlyDb(opts.dbPath)
|
|
107
|
+
if (!db) return []
|
|
108
|
+
let rows: OpenQuestionRow[] = []
|
|
109
|
+
try {
|
|
110
|
+
rows = db.all<OpenQuestionRow>(
|
|
111
|
+
`SELECT p.id AS id,
|
|
112
|
+
p.session_id,
|
|
113
|
+
s.title AS title,
|
|
114
|
+
p.time_created,
|
|
115
|
+
json_extract(p.data,'$.state.status') AS status,
|
|
116
|
+
json_extract(p.data,'$.state.time.start') AS tstart,
|
|
117
|
+
json_extract(p.data,'$.state.time.end') AS tend,
|
|
118
|
+
json_extract(p.data,'$.state.error') AS error,
|
|
119
|
+
json_extract(p.data,'$.state.metadata.interrupted') AS interrupted
|
|
120
|
+
FROM part p
|
|
121
|
+
JOIN session s ON s.id = p.session_id
|
|
122
|
+
WHERE json_extract(p.data,'$.type') = '${PART_TYPE_TOOL}'
|
|
123
|
+
AND json_extract(p.data,'$.tool') = '${TOOL_QUESTION}'
|
|
124
|
+
AND (s.time_archived IS NULL OR s.time_archived = 0)
|
|
125
|
+
AND s.project_id = ?
|
|
126
|
+
ORDER BY p.time_created DESC
|
|
127
|
+
LIMIT 80`,
|
|
128
|
+
opts.projectId,
|
|
129
|
+
)
|
|
130
|
+
} catch {
|
|
131
|
+
return []
|
|
132
|
+
}
|
|
133
|
+
return rows
|
|
134
|
+
.map(classifyQuestionRow)
|
|
135
|
+
.filter((q): q is OpenQuestion => q != null)
|
|
136
|
+
}, () => [])
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Session-scoped open questions. Same SELECT columns and filters as
|
|
141
|
+
* `listOpenQuestions`, but `WHERE p.session_id = ?` rides `part_session_idx`
|
|
142
|
+
* for a cheap targeted re-read. Foundation for later event-driven invalidation
|
|
143
|
+
* of the "My work" queue.
|
|
144
|
+
*/
|
|
145
|
+
export function listSessionQuestions(opts: {
|
|
146
|
+
dbPath: string
|
|
147
|
+
sessionId: string
|
|
148
|
+
projectId: string | null
|
|
149
|
+
}): OpenQuestion[] {
|
|
150
|
+
if (!opts.dbPath || !opts.sessionId || !opts.projectId || !fs.existsSync(opts.dbPath)) return []
|
|
151
|
+
return withDbRead(() => {
|
|
152
|
+
const db = openReadonlyDb(opts.dbPath)
|
|
153
|
+
if (!db) return []
|
|
154
|
+
let rows: OpenQuestionRow[] = []
|
|
155
|
+
try {
|
|
156
|
+
rows = db.all<OpenQuestionRow>(
|
|
157
|
+
`SELECT p.id AS id,
|
|
158
|
+
p.session_id,
|
|
159
|
+
s.title AS title,
|
|
160
|
+
p.time_created,
|
|
161
|
+
json_extract(p.data,'$.state.status') AS status,
|
|
162
|
+
json_extract(p.data,'$.state.time.start') AS tstart,
|
|
163
|
+
json_extract(p.data,'$.state.time.end') AS tend,
|
|
164
|
+
json_extract(p.data,'$.state.error') AS error,
|
|
165
|
+
json_extract(p.data,'$.state.metadata.interrupted') AS interrupted
|
|
166
|
+
FROM part p
|
|
167
|
+
JOIN session s ON s.id = p.session_id
|
|
168
|
+
WHERE p.session_id = ?
|
|
169
|
+
AND json_extract(p.data,'$.type') = '${PART_TYPE_TOOL}'
|
|
170
|
+
AND json_extract(p.data,'$.tool') = '${TOOL_QUESTION}'
|
|
171
|
+
AND (s.time_archived IS NULL OR s.time_archived = 0)
|
|
172
|
+
AND s.project_id = ?
|
|
173
|
+
ORDER BY p.time_created DESC
|
|
174
|
+
LIMIT 20`,
|
|
175
|
+
opts.sessionId,
|
|
176
|
+
opts.projectId,
|
|
177
|
+
)
|
|
178
|
+
} catch {
|
|
179
|
+
return []
|
|
180
|
+
}
|
|
181
|
+
return rows
|
|
182
|
+
.map(classifyQuestionRow)
|
|
183
|
+
.filter((q): q is OpenQuestion => q != null)
|
|
184
|
+
}, () => [])
|
|
185
|
+
}
|