@youtyan/code-viewer 0.6.3 → 0.6.4

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/web/app.js CHANGED
@@ -227,7 +227,7 @@ ${lines.join(`
227
227
  function shouldAutoLoadForRoute(route, options = {}) {
228
228
  if (route.screen === "history")
229
229
  return options.historyWorktreeSelected === true;
230
- if (route.screen === "database" || route.screen === "help" || route.screen === "unknown")
230
+ if (route.screen === "database" || route.screen === "journal" || route.screen === "help" || route.screen === "unknown")
231
231
  return false;
232
232
  return true;
233
233
  }
@@ -517,6 +517,7 @@ ${lines.join(`
517
517
  }
518
518
  var PENCIL_16_PATH = "M11.013 1.427a1.75 1.75 0 0 1 2.474 0l1.086 1.086a1.75 1.75 0 0 1 0 2.474l-8.61 8.61c-.21.21-.47.364-.756.445l-3.251.93a.75.75 0 0 1-.927-.928l.929-3.25c.081-.286.235-.547.445-.758l8.61-8.609Zm.176 4.823L9.75 4.81l-6.286 6.287a.253.253 0 0 0-.064.108l-.558 1.953 1.953-.558a.253.253 0 0 0 .108-.064Zm1.238-3.763a.25.25 0 0 0-.354 0L10.811 3.75l1.439 1.44 1.263-1.263a.25.25 0 0 0 0-.354Z";
519
519
  var COMMENT_DISCUSSION_16_PATH = "M1.75 1h8.5c.966 0 1.75.784 1.75 1.75v5.5A1.75 1.75 0 0 1 10.25 10H7.061l-2.574 2.573A1.458 1.458 0 0 1 2 11.543V10h-.25A1.75 1.75 0 0 1 0 8.25v-5.5C0 1.784.784 1 1.75 1ZM1.5 2.75v5.5c0 .138.112.25.25.25h1a.75.75 0 0 1 .75.75v2.19l2.72-2.72a.749.749 0 0 1 .53-.22h3.5a.25.25 0 0 0 .25-.25v-5.5a.25.25 0 0 0-.25-.25h-8.5a.25.25 0 0 0-.25.25Zm13 2a.25.25 0 0 0-.25-.25h-.5a.75.75 0 0 1 0-1.5h.5c.966 0 1.75.784 1.75 1.75v5.5A1.75 1.75 0 0 1 14.25 12H14v1.543a1.458 1.458 0 0 1-2.487 1.03L9.22 12.28a.749.749 0 0 1 .326-1.275.749.749 0 0 1 .734.215l2.22 2.22v-2.19a.75.75 0 0 1 .75-.75h1a.25.25 0 0 0 .25-.25Z";
520
+ var GRABBER_16_PATH = "M10 13h4v-2h-4v2Zm-8 0h4v-2H2v2Zm8-6h4V5h-4v2ZM2 7h4V5H2v2Zm8-6v2h4V1h-4ZM2 3h4V1H2v2Z";
520
521
  var PULSE_16_PATH = "M6 2c.306 0 .582.187.696.471L10 10.731l1.304-3.26A.751.751 0 0 1 12 7h3.25a.75.75 0 0 1 0 1.5h-2.742l-1.812 4.528a.751.751 0 0 1-1.392 0L6 4.77 4.696 8.03A.75.75 0 0 1 4 8.5H.75a.75.75 0 0 1 0-1.5h2.742l1.812-4.529A.751.751 0 0 1 6 2Z";
521
522
  var MOON_16_PATH = "M9.598 1.591a.749.749 0 0 1 .785-.175 7.001 7.001 0 1 1-8.967 8.967.75.75 0 0 1 .961-.96 5.5 5.5 0 0 0 7.046-7.046.75.75 0 0 1 .175-.786Zm1.616 1.945a7 7 0 0 1-7.678 7.678 5.499 5.499 0 1 0 7.678-7.678Z";
522
523
  var SEARCH_16_PATH = "M10.68 11.74a6 6 0 0 1-7.917-8.984 6 6 0 0 1 8.997 7.905l3.05 3.05a.75.75 0 1 1-1.06 1.06l-3.07-3.03ZM11.5 7a4.499 4.499 0 1 1-8.997 0A4.499 4.499 0 0 1 11.5 7Z";
@@ -913,6 +914,21 @@ ${lines.join(`
913
914
  range
914
915
  };
915
916
  }
917
+ case "/journal": {
918
+ const tabRaw = params.get("tab");
919
+ const tab = tabRaw === "tasks" || tabRaw === "journal" ? tabRaw : undefined;
920
+ const date = params.get("date") || undefined;
921
+ const label = params.get("label") || undefined;
922
+ const task = params.get("task") || undefined;
923
+ return {
924
+ screen: "journal",
925
+ ...tab ? { tab } : {},
926
+ ...date ? { date } : {},
927
+ ...label ? { label } : {},
928
+ ...task ? { task } : {},
929
+ range
930
+ };
931
+ }
916
932
  case "/database": {
917
933
  const db = params.get("db") || undefined;
918
934
  const schema = params.get("schema") || undefined;
@@ -985,6 +1001,19 @@ ${lines.join(`
985
1001
  const qs = params.toString();
986
1002
  return `/history${qs ? `?${qs}` : ""}`;
987
1003
  }
1004
+ case "journal": {
1005
+ const params = new URLSearchParams;
1006
+ if (route.tab && route.tab !== "journal")
1007
+ params.set("tab", route.tab);
1008
+ if (route.date)
1009
+ params.set("date", route.date);
1010
+ if (route.label)
1011
+ params.set("label", route.label);
1012
+ if (route.task)
1013
+ params.set("task", route.task);
1014
+ const qs = params.toString();
1015
+ return `/journal${qs ? `?${qs}` : ""}`;
1016
+ }
988
1017
  case "database": {
989
1018
  const params = new URLSearchParams;
990
1019
  if (route.db)
@@ -8056,6 +8085,132 @@ ${frontmatter.yaml}
8056
8085
  return { width: rect.width || 800, height: rect.height || 600 };
8057
8086
  }
8058
8087
 
8088
+ // web-src/views/diff-line-select.ts
8089
+ var SELECTED_CLASS = "gdp-diff-line-selected";
8090
+ function cardPath(el) {
8091
+ return el.closest(".gdp-file-shell[data-path]")?.dataset.path || "";
8092
+ }
8093
+ function afterLineFromCell(cell) {
8094
+ const sideCell = cell.closest("td.d2h-code-side-linenumber");
8095
+ if (sideCell) {
8096
+ const side = sideCell.closest(".d2h-file-side-diff");
8097
+ const wrapper = sideCell.closest(".d2h-file-wrapper");
8098
+ if (!side || !wrapper)
8099
+ return null;
8100
+ const sides = wrapper.querySelectorAll(".d2h-file-side-diff");
8101
+ if (sides.length < 2 || side !== sides[1])
8102
+ return null;
8103
+ const line2 = Number((sideCell.textContent || "").trim());
8104
+ return Number.isInteger(line2) && line2 > 0 ? line2 : null;
8105
+ }
8106
+ const numCell = cell.closest("td.d2h-code-linenumber");
8107
+ if (!numCell)
8108
+ return null;
8109
+ const raw = (numCell.querySelector(".line-num2")?.textContent || "").trim();
8110
+ const line = Number(raw);
8111
+ return Number.isInteger(line) && line > 0 ? line : null;
8112
+ }
8113
+ function afterLineCellFromRow(row) {
8114
+ const cells = row.querySelectorAll("td.d2h-code-linenumber, td.d2h-code-side-linenumber");
8115
+ for (const cell of cells) {
8116
+ if (afterLineFromCell(cell) !== null)
8117
+ return cell;
8118
+ }
8119
+ return null;
8120
+ }
8121
+ function diffRowAfterLineNumber(row) {
8122
+ const cell = afterLineCellFromRow(row);
8123
+ return cell ? afterLineFromCell(cell) : null;
8124
+ }
8125
+ function diffRowHasAfterChange(row) {
8126
+ const cell = afterLineCellFromRow(row);
8127
+ return !!cell && cell.classList.contains("d2h-ins");
8128
+ }
8129
+ function rowsWithAfterLines(card) {
8130
+ const out = [];
8131
+ card.querySelectorAll("table.d2h-diff-table tr").forEach((row) => {
8132
+ const line = diffRowAfterLineNumber(row);
8133
+ if (line !== null)
8134
+ out.push({ row, line });
8135
+ });
8136
+ return out;
8137
+ }
8138
+ function createDiffLineSelect(deps) {
8139
+ let drag = null;
8140
+ let selection = null;
8141
+ function clearHighlights() {
8142
+ document.querySelectorAll(`.${SELECTED_CLASS}`).forEach((row) => {
8143
+ row.classList.remove(SELECTED_CLASS);
8144
+ });
8145
+ }
8146
+ function applySelection(next) {
8147
+ selection = next;
8148
+ clearHighlights();
8149
+ if (!next) {
8150
+ deps.pill.hide();
8151
+ return;
8152
+ }
8153
+ const start = Math.min(next.start, next.end);
8154
+ const end = Math.max(next.start, next.end);
8155
+ const card = document.querySelector(`.gdp-file-shell[data-path="${CSS.escape(next.path)}"]`);
8156
+ if (card) {
8157
+ for (const item of rowsWithAfterLines(card)) {
8158
+ if (item.line >= start && item.line <= end)
8159
+ item.row.classList.add(SELECTED_CLASS);
8160
+ }
8161
+ }
8162
+ deps.pill.show(next.path, start, end);
8163
+ }
8164
+ function clear() {
8165
+ const hadSelection = !!selection || !!drag;
8166
+ drag = null;
8167
+ applySelection(null);
8168
+ return hadSelection;
8169
+ }
8170
+ const diff = document.querySelector("#diff");
8171
+ if (!diff)
8172
+ return { clear };
8173
+ diff.addEventListener("mousedown", (e2) => {
8174
+ const target = e2.target;
8175
+ const cell = target.closest("td.d2h-code-linenumber, td.d2h-code-side-linenumber");
8176
+ if (!cell)
8177
+ return;
8178
+ const line = afterLineFromCell(cell);
8179
+ const path = cardPath(cell);
8180
+ if (line === null || !path) {
8181
+ if (selection)
8182
+ clear();
8183
+ return;
8184
+ }
8185
+ e2.preventDefault();
8186
+ drag = { path, start: line };
8187
+ applySelection({ path, start: line, end: line });
8188
+ });
8189
+ diff.addEventListener("mouseover", (e2) => {
8190
+ if (!drag)
8191
+ return;
8192
+ const target = e2.target;
8193
+ const cell = target.closest("td.d2h-code-linenumber, td.d2h-code-side-linenumber");
8194
+ if (!cell || cardPath(cell) !== drag.path)
8195
+ return;
8196
+ const line = afterLineFromCell(cell);
8197
+ if (line === null)
8198
+ return;
8199
+ applySelection({ path: drag.path, start: drag.start, end: line });
8200
+ });
8201
+ document.addEventListener("mouseup", () => {
8202
+ drag = null;
8203
+ });
8204
+ document.addEventListener("keydown", (e2) => {
8205
+ if (isImeComposing(e2))
8206
+ return;
8207
+ if (e2.key === "Escape" && selection && !drag && !isEditableKeyTarget(e2.target)) {
8208
+ clear();
8209
+ }
8210
+ });
8211
+ return { clear };
8212
+ }
8213
+
8059
8214
  // web-src/views/ui-dialog.ts
8060
8215
  var BACKDROP_CLASS = "gdp-dialog-backdrop";
8061
8216
  var DIALOG_CLASS = "gdp-dialog";
@@ -8478,15 +8633,34 @@ ${frontmatter.yaml}
8478
8633
  function inlineAnnotationTargetRow(entry) {
8479
8634
  if (!entry.line)
8480
8635
  return null;
8481
- const line = entry.line.end;
8482
8636
  const card = document.querySelector(deps.diffCardSelector(entry.path));
8483
8637
  if (!card)
8484
8638
  return null;
8639
+ const line = entry.line.end;
8485
8640
  const sourceRow = card.querySelector(`.gdp-source-table tr[data-line="${String(line)}"]`);
8486
8641
  if (sourceRow)
8487
8642
  return sourceRow;
8643
+ const start = entry.line.start;
8644
+ const end = entry.line.end;
8488
8645
  const rows = Array.from(card.querySelectorAll("table.d2h-diff-table tr"));
8489
- return rows.find((row) => deps.diffRowLineNumber(row) === line) || null;
8646
+ const byLine = new Map;
8647
+ let hasChangedAfterRow = false;
8648
+ for (const row of rows) {
8649
+ const rowLine = deps.diffRowLineNumber(row);
8650
+ if (rowLine === null || rowLine < start || rowLine > end)
8651
+ continue;
8652
+ if (!byLine.has(rowLine))
8653
+ byLine.set(rowLine, row);
8654
+ if (diffRowHasAfterChange(row))
8655
+ hasChangedAfterRow = true;
8656
+ }
8657
+ if (!hasChangedAfterRow)
8658
+ return null;
8659
+ for (let rowLine = start;rowLine <= end; rowLine++) {
8660
+ if (!byLine.has(rowLine))
8661
+ return null;
8662
+ }
8663
+ return byLine.get(end) || null;
8490
8664
  }
8491
8665
  function siblingSideRow(row) {
8492
8666
  const side = row.closest(".d2h-file-side-diff");
@@ -8517,10 +8691,6 @@ ${frontmatter.yaml}
8517
8691
  row.remove();
8518
8692
  });
8519
8693
  const session = ANNOTATIONS.sessions.find((s2) => s2.id === activeSessionId);
8520
- if (!annotationPanel.hidden) {
8521
- applyDatabaseAnnotations(undefined);
8522
- return;
8523
- }
8524
8694
  applyDatabaseAnnotations(session);
8525
8695
  if (!session)
8526
8696
  return;
@@ -8589,21 +8759,18 @@ ${frontmatter.yaml}
8589
8759
  renderAnnotationPanel();
8590
8760
  applyInlineAnnotations();
8591
8761
  }
