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,526 @@
|
|
|
1
|
+
/** @jsxImportSource @opentui/solid */
|
|
2
|
+
/**
|
|
3
|
+
* pware.oc.ui.sections
|
|
4
|
+
*
|
|
5
|
+
* Shared sidebar primitives on top of chrome: kv-persisted fold state, the
|
|
6
|
+
* foldable section scaffold every tab reuses, the base row renderer and its
|
|
7
|
+
* budget-sliced list (`RowList`), and the brand + tabs + active-panel column.
|
|
8
|
+
*
|
|
9
|
+
* Row budgets stay in the caller — these components render, they do not
|
|
10
|
+
* re-decide how many rows fit (`layout.packSections` still owns that).
|
|
11
|
+
*/
|
|
12
|
+
import { createMemo, createSignal, Show, For, type Accessor, type JSX } from "solid-js"
|
|
13
|
+
import type { TuiPluginApi } from "@opencode-ai/plugin/tui"
|
|
14
|
+
import {
|
|
15
|
+
BrandTabs,
|
|
16
|
+
ClickText,
|
|
17
|
+
DiffStat,
|
|
18
|
+
FoldHeader,
|
|
19
|
+
kvRead,
|
|
20
|
+
kvWrite,
|
|
21
|
+
toneColor,
|
|
22
|
+
type ContextAction,
|
|
23
|
+
type ThemeColors,
|
|
24
|
+
} from "./pware.oc.ui.chrome.js"
|
|
25
|
+
import {
|
|
26
|
+
defaultBodyTone,
|
|
27
|
+
directionGlyph,
|
|
28
|
+
sinPulseAlpha,
|
|
29
|
+
spinnerFrame,
|
|
30
|
+
stateGlyph,
|
|
31
|
+
type GlyphSpec,
|
|
32
|
+
type ToneKey,
|
|
33
|
+
} from "./pware.oc.ui.glyphs.js"
|
|
34
|
+
import { profile } from "../pware.oc.core/pware.oc.core.debug.js"
|
|
35
|
+
import { moreRevealVisible, sliceShown } from "../pware.oc.core/pware.oc.core.layout.js"
|
|
36
|
+
import { clipWidth, strWidth } from "../pware.oc.core/pware.oc.core.width.js"
|
|
37
|
+
import { oesBarParts, statusBarLine, type TabStatus } from "../pware.oc.core/pware.oc.core.status.js"
|
|
38
|
+
import {
|
|
39
|
+
formatTokens,
|
|
40
|
+
formatUsd,
|
|
41
|
+
type AgentMark,
|
|
42
|
+
type FlowDir,
|
|
43
|
+
} from "../pware.oc.core/pware.oc.core.pulse.js"
|
|
44
|
+
import {
|
|
45
|
+
ROW_KIND_AGENT,
|
|
46
|
+
ROW_KIND_FILE,
|
|
47
|
+
ROW_KIND_GROUP,
|
|
48
|
+
type RowKind,
|
|
49
|
+
} from "../pware.oc.core/constants/pware.oc.core.constants.rowKind.js"
|
|
50
|
+
import { MARK_READY } from "../pware.oc.core/constants/pware.oc.core.constants.pulse.js"
|
|
51
|
+
import { formatDiffStat, shortFileName } from "../pware.oc.opencode/pware.oc.opencode.files.js"
|
|
52
|
+
|
|
53
|
+
export type { RowKind }
|
|
54
|
+
|
|
55
|
+
/** Fold state persisted in the host kv store. `open` is true when unfolded. */
|
|
56
|
+
export function useFold(
|
|
57
|
+
api: TuiPluginApi,
|
|
58
|
+
key: string,
|
|
59
|
+
opts?: { after?: () => void; defaultOpen?: boolean },
|
|
60
|
+
): { open: Accessor<boolean>; toggle: () => void } {
|
|
61
|
+
// The kv store keeps the legacy "folded" flag (true = folded), so existing
|
|
62
|
+
// user state survives the refactor; `open` is its inverse. `defaultOpen`
|
|
63
|
+
// decides the state when the user has never toggled this fold (no kv entry) —
|
|
64
|
+
// e.g. the My work "Errors" group starts collapsed.
|
|
65
|
+
const defaultOpen = opts?.defaultOpen ?? true
|
|
66
|
+
const [open, setOpen] = createSignal(defaultOpen ? !kvRead(api, key, false) : kvRead(api, key, true))
|
|
67
|
+
const toggle = (): void => {
|
|
68
|
+
setOpen((prev) => {
|
|
69
|
+
const next = !prev
|
|
70
|
+
kvWrite(api, key, !next)
|
|
71
|
+
return next
|
|
72
|
+
})
|
|
73
|
+
opts?.after?.()
|
|
74
|
+
}
|
|
75
|
+
return { open, toggle }
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Reveal state for a `RowList` "… +N more" line. */
|
|
79
|
+
export type RevealState = { more: Accessor<number>; reveal: () => void }
|
|
80
|
+
|
|
81
|
+
/** Rows revealed per click of a "… +N more" line. */
|
|
82
|
+
export function useReveal(step: number): RevealState {
|
|
83
|
+
const [more, setMore] = createSignal(0)
|
|
84
|
+
return { more, reveal: () => setMore((n) => n + step) }
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** The inline trend is a background hint, and so is a reading of zero. */
|
|
88
|
+
const RATE_BAR_OPACITY = 0.3
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* The global OES status bar: `OES {glyph} {label}` or, when quiet,
|
|
92
|
+
* `OES {8-col bar} {tok/s}`. The trend always renders at
|
|
93
|
+
* {@link RATE_BAR_OPACITY}, and an idle `0 tok/s` fades with it — only a rate
|
|
94
|
+
* that is actually moving gets full contrast. Loading reads `glyphFrame` only
|
|
95
|
+
* in this leaf.
|
|
96
|
+
*/
|
|
97
|
+
export function OesStatusRow(props: {
|
|
98
|
+
status: TabStatus
|
|
99
|
+
colors: ThemeColors
|
|
100
|
+
glyphFrame: () => number
|
|
101
|
+
/** 8-column bar from the live token-rate window — dropped while a status label shows. */
|
|
102
|
+
rateBar?: string
|
|
103
|
+
/** Live tokens per second; `0` / `null` render faded, `undefined` hides the reading. */
|
|
104
|
+
rate?: number | null
|
|
105
|
+
}): JSX.Element {
|
|
106
|
+
const line = createMemo(() => statusBarLine(props.status))
|
|
107
|
+
const glyph = createMemo(() => {
|
|
108
|
+
if (line().tone === "loading") return spinnerFrame(props.glyphFrame())
|
|
109
|
+
if (line().tone === "error") return "×"
|
|
110
|
+
return line().label ? "•" : ""
|
|
111
|
+
})
|
|
112
|
+
const fg = createMemo(() =>
|
|
113
|
+
line().tone === "error"
|
|
114
|
+
? props.colors.error || props.colors.text
|
|
115
|
+
: line().tone === "loading"
|
|
116
|
+
? props.colors.primary || props.colors.text
|
|
117
|
+
: props.colors.text,
|
|
118
|
+
)
|
|
119
|
+
const parts = createMemo(() => oesBarParts(line().label, glyph(), props.rateBar, props.rate))
|
|
120
|
+
return (
|
|
121
|
+
<box flexDirection="row">
|
|
122
|
+
<text fg={fg()}>{parts().head}</text>
|
|
123
|
+
<Show when={parts().bar}>
|
|
124
|
+
<text fg={fg()} opacity={RATE_BAR_OPACITY}>
|
|
125
|
+
{parts().bar}
|
|
126
|
+
</text>
|
|
127
|
+
</Show>
|
|
128
|
+
<Show when={parts().rate}>
|
|
129
|
+
<text fg={fg()} opacity={parts().rateIdle ? RATE_BAR_OPACITY : 1}>
|
|
130
|
+
{parts().rate}
|
|
131
|
+
</text>
|
|
132
|
+
</Show>
|
|
133
|
+
</box>
|
|
134
|
+
)
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** Clickable truncation line: "… +N more", or "… less" in expand-all (toggle) mode. */
|
|
138
|
+
export function MoreReveal(props: {
|
|
139
|
+
hidden: number
|
|
140
|
+
colors: ThemeColors
|
|
141
|
+
onReveal: () => void
|
|
142
|
+
/** Expand-all mode: true keeps the line visible as "… less" after expanding. */
|
|
143
|
+
expanded?: boolean
|
|
144
|
+
onToggle?: () => void
|
|
145
|
+
}): JSX.Element | null {
|
|
146
|
+
return (
|
|
147
|
+
<Show when={moreRevealVisible(props.hidden, props.expanded)}>
|
|
148
|
+
<Show
|
|
149
|
+
when={props.expanded}
|
|
150
|
+
fallback={
|
|
151
|
+
<box onMouseUp={props.onReveal}>
|
|
152
|
+
<ClickText fg={props.colors.textMuted} underline>
|
|
153
|
+
{`… +${props.hidden} more`}
|
|
154
|
+
</ClickText>
|
|
155
|
+
</box>
|
|
156
|
+
}
|
|
157
|
+
>
|
|
158
|
+
<box onMouseUp={props.onToggle}>
|
|
159
|
+
<ClickText fg={props.colors.textMuted} underline>
|
|
160
|
+
… less
|
|
161
|
+
</ClickText>
|
|
162
|
+
</box>
|
|
163
|
+
</Show>
|
|
164
|
+
</Show>
|
|
165
|
+
)
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/** What a "… +N more" / "… less" line should do. */
|
|
169
|
+
export type RowListMore = {
|
|
170
|
+
onReveal: () => void
|
|
171
|
+
expanded?: boolean
|
|
172
|
+
onToggle?: () => void
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* The one place a row list is cut to a budget and the revealer is drawn —
|
|
177
|
+
* `sliceShown` never spends a content row on the note. Every list in the panel
|
|
178
|
+
* (sections, groups, OMO, Perf) renders through here.
|
|
179
|
+
*/
|
|
180
|
+
export function RowList<T>(props: {
|
|
181
|
+
items: readonly T[]
|
|
182
|
+
/** Content rows shown before the "… +N more" revealer. */
|
|
183
|
+
budget: number
|
|
184
|
+
colors: ThemeColors
|
|
185
|
+
renderItem: (item: T, index: Accessor<number>) => JSX.Element
|
|
186
|
+
more?: RowListMore
|
|
187
|
+
}): JSX.Element {
|
|
188
|
+
const cut = () => sliceShown(props.items, props.budget)
|
|
189
|
+
return (
|
|
190
|
+
<>
|
|
191
|
+
<For each={cut().rows}>{(item, i) => props.renderItem(item, i)}</For>
|
|
192
|
+
<MoreReveal
|
|
193
|
+
hidden={cut().hidden}
|
|
194
|
+
colors={props.colors}
|
|
195
|
+
onReveal={props.more?.onReveal ?? (() => {})}
|
|
196
|
+
expanded={props.more?.expanded}
|
|
197
|
+
onToggle={props.more?.onToggle}
|
|
198
|
+
/>
|
|
199
|
+
</>
|
|
200
|
+
)
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
export type RowData = {
|
|
204
|
+
kind: RowKind
|
|
205
|
+
/** Live state source — drives the default state glyph and body tone. */
|
|
206
|
+
mark?: AgentMark
|
|
207
|
+
name: string
|
|
208
|
+
/** Overrides the primary glyph entirely (char + tone). */
|
|
209
|
+
glyph?: GlyphSpec
|
|
210
|
+
/** Future tertiary slot, rendered after the direction column. */
|
|
211
|
+
glyph2?: GlyphSpec
|
|
212
|
+
tokens?: number | null
|
|
213
|
+
cost?: number | null
|
|
214
|
+
title?: string
|
|
215
|
+
/** Right-side trailing text — status, age, review; rendered faded. */
|
|
216
|
+
suffix?: string
|
|
217
|
+
/** Muted second line drawn under the row — e.g. a question's dismissal/error reason. */
|
|
218
|
+
subline?: string
|
|
219
|
+
diff?: { additions: number; deletions: number }
|
|
220
|
+
current?: boolean
|
|
221
|
+
flow?: FlowDir | null
|
|
222
|
+
/** Reserve the direction column so busy and idle rows align. */
|
|
223
|
+
dirSlot?: boolean
|
|
224
|
+
/** Indent one glyph column — a nested child under a group header. */
|
|
225
|
+
indent?: boolean
|
|
226
|
+
/** Override the body text tone (defaults from `defaultBodyTone`). */
|
|
227
|
+
bodyTone?: ToneKey | null
|
|
228
|
+
/** A clickable trailing link (e.g. a pin affordance) at the far right. */
|
|
229
|
+
link?: { label: string; onPick: () => void }
|
|
230
|
+
onSelect?: () => void
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
export function agentDisplayName(name: string): string {
|
|
234
|
+
return (name || "agent").replace(/\s*-\s*/g, " ").trim() || "agent"
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function shortName(name: string, kind: RowKind, max: number): string {
|
|
238
|
+
if (kind === ROW_KIND_FILE) return shortFileName(name, max)
|
|
239
|
+
if (kind === ROW_KIND_AGENT || kind === ROW_KIND_GROUP) return clipWidth(agentDisplayName(name), max)
|
|
240
|
+
return clipWidth(name || "agent", max)
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
export type ComposedRow = {
|
|
244
|
+
/** Identity + meta (+ optional title), clipped once to the available room. */
|
|
245
|
+
body: string
|
|
246
|
+
/** Trailing text (status/age/review), clipped to its own right budget. */
|
|
247
|
+
suffix: string
|
|
248
|
+
/** True when the body was clipped — the caller renders it faded. */
|
|
249
|
+
truncated: boolean
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/** Fraction of the line reserved for the right-side suffix; the name never outruns it. */
|
|
253
|
+
const SUFFIX_BUDGET_FRACTION = 0.4
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Compose one row line from its parts against a column budget. The trailing
|
|
257
|
+
* suffix wins a capped share of the line (`SUFFIX_BUDGET_FRACTION`) so a long
|
|
258
|
+
* status can't starve the name; the name and suffix are each clipped to their
|
|
259
|
+
* own room with a terminal `…`. Pure — unit-tested directly.
|
|
260
|
+
*/
|
|
261
|
+
export function composeRow(
|
|
262
|
+
opts: Pick<
|
|
263
|
+
RowData,
|
|
264
|
+
"name" | "kind" | "tokens" | "cost" | "title" | "suffix" | "diff"
|
|
265
|
+
>,
|
|
266
|
+
lineMax: number,
|
|
267
|
+
): ComposedRow {
|
|
268
|
+
const max = Math.max(0, lineMax - 2)
|
|
269
|
+
const suffixMax = Math.max(0, Math.floor(max * SUFFIX_BUDGET_FRACTION))
|
|
270
|
+
const suffix = clipWidth(opts.suffix?.trim() ?? "", suffixMax)
|
|
271
|
+
const diffW = opts.diff
|
|
272
|
+
? strWidth(formatDiffStat(opts.diff.additions, opts.diff.deletions))
|
|
273
|
+
: 0
|
|
274
|
+
const extra = (suffix ? strWidth(suffix) + 1 : 0) + (diffW ? diffW + 1 : 0)
|
|
275
|
+
const room = Math.max(0, max - extra)
|
|
276
|
+
const tok = opts.tokens === undefined ? "" : formatTokens(opts.tokens)
|
|
277
|
+
const usd = formatUsd(opts.cost)
|
|
278
|
+
const meta = [tok, usd].filter(Boolean).join(" ")
|
|
279
|
+
const metaW = meta ? strWidth(meta) + 1 : 0
|
|
280
|
+
const title = opts.title?.replace(/\s+/g, " ").trim()
|
|
281
|
+
const nameBudget = title
|
|
282
|
+
? Math.max(4, Math.floor((room - metaW) / 2))
|
|
283
|
+
: Math.max(1, room - metaW)
|
|
284
|
+
const agent = shortName(opts.name, opts.kind, nameBudget)
|
|
285
|
+
const head = [agent, tok, usd].filter(Boolean).join(" ")
|
|
286
|
+
const titleDropped = Boolean(title) && room - strWidth(head) < 9
|
|
287
|
+
const full = !title || titleDropped ? head : `${head} ${title}`
|
|
288
|
+
const body = clipWidth(full, room)
|
|
289
|
+
// The name is clipped inside `shortName` (middle/end `…`); a dropped title is
|
|
290
|
+
// the other truncation path. Either way the caller fades the row.
|
|
291
|
+
const truncated = agent.includes("…") || titleDropped
|
|
292
|
+
return { body, suffix, truncated }
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/** Single renderer for every sidebar row: glyph(s), name, tokens, title, suffix, diff. */
|
|
296
|
+
export function AgentLine(props: RowData & {
|
|
297
|
+
lineMax: number
|
|
298
|
+
/** Tick accessor — read only inside the glyph bindings so rows are not rebuilt per tick. */
|
|
299
|
+
frame?: () => number
|
|
300
|
+
/** Fast glyph tick — spinners and direction flows step at `GLYPH_TICK_MS`. */
|
|
301
|
+
glyphFrame?: () => number
|
|
302
|
+
colors: ThemeColors
|
|
303
|
+
}): JSX.Element {
|
|
304
|
+
const mark = () => props.mark ?? MARK_READY
|
|
305
|
+
const frame = () => props.glyphFrame?.() ?? props.frame?.() ?? 0
|
|
306
|
+
const primary = () => props.glyph ?? stateGlyph(mark(), frame())
|
|
307
|
+
const dir = () =>
|
|
308
|
+
props.dirSlot
|
|
309
|
+
? directionGlyph(props.flow ?? null) ?? { char: " ", tone: "textMuted" as ToneKey }
|
|
310
|
+
: null
|
|
311
|
+
const primaryTone = (): ToneKey => {
|
|
312
|
+
const spec = primary()
|
|
313
|
+
const tone = spec.tone
|
|
314
|
+
if (props.glyph) return tone
|
|
315
|
+
return props.current && tone === "textMuted" ? "primary" : tone
|
|
316
|
+
}
|
|
317
|
+
const dirOpacity = () => {
|
|
318
|
+
const spec = dir()
|
|
319
|
+
if (!spec?.blink) return 1
|
|
320
|
+
return sinPulseAlpha(props.frame?.() ?? 0)
|
|
321
|
+
}
|
|
322
|
+
const dirFg = () => {
|
|
323
|
+
const spec = dir()
|
|
324
|
+
if (!spec) return undefined
|
|
325
|
+
return toneColor(spec.tone, props.colors)
|
|
326
|
+
}
|
|
327
|
+
const bodyTone = () => props.bodyTone ?? defaultBodyTone(props.kind, props.mark, Boolean(props.current))
|
|
328
|
+
const row = () => composeRow(props, props.lineMax)
|
|
329
|
+
return profile("row", () => (
|
|
330
|
+
<box flexDirection="column" gap={0}>
|
|
331
|
+
<box flexDirection="row" onMouseUp={props.onSelect}>
|
|
332
|
+
<Show when={props.indent}>
|
|
333
|
+
<text>{` `}</text>
|
|
334
|
+
</Show>
|
|
335
|
+
<text fg={toneColor(primaryTone(), props.colors)}>{`${primary().char} `}</text>
|
|
336
|
+
<Show when={props.dirSlot}>
|
|
337
|
+
<text opacity={dirOpacity()} fg={dirFg()}>{`${dir()?.char ?? " "} `}</text>
|
|
338
|
+
</Show>
|
|
339
|
+
<Show when={props.glyph2}>
|
|
340
|
+
<text fg={toneColor(props.glyph2!.tone, props.colors)}>{`${props.glyph2!.char} `}</text>
|
|
341
|
+
</Show>
|
|
342
|
+
<ClickText
|
|
343
|
+
fg={toneColor(bodyTone(), props.colors)}
|
|
344
|
+
bold={Boolean(props.current)}
|
|
345
|
+
underline={Boolean(props.onSelect)}
|
|
346
|
+
opacity={row().truncated ? 0.5 : undefined}
|
|
347
|
+
>
|
|
348
|
+
{row().body}
|
|
349
|
+
</ClickText>
|
|
350
|
+
<Show when={row().suffix}>
|
|
351
|
+
<text fg={props.colors.textMuted} opacity={0.5}>
|
|
352
|
+
{` ${row().suffix}`}
|
|
353
|
+
</text>
|
|
354
|
+
</Show>
|
|
355
|
+
<Show when={Boolean(props.diff && (props.diff.additions > 0 || props.diff.deletions > 0))}>
|
|
356
|
+
<text> </text>
|
|
357
|
+
<DiffStat
|
|
358
|
+
additions={props.diff?.additions ?? 0}
|
|
359
|
+
deletions={props.diff?.deletions ?? 0}
|
|
360
|
+
colors={props.colors}
|
|
361
|
+
/>
|
|
362
|
+
</Show>
|
|
363
|
+
<Show when={props.link}>
|
|
364
|
+
<box flexGrow={1} />
|
|
365
|
+
<ClickText fg={props.colors.primary || props.colors.text} underline onMouseUp={props.link!.onPick}>
|
|
366
|
+
{props.link!.label}
|
|
367
|
+
</ClickText>
|
|
368
|
+
</Show>
|
|
369
|
+
</box>
|
|
370
|
+
<Show when={props.subline}>
|
|
371
|
+
<text fg={props.colors.textMuted}>{` ${clipWidth(props.subline ?? "", Math.max(0, props.lineMax - 2))}`}</text>
|
|
372
|
+
</Show>
|
|
373
|
+
</box>
|
|
374
|
+
))
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/**
|
|
378
|
+
* Fold header + padded content box. The `open`/`onToggle` pair usually comes
|
|
379
|
+
* from `useFold`; the content is whatever rows the section owns.
|
|
380
|
+
*/
|
|
381
|
+
export function FoldSection(props: {
|
|
382
|
+
title: string
|
|
383
|
+
open: boolean
|
|
384
|
+
onToggle: () => void
|
|
385
|
+
count?: number
|
|
386
|
+
live?: number
|
|
387
|
+
countLabel?: string
|
|
388
|
+
suffix?: string
|
|
389
|
+
diff?: { additions: number; deletions: number }
|
|
390
|
+
actions?: readonly ContextAction[]
|
|
391
|
+
onDetail?: () => void
|
|
392
|
+
colors: ThemeColors
|
|
393
|
+
children: JSX.Element
|
|
394
|
+
}): JSX.Element {
|
|
395
|
+
return (
|
|
396
|
+
<box flexDirection="column" gap={0}>
|
|
397
|
+
<FoldHeader
|
|
398
|
+
title={props.title}
|
|
399
|
+
open={props.open}
|
|
400
|
+
count={props.count}
|
|
401
|
+
live={props.live}
|
|
402
|
+
countLabel={props.countLabel}
|
|
403
|
+
suffix={props.suffix}
|
|
404
|
+
diff={props.diff}
|
|
405
|
+
actions={props.actions}
|
|
406
|
+
onDetail={props.onDetail}
|
|
407
|
+
colors={props.colors}
|
|
408
|
+
onToggle={props.onToggle}
|
|
409
|
+
/>
|
|
410
|
+
<Show when={props.open}>
|
|
411
|
+
<box flexDirection="column" gap={0} paddingLeft={1}>
|
|
412
|
+
{props.children}
|
|
413
|
+
</box>
|
|
414
|
+
</Show>
|
|
415
|
+
</box>
|
|
416
|
+
)
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
/**
|
|
420
|
+
* A foldable data group: a FoldSection whose content is a `RowList` cut to a
|
|
421
|
+
* budget. The budget holds content rows only — the header is counted in the
|
|
422
|
+
* caller's `fixed` rows. Each group owns its own "… +N more" revealer, or the
|
|
423
|
+
* caller hands one in via `reveal` so the state survives `<For>` reconciliation
|
|
424
|
+
* (My work and Docs re-scan on a clock, so their groups are recreated each tick).
|
|
425
|
+
*/
|
|
426
|
+
export function GroupSection<T>(props: {
|
|
427
|
+
title: string
|
|
428
|
+
open: boolean
|
|
429
|
+
onToggle: () => void
|
|
430
|
+
colors: ThemeColors
|
|
431
|
+
items: readonly T[]
|
|
432
|
+
budget: number
|
|
433
|
+
renderItem: (item: T, index: Accessor<number>) => JSX.Element
|
|
434
|
+
/** Rows revealed per click of the "… +N more" line. Defaults to `budget`. */
|
|
435
|
+
revealStep?: number
|
|
436
|
+
/** Hoisted reveal state — used instead of an internal one when provided. */
|
|
437
|
+
reveal?: RevealState
|
|
438
|
+
/** Context actions in the shared right rail. */
|
|
439
|
+
actions?: readonly ContextAction[]
|
|
440
|
+
}): JSX.Element {
|
|
441
|
+
const reveal = props.reveal ?? useReveal(Math.max(1, props.revealStep ?? props.budget))
|
|
442
|
+
return (
|
|
443
|
+
<FoldSection
|
|
444
|
+
title={props.title}
|
|
445
|
+
open={props.open}
|
|
446
|
+
onToggle={props.onToggle}
|
|
447
|
+
count={props.items.length}
|
|
448
|
+
actions={props.actions}
|
|
449
|
+
colors={props.colors}
|
|
450
|
+
>
|
|
451
|
+
<RowList
|
|
452
|
+
items={props.items}
|
|
453
|
+
budget={props.budget + reveal.more()}
|
|
454
|
+
colors={props.colors}
|
|
455
|
+
renderItem={props.renderItem}
|
|
456
|
+
more={{ onReveal: reveal.reveal }}
|
|
457
|
+
/>
|
|
458
|
+
</FoldSection>
|
|
459
|
+
)
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
/**
|
|
463
|
+
* Brand + clickable tabs + the active panel. The plain form (no `onBrand`)
|
|
464
|
+
* is the OES column; the OMO group passes `onBrand` (foldable brand),
|
|
465
|
+
* `indentContent` and a tighter `gap`, plus the `collapsed`/`summary` pair
|
|
466
|
+
* that replaces the whole column with one summary line.
|
|
467
|
+
*/
|
|
468
|
+
export function TabColumn(props: {
|
|
469
|
+
brand: string
|
|
470
|
+
tabs: readonly string[]
|
|
471
|
+
labels: Record<string, string>
|
|
472
|
+
active: string | null
|
|
473
|
+
colors: ThemeColors
|
|
474
|
+
onPick: (tab: string) => void
|
|
475
|
+
onBrand?: () => void
|
|
476
|
+
/** Per-tab status light replacing the `|` separators (see `BrandTabs`). */
|
|
477
|
+
glyph?: (tab: string) => GlyphSpec
|
|
478
|
+
panels: Record<string, () => JSX.Element>
|
|
479
|
+
indentContent?: boolean
|
|
480
|
+
gap?: number
|
|
481
|
+
collapsed?: boolean
|
|
482
|
+
summary?: string
|
|
483
|
+
}): JSX.Element {
|
|
484
|
+
const chevron = props.onBrand ? "▼ " : ""
|
|
485
|
+
return (
|
|
486
|
+
<Show
|
|
487
|
+
when={!props.collapsed}
|
|
488
|
+
fallback={
|
|
489
|
+
<box flexDirection="row" onMouseUp={props.onBrand}>
|
|
490
|
+
<ClickText
|
|
491
|
+
fg={props.colors.primary || props.colors.text}
|
|
492
|
+
bold
|
|
493
|
+
underline={Boolean(props.onBrand)}
|
|
494
|
+
>
|
|
495
|
+
{`▶ ${props.brand}`}
|
|
496
|
+
</ClickText>
|
|
497
|
+
<Show when={props.summary}>
|
|
498
|
+
<text fg={props.colors.textMuted}>{` ${props.summary}`}</text>
|
|
499
|
+
</Show>
|
|
500
|
+
</box>
|
|
501
|
+
}
|
|
502
|
+
>
|
|
503
|
+
<box flexDirection="column" gap={props.gap ?? 1}>
|
|
504
|
+
<BrandTabs
|
|
505
|
+
brand={`${chevron}${props.brand}`}
|
|
506
|
+
tabs={props.tabs}
|
|
507
|
+
labels={props.labels}
|
|
508
|
+
active={props.active}
|
|
509
|
+
colors={props.colors}
|
|
510
|
+
onPick={props.onPick}
|
|
511
|
+
onBrand={props.onBrand}
|
|
512
|
+
glyph={props.glyph}
|
|
513
|
+
/>
|
|
514
|
+
<box flexDirection="column" gap={0} paddingLeft={props.indentContent ? 1 : 0}>
|
|
515
|
+
<For each={props.tabs}>
|
|
516
|
+
{(key) => (
|
|
517
|
+
<Show when={props.active === key}>
|
|
518
|
+
{props.panels[key]?.()}
|
|
519
|
+
</Show>
|
|
520
|
+
)}
|
|
521
|
+
</For>
|
|
522
|
+
</box>
|
|
523
|
+
</box>
|
|
524
|
+
</Show>
|
|
525
|
+
)
|
|
526
|
+
}
|