opencode-extended-sidebar 0.2.90 → 0.2.91

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 (108) hide show
  1. package/.oesignore +17 -17
  2. package/CHANGELOG.md +96 -95
  3. package/LICENSE +21 -21
  4. package/README.md +150 -235
  5. package/oes.json +13 -11
  6. package/package.json +100 -100
  7. package/scripts/install-git-hooks.mjs +29 -29
  8. package/src/pware.oc.core/constants/index.ts +18 -18
  9. package/src/pware.oc.core/constants/pware.oc.core.constants.eventKind.ts +29 -29
  10. package/src/pware.oc.core/constants/pware.oc.core.constants.eventName.ts +15 -13
  11. package/src/pware.oc.core/constants/pware.oc.core.constants.eventType.ts +241 -241
  12. package/src/pware.oc.core/constants/pware.oc.core.constants.myWork.ts +55 -55
  13. package/src/pware.oc.core/constants/pware.oc.core.constants.partType.ts +65 -65
  14. package/src/pware.oc.core/constants/pware.oc.core.constants.phase.ts +58 -58
  15. package/src/pware.oc.core/constants/pware.oc.core.constants.pulse.ts +63 -63
  16. package/src/pware.oc.core/constants/pware.oc.core.constants.rowKind.ts +34 -34
  17. package/src/pware.oc.core/constants/pware.oc.core.constants.status.ts +74 -74
  18. package/src/pware.oc.core/constants/pware.oc.core.constants.toolName.ts +69 -69
  19. package/src/pware.oc.core/git/index.ts +7 -7
  20. package/src/pware.oc.core/git/pware.oc.core.git.ts +237 -237
  21. package/src/pware.oc.core/git/pware.oc.core.gitignore.ts +87 -87
  22. package/src/pware.oc.core/index.ts +19 -18
  23. package/src/pware.oc.core/pware.oc.core.bus.ts +42 -42
  24. package/src/pware.oc.core/pware.oc.core.cache.ts +48 -48
  25. package/src/pware.oc.core/pware.oc.core.clipboard.ts +97 -97
  26. package/src/pware.oc.core/pware.oc.core.debug.ts +280 -280
  27. package/src/pware.oc.core/pware.oc.core.events.ts +79 -79
  28. package/src/pware.oc.core/pware.oc.core.glyph.ts +135 -135
  29. package/src/pware.oc.core/pware.oc.core.layout.ts +234 -234
  30. package/src/pware.oc.core/pware.oc.core.oes.ts +99 -89
  31. package/src/pware.oc.core/pware.oc.core.paths.ts +240 -240
  32. package/src/pware.oc.core/pware.oc.core.preview.ts +74 -74
  33. package/src/pware.oc.core/pware.oc.core.pulse.ts +726 -726
  34. package/src/pware.oc.core/pware.oc.core.runtimeConfig.ts +41 -0
  35. package/src/pware.oc.core/pware.oc.core.sqlite.ts +164 -175
  36. package/src/pware.oc.core/pware.oc.core.sqliteGateway.ts +89 -0
  37. package/src/pware.oc.core/pware.oc.core.status.ts +214 -214
  38. package/src/pware.oc.core/pware.oc.core.timing.ts +63 -63
  39. package/src/pware.oc.core/pware.oc.core.width.ts +215 -215
  40. package/src/pware.oc.omo/constants/index.ts +15 -15
  41. package/src/pware.oc.omo/constants/pware.oc.omo.constants.backgroundTask.ts +18 -18
  42. package/src/pware.oc.omo/constants/pware.oc.omo.constants.boulderStatus.ts +49 -49
  43. package/src/pware.oc.omo/constants/pware.oc.omo.constants.docKind.ts +29 -29
  44. package/src/pware.oc.omo/constants/pware.oc.omo.constants.eventName.ts +11 -11
  45. package/src/pware.oc.omo/constants/pware.oc.omo.constants.planStatus.ts +60 -60
  46. package/src/pware.oc.omo/constants/pware.oc.omo.constants.reviewStatus.ts +49 -49
  47. package/src/pware.oc.omo/constants/pware.oc.omo.constants.startWork.ts +25 -25
  48. package/src/pware.oc.omo/constants/pware.oc.omo.constants.verdict.ts +38 -38
  49. package/src/pware.oc.omo/index.ts +7 -7
  50. package/src/pware.oc.omo/resolver/index.ts +19 -19
  51. package/src/pware.oc.omo/resolver/pware.oc.omo.resolver.approval.ts +179 -179
  52. package/src/pware.oc.omo/resolver/pware.oc.omo.resolver.approvalGroup.ts +116 -116
  53. package/src/pware.oc.omo/resolver/pware.oc.omo.resolver.approvalState.ts +34 -34
  54. package/src/pware.oc.omo/resolver/pware.oc.omo.resolver.boulder.ts +503 -503
  55. package/src/pware.oc.omo/resolver/pware.oc.omo.resolver.config.ts +40 -40
  56. package/src/pware.oc.omo/resolver/pware.oc.omo.resolver.doc.ts +269 -269
  57. package/src/pware.oc.omo/resolver/pware.oc.omo.resolver.draftFile.ts +18 -18
  58. package/src/pware.oc.omo/resolver/pware.oc.omo.resolver.notepadsFile.ts +18 -18
  59. package/src/pware.oc.omo/resolver/pware.oc.omo.resolver.plan.ts +155 -155
  60. package/src/pware.oc.omo/resolver/pware.oc.omo.resolver.planFile.ts +363 -361
  61. package/src/pware.oc.omo/resolver/pware.oc.omo.resolver.proofFile.ts +18 -18
  62. package/src/pware.oc.omo/resolver/pware.oc.omo.resolver.rulesFile.ts +11 -11
  63. package/src/pware.oc.omo/resolver/pware.oc.omo.resolver.runContinuationFile.ts +14 -14
  64. package/src/pware.oc.opencode/constants/index.ts +10 -10
  65. package/src/pware.oc.opencode/constants/pware.oc.opencode.constants.eventName.ts +15 -15
  66. package/src/pware.oc.opencode/constants/pware.oc.opencode.constants.fileTouch.ts +18 -18
  67. package/src/pware.oc.opencode/constants/pware.oc.opencode.constants.questionKind.ts +45 -45
  68. package/src/pware.oc.opencode/constants/pware.oc.opencode.constants.sessionStatus.ts +58 -58
  69. package/src/pware.oc.opencode/index.ts +8 -8
  70. package/src/pware.oc.opencode/pware.oc.opencode.events.ts +182 -182
  71. package/src/pware.oc.opencode/pware.oc.opencode.files.ts +399 -399
  72. package/src/pware.oc.opencode/resolver/index.ts +223 -170
  73. package/src/pware.oc.opencode/resolver/pware.oc.opencode.resolver.file.ts +150 -162
  74. package/src/pware.oc.opencode/resolver/pware.oc.opencode.resolver.question.ts +149 -185
  75. package/src/pware.oc.opencode/resolver/pware.oc.opencode.resolver.session.ts +248 -211
  76. package/src/pware.oc.opencode/resolver/pware.oc.opencode.resolver.todo.ts +22 -26
  77. package/src/pware.oc.opencode/resolver/pware.oc.opencode.resolver.tool.ts +224 -236
  78. package/src/pware.oc.perf/index.ts +14 -14
  79. package/src/pware.oc.perf/pware.oc.perf.asciichart.d.ts +15 -15
  80. package/src/pware.oc.perf/pware.oc.perf.charts.ts +284 -284
  81. package/src/pware.oc.perf/pware.oc.perf.reader.ts +962 -927
  82. package/src/pware.oc.perf/pware.oc.perf.realtime.ts +88 -88
  83. package/src/pware.oc.perf/pware.oc.perf.realtimeBlock.ts +118 -118
  84. package/src/pware.oc.perf/pware.oc.perf.realtimeCpuRam.ts +79 -79
  85. package/src/pware.oc.perf/pware.oc.perf.realtimeSampler.ts +71 -71
  86. package/src/pware.oc.perf/pware.oc.perf.realtimeTimeline.ts +214 -214
  87. package/src/pware.oc.perf/pware.oc.perf.self.ts +179 -179
  88. package/src/pware.oc.perf/pware.oc.perf.view.tsx +463 -462
  89. package/src/pware.oc.runtime/index.ts +13 -13
  90. package/src/pware.oc.runtime/pware.oc.runtime.monitor.ts +142 -142
  91. package/src/pware.oc.runtime/pware.oc.runtime.mywork-enrich.ts +87 -79
  92. package/src/pware.oc.runtime/pware.oc.runtime.mywork.ts +282 -253
  93. package/src/pware.oc.runtime/pware.oc.runtime.omoRead.ts +37 -0
  94. package/src/pware.oc.runtime/pware.oc.runtime.questions.ts +83 -95
  95. package/src/pware.oc.runtime/pware.oc.runtime.snapshotClient.ts +130 -129
  96. package/src/pware.oc.runtime/pware.oc.runtime.source.ts +105 -105
  97. package/src/pware.oc.runtime/pware.oc.runtime.worker.ts +44 -43
  98. package/src/pware.oc.runtime/resolver/index.ts +268 -224
  99. package/src/pware.oc.runtime/resolver/pware.oc.runtime.resolver.delegate.ts +146 -146
  100. package/src/pware.oc.ui/index.ts +12 -12
  101. package/src/pware.oc.ui/pware.oc.ui.chrome.tsx +381 -339
  102. package/src/pware.oc.ui/pware.oc.ui.glyphs.tsx +144 -144
  103. package/src/pware.oc.ui/pware.oc.ui.host.tsx +111 -111
  104. package/src/pware.oc.ui/pware.oc.ui.live.tsx +82 -82
  105. package/src/pware.oc.ui/pware.oc.ui.menudialogs.tsx +843 -843
  106. package/src/pware.oc.ui/pware.oc.ui.sections.tsx +552 -526
  107. package/src/pware.oc.ui/pware.oc.ui.sidebar.tsx +1718 -1629
  108. package/src/pware.oc.ui.tsx +98 -98