8592
- async function expandAnnotationContext(entry) {
8593
- if (!entry.line || inlineAnnotationTargetRow(entry))
8594
- return;
8762
+ async function waitForAnnotationDiffTarget(entry) {
8763
+ if (!entry.line)
8764
+ return null;
8765
+ const ready = inlineAnnotationTargetRow(entry);
8766
+ if (ready)
8767
+ return ready;
8595
8768
  const card = document.querySelector(deps.diffCardSelector(entry.path));
8596
- const file = deps.getFiles().find((f2) => f2.path === entry.path);
8597
- if (!card || !file || card.classList.contains("gdp-standalone-source"))
8598
- return;
8599
- for (let i2 = 0;i2 < 50 && !card.classList.contains("loaded"); i2++)
8600
- await new Promise((resolve) => setTimeout(resolve, 100));
8601
- if (inlineAnnotationTargetRow(entry))
8602
- return;
8603
- if (card.classList.contains("gdp-context-expanded"))
8604
- return;
8605
- await deps.expandAllFileContext(card, file);
8606
- deps.focusDiffLine(card, annotationLineTarget(entry));
8769
+ if (!card || card.classList.contains("gdp-standalone-source"))
8770
+ return null;
8771
+ if (!card.classList.contains("loaded") && !await deps.loadDiffFile(entry.path))
8772
+ return null;
8773
+ return inlineAnnotationTargetRow(entry);
8607
8774
  }
8608
8775
  function syncInlineAnnotationActive() {
8609
8776
  document.querySelectorAll(".gdp-annotation-row").forEach((row) => {
@@ -9097,11 +9264,13 @@ ${frontmatter.yaml}
9097
9264
  return;
9098
9265
  emptyDiffRangeKey = deps.getFiles().length ? null : rangeKey;
9099
9266
  }
9100
- if (deps.getFiles().some((f2) => f2.path === entry.path)) {
9101
- deps.setRoute({ screen: "diff", range, path: entry.path, line }, needDiffLoad);
9102
- deps.setPageMode();
9267
+ let showSourceView = !deps.getFiles().some((f2) => f2.path === entry.path);
9268
+ let replaceSourceRoute = needDiffLoad;
9269
+ if (!showSourceView) {
9103
9270
  const hasDiffCards = !!document.querySelector(".gdp-file-shell:not(.gdp-standalone-source)");
9104
9271
  if (!hasDiffCards) {
9272
+ deps.setRoute({ screen: "diff", range, path: entry.path, line }, needDiffLoad);
9273
+ deps.setPageMode();
9105
9274
  deps.removeStandaloneSource();
9106
9275
  await deps.load();
9107
9276
  if (stale())
@@ -9109,14 +9278,22 @@ ${frontmatter.yaml}
9109
9278
  }
9110
9279
  deps.removeStandaloneSource();
9111
9280
  deps.scrollToFile(entry.path, line);
9112
- await expandAnnotationContext(entry);
9281
+ const target = await waitForAnnotationDiffTarget(entry);
9113
9282
  if (stale())
9114
9283
  return;
9115
- } else {
9284
+ if (!target) {
9285
+ showSourceView = true;
9286
+ replaceSourceRoute = true;
9287
+ } else {
9288
+ deps.setRoute({ screen: "diff", range, path: entry.path, line }, needDiffLoad);
9289
+ deps.setPageMode();
9290
+ }
9291
+ }
9292
+ if (showSourceView) {
9116
9293
  const ref = annotationRefForEntry(entry);
9117
9294
  const card = document.querySelector(".gdp-standalone-source");
9118
9295
  const reusable = prevRoute.screen === "file" && prevRoute.path === entry.path && prevRoute.ref === ref && card?.dataset.path === entry.path && !!card.querySelector(".gdp-source-table");
9119
- deps.setRoute({ screen: "file", path: entry.path, ref, view: "blob", line, range }, needDiffLoad);
9296
+ deps.setRoute({ screen: "file", path: entry.path, ref, view: "blob", line, range }, replaceSourceRoute);
9120
9297
  deps.setPageMode();
9121
9298
  if (reusable && card) {
9122
9299
  focusStandaloneSourceLines(card, entry);
@@ -14544,7 +14721,7 @@ ${frontmatter.yaml}
14544
14721
  refreshBtn.setAttribute("aria-busy", refreshBusy ? "true" : "false");
14545
14722
  refreshBtn.disabled = refreshBusy;
14546
14723
  refreshBtn.classList.toggle("spinning", refreshBusy);
14547
- refreshLabel.textContent = refreshBusy ? t2.refreshingLabel : t2.refreshLabel;
14724
+ refreshLabel.textContent = "";
14548
14725
  }
14549
14726
  function setRefreshBusy(busy) {
14550
14727
  refreshBusy = busy;
@@ -16200,7 +16377,7 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
16200
16377
  refreshBtn.innerHTML = iconSvg("octicon-sync", SYNC_16_PATH);
16201
16378
  const refreshLabel = document.createElement("span");
16202
16379
  refreshLabel.className = "db-grid-refresh-label";
16203
- refreshLabel.textContent = text2().grid.refreshLabel;
16380
+ refreshLabel.textContent = "";
16204
16381
  refreshBtn.appendChild(refreshLabel);
16205
16382
  const refreshResult = document.createElement("span");
16206
16383
  refreshResult.className = "db-refresh-result db-grid-refresh-result";
@@ -16308,7 +16485,8 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
16308
16485
  filteredEmptyActions.hidden = true;
16309
16486
  const filteredEmptyReloadAction = document.createElement("button");
16310
16487
  filteredEmptyReloadAction.type = "button";
16311
- filteredEmptyReloadAction.className = "db-btn db-btn-primary db-btn-sm";
16488
+ filteredEmptyReloadAction.className = "db-btn db-btn-icon db-grid-refresh";
16489
+ filteredEmptyReloadAction.innerHTML = iconSvg("octicon-sync", SYNC_16_PATH);
16312
16490
  filteredEmptyReloadAction.hidden = true;
16313
16491
  filteredEmptyReloadAction.disabled = true;
16314
16492
  const filteredEmptyAction = document.createElement("button");
@@ -16404,7 +16582,6 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
16404
16582
  let rafId = 0;
16405
16583
  let statusEl = null;
16406
16584
  let isRefreshing = false;
16407
- let refreshResultState = null;
16408
16585
  let filterTimer = null;
16409
16586
  let selectedRowIndex = -1;
16410
16587
  let activeCellRowIndex = -1;
@@ -16524,7 +16701,7 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
16524
16701
  function syncRefreshButton() {
16525
16702
  const count = activeFilterCount();
16526
16703
  const t2 = text2().grid;
16527
- refreshLabel.textContent = isRefreshing ? t2.refreshingLabel : count > 0 ? t2.refreshFilteredLabel : t2.refreshLabel;
16704
+ refreshLabel.textContent = "";
16528
16705
  const action = count > 0 ? t2.refreshActionWithFilters(count) : t2.refreshAction;
16529
16706
  refreshBtn.classList.toggle("has-filters", count > 0);
16530
16707
  refreshBtn.title = action;
@@ -16532,26 +16709,11 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
16532
16709
  refreshBtn.setAttribute("aria-busy", isRefreshing ? "true" : "false");
16533
16710
  }
16534
16711
  function syncRefreshResult() {
16535
- const result = refreshResultState;
16536
- refreshResult.hidden = !result;
16537
- refreshResult.classList.toggle("changed", !!result && result.delta !== 0);
16538
- if (!result) {
16539
- refreshResult.textContent = "";
16540
- return;
16541
- }
16542
- const t2 = text2().grid;
16543
- const total = result.totalRows.toLocaleString();
16544
- refreshResult.textContent = result.delta === 0 ? t2.refreshResultUnchanged(total) : t2.refreshResultChanged(result.delta, total);
16712
+ refreshResult.hidden = true;
16713
+ refreshResult.textContent = "";
16714
+ refreshResult.classList.toggle("changed", false);
16545
16715
  }
16546
16716
  function clearRefreshResult() {
16547
- refreshResultState = null;
16548
- syncRefreshResult();
16549
- }
16550
- function setRefreshResult(previousTotalRows, nextTotalRows) {
16551
- refreshResultState = {
16552
- delta: nextTotalRows - previousTotalRows,
16553
- totalRows: nextTotalRows
16554
- };
16555
16717
  syncRefreshResult();
16556
16718
  }
16557
16719
  function syncFilterClearButton() {
@@ -16578,7 +16740,9 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
16578
16740
  const reloadAction = t2.refreshActionWithFilters(count);
16579
16741
  filteredEmptyTitle.textContent = t2.filteredEmptyTitle(count);
16580
16742
  filteredEmptyHint.textContent = t2.filteredEmptyHint;
16581
- filteredEmptyReloadAction.textContent = isRefreshing ? t2.refreshingLabel : t2.refreshFilteredLabel;
16743
+ filteredEmptyReloadAction.classList.toggle("spinning", isRefreshing);
16744
+ filteredEmptyReloadAction.textContent = "";
16745
+ filteredEmptyReloadAction.innerHTML = iconSvg("octicon-sync", SYNC_16_PATH);
16582
16746
  filteredEmptyReloadAction.title = reloadAction;
16583
16747
  filteredEmptyReloadAction.setAttribute("aria-label", reloadAction);
16584
16748
  filteredEmptyReloadAction.setAttribute("aria-busy", isRefreshing ? "true" : "false");
@@ -16628,7 +16792,6 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
16628
16792
  if (!currentTable || refreshBtn.disabled)
16629
16793
  return;
16630
16794
  const refreshTable = currentTable;
16631
- const previousTotalRows = totalRows;
16632
16795
  const refreshFilterKey = JSON.stringify(collectFilters());
16633
16796
  const scrollTop = viewport.scrollTop;
16634
16797
  const pageStart = Math.floor(scrollTop / ROW_HEIGHT / PAGE_SIZE) * PAGE_SIZE;
@@ -16661,7 +16824,7 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
16661
16824
  syncFilteredEmptyState();
16662
16825
  if (!endedInError) {
16663
16826
  if (loadGeneration === refreshGeneration && JSON.stringify(collectFilters()) === refreshFilterKey) {
16664
- setRefreshResult(previousTotalRows, totalRows);
16827
+ syncRefreshResult();
16665
16828
  callbacks.onRefreshComplete?.({
16666
16829
  table: refreshTable,
16667
16830
  totalRows,
@@ -18563,7 +18726,6 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
18563
18726
  const tableSelectGuard = createAbortGuard();
18564
18727
  let historyRefreshPending = null;
18565
18728
  let isRefreshingDatastores = false;
18566
- let datastoreRefreshResult = null;
18567
18729
  const dbSelect = document.createElement("select");
18568
18730
  dbSelect.className = "db-file-select";
18569
18731
  reloc(() => {
@@ -19682,46 +19844,24 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
19682
19844
  handleSchemaSelectChange();
19683
19845
  });
19684
19846
  function syncDbRefreshButton() {
19685
- const text2 = paneText().nav;
19686
- dbRefreshLabel.textContent = isRefreshingDatastores ? text2.refreshDatastoresBusy : text2.refreshDatastoresShort;
19847
+ dbRefreshLabel.textContent = "";
19687
19848
  dbRefreshBtn.setAttribute("aria-busy", isRefreshingDatastores ? "true" : "false");
19688
19849
  syncDatastoreRefreshResult();
19689
19850
  }
19690
19851
  function syncDatastoreRefreshResult() {
19691
- const result = datastoreRefreshResult;
19692
- dbRefreshResult.hidden = !result;
19693
- dbRefreshResult.classList.toggle("changed", !!result && (result.added > 0 || result.removed > 0));
19694
- if (!result)
19695
- return;
19696
- const text2 = paneText().nav;
19697
- dbRefreshResult.textContent = result.added > 0 || result.removed > 0 ? text2.refreshDatastoresChanged(result.added, result.removed) : text2.refreshDatastoresUnchanged;
19698
- }
19699
- function diffDatastoreFiles(before, after) {
19700
- const beforeIds = new Set(before.map((file) => file.id));
19701
- const afterIds = new Set(after.map((file) => file.id));
19702
- let added = 0;
19703
- let removed = 0;
19704
- for (const id of afterIds) {
19705
- if (!beforeIds.has(id))
19706
- added++;
19707
- }
19708
- for (const id of beforeIds) {
19709
- if (!afterIds.has(id))
19710
- removed++;
19711
- }
19712
- return { added, removed };
19852
+ dbRefreshResult.hidden = true;
19853
+ dbRefreshResult.textContent = "";
19854
+ dbRefreshResult.classList.toggle("changed", false);
19713
19855
  }
19714
19856
  async function refreshDatastoreList() {
19715
19857
  if (dbRefreshBtn.disabled)
19716
19858
  return;
19717
- const beforeFiles = [...lastFiles];
19718
19859
  isRefreshingDatastores = true;
19719
19860
  dbRefreshBtn.disabled = true;
19720
19861
  dbRefreshBtn.classList.add("spinning");
19721
19862
  syncDbRefreshButton();
19722
19863
  try {
19723
19864
  await outerDeps.refreshDatastores();
19724
- datastoreRefreshResult = cb.isActive() ? diffDatastoreFiles(beforeFiles, lastFiles) : null;
19725
19865
  } finally {
19726
19866
  isRefreshingDatastores = false;
19727
19867
  dbRefreshBtn.classList.remove("spinning");
@@ -19757,7 +19897,6 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
19757
19897
  const dbId = dbSelect.value;
19758
19898
  if (!dbId)
19759
19899
  return;
19760
- datastoreRefreshResult = null;
19761
19900
  syncDatastoreRefreshResult();
19762
19901
  const generation = ++loadGeneration;
19763
19902
  const file = lastFiles.find((f2) => f2.id === dbId);
@@ -21146,119 +21285,6 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
21146
21285
  return `${bytes} B`;
21147
21286
  }
21148
21287
 
21149
- // web-src/views/diff-line-select.ts
21150
- var SELECTED_CLASS = "gdp-diff-line-selected";
21151
- function cardPath(el) {
21152
- return el.closest(".gdp-file-shell[data-path]")?.dataset.path || "";
21153
- }
21154
- function afterLineFromCell(cell) {
21155
- const sideCell = cell.closest("td.d2h-code-side-linenumber");
21156
- if (sideCell) {
21157
- const side = sideCell.closest(".d2h-file-side-diff");
21158
- const wrapper = sideCell.closest(".d2h-file-wrapper");
21159
- if (!side || !wrapper)
21160
- return null;
21161
- const sides = wrapper.querySelectorAll(".d2h-file-side-diff");
21162
- if (sides.length < 2 || side !== sides[1])
21163
- return null;
21164
- const line2 = Number((sideCell.textContent || "").trim());
21165
- return Number.isInteger(line2) && line2 > 0 ? line2 : null;
21166
- }
21167
- const numCell = cell.closest("td.d2h-code-linenumber");
21168
- if (!numCell)
21169
- return null;
21170
- const raw = (numCell.querySelector(".line-num2")?.textContent || "").trim();
21171
- const line = Number(raw);
21172
- return Number.isInteger(line) && line > 0 ? line : null;
21173
- }
21174
- function rowsWithAfterLines(card) {
21175
- const out = [];
21176
- card.querySelectorAll("table.d2h-diff-table tr").forEach((row) => {
21177
- const cell = row.querySelector("td.d2h-code-linenumber, td.d2h-code-side-linenumber");
21178
- if (!cell)
21179
- return;
21180
- const line = afterLineFromCell(cell);
21181
- if (line !== null)
21182
- out.push({ row, line });
21183
- });
21184
- return out;
21185
- }
21186
- function createDiffLineSelect(deps) {
21187
- let drag = null;
21188
- let selection = null;
21189
- function clearHighlights() {
21190
- document.querySelectorAll(`.${SELECTED_CLASS}`).forEach((row) => {
21191
- row.classList.remove(SELECTED_CLASS);
21192
- });
21193
- }
21194
- function applySelection(next) {
21195
- selection = next;
21196
- clearHighlights();
21197
- if (!next) {
21198
- deps.pill.hide();
21199
- return;
21200
- }
21201
- const start = Math.min(next.start, next.end);
21202
- const end = Math.max(next.start, next.end);
21203
- const card = document.querySelector(`.gdp-file-shell[data-path="${CSS.escape(next.path)}"]`);
21204
- if (card) {
21205
- for (const item of rowsWithAfterLines(card)) {
21206
- if (item.line >= start && item.line <= end)
21207
- item.row.classList.add(SELECTED_CLASS);
21208
- }
21209
- }
21210
- deps.pill.show(next.path, start, end);
21211
- }
21212
- function clear() {
21213
- const hadSelection = !!selection || !!drag;
21214
- drag = null;
21215
- applySelection(null);
21216
- return hadSelection;
21217
- }
21218
- const diff = document.querySelector("#diff");
21219
- if (!diff)
21220
- return { clear };
21221
- diff.addEventListener("mousedown", (e2) => {
21222
- const target = e2.target;
21223
- const cell = target.closest("td.d2h-code-linenumber, td.d2h-code-side-linenumber");
21224
- if (!cell)
21225
- return;
21226
- const line = afterLineFromCell(cell);
21227
- const path = cardPath(cell);
21228
- if (line === null || !path) {
21229
- if (selection)
21230
- clear();
21231
- return;
21232
- }
21233
- e2.preventDefault();
21234
- drag = { path, start: line };
21235
- applySelection({ path, start: line, end: line });
21236
- });
21237
- diff.addEventListener("mouseover", (e2) => {
21238
- if (!drag)
21239
- return;
21240
- const target = e2.target;
21241
- const cell = target.closest("td.d2h-code-linenumber, td.d2h-code-side-linenumber");
21242
- if (!cell || cardPath(cell) !== drag.path)
21243
- return;
21244
- const line = afterLineFromCell(cell);
21245
- if (line === null)
21246
- return;
21247
- applySelection({ path: drag.path, start: drag.start, end: line });
21248
- });
21249
- document.addEventListener("mouseup", () => {
21250
- drag = null;
21251
- });
21252
- document.addEventListener("keydown", (e2) => {
21253
- if (isImeComposing(e2))
21254
- return;
21255
- if (e2.key === "Escape" && selection && !drag && !isEditableKeyTarget(e2.target)) {
21256
- clear();
21257
- }
21258
- });
21259
- return { clear };
21260
- }
21261
-
21262
21288
  // web-src/core/ws-highlight.ts
21263
21289
  function isWhitespaceOnlyInlineHighlight(text2) {
21264
21290
  return !!text2 && !/\S/.test(text2);
@@ -21552,22 +21578,31 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
21552
21578
  });
21553
21579
  }
21554
21580
  function diffRowLineNumber(row) {
21555
- const newLine = row.querySelector(".line-num2, td.d2h-code-side-linenumber");
21556
- const raw = (newLine?.textContent || "").trim();
21557
- const line = Number(raw);
21558
- return Number.isInteger(line) && line > 0 ? line : null;
21581
+ return diffRowAfterLineNumber(row);
21559
21582
  }
21560
21583
  function focusDiffLine(card, line) {
21561
21584
  const start = lineTargetStart(line);
21562
21585
  if (!start)
21563
21586
  return false;
21587
+ const end = typeof line === "object" ? line.end : start;
21564
21588
  const rows = Array.from(card.querySelectorAll("table.d2h-diff-table tr"));
21565
- const row = rows.find((candidate) => diffRowLineNumber(candidate) === start);
21566
- if (!row)
21589
+ const targetRows = rows.filter((candidate) => {
21590
+ const rowLine = diffRowLineNumber(candidate);
21591
+ return rowLine !== null && rowLine >= start && rowLine <= end;
21592
+ });
21593
+ const targetLines = new Set(targetRows.map((candidate) => diffRowLineNumber(candidate)).filter((value) => value !== null));
21594
+ for (let lineNo = start;lineNo <= end; lineNo++) {
21595
+ if (!targetLines.has(lineNo))
21596
+ return false;
21597
+ }
21598
+ const firstRow = targetRows[0];
21599
+ if (!firstRow)
21567
21600
  return false;
21568
21601
  clearDiffLineFocus();
21569
- row.classList.add("gdp-diff-line-target");
21570
- scrollDiffElementIntoView(row, "center");
21602
+ targetRows.forEach((row) => {
21603
+ row.classList.add("gdp-diff-line-target");
21604
+ });
21605
+ scrollDiffElementIntoView(firstRow, "center");
21571
21606
  return true;
21572
21607
  }
21573
21608
  function scrollDiffElementIntoView(element, block2) {
@@ -22075,7 +22110,7 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
22075
22110
  }
22076
22111
  });
22077
22112
  }
22078
- function loadFile(file, card, urlOverride) {
22113
+ function loadFile(file, card, urlOverride, options) {
22079
22114
  card.classList.remove("pending");
22080
22115
  card.classList.add("loading");
22081
22116
  if (lazyObserver)
@@ -22109,7 +22144,8 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
22109
22144
  retryStale();
22110
22145
  return;
22111
22146
  }
22112
- await nextIdle();
22147
+ if (!options?.immediate)
22148
+ await nextIdle();
22113
22149
  if (String(myReq) !== card.dataset.reqId)
22114
22150
  return;
22115
22151
  renderFile(file, data, card);
@@ -22132,6 +22168,18 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
22132
22168
  });
22133
22169
  });
22134
22170
  }
22171
+ async function loadDiffFile(path) {
22172
+ const card = document.querySelector(diffCardSelector(path));
22173
+ if (!card || card.classList.contains("gdp-standalone-source"))
22174
+ return false;
22175
+ if (card.classList.contains("loaded"))
22176
+ return true;
22177
+ const file = STATE.files.find((x) => x.path === path);
22178
+ if (!file)
22179
+ return false;
22180
+ await loadFile(file, card, undefined, { immediate: true });
22181
+ return card.classList.contains("loaded");
22182
+ }
22135
22183
  function mountDiff(card, file, data) {
22136
22184
  const head = card.querySelector(".gdp-shell-header");
22137
22185
  if (head)
@@ -22656,6 +22704,7 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
22656
22704
  enqueueInitialLoads,
22657
22705
  enqueueLoad,
22658
22706
  loadFile,
22707
+ loadDiffFile,
22659
22708
  createFileBreadcrumb,
22660
22709
  highlightInsertedSpans,
22661
22710
  setFileCollapsed,
@@ -22937,13 +22986,8 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
22937
22986
  bodyExpandClose: "Collapse",
22938
22987
  bodyExpandMore: (n2) => `Show more (${n2} lines)`,
22939
22988
  refreshLabel: "Refresh",
22940
- refreshLabelPending: "Update",
22941
22989
  refreshTitle: "Refresh commit history",
22942
22990
  refreshTitlePending: "History may have changed. Refresh",
22943
- refreshPendingStatus: "History may have changed",
22944
- refreshResultUpdated: (sha) => `Updated: ${sha} is now latest`,
22945
- refreshResultUnchanged: "No new commits",
22946
- refreshResultUnchangedInView: "No new commits in this view",
22947
22991
  filterClearLabel: "Clear",
22948
22992
  filterClearTitle: "Clear commit filter"
22949
22993
  },
@@ -22952,13 +22996,8 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
22952
22996
  bodyExpandClose: "閉じる",
22953
22997
  bodyExpandMore: (n2) => `もっと見る (${n2} 行)`,
22954
22998
  refreshLabel: "更新",
22955
- refreshLabelPending: "更新あり",
22956
22999
  refreshTitle: "コミット履歴を更新",
22957
23000
  refreshTitlePending: "新しい履歴がある可能性があります。更新",
22958
- refreshPendingStatus: "新しい履歴がある可能性があります",
22959
- refreshResultUpdated: (sha) => `更新: ${sha} が最新です`,
22960
- refreshResultUnchanged: "新しいコミットはありません",
22961
- refreshResultUnchangedInView: "この表示では新しいコミットはありません",
22962
23001
  filterClearLabel: "解除",
22963
23002
  filterClearTitle: "コミットフィルタを解除"
22964
23003
  }
@@ -22994,7 +23033,7 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
22994
23033
  refreshButton.innerHTML = iconSvg("octicon-sync", SYNC_16_PATH);
22995
23034
  const refreshLabel = document.createElement("span");
22996
23035
  refreshLabel.className = "history-refresh-label";
22997
- refreshLabel.textContent = HISTORY_TEXT.en.refreshLabel;
23036
+ refreshLabel.textContent = "";
22998
23037
  refreshButton.appendChild(refreshLabel);
22999
23038
  const refreshResult = document.createElement("span");
23000
23039
  refreshResult.className = "db-refresh-result history-refresh-result";
@@ -23245,28 +23284,11 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
23245
23284
  }
23246
23285
  function setRefreshResult(previousTopSha, nextTopSha) {
23247
23286
  freshSha = previousTopSha && nextTopSha && previousTopSha !== nextTopSha ? nextTopSha : "";
23248
- refreshStatus = freshSha ? { type: "updated", sha: nextTopSha.slice(0, 7) } : { type: "unchanged", scoped: Boolean(query || pathFilter) };
23287
+ refreshStatus = { type: "none" };
23249
23288
  renderList();
23250
23289
  }
23251
- function refreshStatusMessage() {
23252
- const text2 = historyText(deps.getLanguage());
23253
- switch (refreshStatus.type) {
23254
- case "pending":
23255
- return text2.refreshPendingStatus;
23256
- case "updated":
23257
- return text2.refreshResultUpdated(refreshStatus.sha);
23258
- case "unchanged":
23259
- return refreshStatus.scoped ? text2.refreshResultUnchangedInView : text2.refreshResultUnchanged;
23260
- case "none":
23261
- return "";
23262
- default: {
23263
- const exhaustive = refreshStatus;
23264
- return exhaustive;
23265
- }
23266
- }
23267
- }
23268
23290
  function syncRefreshStatusText() {
23269
- setStatusText(loading ? "loading..." : refreshStatusMessage() || (commits.length ? "" : "no commits"));
23291
+ setStatusText(loading ? "loading..." : refreshStatus.type === "pending" ? "" : commits.length ? "" : "no commits");
23270
23292
  }
23271
23293
  function commitInfoElement() {
23272
23294
  return activeMount.commitInfo || document.querySelector("#history-commit-info");
@@ -23377,19 +23399,17 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
23377
23399
  button.setAttribute("aria-label", title);
23378
23400
  button.classList.toggle("has-update", hasPendingUpdate);
23379
23401
  const label = button.querySelector(".history-refresh-label");
23380
- if (label) {
23381
- label.textContent = hasPendingUpdate ? text2.refreshLabelPending : text2.refreshLabel;
23382
- }
23402
+ if (label)
23403
+ label.textContent = "";
23383
23404
  }
23384
23405
  function syncRefreshResult(result) {
23385
23406
  const el = result ?? activeMount.refreshResult;
23386
23407
  if (!el)
23387
23408
  return;
23388
- const message = refreshStatus.type === "pending" || refreshStatus.type === "updated" || refreshStatus.type === "unchanged" ? refreshStatusMessage() : "";
23389
- el.textContent = message;
23390
- el.hidden = message.length === 0;
23391
- el.classList.toggle("changed", refreshStatus.type === "updated");
23392
- el.classList.toggle("pending", refreshStatus.type === "pending");
23409
+ el.textContent = "";
23410
+ el.hidden = true;
23411
+ el.classList.toggle("changed", false);
23412
+ el.classList.toggle("pending", false);
23393
23413
  }
23394
23414
  function syncFilterClearButton(button) {
23395
23415
  const clearButton = button ?? activeMount.filterClearButton;
@@ -24404,11 +24424,11 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
24404
24424
  },
24405
24425
  {
24406
24426
  kind: "paragraph",
24407
- text: "Each row reports OK / WARN / ERROR with a remediation hint when relevant. The check groups are Runtime (Node / Bun / NODE_MODULE_VERSION), Package (version + execution origin including npx cache), SQLite driver, Snapshot store, Git, Discovery summary, Docker / Compose (CLI / v2 plugin / daemon / compose config dry-parse / compose ps health per discovered service), and Server. The most common hint is the npx cache fix for better-sqlite3 NODE_MODULE_VERSION mismatch (rm -rf ~/.npm/_npx then re-run with npx -y @youtyan/code-viewer@latest)."
24427
+ text: "Each row reports OK / WARN / ERROR with a remediation hint when relevant. The check groups are Runtime (Node / Bun / NODE_MODULE_VERSION), Package (version + execution origin including npx cache), SQLite driver, Snapshot store, Git, GitHub CLI, Discovery summary, Docker / Compose (CLI / v2 plugin / daemon / compose config dry-parse / compose ps health per discovered service), and Server. The most common hint is the npx cache fix for better-sqlite3 NODE_MODULE_VERSION mismatch (rm -rf ~/.npm/_npx then re-run with npx -y @youtyan/code-viewer@latest)."
24408
24428
  },
24409
24429
  {
24410
24430
  kind: "paragraph",
24411
- text: 'From the terminal — and for AI agents or CI — the same report is available without a browser. `code-viewer doctor` prints a status summary; add `--json` for the full DoctorReport (matches the /_doctor endpoint). Use `--bin git=/absolute/path` or `--bin docker=/absolute/path` when PATH resolves the wrong tool. Exit code is 1 when worstStatus is "error", so it doubles as a CI gate.'
24431
+ text: 'From the terminal — and for AI agents or CI — the same report is available without a browser. `code-viewer doctor` prints a status summary; add `--json` for the full DoctorReport (matches the /_doctor endpoint). Use `--bin git=/absolute/path`, `--bin docker=/absolute/path`, or `--bin gh=/absolute/path` when PATH resolves the wrong tool. Exit code is 1 when worstStatus is "error", so it doubles as a CI gate.'
24412
24432
  },
24413
24433
  {
24414
24434
  kind: "command",
@@ -24420,7 +24440,7 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
24420
24440
  title: "Doctor JSON for agents and CI",
24421
24441
  command: `code-viewer doctor --json
24422
24442
  code-viewer doctor --cwd /path/to/repo --port 64160 --json
24423
- code-viewer doctor --bin git=/opt/bin/git --bin docker=/opt/bin/docker --json`
24443
+ code-viewer doctor --bin git=/opt/bin/git --bin docker=/opt/bin/docker --bin gh=/opt/bin/gh --json`
24424
24444
  }
24425
24445
  ]
24426
24446
  }
@@ -24776,7 +24796,7 @@ code-viewer query snapshot delete --id snap-abc123`
24776
24796
  skills: {
24777
24797
  nav: "Agent Skill",
24778
24798
  title: "Agent Skill Setup",
24779
- intro: "The package bundles three skills — code-viewer-annotate, code-viewer-query, and code-viewer-snapshot — so AI agents know when and how to create browser walkthroughs, run read-only queries, and capture snapshot / diff sessions. A single skill install command copies all three into the selected agent directories.",
24799
+ intro: "The package bundles four skills — code-viewer-annotate, code-viewer-journal, code-viewer-query, and code-viewer-snapshot — so AI agents know when and how to create browser walkthroughs, manage Work Log task queues, run read-only queries, and capture snapshot / diff sessions. A single skill install command copies all four into the selected agent directories.",
24780
24800
  groups: [
24781
24801
  {
24782
24802
  title: "Install the skills",
@@ -24821,12 +24841,16 @@ code-viewer query snapshot delete --id snap-abc123`
24821
24841
  "Code reviews, onboarding walkthroughs, and explanations of changes."
24822
24842
  ],
24823
24843
  [
24824
- "How to target code",
24825
- "Use --file and --line, and include --from / --to when explaining a diff range."
24844
+ "How to manage Work Log tasks",
24845
+ "Use the journal CLI for task lists, task creation, claim/done flows, and GitHub Issue linking."
24846
+ ],
24847
+ [
24848
+ "How to inspect data",
24849
+ "Use read-only query commands for schema, SQL, saved query history, and table-wide search."
24826
24850
  ],
24827
24851
  [
24828
- "How to keep history clean",
24829
- "Use one session per topic, edit wrong notes in place, and avoid unrelated cleanup."
24852
+ "How to verify data changes",
24853
+ "Use snapshot create/list/diff workflows to compare before and after states."
24830
24854
  ]
24831
24855
  ]
24832
24856
  }
@@ -24841,8 +24865,10 @@ code-viewer query snapshot delete --id snap-abc123`
24841
24865
  },
24842
24866
  {
24843
24867
  kind: "command",
24844
- title: "Show the agent guide",
24845
- command: "code-viewer annotate agent-help"
24868
+ title: "Show the agent guides",
24869
+ command: `code-viewer annotate agent-help
24870
+ code-viewer journal agent-help
24871
+ code-viewer query agent-help`
24846
24872
  }
24847
24873
  ]
24848
24874
  }
@@ -25057,11 +25083,11 @@ code-viewer query snapshot delete --id snap-abc123`
25057
25083
  },
25058
25084
  {
25059
25085
  kind: "paragraph",
25060
- text: "各項目は OK / WARN / ERROR で表示され、必要に応じて対処手順のヒントが付きます。診断グループは Runtime (Node / Bun / NODE_MODULE_VERSION)、Package (バージョン + 実行元: npx cache / global / local / bunx)、SQLite driver、Snapshot store、Git、Discovery summary、Docker / Compose (CLI / v2 plugin / daemon / compose config dry parse / compose ps による各サービスのヘルス)、Server (待ち受けポート) 8 つ。よくあるヒントは npx キャッシュ起因の better-sqlite3 NODE_MODULE_VERSION 不一致で、rm -rf ~/.npm/_npx の後に npx -y @youtyan/code-viewer@latest を再実行する手順を表示します。"
25086
+ text: "各項目は OK / WARN / ERROR で表示され、必要に応じて対処手順のヒントが付きます。診断グループは Runtime (Node / Bun / NODE_MODULE_VERSION)、Package (バージョン + 実行元: npx cache / global / local / bunx)、SQLite driver、Snapshot store、Git、GitHub CLI、Discovery summary、Docker / Compose (CLI / v2 plugin / daemon / compose config dry parse / compose ps による各サービスのヘルス)、Server (待ち受けポート) です。よくあるヒントは npx キャッシュ起因の better-sqlite3 NODE_MODULE_VERSION 不一致で、rm -rf ~/.npm/_npx の後に npx -y @youtyan/code-viewer@latest を再実行する手順を表示します。"
25061
25087
  },
25062
25088
  {
25063
25089
  kind: "paragraph",
25064
- text: 'ターミナルからは `code-viewer doctor` で同じレポートを取得できます。AI エージェントや CI からは `--json` で /_doctor と同じ DoctorReport を受け取れます。PATH と別の実行ファイルを使いたい場合は `--bin git=/absolute/path` や `--bin docker=/absolute/path` を指定できます。worstStatus が "error" なら exit code 1 を返すので CI ガードに直接使えます。'
25090
+ text: 'ターミナルからは `code-viewer doctor` で同じレポートを取得できます。AI エージェントや CI からは `--json` で /_doctor と同じ DoctorReport を受け取れます。PATH と別の実行ファイルを使いたい場合は `--bin git=/absolute/path`、`--bin docker=/absolute/path`、`--bin gh=/absolute/path` を指定できます。worstStatus が "error" なら exit code 1 を返すので CI ガードに直接使えます。'
25065
25091
  },
25066
25092
  {
25067
25093
  kind: "command",
@@ -25073,7 +25099,7 @@ code-viewer query snapshot delete --id snap-abc123`
25073
25099
  title: "AI / CI 用 doctor JSON",
25074
25100
  command: `code-viewer doctor --json
25075
25101
  code-viewer doctor --cwd /path/to/repo --port 64160 --json
25076
- code-viewer doctor --bin git=/opt/bin/git --bin docker=/opt/bin/docker --json`
25102
+ code-viewer doctor --bin git=/opt/bin/git --bin docker=/opt/bin/docker --bin gh=/opt/bin/gh --json`
25077
25103
  }
25078
25104
  ]
25079
25105
  }
@@ -25429,7 +25455,7 @@ code-viewer query snapshot delete --id snap-abc123`
25429
25455
  skills: {
25430
25456
  nav: "スキル登録",
25431
25457
  title: "Agent Skill の登録",
25432
- intro: "このパッケージには 3 つのスキル (code-viewer-annotate / code-viewer-query / code-viewer-snapshot) が同梱されており、AI エージェントに annotate でのウォークスルー、read-only な query、スナップショット / 差分の使い分けを教えます。skill install を 1 回叩くと、選んだエージェントすべてに 3 スキルがコピーされます。",
25458
+ intro: "このパッケージには 4 つのスキル (code-viewer-annotate / code-viewer-journal / code-viewer-query / code-viewer-snapshot) が同梱されており、AI エージェントに annotate でのウォークスルー、Work Log タスク管理、read-only な query、スナップショット / 差分の使い分けを教えます。skill install を 1 回叩くと、選んだエージェントすべてに 4 スキルがコピーされます。",
25433
25459
  groups: [
25434
25460
  {
25435
25461
  title: "スキルをインストールする",
@@ -25474,12 +25500,16 @@ code-viewer query snapshot delete --id snap-abc123`
25474
25500
  "コードレビュー、オンボーディング、変更内容の解説で使うこと。"
25475
25501
  ],
25476
25502
  [
25477
- "どこへ注釈するか",
25478
- "--file --line を使い、diff 範囲を説明するときは --from / --to も渡すこと。"
25503
+ "Work Log タスクをどう扱うか",
25504
+ "journal CLI でタスク一覧取得、作成、claim/done、GitHub Issue 紐づけを行うこと。"
25479
25505
  ],
25480
25506
  [
25481
- "履歴をきれいに保つ方法",
25482
- "1テーマ1セッション、間違いは edit で修正、無関係な削除をしないこと。"
25507
+ "データをどう調査するか",
25508
+ "read-only query コマンドでスキーマ、SQL、履歴、横断検索を扱うこと。"
25509
+ ],
25510
+ [
25511
+ "データ変更をどう検証するか",
25512
+ "snapshot create/list/diff で before/after を比較すること。"
25483
25513
  ]
25484
25514
  ]
25485
25515
  }
@@ -25495,7 +25525,9 @@ code-viewer query snapshot delete --id snap-abc123`
25495
25525
  {
25496
25526
  kind: "command",
25497
25527
  title: "AI 向けガイドを表示",
25498
- command: "code-viewer annotate agent-help"
25528
+ command: `code-viewer annotate agent-help
25529
+ code-viewer journal agent-help
25530
+ code-viewer query agent-help`
25499
25531
  }
25500
25532
  ]
25501
25533
  }
@@ -26177,6 +26209,2004 @@ code-viewer query snapshot delete --id snap-abc123`
26177
26209
  return { setupHunkExpand };
26178
26210
  }
26179
26211
 
26212
+ // web-src/core/journal.ts
26213
+ var JOURNAL_TASK_STATUSES = [
26214
+ "draft",
26215
+ "todo",
26216
+ "doing",
26217
+ "blocked",
26218
+ "done"
26219
+ ];
26220
+ var JOURNAL_TASK_PRIORITIES = ["p0", "p1", "p2", "p3"];
26221
+ var JOURNAL_LABEL_MAX_CHARS = 48;
26222
+ var JOURNAL_MAX_LABELS = 24;
26223
+ var PRIORITY_SCORE = {
26224
+ p0: 0,
26225
+ p1: 1,
26226
+ p2: 2,
26227
+ p3: 3
26228
+ };
26229
+ function isJournalTaskStatus(value) {
26230
+ return JOURNAL_TASK_STATUSES.includes(value);
26231
+ }
26232
+ function todayIsoDate(now = new Date) {
26233
+ const year = now.getFullYear();
26234
+ const month = String(now.getMonth() + 1).padStart(2, "0");
26235
+ const date = String(now.getDate()).padStart(2, "0");
26236
+ return `${year}-${month}-${date}`;
26237
+ }
26238
+ function taskClaimActive(task, now = Date.now()) {
26239
+ if (!task.claim)
26240
+ return false;
26241
+ const expires = Date.parse(task.claim.lease_expires_at);
26242
+ return Number.isFinite(expires) && expires > now;
26243
+ }
26244
+ function normalizeJournalLabel(value) {
26245
+ if (typeof value !== "string")
26246
+ return null;
26247
+ const label = value.trim().toLowerCase().replace(/\s+/g, "-").replace(/[^\p{L}\p{N}._:-]+/gu, "").slice(0, JOURNAL_LABEL_MAX_CHARS).replace(/^[-_.:]+|[-_.:]+$/g, "");
26248
+ return label || null;
26249
+ }
26250
+ function normalizeJournalLabels(raw) {
26251
+ if (!Array.isArray(raw))
26252
+ return [];
26253
+ const labels = [];
26254
+ const seen = new Set;
26255
+ for (const item of raw) {
26256
+ const label = normalizeJournalLabel(item);
26257
+ if (!label || seen.has(label))
26258
+ continue;
26259
+ seen.add(label);
26260
+ labels.push(label);
26261
+ if (labels.length >= JOURNAL_MAX_LABELS)
26262
+ break;
26263
+ }
26264
+ return labels;
26265
+ }
26266
+ function journalIssueLabel(issueNumber) {
26267
+ return `issue-${issueNumber}`;
26268
+ }
26269
+ function journalIssueRepoLabel(repo) {
26270
+ if (typeof repo !== "string")
26271
+ return null;
26272
+ return normalizeJournalLabel(`repo-${repo.replace(/[\\/]+/g, "-")}`);
26273
+ }
26274
+ function filterJournalTasks(state, filter = {}) {
26275
+ const statuses = Array.isArray(filter.status) ? filter.status : filter.status ? [filter.status] : undefined;
26276
+ const labels = filter.labels?.filter(Boolean) || [];
26277
+ return state.tasks.filter((task) => {
26278
+ if (statuses && !statuses.includes(task.status))
26279
+ return false;
26280
+ if (labels.length && !labels.every((label) => task.labels.includes(label))) {
26281
+ return false;
26282
+ }
26283
+ if (!filter.includeClaimed && taskClaimActive(task, filter.now))
26284
+ return false;
26285
+ return true;
26286
+ });
26287
+ }
26288
+ function selectNextJournalTasks(state, filter = {}, limit = 1) {
26289
+ const status = filter.status || "todo";
26290
+ const indexed = filterJournalTasks(state, { ...filter, status }).map((task, index) => ({ task, index }));
26291
+ indexed.sort((a2, b2) => {
26292
+ const priority = PRIORITY_SCORE[a2.task.priority] - PRIORITY_SCORE[b2.task.priority];
26293
+ if (priority !== 0)
26294
+ return priority;
26295
+ if (a2.index !== b2.index)
26296
+ return a2.index - b2.index;
26297
+ return a2.task.created_at.localeCompare(b2.task.created_at);
26298
+ });
26299
+ return indexed.slice(0, Math.max(0, limit)).map((item) => item.task);
26300
+ }
26301
+
26302
+ // web-src/views/journal-view.ts
26303
+ var GITHUB_ISSUE_INITIAL_LIMIT = 30;
26304
+ var GITHUB_ISSUE_LIMIT_STEP = 30;
26305
+ var GITHUB_ISSUE_LIMIT_MAX = 100;
26306
+ var GITHUB_ISSUE_RATE_LIMIT_RETRY_MS = 15000;
26307
+ var TASK_DRAG_AUTOSCROLL_EDGE_PX = 72;
26308
+ var TASK_DRAG_AUTOSCROLL_MAX_STEP_PX = 22;
26309
+ var TASK_EDITOR_WIDTH_STORAGE_KEY = "code-viewer:journal-task-editor-width";
26310
+ var TASK_EDITOR_DEFAULT_WIDTH = 380;
26311
+ var TASK_EDITOR_MIN_WIDTH = 320;
26312
+ var TASK_EDITOR_MAX_WIDTH = 760;
26313
+ function isJournalRoute(route) {
26314
+ return route.screen === "journal";
26315
+ }
26316
+ function parseGithubLabels(raw) {
26317
+ const labels = [];
26318
+ const seen = new Set;
26319
+ for (const item of raw.split(",")) {
26320
+ const label = item.trim().slice(0, 200);
26321
+ if (!label || seen.has(label))
26322
+ continue;
26323
+ seen.add(label);
26324
+ labels.push(label);
26325
+ }
26326
+ return labels;
26327
+ }
26328
+ function taskDateRange(task) {
26329
+ const first = task.source_date || task.due_date;
26330
+ const last = task.due_date || task.source_date;
26331
+ if (!first || !last)
26332
+ return null;
26333
+ return first <= last ? { start: first, end: last } : { start: last, end: first };
26334
+ }
26335
+ function taskCoversDate(task, date) {
26336
+ const range = taskDateRange(task);
26337
+ return !!range && range.start <= date && date <= range.end;
26338
+ }
26339
+ function monthKey(date) {
26340
+ return date.slice(0, 7);
26341
+ }
26342
+ function monthTitle(date, locale) {
26343
+ const parsed = new Date(`${monthKey(date)}-01T00:00:00`);
26344
+ return parsed.toLocaleString(locale, { month: "long", year: "numeric" });
26345
+ }
26346
+ function offsetMonth(date, delta) {
26347
+ const parsed = new Date(`${monthKey(date)}-01T00:00:00`);
26348
+ parsed.setMonth(parsed.getMonth() + delta);
26349
+ return `${todayIsoDate(parsed).slice(0, 7)}-01`;
26350
+ }
26351
+ function labelChip(label) {
26352
+ const chip = document.createElement("span");
26353
+ chip.className = "journal-label-chip";
26354
+ chip.textContent = label;
26355
+ return chip;
26356
+ }
26357
+ function taskField(labelText, control) {
26358
+ const field = document.createElement("label");
26359
+ field.className = "journal-field";
26360
+ const caption = document.createElement("span");
26361
+ caption.className = "journal-field-label";
26362
+ caption.textContent = labelText;
26363
+ field.append(caption, control);
26364
+ return field;
26365
+ }
26366
+ function createLabelEditor(initialLabels, placeholder, suggestionLabels = [], removeLabelText = (label) => `Remove ${label}`) {
26367
+ const root = document.createElement("div");
26368
+ root.className = "journal-label-editor";
26369
+ const input = document.createElement("input");
26370
+ input.className = "journal-label-editor-input";
26371
+ input.type = "text";
26372
+ input.autocomplete = "off";
26373
+ const labels = normalizeJournalLabels(initialLabels);
26374
+ const suggestions = normalizeJournalLabels(suggestionLabels);
26375
+ const menu = document.createElement("div");
26376
+ menu.className = "journal-label-editor-suggestions";
26377
+ menu.hidden = true;
26378
+ const quick = document.createElement("div");
26379
+ quick.className = "journal-label-editor-quick";
26380
+ quick.hidden = true;
26381
+ let highlightedSuggestion = -1;
26382
+ const suggestionItems = () => {
26383
+ const raw = input.value.trim().toLowerCase();
26384
+ const normalized = normalizeJournalLabel(raw) || raw;
26385
+ return suggestions.filter((label) => !labels.includes(label) && (!normalized || label.includes(normalized))).slice(0, 8);
26386
+ };
26387
+ const renderSuggestions = () => {
26388
+ const items = suggestionItems();
26389
+ const hasQuery = !!(normalizeJournalLabel(input.value) || input.value.trim());
26390
+ menu.replaceChildren();
26391
+ if (!items.length || !hasQuery || document.activeElement !== input) {
26392
+ menu.hidden = true;
26393
+ highlightedSuggestion = -1;
26394
+ return;
26395
+ }
26396
+ menu.hidden = false;
26397
+ if (highlightedSuggestion >= items.length)
26398
+ highlightedSuggestion = 0;
26399
+ items.forEach((label, index) => {
26400
+ const button = document.createElement("button");
26401
+ button.type = "button";
26402
+ button.className = "journal-label-editor-suggestion";
26403
+ button.classList.toggle("active", index === highlightedSuggestion);
26404
+ button.textContent = label;
26405
+ button.addEventListener("mousedown", (event) => {
26406
+ event.preventDefault();
26407
+ commit(label);
26408
+ });
26409
+ menu.appendChild(button);
26410
+ });
26411
+ };
26412
+ const renderQuickSuggestions = () => {
26413
+ const items = suggestionItems().slice(0, 6);
26414
+ quick.replaceChildren();
26415
+ quick.hidden = !items.length;
26416
+ for (const label of items) {
26417
+ const button = document.createElement("button");
26418
+ button.type = "button";
26419
+ button.className = "journal-label-editor-quick-chip";
26420
+ button.textContent = label;
26421
+ button.addEventListener("click", (event) => {
26422
+ event.preventDefault();
26423
+ event.stopPropagation();
26424
+ commit(label);
26425
+ });
26426
+ quick.appendChild(button);
26427
+ }
26428
+ };
26429
+ const render = () => {
26430
+ root.replaceChildren();
26431
+ for (const label of labels) {
26432
+ const chip = document.createElement("span");
26433
+ chip.className = "journal-label-editor-chip";
26434
+ const name = document.createElement("span");
26435
+ name.textContent = label;
26436
+ const remove = document.createElement("button");
26437
+ remove.type = "button";
26438
+ remove.className = "journal-label-editor-remove";
26439
+ remove.setAttribute("aria-label", removeLabelText(label));
26440
+ remove.innerHTML = iconSvg("octicon-x", X_16_PATH);
26441
+ remove.addEventListener("click", (event) => {
26442
+ event.stopPropagation();
26443
+ const index = labels.indexOf(label);
26444
+ if (index >= 0)
26445
+ labels.splice(index, 1);
26446
+ render();
26447
+ input.focus();
26448
+ });
26449
+ chip.append(name, remove);
26450
+ root.appendChild(chip);
26451
+ }
26452
+ input.placeholder = labels.length ? "" : placeholder;
26453
+ root.append(input, quick, menu);
26454
+ renderQuickSuggestions();
26455
+ renderSuggestions();
26456
+ };
26457
+ const commit = (raw) => {
26458
+ const label = normalizeJournalLabel(raw);
26459
+ input.value = "";
26460
+ if (!label || labels.includes(label))
26461
+ return false;
26462
+ labels.push(label);
26463
+ highlightedSuggestion = -1;
26464
+ render();
26465
+ input.focus();
26466
+ return true;
26467
+ };
26468
+ input.addEventListener("keydown", (event) => {
26469
+ const items = suggestionItems();
26470
+ if (event.key === "ArrowDown" && items.length) {
26471
+ event.preventDefault();
26472
+ highlightedSuggestion = (highlightedSuggestion + 1) % items.length;
26473
+ renderSuggestions();
26474
+ return;
26475
+ }
26476
+ if (event.key === "ArrowUp" && items.length) {
26477
+ event.preventDefault();
26478
+ highlightedSuggestion = (highlightedSuggestion - 1 + items.length) % items.length;
26479
+ renderSuggestions();
26480
+ return;
26481
+ }
26482
+ const value = input.value.trim();
26483
+ if (event.key === "Enter" || event.key === ",") {
26484
+ event.preventDefault();
26485
+ commit(event.key === "Enter" && highlightedSuggestion >= 0 && items[highlightedSuggestion] ? items[highlightedSuggestion] : value);
26486
+ return;
26487
+ }
26488
+ if (event.key === "Tab" && value) {
26489
+ event.preventDefault();
26490
+ commit(value);
26491
+ return;
26492
+ }
26493
+ if (event.key === "Backspace" && !value && labels.length) {
26494
+ labels.pop();
26495
+ render();
26496
+ input.focus();
26497
+ }
26498
+ });
26499
+ input.addEventListener("input", () => {
26500
+ highlightedSuggestion = -1;
26501
+ renderQuickSuggestions();
26502
+ renderSuggestions();
26503
+ });
26504
+ input.addEventListener("focus", renderSuggestions);
26505
+ input.addEventListener("paste", (event) => {
26506
+ const pasted = event.clipboardData?.getData("text") || "";
26507
+ if (!/[,\n]/.test(pasted))
26508
+ return;
26509
+ event.preventDefault();
26510
+ for (const item of pasted.split(/[,\n]+/))
26511
+ commit(item);
26512
+ });
26513
+ input.addEventListener("blur", () => {
26514
+ if (input.value.trim())
26515
+ commit(input.value);
26516
+ window.setTimeout(() => {
26517
+ menu.hidden = true;
26518
+ highlightedSuggestion = -1;
26519
+ }, 120);
26520
+ });
26521
+ root.addEventListener("click", () => input.focus());
26522
+ render();
26523
+ return {
26524
+ element: root,
26525
+ getLabels: () => [...labels]
26526
+ };
26527
+ }
26528
+ function setButtonBusy(button, busy) {
26529
+ button.disabled = busy;
26530
+ button.setAttribute("aria-busy", busy ? "true" : "false");
26531
+ }
26532
+ function createJournalView(deps) {
26533
+ const root = document.createElement("section");
26534
+ root.className = "journal-page";
26535
+ root.hidden = true;
26536
+ root.setAttribute("aria-label", deps.getText().ariaLabel);
26537
+ let mounted = false;
26538
+ let lifecycle = 0;
26539
+ let data = null;
26540
+ let activeTab = "journal";
26541
+ let selectedDate = todayIsoDate();
26542
+ let selectedEntryId = "";
26543
+ let creatingEntry = false;
26544
+ let selectedTaskId = "";
26545
+ let labelFilter = "";
26546
+ let editorMode = "split";
26547
+ let message = "";
26548
+ let draggingTaskId = "";
26549
+ let draggingGithubIssueNumber = null;
26550
+ let githubIssues = [];
26551
+ let githubIssuesLoading = false;
26552
+ let githubIssuesLoaded = false;
26553
+ let githubIssuesError = "";
26554
+ let githubRepoFilter = "";
26555
+ let githubLabelFilter = "";
26556
+ let githubSearchFilter = "";
26557
+ let githubStateFilter = "open";
26558
+ let githubIssueLimit = GITHUB_ISSUE_INITIAL_LIMIT;
26559
+ let githubIssuesRetryAt = 0;
26560
+ let githubIssueGeneration = 0;
26561
+ let githubIssuesAutoRequested = false;
26562
+ let githubInboxOpen = false;
26563
+ let suppressTaskClick = false;
26564
+ let pendingTaskEditorFocus = false;
26565
+ let pendingTaskEditorReveal = false;
26566
+ let lastTaskDropTarget = null;
26567
+ let nativeTaskDragActive = false;
26568
+ let taskEditorResizePointerId = null;
26569
+ let taskEditorResizeStartX = 0;
26570
+ let taskEditorResizeStartWidth = TASK_EDITOR_DEFAULT_WIDTH;
26571
+ let taskEditorFocusRequest = 0;
26572
+ let taskDragAutoScrollFrame = 0;
26573
+ let taskDragAutoScrollX = 0;
26574
+ let taskDragAutoScrollY = 0;
26575
+ let pointerDrag = null;
26576
+ function text2() {
26577
+ return deps.getText();
26578
+ }
26579
+ function selectedEntry() {
26580
+ if (!data)
26581
+ return null;
26582
+ return data.journal.entries.find((entry) => entry.id === selectedEntryId) || null;
26583
+ }
26584
+ function selectedTask() {
26585
+ if (!data)
26586
+ return null;
26587
+ return data.tasks.tasks.find((task) => task.id === selectedTaskId) || null;
26588
+ }
26589
+ function journalEditorHasFocus() {
26590
+ const active = document.activeElement;
26591
+ return !!(active && root.contains(active) && active.closest(".journal-editor, .journal-task-editor"));
26592
+ }
26593
+ function labelSuggestions() {
26594
+ return (data?.labels || []).filter((label) => !/^issue-\d+$/.test(label) && !label.startsWith("repo-"));
26595
+ }
26596
+ function taskEditorMaxWidth() {
26597
+ const layoutWidth = root.querySelector(".journal-tasks-layout")?.clientWidth || window.innerWidth;
26598
+ if (layoutWidth <= 1100)
26599
+ return TASK_EDITOR_MAX_WIDTH;
26600
+ return Math.max(TASK_EDITOR_MIN_WIDTH, Math.min(TASK_EDITOR_MAX_WIDTH, layoutWidth - 560));
26601
+ }
26602
+ function clampTaskEditorWidth(width) {
26603
+ return Math.max(TASK_EDITOR_MIN_WIDTH, Math.min(taskEditorMaxWidth(), Math.round(width)));
26604
+ }
26605
+ function applyTaskEditorWidth(width, persist = true) {
26606
+ const clamped = clampTaskEditorWidth(width);
26607
+ document.documentElement.style.setProperty("--journal-task-editor-w", `${clamped}px`);
26608
+ if (!persist)
26609
+ return;
26610
+ try {
26611
+ window.localStorage.setItem(TASK_EDITOR_WIDTH_STORAGE_KEY, String(clamped));
26612
+ } catch {}
26613
+ }
26614
+ function restoreTaskEditorWidth() {
26615
+ let width = TASK_EDITOR_DEFAULT_WIDTH;
26616
+ try {
26617
+ const raw = window.localStorage.getItem(TASK_EDITOR_WIDTH_STORAGE_KEY);
26618
+ const parsed = raw ? Number(raw) : NaN;
26619
+ if (Number.isFinite(parsed))
26620
+ width = parsed;
26621
+ } catch {
26622
+ width = TASK_EDITOR_DEFAULT_WIDTH;
26623
+ }
26624
+ applyTaskEditorWidth(width, false);
26625
+ }
26626
+ function startTaskEditorResize(event, panel) {
26627
+ if (event.button !== 0)
26628
+ return;
26629
+ taskEditorResizePointerId = event.pointerId;
26630
+ taskEditorResizeStartX = event.clientX;
26631
+ taskEditorResizeStartWidth = panel.getBoundingClientRect().width;
26632
+ panel.setPointerCapture?.(event.pointerId);
26633
+ document.body.classList.add("journal-task-editor-resizing");
26634
+ event.preventDefault();
26635
+ }
26636
+ function handleTaskEditorResizeMove(event) {
26637
+ if (taskEditorResizePointerId !== event.pointerId)
26638
+ return;
26639
+ applyTaskEditorWidth(taskEditorResizeStartWidth - (event.clientX - taskEditorResizeStartX), false);
26640
+ }
26641
+ function finishTaskEditorResize(event) {
26642
+ if (taskEditorResizePointerId !== event.pointerId)
26643
+ return;
26644
+ taskEditorResizePointerId = null;
26645
+ document.body.classList.remove("journal-task-editor-resizing");
26646
+ const panel = root.querySelector(".journal-task-editor");
26647
+ if (panel)
26648
+ applyTaskEditorWidth(panel.getBoundingClientRect().width);
26649
+ }
26650
+ function revealTaskEditorIfNeeded(editor) {
26651
+ const layout = root.querySelector(".journal-tasks-layout");
26652
+ if (!layout || layout.scrollHeight <= layout.clientHeight + 1)
26653
+ return;
26654
+ const layoutRect = layout.getBoundingClientRect();
26655
+ const editorRect = editor.getBoundingClientRect();
26656
+ const topPadding = 8;
26657
+ const bottomPadding = 8;
26658
+ const visibleTop = layoutRect.top + topPadding;
26659
+ const visibleBottom = layoutRect.bottom - bottomPadding;
26660
+ const minVisibleTopGap = Math.min(160, editorRect.height);
26661
+ if (editorRect.top >= visibleTop && editorRect.top <= visibleBottom - minVisibleTopGap)
26662
+ return;
26663
+ layout.scrollTop += editorRect.top - visibleTop;
26664
+ }
26665
+ function scheduleTaskEditorInteraction(options = {}) {
26666
+ if (options.focusEditor)
26667
+ pendingTaskEditorFocus = true;
26668
+ if (options.revealEditor)
26669
+ pendingTaskEditorReveal = true;
26670
+ const request = ++taskEditorFocusRequest;
26671
+ const interact = (final = false) => {
26672
+ if (request !== taskEditorFocusRequest)
26673
+ return;
26674
+ const editor = root.querySelector(".journal-task-editor");
26675
+ const input = editor?.querySelector(".journal-title-input") || null;
26676
+ if (pendingTaskEditorReveal && editor) {
26677
+ revealTaskEditorIfNeeded(editor);
26678
+ pendingTaskEditorReveal = false;
26679
+ }
26680
+ if (pendingTaskEditorFocus && input)
26681
+ input.focus({ preventScroll: true });
26682
+ if (final && request === taskEditorFocusRequest) {
26683
+ pendingTaskEditorFocus = false;
26684
+ pendingTaskEditorReveal = false;
26685
+ }
26686
+ };
26687
+ window.requestAnimationFrame(() => interact());
26688
+ window.setTimeout(() => interact(), 50);
26689
+ window.setTimeout(() => interact(true), 250);
26690
+ }
26691
+ function selectTask(taskId, options = {}) {
26692
+ activeTab = "tasks";
26693
+ selectedTaskId = taskId;
26694
+ setRoute({ tab: "tasks", task: taskId });
26695
+ render();
26696
+ if (options.focusEditor || options.revealEditor)
26697
+ scheduleTaskEditorInteraction(options);
26698
+ }
26699
+ function routeFor(next) {
26700
+ return {
26701
+ screen: "journal",
26702
+ tab: activeTab,
26703
+ date: selectedDate,
26704
+ ...labelFilter ? { label: labelFilter } : {},
26705
+ ...selectedTaskId ? { task: selectedTaskId } : {},
26706
+ ...next,
26707
+ range: deps.currentRange()
26708
+ };
26709
+ }
26710
+ function applyRoute(route = deps.getRoute()) {
26711
+ if (!isJournalRoute(route))
26712
+ return;
26713
+ activeTab = route.tab || activeTab || "journal";
26714
+ selectedDate = route.date || selectedDate || todayIsoDate();
26715
+ labelFilter = route.label || "";
26716
+ selectedTaskId = route.task || "";
26717
+ }
26718
+ function mount() {
26719
+ if (mounted)
26720
+ return;
26721
+ const content = document.getElementById("content");
26722
+ if (!content)
26723
+ return;
26724
+ document.getElementById("diff")?.setAttribute("hidden", "true");
26725
+ document.getElementById("empty")?.classList.add("hidden");
26726
+ document.getElementById("history-commit-info")?.setAttribute("hidden", "true");
26727
+ root.hidden = false;
26728
+ content.appendChild(root);
26729
+ mounted = true;
26730
+ document.body.classList.add("gdp-journal-page");
26731
+ deps.setPageMode();
26732
+ deps.syncHeaderMenu();
26733
+ }
26734
+ function suspend() {
26735
+ lifecycle++;
26736
+ if (!mounted) {
26737
+ root.remove();
26738
+ root.hidden = false;
26739
+ document.body.classList.remove("gdp-journal-page");
26740
+ document.getElementById("diff")?.removeAttribute("hidden");
26741
+ return;
26742
+ }
26743
+ root.remove();
26744
+ root.hidden = false;
26745
+ document.body.classList.remove("gdp-journal-page");
26746
+ document.getElementById("diff")?.removeAttribute("hidden");
26747
+ mounted = false;
26748
+ }
26749
+ async function requestJournal(action, body, afterResponse) {
26750
+ deps.setStatus("refreshing");
26751
+ const res = await deps.trackLoad(fetch("/_journal", {
26752
+ method: "POST",
26753
+ headers: {
26754
+ "Content-Type": "application/json",
26755
+ "X-Code-Viewer-Action": "1"
26756
+ },
26757
+ body: JSON.stringify({ action, ...body })
26758
+ }));
26759
+ if (!res.ok)
26760
+ throw new Error(await res.text());
26761
+ const response = await res.json();
26762
+ afterResponse?.(response);
26763
+ await refresh();
26764
+ return response;
26765
+ }
26766
+ async function refresh() {
26767
+ const seq = ++lifecycle;
26768
+ message = data ? "" : text2().loading;
26769
+ render();
26770
+ deps.setStatus("refreshing");
26771
+ try {
26772
+ const next = await deps.trackLoad(fetch("/_journal").then(async (res) => {
26773
+ if (!res.ok)
26774
+ throw new Error(await res.text());
26775
+ return await res.json();
26776
+ }));
26777
+ if (seq !== lifecycle)
26778
+ return;
26779
+ data = next;
26780
+ if (selectedEntryId && !next.journal.entries.some((entry) => entry.id === selectedEntryId)) {
26781
+ selectedEntryId = "";
26782
+ }
26783
+ if (!creatingEntry && !selectedEntryId) {
26784
+ selectedEntryId = next.journal.entries.find((entry) => entry.date === selectedDate)?.id || "";
26785
+ }
26786
+ if (selectedTaskId && !next.tasks.tasks.some((task) => task.id === selectedTaskId)) {
26787
+ selectedTaskId = "";
26788
+ }
26789
+ message = "";
26790
+ deps.setStatus("live");
26791
+ render();
26792
+ } catch (error2) {
26793
+ if (seq !== lifecycle)
26794
+ return;
26795
+ message = error2 instanceof Error ? error2.message : text2().loadFailed;
26796
+ deps.setStatus("error");
26797
+ render();
26798
+ }
26799
+ }
26800
+ function setRoute(next, replace2 = true) {
26801
+ deps.setRoute(routeFor(next), replace2);
26802
+ }
26803
+ function renderHeader() {
26804
+ const header = document.createElement("div");
26805
+ header.className = "journal-header";
26806
+ const title = document.createElement("div");
26807
+ title.className = "journal-title";
26808
+ title.textContent = text2().title;
26809
+ const tabs = document.createElement("div");
26810
+ tabs.className = "seg journal-tabs";
26811
+ tabs.setAttribute("role", "tablist");
26812
+ for (const tab of ["journal", "tasks"]) {
26813
+ const button = document.createElement("button");
26814
+ button.type = "button";
26815
+ button.classList.toggle("active", activeTab === tab);
26816
+ button.textContent = text2().tabs[tab];
26817
+ button.addEventListener("click", () => {
26818
+ activeTab = tab;
26819
+ setRoute({ tab });
26820
+ });
26821
+ tabs.appendChild(button);
26822
+ }
26823
+ const refreshButton = document.createElement("button");
26824
+ refreshButton.type = "button";
26825
+ refreshButton.className = "global-icon-action journal-icon-action";
26826
+ refreshButton.title = text2().refresh;
26827
+ refreshButton.setAttribute("aria-label", text2().refresh);
26828
+ refreshButton.innerHTML = iconSvg("octicon-sync", SYNC_16_PATH);
26829
+ refreshButton.addEventListener("click", () => void refresh());
26830
+ const status = document.createElement("span");
26831
+ status.className = "journal-status";
26832
+ status.textContent = message;
26833
+ status.hidden = !message;
26834
+ status.setAttribute("role", "status");
26835
+ header.append(title, tabs, refreshButton, status);
26836
+ return header;
26837
+ }
26838
+ function renderCalendar() {
26839
+ const panel = document.createElement("aside");
26840
+ panel.className = "journal-calendar";
26841
+ const head = document.createElement("div");
26842
+ head.className = "journal-calendar-head";
26843
+ const prev = document.createElement("button");
26844
+ prev.type = "button";
26845
+ prev.textContent = "<";
26846
+ prev.setAttribute("aria-label", text2().previousMonth);
26847
+ const next = document.createElement("button");
26848
+ next.type = "button";
26849
+ next.textContent = ">";
26850
+ next.setAttribute("aria-label", text2().nextMonth);
26851
+ const title = document.createElement("strong");
26852
+ title.textContent = monthTitle(selectedDate, text2().locale);
26853
+ prev.addEventListener("click", () => {
26854
+ selectedDate = offsetMonth(selectedDate, -1);
26855
+ selectedEntryId = "";
26856
+ creatingEntry = false;
26857
+ setRoute({ date: selectedDate });
26858
+ });
26859
+ next.addEventListener("click", () => {
26860
+ selectedDate = offsetMonth(selectedDate, 1);
26861
+ selectedEntryId = "";
26862
+ creatingEntry = false;
26863
+ setRoute({ date: selectedDate });
26864
+ });
26865
+ head.append(prev, title, next);
26866
+ const days = document.createElement("div");
26867
+ days.className = "journal-calendar-grid";
26868
+ for (const label of text2().weekDays) {
26869
+ const day = document.createElement("span");
26870
+ day.className = "journal-weekday";
26871
+ day.textContent = label;
26872
+ days.appendChild(day);
26873
+ }
26874
+ const monthStart = new Date(`${monthKey(selectedDate)}-01T00:00:00`);
26875
+ const first = new Date(monthStart);
26876
+ first.setDate(first.getDate() - first.getDay());
26877
+ const entryDates = new Set((data?.journal.entries || []).map((entry) => entry.date));
26878
+ const monthTasks = (data?.tasks.tasks || []).filter((task) => {
26879
+ const range = taskDateRange(task);
26880
+ return !!range && range.end >= todayIsoDate(first) && range.start <= todayIsoDate(new Date(first.getTime() + 41 * 24 * 60 * 60 * 1000));
26881
+ }).slice(0, 60);
26882
+ const today = todayIsoDate();
26883
+ for (let i2 = 0;i2 < 42; i2++) {
26884
+ const current = new Date(first);
26885
+ current.setDate(first.getDate() + i2);
26886
+ const value = todayIsoDate(current);
26887
+ const button = document.createElement("button");
26888
+ button.type = "button";
26889
+ button.className = "journal-day-button";
26890
+ button.classList.toggle("muted", monthKey(value) !== monthKey(selectedDate));
26891
+ button.classList.toggle("active", value === selectedDate);
26892
+ button.classList.toggle("today", value === today);
26893
+ button.classList.toggle("has-entry", entryDates.has(value));
26894
+ const number = document.createElement("span");
26895
+ number.className = "journal-day-number";
26896
+ number.textContent = String(current.getDate());
26897
+ const bars = document.createElement("span");
26898
+ bars.className = "journal-day-ranges";
26899
+ const tasksForDay = monthTasks.filter((item) => taskCoversDate(item, value));
26900
+ for (const task of tasksForDay.slice(0, 3)) {
26901
+ const range = taskDateRange(task);
26902
+ if (!range)
26903
+ continue;
26904
+ const bar = document.createElement("span");
26905
+ bar.className = `journal-day-range journal-range-${task.priority}`;
26906
+ bar.classList.toggle("start", value === range.start);
26907
+ bar.classList.toggle("end", value === range.end);
26908
+ bar.classList.toggle("single", value === range.start && value === range.end);
26909
+ if (value === range.start)
26910
+ bar.textContent = task.title;
26911
+ bar.title = `${task.title} (${range.start} - ${range.end})`;
26912
+ bars.appendChild(bar);
26913
+ }
26914
+ if (tasksForDay.length > 3) {
26915
+ const more = document.createElement("span");
26916
+ more.className = "journal-day-range-more";
26917
+ more.textContent = text2().moreTasks(tasksForDay.length - 3);
26918
+ more.title = tasksForDay.slice(3).map((task) => task.title).join(`
26919
+ `);
26920
+ bars.appendChild(more);
26921
+ }
26922
+ button.append(number, bars);
26923
+ button.addEventListener("click", () => {
26924
+ selectedDate = value;
26925
+ creatingEntry = false;
26926
+ selectedEntryId = data?.journal.entries.find((entry) => entry.date === value)?.id || "";
26927
+ setRoute({ date: value, tab: "journal" });
26928
+ });
26929
+ days.appendChild(button);
26930
+ }
26931
+ panel.append(head, days);
26932
+ return panel;
26933
+ }
26934
+ function renderJournalEntryList(entries) {
26935
+ const list2 = document.createElement("div");
26936
+ list2.className = "journal-entry-list";
26937
+ if (!entries.length) {
26938
+ const empty = document.createElement("div");
26939
+ empty.className = "journal-empty";
26940
+ empty.textContent = text2().noEntries;
26941
+ list2.appendChild(empty);
26942
+ return list2;
26943
+ }
26944
+ for (const entry of entries) {
26945
+ const row = document.createElement("button");
26946
+ row.type = "button";
26947
+ row.className = "journal-entry-row";
26948
+ row.classList.toggle("active", entry.id === selectedEntryId);
26949
+ row.textContent = entry.title || entry.body.split(`
26950
+ `)[0] || entry.date;
26951
+ row.addEventListener("click", () => {
26952
+ selectedEntryId = entry.id;
26953
+ creatingEntry = false;
26954
+ render();
26955
+ });
26956
+ list2.appendChild(row);
26957
+ }
26958
+ return list2;
26959
+ }
26960
+ function renderJournalEditor() {
26961
+ const selected = selectedEntry();
26962
+ const wrap = document.createElement("section");
26963
+ wrap.className = "journal-editor";
26964
+ const head = document.createElement("div");
26965
+ head.className = "journal-section-head";
26966
+ const dateInput = document.createElement("input");
26967
+ dateInput.type = "date";
26968
+ dateInput.value = selectedDate;
26969
+ dateInput.addEventListener("change", () => {
26970
+ selectedDate = dateInput.value || todayIsoDate();
26971
+ selectedEntryId = "";
26972
+ creatingEntry = false;
26973
+ setRoute({ date: selectedDate });
26974
+ });
26975
+ const newButton = document.createElement("button");
26976
+ newButton.type = "button";
26977
+ newButton.innerHTML = `${iconSvg("octicon-plus", PLUS_16_PATH)}<span>${text2().new}</span>`;
26978
+ newButton.addEventListener("click", () => {
26979
+ selectedEntryId = "";
26980
+ creatingEntry = true;
26981
+ render();
26982
+ });
26983
+ head.append(dateInput, newButton);
26984
+ const entriesForDate = (data?.journal.entries || []).filter((entry) => entry.date === selectedDate);
26985
+ const title = document.createElement("input");
26986
+ title.className = "journal-title-input";
26987
+ title.placeholder = text2().titlePlaceholder;
26988
+ title.value = selected?.title || "";
26989
+ const labels = createLabelEditor(selected ? selected.labels : [], text2().labelPlaceholder, labelSuggestions(), text2().removeLabel);
26990
+ const body = document.createElement("textarea");
26991
+ body.className = "journal-body-input";
26992
+ body.placeholder = text2().entryBodyPlaceholder;
26993
+ body.value = selected?.body || "";
26994
+ const actions = document.createElement("div");
26995
+ actions.className = "journal-editor-actions";
26996
+ const save = document.createElement("button");
26997
+ save.type = "button";
26998
+ save.className = "journal-primary-action";
26999
+ save.textContent = selected ? text2().saveEntry : text2().addEntry;
27000
+ save.addEventListener("click", async () => {
27001
+ setButtonBusy(save, true);
27002
+ try {
27003
+ const payload = {
27004
+ date: selectedDate,
27005
+ title: title.value,
27006
+ labels: labels.getLabels(),
27007
+ body: body.value
27008
+ };
27009
+ if (selected)
27010
+ await requestJournal("update-entry", { id: selected.id, ...payload });
27011
+ else
27012
+ await requestJournal("add-entry", payload, (next) => {
27013
+ if (next.entry) {
27014
+ selectedEntryId = next.entry.id;
27015
+ creatingEntry = false;
27016
+ }
27017
+ });
27018
+ } catch (error2) {
27019
+ message = error2 instanceof Error ? error2.message : text2().saveEntryFailed;
27020
+ deps.setStatus("error");
27021
+ render();
27022
+ } finally {
27023
+ setButtonBusy(save, false);
27024
+ }
27025
+ });
27026
+ actions.appendChild(save);
27027
+ if (selected) {
27028
+ const del = document.createElement("button");
27029
+ del.type = "button";
27030
+ del.className = "journal-danger-action";
27031
+ del.innerHTML = `${iconSvg("octicon-trash", TRASH_16_PATH)}<span>${text2().delete}</span>`;
27032
+ del.addEventListener("click", async () => {
27033
+ const ok = await showConfirmDialog({
27034
+ title: `${text2().delete}?`,
27035
+ body: selected.title || selected.date,
27036
+ confirmLabel: text2().delete,
27037
+ danger: true,
27038
+ focusReturnTarget: del
27039
+ });
27040
+ if (!ok)
27041
+ return;
27042
+ setButtonBusy(del, true);
27043
+ try {
27044
+ await requestJournal("delete-entry", { id: selected.id });
27045
+ selectedEntryId = "";
27046
+ creatingEntry = false;
27047
+ } catch (error2) {
27048
+ message = error2 instanceof Error ? error2.message : text2().deleteEntryFailed;
27049
+ deps.setStatus("error");
27050
+ render();
27051
+ } finally {
27052
+ setButtonBusy(del, false);
27053
+ }
27054
+ });
27055
+ actions.appendChild(del);
27056
+ }
27057
+ wrap.append(head, renderJournalEntryList(entriesForDate), title, labels.element, body, actions);
27058
+ return wrap;
27059
+ }
27060
+ function renderRelatedTasks() {
27061
+ const panel = document.createElement("aside");
27062
+ panel.className = "journal-related";
27063
+ const heading2 = document.createElement("h2");
27064
+ heading2.textContent = text2().relatedTasks;
27065
+ panel.appendChild(heading2);
27066
+ const tasks = (data?.tasks.tasks || []).filter((task) => taskCoversDate(task, selectedDate));
27067
+ if (!tasks.length) {
27068
+ const empty = document.createElement("div");
27069
+ empty.className = "journal-empty";
27070
+ empty.textContent = text2().noRelatedTasks;
27071
+ panel.appendChild(empty);
27072
+ return panel;
27073
+ }
27074
+ for (const task of tasks)
27075
+ panel.appendChild(renderTaskCard(task, { compact: true }));
27076
+ return panel;
27077
+ }
27078
+ function renderJournalTab() {
27079
+ const body = document.createElement("div");
27080
+ body.className = "journal-daily-layout";
27081
+ body.append(renderCalendar(), renderJournalEditor(), renderRelatedTasks());
27082
+ return body;
27083
+ }
27084
+ function taskVisible(task) {
27085
+ return !labelFilter || task.labels.includes(labelFilter);
27086
+ }
27087
+ function renderTaskCard(task, options = {}) {
27088
+ const card = document.createElement("button");
27089
+ card.type = "button";
27090
+ card.className = "journal-task-card";
27091
+ card.dataset.taskId = task.id;
27092
+ card.classList.toggle("selected", task.id === selectedTaskId);
27093
+ card.classList.toggle("compact", !!options.compact);
27094
+ card.addEventListener("click", (event) => {
27095
+ if (draggingTaskId || suppressTaskClick || taskDragHandleFromTarget(card, event.target))
27096
+ return;
27097
+ selectTask(task.id, { revealEditor: true });
27098
+ });
27099
+ card.addEventListener("dragstart", (event) => {
27100
+ if (!taskDragHandleFromTarget(card, event.target)) {
27101
+ event.preventDefault();
27102
+ return;
27103
+ }
27104
+ draggingTaskId = task.id;
27105
+ nativeTaskDragActive = true;
27106
+ lastTaskDropTarget = null;
27107
+ card.classList.add("dragging");
27108
+ if (event.dataTransfer) {
27109
+ event.dataTransfer.effectAllowed = "move";
27110
+ event.dataTransfer.setData("text/plain", task.id);
27111
+ const rect = card.getBoundingClientRect();
27112
+ event.dataTransfer.setDragImage?.(card, Math.max(0, Math.min(rect.width, event.clientX - rect.left)), Math.max(0, Math.min(rect.height, event.clientY - rect.top)));
27113
+ }
27114
+ });
27115
+ card.addEventListener("dragenter", (event) => {
27116
+ if (!draggingTaskId && !draggingGithubIssue() || draggingTaskId === task.id)
27117
+ return;
27118
+ event.preventDefault();
27119
+ const after = isTaskDropAfter(card, event);
27120
+ lastTaskDropTarget = after ? { after_id: task.id } : { before_id: task.id };
27121
+ markTaskDropTarget(card, after);
27122
+ });
27123
+ card.addEventListener("dragover", (event) => {
27124
+ const issue = draggingGithubIssue();
27125
+ if (!draggingTaskId && !issue || draggingTaskId === task.id)
27126
+ return;
27127
+ event.preventDefault();
27128
+ if (event.dataTransfer)
27129
+ event.dataTransfer.dropEffect = issue ? "copy" : "move";
27130
+ scheduleTaskDragAutoScroll(event);
27131
+ const after = isTaskDropAfter(card, event);
27132
+ lastTaskDropTarget = after ? { after_id: task.id } : { before_id: task.id };
27133
+ markTaskDropTarget(card, after);
27134
+ });
27135
+ card.addEventListener("dragleave", (event) => {
27136
+ const related = event.relatedTarget;
27137
+ if (related && card.contains(related))
27138
+ return;
27139
+ lastTaskDropTarget = null;
27140
+ card.classList.remove("drop-target", "drop-after");
27141
+ });
27142
+ card.addEventListener("drop", (event) => {
27143
+ const issue = draggingGithubIssue() || githubIssueFromDataTransfer(event.dataTransfer);
27144
+ if (!draggingTaskId && !issue || draggingTaskId === task.id)
27145
+ return;
27146
+ event.preventDefault();
27147
+ event.stopPropagation();
27148
+ const after = isTaskDropAfter(card, event);
27149
+ const target = after ? { after_id: task.id } : { before_id: task.id };
27150
+ lastTaskDropTarget = null;
27151
+ if (issue)
27152
+ linkGithubIssueToTask(issue, target);
27153
+ else
27154
+ moveDraggedTask(target);
27155
+ });
27156
+ card.addEventListener("dragend", (event) => finishNativeTaskDrag(event));
27157
+ card.addEventListener("pointerdown", (event) => {
27158
+ beginPointerTaskDrag(task.id, event);
27159
+ });
27160
+ const top = document.createElement("span");
27161
+ top.className = "journal-task-card-top";
27162
+ const priority = document.createElement("span");
27163
+ priority.className = `journal-priority journal-priority-${task.priority}`;
27164
+ priority.textContent = text2().priorityLabels[task.priority];
27165
+ const status = document.createElement("span");
27166
+ status.className = "journal-task-status";
27167
+ status.textContent = text2().statusLabels[task.status];
27168
+ top.append(priority, status);
27169
+ const dragHandle = document.createElement("span");
27170
+ dragHandle.className = "journal-task-drag-handle";
27171
+ dragHandle.draggable = true;
27172
+ dragHandle.title = text2().dragTask;
27173
+ dragHandle.innerHTML = iconSvg("journal-task-drag-icon", GRABBER_16_PATH);
27174
+ dragHandle.setAttribute("aria-hidden", "true");
27175
+ top.appendChild(dragHandle);
27176
+ const title = document.createElement("strong");
27177
+ title.textContent = task.title;
27178
+ const labels = document.createElement("span");
27179
+ labels.className = "journal-task-labels";
27180
+ for (const label of task.labels)
27181
+ labels.appendChild(labelChip(label));
27182
+ card.append(top, title, labels);
27183
+ if (task.due_date) {
27184
+ const due = document.createElement("span");
27185
+ due.className = "journal-task-due";
27186
+ due.textContent = `${text2().duePrefix} ${task.due_date}`;
27187
+ card.appendChild(due);
27188
+ }
27189
+ if (task.claim) {
27190
+ const claim = document.createElement("span");
27191
+ claim.className = "journal-task-claim";
27192
+ claim.textContent = text2().claimedBy(task.claim.by);
27193
+ card.appendChild(claim);
27194
+ }
27195
+ return card;
27196
+ }
27197
+ function taskDragHandleFromTarget(card, target) {
27198
+ const handle = target?.closest(".journal-task-drag-handle");
27199
+ return handle && card.contains(handle) ? handle : null;
27200
+ }
27201
+ function isTaskDropAfter(card, event) {
27202
+ const rect = card.getBoundingClientRect();
27203
+ return event.clientY > rect.top + rect.height / 2;
27204
+ }
27205
+ function clearTaskDropTargets() {
27206
+ root.querySelectorAll(".journal-task-card.drop-target, .journal-task-column.drop-target").forEach((el) => {
27207
+ el.classList.remove("drop-target", "drop-after");
27208
+ });
27209
+ }
27210
+ function clearTaskDragState() {
27211
+ root.querySelectorAll(".journal-task-card.dragging").forEach((el) => {
27212
+ el.classList.remove("dragging");
27213
+ });
27214
+ stopTaskDragAutoScroll();
27215
+ clearTaskDropTargets();
27216
+ lastTaskDropTarget = null;
27217
+ nativeTaskDragActive = false;
27218
+ draggingTaskId = "";
27219
+ suppressTaskClick = true;
27220
+ window.setTimeout(() => {
27221
+ suppressTaskClick = false;
27222
+ }, 50);
27223
+ }
27224
+ function clearGithubIssueDragState() {
27225
+ draggingGithubIssueNumber = null;
27226
+ lastTaskDropTarget = null;
27227
+ stopTaskDragAutoScroll();
27228
+ clearTaskDropTargets();
27229
+ }
27230
+ function taskCardById(taskId) {
27231
+ return root.querySelector(`.journal-task-card[data-task-id="${CSS.escape(taskId)}"]`);
27232
+ }
27233
+ function taskDragScrollTargetAtPoint(clientX, clientY) {
27234
+ const element = document.elementFromPoint(clientX, clientY);
27235
+ let cursor = element?.closest(".journal-github-issues, .journal-github-panel, .journal-task-board-wrap, .journal-tasks-layout") || root.querySelector(".journal-task-board-wrap");
27236
+ while (cursor && cursor !== root) {
27237
+ const style = window.getComputedStyle(cursor);
27238
+ if (/(auto|scroll)/.test(style.overflowY) && cursor.scrollHeight > cursor.clientHeight + 1) {
27239
+ return cursor;
27240
+ }
27241
+ cursor = cursor.parentElement;
27242
+ }
27243
+ const layout = root.querySelector(".journal-tasks-layout");
27244
+ return layout && layout.scrollHeight > layout.clientHeight + 1 ? layout : null;
27245
+ }
27246
+ function taskDragAutoScrollDelta(target, clientY) {
27247
+ const rect = target.getBoundingClientRect();
27248
+ const canScrollUp = target.scrollTop > 0;
27249
+ const canScrollDown = target.scrollTop + target.clientHeight < target.scrollHeight - 1;
27250
+ const topDistance = clientY - rect.top;
27251
+ if (canScrollUp && topDistance < TASK_DRAG_AUTOSCROLL_EDGE_PX) {
27252
+ const intensity = (TASK_DRAG_AUTOSCROLL_EDGE_PX - Math.max(0, topDistance)) / TASK_DRAG_AUTOSCROLL_EDGE_PX;
27253
+ return -Math.max(4, Math.round(TASK_DRAG_AUTOSCROLL_MAX_STEP_PX * intensity));
27254
+ }
27255
+ const bottomDistance = rect.bottom - clientY;
27256
+ if (canScrollDown && bottomDistance < TASK_DRAG_AUTOSCROLL_EDGE_PX) {
27257
+ const intensity = (TASK_DRAG_AUTOSCROLL_EDGE_PX - Math.max(0, bottomDistance)) / TASK_DRAG_AUTOSCROLL_EDGE_PX;
27258
+ return Math.max(4, Math.round(TASK_DRAG_AUTOSCROLL_MAX_STEP_PX * intensity));
27259
+ }
27260
+ return 0;
27261
+ }
27262
+ function stopTaskDragAutoScroll() {
27263
+ if (!taskDragAutoScrollFrame)
27264
+ return;
27265
+ window.cancelAnimationFrame(taskDragAutoScrollFrame);
27266
+ taskDragAutoScrollFrame = 0;
27267
+ }
27268
+ function runTaskDragAutoScroll() {
27269
+ taskDragAutoScrollFrame = 0;
27270
+ if (!draggingTaskId && !draggingGithubIssue())
27271
+ return;
27272
+ const target = taskDragScrollTargetAtPoint(taskDragAutoScrollX, taskDragAutoScrollY);
27273
+ if (!target)
27274
+ return;
27275
+ const delta = taskDragAutoScrollDelta(target, taskDragAutoScrollY);
27276
+ if (!delta)
27277
+ return;
27278
+ target.scrollBy({ top: delta, behavior: "auto" });
27279
+ markPointerDropTarget({
27280
+ clientX: taskDragAutoScrollX,
27281
+ clientY: taskDragAutoScrollY
27282
+ });
27283
+ taskDragAutoScrollFrame = window.requestAnimationFrame(runTaskDragAutoScroll);
27284
+ }
27285
+ function scheduleTaskDragAutoScroll(event) {
27286
+ taskDragAutoScrollX = event.clientX;
27287
+ taskDragAutoScrollY = event.clientY;
27288
+ if (taskDragAutoScrollFrame)
27289
+ return;
27290
+ taskDragAutoScrollFrame = window.requestAnimationFrame(runTaskDragAutoScroll);
27291
+ }
27292
+ function markTaskDropTarget(card, after) {
27293
+ clearTaskDropTargets();
27294
+ card.classList.add("drop-target");
27295
+ card.classList.toggle("drop-after", after);
27296
+ }
27297
+ function markColumnDropTarget(column) {
27298
+ clearTaskDropTargets();
27299
+ column.classList.add("drop-target");
27300
+ }
27301
+ function taskDropTargetFromPoint(event) {
27302
+ const element = document.elementFromPoint(event.clientX, event.clientY);
27303
+ if (!element)
27304
+ return null;
27305
+ const card = element.closest(".journal-task-card");
27306
+ if (card?.dataset.taskId && card.dataset.taskId !== draggingTaskId) {
27307
+ const after = isTaskDropAfter(card, event);
27308
+ return after ? { after_id: card.dataset.taskId } : { before_id: card.dataset.taskId };
27309
+ }
27310
+ const column = element.closest(".journal-task-column");
27311
+ const status = column?.dataset.status;
27312
+ return isJournalTaskStatus(status) ? { status } : null;
27313
+ }
27314
+ function finishNativeTaskDrag(event) {
27315
+ const issue = draggingGithubIssue();
27316
+ if (!draggingTaskId && !issue) {
27317
+ clearTaskDropTargets();
27318
+ stopTaskDragAutoScroll();
27319
+ return;
27320
+ }
27321
+ const target = taskDropTargetFromPoint(event) || lastTaskDropTarget;
27322
+ if (!target) {
27323
+ if (issue)
27324
+ clearGithubIssueDragState();
27325
+ else
27326
+ clearTaskDragState();
27327
+ return;
27328
+ }
27329
+ lastTaskDropTarget = null;
27330
+ if (issue)
27331
+ linkGithubIssueToTask(issue, target);
27332
+ else
27333
+ moveDraggedTask(target);
27334
+ }
27335
+ function markPointerDropTarget(event) {
27336
+ const element = document.elementFromPoint(event.clientX, event.clientY);
27337
+ const card = element?.closest(".journal-task-card");
27338
+ if (card?.dataset.taskId && card.dataset.taskId !== draggingTaskId) {
27339
+ markTaskDropTarget(card, isTaskDropAfter(card, event));
27340
+ return;
27341
+ }
27342
+ const column = element?.closest(".journal-task-column");
27343
+ if (column) {
27344
+ markColumnDropTarget(column);
27345
+ return;
27346
+ }
27347
+ clearTaskDropTargets();
27348
+ }
27349
+ function beginPointerTaskDrag(taskId, event) {
27350
+ if (event.button !== 0)
27351
+ return;
27352
+ const source = event.currentTarget;
27353
+ if (!source)
27354
+ return;
27355
+ if (!taskDragHandleFromTarget(source, event.target))
27356
+ return;
27357
+ pointerDrag = {
27358
+ taskId,
27359
+ pointerId: event.pointerId,
27360
+ source,
27361
+ startX: event.clientX,
27362
+ startY: event.clientY,
27363
+ active: false
27364
+ };
27365
+ source.setPointerCapture?.(event.pointerId);
27366
+ document.addEventListener("pointermove", handlePointerTaskDragMove);
27367
+ document.addEventListener("pointerup", handlePointerTaskDragEnd);
27368
+ document.addEventListener("pointercancel", handlePointerTaskDragCancel);
27369
+ }
27370
+ function stopPointerTaskDragListeners() {
27371
+ document.removeEventListener("pointermove", handlePointerTaskDragMove);
27372
+ document.removeEventListener("pointerup", handlePointerTaskDragEnd);
27373
+ document.removeEventListener("pointercancel", handlePointerTaskDragCancel);
27374
+ }
27375
+ function handlePointerTaskDragMove(event) {
27376
+ if (!pointerDrag)
27377
+ return;
27378
+ if (event.pointerId !== pointerDrag.pointerId)
27379
+ return;
27380
+ const distance = Math.hypot(event.clientX - pointerDrag.startX, event.clientY - pointerDrag.startY);
27381
+ if (!pointerDrag.active) {
27382
+ if (distance < 5)
27383
+ return;
27384
+ pointerDrag.active = true;
27385
+ draggingTaskId = pointerDrag.taskId;
27386
+ taskCardById(pointerDrag.taskId)?.classList.add("dragging");
27387
+ }
27388
+ event.preventDefault();
27389
+ scheduleTaskDragAutoScroll(event);
27390
+ markPointerDropTarget(event);
27391
+ }
27392
+ function handlePointerTaskDragEnd(event) {
27393
+ const state = pointerDrag;
27394
+ if (state && event.pointerId !== state.pointerId)
27395
+ return;
27396
+ pointerDrag = null;
27397
+ stopPointerTaskDragListeners();
27398
+ state?.source.releasePointerCapture?.(event.pointerId);
27399
+ if (!state?.active)
27400
+ return;
27401
+ const target = taskDropTargetFromPoint(event);
27402
+ if (target)
27403
+ moveDraggedTask(target);
27404
+ else
27405
+ clearTaskDragState();
27406
+ }
27407
+ function handlePointerTaskDragCancel(event) {
27408
+ const state = pointerDrag;
27409
+ if (state && event.pointerId !== state.pointerId)
27410
+ return;
27411
+ pointerDrag = null;
27412
+ stopPointerTaskDragListeners();
27413
+ state?.source.releasePointerCapture?.(event.pointerId);
27414
+ if (nativeTaskDragActive)
27415
+ return;
27416
+ clearTaskDragState();
27417
+ }
27418
+ async function moveDraggedTask(target) {
27419
+ const id = draggingTaskId;
27420
+ clearTaskDragState();
27421
+ if (!id)
27422
+ return;
27423
+ try {
27424
+ await requestJournal("move-task", { id, ...target }, () => {
27425
+ selectedTaskId = id;
27426
+ });
27427
+ } catch (error2) {
27428
+ message = error2 instanceof Error ? error2.message : text2().moveTaskFailed;
27429
+ deps.setStatus("error");
27430
+ render();
27431
+ }
27432
+ }
27433
+ function renderQueuePreview() {
27434
+ const queue = document.createElement("section");
27435
+ queue.className = "journal-command-preview journal-queue";
27436
+ const title = document.createElement("strong");
27437
+ title.textContent = text2().aiQueue;
27438
+ const command = document.createElement("code");
27439
+ command.textContent = labelFilter ? `code-viewer journal task-next --label ${labelFilter} --limit 5` : "code-viewer journal task-next --limit 5";
27440
+ queue.append(title, command);
27441
+ const next = data ? selectNextJournalTasks(data.tasks, { labels: labelFilter ? [labelFilter] : [] }, 5) : [];
27442
+ if (next.length) {
27443
+ const list2 = document.createElement("ol");
27444
+ for (const task of next) {
27445
+ const item = document.createElement("li");
27446
+ item.textContent = `${text2().priorityLabels[task.priority]} ${task.title}`;
27447
+ list2.appendChild(item);
27448
+ }
27449
+ queue.appendChild(list2);
27450
+ }
27451
+ return queue;
27452
+ }
27453
+ function renderGithubIssueToggle() {
27454
+ const button = document.createElement("button");
27455
+ button.type = "button";
27456
+ button.className = "journal-github-toggle";
27457
+ button.classList.toggle("active", githubInboxOpen);
27458
+ button.setAttribute("aria-expanded", githubInboxOpen ? "true" : "false");
27459
+ button.innerHTML = iconSvg("journal-github-toggle-icon", COMMENT_DISCUSSION_16_PATH);
27460
+ const label = document.createElement("span");
27461
+ label.textContent = text2().githubIssues;
27462
+ button.appendChild(label);
27463
+ const count = document.createElement("span");
27464
+ count.className = "journal-github-toggle-count";
27465
+ count.textContent = githubIssuesLoading ? "..." : githubIssuesLoaded ? String(githubIssues.length) : "0";
27466
+ button.appendChild(count);
27467
+ button.addEventListener("click", () => {
27468
+ githubInboxOpen = !githubInboxOpen;
27469
+ render();
27470
+ if (githubInboxOpen && !githubIssuesAutoRequested && !githubIssuesLoading) {
27471
+ loadGithubIssues();
27472
+ }
27473
+ });
27474
+ return button;
27475
+ }
27476
+ function issueLabel(issueNumber) {
27477
+ return journalIssueLabel(issueNumber);
27478
+ }
27479
+ function linkedTaskForIssue(issueNumber) {
27480
+ const label = issueLabel(issueNumber);
27481
+ const repoLabel = journalIssueRepoLabel(githubRepoFilter || undefined);
27482
+ return data?.tasks.tasks.find((task) => {
27483
+ if (!task.labels.includes("github") || !task.labels.includes(label)) {
27484
+ return false;
27485
+ }
27486
+ if (repoLabel)
27487
+ return task.labels.includes(repoLabel);
27488
+ return !task.labels.some((taskLabel) => taskLabel.startsWith("repo-"));
27489
+ }) || null;
27490
+ }
27491
+ function githubIssueStateLabel(state) {
27492
+ if (state === "open" || state === "closed" || state === "all") {
27493
+ return text2().githubStateLabels[state];
27494
+ }
27495
+ return state;
27496
+ }
27497
+ function closeGithubInbox() {
27498
+ if (!githubInboxOpen)
27499
+ return;
27500
+ githubInboxOpen = false;
27501
+ render();
27502
+ }
27503
+ function handleGithubInboxKeydown(event) {
27504
+ if (event.key !== "Escape" || !githubInboxOpen)
27505
+ return;
27506
+ closeGithubInbox();
27507
+ }
27508
+ function handleGithubInboxOutsidePointerDown(event) {
27509
+ if (!githubInboxOpen)
27510
+ return;
27511
+ const target = event.target;
27512
+ const panel = root.querySelector(".journal-github-panel");
27513
+ const toggle = root.querySelector(".journal-github-toggle");
27514
+ if (target && (panel?.contains(target) || toggle?.contains(target)))
27515
+ return;
27516
+ closeGithubInbox();
27517
+ }
27518
+ function resetGithubIssueFilters(shouldRender = true) {
27519
+ githubIssueGeneration++;
27520
+ githubIssueLimit = GITHUB_ISSUE_INITIAL_LIMIT;
27521
+ githubIssues = [];
27522
+ githubIssuesLoaded = false;
27523
+ githubIssuesLoading = false;
27524
+ githubIssuesAutoRequested = false;
27525
+ githubIssuesError = githubRetrySeconds() > 0 ? text2().githubRateLimited(githubRetrySeconds()) : "";
27526
+ if (shouldRender)
27527
+ render();
27528
+ }
27529
+ function stableGithubIssues(issues) {
27530
+ const byNumber = new Map;
27531
+ for (const issue of issues)
27532
+ byNumber.set(issue.number, issue);
27533
+ return [...byNumber.values()].sort((a2, b2) => b2.number - a2.number);
27534
+ }
27535
+ function githubRetrySeconds() {
27536
+ return Math.max(0, Math.ceil((githubIssuesRetryAt - Date.now()) / 1000));
27537
+ }
27538
+ function githubCanRequest() {
27539
+ return !githubIssuesLoading && githubRetrySeconds() === 0;
27540
+ }
27541
+ async function loadGithubIssues(limit = githubIssueLimit) {
27542
+ if (!githubCanRequest()) {
27543
+ render();
27544
+ return;
27545
+ }
27546
+ const seq = ++githubIssueGeneration;
27547
+ githubIssuesAutoRequested = true;
27548
+ githubIssuesLoading = true;
27549
+ githubIssueLimit = Math.min(Math.max(GITHUB_ISSUE_INITIAL_LIMIT, limit), GITHUB_ISSUE_LIMIT_MAX);
27550
+ githubIssuesError = "";
27551
+ render();
27552
+ deps.setStatus("refreshing");
27553
+ try {
27554
+ const res = await deps.trackLoad(fetch("/_journal", {
27555
+ method: "POST",
27556
+ headers: {
27557
+ "Content-Type": "application/json",
27558
+ "X-Code-Viewer-Action": "1"
27559
+ },
27560
+ body: JSON.stringify({
27561
+ action: "list-github-issues",
27562
+ repo: githubRepoFilter || undefined,
27563
+ labels: parseGithubLabels(githubLabelFilter),
27564
+ search: githubSearchFilter || undefined,
27565
+ state: githubStateFilter,
27566
+ limit: githubIssueLimit
27567
+ })
27568
+ }));
27569
+ if (!res.ok)
27570
+ throw new Error(await res.text());
27571
+ const response = await res.json();
27572
+ if (seq !== githubIssueGeneration)
27573
+ return;
27574
+ githubIssues = stableGithubIssues(response.issues || []);
27575
+ githubIssuesLoaded = true;
27576
+ deps.setStatus("live");
27577
+ } catch (error2) {
27578
+ if (seq !== githubIssueGeneration)
27579
+ return;
27580
+ if (error2 instanceof Error && /rate limit|secondary rate limit/i.test(error2.message)) {
27581
+ githubIssuesRetryAt = Date.now() + GITHUB_ISSUE_RATE_LIMIT_RETRY_MS;
27582
+ githubIssuesError = text2().githubRateLimited(githubRetrySeconds());
27583
+ window.setTimeout(() => {
27584
+ if (githubRetrySeconds() === 0) {
27585
+ githubIssuesError = "";
27586
+ render();
27587
+ }
27588
+ }, GITHUB_ISSUE_RATE_LIMIT_RETRY_MS + 50);
27589
+ } else {
27590
+ githubIssuesError = error2 instanceof Error ? error2.message : text2().githubLoadFailed;
27591
+ }
27592
+ githubIssuesLoaded = false;
27593
+ deps.setStatus("error");
27594
+ } finally {
27595
+ if (seq === githubIssueGeneration) {
27596
+ githubIssuesLoading = false;
27597
+ render();
27598
+ }
27599
+ }
27600
+ }
27601
+ async function linkGithubIssueToTask(issue, target) {
27602
+ try {
27603
+ const response = await requestJournal("link-github-issue", {
27604
+ issue_number: issue.number,
27605
+ repo: githubRepoFilter || undefined,
27606
+ title: issue.title,
27607
+ url: issue.url,
27608
+ memo_label: text2().githubMemoLabel,
27609
+ status: target.status,
27610
+ before_id: target.before_id,
27611
+ after_id: target.after_id,
27612
+ priority: "p2",
27613
+ labels: normalizeJournalLabels([labelFilter])
27614
+ });
27615
+ if (response.task) {
27616
+ selectedTaskId = response.task.id;
27617
+ githubInboxOpen = false;
27618
+ render();
27619
+ return response.task.id;
27620
+ }
27621
+ } catch (error2) {
27622
+ message = error2 instanceof Error ? error2.message : text2().githubLinkTaskFailed;
27623
+ deps.setStatus("error");
27624
+ render();
27625
+ } finally {
27626
+ stopTaskDragAutoScroll();
27627
+ draggingGithubIssueNumber = null;
27628
+ }
27629
+ return null;
27630
+ }
27631
+ function draggingGithubIssue() {
27632
+ if (draggingGithubIssueNumber === null)
27633
+ return null;
27634
+ return githubIssues.find((issue) => issue.number === draggingGithubIssueNumber) || null;
27635
+ }
27636
+ function githubIssueFromDataTransfer(dataTransfer) {
27637
+ const raw = dataTransfer?.getData("text/plain") || "";
27638
+ const match2 = raw.trim().match(/^#?(\d+)$/);
27639
+ if (!match2)
27640
+ return null;
27641
+ const issueNumber = Number(match2[1]);
27642
+ if (!Number.isInteger(issueNumber))
27643
+ return null;
27644
+ return githubIssues.find((issue) => issue.number === issueNumber) || null;
27645
+ }
27646
+ function renderGithubIssueCard(issue) {
27647
+ const card = document.createElement("article");
27648
+ card.className = "journal-github-issue-card";
27649
+ card.dataset.issueNumber = String(issue.number);
27650
+ const linked = linkedTaskForIssue(issue.number);
27651
+ card.draggable = !linked;
27652
+ if (linked)
27653
+ card.classList.add("linked");
27654
+ if (!linked) {
27655
+ card.addEventListener("dragstart", (event) => {
27656
+ draggingGithubIssueNumber = issue.number;
27657
+ lastTaskDropTarget = null;
27658
+ event.dataTransfer?.setData("text/plain", `#${issue.number}`);
27659
+ if (event.dataTransfer)
27660
+ event.dataTransfer.effectAllowed = "copyMove";
27661
+ });
27662
+ card.addEventListener("dragend", (event) => finishNativeTaskDrag(event));
27663
+ }
27664
+ const head = document.createElement("div");
27665
+ head.className = "journal-github-issue-head";
27666
+ const number = document.createElement("span");
27667
+ number.textContent = `#${issue.number}`;
27668
+ const state = document.createElement("span");
27669
+ state.textContent = githubIssueStateLabel(issue.state);
27670
+ head.append(number, state);
27671
+ if (linked) {
27672
+ const linkedBadge = document.createElement("span");
27673
+ linkedBadge.className = "journal-github-linked";
27674
+ linkedBadge.textContent = text2().githubLinked;
27675
+ head.appendChild(linkedBadge);
27676
+ }
27677
+ const title = document.createElement("strong");
27678
+ title.textContent = issue.title;
27679
+ const labels = document.createElement("div");
27680
+ labels.className = "journal-github-issue-labels";
27681
+ for (const label of issue.labels.slice(0, 4)) {
27682
+ labels.appendChild(labelChip(label));
27683
+ }
27684
+ const action = document.createElement("button");
27685
+ action.type = "button";
27686
+ action.draggable = false;
27687
+ action.className = "journal-github-issue-action";
27688
+ action.textContent = linked ? text2().githubOpenTask : text2().githubAddToBoard;
27689
+ action.addEventListener("click", async (event) => {
27690
+ event.preventDefault();
27691
+ event.stopPropagation();
27692
+ if (linked) {
27693
+ githubInboxOpen = false;
27694
+ selectTask(linked.id, { focusEditor: true, revealEditor: true });
27695
+ return;
27696
+ }
27697
+ action.disabled = true;
27698
+ const taskId = await linkGithubIssueToTask(issue, { status: "draft" });
27699
+ if (taskId)
27700
+ selectTask(taskId, { focusEditor: true, revealEditor: true });
27701
+ });
27702
+ card.append(head, title, labels, action);
27703
+ return card;
27704
+ }
27705
+ function renderGithubIssuesPanel() {
27706
+ const panel = document.createElement("section");
27707
+ panel.className = "journal-github-panel";
27708
+ const title = document.createElement("strong");
27709
+ title.textContent = text2().githubIssues;
27710
+ const controls = document.createElement("div");
27711
+ controls.className = "journal-github-controls";
27712
+ const repo = document.createElement("input");
27713
+ repo.type = "text";
27714
+ repo.placeholder = text2().githubRepoPlaceholder;
27715
+ repo.value = githubRepoFilter;
27716
+ const ghLabel = document.createElement("input");
27717
+ ghLabel.type = "text";
27718
+ ghLabel.placeholder = text2().githubLabelPlaceholder;
27719
+ ghLabel.value = githubLabelFilter;
27720
+ const search = document.createElement("input");
27721
+ search.type = "search";
27722
+ search.placeholder = text2().githubSearchPlaceholder;
27723
+ search.value = githubSearchFilter;
27724
+ const state = document.createElement("select");
27725
+ for (const value of ["open", "closed", "all"]) {
27726
+ const option = document.createElement("option");
27727
+ option.value = value;
27728
+ option.textContent = text2().githubStateLabels[value];
27729
+ option.selected = value === githubStateFilter;
27730
+ state.appendChild(option);
27731
+ }
27732
+ const syncControls = (shouldRender = true, shouldLoad = shouldRender) => {
27733
+ const nextState = state.value === "closed" || state.value === "all" ? state.value : "open";
27734
+ const nextRepo = repo.value.trim();
27735
+ const nextLabel = ghLabel.value.trim();
27736
+ const nextSearch = search.value.trim();
27737
+ const changed = nextRepo !== githubRepoFilter || nextLabel !== githubLabelFilter || nextSearch !== githubSearchFilter || nextState !== githubStateFilter;
27738
+ githubRepoFilter = nextRepo;
27739
+ githubLabelFilter = nextLabel;
27740
+ githubSearchFilter = nextSearch;
27741
+ githubStateFilter = nextState;
27742
+ if (!changed)
27743
+ return;
27744
+ resetGithubIssueFilters(shouldRender);
27745
+ if (shouldLoad && githubInboxOpen)
27746
+ loadGithubIssues();
27747
+ };
27748
+ const loadOnEnter = (event) => {
27749
+ if (event.key !== "Enter")
27750
+ return;
27751
+ event.preventDefault();
27752
+ syncControls(false, false);
27753
+ loadGithubIssues();
27754
+ };
27755
+ repo.addEventListener("change", () => syncControls());
27756
+ repo.addEventListener("keydown", loadOnEnter);
27757
+ ghLabel.addEventListener("change", () => syncControls());
27758
+ ghLabel.addEventListener("keydown", loadOnEnter);
27759
+ search.addEventListener("change", () => syncControls());
27760
+ search.addEventListener("keydown", loadOnEnter);
27761
+ state.addEventListener("change", () => syncControls());
27762
+ const load = document.createElement("button");
27763
+ load.type = "button";
27764
+ load.textContent = githubIssuesLoading ? text2().githubLoading : text2().githubLoad;
27765
+ load.disabled = !githubCanRequest();
27766
+ load.addEventListener("click", () => {
27767
+ syncControls(false, false);
27768
+ loadGithubIssues();
27769
+ });
27770
+ controls.append(repo, ghLabel, search, state, load);
27771
+ const hint = document.createElement("p");
27772
+ hint.className = "journal-github-hint";
27773
+ const showing = githubIssuesLoaded && !githubIssuesError ? ` ${text2().githubShowing(githubIssues.length, githubIssueLimit)}` : "";
27774
+ hint.textContent = githubIssuesError || `${text2().githubDragHint}${showing}`;
27775
+ const list2 = document.createElement("div");
27776
+ list2.className = "journal-github-issues";
27777
+ list2.addEventListener("dragover", (event) => {
27778
+ if (!draggingGithubIssue())
27779
+ return;
27780
+ event.preventDefault();
27781
+ if (event.dataTransfer)
27782
+ event.dataTransfer.dropEffect = "copy";
27783
+ scheduleTaskDragAutoScroll(event);
27784
+ });
27785
+ if (githubIssues.length) {
27786
+ for (const issue of githubIssues) {
27787
+ list2.appendChild(renderGithubIssueCard(issue));
27788
+ }
27789
+ } else {
27790
+ const empty = document.createElement("div");
27791
+ empty.className = "journal-empty";
27792
+ empty.textContent = githubIssuesLoading ? text2().githubLoading : githubIssuesLoaded ? text2().githubNoIssues : text2().githubNotLoaded;
27793
+ list2.appendChild(empty);
27794
+ }
27795
+ const head = document.createElement("div");
27796
+ head.className = "journal-github-panel-head";
27797
+ const close = document.createElement("button");
27798
+ close.type = "button";
27799
+ close.className = "journal-github-close";
27800
+ close.title = text2().githubClose;
27801
+ close.setAttribute("aria-label", text2().githubClose);
27802
+ close.innerHTML = iconSvg("journal-github-close-icon", X_16_PATH);
27803
+ close.addEventListener("click", () => {
27804
+ closeGithubInbox();
27805
+ });
27806
+ head.append(title, close);
27807
+ panel.append(head, controls, hint, list2);
27808
+ if (githubIssuesLoaded && githubIssues.length >= githubIssueLimit && githubIssueLimit < GITHUB_ISSUE_LIMIT_MAX) {
27809
+ const more = document.createElement("button");
27810
+ more.type = "button";
27811
+ more.className = "journal-github-load-more";
27812
+ more.textContent = text2().githubLoadMore;
27813
+ more.disabled = !githubCanRequest();
27814
+ more.addEventListener("click", () => loadGithubIssues(githubIssueLimit + GITHUB_ISSUE_LIMIT_STEP));
27815
+ panel.appendChild(more);
27816
+ }
27817
+ return panel;
27818
+ }
27819
+ function taskLabelCounts() {
27820
+ const counts = new Map;
27821
+ for (const task of data?.tasks.tasks || []) {
27822
+ for (const label of task.labels) {
27823
+ counts.set(label, (counts.get(label) || 0) + 1);
27824
+ }
27825
+ }
27826
+ return counts;
27827
+ }
27828
+ function renderLabelFilterButton(label, count, active, value = label) {
27829
+ const button = document.createElement("button");
27830
+ button.type = "button";
27831
+ button.className = "journal-label-filter";
27832
+ button.classList.toggle("active", active);
27833
+ button.setAttribute("aria-pressed", active ? "true" : "false");
27834
+ const name = document.createElement("span");
27835
+ name.className = "journal-label-filter-name";
27836
+ name.textContent = label;
27837
+ const badge = document.createElement("span");
27838
+ badge.className = "journal-label-filter-count";
27839
+ badge.textContent = String(count);
27840
+ button.append(name, badge);
27841
+ button.addEventListener("click", () => {
27842
+ labelFilter = active ? "" : value;
27843
+ setRoute({ tab: "tasks", label: labelFilter || undefined });
27844
+ });
27845
+ return button;
27846
+ }
27847
+ function renderLabelFilters() {
27848
+ const wrap = document.createElement("div");
27849
+ wrap.className = "journal-label-filters";
27850
+ wrap.setAttribute("aria-label", text2().labelFilters);
27851
+ const counts = taskLabelCounts();
27852
+ const total = data?.tasks.tasks.length || 0;
27853
+ wrap.appendChild(renderLabelFilterButton(text2().allLabels, total, !labelFilter, ""));
27854
+ for (const [label, count] of [...counts.entries()].sort((a2, b2) => a2[0].localeCompare(b2[0]))) {
27855
+ wrap.appendChild(renderLabelFilterButton(label, count, label === labelFilter));
27856
+ }
27857
+ return wrap;
27858
+ }
27859
+ function renderTaskBoard() {
27860
+ const board = document.createElement("div");
27861
+ board.className = "journal-task-board";
27862
+ for (const status of JOURNAL_TASK_STATUSES) {
27863
+ const column = document.createElement("section");
27864
+ column.className = `journal-task-column journal-task-column-${status}`;
27865
+ column.dataset.status = status;
27866
+ column.addEventListener("dragover", (event) => {
27867
+ const issue = draggingGithubIssue() || githubIssueFromDataTransfer(event.dataTransfer);
27868
+ if (!draggingTaskId && !issue)
27869
+ return;
27870
+ const targetCard = event.target?.closest(".journal-task-card");
27871
+ if (targetCard && column.contains(targetCard))
27872
+ return;
27873
+ event.preventDefault();
27874
+ if (event.dataTransfer)
27875
+ event.dataTransfer.dropEffect = issue ? "copy" : "move";
27876
+ scheduleTaskDragAutoScroll(event);
27877
+ lastTaskDropTarget = { status };
27878
+ markColumnDropTarget(column);
27879
+ });
27880
+ column.addEventListener("dragleave", (event) => {
27881
+ const related = event.relatedTarget;
27882
+ if (related && column.contains(related))
27883
+ return;
27884
+ lastTaskDropTarget = null;
27885
+ column.classList.remove("drop-target");
27886
+ });
27887
+ column.addEventListener("drop", (event) => {
27888
+ const issue = draggingGithubIssue();
27889
+ if (!draggingTaskId && !issue)
27890
+ return;
27891
+ const targetCard = event.target?.closest(".journal-task-card");
27892
+ if (targetCard && column.contains(targetCard))
27893
+ return;
27894
+ event.preventDefault();
27895
+ lastTaskDropTarget = null;
27896
+ if (issue)
27897
+ linkGithubIssueToTask(issue, { status });
27898
+ else
27899
+ moveDraggedTask({ status });
27900
+ });
27901
+ const head = document.createElement("div");
27902
+ head.className = "journal-task-column-head";
27903
+ const title = document.createElement("strong");
27904
+ title.textContent = text2().statusLabels[status];
27905
+ const count = document.createElement("span");
27906
+ const tasks = (data?.tasks.tasks || []).filter((task) => task.status === status && taskVisible(task));
27907
+ count.textContent = String(tasks.length);
27908
+ head.append(title, count);
27909
+ column.appendChild(head);
27910
+ if (!tasks.length) {
27911
+ const empty = document.createElement("div");
27912
+ empty.className = "journal-column-empty";
27913
+ empty.textContent = text2().empty;
27914
+ column.appendChild(empty);
27915
+ }
27916
+ for (const task of tasks)
27917
+ column.appendChild(renderTaskCard(task));
27918
+ board.appendChild(column);
27919
+ }
27920
+ return board;
27921
+ }
27922
+ function insertMarkdown(textarea, before, after = "") {
27923
+ const start = textarea.selectionStart;
27924
+ const end = textarea.selectionEnd;
27925
+ const selected = textarea.value.slice(start, end);
27926
+ textarea.value = textarea.value.slice(0, start) + before + selected + after + textarea.value.slice(end);
27927
+ textarea.focus();
27928
+ textarea.selectionStart = start + before.length;
27929
+ textarea.selectionEnd = end + before.length;
27930
+ }
27931
+ async function renderTaskPreview(host, body) {
27932
+ host.replaceChildren();
27933
+ const rendered = await renderMarkdownPreview(body || `_${text2().noBody}_`, { path: "journal-task.md", ref: "worktree" }, { syntaxHighlight: false });
27934
+ host.appendChild(rendered);
27935
+ }
27936
+ function renderTaskEditor() {
27937
+ const task = selectedTask();
27938
+ const panel = document.createElement("aside");
27939
+ panel.className = "journal-task-editor";
27940
+ const resizer = document.createElement("div");
27941
+ resizer.className = "journal-task-editor-resizer";
27942
+ resizer.role = "separator";
27943
+ resizer.tabIndex = 0;
27944
+ resizer.setAttribute("aria-orientation", "vertical");
27945
+ resizer.setAttribute("aria-label", text2().resizeTaskPanel);
27946
+ resizer.addEventListener("pointerdown", (event) => startTaskEditorResize(event, panel));
27947
+ const heading2 = document.createElement("div");
27948
+ heading2.className = "journal-section-head";
27949
+ const titleText = document.createElement("strong");
27950
+ titleText.textContent = task ? text2().taskHeading : text2().newTaskHeading;
27951
+ const newTask = document.createElement("button");
27952
+ newTask.type = "button";
27953
+ newTask.innerHTML = `${iconSvg("octicon-plus", PLUS_16_PATH)}<span>${text2().new}</span>`;
27954
+ newTask.addEventListener("click", () => {
27955
+ selectedTaskId = "";
27956
+ setRoute({ tab: "tasks", task: undefined });
27957
+ });
27958
+ heading2.append(titleText, newTask);
27959
+ const title = document.createElement("input");
27960
+ title.className = "journal-title-input";
27961
+ title.placeholder = text2().titlePlaceholder;
27962
+ title.value = task?.title || "";
27963
+ const row = document.createElement("div");
27964
+ row.className = "journal-task-fields";
27965
+ const status = document.createElement("select");
27966
+ for (const value of JOURNAL_TASK_STATUSES) {
27967
+ const option = document.createElement("option");
27968
+ option.value = value;
27969
+ option.textContent = text2().statusLabels[value];
27970
+ option.selected = (task?.status || "todo") === value;
27971
+ status.appendChild(option);
27972
+ }
27973
+ const priority = document.createElement("select");
27974
+ for (const value of JOURNAL_TASK_PRIORITIES) {
27975
+ const option = document.createElement("option");
27976
+ option.value = value;
27977
+ option.textContent = text2().priorityLabels[value];
27978
+ option.selected = (task?.priority || "p2") === value;
27979
+ priority.appendChild(option);
27980
+ }
27981
+ const due = document.createElement("input");
27982
+ due.type = "date";
27983
+ due.title = text2().endDate;
27984
+ due.setAttribute("aria-label", text2().endDate);
27985
+ due.value = task?.due_date || "";
27986
+ const sourceDate = document.createElement("input");
27987
+ sourceDate.type = "date";
27988
+ sourceDate.title = text2().startDate;
27989
+ sourceDate.setAttribute("aria-label", text2().startDate);
27990
+ sourceDate.value = task?.source_date || selectedDate;
27991
+ row.append(taskField(text2().statusField, status), taskField(text2().priorityField, priority), taskField(text2().startDate, sourceDate), taskField(text2().endDate, due));
27992
+ const labels = createLabelEditor(task ? task.labels : normalizeJournalLabels([labelFilter]), text2().labelPlaceholder, labelSuggestions(), text2().removeLabel);
27993
+ const toolbar = document.createElement("div");
27994
+ toolbar.className = "journal-markdown-toolbar";
27995
+ const body = document.createElement("textarea");
27996
+ body.className = "journal-body-input journal-task-body-input";
27997
+ body.placeholder = text2().taskBodyPlaceholder;
27998
+ body.value = task?.body || "";
27999
+ const preview = document.createElement("div");
28000
+ preview.className = "journal-markdown-preview";
28001
+ const syncMode = () => {
28002
+ body.hidden = editorMode === "preview";
28003
+ preview.hidden = editorMode === "write";
28004
+ renderTaskPreview(preview, body.value);
28005
+ };
28006
+ for (const spec of [
28007
+ ["B", "**", "**"],
28008
+ ["`", "`", "`"],
28009
+ ["[]", "- [ ] ", ""],
28010
+ [">", "> ", ""]
28011
+ ]) {
28012
+ const button = document.createElement("button");
28013
+ button.type = "button";
28014
+ button.textContent = spec[0];
28015
+ button.addEventListener("click", () => insertMarkdown(body, spec[1], spec[2]));
28016
+ toolbar.appendChild(button);
28017
+ }
28018
+ const modes = document.createElement("div");
28019
+ modes.className = "seg journal-editor-mode";
28020
+ for (const mode of ["write", "preview", "split"]) {
28021
+ const button = document.createElement("button");
28022
+ button.type = "button";
28023
+ button.classList.toggle("active", editorMode === mode);
28024
+ button.textContent = text2().editorModes[mode];
28025
+ button.addEventListener("click", () => {
28026
+ editorMode = mode;
28027
+ render();
28028
+ });
28029
+ modes.appendChild(button);
28030
+ }
28031
+ toolbar.appendChild(modes);
28032
+ body.addEventListener("input", () => {
28033
+ if (editorMode !== "write")
28034
+ renderTaskPreview(preview, body.value);
28035
+ });
28036
+ syncMode();
28037
+ const actions = document.createElement("div");
28038
+ actions.className = "journal-editor-actions";
28039
+ const save = document.createElement("button");
28040
+ save.type = "button";
28041
+ save.className = "journal-primary-action";
28042
+ save.textContent = task ? text2().saveTask : text2().addTask;
28043
+ save.addEventListener("click", async () => {
28044
+ setButtonBusy(save, true);
28045
+ const payload = {
28046
+ title: title.value,
28047
+ status: status.value,
28048
+ priority: priority.value,
28049
+ labels: labels.getLabels(),
28050
+ due_date: due.value || null,
28051
+ source_date: sourceDate.value || null,
28052
+ body: body.value
28053
+ };
28054
+ try {
28055
+ if (task)
28056
+ await requestJournal("update-task", { id: task.id, ...payload });
28057
+ else {
28058
+ const response = await requestJournal("add-task", payload, (next) => {
28059
+ if (next.task)
28060
+ selectedTaskId = next.task.id;
28061
+ });
28062
+ if (response.task)
28063
+ setRoute({ tab: "tasks", task: response.task.id });
28064
+ }
28065
+ } catch (error2) {
28066
+ message = error2 instanceof Error ? error2.message : text2().saveTaskFailed;
28067
+ deps.setStatus("error");
28068
+ render();
28069
+ } finally {
28070
+ setButtonBusy(save, false);
28071
+ }
28072
+ });
28073
+ actions.appendChild(save);
28074
+ if (task) {
28075
+ const claim = document.createElement("button");
28076
+ claim.type = "button";
28077
+ claim.textContent = text2().claim;
28078
+ claim.addEventListener("click", async () => {
28079
+ setButtonBusy(claim, true);
28080
+ try {
28081
+ await requestJournal("claim-task", { id: task.id, by: "user" });
28082
+ } catch (error2) {
28083
+ message = error2 instanceof Error ? error2.message : text2().claimTaskFailed;
28084
+ deps.setStatus("error");
28085
+ render();
28086
+ } finally {
28087
+ setButtonBusy(claim, false);
28088
+ }
28089
+ });
28090
+ const done = document.createElement("button");
28091
+ done.type = "button";
28092
+ done.textContent = text2().done;
28093
+ done.disabled = task.status !== "doing" || task.claim?.by !== "user";
28094
+ done.addEventListener("click", async () => {
28095
+ setButtonBusy(done, true);
28096
+ try {
28097
+ await requestJournal("complete-task", {
28098
+ id: task.id,
28099
+ by: "user",
28100
+ source: "user"
28101
+ });
28102
+ } catch (error2) {
28103
+ message = error2 instanceof Error ? error2.message : text2().doneTaskFailed;
28104
+ deps.setStatus("error");
28105
+ render();
28106
+ } finally {
28107
+ setButtonBusy(done, false);
28108
+ }
28109
+ });
28110
+ const del = document.createElement("button");
28111
+ del.type = "button";
28112
+ del.className = "journal-danger-action";
28113
+ del.innerHTML = `${iconSvg("octicon-trash", TRASH_16_PATH)}<span>${text2().delete}</span>`;
28114
+ del.addEventListener("click", async () => {
28115
+ const ok = await showConfirmDialog({
28116
+ title: `${text2().delete}?`,
28117
+ body: task.title,
28118
+ confirmLabel: text2().delete,
28119
+ danger: true,
28120
+ focusReturnTarget: del
28121
+ });
28122
+ if (!ok)
28123
+ return;
28124
+ setButtonBusy(del, true);
28125
+ try {
28126
+ const result = await requestJournal("delete-task", { id: task.id });
28127
+ if (!result.removed)
28128
+ throw new Error(text2().deleteTaskFailed);
28129
+ selectedTaskId = "";
28130
+ setRoute({ tab: "tasks", task: undefined });
28131
+ } catch (error2) {
28132
+ message = error2 instanceof Error ? error2.message : text2().deleteTaskFailed;
28133
+ deps.setStatus("error");
28134
+ render();
28135
+ } finally {
28136
+ setButtonBusy(del, false);
28137
+ }
28138
+ });
28139
+ actions.append(claim, done, del);
28140
+ }
28141
+ panel.append(resizer, heading2, title, row, labels.element, toolbar, body, preview, actions);
28142
+ return panel;
28143
+ }
28144
+ function renderTasksTab() {
28145
+ const wrap = document.createElement("div");
28146
+ wrap.className = "journal-tasks-layout";
28147
+ const toolbar = document.createElement("div");
28148
+ toolbar.className = "journal-task-toolbar";
28149
+ const labelInput = document.createElement("input");
28150
+ labelInput.type = "search";
28151
+ labelInput.placeholder = text2().labelFilterPlaceholder;
28152
+ labelInput.value = labelFilter;
28153
+ labelInput.addEventListener("change", () => {
28154
+ labelFilter = normalizeJournalLabel(labelInput.value) || "";
28155
+ setRoute({ tab: "tasks", label: labelFilter || undefined });
28156
+ });
28157
+ toolbar.append(labelInput, renderQueuePreview(), renderGithubIssueToggle(), renderLabelFilters());
28158
+ if (githubInboxOpen)
28159
+ toolbar.appendChild(renderGithubIssuesPanel());
28160
+ const boardWrap = document.createElement("div");
28161
+ boardWrap.className = "journal-task-board-wrap";
28162
+ boardWrap.appendChild(renderTaskBoard());
28163
+ wrap.append(toolbar, boardWrap, renderTaskEditor());
28164
+ return wrap;
28165
+ }
28166
+ function render() {
28167
+ root.setAttribute("aria-label", text2().ariaLabel);
28168
+ root.replaceChildren(renderHeader());
28169
+ const content = document.createElement("div");
28170
+ content.className = "journal-content";
28171
+ content.appendChild(activeTab === "journal" ? renderJournalTab() : renderTasksTab());
28172
+ root.appendChild(content);
28173
+ if (pendingTaskEditorFocus || pendingTaskEditorReveal)
28174
+ scheduleTaskEditorInteraction();
28175
+ }
28176
+ async function enter() {
28177
+ applyRoute();
28178
+ mount();
28179
+ render();
28180
+ await refresh();
28181
+ }
28182
+ function handleSse() {
28183
+ if (!mounted)
28184
+ return;
28185
+ if (draggingTaskId || draggingGithubIssueNumber !== null || pointerDrag || journalEditorHasFocus())
28186
+ return;
28187
+ refresh();
28188
+ }
28189
+ function localize() {
28190
+ if (mounted)
28191
+ render();
28192
+ }
28193
+ restoreTaskEditorWidth();
28194
+ document.addEventListener("keydown", handleGithubInboxKeydown);
28195
+ document.addEventListener("pointerdown", handleGithubInboxOutsidePointerDown);
28196
+ window.addEventListener("pointermove", handleTaskEditorResizeMove);
28197
+ window.addEventListener("pointerup", finishTaskEditorResize);
28198
+ window.addEventListener("pointercancel", finishTaskEditorResize);
28199
+ window.addEventListener("resize", () => {
28200
+ const layout = root.querySelector(".journal-tasks-layout");
28201
+ if (layout && layout.clientWidth <= 1100)
28202
+ return;
28203
+ const panel = root.querySelector(".journal-task-editor");
28204
+ if (panel)
28205
+ applyTaskEditorWidth(panel.getBoundingClientRect().width, false);
28206
+ });
28207
+ return { enter, suspend, handleSse, localize };
28208
+ }
28209
+
26180
28210
  // web-src/core/html-escape.ts
