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,175 @@
1
+ /**
2
+ * Thin readonly SQLite handle: prefers bun:sqlite, falls back to node:sqlite.
3
+ * Never writes. Returns null when neither backend / DB is available.
4
+ */
5
+ import { createRequire } from "node:module"
6
+ import fs from "node:fs"
7
+ import { dbg, profile, pushScreenLine } from "./pware.oc.core.debug.js"
8
+
9
+ export type SqlRow = Record<string, unknown>
10
+
11
+ export type SqlDb = {
12
+ all: <T extends SqlRow = SqlRow>(sql: string, ...params: unknown[]) => T[]
13
+ get: <T extends SqlRow = SqlRow>(sql: string, ...params: unknown[]) => T | null
14
+ close: () => void
15
+ }
16
+
17
+ const require = createRequire(import.meta.url)
18
+
19
+ /**
20
+ * Lock wait before a query fails with SQLITE_BUSY. Fail-fast: a transient WAL
21
+ * checkpoint lock should surface as an error row (and a `sql.busy` debug line)
22
+ * instead of blocking the whole TUI for seconds.
23
+ */
24
+ const BUSY_TIMEOUT_MS = 100
25
+
26
+ /** One-line query label for the profile log; computed lazily (profiling off = free). */
27
+ function sqlLabel(sql: string): string {
28
+ return sql.replace(/\s+/g, " ").trim().slice(0, 80)
29
+ }
30
+
31
+ /** True when a thrown error is a SQLite lock/busy failure (SQLITE_BUSY). */
32
+ export function isBusyError(e: unknown): boolean {
33
+ return e instanceof Error ? /locked|busy/i.test(e.message) : false
34
+ }
35
+
36
+ /**
37
+ * Run a query, logging a `sql.busy` debug line when the DB is locked. The
38
+ * error still propagates so `withDbRead` can retry once and then soft-fail.
39
+ */
40
+ function guardQuery<T>(sql: string, fn: () => T): T {
41
+ try {
42
+ return fn()
43
+ } catch (e) {
44
+ if (isBusyError(e)) dbg("sql.busy", "db locked", { q: sqlLabel(sql) })
45
+ throw e
46
+ }
47
+ }
48
+
49
+ function wrapBun(db: {
50
+ query: (sql: string) => { all: (...p: unknown[]) => SqlRow[]; get: (...p: unknown[]) => SqlRow | null }
51
+ exec: (sql: string) => void
52
+ close: () => void
53
+ }): SqlDb {
54
+ try {
55
+ db.exec("PRAGMA query_only = ON")
56
+ db.exec(`PRAGMA busy_timeout = ${BUSY_TIMEOUT_MS}`)
57
+ } catch {
58
+ // older / restricted
59
+ }
60
+ return {
61
+ all: <T extends SqlRow>(sql: string, ...params: unknown[]) =>
62
+ profile("sql", () => guardQuery(sql, () => db.query(sql).all(...params) as T[]), () => ({ q: sqlLabel(sql) })),
63
+ get: <T extends SqlRow>(sql: string, ...params: unknown[]) =>
64
+ profile("sql", () => guardQuery(sql, () => (db.query(sql).get(...params) as T | null) ?? null), () => ({ q: sqlLabel(sql) })),
65
+ close: () => {
66
+ try {
67
+ db.close()
68
+ } catch {
69
+ // ignore
70
+ }
71
+ },
72
+ }
73
+ }
74
+
75
+ function wrapNodeSync(db: {
76
+ prepare: (sql: string) => {
77
+ all: (...p: unknown[]) => SqlRow[]
78
+ get: (...p: unknown[]) => SqlRow | undefined
79
+ }
80
+ exec: (sql: string) => void
81
+ close: () => void
82
+ }): SqlDb {
83
+ try {
84
+ db.exec("PRAGMA query_only = ON")
85
+ db.exec(`PRAGMA busy_timeout = ${BUSY_TIMEOUT_MS}`)
86
+ } catch {
87
+ // ignore
88
+ }
89
+ return {
90
+ all: <T extends SqlRow>(sql: string, ...params: unknown[]) =>
91
+ profile("sql", () => guardQuery(sql, () => db.prepare(sql).all(...params) as T[]), () => ({ q: sqlLabel(sql) })),
92
+ get: <T extends SqlRow>(sql: string, ...params: unknown[]) =>
93
+ profile("sql", () => guardQuery(sql, () => (db.prepare(sql).get(...params) as T | undefined) ?? null), () => ({ q: sqlLabel(sql) })),
94
+ close: () => {
95
+ try {
96
+ db.close()
97
+ } catch {
98
+ // ignore
99
+ }
100
+ },
101
+ }
102
+ }
103
+
104
+ let hold: { path: string; db: SqlDb } | null = null
105
+
106
+ function openFresh(dbPath: string): SqlDb | null {
107
+ if (!dbPath || !fs.existsSync(dbPath)) return null
108
+ try {
109
+ if (typeof process.versions.bun === "string") {
110
+ const { Database } = require("bun:sqlite") as {
111
+ Database: new (
112
+ path: string,
113
+ opts?: { readonly?: boolean; create?: boolean },
114
+ ) => Parameters<typeof wrapBun>[0]
115
+ }
116
+ return wrapBun(new Database(dbPath, { readonly: true, create: false }))
117
+ }
118
+ } catch {
119
+ // try node next
120
+ }
121
+ try {
122
+ const mod = require("node:sqlite") as {
123
+ DatabaseSync: new (
124
+ path: string,
125
+ opts?: { readOnly?: boolean },
126
+ ) => Parameters<typeof wrapNodeSync>[0]
127
+ }
128
+ if (!mod?.DatabaseSync) return null
129
+ return wrapNodeSync(new mod.DatabaseSync(dbPath, { readOnly: true }))
130
+ } catch {
131
+ return null
132
+ }
133
+ }
134
+
135
+ /** De-duplicated, non-empty string ids for `IN (...)` queries. */
136
+ export function uniqueIds(ids: readonly string[]): string[] {
137
+ return [...new Set(ids.filter((id) => typeof id === "string" && id.length > 0))]
138
+ }
139
+
140
+ /** Drop the cached handle so the next open is a real reopen. */
141
+ export function resetReadonlyDb(): void {
142
+ if (!hold) return
143
+ try {
144
+ hold.db.close()
145
+ } catch {
146
+ // ignore
147
+ }
148
+ hold = null
149
+ }
150
+
151
+ /** Retry a read after dropping a stale handle. */
152
+ export function withDbRead<T>(run: () => T, fallback: (err: unknown) => T): T {
153
+ try {
154
+ return run()
155
+ } catch (e) {
156
+ resetReadonlyDb()
157
+ try {
158
+ return run()
159
+ } catch {
160
+ return fallback(e)
161
+ }
162
+ }
163
+ }
164
+
165
+ /** Open opencode.db readonly. Reuses one handle per path. Never throws — returns null on failure. */
166
+ export function openReadonlyDb(dbPath: string): SqlDb | null {
167
+ if (!dbPath) return null
168
+ if (hold?.path === dbPath) return hold.db
169
+ resetReadonlyDb()
170
+ const db = openFresh(dbPath)
171
+ if (!db) return null
172
+ hold = { path: dbPath, db }
173
+ pushScreenLine("db open")
174
+ return db
175
+ }
@@ -0,0 +1,214 @@
1
+ /**
2
+ * Canonical lifecycle / tool / work status. OpenCode and OMO use different
3
+ * synonyms; every mapper in the panel goes through this file. The raw status
4
+ * strings live in `constants/pware.oc.core.constants.status.js`.
5
+ */
6
+ import {
7
+ STATUS_ABANDONED,
8
+ STATUS_ARCHIVED,
9
+ STATUS_COMPLETED,
10
+ STATUS_ERROR,
11
+ STATUS_IDLE,
12
+ STATUS_PAUSED,
13
+ STATUS_PENDING,
14
+ STATUS_RUNNING,
15
+ STATUS_UNKNOWN,
16
+ type CanonicalStatus,
17
+ type ToolStatus,
18
+ } from "./constants/pware.oc.core.constants.status.js"
19
+ import { formatTokenRate } from "./pware.oc.core.pulse.js"
20
+ import { clipMiddleWidth } from "./pware.oc.core.width.js"
21
+
22
+ export type { CanonicalStatus, ToolStatus }
23
+
24
+ /** The transient one-line status a tab shows while it waits for its data. */
25
+ export type TabTone = "loading" | "error" | "muted"
26
+
27
+ /** Null when the tab's data is ready — the row does not exist then. */
28
+ export type TabStatus = { label: string; tone: TabTone } | null
29
+
30
+ /** The transient DB state right after a session is selected but its row is not visible yet. */
31
+ export const TAB_STATUS_SESSION_NOT_IN_DB = "session not in db yet"
32
+
33
+ /** A tab status resolved to a render line: static glyph + label + tone. Null = no row. */
34
+ export type TabStatusLine = {
35
+ label: string
36
+ tone: TabTone
37
+ /** Static prefix glyph for error/muted; null for loading — the row animates the spinner. */
38
+ glyph: "×" | "•" | null
39
+ } | null
40
+
41
+ export function normalizeStatus(raw: string | null | undefined): CanonicalStatus {
42
+ const s = (raw || "").toLowerCase()
43
+ if (s === "running" || s === "in_progress" || s === "active") return STATUS_RUNNING
44
+ if (s === "completed" || s === "done" || s === "success") return STATUS_COMPLETED
45
+ if (s === "error" || s === "failed") return STATUS_ERROR
46
+ if (s === "pending" || s === "queued") return STATUS_PENDING
47
+ if (s === "paused") return STATUS_PAUSED
48
+ if (s === "abandoned") return STATUS_ABANDONED
49
+ if (s === "archived") return STATUS_ARCHIVED
50
+ if (s === "idle") return STATUS_IDLE
51
+ return STATUS_UNKNOWN
52
+ }
53
+
54
+ export function toToolStatus(raw: string | null | undefined): ToolStatus {
55
+ const c = normalizeStatus(raw)
56
+ if (c === STATUS_RUNNING) return STATUS_RUNNING
57
+ if (c === STATUS_COMPLETED) return STATUS_COMPLETED
58
+ if (c === STATUS_ERROR) return STATUS_ERROR
59
+ return STATUS_PENDING
60
+ }
61
+
62
+ /** Short work status for the panel — no full paths, no checklist text. */
63
+ export function toWorkLabel(raw: string | null | undefined): string {
64
+ const c = normalizeStatus(raw)
65
+ if (c === STATUS_COMPLETED) return "done"
66
+ if (c === STATUS_UNKNOWN) return (raw || "").toLowerCase() || "unknown"
67
+ return c
68
+ }
69
+
70
+ /**
71
+ * One-cell glyph for a work row's status: ✓ done, × error, ⧗ pending/queued,
72
+ * ║ paused, ⊘ abandoned, ○ unknown. Null while running — the pulse spinner owns
73
+ * the running cell. Core owns this mapping; the ui layer re-exports it.
74
+ */
75
+ export function workStatusGlyph(status: string): string | null {
76
+ const s = toWorkLabel(status)
77
+ if (s === "done") return "✓"
78
+ if (s === STATUS_ERROR) return "×"
79
+ if (s === STATUS_RUNNING) return null
80
+ // A single-cell geometric hourglass, not the emoji — one cell, monochrome.
81
+ if (s === STATUS_PENDING) return "⧗"
82
+ if (s === STATUS_PAUSED) return "║"
83
+ if (s === STATUS_ABANDONED) return "⊘"
84
+ return "○"
85
+ }
86
+
87
+ export function sessionStatusLabel(status: string): string {
88
+ return status
89
+ }
90
+
91
+ /**
92
+ * The one shared decision behind every tab's status row. Priority order:
93
+ * a session switch in flight, then the transient "row not visible yet" state,
94
+ * then any real DB error, then Perf's own states, then a cold (not yet loaded)
95
+ * tab. Null when the tab's data is ready.
96
+ */
97
+ export function tabStatus(opts: {
98
+ tab: string
99
+ currentId: string | null
100
+ dbError: string | null
101
+ dbPresent: boolean
102
+ switching: string | null
103
+ perfError: string | null
104
+ perfTurns: number
105
+ cold: boolean
106
+ }): TabStatus {
107
+ if (opts.switching && opts.currentId !== opts.switching && !opts.dbError) {
108
+ return { label: `switching · ${clipMiddleWidth(opts.switching, 10)}`, tone: "loading" }
109
+ }
110
+ if (opts.dbError === TAB_STATUS_SESSION_NOT_IN_DB) {
111
+ return { label: "waiting for session", tone: "loading" }
112
+ }
113
+ if (opts.dbError) {
114
+ return { label: opts.dbError, tone: "error" }
115
+ }
116
+ if (!opts.dbPresent) {
117
+ return { label: "waiting for session", tone: "loading" }
118
+ }
119
+ if (opts.tab === "perf") {
120
+ if (opts.perfError) return { label: opts.perfError, tone: "error" }
121
+ if (opts.cold) return { label: "loading · stats", tone: "loading" }
122
+ if (opts.perfTurns === 0) return { label: "no turns yet", tone: "muted" }
123
+ return null
124
+ }
125
+ if (opts.cold) {
126
+ return {
127
+ label: opts.tab === "mywork" ? "loading · my work" : "loading · sessions",
128
+ tone: "loading",
129
+ }
130
+ }
131
+ return null
132
+ }
133
+
134
+ /**
135
+ * Resolve a tab status to its render line. The null contract lives here so it
136
+ * is unit-testable: a ready tab (`null`) returns `null`, and the glyph maps
137
+ * tone → `×` error, `•` muted, `null` loading (the row animates the spinner).
138
+ */
139
+ export function tabStatusLine(status: TabStatus): TabStatusLine {
140
+ if (!status) return null
141
+ if (status.tone === "error") return { label: status.label, tone: "error", glyph: "×" }
142
+ if (status.tone === "muted") return { label: status.label, tone: "muted", glyph: "•" }
143
+ return { label: status.label, tone: "loading", glyph: null }
144
+ }
145
+
146
+ /** Tone of the global status bar: loading/error/muted, or "ready" when a tab is done. */
147
+ export type StatusBarTone = TabTone | "ready"
148
+
149
+ /**
150
+ * Global status-bar line. A ready tab (`null`) still yields a line — with an
151
+ * empty label the bar is a bare `OES` (the realtime category tabs on the same
152
+ * row already lead with their own `•`), never `OES •` + a tab bullet.
153
+ */
154
+ export type StatusBarLine = { label: string; tone: StatusBarTone }
155
+
156
+ export function statusBarLine(status: TabStatus): StatusBarLine {
157
+ const line = tabStatusLine(status)
158
+ if (!line) return { label: "", tone: "ready" }
159
+ return { label: line.label, tone: line.tone }
160
+ }
161
+
162
+ /**
163
+ * The OES bar split into the pieces the row draws separately, because the
164
+ * inline trend renders faded while the reading next to it stays full strength.
165
+ * Empty `bar` / `rate` mean the row is a single run of text. `rateIdle` marks
166
+ * a reading of zero, which the row fades to match the trend behind it.
167
+ */
168
+ export type OesBarParts = { head: string; bar: string; rate: string; rateIdle: boolean }
169
+
170
+ /**
171
+ * What the OES bar says. A busy label always wins — while the bar is reporting
172
+ * a load, a switch or an error, the live token reading is noise and is dropped.
173
+ * On a quiet bar the trend and the rate follow a bare `OES`; with neither, the
174
+ * row is `OES` plus whatever glyph the tone asked for. Pass `rate` as the raw
175
+ * per-second number; idleness follows the number the row actually prints, so a
176
+ * trickle that rounds down to `0 tok/s` fades like a dead stop.
177
+ */
178
+ export function oesBarParts(
179
+ label: string,
180
+ glyph: string,
181
+ rateBar = "",
182
+ rate?: number | null,
183
+ ): OesBarParts {
184
+ if (label) return { head: `OES ${glyph} ${label}`, bar: "", rate: "", rateIdle: false }
185
+ if (rateBar || rate !== undefined) {
186
+ const shown = typeof rate === "number" && Number.isFinite(rate) ? Math.round(rate) : 0
187
+ return {
188
+ head: "OES ",
189
+ bar: rateBar,
190
+ rate: rate === undefined ? "" : ` ${formatTokenRate(rate)}`,
191
+ rateIdle: shown <= 0,
192
+ }
193
+ }
194
+ return { head: glyph ? `OES ${glyph}` : "OES", bar: "", rate: "", rateIdle: false }
195
+ }
196
+
197
+ /** Paused and abandoned are deliberate stops — they must not keep pulsing. */
198
+ export function workIsTerminal(status: string): boolean {
199
+ const s = toWorkLabel(status)
200
+ return s === "done" || s === STATUS_ERROR || s === STATUS_PAUSED || s === STATUS_ABANDONED
201
+ }
202
+
203
+ export function isRunningLifecycle(raw: string | null | undefined): boolean {
204
+ return normalizeStatus(raw) === STATUS_RUNNING
205
+ }
206
+
207
+ export function taskRank(status: string): number {
208
+ const c = normalizeStatus(status)
209
+ if (c === STATUS_RUNNING) return 0
210
+ if (c === STATUS_PENDING) return 1
211
+ if (c === STATUS_ERROR || (status || "").toLowerCase() === "cancelled") return 2
212
+ if (c === STATUS_COMPLETED) return 3
213
+ return 4
214
+ }
@@ -0,0 +1,63 @@
1
+ /**
2
+ * pware.oc.core.timing
3
+ *
4
+ * Panel clock budgets — every interval and granularity in one place. The tick
5
+ * drives the animation heartbeat; `now` and the scan debounce decide how often
6
+ * rows and the DB re-read actually re-run. See docs/tick-system.md.
7
+ */
8
+
9
+ /** UI heartbeat — spinner/arrow animation phase. */
10
+ export const TICK_MS = 50
11
+
12
+ /**
13
+ * Fast glyph cadence — spinners and direction flows step this often on a
14
+ * separate signal (`glyphFrame`), so the animation is smooth while rows and
15
+ * ages still run on the coarse `TICK_MS` / `NOW_MS` clocks.
16
+ */
17
+ export const GLYPH_TICK_MS = 80
18
+
19
+ /**
20
+ * Coarse `now` granularity — ages, marks and row arrays recompute at most this
21
+ * often, while glyphs still animate at `TICK_MS`. Displayed ages use 1s
22
+ * granularity anyway (`formatCoarseSec`).
23
+ */
24
+ export const NOW_MS = 1_000
25
+
26
+ /** FPS read cadence in ticks (TICK_MS × 6 = 300ms). */
27
+ export const FPS_READ_EVERY_TICKS = 6
28
+
29
+ /** Blink half-period in ticks (TICK_MS × 2 = 100ms). */
30
+ export const BLINK_TICKS = 2
31
+
32
+ /** Monitor: SQLite poll interval. */
33
+ export const MONITOR_POLL_MS = 1_500
34
+
35
+ /** Monitor: fs-watch → emit debounce. */
36
+ export const MONITOR_WATCH_DEBOUNCE_MS = 120
37
+
38
+ /**
39
+ * My-work attention scan cadence while the tab is quiet — the periodic floor.
40
+ * DB-change-driven rescans (see MYWORK_BADGE_COOLDOWN_MS) refresh it sooner.
41
+ */
42
+ export const MYWORK_BADGE_MS = 5_000
43
+
44
+ /**
45
+ * Minimum gap between My-work badge rescans when a new DB snapshot just landed
46
+ * — a question appears within ~1s instead of waiting for the 5s floor.
47
+ */
48
+ export const MYWORK_BADGE_COOLDOWN_MS = 1_000
49
+
50
+ /** Host event → scan debounce (trailing). */
51
+ export const EVENT_SCAN_DEBOUNCE_MS = 100
52
+
53
+ /** Sliding window over which the live token rate is measured. */
54
+ export const TOKEN_RATE_WINDOW_MS = 5_000
55
+
56
+ /** How long the shared realtime timeline stays in RAM (3 minutes). */
57
+ export const REALTIME_WINDOW_MS = 3 * 60 * 1000
58
+
59
+ /** Window over which token/cache rates and CPU% are derived (≈ "rate right now"). */
60
+ export const REALTIME_RATE_WINDOW_MS = 1_000
61
+
62
+ /** Session-switch status row safety: hide the indicator even if no snapshot lands. */
63
+ export const SWITCH_TIMEOUT_MS = 3_000
@@ -0,0 +1,215 @@
1
+ /**
2
+ * Display-width helpers — columns, not code units.
3
+ *
4
+ * OpenTUI lays text out by wcwidth columns (a wide char is 2 columns, a
5
+ * combining mark is 0), but `String.length` counts UTF-16 code units. Every
6
+ * row-budget calculation in this plugin used `.length`, so a row with a wide
7
+ * character (emoji, CJK) measured "fits" while actually overflowing and
8
+ * wrapping to a second line. This module is the single place that turns text
9
+ * into columns and clips it grapheme-safely, so budgets are real columns.
10
+ */
11
+
12
+ /** Inclusive [start, end] code-point ranges of width-0 characters. */
13
+ const ZERO: readonly (readonly [number, number])[] = [
14
+ [0x0300, 0x036f], [0x0483, 0x0489], [0x0591, 0x05bd], [0x05bf, 0x05bf],
15
+ [0x05c1, 0x05c2], [0x05c4, 0x05c5], [0x05c7, 0x05c7], [0x0610, 0x061a],
16
+ [0x064b, 0x065f], [0x0670, 0x0670], [0x06d6, 0x06dc], [0x06df, 0x06e4],
17
+ [0x06e7, 0x06e8], [0x06ea, 0x06ed], [0x0711, 0x0711], [0x0730, 0x074a],
18
+ [0x07a6, 0x07b0], [0x07eb, 0x07f3], [0x0816, 0x0819], [0x081b, 0x0823],
19
+ [0x0825, 0x0827], [0x0829, 0x082d], [0x0859, 0x085b], [0x08d3, 0x0902],
20
+ [0x093a, 0x093a], [0x093c, 0x093c], [0x0941, 0x0948], [0x094d, 0x094d],
21
+ [0x0951, 0x0957], [0x0962, 0x0963], [0x0981, 0x0981], [0x09bc, 0x09bc],
22
+ [0x09c1, 0x09c4], [0x09cd, 0x09cd], [0x09e2, 0x09e3], [0x0a01, 0x0a02],
23
+ [0x0a3c, 0x0a3c], [0x0a41, 0x0a42], [0x0a47, 0x0a48], [0x0a4b, 0x0a4d],
24
+ [0x0a51, 0x0a51], [0x0a70, 0x0a71], [0x0a75, 0x0a75], [0x0a81, 0x0a82],
25
+ [0x0abc, 0x0abc], [0x0ac1, 0x0ac5], [0x0ac7, 0x0ac8], [0x0acd, 0x0acd],
26
+ [0x0ae2, 0x0ae3], [0x0afa, 0x0aff], [0x0b01, 0x0b01], [0x0b3c, 0x0b3c],
27
+ [0x0b3f, 0x0b3f], [0x0b41, 0x0b44], [0x0b4d, 0x0b4d], [0x0b56, 0x0b56],
28
+ [0x0b62, 0x0b63], [0x0b82, 0x0b82], [0x0bc0, 0x0bc0], [0x0bcd, 0x0bcd],
29
+ [0x0c00, 0x0c00], [0x0c04, 0x0c04], [0x0c3e, 0x0c40], [0x0c46, 0x0c48],
30
+ [0x0c4a, 0x0c4d], [0x0c55, 0x0c56], [0x0c62, 0x0c63], [0x0c81, 0x0c81],
31
+ [0x0cbc, 0x0cbc], [0x0cbf, 0x0cbf], [0x0cc6, 0x0cc6], [0x0ccc, 0x0ccd],
32
+ [0x0ce2, 0x0ce3], [0x0d00, 0x0d01], [0x0d3b, 0x0d3c], [0x0d41, 0x0d44],
33
+ [0x0d4d, 0x0d4d], [0x0d62, 0x0d63], [0x0dca, 0x0dca], [0x0dd2, 0x0dd4],
34
+ [0x0dd6, 0x0dd6], [0x0e31, 0x0e31], [0x0e34, 0x0e3a], [0x0e47, 0x0e4e],
35
+ [0x0eb1, 0x0eb1], [0x0eb4, 0x0ebc], [0x0ec8, 0x0ecd], [0x0f18, 0x0f19],
36
+ [0x0f35, 0x0f35], [0x0f37, 0x0f37], [0x0f39, 0x0f39], [0x0f71, 0x0f7e],
37
+ [0x0f80, 0x0f84], [0x0f86, 0x0f87], [0x0f8d, 0x0f97], [0x0f99, 0x0fbc],
38
+ [0x0fc6, 0x0fc6], [0x102d, 0x1030], [0x1032, 0x1037], [0x1039, 0x103a],
39
+ [0x103d, 0x103e], [0x1058, 0x1059], [0x105e, 0x1060], [0x1071, 0x1074],
40
+ [0x1082, 0x1082], [0x1085, 0x1086], [0x108d, 0x108d], [0x109d, 0x109d],
41
+ [0x135d, 0x135f], [0x1712, 0x1714], [0x1732, 0x1734], [0x1752, 0x1753],
42
+ [0x1772, 0x1773], [0x17b4, 0x17b5], [0x17b7, 0x17bd], [0x17c6, 0x17c6],
43
+ [0x17c9, 0x17d3], [0x17dd, 0x17dd], [0x180b, 0x180e], [0x1885, 0x1886],
44
+ [0x18a9, 0x18a9], [0x1920, 0x1922], [0x1927, 0x1928], [0x1932, 0x1932],
45
+ [0x1939, 0x193b], [0x1a17, 0x1a18], [0x1a1b, 0x1a1b], [0x1a56, 0x1a56],
46
+ [0x1a58, 0x1a5e], [0x1a60, 0x1a60], [0x1a62, 0x1a62], [0x1a65, 0x1a6c],
47
+ [0x1a73, 0x1a7c], [0x1a7f, 0x1a7f], [0x1ab0, 0x1ac0], [0x1b00, 0x1b03],
48
+ [0x1b34, 0x1b34], [0x1b36, 0x1b3a], [0x1b3c, 0x1b3c], [0x1b42, 0x1b42],
49
+ [0x1b6b, 0x1b73], [0x1b80, 0x1b81], [0x1ba2, 0x1ba5], [0x1ba8, 0x1ba9],
50
+ [0x1bab, 0x1bad], [0x1be6, 0x1be6], [0x1be8, 0x1be9], [0x1bed, 0x1bed],
51
+ [0x1bef, 0x1bf1], [0x1c2c, 0x1c33], [0x1c36, 0x1c37], [0x1cd0, 0x1cd2],
52
+ [0x1cd4, 0x1ce0], [0x1ce2, 0x1ce8], [0x1ced, 0x1ced], [0x1cf4, 0x1cf4],
53
+ [0x1cf8, 0x1cf9], [0x1dc0, 0x1df9], [0x1dfb, 0x1dff], [0x200b, 0x200f],
54
+ [0x202a, 0x202e], [0x2060, 0x2064], [0x2066, 0x206f], [0x20d0, 0x20f0],
55
+ [0x2cef, 0x2cf1], [0x2d7f, 0x2d7f], [0x2de0, 0x2dff], [0x302a, 0x302d],
56
+ [0x3099, 0x309a], [0xa66f, 0xa672], [0xa674, 0xa67d], [0xa69e, 0xa69f],
57
+ [0xa6f0, 0xa6f1], [0xa802, 0xa802], [0xa806, 0xa806], [0xa80b, 0xa80b],
58
+ [0xa825, 0xa826], [0xa8c4, 0xa8c5], [0xa8e0, 0xa8f1], [0xa926, 0xa92d],
59
+ [0xa947, 0xa951], [0xa980, 0xa982], [0xa9b3, 0xa9b3], [0xa9b6, 0xa9b9],
60
+ [0xa9bc, 0xa9bd], [0xa9e5, 0xa9e5], [0xaa29, 0xaa2e], [0xaa31, 0xaa32],
61
+ [0xaa35, 0xaa36], [0xaa43, 0xaa43], [0xaa4c, 0xaa4c], [0xaa7c, 0xaa7c],
62
+ [0xaab0, 0xaab0], [0xaab2, 0xaab4], [0xaab7, 0xaab8], [0xaabe, 0xaabf],
63
+ [0xaac1, 0xaac1], [0xaaec, 0xaaed], [0xaaf6, 0xaaf6], [0xabe5, 0xabe5],
64
+ [0xabe8, 0xabe8], [0xabed, 0xabed], [0xfb1e, 0xfb1e], [0xfe00, 0xfe0f],
65
+ [0xfe20, 0xfe2f], [0x101fd, 0x101fd], [0x102e0, 0x102e0], [0x10376, 0x1037a],
66
+ [0x10a01, 0x10a03], [0x10a05, 0x10a06], [0x10a0c, 0x10a0f], [0x10a38, 0x10a3a],
67
+ [0x10a3f, 0x10a3f], [0x10ae5, 0x10ae6], [0x11001, 0x11001], [0x11038, 0x11046],
68
+ [0x1107f, 0x11081], [0x110b3, 0x110b6], [0x110b9, 0x110ba], [0x11100, 0x11102],
69
+ [0x11127, 0x1112b], [0x1112d, 0x11134], [0x11173, 0x11173], [0x11180, 0x11181],
70
+ [0x111b6, 0x111be], [0x111ca, 0x111cc], [0x1122f, 0x11231], [0x11234, 0x11234],
71
+ [0x11236, 0x11237], [0x1123e, 0x1123e], [0x112df, 0x112df], [0x112e3, 0x112ea],
72
+ [0x11300, 0x11301], [0x1133c, 0x1133c], [0x11340, 0x11340], [0x11366, 0x1136c],
73
+ [0x11370, 0x11374], [0x11438, 0x1143f], [0x11442, 0x11444], [0x11446, 0x11446],
74
+ [0x114b3, 0x114b8], [0x114ba, 0x114ba], [0x114bf, 0x114c0], [0x114c2, 0x114c3],
75
+ [0x115b2, 0x115b5], [0x115bc, 0x115bd], [0x115bf, 0x115c0], [0x115dc, 0x115dd],
76
+ [0x11633, 0x1163a], [0x1163d, 0x1163d], [0x1163f, 0x11640], [0x116ab, 0x116ab],
77
+ [0x116ad, 0x116ad], [0x116b0, 0x116b5], [0x116b7, 0x116b7], [0x1171d, 0x1171f],
78
+ [0x11722, 0x11725], [0x11727, 0x1172b], [0x11c30, 0x11c36], [0x11c38, 0x11c3d],
79
+ [0x11c3f, 0x11c3f], [0x11c92, 0x11ca7], [0x11caa, 0x11cb0], [0x11cb2, 0x11cb3],
80
+ [0x11cb5, 0x11cb6], [0x11d31, 0x11d36], [0x11d3a, 0x11d3a], [0x11d3c, 0x11d3d],
81
+ [0x11d3f, 0x11d45], [0x11d47, 0x11d47], [0x16af0, 0x16af4], [0x16b30, 0x16b36],
82
+ [0x16f8f, 0x16f92], [0x1bc9d, 0x1bc9e], [0x1bca0, 0x1bca3], [0x1d165, 0x1d169],
83
+ [0x1d16d, 0x1d182], [0x1d185, 0x1d18b], [0x1d1aa, 0x1d1ad], [0x1d242, 0x1d244],
84
+ [0x1da00, 0x1da36], [0x1da3b, 0x1da6c], [0x1da75, 0x1da75], [0x1da84, 0x1da84],
85
+ [0x1da9b, 0x1da9f], [0x1daa1, 0x1daaf], [0x1e000, 0x1e006], [0x1e008, 0x1e018],
86
+ [0x1e01b, 0x1e021], [0x1e023, 0x1e024], [0x1e026, 0x1e02a], [0x1e8d0, 0x1e8d6],
87
+ [0x1e944, 0x1e94a], [0xe0100, 0xe01ef],
88
+ ]
89
+
90
+ /** Inclusive [start, end] code-point ranges of width-2 (wide) characters. */
91
+ const WIDE: readonly (readonly [number, number])[] = [
92
+ [0x1100, 0x115f], [0x231a, 0x231b], [0x2329, 0x232a], [0x23e9, 0x23ec],
93
+ [0x23f0, 0x23f0], [0x23f3, 0x23f3], [0x25fd, 0x25fe], [0x2614, 0x2615],
94
+ [0x2648, 0x2653], [0x267f, 0x267f], [0x2693, 0x2693], [0x26a1, 0x26a1],
95
+ [0x26aa, 0x26ab], [0x26bd, 0x26be], [0x26c4, 0x26c5], [0x26ce, 0x26ce],
96
+ [0x26d4, 0x26d4], [0x26ea, 0x26ea], [0x26f2, 0x26f3], [0x26f5, 0x26f5],
97
+ [0x26fa, 0x26fa], [0x26fd, 0x26fd], [0x2705, 0x2705], [0x270a, 0x270b],
98
+ [0x2728, 0x2728], [0x274c, 0x274c], [0x274e, 0x274e], [0x2753, 0x2755],
99
+ [0x2757, 0x2757], [0x2795, 0x2797], [0x27b0, 0x27b0], [0x27bf, 0x27bf],
100
+ [0x2b1b, 0x2b1c], [0x2b50, 0x2b50], [0x2b55, 0x2b55], [0x2e80, 0x2e99],
101
+ [0x2e9b, 0x2ef3], [0x2f00, 0x2fd5], [0x2ff0, 0x2ffb], [0x3000, 0x303e],
102
+ [0x3041, 0x3096], [0x3099, 0x30ff], [0x3105, 0x312f], [0x3131, 0x318e],
103
+ [0x3190, 0x31e3], [0x31f0, 0x321e], [0x3220, 0x3247], [0x3250, 0x4dbf],
104
+ [0x4e00, 0xa48c], [0xa490, 0xa4c6], [0xa960, 0xa97c], [0xac00, 0xd7a3],
105
+ [0xf900, 0xfaff], [0xfe10, 0xfe19], [0xfe30, 0xfe52], [0xfe54, 0xfe66],
106
+ [0xfe68, 0xfe6b], [0xff00, 0xff60], [0xffe0, 0xffe6], [0x16fe0, 0x16fe4],
107
+ [0x17000, 0x187f7], [0x18800, 0x18cd5], [0x18d00, 0x18d08], [0x1b000, 0x1b11e],
108
+ [0x1b150, 0x1b152], [0x1b164, 0x1b167], [0x1b170, 0x1b2fb], [0x1f004, 0x1f004],
109
+ [0x1f0cf, 0x1f0cf], [0x1f18e, 0x1f18e], [0x1f191, 0x1f19a], [0x1f200, 0x1f202],
110
+ [0x1f210, 0x1f23b], [0x1f240, 0x1f248], [0x1f250, 0x1f251], [0x1f260, 0x1f265],
111
+ [0x1f300, 0x1f320], [0x1f32d, 0x1f335], [0x1f337, 0x1f37c], [0x1f37e, 0x1f393],
112
+ [0x1f3a0, 0x1f3ca], [0x1f3cf, 0x1f3d3], [0x1f3e0, 0x1f3f0], [0x1f3f4, 0x1f3f4],
113
+ [0x1f3f8, 0x1f43e], [0x1f440, 0x1f440], [0x1f442, 0x1f4fc], [0x1f4ff, 0x1f53d],
114
+ [0x1f54b, 0x1f54e], [0x1f550, 0x1f567], [0x1f57a, 0x1f57a], [0x1f595, 0x1f596],
115
+ [0x1f5a4, 0x1f5a4], [0x1f5fb, 0x1f64f], [0x1f680, 0x1f6c5], [0x1f6cc, 0x1f6cc],
116
+ [0x1f6d0, 0x1f6d2], [0x1f6d5, 0x1f6d7], [0x1f6eb, 0x1f6ec], [0x1f6f4, 0x1f6fc],
117
+ [0x1f7e0, 0x1f7eb], [0x1f90c, 0x1f93a], [0x1f93c, 0x1f945], [0x1f947, 0x1f978],
118
+ [0x1f97a, 0x1f9cb], [0x1f9cd, 0x1f9ff], [0x1fa70, 0x1fa74], [0x1fa78, 0x1fa7a],
119
+ [0x1fa80, 0x1fa86], [0x1fa90, 0x1faa8], [0x1fab0, 0x1fab6], [0x1fac0, 0x1fac2],
120
+ [0x1fad0, 0x1fad6], [0x20000, 0x2fffd], [0x30000, 0x3fffd],
121
+ ]
122
+
123
+ function inRanges(cp: number, table: readonly (readonly [number, number])[]): boolean {
124
+ let lo = 0
125
+ let hi = table.length - 1
126
+ while (lo <= hi) {
127
+ const mid = (lo + hi) >> 1
128
+ const [a, b] = table[mid]!
129
+ if (cp < a) hi = mid - 1
130
+ else if (cp > b) lo = mid + 1
131
+ else return true
132
+ }
133
+ return false
134
+ }
135
+
136
+ /** wcwidth of one code point: 0 combining/control, 2 wide, else 1. */
137
+ export function codepointWidth(cp: number): number {
138
+ if (cp === 0) return 0
139
+ if (cp < 32 || (cp >= 0x7f && cp < 0xa0)) return 0
140
+ if (inRanges(cp, ZERO)) return 0
141
+ if (inRanges(cp, WIDE)) return 2
142
+ return 1
143
+ }
144
+
145
+ /** Display width in columns (wcwidth) of a string. */
146
+ export function strWidth(s: string): number {
147
+ let w = 0
148
+ for (const ch of s) w += codepointWidth(ch.codePointAt(0)!)
149
+ return w
150
+ }
151
+
152
+ /** Split into grapheme clusters; falls back to code points when Segmenter is absent. */
153
+ function graphemes(s: string): string[] {
154
+ const Seg = (Intl as unknown as {
155
+ Segmenter?: new (l?: string, o?: { granularity: string }) => { segment: (t: string) => Iterable<{ segment: string }> }
156
+ }).Segmenter
157
+ if (typeof Seg === "function") {
158
+ return Array.from(new Seg(undefined, { granularity: "grapheme" }).segment(s), (x) => x.segment)
159
+ }
160
+ return Array.from(s)
161
+ }
162
+
163
+ /** Longest prefix of `s` whose column width is at most `cols`. */
164
+ export function takeCols(s: string, cols: number): string {
165
+ if (cols <= 0) return ""
166
+ let width = 0
167
+ let out = ""
168
+ for (const g of graphemes(s)) {
169
+ const gw = strWidth(g)
170
+ if (width + gw > cols) break
171
+ width += gw
172
+ out += g
173
+ }
174
+ return out
175
+ }
176
+
177
+ /** Longest suffix of `s` whose column width is at most `cols`. */
178
+ export function takeLastCols(s: string, cols: number): string {
179
+ if (cols <= 0) return ""
180
+ const gs = graphemes(s)
181
+ let width = 0
182
+ const picked: string[] = []
183
+ for (let i = gs.length - 1; i >= 0; i--) {
184
+ const g = gs[i]!
185
+ const gw = strWidth(g)
186
+ if (width + gw > cols) break
187
+ width += gw
188
+ picked.unshift(g)
189
+ }
190
+ return picked.join("")
191
+ }
192
+
193
+ const ELLIPSIS = "…"
194
+
195
+ /** Clip to at most `max` columns with a trailing ellipsis; collapses whitespace. */
196
+ export function clipWidth(s: string, max: number): string {
197
+ const t = s.replace(/\s+/g, " ").trim()
198
+ if (max <= 0) return ""
199
+ if (strWidth(t) <= max) return t
200
+ if (max === 1) return ELLIPSIS
201
+ return `${takeCols(t, max - strWidth(ELLIPSIS))}${ELLIPSIS}`
202
+ }
203
+
204
+ /** Clip to at most `max` columns with a middle ellipsis; collapses whitespace. */
205
+ export function clipMiddleWidth(s: string, max: number): string {
206
+ const t = s.replace(/\s+/g, " ").trim()
207
+ if (max <= 0) return ""
208
+ if (strWidth(t) <= max) return t
209
+ if (max <= 2) return takeCols(t, max)
210
+ // Suffix-bias the middle split, but never wider than the post-ellipsis budget.
211
+ const budget = max - strWidth(ELLIPSIS)
212
+ const tail = Math.min(budget, Math.max(3, Math.floor(budget / 2)))
213
+ const head = budget - tail
214
+ return `${takeCols(t, head)}${ELLIPSIS}${takeLastCols(t, tail)}`
215
+ }
@@ -0,0 +1,15 @@
1
+ /**
2
+ * pware.oc.omo.constants
3
+ *
4
+ * Single source of truth for the OMO string literals: plan pending statuses,
5
+ * boulder statuses, run-continuation background-task states, review-lifecycle
6
+ * statuses, and review lane verdicts.
7
+ */
8
+ export * from "./pware.oc.omo.constants.planStatus.js"
9
+ export * from "./pware.oc.omo.constants.boulderStatus.js"
10
+ export * from "./pware.oc.omo.constants.backgroundTask.js"
11
+ export * from "./pware.oc.omo.constants.reviewStatus.js"
12
+ export * from "./pware.oc.omo.constants.verdict.js"
13
+ export * from "./pware.oc.omo.constants.docKind.js"
14
+ export * from "./pware.oc.omo.constants.startWork.js"
15
+ export * from "./pware.oc.omo.constants.eventName.js"