mintree 0.1.8 → 0.1.9

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.
@@ -272,6 +272,83 @@ function buildListRows(issues) {
272
272
  });
273
273
  return rows;
274
274
  }
275
+ /**
276
+ * Splits the grouped list into a pinned-header region and a scrollable body
277
+ * windowed around the selected issue. The selected issue's own project and
278
+ * Status headers are lifted out of the scroll flow so they stay visible
279
+ * ("sticky") while paging through a long group — without them the user loses
280
+ * track of which project/status the rows below belong to.
281
+ *
282
+ * Navigation is unaffected: it still moves `selectedIndex` over the flat
283
+ * issues array; this only decides what's drawn where.
284
+ */
285
+ function windowListRows(listRows, selectedIndex, viewportRows) {
286
+ const selRow = listRows.findIndex((r) => r.kind === "issue" && r.index === selectedIndex);
287
+ const anchor = selRow >= 0 ? selRow : 0;
288
+ // Walk back from the selected row to find its enclosing project and Status
289
+ // headers. A "Sin proyecto" issue has a project header but no status one.
290
+ let projIdx = -1;
291
+ let statusIdx = -1;
292
+ for (let i = anchor; i >= 0; i--) {
293
+ const r = listRows[i];
294
+ if (!r)
295
+ continue;
296
+ if (statusIdx === -1 && r.kind === "status")
297
+ statusIdx = i;
298
+ if (r.kind === "project") {
299
+ projIdx = i;
300
+ break;
301
+ }
302
+ }
303
+ // Pinned rows are pulled out of the body so they never render twice. The
304
+ // blank spacer that precedes a project header is dropped too, so the pane
305
+ // doesn't open with an empty line.
306
+ const pinned = new Set();
307
+ const sticky = [];
308
+ if (projIdx >= 0) {
309
+ pinned.add(projIdx);
310
+ sticky.push(listRows[projIdx]);
311
+ const before = listRows[projIdx - 1];
312
+ if (before && before.kind === "spacer")
313
+ pinned.add(projIdx - 1);
314
+ }
315
+ if (statusIdx >= 0) {
316
+ pinned.add(statusIdx);
317
+ sticky.push(listRows[statusIdx]);
318
+ }
319
+ const body = [];
320
+ let anchorInBody = 0;
321
+ listRows.forEach((r, i) => {
322
+ if (pinned.has(i))
323
+ return;
324
+ if (i === anchor)
325
+ anchorInBody = body.length;
326
+ body.push(r);
327
+ });
328
+ const bodyViewport = Math.max(1, viewportRows - sticky.length);
329
+ const maxStart = Math.max(0, body.length - bodyViewport);
330
+ const start = Math.max(0, Math.min(maxStart, anchorInBody - Math.floor(bodyViewport / 2)));
331
+ const end = Math.min(body.length, start + bodyViewport);
332
+ return {
333
+ sticky,
334
+ body: body.slice(start, end),
335
+ issuesAbove: body.slice(0, start).filter((r) => r.kind === "issue").length,
336
+ issuesBelow: body.slice(end).filter((r) => r.kind === "issue").length,
337
+ };
338
+ }
339
+ // Renders a single grouped-list row — used for both the sticky header region
340
+ // and the scrollable body so the two stay visually identical.
341
+ function ListRowView({ row, selectedIndex, identifierWidth, maxTitleWidth, width, }) {
342
+ if (row.kind === "spacer")
343
+ return _jsx(Text, { children: " " });
344
+ if (row.kind === "project") {
345
+ return _jsx(ProjectHeaderRow, { title: row.title, count: row.count, width: width });
346
+ }
347
+ if (row.kind === "status") {
348
+ return _jsx(StatusHeaderRow, { name: row.name, color: row.color, count: row.count, width: width });
349
+ }
350
+ return (_jsx(IssueListRow, { d: row.d, selected: row.index === selectedIndex, identifierWidth: identifierWidth, maxTitleWidth: maxTitleWidth }));
351
+ }
275
352
  // Word-wraps a single line at `width` columns, breaking on the last space
276
353
  // before the limit when that yields a reasonable cut. Falls back to a hard