26181
28211
  function escapeHtml2(value) {
26182
28212
  return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
@@ -27700,7 +29730,7 @@ code-viewer query snapshot delete --id snap-abc123`
27700
29730
  } else {
27701
29731
  renderFlat(files, ul, onFileClick);
27702
29732
  }
27703
- $("#totals").textContent = files.length ? `${files.length} file${files.length === 1 ? "" : "s"}` : "";
29733
+ $("#totals").textContent = !repoSidebar && files.length ? `${files.length} file${files.length === 1 ? "" : "s"}` : "";
27704
29734
  const effectiveView = treeMode ? "tree" : STATE.sbView;
27705
29735
  $$(".sb-view-seg button").forEach((b2) => {
27706
29736
  b2.classList.toggle("active", b2.dataset.view === effectiveView);
@@ -32284,6 +34314,7 @@ code-viewer query snapshot delete --id snap-abc123`
32284
34314
  repo: "Repository",
32285
34315
  diff: "Diff Viewer",
32286
34316
  history: "History",
34317
+ journal: "Work Log",
32287
34318
  database: "Datastores",
32288
34319
  help: "Help"
32289
34320
  },
@@ -32421,6 +34452,103 @@ code-viewer query snapshot delete --id snap-abc123`
32421
34452
  filterTitle: "Filter commits by message, SHA, author:name, or path:file.",
32422
34453
  refreshTitle: "Refresh commit history"
32423
34454
  },
34455
+ journal: {
34456
+ locale: "en",
34457
+ ariaLabel: "Work log and tasks",
34458
+ title: "Work Log",
34459
+ tabs: {
34460
+ journal: "Log",
34461
+ tasks: "Tasks"
34462
+ },
34463
+ refresh: "Refresh work log",
34464
+ loading: "loading...",
34465
+ loadFailed: "failed to load work log",
34466
+ statusLabels: {
34467
+ draft: "Draft",
34468
+ todo: "Todo",
34469
+ doing: "Doing",
34470
+ blocked: "Blocked",
34471
+ done: "Done"
34472
+ },
34473
+ priorityLabels: {
34474
+ p0: "P0",
34475
+ p1: "P1",
34476
+ p2: "P2",
34477
+ p3: "P3"
34478
+ },
34479
+ statusField: "Status",
34480
+ priorityField: "Priority",
34481
+ previousMonth: "Previous month",
34482
+ nextMonth: "Next month",
34483
+ weekDays: ["S", "M", "T", "W", "T", "F", "S"],
34484
+ noEntries: "No logs",
34485
+ noRelatedTasks: "No related tasks",
34486
+ noBody: "No body",
34487
+ relatedTasks: "Tasks",
34488
+ new: "New",
34489
+ titlePlaceholder: "Title",
34490
+ labelPlaceholder: "labels",
34491
+ entryBodyPlaceholder: "What did you work on today?",
34492
+ addEntry: "Add log",
34493
+ saveEntry: "Save log",
34494
+ delete: "Delete",
34495
+ deleteEntryFailed: "failed to delete log",
34496
+ saveEntryFailed: "failed to save log",
34497
+ moveTaskFailed: "failed to move task",
34498
+ aiQueue: "AI queue",
34499
+ empty: "Empty",
34500
+ duePrefix: "due",
34501
+ startDate: "Start date",
34502
+ endDate: "End date",
34503
+ removeLabel: (label) => `Remove ${label}`,
34504
+ claimedBy: (name) => `claimed by ${name}`,
34505
+ taskHeading: "Task",
34506
+ newTaskHeading: "New task",
34507
+ taskBodyPlaceholder: "Task details, acceptance checklist, notes",
34508
+ addTask: "Add task",
34509
+ saveTask: "Save task",
34510
+ saveTaskFailed: "failed to save task",
34511
+ claim: "Claim",
34512
+ claimTaskFailed: "failed to claim task",
34513
+ done: "Done",
34514
+ doneTaskFailed: "failed to complete task",
34515
+ deleteTaskFailed: "failed to delete task",
34516
+ labelFilterPlaceholder: "label",
34517
+ allLabels: "All",
34518
+ labelFilters: "Label filters",
34519
+ githubIssues: "GitHub Issues",
34520
+ githubRepoPlaceholder: "repo (optional)",
34521
+ githubLabelPlaceholder: "GitHub labels, comma separated",
34522
+ githubSearchPlaceholder: "search issues",
34523
+ githubStateLabels: {
34524
+ open: "Open",
34525
+ closed: "Closed",
34526
+ all: "All"
34527
+ },
34528
+ githubLoad: "Load issues",
34529
+ githubLoadMore: "Load more",
34530
+ githubLoading: "loading issues...",
34531
+ githubLoadFailed: "failed to load GitHub issues",
34532
+ githubShowing: (count, limit) => `Showing ${count} of ${limit}.`,
34533
+ githubRateLimited: (seconds) => `GitHub rate limit hit. Try again in ${seconds}s.`,
34534
+ githubNotLoaded: "GitHub issues are not loaded",
34535
+ githubNoIssues: "No GitHub issues",
34536
+ githubClose: "Close GitHub issues",
34537
+ githubLinked: "linked",
34538
+ githubAddToBoard: "Add to board",
34539
+ githubOpenTask: "Open task",
34540
+ githubDragHint: "Add or drag an issue to link it to a local task.",
34541
+ githubLinkTaskFailed: "failed to link GitHub issue",
34542
+ githubMemoLabel: "Memo:",
34543
+ moreTasks: (count) => `${count} more`,
34544
+ resizeTaskPanel: "Resize task panel",
34545
+ dragTask: "Drag task",
34546
+ editorModes: {
34547
+ write: "write",
34548
+ preview: "preview",
34549
+ split: "split"
34550
+ }
34551
+ },
32424
34552
  quickHelp: {
32425
34553
  buttonTitle: "quick help (shortcuts)",
32426
34554
  panelTitle: "Quick Help",
@@ -32476,6 +34604,7 @@ code-viewer query snapshot delete --id snap-abc123`
32476
34604
  repo: "リポジトリ",
