opencode-extended-sidebar 0.2.90
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.oesignore +17 -0
- package/CHANGELOG.md +95 -0
- package/LICENSE +21 -0
- package/README.md +235 -0
- package/oes.json +11 -0
- package/package.json +100 -0
- package/scripts/install-git-hooks.mjs +29 -0
- package/src/pware.oc.core/constants/index.ts +18 -0
- package/src/pware.oc.core/constants/pware.oc.core.constants.eventKind.ts +29 -0
- package/src/pware.oc.core/constants/pware.oc.core.constants.eventName.ts +13 -0
- package/src/pware.oc.core/constants/pware.oc.core.constants.eventType.ts +241 -0
- package/src/pware.oc.core/constants/pware.oc.core.constants.myWork.ts +55 -0
- package/src/pware.oc.core/constants/pware.oc.core.constants.partType.ts +65 -0
- package/src/pware.oc.core/constants/pware.oc.core.constants.phase.ts +58 -0
- package/src/pware.oc.core/constants/pware.oc.core.constants.pulse.ts +63 -0
- package/src/pware.oc.core/constants/pware.oc.core.constants.rowKind.ts +34 -0
- package/src/pware.oc.core/constants/pware.oc.core.constants.status.ts +74 -0
- package/src/pware.oc.core/constants/pware.oc.core.constants.toolName.ts +69 -0
- package/src/pware.oc.core/git/index.ts +7 -0
- package/src/pware.oc.core/git/pware.oc.core.git.ts +237 -0
- package/src/pware.oc.core/git/pware.oc.core.gitignore.ts +87 -0
- package/src/pware.oc.core/index.ts +18 -0
- package/src/pware.oc.core/pware.oc.core.bus.ts +42 -0
- package/src/pware.oc.core/pware.oc.core.cache.ts +48 -0
- package/src/pware.oc.core/pware.oc.core.clipboard.ts +97 -0
- package/src/pware.oc.core/pware.oc.core.debug.ts +280 -0
- package/src/pware.oc.core/pware.oc.core.events.ts +79 -0
- package/src/pware.oc.core/pware.oc.core.glyph.ts +135 -0
- package/src/pware.oc.core/pware.oc.core.layout.ts +234 -0
- package/src/pware.oc.core/pware.oc.core.oes.ts +89 -0
- package/src/pware.oc.core/pware.oc.core.paths.ts +240 -0
- package/src/pware.oc.core/pware.oc.core.preview.ts +74 -0
- package/src/pware.oc.core/pware.oc.core.pulse.ts +726 -0
- package/src/pware.oc.core/pware.oc.core.sqlite.ts +175 -0
- package/src/pware.oc.core/pware.oc.core.status.ts +214 -0
- package/src/pware.oc.core/pware.oc.core.timing.ts +63 -0
- package/src/pware.oc.core/pware.oc.core.width.ts +215 -0
- package/src/pware.oc.omo/constants/index.ts +15 -0
- package/src/pware.oc.omo/constants/pware.oc.omo.constants.backgroundTask.ts +18 -0
- package/src/pware.oc.omo/constants/pware.oc.omo.constants.boulderStatus.ts +49 -0
- package/src/pware.oc.omo/constants/pware.oc.omo.constants.docKind.ts +29 -0
- package/src/pware.oc.omo/constants/pware.oc.omo.constants.eventName.ts +11 -0
- package/src/pware.oc.omo/constants/pware.oc.omo.constants.planStatus.ts +60 -0
- package/src/pware.oc.omo/constants/pware.oc.omo.constants.reviewStatus.ts +49 -0
- package/src/pware.oc.omo/constants/pware.oc.omo.constants.startWork.ts +25 -0
- package/src/pware.oc.omo/constants/pware.oc.omo.constants.verdict.ts +38 -0
- package/src/pware.oc.omo/index.ts +7 -0
- package/src/pware.oc.omo/resolver/index.ts +19 -0
- package/src/pware.oc.omo/resolver/pware.oc.omo.resolver.approval.ts +179 -0
- package/src/pware.oc.omo/resolver/pware.oc.omo.resolver.approvalGroup.ts +116 -0
- package/src/pware.oc.omo/resolver/pware.oc.omo.resolver.approvalState.ts +34 -0
- package/src/pware.oc.omo/resolver/pware.oc.omo.resolver.boulder.ts +503 -0
- package/src/pware.oc.omo/resolver/pware.oc.omo.resolver.config.ts +40 -0
- package/src/pware.oc.omo/resolver/pware.oc.omo.resolver.doc.ts +269 -0
- package/src/pware.oc.omo/resolver/pware.oc.omo.resolver.draftFile.ts +18 -0
- package/src/pware.oc.omo/resolver/pware.oc.omo.resolver.notepadsFile.ts +18 -0
- package/src/pware.oc.omo/resolver/pware.oc.omo.resolver.plan.ts +155 -0
- package/src/pware.oc.omo/resolver/pware.oc.omo.resolver.planFile.ts +361 -0
- package/src/pware.oc.omo/resolver/pware.oc.omo.resolver.proofFile.ts +18 -0
- package/src/pware.oc.omo/resolver/pware.oc.omo.resolver.rulesFile.ts +11 -0
- package/src/pware.oc.omo/resolver/pware.oc.omo.resolver.runContinuationFile.ts +14 -0
- package/src/pware.oc.opencode/constants/index.ts +10 -0
- package/src/pware.oc.opencode/constants/pware.oc.opencode.constants.eventName.ts +15 -0
- package/src/pware.oc.opencode/constants/pware.oc.opencode.constants.fileTouch.ts +18 -0
- package/src/pware.oc.opencode/constants/pware.oc.opencode.constants.questionKind.ts +45 -0
- package/src/pware.oc.opencode/constants/pware.oc.opencode.constants.sessionStatus.ts +58 -0
- package/src/pware.oc.opencode/index.ts +8 -0
- package/src/pware.oc.opencode/pware.oc.opencode.events.ts +182 -0
- package/src/pware.oc.opencode/pware.oc.opencode.files.ts +399 -0
- package/src/pware.oc.opencode/resolver/index.ts +170 -0
- package/src/pware.oc.opencode/resolver/pware.oc.opencode.resolver.file.ts +162 -0
- package/src/pware.oc.opencode/resolver/pware.oc.opencode.resolver.question.ts +185 -0
- package/src/pware.oc.opencode/resolver/pware.oc.opencode.resolver.session.ts +211 -0
- package/src/pware.oc.opencode/resolver/pware.oc.opencode.resolver.todo.ts +26 -0
- package/src/pware.oc.opencode/resolver/pware.oc.opencode.resolver.tool.ts +236 -0
- package/src/pware.oc.perf/index.ts +14 -0
- package/src/pware.oc.perf/pware.oc.perf.asciichart.d.ts +15 -0
- package/src/pware.oc.perf/pware.oc.perf.charts.ts +284 -0
- package/src/pware.oc.perf/pware.oc.perf.reader.ts +927 -0
- package/src/pware.oc.perf/pware.oc.perf.realtime.ts +88 -0
- package/src/pware.oc.perf/pware.oc.perf.realtimeBlock.ts +118 -0
- package/src/pware.oc.perf/pware.oc.perf.realtimeCpuRam.ts +79 -0
- package/src/pware.oc.perf/pware.oc.perf.realtimeSampler.ts +71 -0
- package/src/pware.oc.perf/pware.oc.perf.realtimeTimeline.ts +214 -0
- package/src/pware.oc.perf/pware.oc.perf.self.ts +179 -0
- package/src/pware.oc.perf/pware.oc.perf.view.tsx +462 -0
- package/src/pware.oc.runtime/index.ts +13 -0
- package/src/pware.oc.runtime/pware.oc.runtime.monitor.ts +142 -0
- package/src/pware.oc.runtime/pware.oc.runtime.mywork-enrich.ts +79 -0
- package/src/pware.oc.runtime/pware.oc.runtime.mywork.ts +253 -0
- package/src/pware.oc.runtime/pware.oc.runtime.questions.ts +95 -0
- package/src/pware.oc.runtime/pware.oc.runtime.snapshotClient.ts +129 -0
- package/src/pware.oc.runtime/pware.oc.runtime.source.ts +105 -0
- package/src/pware.oc.runtime/pware.oc.runtime.worker.ts +43 -0
- package/src/pware.oc.runtime/resolver/index.ts +224 -0
- package/src/pware.oc.runtime/resolver/pware.oc.runtime.resolver.delegate.ts +146 -0
- package/src/pware.oc.ui/index.ts +12 -0
- package/src/pware.oc.ui/pware.oc.ui.chrome.tsx +339 -0
- package/src/pware.oc.ui/pware.oc.ui.glyphs.tsx +144 -0
- package/src/pware.oc.ui/pware.oc.ui.host.tsx +111 -0
- package/src/pware.oc.ui/pware.oc.ui.live.tsx +82 -0
- package/src/pware.oc.ui/pware.oc.ui.menudialogs.tsx +843 -0
- package/src/pware.oc.ui/pware.oc.ui.sections.tsx +526 -0
- package/src/pware.oc.ui/pware.oc.ui.sidebar.tsx +1629 -0
- package/src/pware.oc.ui.tsx +98 -0
|
@@ -0,0 +1,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
|
+
}
|