277
354
  // cut for unbroken runs (long URLs, code-fence content) so the detail pane
@@ -980,29 +1057,10 @@ export default function Dashboard() {
980
1057
  // value without re-binding on every resize.
981
1058
  listWidthRef.current = listWidth;
982
1059
  // Grouped list: build the project/status header rows interleaved with
983
- // issue rows, then window the rows around the selected issue's row.
984
- // Navigation still moves `selectedIndex` over the flat issues array
985
- // headers and spacers are non-selectable, purely visual rows.
1060
+ // issue rows, then split into a sticky header region (the selected issue's
1061
+ // project + Status, pinned to the top) and a windowed scrollable body.
986
1062
  const listRows = buildListRows(issues);
987
- const selectedRowIdx = listRows.findIndex((r) => r.kind === "issue" && r.index === selectedIndex);
988
- const rowAnchor = selectedRowIdx >= 0 ? selectedRowIdx : 0;
989
- const maxRowStart = Math.max(0, listRows.length - listVisibleRows);
990
- const rowStart = Math.max(0, Math.min(maxRowStart, rowAnchor - Math.floor(listVisibleRows / 2)));
991
- const rowEnd = Math.min(listRows.length, rowStart + listVisibleRows);
992
- const visibleRows = listRows.slice(rowStart, rowEnd);
993
- const issuesAbove = listRows.slice(0, rowStart).filter((r) => r.kind === "issue").length;
994
- const issuesBelow = listRows.slice(rowEnd).filter((r) => r.kind === "issue").length;
1063
+ const listView = windowListRows(listRows, selectedIndex, listVisibleRows);
995
1064
  const listContentWidth = Math.max(8, listWidth - 4);
996
- return (_jsxs(Box, { flexDirection: "column", width: columns, height: rows, children: [_jsx(Box, { paddingX: 1, paddingTop: 0, flexDirection: "column", children: _jsx(HeaderRow, { repoName: repoName, claudeVersion: claudeVersion, issueCount: issues.length, updateAvailable: latestVersion !== null }) }), overlay ? (_jsx(Box, { flexGrow: 1, flexDirection: "column", borderStyle: "round", borderColor: overlay.kind === "remove" ? "yellow" : "cyan", children: overlay.kind === "create" ? (_jsx(CreateOverlayView, { overlay: overlay, onDescChange: onOverlayDescChange, onPromptChange: onOverlayPromptChange })) : (_jsx(RemoveOverlayView, { overlay: overlay })) })) : (_jsxs(Box, { flexGrow: 1, flexDirection: "row", children: [_jsx(Box, { width: listWidth, flexDirection: "column", borderStyle: "round", borderColor: "gray", paddingX: 1, children: issues.length === 0 ? (_jsx(Text, { dimColor: true, children: "No open issues assigned to you in this repo." })) : (_jsxs(_Fragment, { children: [visibleRows.map((row, i) => {
997
- const key = rowStart + i;
998
- if (row.kind === "spacer")
999
- return _jsx(Text, { children: " " }, key);
1000
- if (row.kind === "project") {
1001
- return (_jsx(ProjectHeaderRow, { title: row.title, count: row.count, width: listContentWidth }, key));
1002
- }
1003
- if (row.kind === "status") {
1004
- return (_jsx(StatusHeaderRow, { name: row.name, color: row.color, count: row.count, width: listContentWidth }, key));
1005
- }
1006
- return (_jsx(IssueListRow, { d: row.d, selected: row.index === selectedIndex, identifierWidth: identifierWidth, maxTitleWidth: maxTitleWidth }, key));
1007
- }), issuesAbove > 0 && _jsxs(Text, { dimColor: true, children: ["\u2191 ", issuesAbove, " more above"] }), issuesBelow > 0 && _jsxs(Text, { dimColor: true, children: ["\u2193 ", issuesBelow, " more below"] })] })) }), _jsx(Box, { width: detailWidth, flexDirection: "column", borderStyle: "round", borderColor: "gray", paddingX: 1, children: _jsx(DetailPane, { d: selected, contentWidth: detailWidth - 4, contentHeight: detailContentHeight, scrollOffset: state.detailScrollOffset }) })] })), _jsxs(Box, { paddingX: 1, flexDirection: "column", children: [toast && (_jsx(Box, { children: _jsxs(Text, { color: toast.kind === "success" ? "green" : toast.kind === "error" ? "red" : "cyan", children: [toast.kind === "success" ? "✓ " : toast.kind === "error" ? "✗ " : "· ", toast.text] }) })), refreshing && (_jsxs(Box, { children: [_jsx(Text, { color: "cyan", children: _jsx(Spinner, { type: "dots" }) }), _jsx(Text, { dimColor: true, children: " refreshing" })] })), _jsx(FooterRow, { phase: "ready", overlayKind: overlay?.kind, latestVersion: latestVersion })] })] }));
1065
+ return (_jsxs(Box, { flexDirection: "column", width: columns, height: rows, children: [_jsx(Box, { paddingX: 1, paddingTop: 0, flexDirection: "column", children: _jsx(HeaderRow, { repoName: repoName, claudeVersion: claudeVersion, issueCount: issues.length, updateAvailable: latestVersion !== null }) }), overlay ? (_jsx(Box, { flexGrow: 1, flexDirection: "column", borderStyle: "round", borderColor: overlay.kind === "remove" ? "yellow" : "cyan", children: overlay.kind === "create" ? (_jsx(CreateOverlayView, { overlay: overlay, onDescChange: onOverlayDescChange, onPromptChange: onOverlayPromptChange })) : (_jsx(RemoveOverlayView, { overlay: overlay })) })) : (_jsxs(Box, { flexGrow: 1, flexDirection: "row", children: [_jsx(Box, { width: listWidth, flexDirection: "column", borderStyle: "round", borderColor: "gray", paddingX: 1, children: issues.length === 0 ? (_jsx(Text, { dimColor: true, children: "No open issues assigned to you in this repo." })) : (_jsxs(_Fragment, { children: [listView.sticky.map((row, i) => (_jsx(ListRowView, { row: row, selectedIndex: selectedIndex, identifierWidth: identifierWidth, maxTitleWidth: maxTitleWidth, width: listContentWidth }, `sticky-${i}`))), listView.issuesAbove > 0 && (_jsxs(Text, { dimColor: true, children: ["\u2191 ", listView.issuesAbove, " more above"] })), listView.body.map((row, i) => (_jsx(ListRowView, { row: row, selectedIndex: selectedIndex, identifierWidth: identifierWidth, maxTitleWidth: maxTitleWidth, width: listContentWidth }, `body-${i}`))), listView.issuesBelow > 0 && (_jsxs(Text, { dimColor: true, children: ["\u2193 ", listView.issuesBelow, " more below"] }))] })) }), _jsx(Box, { width: detailWidth, flexDirection: "column", borderStyle: "round", borderColor: "gray", paddingX: 1, children: _jsx(DetailPane, { d: selected, contentWidth: detailWidth - 4, contentHeight: detailContentHeight, scrollOffset: state.detailScrollOffset }) })] })), _jsxs(Box, { paddingX: 1, flexDirection: "column", children: [toast && (_jsx(Box, { children: _jsxs(Text, { color: toast.kind === "success" ? "green" : toast.kind === "error" ? "red" : "cyan", children: [toast.kind === "success" ? "✓ " : toast.kind === "error" ? "✗ " : "· ", toast.text] }) })), refreshing && (_jsxs(Box, { children: [_jsx(Text, { color: "cyan", children: _jsx(Spinner, { type: "dots" }) }), _jsx(Text, { dimColor: true, children: " refreshing" })] })), _jsx(FooterRow, { phase: "ready", overlayKind: overlay?.kind, latestVersion: latestVersion })] })] }));
1008
1066
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mintree",
3
- "version": "0.1.8",
3
+ "version": "0.1.9",
4
4
  "description": "Issue-driven git worktrees + Claude Code sessions for repos with an opinionated SDD+TDD flow.",
5
5
  "license": "MIT",
6
6
  "author": "Martin Mineo <mmineo@canarytechnologies.com>",