32477
34605
  diff: "Diff ビューア",
32478
34606
  history: "履歴",
34607
+ journal: "ワークログ",
32479
34608
  database: "データストア",
32480
34609
  help: "ヘルプ"
32481
34610
  },
@@ -32613,6 +34742,103 @@ code-viewer query snapshot delete --id snap-abc123`
32613
34742
  filterTitle: "メッセージ、SHA、author:name、path:file でコミットを絞り込みます。",
32614
34743
  refreshTitle: "コミット履歴を更新"
32615
34744
  },
34745
+ journal: {
34746
+ locale: "ja",
34747
+ ariaLabel: "ワークログとタスク",
34748
+ title: "ワークログ",
34749
+ tabs: {
34750
+ journal: "ログ",
34751
+ tasks: "タスク"
34752
+ },
34753
+ refresh: "ワークログを更新",
34754
+ loading: "読み込み中...",
34755
+ loadFailed: "ワークログの読み込みに失敗しました",
34756
+ statusLabels: {
34757
+ draft: "下書き",
34758
+ todo: "未着手",
34759
+ doing: "進行中",
34760
+ blocked: "ブロック",
34761
+ done: "完了"
34762
+ },
34763
+ priorityLabels: {
34764
+ p0: "P0",
34765
+ p1: "P1",
34766
+ p2: "P2",
34767
+ p3: "P3"
34768
+ },
34769
+ statusField: "ステータス",
34770
+ priorityField: "優先度",
34771
+ previousMonth: "前の月",
34772
+ nextMonth: "次の月",
34773
+ weekDays: ["日", "月", "火", "水", "木", "金", "土"],
34774
+ noEntries: "ログはありません",
34775
+ noRelatedTasks: "関連タスクはありません",
34776
+ noBody: "本文はありません",
34777
+ relatedTasks: "タスク",
34778
+ new: "新規",
34779
+ titlePlaceholder: "タイトル",
34780
+ labelPlaceholder: "ラベル",
34781
+ entryBodyPlaceholder: "今日の作業を記録",
34782
+ addEntry: "ログを追加",
34783
+ saveEntry: "ログを保存",
34784
+ delete: "削除",
34785
+ deleteEntryFailed: "ログの削除に失敗しました",
34786
+ saveEntryFailed: "ログの保存に失敗しました",
34787
+ moveTaskFailed: "タスクの移動に失敗しました",
34788
+ aiQueue: "AIキュー",
34789
+ empty: "空",
34790
+ duePrefix: "期限",
34791
+ startDate: "開始日",
34792
+ endDate: "終了日",
34793
+ removeLabel: (label) => `${label} を削除`,
34794
+ claimedBy: (name) => `${name} が確保中`,
34795
+ taskHeading: "タスク",
34796
+ newTaskHeading: "新規タスク",
34797
+ taskBodyPlaceholder: "詳細、受け入れ条件、メモ",
34798
+ addTask: "タスクを追加",
34799
+ saveTask: "タスクを保存",
34800
+ saveTaskFailed: "タスクの保存に失敗しました",
34801
+ claim: "確保",
34802
+ claimTaskFailed: "タスクの確保に失敗しました",
34803
+ done: "完了",
34804
+ doneTaskFailed: "タスクの完了に失敗しました",
34805
+ deleteTaskFailed: "タスクの削除に失敗しました",
34806
+ labelFilterPlaceholder: "ラベル",
34807
+ allLabels: "すべて",
34808
+ labelFilters: "ラベルフィルタ",
34809
+ githubIssues: "GitHub Issue",
34810
+ githubRepoPlaceholder: "リポジトリ(任意)",
34811
+ githubLabelPlaceholder: "GitHubラベル(カンマ区切り)",
34812
+ githubSearchPlaceholder: "Issue検索",
34813
+ githubStateLabels: {
34814
+ open: "未解決",
34815
+ closed: "解決済み",
34816
+ all: "すべて"
34817
+ },
34818
+ githubLoad: "Issueを表示",
34819
+ githubLoadMore: "さらに読む",
34820
+ githubLoading: "Issueを読み込み中...",
34821
+ githubLoadFailed: "GitHub Issueの読み込みに失敗しました",
34822
+ githubShowing: (count, limit) => `${limit}件中${count}件を表示中。`,
34823
+ githubRateLimited: (seconds) => `GitHubの制限に達しました。${seconds}秒後に再試行してください。`,
34824
+ githubNotLoaded: "GitHub Issueは未読み込みです",
34825
+ githubNoIssues: "GitHub Issueは0件です",
34826
+ githubClose: "GitHub Issueを閉じる",
34827
+ githubLinked: "紐づき済み",
34828
+ githubAddToBoard: "看板へ追加",
34829
+ githubOpenTask: "タスクを開く",
34830
+ githubDragHint: "Issueを追加またはドラッグすると、自分用タスクとして紐づけます。",
34831
+ githubLinkTaskFailed: "GitHub Issueの紐づけに失敗しました",
34832
+ githubMemoLabel: "メモ:",
34833
+ moreTasks: (count) => `他${count}件`,
34834
+ resizeTaskPanel: "タスクパネル幅を変更",
34835
+ dragTask: "タスクをドラッグ",
34836
+ editorModes: {
34837
+ write: "編集",
34838
+ preview: "プレビュー",
34839
+ split: "分割"
34840
+ }
34841
+ },
32616
34842
  quickHelp: {
32617
34843
  buttonTitle: "クイックヘルプ(ショートカット)",
32618
34844
  panelTitle: "クイックヘルプ",
@@ -32816,6 +35042,7 @@ code-viewer query snapshot delete --id snap-abc123`
32816
35042
  button.setAttribute("aria-label", text2.history.refreshTitle);
