@f0rbit/overview 0.2.0 → 0.2.2

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 (69) hide show
  1. package/bin/overview +1 -1
  2. package/dist/overview.js +11898 -0
  3. package/package.json +10 -13
  4. package/bunfig.toml +0 -7
  5. package/packages/core/__tests__/concurrency.test.ts +0 -111
  6. package/packages/core/__tests__/helpers.ts +0 -60
  7. package/packages/core/__tests__/integration/git-status.test.ts +0 -62
  8. package/packages/core/__tests__/integration/scanner.test.ts +0 -140
  9. package/packages/core/__tests__/ocn.test.ts +0 -164
  10. package/packages/core/package.json +0 -13
  11. package/packages/core/src/cache.ts +0 -31
  12. package/packages/core/src/concurrency.ts +0 -44
  13. package/packages/core/src/devpad.ts +0 -61
  14. package/packages/core/src/git-graph.ts +0 -54
  15. package/packages/core/src/git-stats.ts +0 -201
  16. package/packages/core/src/git-status.ts +0 -316
  17. package/packages/core/src/github.ts +0 -286
  18. package/packages/core/src/index.ts +0 -58
  19. package/packages/core/src/ocn.ts +0 -74
  20. package/packages/core/src/scanner.ts +0 -118
  21. package/packages/core/src/types.ts +0 -199
  22. package/packages/core/src/watcher.ts +0 -128
  23. package/packages/core/src/worktree.ts +0 -80
  24. package/packages/core/tsconfig.json +0 -5
  25. package/packages/render/bunfig.toml +0 -8
  26. package/packages/render/jsx-runtime.d.ts +0 -3
  27. package/packages/render/package.json +0 -18
  28. package/packages/render/src/components/__tests__/scrollbox-height.test.tsx +0 -780
  29. package/packages/render/src/components/__tests__/widget-container.integration.test.tsx +0 -306
  30. package/packages/render/src/components/git-graph.tsx +0 -127
  31. package/packages/render/src/components/help-overlay.tsx +0 -108
  32. package/packages/render/src/components/index.ts +0 -7
  33. package/packages/render/src/components/repo-list.tsx +0 -127
  34. package/packages/render/src/components/stats-panel.tsx +0 -116
  35. package/packages/render/src/components/status-badge.tsx +0 -70
  36. package/packages/render/src/components/status-bar.tsx +0 -56
  37. package/packages/render/src/components/widget-container.tsx +0 -286
  38. package/packages/render/src/components/widgets/__tests__/widget-rendering.test.tsx +0 -326
  39. package/packages/render/src/components/widgets/branch-list.tsx +0 -93
  40. package/packages/render/src/components/widgets/commit-activity.tsx +0 -112
  41. package/packages/render/src/components/widgets/devpad-milestones.tsx +0 -88
  42. package/packages/render/src/components/widgets/devpad-tasks.tsx +0 -81
  43. package/packages/render/src/components/widgets/file-changes.tsx +0 -78
  44. package/packages/render/src/components/widgets/git-status.tsx +0 -125
  45. package/packages/render/src/components/widgets/github-ci.tsx +0 -98
  46. package/packages/render/src/components/widgets/github-issues.tsx +0 -101
  47. package/packages/render/src/components/widgets/github-prs.tsx +0 -119
  48. package/packages/render/src/components/widgets/github-release.tsx +0 -73
  49. package/packages/render/src/components/widgets/index.ts +0 -12
  50. package/packages/render/src/components/widgets/recent-commits.tsx +0 -64
  51. package/packages/render/src/components/widgets/registry.ts +0 -23
  52. package/packages/render/src/components/widgets/repo-meta.tsx +0 -80
  53. package/packages/render/src/config/index.ts +0 -104
  54. package/packages/render/src/lib/__tests__/fetch-context.test.ts +0 -200
  55. package/packages/render/src/lib/__tests__/widget-grid.test.ts +0 -665
  56. package/packages/render/src/lib/actions.ts +0 -68
  57. package/packages/render/src/lib/fetch-context.ts +0 -102
  58. package/packages/render/src/lib/filter.ts +0 -94
  59. package/packages/render/src/lib/format.ts +0 -36
  60. package/packages/render/src/lib/use-devpad.ts +0 -167
  61. package/packages/render/src/lib/use-github.ts +0 -75
  62. package/packages/render/src/lib/widget-grid.ts +0 -204
  63. package/packages/render/src/lib/widget-state.ts +0 -96
  64. package/packages/render/src/overview.tsx +0 -16
  65. package/packages/render/src/screens/index.ts +0 -1
  66. package/packages/render/src/screens/main-screen.tsx +0 -410
  67. package/packages/render/src/theme/index.ts +0 -37
  68. package/packages/render/tsconfig.json +0 -9
  69. package/tsconfig.json +0 -23