@@ -1,234 +1,234 @@
1
- /**
2
- * Vertical row budget for the sidebar. Pure — no OpenTUI, unit tested directly.
3
- * Same idea as `packChips` in pulse.ts, one axis over: under pressure the worst
4
- * rank gives up rows first, and nothing drops below its `min` until every
5
- * section has already reached it.
6
- */
7
-
8
- /**
9
- * Rows OpenCode's own TUI takes around the `sidebar_content` slot. Measured
10
- * from the opencode TUI source (`packages/tui/src/routes/session/sidebar.tsx`,
11
- * stable across v1.18.27 = HEAD): the sidebar column has no border — its frame
12
- * is `paddingTop` 1 + a fixed footer (`sidebar_footer`, outside the scrollbox:
13
- * 1 spacer + path row + 1 gap + version row) + `paddingBottom` 1.
14
- */
15
- const HOST_SIDEBAR_CHROME = 6
16
-
17
- /**
18
- * Rows the host's `sidebar_title` fallback block (same scrollbox, above the
19
- * content slot) takes before the plugin's first content row: a typical session
20
- * shows a bold title + workspace (2 rows) plus the 1-row `gap` separator, and
21
- * a wrapping title or the optional channel/share lines add more. Kept at 4 so
22
- * `PANEL_CHROME` still covers the old flat-10 headroom.
23
- */
24
- const HOST_SIDEBAR_TITLE_RESERVE = 4
25
-
26
- /** Fixed chrome the sidebar slot's own host takes above the content rows. */
27
- const PANEL_CHROME = HOST_SIDEBAR_CHROME + HOST_SIDEBAR_TITLE_RESERVE
28
- const PANEL_MIN_ROWS = 8
29
-
30
- /** Shared right rail for context actions (`switch new`, `view all`, …). */
31
- export const CONTEXT_ACTION_COL_WIDTH = 10
32
-
33
- /**
34
- * Row width budget before the root box reports its measured layout width.
35
- * `lineMax` used to be an oes.json option; it is now measured, and this is the
36
- * pre-layout fallback (matches the old default).
37
- */
38
- export const ROW_LINE_FALLBACK = 31
39
-
40
- /** Columns subtracted from the measured root width for the scrollbar reserve. */
41
- export const ROW_LINE_RESERVE = 1
42
-
43
- /** Realtime charts link on the OES row (`C` → fullscreen dialog). */
44
- export const RT_ACTION_COL_WIDTH = 1
45
-
46
- /** Compact sparkline body rows — sidebar inline chart removed; kept for docs/tests. */
47
- export const RT_CHART_ROWS = 3
48
-
49
- /** Per-series sparkline body rows in the realtime fullscreen dialog. */
50
- export const RT_DIALOG_CHART_ROWS = 8
51
-
52
- /**
53
- * Host `api.ui.Dialog` inner box widths (`packages/opencode/.../dialog.tsx`).
54
- * `maxWidth` is `terminal − 2`. `xlarge` is 116 on hosts that implement it
55
- * (#44754); older builds only switch `large` (80) vs else (60).
56
- */
57
- export const HOST_DIALOG_OUTER_WIDTH = { medium: 60, large: 80, xlarge: 116 } as const
58
- export type HostDialogSize = keyof typeof HOST_DIALOG_OUTER_WIDTH
59
-
60
- /** `maxWidth={dimensions().width - 2}` on the host inner box. */
61
- const HOST_DIALOG_MAX_INSET = 2
62
- /** `DialogPad` left + right, matching the host dialog bodies (`paddingLeft/Right 2`). */
63
- const DIALOG_PAD_X = 4
64
-
65
- /**
66
- * Columns inside the padded dialog body. Subtracts the host box cap, the
67
- * `maxWidth` inset, and DialogPad — this is the line budget a chart may use,
68
- * including the asciiTrend min/max labels on the left of each row.
69
- */
70
- export function dialogInnerWidth(termWidth: number, size: HostDialogSize): number {
71
- const w = Number.isFinite(termWidth) && termWidth > 0 ? Math.floor(termWidth) : 80
72
- const outer = Math.min(HOST_DIALOG_OUTER_WIDTH[size], Math.max(1, w - HOST_DIALOG_MAX_INSET))
73
- return Math.max(8, outer - DIALOG_PAD_X)
74
- }
75
-
76
- /**
77
- * One right-aligned action line padded to {@link CONTEXT_ACTION_COL_WIDTH}.
78
- * Long labels keep their trailing characters so `view all` still fits the rail.
79
- */
80
- export function contextActionsLine(labels: readonly string[]): string {
81
- const text = labels.join(" ")
82
- if (text.length >= CONTEXT_ACTION_COL_WIDTH) return text.slice(-CONTEXT_ACTION_COL_WIDTH)
83
- return text.padStart(CONTEXT_ACTION_COL_WIDTH)
84
- }
85
-
86
- /** Usable rows in the sidebar slot for a terminal that tall. */
87
- export function panelRows(termHeight: number): number {
88
- const h = Number.isFinite(termHeight) && termHeight > 0 ? termHeight : 24
89
- return Math.max(PANEL_MIN_ROWS, Math.floor(h) - PANEL_CHROME)
90
- }
91
-
92
- /** Rows a section keeps before it is worth folding instead. */
93
- export const ROW_MIN = {
94
- delegates: 2,
95
- drafts: 2,
96
- files: 3,
97
- mywork: 2,
98
- tools: 3,
99
- } as const
100
-
101
- /**
102
- * Higher gives up rows first. Live activity outranks history: on Current the
103
- * tool feed survives longest. My work is a queue of things awaiting the user,
104
- * so it ties with the live feed.
105
- */
106
- export const ROW_RANK = {
107
- delegates: 3,
108
- drafts: 3,
109
- files: 2,
110
- mywork: 1,
111
- tools: 1,
112
- } as const
113
-
114
- export type Section<K extends string = string> = {
115
- key: K
116
- /** Rows the section would like — usually the matching `oes.json` value. */
117
- want: number
118
- min: number
119
- rank: number
120
- }
121
-
122
- /**
123
- * Split `budget - fixed` rows across the sections. Ties are broken by whoever
124
- * currently holds the most rows, so equal ranks shrink together. A section that
125
- * hits `min` while the budget is still short is handed `0` — the caller folds
126
- * it rather than rendering a stub.
127
- */
128
- export function packSections<K extends string>(
129
- budget: number,
130
- fixed: number,
131
- sections: readonly Section<K>[],
132
- ): Record<K, number> {
133
- const live = sections.map((s) => {
134
- const min = Math.max(0, Math.round(s.min))
135
- return { key: s.key, min, rank: s.rank, rows: Math.max(min, Math.max(0, Math.round(s.want))) }
136
- })
137
- const available = Math.max(0, Math.round(budget) - Math.round(fixed))
138
- const total = () => live.reduce((sum, s) => sum + s.rows, 0)
139
-
140
- const worstAbove = (floor: (s: (typeof live)[number]) => number): number => {
141
- let worst = -1
142
- for (let i = 0; i < live.length; i += 1) {
143
- const s = live[i]!
144
- if (s.rows <= floor(s)) continue
145
- const best = worst < 0 ? null : live[worst]!
146
- if (!best || s.rank > best.rank || (s.rank === best.rank && s.rows > best.rows)) worst = i
147
- }
148
- return worst
149
- }
150
-
151
- while (total() > available) {
152
- const i = worstAbove((s) => s.min)
153
- if (i < 0) break
154
- live[i]!.rows -= 1
155
- }
156
-
157
- // Everyone is at their minimum and it still does not fit: fold, worst first.
158
- while (total() > available) {
159
- const i = worstAbove(() => 0)
160
- if (i < 0) break
161
- live[i]!.rows = 0
162
- }
163
-
164
- const out = {} as Record<K, number>
165
- for (const s of live) out[s.key] = s.rows
166
- return out
167
- }
168
-
169
- /**
170
- * Look up a packed section. A missing plan (Solid memo still computing,
171
- * previous compute threw, owner disposed) or a missing / non-finite key
172
- * returns `fallback` — the sidebar slot must not die on `plan[key]`.
173
- */
174
- export function rowsForPlan(
175
- plan: Readonly<Record<string, number>> | null | undefined,
176
- key: string,
177
- fallback: number,
178
- ): number {
179
- if (!plan || typeof plan !== "object") return fallback
180
- const v = plan[key]
181
- return typeof v === "number" && Number.isFinite(v) ? v : fallback
182
- }
183
-
184
- /**
185
- * Show `shown` rows and report how many a "more" control can still reveal.
186
- * The expander is a separate clickable line (`RowList`) the caller renders
187
- * below the list — no row is spent on the note.
188
- */
189
- export function sliceShown<T>(
190
- rows: readonly T[],
191
- shown: number,
192
- ): { rows: T[]; hidden: number } {
193
- const s = Math.max(0, Math.round(shown))
194
- const kept = rows.slice(0, s)
195
- return { rows: kept, hidden: Math.max(0, rows.length - kept.length) }
196
- }
197
-
198
- /**
199
- * Whether the "… +N more" / "… less" line is drawn at all. The expanded toggle
200
- * keeps its "… less" line even when nothing is hidden; otherwise the line
201
- * vanishes the moment there is nothing left to reveal.
202
- */
203
- export function moreRevealVisible(hidden: number, expanded?: boolean): boolean {
204
- return expanded === true || hidden > 0
205
- }
206
-
207
- /**
208
- * Clamp a scroll offset into the window a `visible`-row view can show over
209
- * `total` rows: the offset never goes below 0 or past the last full window.
210
- */
211
- export function clampScrollOffset(total: number, visible: number, offset: number): number {
212
- const t = Math.max(0, Math.round(total))
213
- const v = Math.max(1, Math.round(visible))
214
- const max = Math.max(0, t - v)
215
- const o = Math.max(0, Math.round(offset))
216
- return Math.min(o, max)
217
- }
218
-
219
- /**
220
- * Move a scroll offset by one wheel `step`. Newest-first lists count the offset
221
- * from the newest row, so "down" goes toward older rows and "up" back to the
222
- * newest. The result stays inside the same clamped window as `clampScrollOffset`.
223
- */
224
- export function scrollByStep(
225
- total: number,
226
- visible: number,
227
- offset: number,
228
- direction: "up" | "down",
229
- step: number,
230
- ): number {
231
- const s = Math.max(1, Math.round(step))
232
- const next = direction === "down" ? offset + s : offset - s
233
- return clampScrollOffset(total, visible, next)
234
- }
1
+ /**
2
+ * Vertical row budget for the sidebar. Pure — no OpenTUI, unit tested directly.
3
+ * Same idea as `packChips` in pulse.ts, one axis over: under pressure the worst
4
+ * rank gives up rows first, and nothing drops below its `min` until every
5
+ * section has already reached it.
6
+ */
7
+
8
+ /**
9
+ * Rows OpenCode's own TUI takes around the `sidebar_content` slot. Measured
10
+ * from the opencode TUI source (`packages/tui/src/routes/session/sidebar.tsx`,
11
+ * stable across v1.18.27 = HEAD): the sidebar column has no border — its frame
12
+ * is `paddingTop` 1 + a fixed footer (`sidebar_footer`, outside the scrollbox:
13
+ * 1 spacer + path row + 1 gap + version row) + `paddingBottom` 1.
14
+ */
15
+ const HOST_SIDEBAR_CHROME = 6
16
+
17
+ /**
18
+ * Rows the host's `sidebar_title` fallback block (same scrollbox, above the
19
+ * content slot) takes before the plugin's first content row: a typical session
20
+ * shows a bold title + workspace (2 rows) plus the 1-row `gap` separator, and
21
+ * a wrapping title or the optional channel/share lines add more. Kept at 4 so
22
+ * `PANEL_CHROME` still covers the old flat-10 headroom.
23
+ */
24
+ const HOST_SIDEBAR_TITLE_RESERVE = 4
25
+
26
+ /** Fixed chrome the sidebar slot's own host takes above the content rows. */
27
+ const PANEL_CHROME = HOST_SIDEBAR_CHROME + HOST_SIDEBAR_TITLE_RESERVE
28
+ const PANEL_MIN_ROWS = 8
29
+
30
+ /** Shared right rail for context actions (`switch new`, `view all`, …). */
31
+ export const CONTEXT_ACTION_COL_WIDTH = 10
32
+
33
+ /**
34
+ * Row width budget before the root box reports its measured layout width.
35
+ * `lineMax` used to be an oes.json option; it is now measured, and this is the
36
+ * pre-layout fallback (matches the old default).
37
+ */
38
+ export const ROW_LINE_FALLBACK = 31
39
+
40
+ /** Columns subtracted from the measured root width for the scrollbar reserve. */
41
+ export const ROW_LINE_RESERVE = 1
42
+
43
+ /** Realtime charts link on the OES row (`C` → fullscreen dialog). */
44
+ export const RT_ACTION_COL_WIDTH = 1
45
+
46
+ /** Compact sparkline body rows — sidebar inline chart removed; kept for docs/tests. */
47
+ export const RT_CHART_ROWS = 3
48
+
49
+ /** Per-series sparkline body rows in the realtime fullscreen dialog. */
50
+ export const RT_DIALOG_CHART_ROWS = 8
51
+
52
+ /**
53
+ * Host `api.ui.Dialog` inner box widths (`packages/opencode/.../dialog.tsx`).
54
+ * `maxWidth` is `terminal − 2`. `xlarge` is 116 on hosts that implement it
55
+ * (#44754); older builds only switch `large` (80) vs else (60).
56
+ */
57
+ export const HOST_DIALOG_OUTER_WIDTH = { medium: 60, large: 80, xlarge: 116 } as const
58
+ export type HostDialogSize = keyof typeof HOST_DIALOG_OUTER_WIDTH
59
+
60
+ /** `maxWidth={dimensions().width - 2}` on the host inner box. */
61
+ const HOST_DIALOG_MAX_INSET = 2
62
+ /** `DialogPad` left + right, matching the host dialog bodies (`paddingLeft/Right 2`). */
63
+ const DIALOG_PAD_X = 4
64
+
65
+ /**
66
+ * Columns inside the padded dialog body. Subtracts the host box cap, the
67
+ * `maxWidth` inset, and DialogPad — this is the line budget a chart may use,
68
+ * including the asciiTrend min/max labels on the left of each row.
69
+ */
70
+ export function dialogInnerWidth(termWidth: number, size: HostDialogSize): number {
71
+ const w = Number.isFinite(termWidth) && termWidth > 0 ? Math.floor(termWidth) : 80
72
+ const outer = Math.min(HOST_DIALOG_OUTER_WIDTH[size], Math.max(1, w - HOST_DIALOG_MAX_INSET))
73
+ return Math.max(8, outer - DIALOG_PAD_X)
74
+ }
75
+
76
+ /**
77
+ * One right-aligned action line padded to {@link CONTEXT_ACTION_COL_WIDTH}.
78
+ * Long labels keep their trailing characters so `view all` still fits the rail.
79
+ */
80
+ export function contextActionsLine(labels: readonly string[]): string {
81
+ const text = labels.join(" ")
82
+ if (text.length >= CONTEXT_ACTION_COL_WIDTH) return text.slice(-CONTEXT_ACTION_COL_WIDTH)
83
+ return text.padStart(CONTEXT_ACTION_COL_WIDTH)
84
+ }
85
+
86
+ /** Usable rows in the sidebar slot for a terminal that tall. */
87
+ export function panelRows(termHeight: number): number {
88
+ const h = Number.isFinite(termHeight) && termHeight > 0 ? termHeight : 24
89
+ return Math.max(PANEL_MIN_ROWS, Math.floor(h) - PANEL_CHROME)
90
+ }
91
+
92
+ /** Rows a section keeps before it is worth folding instead. */
93
+ export const ROW_MIN = {
94
+ delegates: 2,
95
+ drafts: 2,
96
+ files: 3,
97
+ mywork: 2,
98
+ tools: 3,
99
+ } as const
100
+
101
+ /**
102
+ * Higher gives up rows first. Live activity outranks history: on Current the
103
+ * tool feed survives longest. My work is a queue of things awaiting the user,
104
+ * so it ties with the live feed.
105
+ */
106
+ export const ROW_RANK = {
107
+ delegates: 3,
108
+ drafts: 3,
109
+ files: 2,
110
+ mywork: 1,
111
+ tools: 1,
112
+ } as const
113
+
114
+ export type Section<K extends string = string> = {
115
+ key: K
116
+ /** Rows the section would like — usually the matching `oes.json` value. */
117
+ want: number
118
+ min: number
119
+ rank: number
120
+ }
121
+
122
+ /**
123
+ * Split `budget - fixed` rows across the sections. Ties are broken by whoever
124
+ * currently holds the most rows, so equal ranks shrink together. A section that
125
+ * hits `min` while the budget is still short is handed `0` — the caller folds
126
+ * it rather than rendering a stub.
127
+ */
128
+ export function packSections<K extends string>(
129
+ budget: number,
130
+ fixed: number,
131
+ sections: readonly Section<K>[],
132
+ ): Record<K, number> {
133
+ const live = sections.map((s) => {
134
+ const min = Math.max(0, Math.round(s.min))
135
+ return { key: s.key, min, rank: s.rank, rows: Math.max(min, Math.max(0, Math.round(s.want))) }
136
+ })
137
+ const available = Math.max(0, Math.round(budget) - Math.round(fixed))
138
+ const total = () => live.reduce((sum, s) => sum + s.rows, 0)
139
+
140
+ const worstAbove = (floor: (s: (typeof live)[number]) => number): number => {
141
+ let worst = -1
142
+ for (let i = 0; i < live.length; i += 1) {
143
+ const s = live[i]!
144
+ if (s.rows <= floor(s)) continue
145
+ const best = worst < 0 ? null : live[worst]!
146
+ if (!best || s.rank > best.rank || (s.rank === best.rank && s.rows > best.rows)) worst = i
147
+ }
148
+ return worst
149
+ }
150
+
151
+ while (total() > available) {
152
+ const i = worstAbove((s) => s.min)
153
+ if (i < 0) break
154
+ live[i]!.rows -= 1
155
+ }
156
+
157
+ // Everyone is at their minimum and it still does not fit: fold, worst first.
158
+ while (total() > available) {
159
+ const i = worstAbove(() => 0)
160
+ if (i < 0) break
161
+ live[i]!.rows = 0
162
+ }
163
+
164
+ const out = {} as Record<K, number>
165
+ for (const s of live) out[s.key] = s.rows
166
+ return out
167
+ }
168
+
169
+ /**
170
+ * Look up a packed section. A missing plan (Solid memo still computing,
171
+ * previous compute threw, owner disposed) or a missing / non-finite key
172
+ * returns `fallback` — the sidebar slot must not die on `plan[key]`.
173
+ */
174
+ export function rowsForPlan(
175
+ plan: Readonly<Record<string, number>> | null | undefined,
176
+ key: string,
177
+ fallback: number,
178
+ ): number {
179
+ if (!plan || typeof plan !== "object") return fallback
180
+ const v = plan[key]
181
+ return typeof v === "number" && Number.isFinite(v) ? v : fallback
182
+ }
183
+
184
+ /**
185
+ * Show `shown` rows and report how many a "more" control can still reveal.
186
+ * The expander is a separate clickable line (`RowList`) the caller renders
187
+ * below the list — no row is spent on the note.
188
+ */
189
+ export function sliceShown<T>(
190
+ rows: readonly T[],
191
+ shown: number,
192
+ ): { rows: T[]; hidden: number } {
193
+ const s = Math.max(0, Math.round(shown))
194
+ const kept = rows.slice(0, s)
195
+ return { rows: kept, hidden: Math.max(0, rows.length - kept.length) }
196
+ }
197
+
198
+ /**
199
+ * Whether the "… +N more" / "… less" line is drawn at all. The expanded toggle
200
+ * keeps its "… less" line even when nothing is hidden; otherwise the line
201
+ * vanishes the moment there is nothing left to reveal.
202
+ */
203
+ export function moreRevealVisible(hidden: number, expanded?: boolean): boolean {
204
+ return expanded === true || hidden > 0
205
+ }
206
+
207
+ /**
208
+ * Clamp a scroll offset into the window a `visible`-row view can show over
209
+ * `total` rows: the offset never goes below 0 or past the last full window.
210
+ */
211
+ export function clampScrollOffset(total: number, visible: number, offset: number): number {
212
+ const t = Math.max(0, Math.round(total))
213
+ const v = Math.max(1, Math.round(visible))
214
+ const max = Math.max(0, t - v)
215
+ const o = Math.max(0, Math.round(offset))
216
+ return Math.min(o, max)
217
+ }
218
+
219
+ /**
220
+ * Move a scroll offset by one wheel `step`. Newest-first lists count the offset
221
+ * from the newest row, so "down" goes toward older rows and "up" back to the
222
+ * newest. The result stays inside the same clamped window as `clampScrollOffset`.
223
+ */
224
+ export function scrollByStep(
225
+ total: number,
226
+ visible: number,
227
+ offset: number,
228
+ direction: "up" | "down",
229
+ step: number,
230
+ ): number {
231
+ const s = Math.max(1, Math.round(step))
232
+ const next = direction === "down" ? offset + s : offset - s
233
+ return clampScrollOffset(total, visible, next)
234
+ }
@@ -1,89 +1,99 @@
1
- /**
2
- * oes.json — OpenCode Extended Sidebar options.
3
- * Merge: plugin defaults < ~/.config/opencode/oes.json < <project>/oes.json
4
- */
5
- import path from "node:path"
6
- import { fileStamp, getOpenCodeConfigDir, pluginRoot, readJson } from "./pware.oc.core.paths.js"
7
- import { createStampCache } from "./pware.oc.core.cache.js"
8
-
9
- export type OesOptions = {
10
- fileRows: number
11
- /** Sessions compared under Perf → History. 0 hides the section. */
12
- perfHistory: number
13
- /** Rows per Perf section (models, tools, history). */
14
- perfRows: number
15
- /** How many recent turns Perf scans. Higher = longer history, slower read. */
16
- perfTurns: number
17
- /** Seconds between full open-question rescans when no event-driven hint arrives. */
18
- questionReconcileSec: number
19
- /** Sessions fetched for the recent-sessions window. */
20
- sessionFetch: number
21
- /** Hide Files that match the project's root .gitignore. Off by default. */
22
- skipGitignore: boolean
23
- toolRows: number
24
- /** Tool-call history kept for the feed's `… +N more` revealer. Distinct from `toolRows`. */
25
- toolFetch: number
26
- }
27
-
28
- export const OES_DEFAULTS: OesOptions = {
29
- fileRows: 8,
30
- perfHistory: 3,
31
- perfRows: 5,
32
- perfTurns: 120,
33
- questionReconcileSec: 15,
34
- sessionFetch: 20,
35
- skipGitignore: false,
36
- toolRows: 5,
37
- toolFetch: 20,
38
- }
39
-
40
- function pluginOesPath(): string {
41
- return path.join(pluginRoot(), "oes.json")
42
- }
43
-
44
- function clamp(n: unknown, min: number, max: number, fallback: number): number {
45
- if (typeof n !== "number" || !Number.isFinite(n)) return fallback
46
- return Math.round(Math.min(max, Math.max(min, n)))
47
- }
48
-
49
- /** Merge one oes.json object onto a base. Exported for tests. */
50
- export function pick(raw: Record<string, unknown> | null, base: OesOptions): OesOptions {
51
- if (!raw) return base
52
- const toolRows = clamp(raw.toolRows, 3, 20, base.toolRows)
53
- return {
54
- fileRows: clamp(raw.fileRows, 3, 20, base.fileRows),
55
- perfHistory: clamp(raw.perfHistory, 0, 10, base.perfHistory),
56
- perfRows: clamp(raw.perfRows, 3, 20, base.perfRows),
57
- perfTurns: clamp(raw.perfTurns, 20, 500, base.perfTurns),
58
- questionReconcileSec: clamp(raw.questionReconcileSec, 5, 300, base.questionReconcileSec),
59
- sessionFetch: clamp(raw.sessionFetch, 2, 80, base.sessionFetch),
60
- skipGitignore: typeof raw.skipGitignore === "boolean" ? raw.skipGitignore : base.skipGitignore,
61
- toolRows,
62
- toolFetch: clamp(raw.toolFetch, toolRows, 80, base.toolFetch),
63
- }
64
- }
65
-
66
- export function oesPaths(projectRoot?: string | null): string[] {
67
- const out = [pluginOesPath(), path.join(getOpenCodeConfigDir(), "oes.json")]
68
- if (projectRoot) out.push(path.join(projectRoot, "oes.json"))
69
- return out
70
- }
71
-
72
- export function oesStamp(projectRoot?: string | null): string {
73
- return oesPaths(projectRoot).map(fileStamp).join("|")
74
- }
75
-
76
- const oesCache = createStampCache<OesOptions>()
77
-
78
- /** Drop the merged-options cache so the next getOes re-reads files. */
79
- export function resetOesCache(): void {
80
- oesCache.reset()
81
- }
82
-
83
- export function getOes(projectRoot?: string | null): OesOptions {
84
- return oesCache.get(oesStamp(projectRoot), () => {
85
- let next = { ...OES_DEFAULTS }
86
- for (const p of oesPaths(projectRoot)) next = pick(readJson(p), next)
87
- return next
88
- })
89
- }
1
+ /**
2
+ * oes.json — OpenCode Extended Sidebar options.
3
+ * Merge: plugin defaults < ~/.config/opencode/oes.json < <project>/oes.json
4
+ */
5
+ import path from "node:path"
6
+ import { fileStamp, getOpenCodeConfigDir, pluginRoot, readJson } from "./pware.oc.core.paths.js"
7
+ import { createStampCache } from "./pware.oc.core.cache.js"
8
+
9
+ export type OesOptions = {
10
+ fileRows: number
11
+ /** Sessions compared under Perf → History. 0 hides the section. */
12
+ perfHistory: number
13
+ /** Rows per Perf section (models, tools, history). */
14
+ perfRows: number
15
+ /** How many recent turns Perf scans. Higher = longer history, slower read. */
16
+ perfTurns: number
17
+ /** Seconds between full open-question rescans when no event-driven hint arrives. */
18
+ questionReconcileSec: number
19
+ /** Sessions fetched for the recent-sessions window. */
20
+ sessionFetch: number
21
+ /** How old (in hours) a session can be before it is dimmed in the recent-sessions window. Always sessionVisibleHours. */
22
+ sessionDimHours: number
23
+ /** How old (in hours) a session can be before it is hidden from the recent-sessions window. */
24
+ sessionVisibleHours: number
25
+ /** Hide Files that match the project's root .gitignore. Off by default. */
26
+ skipGitignore: boolean
27
+ toolRows: number
28
+ /** Tool-call history kept for the feed's `… +N more` revealer. Distinct from `toolRows`. */
29
+ toolFetch: number
30
+ }
31
+
32
+ export const OES_DEFAULTS: OesOptions = {
33
+ fileRows: 8,
34
+ perfHistory: 3,
35
+ perfRows: 5,
36
+ perfTurns: 120,
37
+ questionReconcileSec: 15,
38
+ sessionDimHours: 48,
39
+ sessionFetch: 10,
40
+ sessionVisibleHours: 72,
41
+ skipGitignore: false,
42
+ toolRows: 5,
43
+ toolFetch: 20,
44
+ }
45
+
46
+ function pluginOesPath(): string {
47
+ return path.join(pluginRoot(), "oes.json")
48
+ }
49
+
50
+ function clamp(n: unknown, min: number, max: number, fallback: number): number {
51
+ if (typeof n !== "number" || !Number.isFinite(n)) return fallback
52
+ return Math.round(Math.min(max, Math.max(min, n)))
53
+ }
54
+
55
+ /** Merge one oes.json object onto a base. Exported for tests. */
56
+ export function pick(raw: Record<string, unknown> | null, base: OesOptions): OesOptions {
57
+ if (!raw) return base
58
+ const toolRows = clamp(raw.toolRows, 3, 20, base.toolRows)
59
+ const sessionVisibleHours = clamp(raw.sessionVisibleHours, 1, 720, base.sessionVisibleHours)
60
+ const sessionDimHours = clamp(raw.sessionDimHours, 0, sessionVisibleHours, Math.min(base.sessionDimHours, sessionVisibleHours))
61
+ return {
62
+ fileRows: clamp(raw.fileRows, 3, 20, base.fileRows),
63
+ perfHistory: clamp(raw.perfHistory, 0, 10, base.perfHistory),
64
+ perfRows: clamp(raw.perfRows, 3, 20, base.perfRows),
65
+ perfTurns: clamp(raw.perfTurns, 20, 500, base.perfTurns),
66
+ questionReconcileSec: clamp(raw.questionReconcileSec, 5, 300, base.questionReconcileSec),
67
+ sessionDimHours,
68
+ sessionFetch: clamp(raw.sessionFetch, 2, 80, base.sessionFetch),
69
+ sessionVisibleHours,
70
+ skipGitignore: typeof raw.skipGitignore === "boolean" ? raw.skipGitignore : base.skipGitignore,
71
+ toolRows,
72
+ toolFetch: clamp(raw.toolFetch, toolRows, 80, base.toolFetch),
73
+ }
74
+ }
75
+
76
+ export function oesPaths(projectRoot?: string | null): string[] {
77
+ const out = [pluginOesPath(), path.join(getOpenCodeConfigDir(), "oes.json")]
78
+ if (projectRoot) out.push(path.join(projectRoot, "oes.json"))
79
+ return out
80
+ }
81
+
82
+ export function oesStamp(projectRoot?: string | null): string {
83
+ return oesPaths(projectRoot).map(fileStamp).join("|")
84
+ }
85
+
86
+ const oesCache = createStampCache<OesOptions>()
87
+
88
+ /** Drop the merged-options cache so the next getOes re-reads files. */
89
+ export function resetOesCache(): void {
90
+ oesCache.reset()
91
+ }
92
+
93
+ export function getOes(projectRoot?: string | null): OesOptions {
94
+ return oesCache.get(oesStamp(projectRoot), () => {
95
+ let next = { ...OES_DEFAULTS }
96
+ for (const p of oesPaths(projectRoot)) next = pick(readJson(p), next)
97
+ return next
98
+ })
99
+ }