32817
35043
  });
32818
35044
  relocalizeHistory?.();
35045
+ relocalizeJournal?.();
32819
35046
  setElementText(".scope-settings-head strong", text2.settings.title);
32820
35047
  const settingsClose = document.querySelector("#scope-settings-close");
32821
35048
  settingsClose?.setAttribute("aria-label", text2.settings.close);
@@ -32885,6 +35112,7 @@ code-viewer query snapshot delete --id snap-abc123`
32885
35112
  relocalizeDatabase?.();
32886
35113
  }
32887
35114
  let relocalizeHistory = null;
35115
+ let relocalizeJournal = null;
32888
35116
  let relocalizeDatabase = null;
32889
35117
  let QUICK_HELP = null;
32890
35118
  function setViewerLanguage(language, persist = true) {
@@ -33336,6 +35564,7 @@ code-viewer query snapshot delete --id snap-abc123`
33336
35564
  return mount;
33337
35565
  }
33338
35566
  let ANNOTATIONS_UI = null;
35567
+ let JOURNAL_VIEW = null;
33339
35568
  function applyInlineAnnotations() {
33340
35569
  ANNOTATIONS_UI?.applyInlineAnnotations();
33341
35570
  }
@@ -33413,6 +35642,9 @@ code-viewer query snapshot delete --id snap-abc123`
33413
35642
  preHistoryRange = null;
33414
35643
  removeFileHistoryShell2();
33415
35644
  }
35645
+ if (previousRoute.screen === "journal" && nextRoute.screen !== "journal") {
35646
+ JOURNAL_VIEW?.suspend();
35647
+ }
33416
35648
  STATE.route = nextRoute;
33417
35649
  STATE.from = nextRoute.range.from;
33418
35650
  STATE.to = nextRoute.range.to;
@@ -33435,6 +35667,12 @@ code-viewer query snapshot delete --id snap-abc123`
33435
35667
  if (shouldDispatchFileRouteAfterSetRoute(previousRoute, nextRoute)) {
33436
35668
  dispatchFileRoute(nextRoute);
33437
35669
  }