@@ -1,116 +0,0 @@
1
- import { Show } from "solid-js";
2
- import type { RepoStatus } from "@overview/core";
3
- import { theme } from "../theme";
4
- import { formatRelativeTime } from "../lib/format";
5
-
6
- interface StatsPanelProps {
7
- status: RepoStatus | null;
8
- repoName: string;
9
- loading: boolean;
10
- focused: boolean;
11
- height: number | `${number}%` | "auto";
12
- }
13
-
14
- function StatRow(props: { label: string; value: string; color?: string }) {
15
- return (
16
- <box flexDirection="row" height={1}>
17
- <text fg={theme.fg_dim} content={props.label.padEnd(12)} />
18
- <text fg={props.color ?? theme.fg} content={props.value} />
19
- </box>
20
- );
21
- }
22
-
23
- function CleanState(props: { status: RepoStatus }) {
24
- return (
25
- <box flexDirection="column" gap={1}>
26
- <text fg={theme.status.clean} content="✓ Everything clean & up to date" />
27
- <box flexDirection="column">
28
- <StatRow label="branch" value={props.status.current_branch} />
29
- <StatRow label="last commit" value={formatRelativeTime(props.status.head_time)} />
30
- </box>
31
- </box>
32
- );
33
- }
34
-
35
- function StatusDetails(props: { status: RepoStatus }) {
36
- return (
37
- <box flexDirection="column" gap={1}>
38
- <box flexDirection="column">
39
- <StatRow label="branch" value={props.status.current_branch} />
40
- <StatRow label="remote" value={props.status.remote_url ?? "(none)"} />
41
- </box>
42
-
43
- <Show when={props.status.ahead > 0 || props.status.behind > 0}>
44
- <box flexDirection="row" height={1} gap={2}>
45
- <text fg={theme.status.ahead} content={`↑ ${props.status.ahead} ahead`} />
46
- <text fg={theme.status.behind} content={`↓ ${props.status.behind} behind`} />
47
- </box>
48
- </Show>
49
-
50
- <Show
51
- when={
52
- props.status.modified_count > 0 ||
53
- props.status.staged_count > 0 ||
54
- props.status.untracked_count > 0 ||
55
- props.status.conflict_count > 0
56
- }
57
- >
58
- <box flexDirection="column">
59
- <Show when={props.status.modified_count > 0 || props.status.staged_count > 0}>
60
- <box flexDirection="row" height={1} gap={2}>
61
- <text fg={theme.status.modified} content={`~ ${props.status.modified_count} modified`} />
62
- <text fg={theme.green} content={`+ ${props.status.staged_count} staged`} />
63
- </box>
64
- </Show>
65
- <Show when={props.status.untracked_count > 0 || props.status.conflict_count > 0}>
66
- <box flexDirection="row" height={1} gap={2}>
67
- <text fg={theme.status.untracked} content={`? ${props.status.untracked_count} untracked`} />
68
- <text fg={theme.status.conflict} content={`! ${props.status.conflict_count} conflicts`} />
69
- </box>
70
- </Show>
71
- </box>
72
- </Show>
73
-
74
- <Show when={props.status.stash_count > 0}>
75
- <text fg={theme.status.stash} content={`✂ ${props.status.stash_count} stashes`} />
76
- </Show>
77
-
78
- <StatRow label="last commit" value={formatRelativeTime(props.status.head_time)} />
79
- </box>
80
- );
81
- }
82
-
83
- export function StatsPanel(props: StatsPanelProps) {
84
- return (
85
- <box
86
- borderStyle="rounded"
87
- borderColor={props.focused ? theme.border_highlight : theme.border}
88
- title={`stats: ${props.repoName}`}
89
- titleAlignment="left"
90
- flexDirection="column"
91
- flexGrow={1}
92
- height={props.height}
93
- padding={1}
94
- gap={1}
95
- >
96
- <Show
97
- when={!props.loading}
98
- fallback={<text fg={theme.fg_dim}>loading...</text>}
99
- >
100
- <Show
101
- when={props.status}
102
- fallback={<text fg={theme.fg_dim}>(select a repo)</text>}
103
- >
104
- {(status) => (
105
- <Show
106
- when={!status().is_clean}
107
- fallback={<CleanState status={status()} />}
108
- >
109
- <StatusDetails status={status()} />
110
- </Show>
111
- )}
112
- </Show>
113
- </Show>
114
- </box>
115
- );
116
- }
@@ -1,70 +0,0 @@
1
- import { For } from "solid-js";
2
- import type { RepoStatus } from "@overview/core";
3
- import { theme } from "../theme";
4
-
5
- interface StatusBadgeProps {
6
- status: RepoStatus | null;
7
- }
8
-
9
- interface BadgePart {
10
- text: string;
11
- color: string;
12
- }
13
-
14
- function buildBadgeParts(status: RepoStatus): BadgePart[] {
15
- const parts: BadgePart[] = [];
16
-
17
- if (status.ocn_status) {
18
- switch (status.ocn_status.status) {
19
- case "busy":
20
- parts.push({ text: "*", color: theme.yellow });
21
- break;
22
- case "prompting":
23
- parts.push({ text: ">", color: theme.magenta });
24
- break;
25
- case "error":
26
- parts.push({ text: "!", color: theme.red });
27
- break;
28
- }
29
- }
30
-
31
- if (status.health === "conflict") {
32
- parts.push({ text: "!", color: theme.status.conflict });
33
- } else if (status.health === "clean" && status.modified_count === 0 && status.untracked_count === 0) {
34
- parts.push({ text: "✓", color: theme.status.clean });
35
- } else {
36
- if (status.modified_count > 0) {
37
- parts.push({ text: `~${status.modified_count}`, color: theme.status.modified });
38
- }
39
-
40
- if (status.untracked_count > 0 && status.modified_count === 0 && status.ahead === 0 && status.behind === 0) {
41
- parts.push({ text: "?", color: theme.status.untracked });
42
- }
43
-
44
- if (status.ahead > 0) {
45
- parts.push({ text: `↑${status.ahead}`, color: theme.status.ahead });
46
- }
47
-
48
- if (status.behind > 0) {
49
- parts.push({ text: `↓${status.behind}`, color: theme.status.behind });
50
- }
51
- }
52
-
53
- return parts;
54
- }
55
-
56
- export function StatusBadge(props: StatusBadgeProps) {
57
- const parts = () => {
58
- const s = props.status;
59
- if (!s) return [{ text: "…", color: theme.fg_dim }] as BadgePart[];
60
- return buildBadgeParts(s);
61
- };
62
-
63
- return (
64
- <box flexDirection="row">
65
- <For each={parts()}>
66
- {(p) => <text fg={p.color} content={p.text} />}
67
- </For>
68
- </box>
69
- );
70
- }
@@ -1,56 +0,0 @@
1
- import { createMemo, Show } from "solid-js";
2
- import { theme } from "../theme";
3
-
4
- export type AppMode = "NORMAL" | "DETAIL" | "SEARCH" | "HELP";
5
-
6
- interface StatusBarProps {
7
- mode: AppMode;
8
- repoCount: number;
9
- dirtyCount: number;
10
- aheadCount: number;
11
- scanning: boolean;
12
- message: string | null;
13
- widgetSummary?: string;
14
- }
15
-
16
- const KEY_HINTS: Record<AppMode, string> = {
17
- NORMAL: "j/k:nav Enter:expand g:ggi r:refresh q:quit ?:help",
18
- DETAIL: "j/k:scroll h/l:panel g:ggi r:refresh q:back",
19
- SEARCH: "type to filter Esc:cancel",
20
- HELP: "q:close",
21
- };
22
-
23
- export function StatusBar(props: StatusBarProps) {
24
- const keyHints = createMemo(() => KEY_HINTS[props.mode]);
25
-
26
- const summaryColor = createMemo(() => {
27
- if (props.scanning) return theme.fg_dim;
28
- if (props.dirtyCount === 0 && props.aheadCount === 0) return theme.green;
29
- return theme.yellow;
30
- });
31
-
32
- const summaryText = createMemo(() => {
33
- if (props.scanning) return "scanning...";
34
- if (props.dirtyCount === 0 && props.aheadCount === 0) {
35
- return `✓ all ${props.repoCount} repos clean`;
36
- }
37
- const parts: string[] = [];
38
- if (props.dirtyCount > 0) parts.push(`${props.dirtyCount} dirty`);
39
- if (props.aheadCount > 0) parts.push(`${props.aheadCount} ahead`);
40
- return parts.join(", ");
41
- });
42
-
43
- return (
44
- <box height={1} width="100%" backgroundColor={theme.bg_dark} flexDirection="row" paddingLeft={1} paddingRight={1}>
45
- <text fg={theme.blue} content={`[${props.mode}]`} />
46
- <Show when={props.message} fallback={<text fg={theme.fg_dim} content={` ${keyHints()}`} />}>
47
- <text fg={theme.yellow} content={` ${props.message}`} />
48
- </Show>
49
- <box flexGrow={1} />
50
- <Show when={props.widgetSummary}>
51
- <text fg={theme.fg_dim} content={`${props.widgetSummary} `} />
52
- </Show>
53
- <text fg={summaryColor()} content={summaryText()} />
54
- </box>
55
- );
56
- }
@@ -1,286 +0,0 @@
1
- import { For, Show, createMemo, createSignal, createEffect, on } from "solid-js";
2
- import { useKeyboard } from "@opentui/solid";
3
- import type { ScrollBoxRenderable, Renderable } from "@opentui/core";
4
- import type { RepoStatus, WidgetConfig, WidgetId } from "@overview/core";
5
- import { getWidget } from "./widgets/registry";
6
- import "./widgets/index";
7
- import { theme } from "../theme";
8
- import {
9
- computeGridLayout,
10
- buildBorderLine,
11
- buildBorderLineWithTitle,
12
- getWidgetBorderSides,
13
- type GridWidget,
14
- type GridRow,
15
- } from "../lib/widget-grid";
16
-
17
- interface WidgetContainerProps {
18
- status: RepoStatus | null;
19
- repoName: string;
20
- loading: boolean;
21
- focused: boolean;
22
- height: number | `${number}%` | "auto";
23
- availableWidth: number;
24
- widgetConfigs: WidgetConfig[];
25
- onWidgetConfigChange?: (configs: WidgetConfig[]) => void;
26
- }
27
-
28
- export function WidgetContainer(props: WidgetContainerProps) {
29
- const [focused_idx, setFocusedIdx] = createSignal(0);
30
- let scrollbox_ref: ScrollBoxRenderable | undefined;
31
- const row_refs = new Map<number, Renderable>();
32
-
33
- const enabled_widgets = createMemo(() => {
34
- const configs = props.widgetConfigs
35
- .filter((c) => c.enabled)
36
- .sort((a, b) => a.priority - b.priority);
37
- return configs
38
- .map((c): GridWidget | null => {
39
- const def = getWidget(c.id);
40
- if (!def) return null;
41
- return { id: c.id, size_hint: def.size_hint, config: c };
42
- })
43
- .filter((gw): gw is GridWidget => gw !== null);
44
- });
45
-
46
- const content_width = createMemo(() => props.availableWidth - 1);
47
-
48
- const grid_layout = createMemo(() =>
49
- computeGridLayout(enabled_widgets(), content_width()),
50
- );
51
-
52
- const flat_widget_ids = createMemo(() => {
53
- const rows = grid_layout().rows;
54
- const ids: WidgetId[] = [];
55
- for (const row of rows) {
56
- for (const w of row.widgets) {
57
- ids.push(w.id);
58
- }
59
- }
60
- return ids;
61
- });
62
-
63
- function scrollToFocused() {
64
- if (!scrollbox_ref) return;
65
- const focused_id = flat_widget_ids()[focused_idx()];
66
- if (!focused_id) return;
67
-
68
- const rows = grid_layout().rows;
69
-
70
- // Find which row contains the focused widget
71
- let target_row_index = -1;
72
- for (let i = 0; i < rows.length; i++) {
73
- if (rows[i]!.widgets.some((w) => w.id === focused_id)) {
74
- target_row_index = i;
75
- break;
76
- }
77
- }
78
- if (target_row_index < 0) return;
79
-
80
- // Get the rendered element for this row
81
- const row_el = row_refs.get(target_row_index);
82
- if (!row_el) return;
83
-
84
- // el.y is screen-absolute, not relative to scrollbox content.
85
- // Use content.y as origin — both are screen-absolute and shift together,
86
- // so their difference gives stable content-relative position.
87
- const scroll_top = scrollbox_ref.scrollTop;
88
- const content_y = row_el.y - scrollbox_ref.content.y;
89
- const content_h = row_el.height;
90
-
91
- // The region we want fully visible (in content-space):
92
- // - 1 line above for the border/title text
93
- // - the row box itself
94
- // - 1 line below for the bottom border (only last row)
95
- const region_top = content_y - 1;
96
- const is_last = target_row_index === rows.length - 1;
97
- const region_bottom = content_y + content_h + (is_last ? 1 : 0);
98
-
99
- // Viewport bounds (in content-space)
100
- const vp_top = scroll_top;
101
- const vp_height = scrollbox_ref.viewport?.height ?? scrollbox_ref.height;
102
- const vp_bottom = vp_top + vp_height;
103
-
104
- const top_visible = region_top >= vp_top;
105
- const bottom_visible = region_bottom <= vp_bottom;
106
- const region_height = region_bottom - region_top;
107
-
108
- // Already fully in view — do nothing
109
- if (top_visible && bottom_visible) {
110
- return;
111
- }
112
-
113
- // Region fits within viewport — scroll minimally
114
- if (region_height <= vp_height) {
115
- if (!top_visible) {
116
- // Top is clipped — align top of region with top of viewport
117
- const target = Math.max(0, region_top);
118
- scrollbox_ref.scrollTo({ x: 0, y: target });
119
- } else {
120
- // Bottom is clipped — align bottom of region with bottom of viewport
121
- const target = region_bottom - vp_height;
122
- scrollbox_ref.scrollTo({ x: 0, y: target });
123
- }
124
- } else {
125
- // Region taller than viewport — show the top
126
- const target = Math.max(0, region_top);
127
- scrollbox_ref.scrollTo({ x: 0, y: target });
128
- }
129
- }
130
-
131
- createEffect(on(focused_idx, () => scrollToFocused()));
132
-
133
- useKeyboard((key) => {
134
- if (!props.focused) return;
135
-
136
- const ids = flat_widget_ids();
137
- if (ids.length === 0) return;
138
-
139
- switch (key.name) {
140
- case "j":
141
- setFocusedIdx(Math.min(focused_idx() + 1, ids.length - 1));
142
- return;
143
- case "k":
144
- setFocusedIdx(Math.max(focused_idx() - 1, 0));
145
- return;
146
- }
147
-
148
- if (key.raw === "c") {
149
- const widget_id = ids[focused_idx()];
150
- if (widget_id) {
151
- const updated = props.widgetConfigs.map((c) =>
152
- c.id === widget_id ? { ...c, collapsed: !c.collapsed } : c,
153
- );
154
- props.onWidgetConfigChange?.(updated);
155
- }
156
- return;
157
- }
158
-
159
- if (key.raw === "C") {
160
- const all_collapsed = props.widgetConfigs.every((c) => !c.enabled || c.collapsed);
161
- const updated = props.widgetConfigs.map((c) => ({ ...c, collapsed: !all_collapsed }));
162
- props.onWidgetConfigChange?.(updated);
163
- return;
164
- }
165
- });
166
-
167
- function flatIndexOf(widget_id: WidgetId): number {
168
- return flat_widget_ids().indexOf(widget_id);
169
- }
170
-
171
- function isFocused(widget_id: WidgetId): boolean {
172
- return props.focused && flatIndexOf(widget_id) === focused_idx();
173
- }
174
-
175
- function borderLine(type: "top" | "mid" | "bottom", prev: GridRow | null, next: GridRow | null): string {
176
- return buildBorderLine(type, content_width(), prev, next);
177
- }
178
-
179
- return (
180
- <box flexDirection="column" width={props.availableWidth} height={props.height}>
181
- <Show
182
- when={!props.loading}
183
- fallback={<text fg={theme.fg_dim} content="loading..." />}
184
- >
185
- <Show
186
- when={props.status}
187
- fallback={<text fg={theme.fg_dim} content="(select a repo)" />}
188
- >
189
- <scrollbox ref={scrollbox_ref} flexGrow={1}>
190
- <box flexDirection="column" width={content_width()} flexShrink={0}>
191
- <For each={grid_layout().rows}>
192
- {(row, row_index) => {
193
- const rows = grid_layout().rows;
194
- const prev_row = () => row_index() > 0 ? rows[row_index() - 1]! : null;
195
- const is_first = () => row_index() === 0;
196
- const is_last = () => row_index() === rows.length - 1;
197
-
198
- const top_line = () => {
199
- const type = is_first() ? "top" : "mid";
200
- const line = borderLine(type, prev_row(), row);
201
- if (is_first()) {
202
- return buildBorderLineWithTitle(line, `widgets: ${props.repoName}`);
203
- }
204
- return line;
205
- };
206
-
207
- return (
208
- <>
209
- <text fg={props.focused && is_first() ? theme.border_highlight : theme.border} content={top_line()} />
210
-
211
- <box ref={(el: Renderable) => { row_refs.set(row_index(), el); }} flexDirection="row" alignItems="stretch" width={content_width()}>
212
- <For each={row.widgets}>
213
- {(gw, widget_idx) => {
214
- const def = getWidget(gw.id);
215
- if (!def) return null;
216
-
217
- const focused = () => isFocused(gw.id);
218
- const box_width = () => {
219
- if (row.columns === 1) return content_width();
220
- if (row.columns === 2) {
221
- const junction = Math.floor(content_width() / 2);
222
- if (widget_idx() === 0) return junction;
223
- return content_width() - junction;
224
- }
225
- // 3-column
226
- const j1 = Math.floor(content_width() / 3);
227
- const j2 = Math.floor(2 * content_width() / 3);
228
- if (widget_idx() === 0) return j1;
229
- if (widget_idx() === 1) return j2 - j1;
230
- return content_width() - j2;
231
- };
232
- const widget_content_width = () => {
233
- const sides = getWidgetBorderSides(row, widget_idx());
234
- return Math.max(1, box_width() - sides.length);
235
- };
236
-
237
- return (
238
- <box
239
- width={box_width()}
240
- border={getWidgetBorderSides(row, widget_idx())}
241
- borderStyle="rounded"
242
- borderColor={(focused() || (props.focused && (widget_idx() === 0 || widget_idx() === row.columns - 1))) ? theme.border_highlight : theme.border}
243
- backgroundColor={focused() ? theme.bg_highlight : undefined}
244
- flexDirection="column"
245
- minHeight={gw.size_hint.min_height}
246
- overflow="hidden"
247
- >
248
- <Show
249
- when={!gw.config.collapsed}
250
- fallback={
251
- <text
252
- fg={focused() ? theme.yellow : theme.fg_dim}
253
- content={focused() ? `▸ [>] ${def.label} (collapsed)` : `[>] ${def.label} (collapsed)`}
254
- />
255
- }
256
- >
257
- <text
258
- fg={focused() ? theme.yellow : theme.fg_dim}
259
- content={focused() ? `▸ ${def.label}` : ` ${def.label}`}
260
- />
261
- <def.component
262
- width={widget_content_width()}
263
- focused={focused()}
264
- status={props.status}
265
- />
266
- </Show>
267
- </box>
268
- );
269
- }}
270
- </For>
271
- </box>
272
-
273
- <Show when={is_last()}>
274
- <text fg={props.focused ? theme.border_highlight : theme.border} content={borderLine("bottom", row, null)} />
275
- </Show>
276
- </>
277
- );
278
- }}
279
- </For>
280
- </box>
281
- </scrollbox>
282
- </Show>
283
- </Show>
284
- </box>
285
- );
286
- }