@ondrej-svec/hog 1.23.1 → 1.24.1

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/dist/cli.js CHANGED
@@ -70,6 +70,14 @@ function saveFullConfig(config2) {
70
70
  writeFileSync(CONFIG_FILE, `${JSON.stringify(config2, null, 2)}
71
71
  `, { mode: 384 });
72
72
  }
73
+ function validateConfigSchema(raw) {
74
+ const result = HOG_CONFIG_SCHEMA.safeParse(raw);
75
+ if (result.success) {
76
+ return { success: true, data: result.data };
77
+ }
78
+ const messages = result.error.issues.map((i) => ` ${i.path.join(".")}: ${i.message}`);
79
+ return { success: false, error: messages.join("\n") };
80
+ }
73
81
  function loadRawConfig() {
74
82
  if (!existsSync(CONFIG_FILE)) return {};
75
83
  try {
@@ -590,10 +598,13 @@ __export(github_exports, {
590
598
  fetchIssueAsync: () => fetchIssueAsync,
591
599
  fetchIssueCommentsAsync: () => fetchIssueCommentsAsync,
592
600
  fetchProjectEnrichment: () => fetchProjectEnrichment,
601
+ fetchProjectEnrichmentAsync: () => fetchProjectEnrichmentAsync,
593
602
  fetchProjectFields: () => fetchProjectFields,
594
603
  fetchProjectStatusOptions: () => fetchProjectStatusOptions,
604
+ fetchProjectStatusOptionsAsync: () => fetchProjectStatusOptionsAsync,
595
605
  fetchProjectTargetDates: () => fetchProjectTargetDates,
596
606
  fetchRepoIssues: () => fetchRepoIssues,
607
+ fetchRepoIssuesAsync: () => fetchRepoIssuesAsync,
597
608
  fetchRepoLabelsAsync: () => fetchRepoLabelsAsync,
598
609
  removeLabelAsync: () => removeLabelAsync,
599
610
  reopenIssueAsync: () => reopenIssueAsync,
@@ -654,6 +665,105 @@ async function runGhGraphQLAsync(args) {
654
665
  throw err;
655
666
  }
656
667
  }
668
+ function findProjectItemArgs(owner, repoName, issueNumber) {
669
+ return [
670
+ "api",
671
+ "graphql",
672
+ "-f",
673
+ `query=${FIND_PROJECT_ITEM_QUERY}`,
674
+ "-F",
675
+ `owner=${owner}`,
676
+ "-F",
677
+ `repo=${repoName}`,
678
+ "-F",
679
+ `issueNumber=${String(issueNumber)}`
680
+ ];
681
+ }
682
+ function findProjectItemSync(owner, repoName, issueNumber, projectNumber) {
683
+ const result = runGhJson(findProjectItemArgs(owner, repoName, issueNumber));
684
+ const items = result?.data?.repository?.issue?.projectItems?.nodes ?? [];
685
+ return items.find((item) => item?.project?.number === projectNumber) ?? null;
686
+ }
687
+ async function findProjectItemAsync(owner, repoName, issueNumber, projectNumber) {
688
+ const result = await runGhJsonAsync(
689
+ findProjectItemArgs(owner, repoName, issueNumber)
690
+ );
691
+ const items = result?.data?.repository?.issue?.projectItems?.nodes ?? [];
692
+ return items.find((item) => item?.project?.number === projectNumber) ?? null;
693
+ }
694
+ function parseFieldValues(fieldValues, statusKey) {
695
+ const result = {};
696
+ for (const fv of fieldValues) {
697
+ if (!fv) continue;
698
+ const fieldName = fv.field?.name ?? "";
699
+ if ("date" in fv && fv.date && DATE_FIELD_NAME_RE2.test(fieldName)) {
700
+ result.targetDate = fv.date;
701
+ } else if ("name" in fv && fieldName === "Status" && fv.name) {
702
+ result[statusKey] = fv.name;
703
+ } else if (fieldName) {
704
+ const value = "text" in fv && fv.text != null ? fv.text : "number" in fv && fv.number != null ? String(fv.number) : "name" in fv && fv.name != null ? fv.name : "title" in fv && fv.title != null ? fv.title : null;
705
+ if (value != null) {
706
+ if (!result.customFields) result.customFields = {};
707
+ result.customFields[fieldName] = value;
708
+ }
709
+ }
710
+ }
711
+ return result;
712
+ }
713
+ function getProjectNodeIdSync(owner, projectNumber) {
714
+ const key = `${owner}/${String(projectNumber)}`;
715
+ const cached = projectNodeIdCache.get(key);
716
+ if (cached !== void 0) return cached;
717
+ const idFragment = `projectV2(number: $projectNumber) { id }`;
718
+ const projectQuery = `
719
+ query($owner: String!, $projectNumber: Int!) {
720
+ organization(login: $owner) { ${idFragment} }
721
+ user(login: $owner) { ${idFragment} }
722
+ }
723
+ `;
724
+ const projectResult = runGhGraphQL([
725
+ "api",
726
+ "graphql",
727
+ "-f",
728
+ `query=${projectQuery}`,
729
+ "-F",
730
+ `owner=${owner}`,
731
+ "-F",
732
+ `projectNumber=${String(projectNumber)}`
733
+ ]);
734
+ const ownerNode = projectResult?.data?.organization ?? projectResult?.data?.user;
735
+ const projectId = ownerNode?.projectV2?.id;
736
+ if (!projectId) return null;
737
+ projectNodeIdCache.set(key, projectId);
738
+ return projectId;
739
+ }
740
+ async function getProjectNodeId(owner, projectNumber) {
741
+ const key = `${owner}/${String(projectNumber)}`;
742
+ const cached = projectNodeIdCache.get(key);
743
+ if (cached !== void 0) return cached;
744
+ const idFragment = `projectV2(number: $projectNumber) { id }`;
745
+ const projectQuery = `
746
+ query($owner: String!, $projectNumber: Int!) {
747
+ organization(login: $owner) { ${idFragment} }
748
+ user(login: $owner) { ${idFragment} }
749
+ }
750
+ `;
751
+ const projectResult = await runGhGraphQLAsync([
752
+ "api",
753
+ "graphql",
754
+ "-f",
755
+ `query=${projectQuery}`,
756
+ "-F",
757
+ `owner=${owner}`,
758
+ "-F",
759
+ `projectNumber=${String(projectNumber)}`
760
+ ]);
761
+ const ownerNode = projectResult?.data?.organization ?? projectResult?.data?.user;
762
+ const projectId = ownerNode?.projectV2?.id;
763
+ if (!projectId) return null;
764
+ projectNodeIdCache.set(key, projectId);
765
+ return projectId;
766
+ }
657
767
  function fetchAssignedIssues(repo, assignee) {
658
768
  return runGhJson([
659
769
  "issue",
@@ -689,6 +799,25 @@ function fetchRepoIssues(repo, options = {}) {
689
799
  }
690
800
  return runGhJson(args);
691
801
  }
802
+ async function fetchRepoIssuesAsync(repo, options = {}) {
803
+ const { state = "open", limit = 100 } = options;
804
+ const args = [
805
+ "issue",
806
+ "list",
807
+ "--repo",
808
+ repo,
809
+ "--state",
810
+ state,
811
+ "--json",
812
+ "number,title,url,state,updatedAt,labels,assignees,body",
813
+ "--limit",
814
+ String(limit)
815
+ ];
816
+ if (options.assignee) {
817
+ args.push("--assignee", options.assignee);
818
+ }
819
+ return runGhJsonAsync(args);
820
+ }
692
821
  function assignIssue(repo, issueNumber) {
693
822
  runGh(["issue", "edit", String(issueNumber), "--repo", repo, "--add-assignee", "@me"]);
694
823
  }
@@ -769,79 +898,12 @@ async function fetchIssueCommentsAsync(repo, issueNumber) {
769
898
  return result.comments ?? [];
770
899
  }
771
900
  function fetchProjectFields(repo, issueNumber, projectNumber) {
772
- const query = `
773
- query($owner: String!, $repo: String!, $issueNumber: Int!) {
774
- repository(owner: $owner, name: $repo) {
775
- issue(number: $issueNumber) {
776
- projectItems(first: 10) {
777
- nodes {
778
- project { number }
779
- fieldValues(first: 20) {
780
- nodes {
781
- ... on ProjectV2ItemFieldDateValue {
782
- field { ... on ProjectV2Field { name } }
783
- date
784
- }
785
- ... on ProjectV2ItemFieldSingleSelectValue {
786
- field { ... on ProjectV2SingleSelectField { name } }
787
- name
788
- }
789
- ... on ProjectV2ItemFieldTextValue {
790
- field { ... on ProjectV2Field { name } }
791
- text
792
- }
793
- ... on ProjectV2ItemFieldNumberValue {
794
- field { ... on ProjectV2Field { name } }
795
- number
796
- }
797
- ... on ProjectV2ItemFieldIterationValue {
798
- field { ... on ProjectV2IterationField { name } }
799
- title
800
- }
801
- }
802
- }
803
- }
804
- }
805
- }
806
- }
807
- }
808
- `;
809
901
  const [owner, repoName] = repo.split("/");
810
902
  if (!(owner && repoName)) return {};
811
903
  try {
812
- const result = runGhJson([
813
- "api",
814
- "graphql",
815
- "-f",
816
- `query=${query}`,
817
- "-F",
818
- `owner=${owner}`,
819
- "-F",
820
- `repo=${repoName}`,
821
- "-F",
822
- `issueNumber=${String(issueNumber)}`
823
- ]);
824
- const items = result?.data?.repository?.issue?.projectItems?.nodes ?? [];
825
- const projectItem = items.find((item) => item?.project?.number === projectNumber);
904
+ const projectItem = findProjectItemSync(owner, repoName, issueNumber, projectNumber);
826
905
  if (!projectItem) return {};
827
- const fields = {};
828
- const fieldValues = projectItem.fieldValues?.nodes ?? [];
829
- for (const fv of fieldValues) {
830
- if (!fv) continue;
831
- const fieldName = fv.field?.name ?? "";
832
- if ("date" in fv && DATE_FIELD_NAME_RE2.test(fieldName)) {
833
- fields.targetDate = fv.date;
834
- } else if ("name" in fv && fieldName === "Status") {
835
- fields.status = fv.name;
836
- } else if (fieldName) {
837
- const value = "text" in fv && fv.text != null ? fv.text : "number" in fv && fv.number != null ? String(fv.number) : "name" in fv && fv.name != null ? fv.name : "title" in fv && fv.title != null ? fv.title : null;
838
- if (value != null) {
839
- if (!fields.customFields) fields.customFields = {};
840
- fields.customFields[fieldName] = value;
841
- }
842
- }
843
- }
844
- return fields;
906
+ return parseFieldValues(projectItem.fieldValues?.nodes ?? [], "status");
845
907
  } catch {
846
908
  return {};
847
909
  }
@@ -914,23 +976,86 @@ function fetchProjectEnrichment(repo, projectNumber) {
914
976
  const nodes = page?.nodes ?? [];
915
977
  for (const item of nodes) {
916
978
  if (!item?.content?.number) continue;
917
- const enrichment = {};
918
- const fieldValues = item.fieldValues?.nodes ?? [];
919
- for (const fv of fieldValues) {
920
- if (!fv) continue;
921
- const fieldName = fv.field?.name ?? "";
922
- if ("date" in fv && fv.date && DATE_FIELD_NAME_RE2.test(fieldName)) {
923
- enrichment.targetDate = fv.date;
924
- } else if ("name" in fv && fieldName === "Status" && fv.name) {
925
- enrichment.projectStatus = fv.name;
926
- } else if (fieldName) {
927
- const value = "text" in fv && fv.text != null ? fv.text : "number" in fv && fv.number != null ? String(fv.number) : "name" in fv && fv.name != null ? fv.name : "title" in fv && fv.title != null ? fv.title : null;
928
- if (value != null) {
929
- if (!enrichment.customFields) enrichment.customFields = {};
930
- enrichment.customFields[fieldName] = value;
979
+ const enrichment = parseFieldValues(item.fieldValues?.nodes ?? [], "projectStatus");
980
+ enrichMap.set(item.content.number, enrichment);
981
+ }
982
+ if (!page?.pageInfo?.hasNextPage) break;
983
+ cursor = page.pageInfo.endCursor ?? null;
984
+ } while (cursor);
985
+ return enrichMap;
986
+ } catch {
987
+ return /* @__PURE__ */ new Map();
988
+ }
989
+ }
990
+ async function fetchProjectEnrichmentAsync(repo, projectNumber) {
991
+ const [owner] = repo.split("/");
992
+ if (!owner) return /* @__PURE__ */ new Map();
993
+ const projectItemsFragment = `
994
+ projectV2(number: $projectNumber) {
995
+ items(first: 100, after: $cursor) {
996
+ pageInfo { hasNextPage endCursor }
997
+ nodes {
998
+ content {
999
+ ... on Issue {
1000
+ number
1001
+ }
1002
+ }
1003
+ fieldValues(first: 20) {
1004
+ nodes {
1005
+ ... on ProjectV2ItemFieldDateValue {
1006
+ field { ... on ProjectV2Field { name } }
1007
+ date
1008
+ }
1009
+ ... on ProjectV2ItemFieldSingleSelectValue {
1010
+ field { ... on ProjectV2SingleSelectField { name } }
1011
+ name
1012
+ }
1013
+ ... on ProjectV2ItemFieldTextValue {
1014
+ field { ... on ProjectV2Field { name } }
1015
+ text
1016
+ }
1017
+ ... on ProjectV2ItemFieldNumberValue {
1018
+ field { ... on ProjectV2Field { name } }
1019
+ number
1020
+ }
1021
+ ... on ProjectV2ItemFieldIterationValue {
1022
+ field { ... on ProjectV2IterationField { name } }
1023
+ title
1024
+ }
931
1025
  }
932
1026
  }
933
1027
  }
1028
+ }
1029
+ }
1030
+ `;
1031
+ const query = `
1032
+ query($owner: String!, $projectNumber: Int!, $cursor: String) {
1033
+ organization(login: $owner) { ${projectItemsFragment} }
1034
+ user(login: $owner) { ${projectItemsFragment} }
1035
+ }
1036
+ `;
1037
+ try {
1038
+ const enrichMap = /* @__PURE__ */ new Map();
1039
+ let cursor = null;
1040
+ do {
1041
+ const args = [
1042
+ "api",
1043
+ "graphql",
1044
+ "-f",
1045
+ `query=${query}`,
1046
+ "-F",
1047
+ `owner=${owner}`,
1048
+ "-F",
1049
+ `projectNumber=${String(projectNumber)}`
1050
+ ];
1051
+ if (cursor) args.push("-f", `cursor=${cursor}`);
1052
+ const result = await runGhGraphQLAsync(args);
1053
+ const ownerNode = result?.data?.organization ?? result?.data?.user;
1054
+ const page = ownerNode?.projectV2?.items;
1055
+ const nodes = page?.nodes ?? [];
1056
+ for (const item of nodes) {
1057
+ if (!item?.content?.number) continue;
1058
+ const enrichment = parseFieldValues(item.fieldValues?.nodes ?? [], "projectStatus");
934
1059
  enrichMap.set(item.content.number, enrichment);
935
1060
  }
936
1061
  if (!page?.pageInfo?.hasNextPage) break;
@@ -987,6 +1112,44 @@ function fetchProjectStatusOptions(repo, projectNumber, _statusFieldId) {
987
1112
  return [];
988
1113
  }
989
1114
  }
1115
+ async function fetchProjectStatusOptionsAsync(repo, projectNumber, _statusFieldId) {
1116
+ const [owner] = repo.split("/");
1117
+ if (!owner) return [];
1118
+ const statusFragment = `
1119
+ projectV2(number: $projectNumber) {
1120
+ field(name: "Status") {
1121
+ ... on ProjectV2SingleSelectField {
1122
+ options {
1123
+ id
1124
+ name
1125
+ }
1126
+ }
1127
+ }
1128
+ }
1129
+ `;
1130
+ const query = `
1131
+ query($owner: String!, $projectNumber: Int!) {
1132
+ organization(login: $owner) { ${statusFragment} }
1133
+ user(login: $owner) { ${statusFragment} }
1134
+ }
1135
+ `;
1136
+ try {
1137
+ const result = await runGhGraphQLAsync([
1138
+ "api",
1139
+ "graphql",
1140
+ "-f",
1141
+ `query=${query}`,
1142
+ "-F",
1143
+ `owner=${owner}`,
1144
+ "-F",
1145
+ `projectNumber=${String(projectNumber)}`
1146
+ ]);
1147
+ const ownerNode = result?.data?.organization ?? result?.data?.user;
1148
+ return ownerNode?.projectV2?.field?.options ?? [];
1149
+ } catch {
1150
+ return [];
1151
+ }
1152
+ }
990
1153
  function addLabel(repo, issueNumber, label) {
991
1154
  runGh(["issue", "edit", String(issueNumber), "--repo", repo, "--add-label", label]);
992
1155
  }
@@ -1008,88 +1171,18 @@ async function fetchRepoLabelsAsync(repo) {
1008
1171
  function clearProjectNodeIdCache() {
1009
1172
  projectNodeIdCache.clear();
1010
1173
  }
1011
- async function getProjectNodeId(owner, projectNumber) {
1012
- const key = `${owner}/${String(projectNumber)}`;
1013
- const cached = projectNodeIdCache.get(key);
1014
- if (cached !== void 0) return cached;
1015
- const idFragment = `projectV2(number: $projectNumber) { id }`;
1016
- const projectQuery = `
1017
- query($owner: String!, $projectNumber: Int!) {
1018
- organization(login: $owner) { ${idFragment} }
1019
- user(login: $owner) { ${idFragment} }
1020
- }
1021
- `;
1022
- const projectResult = await runGhGraphQLAsync([
1023
- "api",
1024
- "graphql",
1025
- "-f",
1026
- `query=${projectQuery}`,
1027
- "-F",
1028
- `owner=${owner}`,
1029
- "-F",
1030
- `projectNumber=${String(projectNumber)}`
1031
- ]);
1032
- const ownerNode = projectResult?.data?.organization ?? projectResult?.data?.user;
1033
- const projectId = ownerNode?.projectV2?.id;
1034
- if (!projectId) return null;
1035
- projectNodeIdCache.set(key, projectId);
1036
- return projectId;
1037
- }
1038
1174
  function updateProjectItemStatus(repo, issueNumber, projectConfig) {
1039
1175
  const [owner, repoName] = repo.split("/");
1040
1176
  if (!(owner && repoName)) return;
1041
- const findItemQuery = `
1042
- query($owner: String!, $repo: String!, $issueNumber: Int!) {
1043
- repository(owner: $owner, name: $repo) {
1044
- issue(number: $issueNumber) {
1045
- projectItems(first: 10) {
1046
- nodes {
1047
- id
1048
- project { number }
1049
- }
1050
- }
1051
- }
1052
- }
1053
- }
1054
- `;
1055
- const findResult = runGhJson([
1056
- "api",
1057
- "graphql",
1058
- "-f",
1059
- `query=${findItemQuery}`,
1060
- "-F",
1061
- `owner=${owner}`,
1062
- "-F",
1063
- `repo=${repoName}`,
1064
- "-F",
1065
- `issueNumber=${String(issueNumber)}`
1066
- ]);
1067
- const items = findResult?.data?.repository?.issue?.projectItems?.nodes ?? [];
1068
- const projectNumber = projectConfig.projectNumber;
1069
- const projectItem = items.find((item) => item?.project?.number === projectNumber);
1177
+ const projectItem = findProjectItemSync(
1178
+ owner,
1179
+ repoName,
1180
+ issueNumber,
1181
+ projectConfig.projectNumber
1182
+ );
1070
1183
  if (!projectItem?.id) return;
1071
- const idFragment = `projectV2(number: $projectNumber) { id }`;
1072
- const projectQuery = `
1073
- query($owner: String!, $projectNumber: Int!) {
1074
- organization(login: $owner) { ${idFragment} }
1075
- user(login: $owner) { ${idFragment} }
1076
- }
1077
- `;
1078
- const projectResult = runGhGraphQL([
1079
- "api",
1080
- "graphql",
1081
- "-f",
1082
- `query=${projectQuery}`,
1083
- "-F",
1084
- `owner=${owner}`,
1085
- "-F",
1086
- `projectNumber=${String(projectNumber)}`
1087
- ]);
1088
- const projectOwner = projectResult?.data?.organization ?? projectResult?.data?.user;
1089
- const projectId = projectOwner?.projectV2?.id;
1184
+ const projectId = getProjectNodeIdSync(owner, projectConfig.projectNumber);
1090
1185
  if (!projectId) return;
1091
- const statusFieldId = projectConfig.statusFieldId;
1092
- const optionId = projectConfig.optionId;
1093
1186
  const mutation = `
1094
1187
  mutation($projectId: ID!, $itemId: ID!, $fieldId: ID!, $optionId: String!) {
1095
1188
  updateProjectV2ItemFieldValue(
@@ -1114,48 +1207,23 @@ function updateProjectItemStatus(repo, issueNumber, projectConfig) {
1114
1207
  "-F",
1115
1208
  `itemId=${projectItem.id}`,
1116
1209
  "-F",
1117
- `fieldId=${statusFieldId}`,
1210
+ `fieldId=${projectConfig.statusFieldId}`,
1118
1211
  "-F",
1119
- `optionId=${optionId}`
1212
+ `optionId=${projectConfig.optionId}`
1120
1213
  ]);
1121
1214
  }
1122
1215
  async function updateProjectItemStatusAsync(repo, issueNumber, projectConfig) {
1123
1216
  const [owner, repoName] = repo.split("/");
1124
1217
  if (!(owner && repoName)) return;
1125
- const findItemQuery = `
1126
- query($owner: String!, $repo: String!, $issueNumber: Int!) {
1127
- repository(owner: $owner, name: $repo) {
1128
- issue(number: $issueNumber) {
1129
- projectItems(first: 10) {
1130
- nodes {
1131
- id
1132
- project { number }
1133
- }
1134
- }
1135
- }
1136
- }
1137
- }
1138
- `;
1139
- const findResult = await runGhJsonAsync([
1140
- "api",
1141
- "graphql",
1142
- "-f",
1143
- `query=${findItemQuery}`,
1144
- "-F",
1145
- `owner=${owner}`,
1146
- "-F",
1147
- `repo=${repoName}`,
1148
- "-F",
1149
- `issueNumber=${String(issueNumber)}`
1150
- ]);
1151
- const items = findResult?.data?.repository?.issue?.projectItems?.nodes ?? [];
1152
- const projectNumber = projectConfig.projectNumber;
1153
- const projectItem = items.find((item) => item?.project?.number === projectNumber);
1218
+ const projectItem = await findProjectItemAsync(
1219
+ owner,
1220
+ repoName,
1221
+ issueNumber,
1222
+ projectConfig.projectNumber
1223
+ );
1154
1224
  if (!projectItem?.id) return;
1155
- const projectId = await getProjectNodeId(owner, projectNumber);
1225
+ const projectId = await getProjectNodeId(owner, projectConfig.projectNumber);
1156
1226
  if (!projectId) return;
1157
- const statusFieldId = projectConfig.statusFieldId;
1158
- const optionId = projectConfig.optionId;
1159
1227
  const mutation = `
1160
1228
  mutation($projectId: ID!, $itemId: ID!, $fieldId: ID!, $optionId: String!) {
1161
1229
  updateProjectV2ItemFieldValue(
@@ -1180,42 +1248,20 @@ async function updateProjectItemStatusAsync(repo, issueNumber, projectConfig) {
1180
1248
  "-F",
1181
1249
  `itemId=${projectItem.id}`,
1182
1250
  "-F",
1183
- `fieldId=${statusFieldId}`,
1251
+ `fieldId=${projectConfig.statusFieldId}`,
1184
1252
  "-F",
1185
- `optionId=${optionId}`
1253
+ `optionId=${projectConfig.optionId}`
1186
1254
  ]);
1187
1255
  }
1188
1256
  async function updateProjectItemDateAsync(repo, issueNumber, projectConfig, dueDate) {
1189
1257
  const [owner, repoName] = repo.split("/");
1190
1258
  if (!(owner && repoName)) return;
1191
- const findItemQuery = `
1192
- query($owner: String!, $repo: String!, $issueNumber: Int!) {
1193
- repository(owner: $owner, name: $repo) {
1194
- issue(number: $issueNumber) {
1195
- projectItems(first: 10) {
1196
- nodes {
1197
- id
1198
- project { number }
1199
- }
1200
- }
1201
- }
1202
- }
1203
- }
1204
- `;
1205
- const findResult = await runGhJsonAsync([
1206
- "api",
1207
- "graphql",
1208
- "-f",
1209
- `query=${findItemQuery}`,
1210
- "-F",
1211
- `owner=${owner}`,
1212
- "-F",
1213
- `repo=${repoName}`,
1214
- "-F",
1215
- `issueNumber=${String(issueNumber)}`
1216
- ]);
1217
- const items = findResult?.data?.repository?.issue?.projectItems?.nodes ?? [];
1218
- const projectItem = items.find((item) => item?.project?.number === projectConfig.projectNumber);
1259
+ const projectItem = await findProjectItemAsync(
1260
+ owner,
1261
+ repoName,
1262
+ issueNumber,
1263
+ projectConfig.projectNumber
1264
+ );
1219
1265
  if (!projectItem?.id) return;
1220
1266
  const projectId = await getProjectNodeId(owner, projectConfig.projectNumber);
1221
1267
  if (!projectId) return;
@@ -1248,12 +1294,50 @@ async function updateProjectItemDateAsync(repo, issueNumber, projectConfig, dueD
1248
1294
  `date=${dueDate}`
1249
1295
  ]);
1250
1296
  }
1251
- var execFileAsync, DATE_FIELD_NAME_RE2, projectNodeIdCache;
1297
+ var execFileAsync, DATE_FIELD_NAME_RE2, FIND_PROJECT_ITEM_QUERY, projectNodeIdCache;
1252
1298
  var init_github = __esm({
1253
1299
  "src/github.ts"() {
1254
1300
  "use strict";
1255
1301
  execFileAsync = promisify(execFile);
1256
1302
  DATE_FIELD_NAME_RE2 = /^(target\s*date|due\s*date|due|deadline)$/i;
1303
+ FIND_PROJECT_ITEM_QUERY = `
1304
+ query($owner: String!, $repo: String!, $issueNumber: Int!) {
1305
+ repository(owner: $owner, name: $repo) {
1306
+ issue(number: $issueNumber) {
1307
+ projectItems(first: 10) {
1308
+ nodes {
1309
+ id
1310
+ project { number }
1311
+ fieldValues(first: 20) {
1312
+ nodes {
1313
+ ... on ProjectV2ItemFieldDateValue {
1314
+ field { ... on ProjectV2Field { name } }
1315
+ date
1316
+ }
1317
+ ... on ProjectV2ItemFieldSingleSelectValue {
1318
+ field { ... on ProjectV2SingleSelectField { name } }
1319
+ name
1320
+ }
1321
+ ... on ProjectV2ItemFieldTextValue {
1322
+ field { ... on ProjectV2Field { name } }
1323
+ text
1324
+ }
1325
+ ... on ProjectV2ItemFieldNumberValue {
1326
+ field { ... on ProjectV2Field { name } }
1327
+ number
1328
+ }
1329
+ ... on ProjectV2ItemFieldIterationValue {
1330
+ field { ... on ProjectV2IterationField { name } }
1331
+ title
1332
+ }
1333
+ }
1334
+ }
1335
+ }
1336
+ }
1337
+ }
1338
+ }
1339
+ }
1340
+ `;
1257
1341
  projectNodeIdCache = /* @__PURE__ */ new Map();
1258
1342
  }
1259
1343
  });
@@ -1360,6 +1444,182 @@ var init_constants = __esm({
1360
1444
  }
1361
1445
  });
1362
1446
 
1447
+ // src/board/board-tree.ts
1448
+ function resolveStatusGroups(statusOptions, configuredGroups) {
1449
+ if (configuredGroups && configuredGroups.length > 0) {
1450
+ return configuredGroups.map((entry) => {
1451
+ const statuses = entry.split(",").map((s) => s.trim()).filter(Boolean);
1452
+ return { label: statuses[0] ?? entry, statuses };
1453
+ });
1454
+ }
1455
+ const nonTerminal = statusOptions.map((o) => o.name).filter((s) => !isTerminalStatus(s));
1456
+ if (nonTerminal.length > 0 && !nonTerminal.includes("Backlog")) {
1457
+ nonTerminal.push("Backlog");
1458
+ }
1459
+ const order = nonTerminal.length > 0 ? nonTerminal : ["In Progress", "Backlog"];
1460
+ return order.map((s) => ({ label: s, statuses: [s] }));
1461
+ }
1462
+ function issuePriorityRank(issue) {
1463
+ for (const label of issue.labels ?? []) {
1464
+ const rank = PRIORITY_RANK[label.name.toLowerCase()];
1465
+ if (rank != null) return rank;
1466
+ }
1467
+ return 99;
1468
+ }
1469
+ function groupByStatus(issues) {
1470
+ const groups = /* @__PURE__ */ new Map();
1471
+ for (const issue of issues) {
1472
+ const status = issue.projectStatus ?? "Backlog";
1473
+ const list = groups.get(status);
1474
+ if (list) {
1475
+ list.push(issue);
1476
+ } else {
1477
+ groups.set(status, [issue]);
1478
+ }
1479
+ }
1480
+ for (const [, list] of groups) {
1481
+ list.sort((a, b) => issuePriorityRank(a) - issuePriorityRank(b));
1482
+ }
1483
+ return groups;
1484
+ }
1485
+ function buildBoardTree(repos, activity) {
1486
+ const sections = repos.map((rd) => {
1487
+ const sectionId = rd.repo.name;
1488
+ if (rd.error) {
1489
+ return { repo: rd.repo, sectionId, groups: [], error: rd.error };
1490
+ }
1491
+ const statusGroupDefs = resolveStatusGroups(rd.statusOptions, rd.repo.statusGroups);
1492
+ const byStatus = groupByStatus(rd.issues);
1493
+ const coveredKeys = /* @__PURE__ */ new Set();
1494
+ const groups = [];
1495
+ for (const sg of statusGroupDefs) {
1496
+ const issues = [];
1497
+ for (const [status, statusIssues] of byStatus) {
1498
+ if (sg.statuses.some((s) => s.toLowerCase().trim() === status.toLowerCase().trim())) {
1499
+ issues.push(...statusIssues);
1500
+ }
1501
+ }
1502
+ if (issues.length === 0) continue;
1503
+ issues.sort((a, b) => issuePriorityRank(a) - issuePriorityRank(b));
1504
+ groups.push({ label: sg.label, subId: `sub:${sectionId}:${sg.label}`, issues });
1505
+ for (const s of sg.statuses) coveredKeys.add(s.toLowerCase().trim());
1506
+ }
1507
+ for (const [status, statusIssues] of byStatus) {
1508
+ if (!(coveredKeys.has(status.toLowerCase().trim()) || isTerminalStatus(status))) {
1509
+ groups.push({ label: status, subId: `sub:${sectionId}:${status}`, issues: statusIssues });
1510
+ }
1511
+ }
1512
+ return { repo: rd.repo, sectionId, groups, error: null };
1513
+ });
1514
+ return { activity, sections };
1515
+ }
1516
+ function buildNavItemsForRepo(sections, repoName, statusGroupId) {
1517
+ if (!repoName) return [];
1518
+ const section = sections.find((s) => s.sectionId === repoName);
1519
+ if (!section) return [];
1520
+ const activeGroup = section.groups.find((g) => g.subId === statusGroupId) ?? section.groups[0];
1521
+ if (!activeGroup) return [];
1522
+ return activeGroup.issues.map((issue) => ({
1523
+ id: `gh:${section.repo.name}:${issue.number}`,
1524
+ section: repoName,
1525
+ type: "item"
1526
+ }));
1527
+ }
1528
+ function buildFlatRowsForRepo(sections, repoName, statusGroupId) {
1529
+ if (!repoName) {
1530
+ return [
1531
+ {
1532
+ type: "subHeader",
1533
+ key: "select-repo",
1534
+ navId: null,
1535
+ text: "Select a repo in panel [1]"
1536
+ }
1537
+ ];
1538
+ }
1539
+ const section = sections.find((s) => s.sectionId === repoName);
1540
+ if (!section) return [];
1541
+ if (section.error) {
1542
+ return [{ type: "error", key: `error:${repoName}`, navId: null, text: section.error }];
1543
+ }
1544
+ if (section.groups.length === 0) {
1545
+ return [
1546
+ {
1547
+ type: "subHeader",
1548
+ key: `empty:${repoName}`,
1549
+ navId: null,
1550
+ text: "No open issues"
1551
+ }
1552
+ ];
1553
+ }
1554
+ const activeGroup = section.groups.find((g) => g.subId === statusGroupId) ?? section.groups[0];
1555
+ if (!activeGroup) return [];
1556
+ if (activeGroup.issues.length === 0) {
1557
+ return [
1558
+ {
1559
+ type: "subHeader",
1560
+ key: `empty-group:${statusGroupId}`,
1561
+ navId: null,
1562
+ text: "No issues in this status group"
1563
+ }
1564
+ ];
1565
+ }
1566
+ return activeGroup.issues.map((issue) => ({
1567
+ type: "issue",
1568
+ key: `gh:${section.repo.name}:${issue.number}`,
1569
+ navId: `gh:${section.repo.name}:${issue.number}`,
1570
+ issue,
1571
+ repoName: section.repo.name
1572
+ }));
1573
+ }
1574
+ function matchesSearch(issue, query) {
1575
+ if (!query.trim()) return true;
1576
+ const tokens = query.toLowerCase().trim().split(/\s+/);
1577
+ const labels = issue.labels ?? [];
1578
+ const assignees = issue.assignees ?? [];
1579
+ return tokens.every((token) => {
1580
+ if (token.startsWith("#")) {
1581
+ const num = parseInt(token.slice(1), 10);
1582
+ return !Number.isNaN(num) && issue.number === num;
1583
+ }
1584
+ if (token.startsWith("@")) {
1585
+ const login = token.slice(1);
1586
+ return assignees.some((a) => a.login.toLowerCase().includes(login));
1587
+ }
1588
+ if (token === "unassigned") return assignees.length === 0;
1589
+ if (token === "assigned") return assignees.length > 0;
1590
+ if (issue.title.toLowerCase().includes(token)) return true;
1591
+ if (labels.some((l) => l.name.toLowerCase().includes(token))) return true;
1592
+ if (issue.projectStatus?.toLowerCase().includes(token)) return true;
1593
+ if (issue.customFields && Object.values(issue.customFields).some((v) => v.toLowerCase().includes(token)))
1594
+ return true;
1595
+ if (assignees.some((a) => a.login.toLowerCase().includes(token))) return true;
1596
+ return false;
1597
+ });
1598
+ }
1599
+ function findSelectedIssueWithRepo(repos, selectedId) {
1600
+ if (!selectedId?.startsWith("gh:")) return null;
1601
+ for (const rd of repos) {
1602
+ for (const issue of rd.issues) {
1603
+ if (`gh:${rd.repo.name}:${issue.number}` === selectedId)
1604
+ return { issue, repoName: rd.repo.name };
1605
+ }
1606
+ }
1607
+ return null;
1608
+ }
1609
+ var PRIORITY_RANK;
1610
+ var init_board_tree = __esm({
1611
+ "src/board/board-tree.ts"() {
1612
+ "use strict";
1613
+ init_constants();
1614
+ PRIORITY_RANK = {
1615
+ "priority:critical": 0,
1616
+ "priority:high": 1,
1617
+ "priority:medium": 2,
1618
+ "priority:low": 3
1619
+ };
1620
+ }
1621
+ });
1622
+
1363
1623
  // src/board/hooks/use-action-log.ts
1364
1624
  import { useCallback, useRef, useState } from "react";
1365
1625
  function nextEntryId() {
@@ -1427,21 +1687,49 @@ var init_utils = __esm({
1427
1687
  }
1428
1688
  });
1429
1689
 
1430
- // src/board/hooks/use-actions.ts
1431
- import { useCallback as useCallback2, useRef as useRef2 } from "react";
1432
- function findIssueContext(repos, selectedId, config2) {
1433
- if (!selectedId?.startsWith("gh:")) {
1434
- return { issue: null, repoName: null, repoConfig: null, statusOptions: [] };
1435
- }
1690
+ // src/board/board-utils.ts
1691
+ function makeIssueNavId(repoName, issueNumber) {
1692
+ return `gh:${repoName}:${issueNumber}`;
1693
+ }
1694
+ function parseIssueNavId(navId) {
1695
+ if (!navId?.startsWith("gh:")) return null;
1696
+ const parts = navId.split(":");
1697
+ if (parts.length < 3) return null;
1698
+ const num = Number(parts[2]);
1699
+ return Number.isNaN(num) ? null : { repoName: parts[1], issueNumber: num };
1700
+ }
1701
+ function findIssueByNavId(repos, navId) {
1702
+ if (!navId?.startsWith("gh:")) return null;
1436
1703
  for (const rd of repos) {
1437
1704
  for (const issue of rd.issues) {
1438
- if (`gh:${rd.repo.name}:${issue.number}` === selectedId) {
1439
- const repoConfig = config2.repos.find((r) => r.name === rd.repo.name) ?? null;
1440
- return { issue, repoName: rd.repo.name, repoConfig, statusOptions: rd.statusOptions };
1705
+ if (makeIssueNavId(rd.repo.name, issue.number) === navId) {
1706
+ return { issue, repoName: rd.repo.name };
1441
1707
  }
1442
1708
  }
1443
1709
  }
1444
- return { issue: null, repoName: null, repoConfig: null, statusOptions: [] };
1710
+ return null;
1711
+ }
1712
+ var init_board_utils = __esm({
1713
+ "src/board/board-utils.ts"() {
1714
+ "use strict";
1715
+ }
1716
+ });
1717
+
1718
+ // src/board/hooks/use-actions.ts
1719
+ import { useCallback as useCallback2, useRef as useRef2 } from "react";
1720
+ function findIssueContext(repos, selectedId, config2) {
1721
+ const found = findIssueByNavId(repos, selectedId);
1722
+ if (!found) {
1723
+ return { issue: null, repoName: null, repoConfig: null, statusOptions: [] };
1724
+ }
1725
+ const rd = repos.find((r) => r.repo.name === found.repoName);
1726
+ const repoConfig = config2.repos.find((r) => r.name === found.repoName) ?? null;
1727
+ return {
1728
+ issue: found.issue,
1729
+ repoName: found.repoName,
1730
+ repoConfig,
1731
+ statusOptions: rd?.statusOptions ?? []
1732
+ };
1445
1733
  }
1446
1734
  function checkAlreadyAssigned(issue, selfLogin, toast) {
1447
1735
  const assignees = issue.assignees ?? [];
@@ -1863,6 +2151,7 @@ var init_use_actions = __esm({
1863
2151
  init_github();
1864
2152
  init_pick();
1865
2153
  init_utils();
2154
+ init_board_utils();
1866
2155
  init_constants();
1867
2156
  init_use_action_log();
1868
2157
  }
@@ -1995,17 +2284,37 @@ end tell`;
1995
2284
  return { ok: true, value: void 0 };
1996
2285
  }
1997
2286
  case "Terminal": {
1998
- const child = spawn2("open", ["-a", "Terminal", localPath], {
1999
- stdio: "ignore",
2000
- detached: true
2001
- });
2002
- child.unref();
2287
+ const quotedArgs = [command, ...extraArgs, "--", prompt].map(shellQuote).join(" ");
2288
+ const script = `tell application "Terminal"
2289
+ activate
2290
+ do script "cd " & ${JSON.stringify(shellQuote(localPath))} & " && " & ${JSON.stringify(quotedArgs)}
2291
+ end tell`;
2292
+ const result = spawnSync("osascript", ["-e", script], { stdio: "ignore" });
2293
+ if (result.status !== 0) {
2294
+ return {
2295
+ ok: false,
2296
+ error: {
2297
+ kind: "terminal-failed",
2298
+ message: "Terminal.app launch failed."
2299
+ }
2300
+ };
2301
+ }
2003
2302
  return { ok: true, value: void 0 };
2004
2303
  }
2005
2304
  case "Ghostty": {
2006
2305
  const child = spawn2(
2007
2306
  "open",
2008
- ["-na", "Ghostty", "--args", `--working-directory=${localPath}`],
2307
+ [
2308
+ "-na",
2309
+ "Ghostty",
2310
+ "--args",
2311
+ `--working-directory=${localPath}`,
2312
+ "-e",
2313
+ command,
2314
+ ...extraArgs,
2315
+ "--",
2316
+ prompt
2317
+ ],
2009
2318
  { stdio: "ignore", detached: true }
2010
2319
  );
2011
2320
  child.unref();
@@ -2897,7 +3206,8 @@ function useKeyboard({
2897
3206
  onStatusEnter,
2898
3207
  onActivityEnter,
2899
3208
  showDetailPanel,
2900
- leftPanelHidden
3209
+ leftPanelHidden,
3210
+ issuesPageSize
2901
3211
  }) {
2902
3212
  const {
2903
3213
  exit,
@@ -2934,12 +3244,32 @@ function useKeyboard({
2934
3244
  handleToggleZen();
2935
3245
  return;
2936
3246
  }
2937
- if (input2 === "j" || key.downArrow) {
2938
- nav.moveDown();
3247
+ if (input2 === "j" || key.downArrow) {
3248
+ nav.moveDown();
3249
+ return;
3250
+ }
3251
+ if (input2 === "k" || key.upArrow) {
3252
+ nav.moveUp();
3253
+ return;
3254
+ }
3255
+ if (key.ctrl && input2 === "d" || key.pageDown) {
3256
+ nav.moveDownBy(Math.max(1, Math.floor(issuesPageSize / 2)));
3257
+ return;
3258
+ }
3259
+ if (key.ctrl && input2 === "u" || key.pageUp) {
3260
+ nav.moveUpBy(Math.max(1, Math.floor(issuesPageSize / 2)));
3261
+ return;
3262
+ }
3263
+ if (input2 === "G") {
3264
+ nav.goToBottom();
2939
3265
  return;
2940
3266
  }
2941
- if (input2 === "k" || key.upArrow) {
2942
- nav.moveUp();
3267
+ if (key.ctrl && input2 === "f") {
3268
+ nav.moveDownBy(issuesPageSize);
3269
+ return;
3270
+ }
3271
+ if (key.ctrl && input2 === "b") {
3272
+ nav.moveUpBy(issuesPageSize);
2943
3273
  return;
2944
3274
  }
2945
3275
  if (input2 === "q") {
@@ -3027,6 +3357,36 @@ function useKeyboard({
3027
3357
  }
3028
3358
  return;
3029
3359
  }
3360
+ if (key.ctrl && input2 === "d" || key.pageDown) {
3361
+ if (panelFocus.activePanelId === 3) {
3362
+ nav.moveDownBy(Math.max(1, Math.floor(issuesPageSize / 2)));
3363
+ }
3364
+ return;
3365
+ }
3366
+ if (key.ctrl && input2 === "u" || key.pageUp) {
3367
+ if (panelFocus.activePanelId === 3) {
3368
+ nav.moveUpBy(Math.max(1, Math.floor(issuesPageSize / 2)));
3369
+ }
3370
+ return;
3371
+ }
3372
+ if (key.ctrl && input2 === "f") {
3373
+ if (panelFocus.activePanelId === 3) {
3374
+ nav.moveDownBy(issuesPageSize);
3375
+ }
3376
+ return;
3377
+ }
3378
+ if (key.ctrl && input2 === "b") {
3379
+ if (panelFocus.activePanelId === 3) {
3380
+ nav.moveUpBy(issuesPageSize);
3381
+ }
3382
+ return;
3383
+ }
3384
+ if (input2 === "G") {
3385
+ if (panelFocus.activePanelId === 3) {
3386
+ nav.goToBottom();
3387
+ }
3388
+ return;
3389
+ }
3030
3390
  }
3031
3391
  if (ui.state.mode === "multiSelect") {
3032
3392
  if (input2 === " ") {
@@ -3249,7 +3609,8 @@ function useKeyboard({
3249
3609
  handleToggleLeftPanel,
3250
3610
  handleToggleZen,
3251
3611
  showDetailPanel,
3252
- leftPanelHidden
3612
+ leftPanelHidden,
3613
+ issuesPageSize
3253
3614
  ]
3254
3615
  );
3255
3616
  const inputActive = ui.state.mode === "normal" || ui.state.mode === "multiSelect" || ui.state.mode === "focus" || ui.state.mode === "overlay:detail" || ui.state.mode === "zen";
@@ -3474,6 +3835,30 @@ function useNavigation(allItems) {
3474
3835
  const item = visibleItems[newIdx];
3475
3836
  if (item) dispatch({ type: "SELECT", id: item.id, section: item.section });
3476
3837
  }, [selectedIndex, visibleItems]);
3838
+ const moveUpBy = useCallback8(
3839
+ (count) => {
3840
+ const newIdx = Math.max(0, selectedIndex - count);
3841
+ const item = visibleItems[newIdx];
3842
+ if (item) dispatch({ type: "SELECT", id: item.id, section: item.section });
3843
+ },
3844
+ [selectedIndex, visibleItems]
3845
+ );
3846
+ const moveDownBy = useCallback8(
3847
+ (count) => {
3848
+ const newIdx = Math.min(visibleItems.length - 1, selectedIndex + count);
3849
+ const item = visibleItems[newIdx];
3850
+ if (item) dispatch({ type: "SELECT", id: item.id, section: item.section });
3851
+ },
3852
+ [selectedIndex, visibleItems]
3853
+ );
3854
+ const goToTop = useCallback8(() => {
3855
+ const item = visibleItems[0];
3856
+ if (item) dispatch({ type: "SELECT", id: item.id, section: item.section });
3857
+ }, [visibleItems]);
3858
+ const goToBottom = useCallback8(() => {
3859
+ const item = visibleItems[visibleItems.length - 1];
3860
+ if (item) dispatch({ type: "SELECT", id: item.id, section: item.section });
3861
+ }, [visibleItems]);
3477
3862
  const nextSection = useCallback8(() => {
3478
3863
  const currentItem = visibleItems[selectedIndex];
3479
3864
  if (!currentItem) return;
@@ -3518,6 +3903,10 @@ function useNavigation(allItems) {
3518
3903
  collapsedSections: state.collapsedSections,
3519
3904
  moveUp,
3520
3905
  moveDown,
3906
+ moveUpBy,
3907
+ moveDownBy,
3908
+ goToTop,
3909
+ goToBottom,
3521
3910
  nextSection,
3522
3911
  prevSection,
3523
3912
  toggleSection,
@@ -3961,8 +4350,75 @@ var init_use_ui_state = __esm({
3961
4350
  }
3962
4351
  });
3963
4352
 
4353
+ // src/board/hooks/use-viewport-scroll.ts
4354
+ import { useRef as useRef10 } from "react";
4355
+ function computeViewportScroll(totalItems, contentRowCount, cursorIndex, currentOffset) {
4356
+ if (totalItems === 0 || contentRowCount <= 0) {
4357
+ return {
4358
+ scrollOffset: 0,
4359
+ visibleCount: 0,
4360
+ hasMoreAbove: false,
4361
+ hasMoreBelow: false,
4362
+ aboveCount: 0,
4363
+ belowCount: 0
4364
+ };
4365
+ }
4366
+ let offset = currentOffset;
4367
+ const effectiveMargin = Math.min(SCROLL_MARGIN, Math.floor(contentRowCount / 4));
4368
+ if (cursorIndex >= 0) {
4369
+ if (cursorIndex < offset + effectiveMargin) {
4370
+ offset = Math.max(0, cursorIndex - effectiveMargin);
4371
+ }
4372
+ if (cursorIndex >= offset + contentRowCount - effectiveMargin) {
4373
+ offset = cursorIndex - contentRowCount + effectiveMargin + 1;
4374
+ }
4375
+ }
4376
+ const needsAboveIndicator = offset > 0;
4377
+ const tentativeVisibleCount = contentRowCount - (needsAboveIndicator ? 1 : 0);
4378
+ const needsBelowIndicator = offset + tentativeVisibleCount < totalItems;
4379
+ const indicatorRows = (needsAboveIndicator ? 1 : 0) + (needsBelowIndicator ? 1 : 0);
4380
+ const visibleCount = Math.max(1, contentRowCount - indicatorRows);
4381
+ if (cursorIndex >= 0) {
4382
+ if (cursorIndex < offset) {
4383
+ offset = cursorIndex;
4384
+ } else if (cursorIndex >= offset + visibleCount) {
4385
+ offset = cursorIndex - visibleCount + 1;
4386
+ }
4387
+ }
4388
+ const maxOffset = Math.max(0, totalItems - visibleCount);
4389
+ offset = Math.max(0, Math.min(offset, maxOffset));
4390
+ const hasMoreAbove = offset > 0;
4391
+ const hasMoreBelow = offset + visibleCount < totalItems;
4392
+ return {
4393
+ scrollOffset: offset,
4394
+ visibleCount,
4395
+ hasMoreAbove,
4396
+ hasMoreBelow,
4397
+ aboveCount: offset,
4398
+ belowCount: Math.max(0, totalItems - offset - visibleCount)
4399
+ };
4400
+ }
4401
+ function useViewportScroll(totalItems, contentRowCount, cursorIndex, resetKey) {
4402
+ const scrollRef = useRef10(0);
4403
+ const prevResetKeyRef = useRef10(resetKey);
4404
+ if (resetKey !== prevResetKeyRef.current) {
4405
+ prevResetKeyRef.current = resetKey;
4406
+ scrollRef.current = 0;
4407
+ }
4408
+ const result = computeViewportScroll(totalItems, contentRowCount, cursorIndex, scrollRef.current);
4409
+ scrollRef.current = result.scrollOffset;
4410
+ return result;
4411
+ }
4412
+ var SCROLL_MARGIN;
4413
+ var init_use_viewport_scroll = __esm({
4414
+ "src/board/hooks/use-viewport-scroll.ts"() {
4415
+ "use strict";
4416
+ SCROLL_MARGIN = 2;
4417
+ }
4418
+ });
4419
+
3964
4420
  // src/board/hooks/use-workflow-state.ts
3965
- import { useCallback as useCallback12, useRef as useRef10, useState as useState6 } from "react";
4421
+ import { useCallback as useCallback12, useRef as useRef11, useState as useState6 } from "react";
3966
4422
  function resolvePhases(config2, repoConfig) {
3967
4423
  const repoPhases = repoConfig?.workflow?.phases;
3968
4424
  if (repoPhases && repoPhases.length > 0) return repoPhases;
@@ -3988,7 +4444,7 @@ function derivePhaseStatus(phaseName, sessions) {
3988
4444
  }
3989
4445
  function useWorkflowState(config2) {
3990
4446
  const [enrichment, setEnrichment] = useState6(loadEnrichment);
3991
- const enrichmentRef = useRef10(enrichment);
4447
+ const enrichmentRef = useRef11(enrichment);
3992
4448
  enrichmentRef.current = enrichment;
3993
4449
  const reload = useCallback12(() => {
3994
4450
  const data = loadEnrichment();
@@ -4117,12 +4573,9 @@ var init_tmux_pane = __esm({
4117
4573
  });
4118
4574
 
4119
4575
  // src/board/hooks/use-zen-mode.ts
4120
- import { useCallback as useCallback13, useEffect as useEffect5, useRef as useRef11, useState as useState7 } from "react";
4576
+ import { useCallback as useCallback13, useEffect as useEffect5, useRef as useRef12, useState as useState7 } from "react";
4121
4577
  function issueNumberFromId(selectedId) {
4122
- if (!selectedId?.startsWith("gh:")) return null;
4123
- const parts = selectedId.split(":");
4124
- const num = Number(parts[2]);
4125
- return Number.isNaN(num) ? null : num;
4578
+ return parseIssueNavId(selectedId)?.issueNumber ?? null;
4126
4579
  }
4127
4580
  function tryJoinAgent(issueNumber) {
4128
4581
  const winName = agentWindowName(issueNumber);
@@ -4137,7 +4590,7 @@ function useZenMode({
4137
4590
  selectedId
4138
4591
  }) {
4139
4592
  const [zenPaneId, setZenPaneId] = useState7(null);
4140
- const paneRef = useRef11(null);
4593
+ const paneRef = useRef12(null);
4141
4594
  paneRef.current = zenPaneId;
4142
4595
  const cleanupCurrentPane = useCallback13(() => {
4143
4596
  const id = paneRef.current;
@@ -4181,7 +4634,7 @@ function useZenMode({
4181
4634
  },
4182
4635
  [ui.state.mode, cleanupCurrentPane]
4183
4636
  );
4184
- const prevSelectedRef = useRef11(null);
4637
+ const prevSelectedRef = useRef12(null);
4185
4638
  useEffect5(() => {
4186
4639
  if (ui.state.mode !== "zen") {
4187
4640
  prevSelectedRef.current = selectedId;
@@ -4216,6 +4669,7 @@ var ZEN_PANE_WIDTH_PERCENT, ZEN_MIN_COLS, CURSOR_FOLLOW_DEBOUNCE_MS, DEAD_PANE_C
4216
4669
  var init_use_zen_mode = __esm({
4217
4670
  "src/board/hooks/use-zen-mode.ts"() {
4218
4671
  "use strict";
4672
+ init_board_utils();
4219
4673
  init_tmux_pane();
4220
4674
  ZEN_PANE_WIDTH_PERCENT = 65;
4221
4675
  ZEN_MIN_COLS = 100;
@@ -4602,7 +5056,7 @@ function HintBar({
4602
5056
  0: "j/k:scroll Esc:close ? help",
4603
5057
  1: "j/k:move Enter:filter 0-4:panel ? help",
4604
5058
  2: "j/k:move Enter:filter Esc:clear 0-4:panel ? help",
4605
- 3: `j/k:move Enter:detail g:open p:pick m:status c:comment C:claude /:search n:new H:hide-panel Z:zen 0-4:panel${hasUndoable ? " u:undo" : ""} ? help q:quit`,
5059
+ 3: `j/k:move ^d/^u:page G:bottom Enter:detail g:open p:pick m:status c:comment C:claude /:search n:new H:hide-panel Z:zen 0-4:panel${hasUndoable ? " u:undo" : ""} ? help q:quit`,
4606
5060
  4: "j/k:scroll Enter:jump r:refresh 0-4:panel ? help"
4607
5061
  };
4608
5062
  return /* @__PURE__ */ jsxs6(Box6, { children: [
@@ -4625,18 +5079,15 @@ var init_hint_bar = __esm({
4625
5079
  import { Box as Box7, Text as Text7, useInput as useInput2 } from "ink";
4626
5080
  import { useState as useState9 } from "react";
4627
5081
  import { jsx as jsx7, jsxs as jsxs7 } from "react/jsx-runtime";
4628
- function getMenuItems(selectionType) {
4629
- if (selectionType === "github") {
4630
- return [
4631
- { label: "Assign all to me", action: { type: "assign" } },
4632
- { label: "Unassign all from me", action: { type: "unassign" } },
4633
- { label: "Move status (all)", action: { type: "statusChange" } }
4634
- ];
4635
- }
4636
- return [];
5082
+ function getMenuItems() {
5083
+ return [
5084
+ { label: "Assign all to me", action: { type: "assign" } },
5085
+ { label: "Unassign all from me", action: { type: "unassign" } },
5086
+ { label: "Move status (all)", action: { type: "statusChange" } }
5087
+ ];
4637
5088
  }
4638
- function BulkActionMenu({ count, selectionType, onSelect, onCancel }) {
4639
- const items = getMenuItems(selectionType);
5089
+ function BulkActionMenu({ count, onSelect, onCancel }) {
5090
+ const items = getMenuItems();
4640
5091
  const [selectedIdx, setSelectedIdx] = useState9(0);
4641
5092
  useInput2((input2, key) => {
4642
5093
  if (key.escape) return onCancel();
@@ -4652,12 +5103,6 @@ function BulkActionMenu({ count, selectionType, onSelect, onCancel }) {
4652
5103
  setSelectedIdx((i) => Math.max(i - 1, 0));
4653
5104
  }
4654
5105
  });
4655
- if (items.length === 0) {
4656
- return /* @__PURE__ */ jsxs7(Box7, { flexDirection: "column", children: [
4657
- /* @__PURE__ */ jsx7(Text7, { color: "yellow", children: "No bulk actions for mixed selection types." }),
4658
- /* @__PURE__ */ jsx7(Text7, { dimColor: true, children: "Esc to cancel" })
4659
- ] });
4660
- }
4661
5106
  return /* @__PURE__ */ jsxs7(Box7, { flexDirection: "column", children: [
4662
5107
  /* @__PURE__ */ jsxs7(Text7, { color: "cyan", bold: true, children: [
4663
5108
  "Bulk action (",
@@ -4740,7 +5185,7 @@ import { tmpdir } from "os";
4740
5185
  import { join as join5 } from "path";
4741
5186
  import { TextInput } from "@inkjs/ui";
4742
5187
  import { Box as Box8, Text as Text8, useInput as useInput3, useStdin } from "ink";
4743
- import { useEffect as useEffect8, useRef as useRef12, useState as useState10 } from "react";
5188
+ import { useEffect as useEffect8, useRef as useRef13, useState as useState10 } from "react";
4744
5189
  import { jsx as jsx8, jsxs as jsxs8 } from "react/jsx-runtime";
4745
5190
  function CommentInput({
4746
5191
  issueNumber,
@@ -4752,10 +5197,10 @@ function CommentInput({
4752
5197
  const [value, setValue] = useState10("");
4753
5198
  const [editing, setEditing] = useState10(false);
4754
5199
  const { setRawMode } = useStdin();
4755
- const onSubmitRef = useRef12(onSubmit);
4756
- const onCancelRef = useRef12(onCancel);
4757
- const onPauseRef = useRef12(onPauseRefresh);
4758
- const onResumeRef = useRef12(onResumeRefresh);
5200
+ const onSubmitRef = useRef13(onSubmit);
5201
+ const onCancelRef = useRef13(onCancel);
5202
+ const onPauseRef = useRef13(onPauseRefresh);
5203
+ const onResumeRef = useRef13(onResumeRefresh);
4759
5204
  onSubmitRef.current = onSubmit;
4760
5205
  onCancelRef.current = onCancel;
4761
5206
  onPauseRef.current = onPauseRefresh;
@@ -4863,7 +5308,7 @@ var init_confirm_prompt = __esm({
4863
5308
  // src/board/components/label-picker.tsx
4864
5309
  import { Spinner } from "@inkjs/ui";
4865
5310
  import { Box as Box10, Text as Text10, useInput as useInput5 } from "ink";
4866
- import { useEffect as useEffect9, useRef as useRef13, useState as useState11 } from "react";
5311
+ import { useEffect as useEffect9, useRef as useRef14, useState as useState11 } from "react";
4867
5312
  import { jsx as jsx10, jsxs as jsxs10 } from "react/jsx-runtime";
4868
5313
  function LabelPicker({
4869
5314
  repo,
@@ -4878,7 +5323,7 @@ function LabelPicker({
4878
5323
  const [fetchAttempted, setFetchAttempted] = useState11(false);
4879
5324
  const [selected, setSelected] = useState11(new Set(currentLabels));
4880
5325
  const [cursor, setCursor] = useState11(0);
4881
- const submittedRef = useRef13(false);
5326
+ const submittedRef = useRef14(false);
4882
5327
  useEffect9(() => {
4883
5328
  if (labels !== null || fetchAttempted) return;
4884
5329
  setFetchAttempted(true);
@@ -5099,7 +5544,7 @@ import { mkdtempSync as mkdtempSync2, readFileSync as readFileSync7, rmSync as r
5099
5544
  import { tmpdir as tmpdir2 } from "os";
5100
5545
  import { join as join6 } from "path";
5101
5546
  import { Box as Box12, Text as Text12, useStdin as useStdin2 } from "ink";
5102
- import { useEffect as useEffect10, useRef as useRef14, useState as useState13 } from "react";
5547
+ import { useEffect as useEffect10, useRef as useRef15, useState as useState13 } from "react";
5103
5548
  import { jsx as jsx12, jsxs as jsxs12 } from "react/jsx-runtime";
5104
5549
  function buildEditorFile(issue, repoName, statusOptions, repoLabels) {
5105
5550
  const statusNames = statusOptions.map((o) => o.name).join(", ");
@@ -5185,9 +5630,9 @@ function EditIssueOverlay({
5185
5630
  }) {
5186
5631
  const [editing, setEditing] = useState13(true);
5187
5632
  const { setRawMode } = useStdin2();
5188
- const onDoneRef = useRef14(onDone);
5189
- const onPauseRef = useRef14(onPauseRefresh);
5190
- const onResumeRef = useRef14(onResumeRefresh);
5633
+ const onDoneRef = useRef15(onDone);
5634
+ const onPauseRef = useRef15(onPauseRefresh);
5635
+ const onResumeRef = useRef15(onResumeRefresh);
5191
5636
  onDoneRef.current = onDone;
5192
5637
  onPauseRef.current = onPauseRefresh;
5193
5638
  onResumeRef.current = onResumeRefresh;
@@ -5359,7 +5804,7 @@ var init_edit_issue_overlay = __esm({
5359
5804
 
5360
5805
  // src/board/components/focus-mode.tsx
5361
5806
  import { Box as Box13, Text as Text13, useInput as useInput7 } from "ink";
5362
- import { useCallback as useCallback14, useEffect as useEffect11, useRef as useRef15, useState as useState14 } from "react";
5807
+ import { useCallback as useCallback14, useEffect as useEffect11, useRef as useRef16, useState as useState14 } from "react";
5363
5808
  import { jsx as jsx13, jsxs as jsxs13 } from "react/jsx-runtime";
5364
5809
  function formatTime(secs) {
5365
5810
  const m = Math.floor(secs / 60);
@@ -5369,7 +5814,7 @@ function formatTime(secs) {
5369
5814
  function FocusMode({ label, durationSec, onExit, onEndAction }) {
5370
5815
  const [remaining, setRemaining] = useState14(durationSec);
5371
5816
  const [timerDone, setTimerDone] = useState14(false);
5372
- const bellSentRef = useRef15(false);
5817
+ const bellSentRef = useRef16(false);
5373
5818
  useEffect11(() => {
5374
5819
  if (timerDone) return;
5375
5820
  const interval = setInterval(() => {
@@ -5467,8 +5912,8 @@ var init_focus_mode = __esm({
5467
5912
  // src/board/components/fuzzy-picker.tsx
5468
5913
  import { TextInput as TextInput3 } from "@inkjs/ui";
5469
5914
  import { Fzf } from "fzf";
5470
- import { Box as Box14, Text as Text14, useInput as useInput8 } from "ink";
5471
- import { useMemo as useMemo4, useState as useState15 } from "react";
5915
+ import { Box as Box14, Text as Text14, useInput as useInput8, useStdout } from "ink";
5916
+ import { useEffect as useEffect12, useMemo as useMemo4, useState as useState15 } from "react";
5472
5917
  import { jsx as jsx14, jsxs as jsxs14 } from "react/jsx-runtime";
5473
5918
  function keepCursorVisible(cursor, offset, visible) {
5474
5919
  if (cursor < offset) return cursor;
@@ -5484,7 +5929,7 @@ function FuzzyPicker({ repos, onSelect, onClose }) {
5484
5929
  for (const rd of repos) {
5485
5930
  for (const issue of rd.issues) {
5486
5931
  items.push({
5487
- navId: `gh:${rd.repo.name}:${issue.number}`,
5932
+ navId: makeIssueNavId(rd.repo.name, issue.number),
5488
5933
  repoShortName: rd.repo.shortName,
5489
5934
  number: issue.number,
5490
5935
  title: issue.title,
@@ -5534,7 +5979,17 @@ function FuzzyPicker({ repos, onSelect, onClose }) {
5534
5979
  upsert(fuzzyIndex.byLabel.find(query), WEIGHTS.label);
5535
5980
  return [...scoreMap.values()].sort((a, b) => b.score - a.score).map((e) => e.item);
5536
5981
  }, [query, fuzzyIndex, allIssues]);
5537
- const VISIBLE = Math.min((process.stdout.rows ?? 24) - 4, 15);
5982
+ const { stdout } = useStdout();
5983
+ const [termRows, setTermRows] = useState15(stdout?.rows ?? 24);
5984
+ useEffect12(() => {
5985
+ if (!stdout) return;
5986
+ const onResize = () => setTermRows(stdout.rows);
5987
+ stdout.on("resize", onResize);
5988
+ return () => {
5989
+ stdout.off("resize", onResize);
5990
+ };
5991
+ }, [stdout]);
5992
+ const VISIBLE = Math.min(termRows - 4, 15);
5538
5993
  useInput8((input2, key) => {
5539
5994
  if (key.downArrow || key.ctrl && input2 === "j") {
5540
5995
  const newCursor = Math.min(cursor + 1, results.length - 1);
@@ -5644,6 +6099,7 @@ function FuzzyPicker({ repos, onSelect, onClose }) {
5644
6099
  var init_fuzzy_picker = __esm({
5645
6100
  "src/board/components/fuzzy-picker.tsx"() {
5646
6101
  "use strict";
6102
+ init_board_utils();
5647
6103
  }
5648
6104
  });
5649
6105
 
@@ -5683,6 +6139,11 @@ var init_help_overlay = __esm({
5683
6139
  items: [
5684
6140
  { key: "j / Down", desc: "Move down" },
5685
6141
  { key: "k / Up", desc: "Move up" },
6142
+ { key: "Ctrl+d / PgDn", desc: "Half-page down" },
6143
+ { key: "Ctrl+u / PgUp", desc: "Half-page up" },
6144
+ { key: "Ctrl+f", desc: "Full page down" },
6145
+ { key: "Ctrl+b", desc: "Full page up" },
6146
+ { key: "G", desc: "Jump to bottom" },
5686
6147
  { key: "Tab", desc: "Next repo tab" },
5687
6148
  { key: "Shift+Tab", desc: "Previous repo tab" },
5688
6149
  { key: "1-9", desc: "Jump to repo tab by number" },
@@ -5744,7 +6205,7 @@ import { tmpdir as tmpdir3 } from "os";
5744
6205
  import { join as join7 } from "path";
5745
6206
  import { Spinner as Spinner2, TextInput as TextInput4 } from "@inkjs/ui";
5746
6207
  import { Box as Box16, Text as Text16, useInput as useInput10, useStdin as useStdin3 } from "ink";
5747
- import { useCallback as useCallback15, useEffect as useEffect12, useRef as useRef16, useState as useState16 } from "react";
6208
+ import { useCallback as useCallback15, useEffect as useEffect13, useRef as useRef17, useState as useState16 } from "react";
5748
6209
  import { jsx as jsx16, jsxs as jsxs16 } from "react/jsx-runtime";
5749
6210
  function NlCreateOverlay({
5750
6211
  repos,
@@ -5763,12 +6224,12 @@ function NlCreateOverlay({
5763
6224
  const [step, setStep] = useState16("input");
5764
6225
  const [body, setBody] = useState16("");
5765
6226
  const [editingBody, setEditingBody] = useState16(false);
5766
- const submittedRef = useRef16(false);
5767
- const parseParamsRef = useRef16(null);
5768
- const onSubmitRef = useRef16(onSubmit);
5769
- const onCancelRef = useRef16(onCancel);
5770
- const onPauseRef = useRef16(onPauseRefresh);
5771
- const onResumeRef = useRef16(onResumeRefresh);
6227
+ const submittedRef = useRef17(false);
6228
+ const parseParamsRef = useRef17(null);
6229
+ const onSubmitRef = useRef17(onSubmit);
6230
+ const onCancelRef = useRef17(onCancel);
6231
+ const onPauseRef = useRef17(onPauseRefresh);
6232
+ const onResumeRef = useRef17(onResumeRefresh);
5772
6233
  onSubmitRef.current = onSubmit;
5773
6234
  onCancelRef.current = onCancel;
5774
6235
  onPauseRef.current = onPauseRefresh;
@@ -5805,7 +6266,7 @@ function NlCreateOverlay({
5805
6266
  setEditingBody(true);
5806
6267
  }
5807
6268
  });
5808
- useEffect12(() => {
6269
+ useEffect13(() => {
5809
6270
  if (!editingBody) return;
5810
6271
  const editor = resolveEditor();
5811
6272
  if (!editor) {
@@ -5849,7 +6310,7 @@ function NlCreateOverlay({
5849
6310
  },
5850
6311
  [selectedRepo, labelCache]
5851
6312
  );
5852
- useEffect12(() => {
6313
+ useEffect13(() => {
5853
6314
  if (!(isParsing && parseParamsRef.current)) return;
5854
6315
  const { input: capturedInput, validLabels } = parseParamsRef.current;
5855
6316
  extractIssueFields(capturedInput, {
@@ -6105,7 +6566,7 @@ var init_search_bar = __esm({
6105
6566
 
6106
6567
  // src/board/components/status-picker.tsx
6107
6568
  import { Box as Box19, Text as Text19, useInput as useInput12 } from "ink";
6108
- import { useRef as useRef17, useState as useState18 } from "react";
6569
+ import { useRef as useRef18, useState as useState18 } from "react";
6109
6570
  import { jsx as jsx19, jsxs as jsxs19 } from "react/jsx-runtime";
6110
6571
  function isTerminal(name) {
6111
6572
  return TERMINAL_STATUS_RE.test(name);
@@ -6158,7 +6619,7 @@ function StatusPicker({
6158
6619
  return idx >= 0 ? idx : 0;
6159
6620
  });
6160
6621
  const [confirmingTerminal, setConfirmingTerminal] = useState18(false);
6161
- const submittedRef = useRef17(false);
6622
+ const submittedRef = useRef18(false);
6162
6623
  useInput12((input2, key) => {
6163
6624
  if (confirmingTerminal) {
6164
6625
  handleConfirmInput(input2, key, {
@@ -6492,7 +6953,6 @@ function OverlayRenderer({
6492
6953
  onConfirmPick,
6493
6954
  onCancelPick,
6494
6955
  multiSelectCount,
6495
- multiSelectType,
6496
6956
  onBulkAction,
6497
6957
  focusLabel,
6498
6958
  focusKey,
@@ -6553,15 +7013,7 @@ function OverlayRenderer({
6553
7013
  onCancel: onCancelPick
6554
7014
  }
6555
7015
  ) : null,
6556
- mode === "overlay:bulkAction" ? /* @__PURE__ */ jsx22(
6557
- BulkActionMenu,
6558
- {
6559
- count: multiSelectCount,
6560
- selectionType: multiSelectType,
6561
- onSelect: onBulkAction,
6562
- onCancel: onExitOverlay
6563
- }
6564
- ) : null,
7016
+ mode === "overlay:bulkAction" ? /* @__PURE__ */ jsx22(BulkActionMenu, { count: multiSelectCount, onSelect: onBulkAction, onCancel: onExitOverlay }) : null,
6565
7017
  mode === "focus" && focusLabel ? /* @__PURE__ */ jsx22(
6566
7018
  FocusMode,
6567
7019
  {
@@ -6689,6 +7141,7 @@ function getDetailWidth(cols) {
6689
7141
  function PanelLayout({
6690
7142
  cols,
6691
7143
  issuesPanelHeight,
7144
+ totalHeight,
6692
7145
  reposPanel,
6693
7146
  statusesPanel,
6694
7147
  issuesPanel,
@@ -6699,7 +7152,7 @@ function PanelLayout({
6699
7152
  const mode = getLayoutMode(cols);
6700
7153
  if (mode === "wide") {
6701
7154
  const detailWidth = getDetailWidth(cols);
6702
- return /* @__PURE__ */ jsxs23(Box22, { flexDirection: "column", children: [
7155
+ return /* @__PURE__ */ jsxs23(Box22, { flexDirection: "column", height: totalHeight, overflow: "hidden", children: [
6703
7156
  /* @__PURE__ */ jsxs23(Box22, { height: issuesPanelHeight, children: [
6704
7157
  !hideLeftPanel ? /* @__PURE__ */ jsxs23(Box22, { flexDirection: "column", width: LEFT_COL_WIDTH, children: [
6705
7158
  reposPanel,
@@ -6712,7 +7165,7 @@ function PanelLayout({
6712
7165
  ] });
6713
7166
  }
6714
7167
  if (mode === "medium") {
6715
- return /* @__PURE__ */ jsxs23(Box22, { flexDirection: "column", children: [
7168
+ return /* @__PURE__ */ jsxs23(Box22, { flexDirection: "column", height: totalHeight, overflow: "hidden", children: [
6716
7169
  /* @__PURE__ */ jsxs23(Box22, { height: issuesPanelHeight, children: [
6717
7170
  !hideLeftPanel ? /* @__PURE__ */ jsxs23(Box22, { flexDirection: "column", width: LEFT_COL_WIDTH, children: [
6718
7171
  reposPanel,
@@ -6723,10 +7176,11 @@ function PanelLayout({
6723
7176
  /* @__PURE__ */ jsx23(Box22, { height: ACTIVITY_HEIGHT, children: activityPanel })
6724
7177
  ] });
6725
7178
  }
6726
- return /* @__PURE__ */ jsxs23(Box22, { flexDirection: "column", children: [
7179
+ const STACKED_LEFT_HEIGHT = 6;
7180
+ return /* @__PURE__ */ jsxs23(Box22, { flexDirection: "column", height: totalHeight, overflow: "hidden", children: [
6727
7181
  !hideLeftPanel ? /* @__PURE__ */ jsxs23(Fragment3, { children: [
6728
- reposPanel,
6729
- statusesPanel
7182
+ /* @__PURE__ */ jsx23(Box22, { height: STACKED_LEFT_HEIGHT, overflow: "hidden", children: reposPanel }),
7183
+ /* @__PURE__ */ jsx23(Box22, { height: STACKED_LEFT_HEIGHT, overflow: "hidden", children: statusesPanel })
6730
7184
  ] }) : null,
6731
7185
  /* @__PURE__ */ jsx23(Box22, { flexGrow: 1, flexDirection: "column", children: issuesPanel }),
6732
7186
  /* @__PURE__ */ jsx23(Box22, { height: ACTIVITY_HEIGHT, children: activityPanel })
@@ -6745,30 +7199,73 @@ var init_panel_layout = __esm({
6745
7199
 
6746
7200
  // src/board/components/repos-panel.tsx
6747
7201
  import { Box as Box23, Text as Text22 } from "ink";
6748
- import { jsx as jsx24, jsxs as jsxs24 } from "react/jsx-runtime";
7202
+ import { Fragment as Fragment4, jsx as jsx24, jsxs as jsxs24 } from "react/jsx-runtime";
6749
7203
  function shortName(fullName) {
6750
7204
  return fullName.includes("/") ? fullName.split("/")[1] ?? fullName : fullName;
6751
7205
  }
6752
- function ReposPanel({ repos, selectedIdx, isActive, width, flexGrow }) {
7206
+ function ReposPanel({
7207
+ repos,
7208
+ selectedIdx,
7209
+ isActive,
7210
+ width,
7211
+ flexGrow,
7212
+ height
7213
+ }) {
6753
7214
  const maxLabel = Math.max(4, width - 8);
6754
- return /* @__PURE__ */ jsx24(Panel, { title: "[1] Repos", isActive, width, flexGrow, children: repos.length === 0 ? /* @__PURE__ */ jsx24(Text22, { color: "gray", children: "\u2014" }) : repos.map((repo, i) => {
6755
- const isSel = i === selectedIdx;
6756
- const label = shortName(repo.name).slice(0, maxLabel);
6757
- return /* @__PURE__ */ jsxs24(Box23, { children: [
6758
- /* @__PURE__ */ jsxs24(Text22, { color: isSel ? "cyan" : isActive ? "white" : "gray", bold: isSel, children: [
6759
- isSel ? "\u25BA " : " ",
6760
- label
6761
- ] }),
6762
- /* @__PURE__ */ jsxs24(Text22, { color: "gray", children: [
6763
- " ",
6764
- repo.openCount
6765
- ] })
6766
- ] }, repo.name);
6767
- }) });
7215
+ const contentRows = height != null ? Math.max(1, height - 2) : repos.length;
7216
+ const needsScroll = repos.length > contentRows;
7217
+ let visibleRepos = repos;
7218
+ let hasMoreAbove = false;
7219
+ let hasMoreBelow = false;
7220
+ let aboveCount = 0;
7221
+ let belowCount = 0;
7222
+ if (needsScroll) {
7223
+ const scroll = computeViewportScroll(
7224
+ repos.length,
7225
+ contentRows,
7226
+ selectedIdx,
7227
+ Math.max(0, selectedIdx - Math.floor(contentRows / 2))
7228
+ );
7229
+ visibleRepos = repos.slice(scroll.scrollOffset, scroll.scrollOffset + scroll.visibleCount);
7230
+ hasMoreAbove = scroll.hasMoreAbove;
7231
+ hasMoreBelow = scroll.hasMoreBelow;
7232
+ aboveCount = scroll.aboveCount;
7233
+ belowCount = scroll.belowCount;
7234
+ }
7235
+ return /* @__PURE__ */ jsx24(Panel, { title: "[1] Repos", isActive, width, flexGrow, height, children: repos.length === 0 ? /* @__PURE__ */ jsx24(Text22, { color: "gray", children: "\u2014" }) : /* @__PURE__ */ jsxs24(Fragment4, { children: [
7236
+ hasMoreAbove ? /* @__PURE__ */ jsxs24(Text22, { color: "gray", dimColor: true, children: [
7237
+ " ",
7238
+ "\u25B2 ",
7239
+ aboveCount,
7240
+ " more"
7241
+ ] }) : null,
7242
+ visibleRepos.map((repo) => {
7243
+ const actualIdx = repos.indexOf(repo);
7244
+ const isSel = actualIdx === selectedIdx;
7245
+ const label = shortName(repo.name).slice(0, maxLabel);
7246
+ return /* @__PURE__ */ jsxs24(Box23, { children: [
7247
+ /* @__PURE__ */ jsxs24(Text22, { color: isSel ? "cyan" : isActive ? "white" : "gray", bold: isSel, children: [
7248
+ isSel ? "\u25BA " : " ",
7249
+ label
7250
+ ] }),
7251
+ /* @__PURE__ */ jsxs24(Text22, { color: "gray", children: [
7252
+ " ",
7253
+ repo.openCount
7254
+ ] })
7255
+ ] }, repo.name);
7256
+ }),
7257
+ hasMoreBelow ? /* @__PURE__ */ jsxs24(Text22, { color: "gray", dimColor: true, children: [
7258
+ " ",
7259
+ "\u25BC ",
7260
+ belowCount,
7261
+ " more"
7262
+ ] }) : null
7263
+ ] }) });
6768
7264
  }
6769
7265
  var init_repos_panel = __esm({
6770
7266
  "src/board/components/repos-panel.tsx"() {
6771
7267
  "use strict";
7268
+ init_use_viewport_scroll();
6772
7269
  init_panel();
6773
7270
  }
6774
7271
  });
@@ -7033,218 +7530,139 @@ function RowRenderer({
7033
7530
  var init_row_renderer = __esm({
7034
7531
  "src/board/components/row-renderer.tsx"() {
7035
7532
  "use strict";
7036
- init_issue_row();
7037
- }
7038
- });
7039
-
7040
- // src/board/components/statuses-panel.tsx
7041
- import { Box as Box26, Text as Text25 } from "ink";
7042
- import { jsx as jsx27, jsxs as jsxs27 } from "react/jsx-runtime";
7043
- function StatusesPanel({
7044
- groups,
7045
- selectedIdx,
7046
- isActive,
7047
- width,
7048
- flexGrow
7049
- }) {
7050
- const maxLabel = Math.max(4, width - 8);
7051
- return /* @__PURE__ */ jsx27(Panel, { title: "[2] Statuses", isActive, width, flexGrow, children: groups.length === 0 ? /* @__PURE__ */ jsx27(Text25, { color: "gray", children: "\u2014" }) : groups.map((group, i) => {
7052
- const isSel = i === selectedIdx;
7053
- const label = group.label.slice(0, maxLabel);
7054
- return /* @__PURE__ */ jsxs27(Box26, { children: [
7055
- /* @__PURE__ */ jsxs27(Text25, { color: isSel ? "cyan" : isActive ? "white" : "gray", bold: isSel, children: [
7056
- isSel ? "\u25BA " : " ",
7057
- label
7058
- ] }),
7059
- /* @__PURE__ */ jsxs27(Text25, { color: "gray", children: [
7060
- " ",
7061
- group.count
7062
- ] })
7063
- ] }, group.id);
7064
- }) });
7065
- }
7066
- var init_statuses_panel = __esm({
7067
- "src/board/components/statuses-panel.tsx"() {
7068
- "use strict";
7069
- init_panel();
7070
- }
7071
- });
7072
-
7073
- // src/board/components/toast-container.tsx
7074
- import { Spinner as Spinner3 } from "@inkjs/ui";
7075
- import { Box as Box27, Text as Text26 } from "ink";
7076
- import { Fragment as Fragment4, jsx as jsx28, jsxs as jsxs28 } from "react/jsx-runtime";
7077
- function ToastContainer({ toasts }) {
7078
- if (toasts.length === 0) return null;
7079
- return /* @__PURE__ */ jsx28(Box27, { flexDirection: "column", children: toasts.map((t) => /* @__PURE__ */ jsx28(Box27, { children: t.type === "loading" ? /* @__PURE__ */ jsxs28(Fragment4, { children: [
7080
- /* @__PURE__ */ jsx28(Spinner3, { label: "" }),
7081
- /* @__PURE__ */ jsxs28(Text26, { color: "cyan", children: [
7082
- " ",
7083
- t.message
7084
- ] })
7085
- ] }) : /* @__PURE__ */ jsxs28(Text26, { color: TYPE_COLORS[t.type], children: [
7086
- TYPE_PREFIXES[t.type],
7087
- " ",
7088
- t.message,
7089
- t.type === "error" ? /* @__PURE__ */ jsx28(Text26, { color: "gray", children: t.retry ? " [r]etry [d]ismiss" : " [d]ismiss" }) : null
7090
- ] }) }, t.id)) });
7091
- }
7092
- var TYPE_COLORS, TYPE_PREFIXES;
7093
- var init_toast_container = __esm({
7094
- "src/board/components/toast-container.tsx"() {
7095
- "use strict";
7096
- TYPE_COLORS = {
7097
- info: "cyan",
7098
- success: "green",
7099
- error: "red",
7100
- loading: "cyan"
7101
- };
7102
- TYPE_PREFIXES = {
7103
- info: "\u2139",
7104
- success: "\u2713",
7105
- error: "\u2717"
7106
- };
7107
- }
7108
- });
7109
-
7110
- // src/board/components/dashboard.tsx
7111
- import { execFile as execFile2, spawn as spawn4 } from "child_process";
7112
- import { Spinner as Spinner4 } from "@inkjs/ui";
7113
- import { Box as Box28, Text as Text27, useApp, useStdout } from "ink";
7114
- import { useCallback as useCallback16, useEffect as useEffect13, useMemo as useMemo5, useRef as useRef18, useState as useState21 } from "react";
7115
- import { Fragment as Fragment5, jsx as jsx29, jsxs as jsxs29 } from "react/jsx-runtime";
7116
- function resolvePhaseConfig(rc, config2, issueTitle, phase) {
7117
- const phasePrompts = rc.workflow?.phasePrompts ?? config2.board.workflow?.phasePrompts ?? {};
7118
- const template = phasePrompts[phase] ?? DEFAULT_PHASE_PROMPTS[phase];
7119
- const startCommand = rc.claudeStartCommand ?? config2.board.claudeStartCommand;
7120
- const slug = issueTitle.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
7121
- return { template, startCommand, slug };
7122
- }
7123
- function resolveStatusGroups(statusOptions, configuredGroups) {
7124
- if (configuredGroups && configuredGroups.length > 0) {
7125
- return configuredGroups.map((entry) => {
7126
- const statuses = entry.split(",").map((s) => s.trim()).filter(Boolean);
7127
- return { label: statuses[0] ?? entry, statuses };
7128
- });
7129
- }
7130
- const nonTerminal = statusOptions.map((o) => o.name).filter((s) => !isTerminalStatus(s));
7131
- if (nonTerminal.length > 0 && !nonTerminal.includes("Backlog")) {
7132
- nonTerminal.push("Backlog");
7133
- }
7134
- const order = nonTerminal.length > 0 ? nonTerminal : ["In Progress", "Backlog"];
7135
- return order.map((s) => ({ label: s, statuses: [s] }));
7136
- }
7137
- function issuePriorityRank(issue) {
7138
- for (const label of issue.labels ?? []) {
7139
- const rank = PRIORITY_RANK[label.name.toLowerCase()];
7140
- if (rank != null) return rank;
7141
- }
7142
- return 99;
7143
- }
7144
- function groupByStatus(issues) {
7145
- const groups = /* @__PURE__ */ new Map();
7146
- for (const issue of issues) {
7147
- const status = issue.projectStatus ?? "Backlog";
7148
- const list = groups.get(status);
7149
- if (list) {
7150
- list.push(issue);
7151
- } else {
7152
- groups.set(status, [issue]);
7153
- }
7154
- }
7155
- for (const [, list] of groups) {
7156
- list.sort((a, b) => issuePriorityRank(a) - issuePriorityRank(b));
7157
- }
7158
- return groups;
7159
- }
7160
- function buildBoardTree(repos, activity) {
7161
- const sections = repos.map((rd) => {
7162
- const sectionId = rd.repo.name;
7163
- if (rd.error) {
7164
- return { repo: rd.repo, sectionId, groups: [], error: rd.error };
7165
- }
7166
- const statusGroupDefs = resolveStatusGroups(rd.statusOptions, rd.repo.statusGroups);
7167
- const byStatus = groupByStatus(rd.issues);
7168
- const coveredKeys = /* @__PURE__ */ new Set();
7169
- const groups = [];
7170
- for (const sg of statusGroupDefs) {
7171
- const issues = [];
7172
- for (const [status, statusIssues] of byStatus) {
7173
- if (sg.statuses.some((s) => s.toLowerCase().trim() === status.toLowerCase().trim())) {
7174
- issues.push(...statusIssues);
7175
- }
7176
- }
7177
- if (issues.length === 0) continue;
7178
- issues.sort((a, b) => issuePriorityRank(a) - issuePriorityRank(b));
7179
- groups.push({ label: sg.label, subId: `sub:${sectionId}:${sg.label}`, issues });
7180
- for (const s of sg.statuses) coveredKeys.add(s.toLowerCase().trim());
7181
- }
7182
- for (const [status, statusIssues] of byStatus) {
7183
- if (!(coveredKeys.has(status.toLowerCase().trim()) || isTerminalStatus(status))) {
7184
- groups.push({ label: status, subId: `sub:${sectionId}:${status}`, issues: statusIssues });
7185
- }
7186
- }
7187
- return { repo: rd.repo, sectionId, groups, error: null };
7188
- });
7189
- return { activity, sections };
7190
- }
7191
- function buildNavItemsForRepo(sections, repoName, statusGroupId) {
7192
- if (!repoName) return [];
7193
- const section = sections.find((s) => s.sectionId === repoName);
7194
- if (!section) return [];
7195
- const activeGroup = section.groups.find((g) => g.subId === statusGroupId) ?? section.groups[0];
7196
- if (!activeGroup) return [];
7197
- return activeGroup.issues.map((issue) => ({
7198
- id: `gh:${section.repo.name}:${issue.number}`,
7199
- section: repoName,
7200
- type: "item"
7201
- }));
7202
- }
7203
- function buildFlatRowsForRepo(sections, repoName, statusGroupId) {
7204
- if (!repoName) {
7205
- return [
7206
- {
7207
- type: "subHeader",
7208
- key: "select-repo",
7209
- navId: null,
7210
- text: "Select a repo in panel [1]"
7211
- }
7212
- ];
7533
+ init_issue_row();
7213
7534
  }
7214
- const section = sections.find((s) => s.sectionId === repoName);
7215
- if (!section) return [];
7216
- if (section.error) {
7217
- return [{ type: "error", key: `error:${repoName}`, navId: null, text: section.error }];
7535
+ });
7536
+
7537
+ // src/board/components/statuses-panel.tsx
7538
+ import { Box as Box26, Text as Text25 } from "ink";
7539
+ import { Fragment as Fragment5, jsx as jsx27, jsxs as jsxs27 } from "react/jsx-runtime";
7540
+ function StatusesPanel({
7541
+ groups,
7542
+ selectedIdx,
7543
+ isActive,
7544
+ width,
7545
+ flexGrow,
7546
+ height
7547
+ }) {
7548
+ const maxLabel = Math.max(4, width - 8);
7549
+ const contentRows = height != null ? Math.max(1, height - 2) : groups.length;
7550
+ const needsScroll = groups.length > contentRows;
7551
+ let visibleGroups = groups;
7552
+ let hasMoreAbove = false;
7553
+ let hasMoreBelow = false;
7554
+ let aboveCount = 0;
7555
+ let belowCount = 0;
7556
+ if (needsScroll) {
7557
+ const scroll = computeViewportScroll(
7558
+ groups.length,
7559
+ contentRows,
7560
+ selectedIdx,
7561
+ Math.max(0, selectedIdx - Math.floor(contentRows / 2))
7562
+ );
7563
+ visibleGroups = groups.slice(scroll.scrollOffset, scroll.scrollOffset + scroll.visibleCount);
7564
+ hasMoreAbove = scroll.hasMoreAbove;
7565
+ hasMoreBelow = scroll.hasMoreBelow;
7566
+ aboveCount = scroll.aboveCount;
7567
+ belowCount = scroll.belowCount;
7218
7568
  }
7219
- if (section.groups.length === 0) {
7220
- return [
7221
- {
7222
- type: "subHeader",
7223
- key: `empty:${repoName}`,
7224
- navId: null,
7225
- text: "No open issues"
7226
- }
7227
- ];
7569
+ return /* @__PURE__ */ jsx27(
7570
+ Panel,
7571
+ {
7572
+ title: "[2] Statuses",
7573
+ isActive,
7574
+ width,
7575
+ flexGrow,
7576
+ height,
7577
+ children: groups.length === 0 ? /* @__PURE__ */ jsx27(Text25, { color: "gray", children: "\u2014" }) : /* @__PURE__ */ jsxs27(Fragment5, { children: [
7578
+ hasMoreAbove ? /* @__PURE__ */ jsxs27(Text25, { color: "gray", dimColor: true, children: [
7579
+ " ",
7580
+ "\u25B2 ",
7581
+ aboveCount,
7582
+ " more"
7583
+ ] }) : null,
7584
+ visibleGroups.map((group) => {
7585
+ const actualIdx = groups.indexOf(group);
7586
+ const isSel = actualIdx === selectedIdx;
7587
+ const label = group.label.slice(0, maxLabel);
7588
+ return /* @__PURE__ */ jsxs27(Box26, { children: [
7589
+ /* @__PURE__ */ jsxs27(Text25, { color: isSel ? "cyan" : isActive ? "white" : "gray", bold: isSel, children: [
7590
+ isSel ? "\u25BA " : " ",
7591
+ label
7592
+ ] }),
7593
+ /* @__PURE__ */ jsxs27(Text25, { color: "gray", children: [
7594
+ " ",
7595
+ group.count
7596
+ ] })
7597
+ ] }, group.id);
7598
+ }),
7599
+ hasMoreBelow ? /* @__PURE__ */ jsxs27(Text25, { color: "gray", dimColor: true, children: [
7600
+ " ",
7601
+ "\u25BC ",
7602
+ belowCount,
7603
+ " more"
7604
+ ] }) : null
7605
+ ] })
7606
+ }
7607
+ );
7608
+ }
7609
+ var init_statuses_panel = __esm({
7610
+ "src/board/components/statuses-panel.tsx"() {
7611
+ "use strict";
7612
+ init_use_viewport_scroll();
7613
+ init_panel();
7228
7614
  }
7229
- const activeGroup = section.groups.find((g) => g.subId === statusGroupId) ?? section.groups[0];
7230
- if (!activeGroup) return [];
7231
- if (activeGroup.issues.length === 0) {
7232
- return [
7233
- {
7234
- type: "subHeader",
7235
- key: `empty-group:${statusGroupId}`,
7236
- navId: null,
7237
- text: "No issues in this status group"
7238
- }
7239
- ];
7615
+ });
7616
+
7617
+ // src/board/components/toast-container.tsx
7618
+ import { Spinner as Spinner3 } from "@inkjs/ui";
7619
+ import { Box as Box27, Text as Text26 } from "ink";
7620
+ import { Fragment as Fragment6, jsx as jsx28, jsxs as jsxs28 } from "react/jsx-runtime";
7621
+ function ToastContainer({ toasts }) {
7622
+ if (toasts.length === 0) return null;
7623
+ return /* @__PURE__ */ jsx28(Box27, { flexDirection: "column", children: toasts.map((t) => /* @__PURE__ */ jsx28(Box27, { children: t.type === "loading" ? /* @__PURE__ */ jsxs28(Fragment6, { children: [
7624
+ /* @__PURE__ */ jsx28(Spinner3, { label: "" }),
7625
+ /* @__PURE__ */ jsxs28(Text26, { color: "cyan", children: [
7626
+ " ",
7627
+ t.message
7628
+ ] })
7629
+ ] }) : /* @__PURE__ */ jsxs28(Text26, { color: TYPE_COLORS[t.type], children: [
7630
+ TYPE_PREFIXES[t.type],
7631
+ " ",
7632
+ t.message,
7633
+ t.type === "error" ? /* @__PURE__ */ jsx28(Text26, { color: "gray", children: t.retry ? " [r]etry [d]ismiss" : " [d]ismiss" }) : null
7634
+ ] }) }, t.id)) });
7635
+ }
7636
+ var TYPE_COLORS, TYPE_PREFIXES;
7637
+ var init_toast_container = __esm({
7638
+ "src/board/components/toast-container.tsx"() {
7639
+ "use strict";
7640
+ TYPE_COLORS = {
7641
+ info: "cyan",
7642
+ success: "green",
7643
+ error: "red",
7644
+ loading: "cyan"
7645
+ };
7646
+ TYPE_PREFIXES = {
7647
+ info: "\u2139",
7648
+ success: "\u2713",
7649
+ error: "\u2717"
7650
+ };
7240
7651
  }
7241
- return activeGroup.issues.map((issue) => ({
7242
- type: "issue",
7243
- key: `gh:${section.repo.name}:${issue.number}`,
7244
- navId: `gh:${section.repo.name}:${issue.number}`,
7245
- issue,
7246
- repoName: section.repo.name
7247
- }));
7652
+ });
7653
+
7654
+ // src/board/components/dashboard.tsx
7655
+ import { execFile as execFile2, spawn as spawn4 } from "child_process";
7656
+ import { Spinner as Spinner4 } from "@inkjs/ui";
7657
+ import { Box as Box28, Text as Text27, useApp, useStdout as useStdout2 } from "ink";
7658
+ import { useCallback as useCallback16, useEffect as useEffect14, useMemo as useMemo5, useRef as useRef19, useState as useState21 } from "react";
7659
+ import { Fragment as Fragment7, jsx as jsx29, jsxs as jsxs29 } from "react/jsx-runtime";
7660
+ function resolvePhaseConfig(rc, config2, issueTitle, phase) {
7661
+ const phasePrompts = rc.workflow?.phasePrompts ?? config2.board.workflow?.phasePrompts ?? {};
7662
+ const template = phasePrompts[phase] ?? DEFAULT_PHASE_PROMPTS[phase];
7663
+ const startCommand = rc.claudeStartCommand ?? config2.board.claudeStartCommand;
7664
+ const slug = issueTitle.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
7665
+ return { template, startCommand, slug };
7248
7666
  }
7249
7667
  function openInBrowser(url) {
7250
7668
  try {
@@ -7255,19 +7673,9 @@ function openInBrowser(url) {
7255
7673
  } catch {
7256
7674
  }
7257
7675
  }
7258
- function findSelectedIssueWithRepo(repos, selectedId) {
7259
- if (!selectedId?.startsWith("gh:")) return null;
7260
- for (const rd of repos) {
7261
- for (const issue of rd.issues) {
7262
- if (`gh:${rd.repo.name}:${issue.number}` === selectedId)
7263
- return { issue, repoName: rd.repo.name };
7264
- }
7265
- }
7266
- return null;
7267
- }
7268
7676
  function RefreshAge({ lastRefresh }) {
7269
7677
  const [, setTick] = useState21(0);
7270
- useEffect13(() => {
7678
+ useEffect14(() => {
7271
7679
  const id = setInterval(() => setTick((t) => t + 1), 3e4);
7272
7680
  return () => clearInterval(id);
7273
7681
  }, []);
@@ -7277,31 +7685,6 @@ function RefreshAge({ lastRefresh }) {
7277
7685
  timeAgo(lastRefresh)
7278
7686
  ] });
7279
7687
  }
7280
- function matchesSearch(issue, query) {
7281
- if (!query.trim()) return true;
7282
- const tokens = query.toLowerCase().trim().split(/\s+/);
7283
- const labels = issue.labels ?? [];
7284
- const assignees = issue.assignees ?? [];
7285
- return tokens.every((token) => {
7286
- if (token.startsWith("#")) {
7287
- const num = parseInt(token.slice(1), 10);
7288
- return !Number.isNaN(num) && issue.number === num;
7289
- }
7290
- if (token.startsWith("@")) {
7291
- const login = token.slice(1);
7292
- return assignees.some((a) => a.login.toLowerCase().includes(login));
7293
- }
7294
- if (token === "unassigned") return assignees.length === 0;
7295
- if (token === "assigned") return assignees.length > 0;
7296
- if (issue.title.toLowerCase().includes(token)) return true;
7297
- if (labels.some((l) => l.name.toLowerCase().includes(token))) return true;
7298
- if (issue.projectStatus?.toLowerCase().includes(token)) return true;
7299
- if (issue.customFields && Object.values(issue.customFields).some((v) => v.toLowerCase().includes(token)))
7300
- return true;
7301
- if (assignees.some((a) => a.login.toLowerCase().includes(token))) return true;
7302
- return false;
7303
- });
7304
- }
7305
7688
  function Dashboard({ config: config2, options, activeProfile }) {
7306
7689
  const { exit } = useApp();
7307
7690
  const refreshMs = config2.board.refreshInterval * 1e3;
@@ -7361,7 +7744,7 @@ function Dashboard({ config: config2, options, activeProfile }) {
7361
7744
  enrichment: workflowState.enrichment,
7362
7745
  onEnrichmentChange: handleEnrichmentChange
7363
7746
  });
7364
- useEffect13(() => {
7747
+ useEffect14(() => {
7365
7748
  const last = logEntries[logEntries.length - 1];
7366
7749
  if (last?.status === "error") setLogVisible(true);
7367
7750
  }, [logEntries]);
@@ -7449,7 +7832,7 @@ function Dashboard({ config: config2, options, activeProfile }) {
7449
7832
  return null;
7450
7833
  }, []);
7451
7834
  const multiSelect = useMultiSelect(getRepoForId);
7452
- useEffect13(() => {
7835
+ useEffect14(() => {
7453
7836
  if (multiSelect.count === 0) return;
7454
7837
  const validIds = new Set(navItems.map((i) => i.id));
7455
7838
  multiSelect.prune(validIds);
@@ -7466,9 +7849,9 @@ function Dashboard({ config: config2, options, activeProfile }) {
7466
7849
  registerPendingMutation,
7467
7850
  clearPendingMutation
7468
7851
  });
7469
- const pendingPickRef = useRef18(null);
7470
- const labelCacheRef = useRef18({});
7471
- const commentCacheRef = useRef18({});
7852
+ const pendingPickRef = useRef19(null);
7853
+ const labelCacheRef = useRef19({});
7854
+ const commentCacheRef = useRef19({});
7472
7855
  const [commentTick, setCommentTick] = useState21(0);
7473
7856
  const handleFetchComments = useCallback16((repo, issueNumber) => {
7474
7857
  const key = `${repo}:${issueNumber}`;
@@ -7563,12 +7946,12 @@ function Dashboard({ config: config2, options, activeProfile }) {
7563
7946
  [toast, ui]
7564
7947
  );
7565
7948
  const [focusKey, setFocusKey] = useState21(0);
7566
- const { stdout } = useStdout();
7949
+ const { stdout } = useStdout2();
7567
7950
  const [termSize, setTermSize] = useState21({
7568
7951
  cols: stdout?.columns ?? 80,
7569
7952
  rows: stdout?.rows ?? 24
7570
7953
  });
7571
- useEffect13(() => {
7954
+ useEffect14(() => {
7572
7955
  if (!stdout) return;
7573
7956
  const onResize = () => setTermSize({ cols: stdout.columns, rows: stdout.rows });
7574
7957
  stdout.on("resize", onResize);
@@ -7619,32 +8002,22 @@ function Dashboard({ config: config2, options, activeProfile }) {
7619
8002
  return { ...row, phaseIndicator, statusAgeDays };
7620
8003
  });
7621
8004
  }, [boardTree.sections, selectedRepoName, selectedStatusGroupId, workflowState, config2.repos]);
7622
- const scrollRef = useRef18(0);
7623
- const prevRepoRef = useRef18(null);
7624
- const prevStatusRef = useRef18(null);
7625
- if (selectedRepoName !== prevRepoRef.current || selectedStatusGroupId !== prevStatusRef.current) {
7626
- prevRepoRef.current = selectedRepoName;
7627
- prevStatusRef.current = selectedStatusGroupId;
7628
- scrollRef.current = 0;
7629
- }
7630
8005
  const selectedRowIdx = useMemo5(
7631
8006
  () => flatRows.findIndex((r) => r.navId === nav.selectedId),
7632
8007
  [flatRows, nav.selectedId]
7633
8008
  );
7634
- if (selectedRowIdx >= 0) {
7635
- if (selectedRowIdx < scrollRef.current) {
7636
- scrollRef.current = selectedRowIdx;
7637
- } else if (selectedRowIdx >= scrollRef.current + contentRowCount) {
7638
- scrollRef.current = selectedRowIdx - contentRowCount + 1;
7639
- }
7640
- }
7641
- const maxOffset = Math.max(0, flatRows.length - contentRowCount);
7642
- scrollRef.current = Math.max(0, Math.min(scrollRef.current, maxOffset));
7643
- const visibleRows = flatRows.slice(scrollRef.current, scrollRef.current + contentRowCount);
7644
- const hasMoreAbove = scrollRef.current > 0;
7645
- const hasMoreBelow = scrollRef.current + contentRowCount < flatRows.length;
7646
- const aboveCount = scrollRef.current;
7647
- const belowCount = flatRows.length - scrollRef.current - contentRowCount;
8009
+ const scrollResetKey = `${selectedRepoName ?? ""}:${selectedStatusGroupId ?? ""}`;
8010
+ const viewport = useViewportScroll(
8011
+ flatRows.length,
8012
+ contentRowCount,
8013
+ selectedRowIdx,
8014
+ scrollResetKey
8015
+ );
8016
+ const { hasMoreAbove, hasMoreBelow, aboveCount, belowCount } = viewport;
8017
+ const visibleRows = flatRows.slice(
8018
+ viewport.scrollOffset,
8019
+ viewport.scrollOffset + viewport.visibleCount
8020
+ );
7648
8021
  const selectedItem = useMemo5(() => {
7649
8022
  const id = nav.selectedId;
7650
8023
  if (!id || isHeaderId(id)) return { issue: null, repoName: null };
@@ -7953,7 +8326,6 @@ function Dashboard({ config: config2, options, activeProfile }) {
7953
8326
  }
7954
8327
  ui.enterTriage();
7955
8328
  }, [nudges.candidates.length, toast, ui]);
7956
- const multiSelectType = "github";
7957
8329
  const handleBulkAction = useCallback16(
7958
8330
  (action) => {
7959
8331
  const ids = multiSelect.selected;
@@ -7987,15 +8359,9 @@ function Dashboard({ config: config2, options, activeProfile }) {
7987
8359
  case "statusChange":
7988
8360
  ui.enterStatus();
7989
8361
  return;
7990
- case "complete":
7991
- case "delete":
7992
- toast.info(`Bulk ${action.type} not yet implemented`);
7993
- ui.exitOverlay();
7994
- multiSelect.clear();
7995
- return;
7996
8362
  }
7997
8363
  },
7998
- [multiSelect, actions, ui, toast]
8364
+ [multiSelect, actions, ui]
7999
8365
  );
8000
8366
  const handleBulkStatusSelect = useCallback16(
8001
8367
  (optionId) => {
@@ -8077,7 +8443,8 @@ function Dashboard({ config: config2, options, activeProfile }) {
8077
8443
  onStatusEnter,
8078
8444
  onActivityEnter,
8079
8445
  showDetailPanel,
8080
- leftPanelHidden
8446
+ leftPanelHidden,
8447
+ issuesPageSize: viewport.visibleCount
8081
8448
  });
8082
8449
  if (status === "loading" && !data) {
8083
8450
  return /* @__PURE__ */ jsx29(Box28, { flexDirection: "column", padding: 1, children: /* @__PURE__ */ jsx29(Spinner4, { label: "Loading dashboard..." }) });
@@ -8178,7 +8545,7 @@ function Dashboard({ config: config2, options, activeProfile }) {
8178
8545
  }
8179
8546
  )
8180
8547
  ] });
8181
- return /* @__PURE__ */ jsxs29(Box28, { flexDirection: "column", paddingX: 1, children: [
8548
+ return /* @__PURE__ */ jsxs29(Box28, { flexDirection: "column", paddingX: 1, height: termSize.rows, overflow: "hidden", children: [
8182
8549
  /* @__PURE__ */ jsxs29(Box28, { children: [
8183
8550
  /* @__PURE__ */ jsx29(Text27, { color: "cyan", bold: true, children: "HOG BOARD" }),
8184
8551
  activeProfile ? /* @__PURE__ */ jsxs29(Text27, { color: "yellow", children: [
@@ -8193,10 +8560,10 @@ function Dashboard({ config: config2, options, activeProfile }) {
8193
8560
  dateStr
8194
8561
  ] }),
8195
8562
  /* @__PURE__ */ jsx29(Text27, { children: " " }),
8196
- isRefreshing ? /* @__PURE__ */ jsxs29(Fragment5, { children: [
8563
+ isRefreshing ? /* @__PURE__ */ jsxs29(Fragment7, { children: [
8197
8564
  /* @__PURE__ */ jsx29(Spinner4, { label: "" }),
8198
8565
  /* @__PURE__ */ jsx29(Text27, { color: "cyan", children: " Refreshing..." })
8199
- ] }) : /* @__PURE__ */ jsxs29(Fragment5, { children: [
8566
+ ] }) : /* @__PURE__ */ jsxs29(Fragment7, { children: [
8200
8567
  /* @__PURE__ */ jsx29(RefreshAge, { lastRefresh }),
8201
8568
  consecutiveFailures > 0 ? /* @__PURE__ */ jsx29(Text27, { color: "red", children: " (!)" }) : null
8202
8569
  ] }),
@@ -8231,7 +8598,6 @@ function Dashboard({ config: config2, options, activeProfile }) {
8231
8598
  onConfirmPick: handleConfirmPick,
8232
8599
  onCancelPick: handleCancelPick,
8233
8600
  multiSelectCount: multiSelect.count,
8234
- multiSelectType,
8235
8601
  onBulkAction: handleBulkAction,
8236
8602
  focusLabel,
8237
8603
  focusKey,
@@ -8320,6 +8686,7 @@ function Dashboard({ config: config2, options, activeProfile }) {
8320
8686
  {
8321
8687
  cols: termSize.cols,
8322
8688
  issuesPanelHeight,
8689
+ totalHeight: totalPanelHeight,
8323
8690
  reposPanel,
8324
8691
  statusesPanel,
8325
8692
  issuesPanel,
@@ -8343,12 +8710,13 @@ function Dashboard({ config: config2, options, activeProfile }) {
8343
8710
  )
8344
8711
  ] });
8345
8712
  }
8346
- var PRIORITY_RANK, CHROME_ROWS;
8713
+ var CHROME_ROWS;
8347
8714
  var init_dashboard = __esm({
8348
8715
  "src/board/components/dashboard.tsx"() {
8349
8716
  "use strict";
8350
8717
  init_clipboard();
8351
8718
  init_github();
8719
+ init_board_tree();
8352
8720
  init_constants();
8353
8721
  init_use_action_log();
8354
8722
  init_use_actions();
@@ -8361,6 +8729,7 @@ var init_dashboard = __esm({
8361
8729
  init_use_nudges();
8362
8730
  init_use_toast();
8363
8731
  init_use_ui_state();
8732
+ init_use_viewport_scroll();
8364
8733
  init_use_workflow_state();
8365
8734
  init_use_zen_mode();
8366
8735
  init_launch_claude();
@@ -8376,12 +8745,6 @@ var init_dashboard = __esm({
8376
8745
  init_row_renderer();
8377
8746
  init_statuses_panel();
8378
8747
  init_toast_container();
8379
- PRIORITY_RANK = {
8380
- "priority:critical": 0,
8381
- "priority:high": 1,
8382
- "priority:medium": 2,
8383
- "priority:low": 3
8384
- };
8385
8748
  CHROME_ROWS = 3;
8386
8749
  }
8387
8750
  });
@@ -8434,7 +8797,8 @@ __export(fetch_exports, {
8434
8797
  fetchDashboard: () => fetchDashboard,
8435
8798
  fetchRecentActivity: () => fetchRecentActivity
8436
8799
  });
8437
- import { execFileSync as execFileSync4 } from "child_process";
8800
+ import { execFile as execFile3, execFileSync as execFileSync4 } from "child_process";
8801
+ import { promisify as promisify2 } from "util";
8438
8802
  function extractSlackUrl(body) {
8439
8803
  if (!body) return void 0;
8440
8804
  const match = body.match(SLACK_URL_RE2);
@@ -8451,6 +8815,116 @@ function extractLinkedIssueNumbers(title, body) {
8451
8815
  if (!matches) return [];
8452
8816
  return [...new Set(matches.map((m) => parseInt(m.slice(1), 10)).filter((n) => n > 0))];
8453
8817
  }
8818
+ function parseActivityOutput(output, shortName2) {
8819
+ const cutoff = Date.now() - 24 * 60 * 60 * 1e3;
8820
+ const events = [];
8821
+ for (const line of output.trim().split("\n")) {
8822
+ if (!line.trim()) continue;
8823
+ try {
8824
+ const ev = JSON.parse(line);
8825
+ const timestamp = new Date(ev.created_at);
8826
+ if (timestamp.getTime() < cutoff) continue;
8827
+ if (ev.type === "CreateEvent") {
8828
+ if (ev.ref_type !== "branch" || !ev.ref) continue;
8829
+ const issueNumbers = extractIssueNumbersFromBranch(ev.ref);
8830
+ for (const num of issueNumbers) {
8831
+ events.push({
8832
+ type: "branch_created",
8833
+ repoShortName: shortName2,
8834
+ issueNumber: num,
8835
+ actor: ev.actor,
8836
+ summary: `created branch ${ev.ref}`,
8837
+ timestamp,
8838
+ branchName: ev.ref
8839
+ });
8840
+ }
8841
+ continue;
8842
+ }
8843
+ if (!ev.number) continue;
8844
+ let eventType;
8845
+ let summary;
8846
+ let extras = {};
8847
+ if (ev.type === "IssueCommentEvent") {
8848
+ eventType = "comment";
8849
+ const preview = ev.body ? ev.body.slice(0, 60).replace(/\n/g, " ") : "";
8850
+ summary = `commented on #${ev.number}${preview ? ` \u2014 "${preview}${(ev.body?.length ?? 0) > 60 ? "..." : ""}"` : ""}`;
8851
+ } else if (ev.type === "IssuesEvent") {
8852
+ switch (ev.action) {
8853
+ case "opened":
8854
+ eventType = "opened";
8855
+ summary = `opened #${ev.number}: ${ev.title ?? ""}`;
8856
+ break;
8857
+ case "closed":
8858
+ eventType = "closed";
8859
+ summary = `closed #${ev.number}`;
8860
+ break;
8861
+ case "assigned":
8862
+ eventType = "assignment";
8863
+ summary = `assigned #${ev.number}`;
8864
+ break;
8865
+ case "labeled":
8866
+ eventType = "labeled";
8867
+ summary = `labeled #${ev.number}`;
8868
+ break;
8869
+ default:
8870
+ continue;
8871
+ }
8872
+ } else if (ev.type === "PullRequestEvent") {
8873
+ const prNumber = ev.number;
8874
+ extras = { prNumber };
8875
+ if (ev.action === "opened") {
8876
+ eventType = "pr_opened";
8877
+ summary = `opened PR #${prNumber}: ${ev.title ?? ""}`;
8878
+ } else if (ev.action === "closed" && ev.merged) {
8879
+ eventType = "pr_merged";
8880
+ summary = `merged PR #${prNumber}: ${ev.title ?? ""}`;
8881
+ } else if (ev.action === "closed") {
8882
+ eventType = "pr_closed";
8883
+ summary = `closed PR #${prNumber}`;
8884
+ } else {
8885
+ continue;
8886
+ }
8887
+ const linkedIssues = extractLinkedIssueNumbers(ev.title, ev.body);
8888
+ for (const issueNum of linkedIssues) {
8889
+ events.push({
8890
+ type: eventType,
8891
+ repoShortName: shortName2,
8892
+ issueNumber: issueNum,
8893
+ actor: ev.actor,
8894
+ summary,
8895
+ timestamp,
8896
+ prNumber
8897
+ });
8898
+ }
8899
+ if (linkedIssues.length === 0) {
8900
+ events.push({
8901
+ type: eventType,
8902
+ repoShortName: shortName2,
8903
+ issueNumber: prNumber,
8904
+ actor: ev.actor,
8905
+ summary,
8906
+ timestamp,
8907
+ prNumber
8908
+ });
8909
+ }
8910
+ continue;
8911
+ } else {
8912
+ continue;
8913
+ }
8914
+ events.push({
8915
+ type: eventType,
8916
+ repoShortName: shortName2,
8917
+ issueNumber: ev.number,
8918
+ actor: ev.actor,
8919
+ summary,
8920
+ timestamp,
8921
+ ...extras
8922
+ });
8923
+ } catch {
8924
+ }
8925
+ }
8926
+ return events.slice(0, 15);
8927
+ }
8454
8928
  function fetchRecentActivity(repoName, shortName2) {
8455
8929
  try {
8456
8930
  const output = execFileSync4(
@@ -8465,114 +8939,61 @@ function fetchRecentActivity(repoName, shortName2) {
8465
8939
  ],
8466
8940
  { encoding: "utf-8", timeout: 15e3, stdio: "pipe" }
8467
8941
  );
8468
- const cutoff = Date.now() - 24 * 60 * 60 * 1e3;
8469
- const events = [];
8470
- for (const line of output.trim().split("\n")) {
8471
- if (!line.trim()) continue;
8472
- try {
8473
- const ev = JSON.parse(line);
8474
- const timestamp = new Date(ev.created_at);
8475
- if (timestamp.getTime() < cutoff) continue;
8476
- if (ev.type === "CreateEvent") {
8477
- if (ev.ref_type !== "branch" || !ev.ref) continue;
8478
- const issueNumbers = extractIssueNumbersFromBranch(ev.ref);
8479
- for (const num of issueNumbers) {
8480
- events.push({
8481
- type: "branch_created",
8482
- repoShortName: shortName2,
8483
- issueNumber: num,
8484
- actor: ev.actor,
8485
- summary: `created branch ${ev.ref}`,
8486
- timestamp,
8487
- branchName: ev.ref
8488
- });
8489
- }
8490
- continue;
8491
- }
8492
- if (!ev.number) continue;
8493
- let eventType;
8494
- let summary;
8495
- let extras = {};
8496
- if (ev.type === "IssueCommentEvent") {
8497
- eventType = "comment";
8498
- const preview = ev.body ? ev.body.slice(0, 60).replace(/\n/g, " ") : "";
8499
- summary = `commented on #${ev.number}${preview ? ` \u2014 "${preview}${(ev.body?.length ?? 0) > 60 ? "..." : ""}"` : ""}`;
8500
- } else if (ev.type === "IssuesEvent") {
8501
- switch (ev.action) {
8502
- case "opened":
8503
- eventType = "opened";
8504
- summary = `opened #${ev.number}: ${ev.title ?? ""}`;
8505
- break;
8506
- case "closed":
8507
- eventType = "closed";
8508
- summary = `closed #${ev.number}`;
8509
- break;
8510
- case "assigned":
8511
- eventType = "assignment";
8512
- summary = `assigned #${ev.number}`;
8513
- break;
8514
- case "labeled":
8515
- eventType = "labeled";
8516
- summary = `labeled #${ev.number}`;
8517
- break;
8518
- default:
8519
- continue;
8520
- }
8521
- } else if (ev.type === "PullRequestEvent") {
8522
- const prNumber = ev.number;
8523
- extras = { prNumber };
8524
- if (ev.action === "opened") {
8525
- eventType = "pr_opened";
8526
- summary = `opened PR #${prNumber}: ${ev.title ?? ""}`;
8527
- } else if (ev.action === "closed" && ev.merged) {
8528
- eventType = "pr_merged";
8529
- summary = `merged PR #${prNumber}: ${ev.title ?? ""}`;
8530
- } else if (ev.action === "closed") {
8531
- eventType = "pr_closed";
8532
- summary = `closed PR #${prNumber}`;
8533
- } else {
8534
- continue;
8535
- }
8536
- const linkedIssues = extractLinkedIssueNumbers(ev.title, ev.body);
8537
- for (const issueNum of linkedIssues) {
8538
- events.push({
8539
- type: eventType,
8540
- repoShortName: shortName2,
8541
- issueNumber: issueNum,
8542
- actor: ev.actor,
8543
- summary,
8544
- timestamp,
8545
- prNumber
8546
- });
8547
- }
8548
- if (linkedIssues.length === 0) {
8549
- events.push({
8550
- type: eventType,
8551
- repoShortName: shortName2,
8552
- issueNumber: prNumber,
8553
- actor: ev.actor,
8554
- summary,
8555
- timestamp,
8556
- prNumber
8557
- });
8558
- }
8559
- continue;
8560
- } else {
8561
- continue;
8562
- }
8563
- events.push({
8564
- type: eventType,
8565
- repoShortName: shortName2,
8566
- issueNumber: ev.number,
8567
- actor: ev.actor,
8568
- summary,
8569
- timestamp,
8570
- ...extras
8571
- });
8572
- } catch {
8573
- }
8942
+ return parseActivityOutput(output, shortName2);
8943
+ } catch {
8944
+ return [];
8945
+ }
8946
+ }
8947
+ async function fetchRepoDataAsync(repo, assignee) {
8948
+ try {
8949
+ const fetchOpts = {};
8950
+ if (assignee) fetchOpts.assignee = assignee;
8951
+ const issues = await fetchRepoIssuesAsync(repo.name, fetchOpts);
8952
+ let enrichedIssues = issues;
8953
+ let statusOptions = [];
8954
+ try {
8955
+ const [enrichMap, opts] = await Promise.all([
8956
+ fetchProjectEnrichmentAsync(repo.name, repo.projectNumber),
8957
+ fetchProjectStatusOptionsAsync(repo.name, repo.projectNumber, repo.statusFieldId)
8958
+ ]);
8959
+ enrichedIssues = issues.map((issue) => {
8960
+ const e = enrichMap.get(issue.number);
8961
+ const slackUrl = extractSlackUrl(issue.body ?? "");
8962
+ return {
8963
+ ...issue,
8964
+ ...e?.targetDate !== void 0 ? { targetDate: e.targetDate } : {},
8965
+ ...e?.projectStatus !== void 0 ? { projectStatus: e.projectStatus } : {},
8966
+ ...e?.customFields !== void 0 ? { customFields: e.customFields } : {},
8967
+ ...slackUrl ? { slackThreadUrl: slackUrl } : {}
8968
+ };
8969
+ });
8970
+ statusOptions = opts;
8971
+ } catch {
8972
+ enrichedIssues = issues.map((issue) => {
8973
+ const slackUrl = extractSlackUrl(issue.body ?? "");
8974
+ return slackUrl ? { ...issue, slackThreadUrl: slackUrl } : issue;
8975
+ });
8574
8976
  }
8575
- return events.slice(0, 15);
8977
+ return { repo, issues: enrichedIssues, statusOptions, error: null };
8978
+ } catch (err) {
8979
+ return { repo, issues: [], statusOptions: [], error: formatError(err) };
8980
+ }
8981
+ }
8982
+ async function fetchRecentActivityAsync(repoName, shortName2) {
8983
+ try {
8984
+ const { stdout } = await execFileAsync2(
8985
+ "gh",
8986
+ [
8987
+ "api",
8988
+ `repos/${repoName}/events`,
8989
+ "-f",
8990
+ "per_page=30",
8991
+ "-q",
8992
+ '.[] | select(.type == "IssuesEvent" or .type == "IssueCommentEvent" or .type == "PullRequestEvent" or .type == "CreateEvent") | {type: .type, actor: .actor.login, action: .payload.action, number: (.payload.issue.number // .payload.pull_request.number), title: (.payload.issue.title // .payload.pull_request.title), body: (.payload.comment.body // .payload.pull_request.body), created_at: .created_at, ref: .payload.ref, ref_type: .payload.ref_type, merged: .payload.pull_request.merged}'
8993
+ ],
8994
+ { encoding: "utf-8", timeout: 15e3 }
8995
+ );
8996
+ return parseActivityOutput(stdout, shortName2);
8576
8997
  } catch {
8577
8998
  return [];
8578
8999
  }
@@ -8581,62 +9002,25 @@ async function fetchDashboard(config2, options = {}) {
8581
9002
  const repos = options.repoFilter ? config2.repos.filter(
8582
9003
  (r) => r.shortName === options.repoFilter || r.name === options.repoFilter
8583
9004
  ) : config2.repos;
8584
- const repoData = repos.map((repo) => {
8585
- try {
8586
- const fetchOpts = {};
8587
- if (options.mineOnly) {
8588
- fetchOpts.assignee = config2.board.assignee;
8589
- }
8590
- const issues = fetchRepoIssues(repo.name, fetchOpts);
8591
- let enrichedIssues = issues;
8592
- let statusOptions = [];
8593
- try {
8594
- const enrichMap = fetchProjectEnrichment(repo.name, repo.projectNumber);
8595
- enrichedIssues = issues.map((issue) => {
8596
- const e = enrichMap.get(issue.number);
8597
- const slackUrl = extractSlackUrl(issue.body ?? "");
8598
- return {
8599
- ...issue,
8600
- ...e?.targetDate !== void 0 ? { targetDate: e.targetDate } : {},
8601
- ...e?.projectStatus !== void 0 ? { projectStatus: e.projectStatus } : {},
8602
- ...e?.customFields !== void 0 ? { customFields: e.customFields } : {},
8603
- ...slackUrl ? { slackThreadUrl: slackUrl } : {}
8604
- };
8605
- });
8606
- statusOptions = fetchProjectStatusOptions(
8607
- repo.name,
8608
- repo.projectNumber,
8609
- repo.statusFieldId
8610
- );
8611
- } catch {
8612
- enrichedIssues = issues.map((issue) => {
8613
- const slackUrl = extractSlackUrl(issue.body ?? "");
8614
- return slackUrl ? { ...issue, slackThreadUrl: slackUrl } : issue;
8615
- });
8616
- }
8617
- return { repo, issues: enrichedIssues, statusOptions, error: null };
8618
- } catch (err) {
8619
- return { repo, issues: [], statusOptions: [], error: formatError(err) };
8620
- }
8621
- });
8622
- const activity = [];
8623
- for (const repo of repos) {
8624
- const events = fetchRecentActivity(repo.name, repo.shortName);
8625
- activity.push(...events);
8626
- }
8627
- activity.sort((a, b) => b.timestamp.getTime() - a.timestamp.getTime());
9005
+ const assignee = options.mineOnly ? config2.board.assignee : void 0;
9006
+ const [repoData, ...activityResults] = await Promise.all([
9007
+ Promise.all(repos.map((repo) => fetchRepoDataAsync(repo, assignee))),
9008
+ ...repos.map((repo) => fetchRecentActivityAsync(repo.name, repo.shortName))
9009
+ ]);
9010
+ const activity = activityResults.flat().sort((a, b) => b.timestamp.getTime() - a.timestamp.getTime());
8628
9011
  return {
8629
9012
  repos: repoData,
8630
9013
  activity: activity.slice(0, 15),
8631
9014
  fetchedAt: /* @__PURE__ */ new Date()
8632
9015
  };
8633
9016
  }
8634
- var SLACK_URL_RE2;
9017
+ var execFileAsync2, SLACK_URL_RE2;
8635
9018
  var init_fetch = __esm({
8636
9019
  "src/board/fetch.ts"() {
8637
9020
  "use strict";
8638
9021
  init_github();
8639
9022
  init_utils();
9023
+ execFileAsync2 = promisify2(execFile3);
8640
9024
  SLACK_URL_RE2 = /https:\/\/[^/]+\.slack\.com\/archives\/[A-Z0-9]+\/p[0-9]+/i;
8641
9025
  }
8642
9026
  });
@@ -8795,8 +9179,8 @@ var init_format_static = __esm({
8795
9179
  // src/cli.ts
8796
9180
  init_ai();
8797
9181
  init_config();
8798
- import { execFile as execFile3, execFileSync as execFileSync5 } from "child_process";
8799
- import { promisify as promisify2 } from "util";
9182
+ import { execFile as execFile4, execFileSync as execFileSync5 } from "child_process";
9183
+ import { promisify as promisify3 } from "util";
8800
9184
  import { Command } from "commander";
8801
9185
 
8802
9186
  // src/init.ts
@@ -9397,7 +9781,7 @@ if (major < 22) {
9397
9781
  );
9398
9782
  process.exit(1);
9399
9783
  }
9400
- var execFileAsync2 = promisify2(execFile3);
9784
+ var execFileAsync3 = promisify3(execFile4);
9401
9785
  async function resolveRef(ref, config2) {
9402
9786
  const { parseIssueRef: parseIssueRef2 } = await Promise.resolve().then(() => (init_pick(), pick_exports));
9403
9787
  try {
@@ -9407,7 +9791,7 @@ async function resolveRef(ref, config2) {
9407
9791
  }
9408
9792
  }
9409
9793
  var program = new Command();
9410
- program.name("hog").description("Personal command deck \u2014 GitHub Projects dashboard with workflow orchestration").version("1.23.1").option("--json", "Force JSON output").option("--human", "Force human-readable output").hook("preAction", (thisCommand) => {
9794
+ program.name("hog").description("Personal command deck \u2014 GitHub Projects dashboard with workflow orchestration").version("1.24.1").option("--json", "Force JSON output").option("--human", "Force human-readable output").hook("preAction", (thisCommand) => {
9411
9795
  const opts = thisCommand.opts();
9412
9796
  if (opts.json) setFormat("json");
9413
9797
  if (opts.human) setFormat("human");
@@ -9534,6 +9918,40 @@ config.command("show").description("Show full configuration").action(() => {
9534
9918
  }
9535
9919
  }
9536
9920
  });
9921
+ config.command("set <path> <value>").description("Set a configuration value (dot-notation path, e.g. board.assignee)").action((path, rawValue) => {
9922
+ const cfg = loadFullConfig();
9923
+ let value = rawValue;
9924
+ if (rawValue === "true") value = true;
9925
+ else if (rawValue === "false") value = false;
9926
+ else if (rawValue !== "" && !Number.isNaN(Number(rawValue))) value = Number(rawValue);
9927
+ const keys = path.split(".");
9928
+ if (keys.length === 0) {
9929
+ errorOut("Invalid path: empty path");
9930
+ }
9931
+ const updated = JSON.parse(JSON.stringify(cfg));
9932
+ let target = updated;
9933
+ for (let i = 0; i < keys.length - 1; i++) {
9934
+ const key = keys[i];
9935
+ const next = target[key];
9936
+ if (next === void 0 || next === null || typeof next !== "object" || Array.isArray(next)) {
9937
+ errorOut(`Invalid path: "${keys.slice(0, i + 1).join(".")}" is not a nested object`);
9938
+ }
9939
+ target = next;
9940
+ }
9941
+ const lastKey = keys[keys.length - 1];
9942
+ target[lastKey] = value;
9943
+ const result = validateConfigSchema(updated);
9944
+ if (!result.success) {
9945
+ errorOut(`Validation failed:
9946
+ ${result.error}`);
9947
+ }
9948
+ saveFullConfig(result.data);
9949
+ if (useJson()) {
9950
+ jsonOut({ ok: true, path, value });
9951
+ } else {
9952
+ printSuccess(`Set ${path} = ${JSON.stringify(value)}`);
9953
+ }
9954
+ });
9537
9955
  config.command("repos").description("List configured repositories").action(() => {
9538
9956
  const cfg = loadFullConfig();
9539
9957
  if (useJson()) {
@@ -9794,7 +10212,7 @@ issueCommand.command("create <text>").description("Create a GitHub issue from na
9794
10212
  }
9795
10213
  try {
9796
10214
  if (json) {
9797
- const output = await execFileAsync2("gh", ghArgs, { encoding: "utf-8", timeout: 6e4 });
10215
+ const output = await execFileAsync3("gh", ghArgs, { encoding: "utf-8", timeout: 6e4 });
9798
10216
  const url = output.stdout.trim();
9799
10217
  const issueNumber = Number.parseInt(url.split("/").pop() ?? "0", 10);
9800
10218
  jsonOut({ ok: true, data: { url, issueNumber, repo } });
@@ -10019,7 +10437,7 @@ issueCommand.command("edit <issueRef>").description("Edit issue fields (title, b
10019
10437
  if (opts.assignee) ghArgs.push("--add-assignee", opts.assignee);
10020
10438
  if (opts.removeAssignee) ghArgs.push("--remove-assignee", opts.removeAssignee);
10021
10439
  if (useJson()) {
10022
- await execFileAsync2("gh", ghArgs, { encoding: "utf-8", timeout: 3e4 });
10440
+ await execFileAsync3("gh", ghArgs, { encoding: "utf-8", timeout: 3e4 });
10023
10441
  jsonOut({ ok: true, data: { issue: ref.issueNumber, changes } });
10024
10442
  } else {
10025
10443
  execFileSync5("gh", ghArgs, { stdio: "inherit" });