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.
Files changed (105) hide show
  1. package/.oesignore +17 -0
  2. package/CHANGELOG.md +95 -0
  3. package/LICENSE +21 -0
  4. package/README.md +235 -0
  5. package/oes.json +11 -0
  6. package/package.json +100 -0
  7. package/scripts/install-git-hooks.mjs +29 -0
  8. package/src/pware.oc.core/constants/index.ts +18 -0
  9. package/src/pware.oc.core/constants/pware.oc.core.constants.eventKind.ts +29 -0
  10. package/src/pware.oc.core/constants/pware.oc.core.constants.eventName.ts +13 -0
  11. package/src/pware.oc.core/constants/pware.oc.core.constants.eventType.ts +241 -0
  12. package/src/pware.oc.core/constants/pware.oc.core.constants.myWork.ts +55 -0
  13. package/src/pware.oc.core/constants/pware.oc.core.constants.partType.ts +65 -0
  14. package/src/pware.oc.core/constants/pware.oc.core.constants.phase.ts +58 -0
  15. package/src/pware.oc.core/constants/pware.oc.core.constants.pulse.ts +63 -0
  16. package/src/pware.oc.core/constants/pware.oc.core.constants.rowKind.ts +34 -0
  17. package/src/pware.oc.core/constants/pware.oc.core.constants.status.ts +74 -0
  18. package/src/pware.oc.core/constants/pware.oc.core.constants.toolName.ts +69 -0
  19. package/src/pware.oc.core/git/index.ts +7 -0
  20. package/src/pware.oc.core/git/pware.oc.core.git.ts +237 -0
  21. package/src/pware.oc.core/git/pware.oc.core.gitignore.ts +87 -0
  22. package/src/pware.oc.core/index.ts +18 -0
  23. package/src/pware.oc.core/pware.oc.core.bus.ts +42 -0
  24. package/src/pware.oc.core/pware.oc.core.cache.ts +48 -0
  25. package/src/pware.oc.core/pware.oc.core.clipboard.ts +97 -0
  26. package/src/pware.oc.core/pware.oc.core.debug.ts +280 -0
  27. package/src/pware.oc.core/pware.oc.core.events.ts +79 -0
  28. package/src/pware.oc.core/pware.oc.core.glyph.ts +135 -0
  29. package/src/pware.oc.core/pware.oc.core.layout.ts +234 -0
  30. package/src/pware.oc.core/pware.oc.core.oes.ts +89 -0
  31. package/src/pware.oc.core/pware.oc.core.paths.ts +240 -0
  32. package/src/pware.oc.core/pware.oc.core.preview.ts +74 -0
  33. package/src/pware.oc.core/pware.oc.core.pulse.ts +726 -0
  34. package/src/pware.oc.core/pware.oc.core.sqlite.ts +175 -0
  35. package/src/pware.oc.core/pware.oc.core.status.ts +214 -0
  36. package/src/pware.oc.core/pware.oc.core.timing.ts +63 -0
  37. package/src/pware.oc.core/pware.oc.core.width.ts +215 -0
  38. package/src/pware.oc.omo/constants/index.ts +15 -0
  39. package/src/pware.oc.omo/constants/pware.oc.omo.constants.backgroundTask.ts +18 -0
  40. package/src/pware.oc.omo/constants/pware.oc.omo.constants.boulderStatus.ts +49 -0
  41. package/src/pware.oc.omo/constants/pware.oc.omo.constants.docKind.ts +29 -0
  42. package/src/pware.oc.omo/constants/pware.oc.omo.constants.eventName.ts +11 -0
  43. package/src/pware.oc.omo/constants/pware.oc.omo.constants.planStatus.ts +60 -0
  44. package/src/pware.oc.omo/constants/pware.oc.omo.constants.reviewStatus.ts +49 -0
  45. package/src/pware.oc.omo/constants/pware.oc.omo.constants.startWork.ts +25 -0
  46. package/src/pware.oc.omo/constants/pware.oc.omo.constants.verdict.ts +38 -0
  47. package/src/pware.oc.omo/index.ts +7 -0
  48. package/src/pware.oc.omo/resolver/index.ts +19 -0
  49. package/src/pware.oc.omo/resolver/pware.oc.omo.resolver.approval.ts +179 -0
  50. package/src/pware.oc.omo/resolver/pware.oc.omo.resolver.approvalGroup.ts +116 -0
  51. package/src/pware.oc.omo/resolver/pware.oc.omo.resolver.approvalState.ts +34 -0
  52. package/src/pware.oc.omo/resolver/pware.oc.omo.resolver.boulder.ts +503 -0
  53. package/src/pware.oc.omo/resolver/pware.oc.omo.resolver.config.ts +40 -0
  54. package/src/pware.oc.omo/resolver/pware.oc.omo.resolver.doc.ts +269 -0
  55. package/src/pware.oc.omo/resolver/pware.oc.omo.resolver.draftFile.ts +18 -0
  56. package/src/pware.oc.omo/resolver/pware.oc.omo.resolver.notepadsFile.ts +18 -0
  57. package/src/pware.oc.omo/resolver/pware.oc.omo.resolver.plan.ts +155 -0
  58. package/src/pware.oc.omo/resolver/pware.oc.omo.resolver.planFile.ts +361 -0
  59. package/src/pware.oc.omo/resolver/pware.oc.omo.resolver.proofFile.ts +18 -0
  60. package/src/pware.oc.omo/resolver/pware.oc.omo.resolver.rulesFile.ts +11 -0
  61. package/src/pware.oc.omo/resolver/pware.oc.omo.resolver.runContinuationFile.ts +14 -0
  62. package/src/pware.oc.opencode/constants/index.ts +10 -0
  63. package/src/pware.oc.opencode/constants/pware.oc.opencode.constants.eventName.ts +15 -0
  64. package/src/pware.oc.opencode/constants/pware.oc.opencode.constants.fileTouch.ts +18 -0
  65. package/src/pware.oc.opencode/constants/pware.oc.opencode.constants.questionKind.ts +45 -0
  66. package/src/pware.oc.opencode/constants/pware.oc.opencode.constants.sessionStatus.ts +58 -0
  67. package/src/pware.oc.opencode/index.ts +8 -0
  68. package/src/pware.oc.opencode/pware.oc.opencode.events.ts +182 -0
  69. package/src/pware.oc.opencode/pware.oc.opencode.files.ts +399 -0
  70. package/src/pware.oc.opencode/resolver/index.ts +170 -0
  71. package/src/pware.oc.opencode/resolver/pware.oc.opencode.resolver.file.ts +162 -0
  72. package/src/pware.oc.opencode/resolver/pware.oc.opencode.resolver.question.ts +185 -0
  73. package/src/pware.oc.opencode/resolver/pware.oc.opencode.resolver.session.ts +211 -0
  74. package/src/pware.oc.opencode/resolver/pware.oc.opencode.resolver.todo.ts +26 -0
  75. package/src/pware.oc.opencode/resolver/pware.oc.opencode.resolver.tool.ts +236 -0
  76. package/src/pware.oc.perf/index.ts +14 -0
  77. package/src/pware.oc.perf/pware.oc.perf.asciichart.d.ts +15 -0
  78. package/src/pware.oc.perf/pware.oc.perf.charts.ts +284 -0
  79. package/src/pware.oc.perf/pware.oc.perf.reader.ts +927 -0
  80. package/src/pware.oc.perf/pware.oc.perf.realtime.ts +88 -0
  81. package/src/pware.oc.perf/pware.oc.perf.realtimeBlock.ts +118 -0
  82. package/src/pware.oc.perf/pware.oc.perf.realtimeCpuRam.ts +79 -0
  83. package/src/pware.oc.perf/pware.oc.perf.realtimeSampler.ts +71 -0
  84. package/src/pware.oc.perf/pware.oc.perf.realtimeTimeline.ts +214 -0
  85. package/src/pware.oc.perf/pware.oc.perf.self.ts +179 -0
  86. package/src/pware.oc.perf/pware.oc.perf.view.tsx +462 -0
  87. package/src/pware.oc.runtime/index.ts +13 -0
  88. package/src/pware.oc.runtime/pware.oc.runtime.monitor.ts +142 -0
  89. package/src/pware.oc.runtime/pware.oc.runtime.mywork-enrich.ts +79 -0
  90. package/src/pware.oc.runtime/pware.oc.runtime.mywork.ts +253 -0
  91. package/src/pware.oc.runtime/pware.oc.runtime.questions.ts +95 -0
  92. package/src/pware.oc.runtime/pware.oc.runtime.snapshotClient.ts +129 -0
  93. package/src/pware.oc.runtime/pware.oc.runtime.source.ts +105 -0
  94. package/src/pware.oc.runtime/pware.oc.runtime.worker.ts +43 -0
  95. package/src/pware.oc.runtime/resolver/index.ts +224 -0
  96. package/src/pware.oc.runtime/resolver/pware.oc.runtime.resolver.delegate.ts +146 -0
  97. package/src/pware.oc.ui/index.ts +12 -0
  98. package/src/pware.oc.ui/pware.oc.ui.chrome.tsx +339 -0
  99. package/src/pware.oc.ui/pware.oc.ui.glyphs.tsx +144 -0
  100. package/src/pware.oc.ui/pware.oc.ui.host.tsx +111 -0
  101. package/src/pware.oc.ui/pware.oc.ui.live.tsx +82 -0
  102. package/src/pware.oc.ui/pware.oc.ui.menudialogs.tsx +843 -0
  103. package/src/pware.oc.ui/pware.oc.ui.sections.tsx +526 -0
  104. package/src/pware.oc.ui/pware.oc.ui.sidebar.tsx +1629 -0
  105. 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
+ }