35670
+ if (nextRoute.screen === "journal") {
35671
+ cancelActiveSourceLoad("navigation");
35672
+ setPageMode();
35673
+ removeStandaloneSource();
35674
+ JOURNAL_VIEW?.enter();
35675
+ }
33438
35676
  }
33439
35677
  function setQueryHistoryPanelOpen(open) {
33440
35678
  const panel = document.getElementById("query-history-panel");
@@ -33459,6 +35697,7 @@ code-viewer query snapshot delete --id snap-abc123`
33459
35697
  document.body.classList.toggle("gdp-history-page", historyPanelRoute);
33460
35698
  document.body.classList.toggle("gdp-file-history-page", fileHistoryRoute);
33461
35699
  document.body.classList.toggle("gdp-database-page", STATE.route.screen === "database");
35700
+ document.body.classList.toggle("gdp-journal-page", STATE.route.screen === "journal");
33462
35701
  const repoTargetWrap = document.querySelector("#repo-target-wrap");
33463
35702
  if (!repoSidebarRoute && repoTargetWrap) {
33464
35703
  repoTargetWrap.hidden = true;
@@ -33509,6 +35748,12 @@ code-viewer query snapshot delete --id snap-abc123`
33509
35748
  range: currentRange()
33510
35749
  });
33511
35750
  }
