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,339 @@
1
+ /** @jsxImportSource @opentui/solid */
2
+ /** Shared sidebar chrome: brand tabs, theme colours, fold headers, diff stats, kv persistence. */
3
+ import { createSignal, For, Show, type JSX } from "solid-js"
4
+ import { TextAttributes } from "@opentui/core"
5
+ import type { TuiPluginApi } from "@opencode-ai/plugin/tui"
6
+ import type { GlyphSpec, ToneKey } from "../pware.oc.core/pware.oc.core.glyph.js"
7
+ import { CONTEXT_ACTION_COL_WIDTH } from "../pware.oc.core/pware.oc.core.layout.js"
8
+ import { formatDismissed, parseDismissed } from "../pware.oc.runtime/pware.oc.runtime.mywork.js"
9
+
10
+ /** One clickable context action in the shared right rail. */
11
+ export type ContextAction = {
12
+ label: string
13
+ onPick: () => void
14
+ bold?: boolean
15
+ }
16
+
17
+ /** OpenTUI has no bold/underline props on `<text>` — only the `attributes` bitmask. */
18
+ export function textAttrs(bold?: boolean, underline?: boolean): number {
19
+ let a = 0
20
+ if (bold) a |= TextAttributes.BOLD
21
+ if (underline) a |= TextAttributes.UNDERLINE
22
+ return a
23
+ }
24
+
25
+ /**
26
+ * Clickable label: underlined only while the pointer hovers it.
27
+ * OpenTUI fires `over`/`out` on hit-test change, so the underline
28
+ * tracks the mouse instead of being baked in. `underline` marks
29
+ * clickability; without it the label never underlines.
30
+ */
31
+ export function ClickText(props: {
32
+ fg?: string
33
+ bold?: boolean
34
+ /** Clickable — the underline appears while the pointer is over the label. */
35
+ underline?: boolean
36
+ opacity?: number
37
+ onMouseUp?: () => void
38
+ children: JSX.Element
39
+ }): JSX.Element {
40
+ const [hovered, setHovered] = createSignal(false)
41
+ return (
42
+ <text
43
+ fg={props.fg}
44
+ opacity={props.opacity}
45
+ attributes={textAttrs(Boolean(props.bold), Boolean(props.underline) && hovered())}
46
+ onMouseUp={props.onMouseUp}
47
+ onMouseOver={() => setHovered(true)}
48
+ onMouseOut={() => setHovered(false)}
49
+ >
50
+ {props.children}
51
+ </text>
52
+ )
53
+ }
54
+
55
+ export type ThemeColors = {
56
+ text: string
57
+ textMuted: string
58
+ success: string
59
+ primary?: string
60
+ warning?: string
61
+ error?: string
62
+ diffAdded?: string
63
+ diffRemoved?: string
64
+ }
65
+
66
+ export function kvRead(api: TuiPluginApi, key: string, fallback: boolean): boolean {
67
+ try {
68
+ const kv = (api as TuiPluginApi & { kv?: { get: (k: string, d?: boolean) => boolean } }).kv
69
+ return kv?.get(key, fallback) ?? fallback
70
+ } catch {
71
+ return fallback
72
+ }
73
+ }
74
+
75
+ export function kvWrite(api: TuiPluginApi, key: string, value: boolean): void {
76
+ try {
77
+ const kv = (api as TuiPluginApi & { kv?: { set: (k: string, v: boolean) => void } }).kv
78
+ kv?.set(key, value)
79
+ } catch {
80
+ // no kv in older hosts
81
+ }
82
+ }
83
+
84
+ /** Same store, string value. Anything outside `allowed` falls back. */
85
+ export function kvReadOne<T extends string>(
86
+ api: TuiPluginApi,
87
+ key: string,
88
+ fallback: T,
89
+ allowed: readonly T[],
90
+ ): T {
91
+ try {
92
+ const kv = (api as TuiPluginApi & { kv?: { get: (k: string, d?: string) => unknown } }).kv
93
+ const raw = kv?.get(key, fallback)
94
+ return allowed.includes(raw as T) ? (raw as T) : fallback
95
+ } catch {
96
+ return fallback
97
+ }
98
+ }
99
+
100
+ export function kvWriteOne(api: TuiPluginApi, key: string, value: string): void {
101
+ try {
102
+ const kv = (api as TuiPluginApi & { kv?: { set: (k: string, v: string) => void } }).kv
103
+ kv?.set(key, value)
104
+ } catch {
105
+ // no kv in older hosts
106
+ }
107
+ }
108
+
109
+ const KV_DISMISSED_QUESTIONS = "oes.dismissed.questions"
110
+
111
+ export function readDismissedQuestions(api: TuiPluginApi): ReadonlySet<string> {
112
+ try {
113
+ const kv = (api as TuiPluginApi & { kv?: { get: (k: string, d?: unknown) => unknown } }).kv
114
+ const raw = kv?.get(KV_DISMISSED_QUESTIONS, null)
115
+ return parseDismissed(typeof raw === "string" ? raw : null)
116
+ } catch {
117
+ return new Set()
118
+ }
119
+ }
120
+
121
+ export function dismissQuestion(api: TuiPluginApi, partId: string): void {
122
+ try {
123
+ const ids = new Set(readDismissedQuestions(api))
124
+ ids.add(partId)
125
+ const kv = (api as TuiPluginApi & { kv?: { set: (k: string, v: string) => void } }).kv
126
+ kv?.set(KV_DISMISSED_QUESTIONS, formatDismissed(ids))
127
+ } catch {
128
+ // no kv in older hosts
129
+ }
130
+ }
131
+
132
+ /** The single mapping from a semantic tone to a concrete theme colour. */
133
+ export function toneColor(tone: ToneKey, colors: ThemeColors): string {
134
+ switch (tone) {
135
+ case "success":
136
+ return colors.success
137
+ case "warning":
138
+ return colors.warning || colors.text
139
+ case "primary":
140
+ return colors.primary || colors.text
141
+ case "error":
142
+ return colors.error || colors.text
143
+ case "text":
144
+ return colors.text
145
+ default:
146
+ return colors.textMuted
147
+ }
148
+ }
149
+
150
+ export function diffAddFg(colors: ThemeColors): string {
151
+ return colors.diffAdded || colors.success
152
+ }
153
+
154
+ export function diffDelFg(colors: ThemeColors): string {
155
+ return colors.diffRemoved || colors.error || colors.text
156
+ }
157
+
158
+ export function DiffStat(props: {
159
+ additions: number
160
+ deletions: number
161
+ colors: ThemeColors
162
+ }): JSX.Element {
163
+ const lead = () => (props.additions > 0 && props.deletions > 0 ? " " : "")
164
+ return (
165
+ <box flexDirection="row">
166
+ <Show when={props.additions > 0}>
167
+ <text fg={diffAddFg(props.colors)}>{`+${props.additions}`}</text>
168
+ </Show>
169
+ <Show when={props.deletions > 0}>
170
+ <text fg={diffDelFg(props.colors)}>{`${lead()}−${props.deletions}`}</text>
171
+ </Show>
172
+ </box>
173
+ )
174
+ }
175
+
176
+ /**
177
+ * Right-rail action labels — fold headers, realtime `F`, and any future row
178
+ * actions share {@link CONTEXT_ACTION_COL_WIDTH} so the column lines up.
179
+ */
180
+ export function ContextActions(props: {
181
+ actions: readonly ContextAction[]
182
+ colors: ThemeColors
183
+ width?: number
184
+ }): JSX.Element {
185
+ return (
186
+ <box
187
+ flexDirection="row"
188
+ width={props.width ?? CONTEXT_ACTION_COL_WIDTH}
189
+ justifyContent="flex-end"
190
+ flexShrink={0}
191
+ >
192
+ <For each={props.actions}>
193
+ {(action, i) => (
194
+ <>
195
+ <Show when={i() > 0}>
196
+ <text> </text>
197
+ </Show>
198
+ <ClickText
199
+ fg={props.colors.primary || props.colors.text}
200
+ bold={action.bold}
201
+ underline
202
+ onMouseUp={action.onPick}
203
+ >
204
+ {action.label}
205
+ </ClickText>
206
+ </>
207
+ )}
208
+ </For>
209
+ </box>
210
+ )
211
+ }
212
+
213
+ /** Header text for a fold row: title, optional `(N)` / `(live/N)` / `(countLabel)` parenthetical, optional suffix. */
214
+ export function foldHeaderTitle(
215
+ title: string,
216
+ opts: { count?: number; live?: number; countLabel?: string; suffix?: string } = {},
217
+ ): string {
218
+ const { count, live = 0, countLabel, suffix } = opts
219
+ const stat = suffix ? ` ${suffix}` : ""
220
+ if (typeof count !== "number") return `${title}${stat}`
221
+ const extra = countLabel
222
+ ? ` (${countLabel})`
223
+ : live > 0
224
+ ? ` (${live}/${count})`
225
+ : ` (${count})`
226
+ return `${title}${extra}${stat}`
227
+ }
228
+
229
+ export function FoldHeader(props: {
230
+ title: string
231
+ open: boolean
232
+ count?: number
233
+ live?: number
234
+ /** Overrides the `(N)` / `(live/N)` parenthetical, e.g. `last 4`. */
235
+ countLabel?: string
236
+ suffix?: string
237
+ diff?: { additions: number; deletions: number }
238
+ colors: ThemeColors
239
+ onToggle: () => void
240
+ /** Title click opens a detail (fold stays on the chevron). */
241
+ onDetail?: () => void
242
+ /** Context actions in the shared right rail (`switch` / `new`, `view all`, …). */
243
+ actions?: readonly ContextAction[]
244
+ }): JSX.Element {
245
+ const rest = () => foldHeaderTitle(props.title, props)
246
+ const hasDiff = () =>
247
+ Boolean(props.diff && (props.diff.additions > 0 || props.diff.deletions > 0))
248
+ const chevron = () => (props.open ? "▼" : "▶")
249
+ const actions = () => props.actions ?? []
250
+ const split = Boolean(props.onDetail || actions().length > 0)
251
+ return (
252
+ <box flexDirection="row" width="100%" onMouseUp={split ? undefined : props.onToggle}>
253
+ <box flexDirection="row" flexGrow={1} flexShrink={1} minWidth={0}>
254
+ <ClickText
255
+ fg={props.colors.text}
256
+ underline={!props.onDetail}
257
+ onMouseUp={split ? props.onToggle : undefined}
258
+ >
259
+ {props.onDetail ? `${chevron()} ` : `${chevron()} ${rest()}`}
260
+ </ClickText>
261
+ <Show when={props.onDetail}>
262
+ <ClickText fg={props.colors.text} underline onMouseUp={props.onDetail}>
263
+ {rest()}
264
+ </ClickText>
265
+ </Show>
266
+ <Show when={hasDiff()}>
267
+ <text> </text>
268
+ <DiffStat
269
+ additions={props.diff?.additions ?? 0}
270
+ deletions={props.diff?.deletions ?? 0}
271
+ colors={props.colors}
272
+ />
273
+ </Show>
274
+ </box>
275
+ <Show when={actions().length > 0}>
276
+ <ContextActions actions={actions()} colors={props.colors} />
277
+ </Show>
278
+ </box>
279
+ )
280
+ }
281
+
282
+ /**
283
+ * Brand + clickable tabs. Active tab is bold + primary; clickable labels
284
+ * underline on hover. `onBrand` makes the brand itself a control — the OMO
285
+ * group folds on it.
286
+ */
287
+ export function BrandTabs(props: {
288
+ brand: string
289
+ tabs: readonly string[]
290
+ labels: Record<string, string>
291
+ active: string | null
292
+ colors: ThemeColors
293
+ onPick: (tab: string) => void
294
+ onBrand?: () => void
295
+ /**
296
+ * Per-tab leading glyph replacing the `|` separator between tabs — the tab's
297
+ * status light. The glyph doubles as spacing, so headers read `• My work • Session …`.
298
+ */
299
+ glyph?: (tab: string) => GlyphSpec
300
+ }): JSX.Element {
301
+ const brand = () => props.colors.primary || props.colors.text
302
+ const tabFg = (tab: string) =>
303
+ props.active === tab ? props.colors.primary || props.colors.text : props.colors.textMuted
304
+ return (
305
+ <box flexDirection="row" gap={1}>
306
+ <Show when={props.brand}>
307
+ <box flexDirection="row" onMouseUp={props.onBrand}>
308
+ <ClickText fg={brand()} bold underline={Boolean(props.onBrand)}>
309
+ {props.brand}
310
+ </ClickText>
311
+ </box>
312
+ </Show>
313
+ <For each={props.tabs}>
314
+ {(tab: string, i) => {
315
+ // Read the glyph through an accessor invoked inside the JSX, never a
316
+ // plain body const: SolidJS only re-runs JSX expressions, so the
317
+ // My-work tab light (a live memo) updates only when it is read at the
318
+ // expression site. A body-level `const glyph = props.glyph(tab)` would
319
+ // freeze the header at its first render.
320
+ const glyph = () => (props.glyph ? props.glyph(tab) : null)
321
+ return (
322
+ <box flexDirection="row" gap={1} onMouseUp={() => props.onPick(tab)}>
323
+ {glyph() ? (
324
+ <text fg={toneColor(glyph()!.tone, props.colors)}>{`${glyph()!.char} `}</text>
325
+ ) : (
326
+ <Show when={Boolean(props.brand) || i() > 0}>
327
+ <text fg={props.colors.textMuted}>|</text>
328
+ </Show>
329
+ )}
330
+ <ClickText fg={tabFg(tab)} bold={props.active === tab} underline>
331
+ {props.labels[tab] ?? tab}
332
+ </ClickText>
333
+ </box>
334
+ )
335
+ }}
336
+ </For>
337
+ </box>
338
+ )
339
+ }
@@ -0,0 +1,144 @@
1
+ /**
2
+ * pware.oc.ui.glyphs
3
+ *
4
+ * Domain glyph definitions (My work kinds, git letters, review lanes) that
5
+ * depend on opencode / omo / runtime types, so they cannot live in core. Each
6
+ * returns a complete `GlyphSpec` (char + tone). The pure mark/flow glyphs live
7
+ * in core (`pware.oc.core/pware.oc.core.glyph.ts`) and are re-exported here so
8
+ * every glyph resolves through this one module.
9
+ */
10
+ import type { FileLetter } from "../pware.oc.opencode/pware.oc.opencode.files.js"
11
+ import type { MyWorkKind } from "../pware.oc.runtime/pware.oc.runtime.mywork.js"
12
+ import type { ReviewLane, ReviewState } from "../pware.oc.omo/resolver/pware.oc.omo.resolver.plan.js"
13
+ import {
14
+ REVIEW_STATUS_APPROVED,
15
+ REVIEW_STATUS_CHANGES_REQUESTED,
16
+ REVIEW_STATUS_INCONCLUSIVE,
17
+ REVIEW_STATUS_PENDING,
18
+ ROUND_STATUS_ACTIVE,
19
+ } from "../pware.oc.omo/constants/pware.oc.omo.constants.reviewStatus.js"
20
+ import {
21
+ MY_WORK_GROUP_DISMISSED,
22
+ MY_WORK_GROUP_DRAFT_DOCS,
23
+ MY_WORK_GROUP_DRAFTING,
24
+ MY_WORK_GROUP_FINISHED,
25
+ MY_WORK_GROUP_PINNED,
26
+ MY_WORK_GROUP_PLANS,
27
+ MY_WORK_GROUP_READY_REVIEW,
28
+ MY_WORK_GROUP_READY_START,
29
+ MY_WORK_GROUP_SESSIONS,
30
+ } from "../pware.oc.core/constants/pware.oc.core.constants.myWork.js"
31
+ import {
32
+ QUESTION_KIND_ERROR,
33
+ QUESTION_KIND_INTERRUPTED,
34
+ QUESTION_KIND_QUESTION,
35
+ } from "../pware.oc.opencode/constants/pware.oc.opencode.constants.questionKind.js"
36
+ import type { GlyphSpec } from "../pware.oc.core/pware.oc.core.glyph.js"
37
+
38
+ export { workStatusGlyph } from "../pware.oc.core/pware.oc.core.status.js"
39
+ export {
40
+ SPINNER_FRAMES,
41
+ QUEUED_GLYPH,
42
+ ENGAGE_MIN_FRAMES,
43
+ ENGAGE_MAX_FRAMES,
44
+ spinnerFrame,
45
+ engageFill,
46
+ engageDone,
47
+ flowBlinkOn,
48
+ sinPulseAlpha,
49
+ type SinPulseAlphaOpts,
50
+ markTone,
51
+ stateGlyph,
52
+ directionGlyph,
53
+ defaultBodyTone,
54
+ type GlyphSpec,
55
+ type ToneKey,
56
+ } from "../pware.oc.core/pware.oc.core.glyph.js"
57
+
58
+ /** Group header before a clustered row list. */
59
+ export const GROUP_GLYPH = "▾"
60
+ /** Perf: a phase of the wall clock spent thinking. */
61
+ export const THINK_GLYPH = "∴"
62
+
63
+ /** The glyph for a My work kind — a complete spec; the tone says how urgent it is. */
64
+ export function myWorkGlyph(kind: MyWorkKind): GlyphSpec {
65
+ if (kind === QUESTION_KIND_QUESTION) return { char: "?", tone: "warning" }
66
+ if (kind === QUESTION_KIND_INTERRUPTED) return { char: "⊘", tone: "textMuted" }
67
+ if (kind === QUESTION_KIND_ERROR) return { char: "×", tone: "error" }
68
+ if (kind === MY_WORK_GROUP_SESSIONS) return { char: "◔", tone: "primary" }
69
+ if (kind === MY_WORK_GROUP_PINNED) return { char: "P", tone: "primary" }
70
+ if (kind === MY_WORK_GROUP_READY_REVIEW) return { char: "!", tone: "warning" }
71
+ if (kind === MY_WORK_GROUP_DRAFTING) return { char: "…", tone: "textMuted" }
72
+ if (kind === MY_WORK_GROUP_DRAFT_DOCS) return { char: "•", tone: "textMuted" }
73
+ if (kind === MY_WORK_GROUP_PLANS) return { char: "▸", tone: "textMuted" }
74
+ if (kind === MY_WORK_GROUP_READY_START) return { char: "▶", tone: "primary" }
75
+ if (kind === MY_WORK_GROUP_FINISHED) return { char: "✓", tone: "success" }
76
+ if (kind === MY_WORK_GROUP_DISMISSED) return { char: "⊘", tone: "textMuted" }
77
+ return { char: "!", tone: "warning" }
78
+ }
79
+
80
+ /**
81
+ * Neutral tab-header light: a small muted bullet, shown when nothing is
82
+ * waiting on the user (and for every non-My-work tab).
83
+ */
84
+ export const TAB_NEUTRAL_GLYPH: GlyphSpec = { char: "•", tone: "textMuted" }
85
+
86
+ /** One item the My-work tab light reasons about. `ended` marks a terminated part. */
87
+ export type TabAttentionItem = {
88
+ kind: MyWorkKind
89
+ /** True when the part already has a terminal end time — history, not live. */
90
+ ended?: boolean
91
+ }
92
+
93
+ /**
94
+ * The tab light's winner list, most urgent first. A live open question beats
95
+ * everything; an *ended* error is history (the panel still lists it in Errors)
96
+ * and never lights the tab; running/drafting/finished are not waiting on you.
97
+ */
98
+ const TAB_ATTENTION_PRIORITY: readonly ((i: TabAttentionItem) => boolean)[] = [
99
+ (i) => i.kind === QUESTION_KIND_QUESTION,
100
+ (i) => i.kind === QUESTION_KIND_ERROR && !i.ended,
101
+ (i) => i.kind === MY_WORK_GROUP_READY_REVIEW,
102
+ (i) => i.kind === MY_WORK_GROUP_READY_START,
103
+ (i) => i.kind === QUESTION_KIND_INTERRUPTED && !i.ended,
104
+ ]
105
+
106
+ /** The My-work tab light: the most urgent live waiting item, or a neutral bullet. */
107
+ export function tabAttentionGlyph(items: readonly TabAttentionItem[]): GlyphSpec {
108
+ for (const match of TAB_ATTENTION_PRIORITY) {
109
+ for (const item of items) {
110
+ if (match(item)) return myWorkGlyph(item.kind)
111
+ }
112
+ }
113
+ return TAB_NEUTRAL_GLYPH
114
+ }
115
+
116
+ /** A git-status letter as a glyph: added green, deleted red, modified yellow, rest muted. */
117
+ export function fileLetterGlyph(letter: FileLetter | null): GlyphSpec {
118
+ const char = letter ?? "•"
119
+ const tone =
120
+ letter === "A" ? "success" : letter === "D" ? "error" : letter === "M" ? "warning" : "textMuted"
121
+ return { char, tone }
122
+ }
123
+
124
+ /** One ulw-plan review lane: ✓ approved, ! changes, ? inconclusive, … live, · pending. */
125
+ export function reviewLaneGlyph(lane: ReviewLane | null | undefined): GlyphSpec {
126
+ const s = lane?.status
127
+ if (s === REVIEW_STATUS_APPROVED) return { char: "✓", tone: "success" }
128
+ if (s === REVIEW_STATUS_CHANGES_REQUESTED) return { char: "!", tone: "warning" }
129
+ if (s === REVIEW_STATUS_INCONCLUSIVE) return { char: "?", tone: "warning" }
130
+ if (s === REVIEW_STATUS_PENDING || !s) return { char: "·", tone: "textMuted" }
131
+ return { char: "…", tone: "textMuted" }
132
+ }
133
+
134
+ /**
135
+ * Compact review state for a plan row: `R<round> <momus><independent>` while a
136
+ * round is recorded, otherwise just the two lanes. Null when no review exists.
137
+ */
138
+ export function reviewStateSuffix(review: ReviewState | null | undefined): string | null {
139
+ if (!review || (!review.required && !review.roundId && !review.roundStatus)) return null
140
+ const lanes = `${reviewLaneGlyph(review.lanes.momus).char}${reviewLaneGlyph(review.lanes.independent).char}`
141
+ if (review.roundId) return `R${review.roundId} ${lanes}`
142
+ if (review.roundStatus === ROUND_STATUS_ACTIVE) return `R… ${lanes}`
143
+ return lanes
144
+ }
@@ -0,0 +1,111 @@
1
+ import type { TuiKeymap, TuiPluginApi } from "@opencode-ai/plugin/tui"
2
+ import { profileAsync } from "../pware.oc.core/pware.oc.core.debug.js"
3
+ import { PART_TYPE_TEXT } from "../pware.oc.core/constants/pware.oc.core.constants.partType.js"
4
+ import {
5
+ START_WORK_MAKE_PR,
6
+ START_WORK_SHIP,
7
+ } from "../pware.oc.omo/constants/pware.oc.omo.constants.startWork.js"
8
+ import { startWorkCommand, type StartWorkMode } from "../pware.oc.runtime/pware.oc.runtime.mywork.js"
9
+
10
+ export function selectSession(api: TuiPluginApi, sessionId: string | null | undefined): void {
11
+ if (!sessionId) return
12
+ const tui = (api as TuiPluginApi & {
13
+ client?: {
14
+ tui?: {
15
+ selectSession?: (arg: unknown) => Promise<unknown> | unknown
16
+ publish?: (arg: unknown) => Promise<unknown> | unknown
17
+ }
18
+ }
19
+ }).client?.tui
20
+ if (!tui) return
21
+ const go = async () => {
22
+ try {
23
+ if (typeof tui.selectSession === "function") {
24
+ await tui.selectSession({ sessionID: sessionId })
25
+ return
26
+ }
27
+ } catch {
28
+ try {
29
+ await tui.selectSession?.(sessionId)
30
+ return
31
+ } catch {
32
+ }
33
+ }
34
+ try {
35
+ await tui.publish?.({
36
+ type: "tui.session.select",
37
+ properties: { sessionID: sessionId },
38
+ })
39
+ } catch {
40
+ }
41
+ }
42
+ void profileAsync("rpc.selectSession", go)
43
+ }
44
+
45
+ export function openSessionSwitcher(api: TuiPluginApi): void {
46
+ try {
47
+ const dispatch = (api.keymap as TuiKeymap & { dispatchCommand?: (name: string) => void })
48
+ .dispatchCommand
49
+ if (typeof dispatch === "function") {
50
+ dispatch("session.list")
51
+ return
52
+ }
53
+ } catch {
54
+ }
55
+ try {
56
+ api.command?.show()
57
+ } catch {
58
+ }
59
+ }
60
+
61
+ export function openNewSessionPrompt(api: TuiPluginApi): void {
62
+ try {
63
+ api.route.navigate("home")
64
+ } catch {
65
+ }
66
+ }
67
+
68
+ export function runStartWork(
69
+ api: TuiPluginApi,
70
+ sessionId: string,
71
+ mode: StartWorkMode,
72
+ planName: string,
73
+ ): void {
74
+ const client = api.client
75
+ const text = startWorkCommand(mode, planName)
76
+ const flag = mode === START_WORK_MAKE_PR ? "--make-pr" : mode === START_WORK_SHIP ? "--ship" : ""
77
+ const args = [planName, flag].map((s) => s.trim()).filter(Boolean).join(" ") || ""
78
+ const go = async () => {
79
+ try {
80
+ const res = await client.session.command({
81
+ sessionID: sessionId,
82
+ command: "start-work",
83
+ arguments: args,
84
+ })
85
+ if (res && !res.error) return
86
+ } catch {
87
+ }
88
+ try {
89
+ await client.session.promptAsync({
90
+ sessionID: sessionId,
91
+ parts: [{ type: PART_TYPE_TEXT, text }],
92
+ })
93
+ } catch {
94
+ }
95
+ }
96
+ void profileAsync("rpc.startWork", go)
97
+ }
98
+
99
+ export function approvePlan(api: TuiPluginApi, sessionId: string): void {
100
+ const client = api.client
101
+ const go = async () => {
102
+ try {
103
+ await client.session.promptAsync({
104
+ sessionID: sessionId,
105
+ parts: [{ type: PART_TYPE_TEXT, text: "ok" }],
106
+ })
107
+ } catch {
108
+ }
109
+ }
110
+ void profileAsync("rpc.approve", go)
111
+ }
@@ -0,0 +1,82 @@
1
+ import type { TuiPluginApi } from "@opencode-ai/plugin/tui"
2
+ import type { PwareEventBus } from "../pware.oc.core/pware.oc.core.bus.js"
3
+ import { eventType, shouldRefreshDb } from "../pware.oc.core/pware.oc.core.events.js"
4
+ import {
5
+ EVENT_TUI_SESSION_SELECT,
6
+ PANEL_HOST_TYPES,
7
+ } from "../pware.oc.core/constants/pware.oc.core.constants.eventType.js"
8
+ import {
9
+ EV_OES_QUESTION_HINT,
10
+ EV_OES_REFRESH_HINT,
11
+ EV_OES_SESSION_SELECT,
12
+ } from "../pware.oc.core/constants/pware.oc.core.constants.eventName.js"
13
+ import { profile } from "../pware.oc.core/pware.oc.core.debug.js"
14
+ import {
15
+ hostEventToOcEvents,
16
+ questionSessionFromEvent,
17
+ } from "../pware.oc.opencode/pware.oc.opencode.events.js"
18
+
19
+ export type HostEventBridgeOptions = {
20
+ api: TuiPluginApi
21
+ bus: PwareEventBus
22
+ sessionId: () => string
23
+ projectRoot: () => string | null
24
+ onRender: () => void
25
+ }
26
+
27
+ function normalizeEvent(type: string, raw: unknown): unknown {
28
+ if (raw && typeof raw === "object") {
29
+ const obj = raw as Record<string, unknown>
30
+ if (typeof obj.type === "string" && obj.type) return raw
31
+ return { type, properties: raw }
32
+ }
33
+ return { type, properties: raw ?? {} }
34
+ }
35
+
36
+ export function startHostEventBridge(opts: HostEventBridgeOptions): { stop: () => void } {
37
+ const offs: Array<() => void> = []
38
+ const on = opts.api.event.on as (name: string, cb: (...args: unknown[]) => void) => unknown
39
+
40
+ for (const type of PANEL_HOST_TYPES) {
41
+ try {
42
+ const off = on(type, (...args: unknown[]) => {
43
+ profile(
44
+ "event",
45
+ () => {
46
+ const evt = normalizeEvent(type, args[0])
47
+ for (const e of hostEventToOcEvents(evt, {
48
+ sessionId: opts.sessionId(),
49
+ projectRoot: opts.projectRoot(),
50
+ })) {
51
+ opts.bus.emit(e)
52
+ }
53
+ const qsid = questionSessionFromEvent(evt)
54
+ if (qsid) {
55
+ opts.bus.emit({ type: EV_OES_QUESTION_HINT, ts: Date.now(), data: { sessionId: qsid } })
56
+ }
57
+ if (shouldRefreshDb(eventType(evt))) {
58
+ opts.bus.emit({ type: EV_OES_REFRESH_HINT, ts: Date.now(), data: {} })
59
+ }
60
+ if (type === EVENT_TUI_SESSION_SELECT) {
61
+ opts.bus.emit({
62
+ type: EV_OES_SESSION_SELECT,
63
+ ts: Date.now(),
64
+ data: { sessionId: opts.sessionId() },
65
+ })
66
+ }
67
+ queueMicrotask(opts.onRender)
68
+ },
69
+ { type: eventType(args[0]) || undefined },
70
+ )
71
+ })
72
+ if (typeof off === "function") offs.push(off as () => void)
73
+ } catch {
74
+ }
75
+ }
76
+
77
+ return {
78
+ stop: () => {
79
+ for (const off of offs) off()
80
+ },
81
+ }
82
+ }