35751
+ if (link2.dataset.route === "journal") {
35752
+ link2.href = buildRoute({
35753
+ screen: "journal",
35754
+ range: currentRange()
35755
+ });
35756
+ }
33512
35757
  if (link2.dataset.route === "database") {
33513
35758
  link2.href = buildRoute({
33514
35759
  screen: "database",
@@ -33682,6 +35927,7 @@ code-viewer query snapshot delete --id snap-abc123`
33682
35927
  focusDiffLine,
33683
35928
  scrollDiffElementIntoView,
33684
35929
  expandAllFileContext,
35930
+ loadDiffFile,
33685
35931
  applyViewedState,
33686
35932
  enqueueInitialLoads
33687
35933
  } = DIFF_VIEW;
@@ -34323,6 +36569,7 @@ code-viewer query snapshot delete --id snap-abc123`
34323
36569
  function reloadDiffFromUi(trigger) {
34324
36570
  const topbarButton = $("#reload-prom");
34325
36571
  topbarButton.classList.add("spinning");
36572
+ topbarButton.setAttribute("aria-busy", "true");
34326
36573
  if (trigger && trigger !== topbarButton) {
34327
36574
  trigger.classList.add("spinning");
34328
36575
  trigger.setAttribute("aria-busy", "true");
@@ -34330,6 +36577,7 @@ code-viewer query snapshot delete --id snap-abc123`
34330
36577
  load().finally(() => {
34331
36578
  setTimeout(() => {
34332
36579
  topbarButton.classList.remove("spinning");
36580
+ topbarButton.setAttribute("aria-busy", "false");
34333
36581
  if (trigger && trigger !== topbarButton) {
34334
36582
  trigger.classList.remove("spinning");
34335
36583
  trigger.setAttribute("aria-busy", "false");
@@ -34349,6 +36597,11 @@ code-viewer query snapshot delete --id snap-abc123`
34349
36597
  setStatus("live");
34350
36598
  return Promise.resolve(null);
34351
36599
  }
36600
+ if (STATE.route.screen === "journal") {
36601
+ JOURNAL_VIEW?.enter();
36602
+ setStatus("live");
36603
+ return Promise.resolve(null);
36604
+ }
34352
36605
  if (STATE.route.screen === "file" && !(isFileHistoryRoute(STATE.route) && activeHistoryPathFilter) && dispatchFileRoute(STATE.route)) {
34353
36606
  return Promise.resolve({
34354
36607
  structureChanged: false,
@@ -34410,6 +36663,9 @@ code-viewer query snapshot delete --id snap-abc123`
34410
36663
  } else if (STATE.route.screen === "database") {
34411
36664
  setStatus("live");
34412
36665
  DATABASE_VIEW.enter(STATE.route.db, STATE.route.schema, STATE.route.table, STATE.route.tab).then(() => ANNOTATIONS_UI?.applyInlineAnnotations());
36666
+ } else if (STATE.route.screen === "journal") {
36667
+ setStatus("live");
36668
+ JOURNAL_VIEW?.enter();
34413
36669
  } else
34414
36670
  load();
34415
36671
  syncLineRefPill();
@@ -34589,6 +36845,17 @@ code-viewer query snapshot delete --id snap-abc123`
34589
36845
  event.preventDefault();
34590
36846
  closeDoctorSheet();
34591
36847
  });
36848
+ JOURNAL_VIEW = createJournalView({
36849
+ getRoute: () => STATE.route,
36850
+ setRoute,
36851
+ currentRange,
36852
+ trackLoad,
36853
+ getText: () => uiText().journal,
36854
+ setPageMode,
36855
+ syncHeaderMenu,
36856
+ setStatus
36857
+ });
36858
+ relocalizeJournal = () => JOURNAL_VIEW?.localize();
34592
36859
  const DATABASE_VIEW = createDatabaseView({
34593
36860
  setRoute,
34594
36861
  setPageMode,
@@ -34637,6 +36904,8 @@ code-viewer query snapshot delete --id snap-abc123`
34637
36904
  nextRoute = normalizeInternalFileRoute(nextRoute);
34638
36905
  if (previousRoute.screen === "database" && nextRoute.screen !== "database")
34639
36906
  DATABASE_VIEW.suspend();
36907
+ if (previousRoute.screen === "journal" && nextRoute.screen !== "journal")
36908
+ JOURNAL_VIEW?.suspend();
34640
36909
  if (isHistoryPanelRoute(previousRoute) && !isHistoryPanelRoute(nextRoute))
34641
36910
  HISTORY_VIEW.leaveHistory();
34642
36911
  if (isHistoryPanelRoute(previousRoute) && !isHistoryPanelRoute(nextRoute))
@@ -34691,6 +36960,14 @@ code-viewer query snapshot delete --id snap-abc123`
34691
36960
  setStatus("live");
34692
36961
  return;
34693
36962
  }
36963
+ if (STATE.route.screen === "journal") {
36964
+ cancelActiveSourceLoad("navigation");
36965
+ setPageMode();
36966
+ removeStandaloneSource();
36967
+ JOURNAL_VIEW?.enter();
36968
+ setStatus("live");
36969
+ return;
36970
+ }
34694
36971
  if (STATE.route.screen !== "file") {
34695
36972
  cancelActiveSourceLoad("navigation");
34696
36973
  setPageMode();
@@ -34811,6 +37088,7 @@ code-viewer query snapshot delete --id snap-abc123`
34811
37088
  focusDiffLine,
34812
37089
  scrollDiffElementIntoView,
34813
37090
  expandAllFileContext,
37091
+ loadDiffFile,
34814
37092
  scrollToFile,
34815
37093
  renderStandaloneSource,
34816
37094
  removeStandaloneSource,
@@ -35158,6 +37436,9 @@ code-viewer query snapshot delete --id snap-abc123`
35158
37436
  es.addEventListener("annotation", (event) => {
35159
37437
  ANNOTATIONS_UI?.handleSse(event.data);
35160
37438
  });
37439
+ es.addEventListener("journal", () => {
37440
+ JOURNAL_VIEW?.handleSse();
37441
+ });
35161
37442
  es.addEventListener("db-query", (event) => {
35162
37443
  DATABASE_VIEW.handleSse("db-query", event.data);
35163
37444
  });