@youtyan/code-viewer 0.3.0 → 0.4.0

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
@@ -6645,10 +6645,48 @@
6645
6645
  md.core.ruler.after("inline", "footnote_tail", footnote_tail);
6646
6646
  }
6647
6647
 
6648
- // web-src/core/markdown-preview.ts
6648
+ // web-src/core/mermaid-loader.ts
6649
+ var DEFAULT_CONFIG = {
6650
+ startOnLoad: false,
6651
+ securityLevel: "strict",
6652
+ theme: "default",
6653
+ er: { useMaxWidth: false }
6654
+ };
6649
6655
  var mermaidPromise = null;
6650
- var mermaidInitialized = false;
6651
- var shikiPromise = null;
6656
+ var initialized = false;
6657
+ function loadMermaid() {
6658
+ if (!mermaidPromise) {
6659
+ mermaidPromise = import("/mermaid.js").then((mod) => {
6660
+ const mermaid = mod.default;
6661
+ if (!initialized) {
6662
+ mermaid.initialize(DEFAULT_CONFIG);
6663
+ initialized = true;
6664
+ }
6665
+ return mermaid;
6666
+ }).catch(() => null);
6667
+ }
6668
+ return mermaidPromise;
6669
+ }
6670
+
6671
+ // web-src/core/shiki-loader.ts
6672
+ var cache = new Map;
6673
+ function loadShikiHighlighter(options) {
6674
+ const key = JSON.stringify({
6675
+ themes: [...options.themes].sort(),
6676
+ langs: [...options.langs].sort()
6677
+ });
6678
+ const cached = cache.get(key);
6679
+ if (cached)
6680
+ return cached;
6681
+ const promise = import("/shiki.js").then((mod) => mod.createHighlighter({
6682
+ themes: options.themes,
6683
+ langs: options.langs
6684
+ })).catch(() => null);
6685
+ cache.set(key, promise);
6686
+ return promise;
6687
+ }
6688
+
6689
+ // web-src/core/markdown-preview.ts
6652
6690
  var MARKDOWN_FENCE_LANG_ALIASES = {
6653
6691
  sh: "bash",
6654
6692
  zsh: "bash",
@@ -6884,17 +6922,11 @@ ${frontmatter.yaml}
6884
6922
  body: textValue.slice(closing + newline2.length + 3 + newline2.length)
6885
6923
  };
6886
6924
  }
6887
- async function loadMarkdownHighlighter() {
6888
- if (!shikiPromise) {
6889
- shikiPromise = import("/shiki.js").then((mod) => {
6890
- const typed = mod;
6891
- return typed.createHighlighter({
6892
- themes: ["github-light", "github-dark"],
6893
- langs: MARKDOWN_SHIKI_LANGS
6894
- });
6895
- }).catch(() => null);
6896
- }
6897
- return shikiPromise;
6925
+ function loadMarkdownHighlighter() {
6926
+ return loadShikiHighlighter({
6927
+ themes: ["github-light", "github-dark"],
6928
+ langs: MARKDOWN_SHIKI_LANGS
6929
+ });
6898
6930
  }
6899
6931
  function enhanceTaskLists(root) {
6900
6932
  root.querySelectorAll("[data-gdp-task]").forEach((inline2) => {
@@ -7091,24 +7123,6 @@ ${frontmatter.yaml}
7091
7123
  await renderMermaidError(node, mermaid);
7092
7124
  }
7093
7125
  }
7094
- async function loadMermaid() {
7095
- if (!mermaidPromise) {
7096
- mermaidPromise = import("/mermaid.js").then((mod) => {
7097
- const typed = mod;
7098
- const mermaid = typed.default;
7099
- if (!mermaidInitialized) {
7100
- mermaid.initialize({
7101
- startOnLoad: false,
7102
- securityLevel: "strict",
7103
- theme: "default"
7104
- });
7105
- mermaidInitialized = true;
7106
- }
7107
- return mermaid;
7108
- }).catch(() => null);
7109
- }
7110
- return mermaidPromise;
7111
- }
7112
7126
  function isMermaidErrorSvg(svg) {
7113
7127
  return !!svg && /Syntax error/i.test(svg.textContent || "");
7114
7128
  }
@@ -7210,12 +7224,12 @@ ${frontmatter.yaml}
7210
7224
  overlay.remove();
7211
7225
  };
7212
7226
  button("+", "zoom in", () => zoomCentered(1.25));
7213
- button("-", "zoom out", () => zoomCentered(0.8));
7227
+ button("-", "zoom out", () => zoomCentered(1 / 1.25));
7214
7228
  button("fit", "fit", fit);
7215
7229
  button("x", "close", close);
7216
7230
  overlay.addEventListener("wheel", (e2) => {
7217
7231
  e2.preventDefault();
7218
- zoomAt(e2.clientX, e2.clientY, e2.deltaY < 0 ? 1.12 : 0.8928571428571428);
7232
+ zoomAt(e2.clientX, e2.clientY, e2.deltaY < 0 ? 1.12 : 1 / 1.12);
7219
7233
  }, { passive: false });
7220
7234
  let dragging = false;
7221
7235
  let lastX = 0;
@@ -7251,7 +7265,7 @@ ${frontmatter.yaml}
7251
7265
  else if (e2.key === "+" || e2.key === "=")
7252
7266
  zoomCentered(1.25);
7253
7267
  else if (e2.key === "-")
7254
- zoomCentered(0.8);
7268
+ zoomCentered(1 / 1.25);
7255
7269
  };
7256
7270
  window.addEventListener("mousemove", onMove);
7257
7271
  window.addEventListener("mouseup", onUp);
@@ -7281,6 +7295,235 @@ ${frontmatter.yaml}
7281
7295
  return { width: rect.width || 800, height: rect.height || 600 };
7282
7296
  }
7283
7297
 
7298
+ // web-src/views/ui-dialog.ts
7299
+ var BACKDROP_CLASS = "gdp-dialog-backdrop";
7300
+ var DIALOG_CLASS = "gdp-dialog";
7301
+ function closeOpenDialog() {
7302
+ document.querySelector(`.${BACKDROP_CLASS}`)?.remove();
7303
+ }
7304
+ function createDialogShell(titleText, bodyText, actions) {
7305
+ closeOpenDialog();
7306
+ const backdrop = document.createElement("div");
7307
+ backdrop.className = BACKDROP_CLASS;
7308
+ const dialog = document.createElement("div");
7309
+ dialog.className = DIALOG_CLASS;
7310
+ dialog.setAttribute("role", "dialog");
7311
+ dialog.setAttribute("aria-modal", "true");
7312
+ if (titleText) {
7313
+ const titleId = "gdp-dialog-title";
7314
+ dialog.setAttribute("aria-labelledby", titleId);
7315
+ const heading2 = document.createElement("div");
7316
+ heading2.id = titleId;
7317
+ heading2.className = "gdp-dialog-title";
7318
+ heading2.textContent = titleText;
7319
+ dialog.appendChild(heading2);
7320
+ }
7321
+ const bodyId = "gdp-dialog-body";
7322
+ dialog.setAttribute("aria-describedby", bodyId);
7323
+ const body = document.createElement("div");
7324
+ body.id = bodyId;
7325
+ body.className = "gdp-dialog-body";
7326
+ if (bodyText)
7327
+ body.textContent = bodyText;
7328
+ dialog.appendChild(body);
7329
+ const actionRow = document.createElement("div");
7330
+ actionRow.className = "gdp-dialog-actions";
7331
+ actionRow.append(...actions);
7332
+ dialog.appendChild(actionRow);
7333
+ backdrop.appendChild(dialog);
7334
+ document.body.appendChild(backdrop);
7335
+ return { backdrop, body };
7336
+ }
7337
+ function trapTabKey(event, focusables) {
7338
+ if (event.key !== "Tab")
7339
+ return false;
7340
+ const index = focusables.indexOf(document.activeElement);
7341
+ if (index < 0) {
7342
+ event.preventDefault();
7343
+ focusables[0]?.focus();
7344
+ return true;
7345
+ }
7346
+ if (event.shiftKey && index <= 0) {
7347
+ event.preventDefault();
7348
+ focusables[focusables.length - 1]?.focus();
7349
+ return true;
7350
+ }
7351
+ if (!event.shiftKey && index === focusables.length - 1) {
7352
+ event.preventDefault();
7353
+ focusables[0]?.focus();
7354
+ return true;
7355
+ }
7356
+ return false;
7357
+ }
7358
+ function showConfirmDialog(opts) {
7359
+ return new Promise((resolve) => {
7360
+ const previousFocus = opts.focusReturnTarget ?? document.activeElement;
7361
+ const cancel = document.createElement("button");
7362
+ cancel.type = "button";
7363
+ cancel.className = "gdp-btn gdp-btn-sm";
7364
+ cancel.textContent = opts.cancelLabel ?? "Cancel";
7365
+ const confirm = document.createElement("button");
7366
+ confirm.type = "button";
7367
+ confirm.className = "gdp-btn gdp-btn-sm";
7368
+ if (opts.danger)
7369
+ confirm.classList.add("gdp-dialog-danger");
7370
+ confirm.textContent = opts.confirmLabel ?? "OK";
7371
+ const done = (ok) => {
7372
+ document.removeEventListener("keydown", onKeydown);
7373
+ closeOpenDialog();
7374
+ previousFocus?.focus?.();
7375
+ resolve(ok);
7376
+ };
7377
+ const onKeydown = (event) => {
7378
+ if (isImeComposing(event))
7379
+ return;
7380
+ if (event.key === "Escape") {
7381
+ event.preventDefault();
7382
+ event.stopPropagation();
7383
+ done(false);
7384
+ return;
7385
+ }
7386
+ if (event.key === "Enter" && document.activeElement !== cancel) {
7387
+ event.preventDefault();
7388
+ done(true);
7389
+ return;
7390
+ }
7391
+ trapTabKey(event, [cancel, confirm]);
7392
+ };
7393
+ cancel.addEventListener("click", () => done(false));
7394
+ confirm.addEventListener("click", () => done(true));
7395
+ const { backdrop } = createDialogShell(opts.title, opts.body, [
7396
+ cancel,
7397
+ confirm
7398
+ ]);
7399
+ backdrop.addEventListener("pointerdown", (event) => {
7400
+ if (event.target === backdrop)
7401
+ done(false);
7402
+ });
7403
+ document.addEventListener("keydown", onKeydown);
7404
+ confirm.focus();
7405
+ });
7406
+ }
7407
+ function showAlertDialog(opts) {
7408
+ return new Promise((resolve) => {
7409
+ const previousFocus = opts.focusReturnTarget ?? document.activeElement;
7410
+ const ok = document.createElement("button");
7411
+ ok.type = "button";
7412
+ ok.className = "gdp-btn gdp-btn-sm";
7413
+ if (opts.danger)
7414
+ ok.classList.add("gdp-dialog-danger");
7415
+ ok.textContent = opts.confirmLabel ?? "OK";
7416
+ const done = () => {
7417
+ document.removeEventListener("keydown", onKeydown);
7418
+ closeOpenDialog();
7419
+ previousFocus?.focus?.();
7420
+ resolve();
7421
+ };
7422
+ const onKeydown = (event) => {
7423
+ if (isImeComposing(event))
7424
+ return;
7425
+ if (event.key === "Escape" || event.key === "Enter") {
7426
+ event.preventDefault();
7427
+ event.stopPropagation();
7428
+ done();
7429
+ return;
7430
+ }
7431
+ trapTabKey(event, [ok]);
7432
+ };
7433
+ ok.addEventListener("click", done);
7434
+ const { backdrop } = createDialogShell(opts.title, opts.body, [ok]);
7435
+ backdrop.addEventListener("pointerdown", (event) => {
7436
+ if (event.target === backdrop)
7437
+ done();
7438
+ });
7439
+ document.addEventListener("keydown", onKeydown);
7440
+ ok.focus();
7441
+ });
7442
+ }
7443
+ function showPromptDialog(opts) {
7444
+ return new Promise((resolve) => {
7445
+ const previousFocus = opts.focusReturnTarget ?? document.activeElement;
7446
+ const cancel = document.createElement("button");
7447
+ cancel.type = "button";
7448
+ cancel.className = "gdp-btn gdp-btn-sm";
7449
+ cancel.textContent = opts.cancelLabel ?? "Cancel";
7450
+ const submit = document.createElement("button");
7451
+ submit.type = "button";
7452
+ submit.className = "gdp-btn gdp-btn-sm";
7453
+ submit.textContent = opts.confirmLabel ?? "OK";
7454
+ const input = document.createElement("input");
7455
+ input.className = "gdp-dialog-input";
7456
+ input.type = "text";
7457
+ input.autocomplete = "off";
7458
+ if (opts.placeholder)
7459
+ input.placeholder = opts.placeholder;
7460
+ if (opts.ariaLabel)
7461
+ input.setAttribute("aria-label", opts.ariaLabel);
7462
+ input.value = opts.defaultValue ?? "";
7463
+ const error2 = document.createElement("div");
7464
+ error2.className = "gdp-dialog-error";
7465
+ error2.setAttribute("role", "alert");
7466
+ const computeValid = () => {
7467
+ if (opts.validate)
7468
+ return opts.validate(input.value);
7469
+ return input.value;
7470
+ };
7471
+ const syncValidity = () => {
7472
+ const normalized = computeValid();
7473
+ const valid = normalized !== null && normalized !== "";
7474
+ submit.disabled = !valid;
7475
+ error2.textContent = !valid && input.value && opts.invalidMessage ? opts.invalidMessage : "";
7476
+ return valid;
7477
+ };
7478
+ const done = (value) => {
7479
+ document.removeEventListener("keydown", onKeydown);
7480
+ closeOpenDialog();
7481
+ previousFocus?.focus?.();
7482
+ resolve(value);
7483
+ };
7484
+ const trySubmit = () => {
7485
+ const normalized = computeValid();
7486
+ if (normalized === null || normalized === "") {
7487
+ syncValidity();
7488
+ input.focus();
7489
+ return;
7490
+ }
7491
+ done(normalized);
7492
+ };
7493
+ const onKeydown = (event) => {
7494
+ if (isImeComposing(event))
7495
+ return;
7496
+ if (event.key === "Escape") {
7497
+ event.preventDefault();
7498
+ event.stopPropagation();
7499
+ done(null);
7500
+ return;
7501
+ }
7502
+ if (event.key === "Enter") {
7503
+ event.preventDefault();
7504
+ trySubmit();
7505
+ return;
7506
+ }
7507
+ trapTabKey(event, [input, cancel, submit]);
7508
+ };
7509
+ cancel.addEventListener("click", () => done(null));
7510
+ submit.addEventListener("click", trySubmit);
7511
+ input.addEventListener("input", syncValidity);
7512
+ const { body } = createDialogShell(opts.title, opts.body, [cancel, submit]);
7513
+ body.append(input, error2);
7514
+ body.parentElement?.parentElement?.addEventListener("pointerdown", (event) => {
7515
+ const target = event.target;
7516
+ if (target?.classList.contains(BACKDROP_CLASS))
7517
+ done(null);
7518
+ });
7519
+ document.addEventListener("keydown", onKeydown);
7520
+ syncValidity();
7521
+ input.focus();
7522
+ if (input.value)
7523
+ input.select();
7524
+ });
7525
+ }
7526
+
7284
7527
  // web-src/views/annotations-ui.ts
7285
7528
  var ANNOTATION_SESSION_PARAM = "annotationSession";
7286
7529
  function createAnnotationsUi(deps) {
@@ -7830,9 +8073,18 @@ ${frontmatter.yaml}
7830
8073
  setActiveSession(session.id === activeSessionId ? null : session.id);
7831
8074
  });
7832
8075
  const rename = annotationIconButton("octicon-pencil", PENCIL_16_PATH, `rename session ${session.title}`);
7833
- rename.addEventListener("click", () => {
7834
- const next = window.prompt("Rename session", session.title);
7835
- if (next === null || !next.trim())
8076
+ rename.addEventListener("click", async () => {
8077
+ const next = await showPromptDialog({
8078
+ title: "Rename session",
8079
+ defaultValue: session.title,
8080
+ ariaLabel: "Session name",
8081
+ confirmLabel: "Rename",
8082
+ validate: (v) => {
8083
+ const trimmed = v.trim();
8084
+ return trimmed ? trimmed : null;
8085
+ }
8086
+ });
8087
+ if (next === null)
7836
8088
  return;
7837
8089
  postAnnotationAction({
7838
8090
  action: "rename",
@@ -7841,8 +8093,14 @@ ${frontmatter.yaml}
7841
8093
  });
7842
8094
  });
7843
8095
  const del = annotationIconButton("octicon-trash", TRASH_16_PATH, `delete session ${session.title}`);
7844
- del.addEventListener("click", () => {
7845
- if (!window.confirm(`Delete annotation session "${session.title}"?`))
8096
+ del.addEventListener("click", async () => {
8097
+ const ok = await showConfirmDialog({
8098
+ title: "Delete session?",
8099
+ body: `Delete annotation session "${session.title}"?`,
8100
+ confirmLabel: "Delete",
8101
+ danger: true
8102
+ });
8103
+ if (!ok)
7846
8104
  return;
7847
8105
  postAnnotationAction({ action: "delete", id: session.id });
7848
8106
  });
@@ -7868,8 +8126,14 @@ ${frontmatter.yaml}
7868
8126
  openAnnotationEntry(entry.id);
7869
8127
  });
7870
8128
  const remove = annotationIconButton("octicon-trash", TRASH_16_PATH, `delete annotation for ${annotationLocationLabel(entry)}`);
7871
- remove.addEventListener("click", () => {
7872
- if (!window.confirm(`Delete annotation for ${annotationLocationLabel(entry)}?`))
8129
+ remove.addEventListener("click", async () => {
8130
+ const ok = await showConfirmDialog({
8131
+ title: "Delete annotation?",
8132
+ body: `Delete annotation for ${annotationLocationLabel(entry)}?`,
8133
+ confirmLabel: "Delete",
8134
+ danger: true
8135
+ });
8136
+ if (!ok)
7873
8137
  return;
7874
8138
  postAnnotationAction({ action: "delete", id: entry.id });
7875
8139
  });
@@ -8131,8 +8395,14 @@ ${frontmatter.yaml}
8131
8395
  annotationFollow = followCheckbox.checked;
8132
8396
  deps.setAnnotationFollow(annotationFollow);
8133
8397
  });
8134
- $("#annotation-clear").addEventListener("click", () => {
8135
- if (!window.confirm("Delete all annotations?"))
8398
+ $("#annotation-clear").addEventListener("click", async () => {
8399
+ const ok = await showConfirmDialog({
8400
+ title: "Delete all annotations?",
8401
+ body: "Delete all annotations?",
8402
+ confirmLabel: "Delete all",
8403
+ danger: true
8404
+ });
8405
+ if (!ok)
8136
8406
  return;
8137
8407
  hideAnnotationDetail();
8138
8408
  postAnnotationAction({ action: "clear" });
@@ -9171,13 +9441,17 @@ ${frontmatter.yaml}
9171
9441
  searchTitle: "Search across tables",
9172
9442
  snapshot: "Snapshot",
9173
9443
  snapshotTitle: "Snapshot & Diff",
9174
- queryHistory: "Query History",
9175
- queryHistoryTitle: "Toggle query history panel",
9444
+ queryHistory: "Close",
9445
+ queryHistoryTitle: "Close panel",
9176
9446
  newTab: "New tab",
9177
9447
  closeTab: (label) => `Close ${label}`,
9178
9448
  loadingSchema: "Loading schema…",
9179
9449
  noDatastores: "No datastores found",
9180
- dockerLimitReached: "Docker discovery reached the service limit; some compose services may be hidden."
9450
+ dockerLimitReached: "Docker discovery reached the service limit; some compose services may be hidden.",
9451
+ inferFkLabel: "Rails FK inference",
9452
+ inferFkTitle: "Infer FK from Rails-style <name>_id → <names>.id",
9453
+ inferredBadge: "inferred",
9454
+ inferredBadgeTitle: "Inferred from Rails-style naming, not declared in DB"
9181
9455
  },
9182
9456
  grid: {
9183
9457
  searchPlaceholder: "Search all columns…",
@@ -9189,6 +9463,22 @@ ${frontmatter.yaml}
9189
9463
  statusSort: (column, dir) => `Sort: ${column} ${dir}`,
9190
9464
  statusFilters: (n2) => `${n2} filter(s)`
9191
9465
  },
9466
+ edit: {
9467
+ editMode: "Edit",
9468
+ editModeTitle: "Toggle edit mode",
9469
+ newRow: "New row",
9470
+ commit: "Commit",
9471
+ discard: "Discard",
9472
+ deleteRow: "Mark row for deletion",
9473
+ undoDelete: "Undo deletion",
9474
+ setNull: "Set NULL",
9475
+ dblclickToEdit: "Double-click to edit",
9476
+ pending: (n2) => `${n2} change(s)`,
9477
+ committing: "Saving…",
9478
+ commitError: (message) => `Save failed: ${message}`,
9479
+ confirmDiscard: "Discard all pending changes?",
9480
+ noPrimaryKey: "This table has no primary key. Existing rows cannot be edited or deleted (you can still add new rows)."
9481
+ },
9192
9482
  schema: {
9193
9483
  columns: "Columns",
9194
9484
  foreignKeys: "Foreign Keys",
@@ -9246,6 +9536,24 @@ ${frontmatter.yaml}
9246
9536
  confirmClear: "Confirm clear",
9247
9537
  truncatedRows: (saved, total) => `Showing ${saved} of ${total} rows`
9248
9538
  },
9539
+ sessionLog: {
9540
+ tabLabel: "Log",
9541
+ historyTabLabel: "Query history",
9542
+ clear: "Clear",
9543
+ clearTitle: "Clear the session log",
9544
+ empty: "No entries yet. SQL executions and commits will appear here.",
9545
+ selectPlaceholder: "Select an entry to view details.",
9546
+ statusOk: "ok",
9547
+ statusError: "error",
9548
+ kindQuery: "query",
9549
+ kindMutate: "commit",
9550
+ useInEditor: "Open in editor",
9551
+ copy: "Copy",
9552
+ copied: "Copied",
9553
+ autoFollow: "Auto-follow",
9554
+ autoFollowTitle: "Automatically scroll to the newest entry. Scroll up to pause.",
9555
+ commitLabel: (changes) => `Commit (${changes} ${changes === 1 ? "change" : "changes"})`
9556
+ },
9249
9557
  er: {
9250
9558
  zoomIn: "Zoom in",
9251
9559
  zoomOut: "Zoom out",
@@ -9310,7 +9618,16 @@ ${frontmatter.yaml}
9310
9618
  label: (date, tables, note) => `${date} (${tables} tables)${note}`
9311
9619
  },
9312
9620
  explorer: {
9313
- common: { loadMore: "Load more", search: "Search" },
9621
+ common: {
9622
+ loadMore: "Load more",
9623
+ search: "Search",
9624
+ edit: "Edit",
9625
+ save: "Save",
9626
+ cancel: "Cancel",
9627
+ delete: "Delete",
9628
+ saving: "Saving…",
9629
+ saveError: (message) => `Save failed: ${message}`
9630
+ },
9314
9631
  redis: {
9315
9632
  databases: "Databases",
9316
9633
  keys: "Keys",
@@ -9322,7 +9639,12 @@ ${frontmatter.yaml}
9322
9639
  emptyHash: "(empty hash)",
9323
9640
  emptyList: "(empty list)",
9324
9641
  noKeys: "(no keys)",
9325
- keyCount: (n2) => `${n2} keys`
9642
+ keyCount: (n2) => `${n2} keys`,
9643
+ newKey: "New key",
9644
+ newKeyNamePlaceholder: "key name",
9645
+ newKeyValuePlaceholder: "value",
9646
+ confirmDeleteKey: (key) => `Delete key "${key}"?`,
9647
+ create: "Create"
9326
9648
  },
9327
9649
  es: {
9328
9650
  indices: "Indices",
@@ -9338,7 +9660,15 @@ ${frontmatter.yaml}
9338
9660
  noMappedFields: "(no mapped fields)",
9339
9661
  noDocs: "(no docs)",
9340
9662
  docNotFound: "(doc not found)",
9341
- loadingIndices: "Loading indices..."
9663
+ loadingIndices: "Loading indices...",
9664
+ newDoc: "New document",
9665
+ newDocIdPlaceholder: "document id (optional — auto-generated if blank)",
9666
+ sourceJsonPlaceholder: `{
9667
+ "field": "value"
9668
+ }`,
9669
+ confirmDeleteDoc: (id) => `Delete document "${id}"?`,
9670
+ invalidJson: "Invalid JSON",
9671
+ create: "Create"
9342
9672
  },
9343
9673
  s3: {
9344
9674
  bucket: "Bucket",
@@ -9358,7 +9688,13 @@ ${frontmatter.yaml}
9358
9688
  copied: "Copied",
9359
9689
  copyFailed: "Copy failed",
9360
9690
  unsupported: "This object type cannot be previewed safely in the browser.",
9361
- truncatedNotice: (size) => `Showing first ${size}.`
9691
+ truncatedNotice: (size) => `Showing first ${size}.`,
9692
+ newObject: "New object",
9693
+ newObjectKeyPlaceholder: "object key, e.g. folder/file.txt",
9694
+ contentPlaceholder: "object content (text)",
9695
+ confirmDeleteObject: (key) => `Delete object "${key}"?`,
9696
+ editTextHint: "Only text objects can be edited in the browser.",
9697
+ create: "Create"
9362
9698
  }
9363
9699
  }
9364
9700
  };
@@ -9377,13 +9713,17 @@ ${frontmatter.yaml}
9377
9713
  searchTitle: "テーブル横断検索",
9378
9714
  snapshot: "スナップショット",
9379
9715
  snapshotTitle: "スナップショットと差分",
9380
- queryHistory: "クエリ履歴",
9381
- queryHistoryTitle: "クエリ履歴パネルの表示切替",
9716
+ queryHistory: "閉じる",
9717
+ queryHistoryTitle: "パネルを閉じる",
9382
9718
  newTab: "新しいタブ",
9383
9719
  closeTab: (label) => `${label} を閉じる`,
9384
9720
  loadingSchema: "スキーマを読み込み中…",
9385
9721
  noDatastores: "データストアが見つかりません",
9386
- dockerLimitReached: "Docker のサービス数が上限に達しました。一部の compose サービスは表示されていない可能性があります。"
9722
+ dockerLimitReached: "Docker のサービス数が上限に達しました。一部の compose サービスは表示されていない可能性があります。",
9723
+ inferFkLabel: "Rails FK 推測",
9724
+ inferFkTitle: "Rails 命名規約 (<name>_id → <names>.id) から FK を推測",
9725
+ inferredBadge: "推測",
9726
+ inferredBadgeTitle: "Rails 命名規約から推測した FK (DB の宣言ではありません)"
9387
9727
  },
9388
9728
  grid: {
9389
9729
  searchPlaceholder: "全カラムを検索…",
@@ -9395,6 +9735,22 @@ ${frontmatter.yaml}
9395
9735
  statusSort: (column, dir) => `並び替え: ${column} ${dir}`,
9396
9736
  statusFilters: (n2) => `フィルタ ${n2} 件`
9397
9737
  },
9738
+ edit: {
9739
+ editMode: "編集",
9740
+ editModeTitle: "編集モードの切り替え",
9741
+ newRow: "新規行",
9742
+ commit: "コミット",
9743
+ discard: "破棄",
9744
+ deleteRow: "行を削除対象にする",
9745
+ undoDelete: "削除を取り消す",
9746
+ setNull: "NULL にする",
9747
+ dblclickToEdit: "ダブルクリックで編集",
9748
+ pending: (n2) => `変更 ${n2} 件`,
9749
+ committing: "保存中…",
9750
+ commitError: (message) => `保存に失敗しました: ${message}`,
9751
+ confirmDiscard: "保留中の変更をすべて破棄しますか?",
9752
+ noPrimaryKey: "このテーブルには主キーがありません。既存行の編集・削除はできません(新規行の追加は可能です)。"
9753
+ },
9398
9754
  schema: {
9399
9755
  columns: "カラム",
9400
9756
  foreignKeys: "外部キー",
@@ -9452,6 +9808,24 @@ ${frontmatter.yaml}
9452
9808
  confirmClear: "全削除を確認",
9453
9809
  truncatedRows: (saved, total) => `全 ${total} 行中 ${saved} 行を表示`
9454
9810
  },
9811
+ sessionLog: {
9812
+ tabLabel: "ログ",
9813
+ historyTabLabel: "クエリ履歴",
9814
+ clear: "クリア",
9815
+ clearTitle: "セッションログを消去",
9816
+ empty: "まだログはありません。SQL の実行や編集コミットがここに出ます。",
9817
+ selectPlaceholder: "エントリを選ぶと詳細を表示します。",
9818
+ statusOk: "成功",
9819
+ statusError: "エラー",
9820
+ kindQuery: "クエリ",
9821
+ kindMutate: "コミット",
9822
+ useInEditor: "エディタに開く",
9823
+ copy: "コピー",
9824
+ copied: "コピーしました",
9825
+ autoFollow: "自動追従",
9826
+ autoFollowTitle: "新しいエントリを常に表示。下に手動スクロールすると自動解除されます。",
9827
+ commitLabel: (changes) => `コミット (${changes} 件)`
9828
+ },
9455
9829
  er: {
9456
9830
  zoomIn: "拡大",
9457
9831
  zoomOut: "縮小",
@@ -9516,7 +9890,16 @@ ${frontmatter.yaml}
9516
9890
  label: (date, tables, note) => `${date} (${tables}テーブル)${note}`
9517
9891
  },
9518
9892
  explorer: {
9519
- common: { loadMore: "さらに読み込む", search: "検索" },
9893
+ common: {
9894
+ loadMore: "さらに読み込む",
9895
+ search: "検索",
9896
+ edit: "編集",
9897
+ save: "保存",
9898
+ cancel: "キャンセル",
9899
+ delete: "削除",
9900
+ saving: "保存中…",
9901
+ saveError: (message) => `保存に失敗しました: ${message}`
9902
+ },
9520
9903
  redis: {
9521
9904
  databases: "データベース",
9522
9905
  keys: "キー",
@@ -9528,7 +9911,12 @@ ${frontmatter.yaml}
9528
9911
  emptyHash: "(空のハッシュ)",
9529
9912
  emptyList: "(空のリスト)",
9530
9913
  noKeys: "(キーがありません)",
9531
- keyCount: (n2) => `${n2} キー`
9914
+ keyCount: (n2) => `${n2} キー`,
9915
+ newKey: "新規キー",
9916
+ newKeyNamePlaceholder: "キー名",
9917
+ newKeyValuePlaceholder: "値",
9918
+ confirmDeleteKey: (key) => `キー "${key}" を削除しますか?`,
9919
+ create: "作成"
9532
9920
  },
9533
9921
  es: {
9534
9922
  indices: "インデックス",
@@ -9544,7 +9932,15 @@ ${frontmatter.yaml}
9544
9932
  noMappedFields: "(マッピング済みフィールドがありません)",
9545
9933
  noDocs: "(ドキュメントがありません)",
9546
9934
  docNotFound: "(ドキュメントが見つかりません)",
9547
- loadingIndices: "インデックスを読み込み中..."
9935
+ loadingIndices: "インデックスを読み込み中...",
9936
+ newDoc: "新規ドキュメント",
9937
+ newDocIdPlaceholder: "ドキュメント ID (省略時は自動採番)",
9938
+ sourceJsonPlaceholder: `{
9939
+ "field": "value"
9940
+ }`,
9941
+ confirmDeleteDoc: (id) => `ドキュメント "${id}" を削除しますか?`,
9942
+ invalidJson: "JSON が不正です",
9943
+ create: "作成"
9548
9944
  },
9549
9945
  s3: {
9550
9946
  bucket: "バケット",
@@ -9564,7 +9960,13 @@ ${frontmatter.yaml}
9564
9960
  copied: "コピーしました",
9565
9961
  copyFailed: "コピーに失敗しました",
9566
9962
  unsupported: "この種類のオブジェクトはブラウザで安全にプレビューできません。",
9567
- truncatedNotice: (size) => `先頭 ${size} を表示中。`
9963
+ truncatedNotice: (size) => `先頭 ${size} を表示中。`,
9964
+ newObject: "新規オブジェクト",
9965
+ newObjectKeyPlaceholder: "オブジェクトキー 例: folder/file.txt",
9966
+ contentPlaceholder: "オブジェクトの内容 (テキスト)",
9967
+ confirmDeleteObject: (key) => `オブジェクト "${key}" を削除しますか?`,
9968
+ editTextHint: "ブラウザで編集できるのはテキストオブジェクトのみです。",
9969
+ create: "作成"
9568
9970
  }
9569
9971
  }
9570
9972
  };
@@ -9619,8 +10021,16 @@ ${frontmatter.yaml}
9619
10021
  const docListPane = document.createElement("div");
9620
10022
  docListPane.className = "es-doc-list-pane";
9621
10023
  const docListHeader = document.createElement("div");
9622
- docListHeader.className = "db-explorer-pane-header";
9623
- docListHeader.textContent = text2().es.docs;
10024
+ docListHeader.className = "db-explorer-pane-header es-doc-list-header";
10025
+ const docListTitle = document.createElement("span");
10026
+ docListTitle.textContent = text2().es.docs;
10027
+ const newDocBtn = document.createElement("button");
10028
+ newDocBtn.type = "button";
10029
+ newDocBtn.className = "db-btn db-btn-sm es-new-doc-btn";
10030
+ newDocBtn.textContent = `+ ${text2().es.newDoc}`;
10031
+ newDocBtn.title = text2().es.newDoc;
10032
+ newDocBtn.hidden = true;
10033
+ docListHeader.append(docListTitle, newDocBtn);
9624
10034
  docListPane.appendChild(docListHeader);
9625
10035
  const searchBar = document.createElement("div");
9626
10036
  searchBar.className = "es-search-bar";
@@ -9839,12 +10249,176 @@ ${frontmatter.yaml}
9839
10249
  table2.appendChild(tbody);
9840
10250
  mappingBody.appendChild(table2);
9841
10251
  }
10252
+ function mkBtn(label, cls) {
10253
+ const b2 = document.createElement("button");
10254
+ b2.type = "button";
10255
+ b2.className = cls;
10256
+ b2.textContent = label;
10257
+ return b2;
10258
+ }
10259
+ async function postEsWrite(body) {
10260
+ const doFetch = fetch("/_db/elasticsearch/write", {
10261
+ method: "POST",
10262
+ headers: {
10263
+ "Content-Type": "application/json",
10264
+ "X-Code-Viewer-Action": "1"
10265
+ },
10266
+ body: JSON.stringify(body)
10267
+ });
10268
+ const res = await (callbacks.trackLoad ? callbacks.trackLoad(doFetch) : doFetch);
10269
+ if (!res.ok)
10270
+ throw new Error(await res.text() || res.statusText);
10271
+ }
10272
+ function startDocEdit(resp) {
10273
+ docBody.innerHTML = "";
10274
+ const bar = document.createElement("div");
10275
+ bar.className = "es-doc-edit-bar";
10276
+ const save = mkBtn(text2().common.save, "db-btn db-btn-primary db-btn-sm");
10277
+ const cancel = mkBtn(text2().common.cancel, "db-btn db-btn-sm");
10278
+ const status = document.createElement("span");
10279
+ status.className = "es-doc-edit-status";
10280
+ bar.append(save, cancel, status);
10281
+ const ta = document.createElement("textarea");
10282
+ ta.className = "es-doc-edit-textarea";
10283
+ ta.value = JSON.stringify(resp.source, null, 2);
10284
+ docBody.append(bar, ta);
10285
+ cancel.addEventListener("click", () => renderDoc(resp));
10286
+ save.addEventListener("click", async () => {
10287
+ let parsed;
10288
+ try {
10289
+ parsed = JSON.parse(ta.value);
10290
+ } catch {
10291
+ status.textContent = text2().es.invalidJson;
10292
+ return;
10293
+ }
10294
+ if (!currentDbId || !currentIndex)
10295
+ return;
10296
+ save.disabled = true;
10297
+ cancel.disabled = true;
10298
+ status.textContent = text2().common.saving;
10299
+ try {
10300
+ await postEsWrite({
10301
+ db: currentDbId,
10302
+ index: currentIndex,
10303
+ id: resp.id,
10304
+ source: parsed,
10305
+ seqNo: resp.seqNo,
10306
+ primaryTerm: resp.primaryTerm
10307
+ });
10308
+ await selectDoc(resp.id);
10309
+ } catch (err) {
10310
+ save.disabled = false;
10311
+ cancel.disabled = false;
10312
+ status.textContent = text2().common.saveError(err instanceof Error ? err.message : String(err));
10313
+ }
10314
+ });
10315
+ ta.focus();
10316
+ }
10317
+ async function deleteDoc(resp) {
10318
+ if (!currentDbId || !currentIndex)
10319
+ return;
10320
+ const ok = await showConfirmDialog({
10321
+ body: text2().es.confirmDeleteDoc(resp.id),
10322
+ confirmLabel: text2().common.delete,
10323
+ danger: true
10324
+ });
10325
+ if (!ok)
10326
+ return;
10327
+ try {
10328
+ await postEsWrite({
10329
+ db: currentDbId,
10330
+ index: currentIndex,
10331
+ id: resp.id,
10332
+ op: "delete"
10333
+ });
10334
+ setPaneEmpty(docBody, text2().es.selectDoc);
10335
+ lastDoc = null;
10336
+ loadDocs(false);
10337
+ } catch (err) {
10338
+ setPaneStatus(docBody, text2().common.saveError(err instanceof Error ? err.message : String(err)), { error: true });
10339
+ }
10340
+ }
10341
+ function showNewDocForm() {
10342
+ if (!currentIndex) {
10343
+ setPaneStatus(docBody, text2().es.selectIndex);
10344
+ return;
10345
+ }
10346
+ setDetailTab("doc");
10347
+ docBody.innerHTML = "";
10348
+ const form = document.createElement("form");
10349
+ form.className = "es-new-doc-form";
10350
+ const idInput = document.createElement("input");
10351
+ idInput.type = "text";
10352
+ idInput.className = "es-new-doc-id";
10353
+ idInput.placeholder = text2().es.newDocIdPlaceholder;
10354
+ idInput.autocomplete = "off";
10355
+ const ta = document.createElement("textarea");
10356
+ ta.className = "es-doc-edit-textarea";
10357
+ ta.placeholder = text2().es.sourceJsonPlaceholder;
10358
+ const bar = document.createElement("div");
10359
+ bar.className = "es-doc-edit-bar";
10360
+ const create = mkBtn(text2().es.create, "db-btn db-btn-primary db-btn-sm");
10361
+ create.type = "submit";
10362
+ const cancel = mkBtn(text2().common.cancel, "db-btn db-btn-sm");
10363
+ const status = document.createElement("span");
10364
+ status.className = "es-doc-edit-status";
10365
+ bar.append(create, cancel, status);
10366
+ form.append(bar, idInput, ta);
10367
+ docBody.append(form);
10368
+ idInput.focus();
10369
+ cancel.addEventListener("click", () => setPaneEmpty(docBody, text2().es.selectDoc));
10370
+ form.addEventListener("submit", async (e2) => {
10371
+ e2.preventDefault();
10372
+ if (!currentDbId || !currentIndex)
10373
+ return;
10374
+ let parsed;
10375
+ try {
10376
+ parsed = JSON.parse(ta.value);
10377
+ } catch {
10378
+ status.textContent = text2().es.invalidJson;
10379
+ return;
10380
+ }
10381
+ const id = idInput.value.trim();
10382
+ create.disabled = true;
10383
+ status.textContent = text2().common.saving;
10384
+ try {
10385
+ await postEsWrite({
10386
+ db: currentDbId,
10387
+ index: currentIndex,
10388
+ ...id ? { id } : {},
10389
+ source: parsed,
10390
+ op: "create"
10391
+ });
10392
+ loadDocs(false);
10393
+ if (id)
10394
+ await selectDoc(id);
10395
+ else
10396
+ setPaneEmpty(docBody, text2().es.selectDoc);
10397
+ } catch (err) {
10398
+ create.disabled = false;
10399
+ status.textContent = text2().common.saveError(err instanceof Error ? err.message : String(err));
10400
+ }
10401
+ });
10402
+ }
10403
+ newDocBtn.addEventListener("click", () => showNewDocForm());
9842
10404
  function renderDoc(resp) {
9843
10405
  lastDoc = resp;
9844
10406
  docBody.innerHTML = "";
9845
10407
  const header = document.createElement("div");
9846
10408
  header.className = "es-doc-detail-header";
9847
- header.textContent = `${resp.index} / ${resp.id}`;
10409
+ const title = document.createElement("span");
10410
+ title.textContent = `${resp.index} / ${resp.id}`;
10411
+ header.appendChild(title);
10412
+ if (resp.found) {
10413
+ const actions = document.createElement("div");
10414
+ actions.className = "es-doc-actions";
10415
+ const editBtn = mkBtn(text2().common.edit, "db-btn db-btn-sm");
10416
+ editBtn.addEventListener("click", () => startDocEdit(resp));
10417
+ const delBtn = mkBtn(text2().common.delete, "db-btn db-btn-sm");
10418
+ delBtn.addEventListener("click", () => void deleteDoc(resp));
10419
+ actions.append(editBtn, delBtn);
10420
+ header.appendChild(actions);
10421
+ }
9848
10422
  docBody.appendChild(header);
9849
10423
  if (!resp.found) {
9850
10424
  setPaneStatus(docBody, text2().es.docNotFound, {
@@ -9975,6 +10549,7 @@ ${frontmatter.yaml}
9975
10549
  docBody.innerHTML = "";
9976
10550
  setPaneEmpty(docBody, text2().es.selectDoc);
9977
10551
  setDetailTab("mapping");
10552
+ newDocBtn.hidden = false;
9978
10553
  await Promise.all([fetchMapping(name), loadDocs(false)]);
9979
10554
  }
9980
10555
  async function selectDoc(id) {
@@ -10171,7 +10746,9 @@ ${frontmatter.yaml}
10171
10746
  function localize() {
10172
10747
  const t2 = text2();
10173
10748
  indexListHeader.textContent = t2.es.indices;
10174
- docListHeader.textContent = t2.es.docs;
10749
+ docListTitle.textContent = t2.es.docs;
10750
+ newDocBtn.textContent = `+ ${t2.es.newDoc}`;
10751
+ newDocBtn.title = t2.es.newDoc;
10175
10752
  searchInput.placeholder = t2.es.queryPlaceholder;
10176
10753
  searchBtn.textContent = t2.common.search;
10177
10754
  docMoreBtn.textContent = t2.common.loadMore;
@@ -10200,27 +10777,6 @@ ${frontmatter.yaml}
10200
10777
  }
10201
10778
 
10202
10779
  // web-src/views/database/er-diagram.ts
10203
- var mermaidPromise2 = null;
10204
- var mermaidInitialized2 = false;
10205
- async function loadMermaid2() {
10206
- if (!mermaidPromise2) {
10207
- mermaidPromise2 = import("/mermaid.js").then((mod) => {
10208
- const typed = mod;
10209
- const mermaid = typed.default;
10210
- if (!mermaidInitialized2) {
10211
- mermaid.initialize({
10212
- startOnLoad: false,
10213
- securityLevel: "strict",
10214
- theme: "default",
10215
- er: { useMaxWidth: false }
10216
- });
10217
- mermaidInitialized2 = true;
10218
- }
10219
- return mermaid;
10220
- }).catch(() => null);
10221
- }
10222
- return mermaidPromise2;
10223
- }
10224
10780
  function mermaidType(sqlType) {
10225
10781
  const upper = sqlType.toUpperCase();
10226
10782
  if (upper.includes("INT"))
@@ -10391,7 +10947,7 @@ ${frontmatter.yaml}
10391
10947
  }
10392
10948
  const markup = buildErMarkup(schema, columnsMap);
10393
10949
  lastMarkup = markup;
10394
- const mermaid = await loadMermaid2();
10950
+ const mermaid = await loadMermaid();
10395
10951
  if (!mermaid) {
10396
10952
  svgWrap.textContent = text2().er.loadError;
10397
10953
  return;
@@ -10661,6 +11217,12 @@ ${frontmatter.yaml}
10661
11217
  }
10662
11218
 
10663
11219
  // web-src/views/database/pref-toggle.ts
11220
+ function localizePrefToggle(btn, label, title) {
11221
+ btn.title = title;
11222
+ const labelEl = btn.querySelector(".db-pref-toggle-label");
11223
+ if (labelEl)
11224
+ labelEl.textContent = label;
11225
+ }
10664
11226
  function makePrefToggle(opts) {
10665
11227
  const btn = document.createElement("button");
10666
11228
  btn.type = "button";
@@ -10683,20 +11245,22 @@ ${frontmatter.yaml}
10683
11245
  return btn;
10684
11246
  }
10685
11247
 
10686
- // web-src/views/database/query-editor.ts
10687
- var shikiPromise2 = null;
10688
- function loadShikiSql() {
10689
- if (!shikiPromise2) {
10690
- shikiPromise2 = import("/shiki.js").then((mod) => {
10691
- const typed = mod;
10692
- return typed.createHighlighter({
10693
- themes: ["github-light", "github-dark"],
10694
- langs: ["sql"]
10695
- });
10696
- }).catch(() => null);
10697
- }
10698
- return shikiPromise2;
11248
+ // web-src/views/database/shiki-sql.ts
11249
+ function highlightSqlToInnerHtml(code2, highlighter) {
11250
+ if (!highlighter || !code2)
11251
+ return "";
11252
+ const html = highlighter.codeToHtml(code2, {
11253
+ lang: "sql",
11254
+ themes: { light: "github-light", dark: "github-dark" },
11255
+ defaultColor: false
11256
+ });
11257
+ const template = document.createElement("template");
11258
+ template.innerHTML = html;
11259
+ const pre = template.content.querySelector("pre");
11260
+ return pre ? pre.innerHTML : "";
10699
11261
  }
11262
+
11263
+ // web-src/views/database/query-editor.ts
10700
11264
  var MAX_HISTORY = 50;
10701
11265
  function createQueryEditor(callbacks) {
10702
11266
  const text2 = () => callbacks.getText?.() ?? dbText("en");
@@ -10716,7 +11280,10 @@ ${frontmatter.yaml}
10716
11280
  textarea.rows = 3;
10717
11281
  editorWrap.append(highlight, textarea);
10718
11282
  let shiki = null;
10719
- loadShikiSql().then((h) => {
11283
+ loadShikiHighlighter({
11284
+ themes: ["github-light", "github-dark"],
11285
+ langs: ["sql"]
11286
+ }).then((h) => {
10720
11287
  shiki = h;
10721
11288
  syncHighlight();
10722
11289
  });
@@ -10733,21 +11300,9 @@ ${frontmatter.yaml}
10733
11300
  syncEditorHeight();
10734
11301
  return;
10735
11302
  }
10736
- if (!shiki) {
10737
- highlight.textContent = code2;
10738
- syncEditorHeight();
10739
- return;
10740
- }
10741
- const html = shiki.codeToHtml(code2, {
10742
- lang: "sql",
10743
- themes: { light: "github-light", dark: "github-dark" },
10744
- defaultColor: false
10745
- });
10746
- const template = document.createElement("template");
10747
- template.innerHTML = html;
10748
- const pre = template.content.querySelector("pre");
10749
- if (pre) {
10750
- highlight.innerHTML = pre.innerHTML;
11303
+ const inner = highlightSqlToInnerHtml(code2, shiki);
11304
+ if (inner) {
11305
+ highlight.innerHTML = inner;
10751
11306
  } else {
10752
11307
  highlight.textContent = code2;
10753
11308
  }
@@ -11462,8 +12017,15 @@ ${frontmatter.yaml}
11462
12017
  const keyListPane = document.createElement("div");
11463
12018
  keyListPane.className = "redis-key-list-pane";
11464
12019
  const keyListHeader = document.createElement("div");
11465
- keyListHeader.className = "db-explorer-pane-header";
11466
- keyListHeader.textContent = text2().redis.keys;
12020
+ keyListHeader.className = "db-explorer-pane-header redis-key-list-header";
12021
+ const keyListTitle = document.createElement("span");
12022
+ keyListTitle.textContent = text2().redis.keys;
12023
+ const newKeyBtn = document.createElement("button");
12024
+ newKeyBtn.type = "button";
12025
+ newKeyBtn.className = "db-btn db-btn-sm redis-new-key-btn";
12026
+ newKeyBtn.textContent = `+ ${text2().redis.newKey}`;
12027
+ newKeyBtn.title = text2().redis.newKey;
12028
+ keyListHeader.append(keyListTitle, newKeyBtn);
11467
12029
  keyListPane.appendChild(keyListHeader);
11468
12030
  const keyFilterForm = document.createElement("form");
11469
12031
  keyFilterForm.className = "redis-key-filter-form";
@@ -11586,6 +12148,152 @@ ${frontmatter.yaml}
11586
12148
  div.textContent = message;
11587
12149
  return div;
11588
12150
  }
12151
+ function mkBtn(label, cls) {
12152
+ const b2 = document.createElement("button");
12153
+ b2.type = "button";
12154
+ b2.className = cls;
12155
+ b2.textContent = label;
12156
+ return b2;
12157
+ }
12158
+ function writeBase() {
12159
+ if (currentDbId === null || currentDbIndex === null)
12160
+ return null;
12161
+ return { db: currentDbId, dbIndex: currentDbIndex };
12162
+ }
12163
+ async function postRedisWrite(body) {
12164
+ const doFetch = fetch("/_db/redis/write", {
12165
+ method: "POST",
12166
+ headers: {
12167
+ "Content-Type": "application/json",
12168
+ "X-Code-Viewer-Action": "1"
12169
+ },
12170
+ body: JSON.stringify(body)
12171
+ });
12172
+ const res = await (callbacks.trackLoad ? callbacks.trackLoad(doFetch) : doFetch);
12173
+ if (!res.ok)
12174
+ throw new Error(await res.text() || res.statusText);
12175
+ }
12176
+ async function deleteCurrentKey(key) {
12177
+ const base2 = writeBase();
12178
+ if (!base2)
12179
+ return;
12180
+ const ok = await showConfirmDialog({
12181
+ body: text2().redis.confirmDeleteKey(key),
12182
+ confirmLabel: text2().common.delete,
12183
+ danger: true
12184
+ });
12185
+ if (!ok)
12186
+ return;
12187
+ try {
12188
+ await postRedisWrite({ ...base2, key, op: "delete" });
12189
+ currentKey = null;
12190
+ setPaneEmpty(mainPane, text2().redis.selectKey);
12191
+ await loadKeys(false);
12192
+ } catch (err) {
12193
+ setPaneStatus(mainPane, text2().common.saveError(err instanceof Error ? err.message : String(err)), { error: true });
12194
+ }
12195
+ }
12196
+ function startStringEdit(key, current) {
12197
+ const body = mainPane.querySelector(".redis-value-body");
12198
+ if (!body)
12199
+ return;
12200
+ body.innerHTML = "";
12201
+ const bar = document.createElement("div");
12202
+ bar.className = "redis-value-edit-bar";
12203
+ const save = mkBtn(text2().common.save, "db-btn db-btn-primary db-btn-sm");
12204
+ const cancel = mkBtn(text2().common.cancel, "db-btn db-btn-sm");
12205
+ const status = document.createElement("span");
12206
+ status.className = "redis-value-edit-status";
12207
+ bar.append(save, cancel, status);
12208
+ const ta = document.createElement("textarea");
12209
+ ta.className = "redis-value-edit-textarea";
12210
+ ta.value = current;
12211
+ body.append(bar, ta);
12212
+ cancel.addEventListener("click", () => void selectKey(key));
12213
+ save.addEventListener("click", async () => {
12214
+ const base2 = writeBase();
12215
+ if (!base2)
12216
+ return;
12217
+ save.disabled = true;
12218
+ cancel.disabled = true;
12219
+ status.textContent = text2().common.saving;
12220
+ try {
12221
+ await postRedisWrite({
12222
+ ...base2,
12223
+ key,
12224
+ op: "setString",
12225
+ value: ta.value
12226
+ });
12227
+ await selectKey(key);
12228
+ } catch (err) {
12229
+ save.disabled = false;
12230
+ cancel.disabled = false;
12231
+ status.textContent = text2().common.saveError(err instanceof Error ? err.message : String(err));
12232
+ }
12233
+ });
12234
+ ta.focus();
12235
+ }
12236
+ function showNewKeyForm() {
12237
+ if (currentDbIndex === null) {
12238
+ setPaneStatus(mainPane, text2().redis.selectDatabase);
12239
+ return;
12240
+ }
12241
+ mainPane.innerHTML = "";
12242
+ const form = document.createElement("form");
12243
+ form.className = "redis-new-key-form";
12244
+ const nameInput = document.createElement("input");
12245
+ nameInput.type = "text";
12246
+ nameInput.className = "redis-new-key-name";
12247
+ nameInput.placeholder = text2().redis.newKeyNamePlaceholder;
12248
+ nameInput.autocomplete = "off";
12249
+ const valueInput = document.createElement("textarea");
12250
+ valueInput.className = "redis-new-key-value";
12251
+ valueInput.placeholder = text2().redis.newKeyValuePlaceholder;
12252
+ const bar = document.createElement("div");
12253
+ bar.className = "redis-value-edit-bar";
12254
+ const create = mkBtn(text2().redis.create, "db-btn db-btn-primary db-btn-sm");
12255
+ create.type = "submit";
12256
+ const cancel = mkBtn(text2().common.cancel, "db-btn db-btn-sm");
12257
+ const status = document.createElement("span");
12258
+ status.className = "redis-value-edit-status";
12259
+ bar.append(create, cancel, status);
12260
+ form.append(bar, nameInput, valueInput);
12261
+ mainPane.append(form);
12262
+ nameInput.focus();
12263
+ cancel.addEventListener("click", () => {
12264
+ if (currentKey)
12265
+ selectKey(currentKey);
12266
+ else
12267
+ setPaneEmpty(mainPane, text2().redis.selectKey);
12268
+ });
12269
+ form.addEventListener("submit", async (e2) => {
12270
+ e2.preventDefault();
12271
+ const base2 = writeBase();
12272
+ if (!base2)
12273
+ return;
12274
+ const name = nameInput.value.trim();
12275
+ if (!name) {
12276
+ nameInput.focus();
12277
+ return;
12278
+ }
12279
+ create.disabled = true;
12280
+ status.textContent = text2().common.saving;
12281
+ try {
12282
+ await postRedisWrite({
12283
+ ...base2,
12284
+ key: name,
12285
+ op: "createString",
12286
+ value: valueInput.value
12287
+ });
12288
+ await loadKeys(false);
12289
+ await selectKey(name);
12290
+ } catch (err) {
12291
+ create.disabled = false;
12292
+ status.textContent = text2().common.saveError(err instanceof Error ? err.message : String(err));
12293
+ }
12294
+ });
12295
+ }
12296
+ newKeyBtn.addEventListener("click", () => showNewKeyForm());
11589
12297
  function renderValue(key, value) {
11590
12298
  mainPane.innerHTML = "";
11591
12299
  const header = document.createElement("div");
@@ -11597,6 +12305,20 @@ ${frontmatter.yaml}
11597
12305
  keyEl.className = "redis-value-key-name";
11598
12306
  keyEl.textContent = key;
11599
12307
  header.append(typeBadge, keyEl);
12308
+ const actions = document.createElement("div");
12309
+ actions.className = "redis-value-actions";
12310
+ if (value.type === "string" && value.binaryBase64 === undefined && !value.truncated) {
12311
+ const editBtn = mkBtn(text2().common.edit, "db-btn db-btn-sm");
12312
+ const stringValue = value.value;
12313
+ editBtn.addEventListener("click", () => startStringEdit(key, stringValue));
12314
+ actions.appendChild(editBtn);
12315
+ }
12316
+ if (value.type !== "none") {
12317
+ const delBtn = mkBtn(text2().common.delete, "db-btn db-btn-sm");
12318
+ delBtn.addEventListener("click", () => void deleteCurrentKey(key));
12319
+ actions.appendChild(delBtn);
12320
+ }
12321
+ header.appendChild(actions);
11600
12322
  mainPane.appendChild(header);
11601
12323
  const body = document.createElement("div");
11602
12324
  body.className = "redis-value-body";
@@ -11935,7 +12657,9 @@ ${frontmatter.yaml}
11935
12657
  function localize() {
11936
12658
  const t2 = text2();
11937
12659
  dbListHeader.textContent = t2.redis.databases;
11938
- keyListHeader.textContent = t2.redis.keys;
12660
+ keyListTitle.textContent = t2.redis.keys;
12661
+ newKeyBtn.textContent = `+ ${t2.redis.newKey}`;
12662
+ newKeyBtn.title = t2.redis.newKey;
11939
12663
  keyFilterInput.placeholder = t2.redis.keyFilterPlaceholder;
11940
12664
  keyFilterBtn.textContent = t2.common.search;
11941
12665
  keyMoreBtn.textContent = t2.common.loadMore;
@@ -12085,13 +12809,6 @@ ${frontmatter.yaml}
12085
12809
  explorerViewBtn.textContent = "Explorer";
12086
12810
  viewSeg.append(listViewBtn, explorerViewBtn);
12087
12811
  bucketRow.appendChild(viewSeg);
12088
- const tooltipToggle = makePrefToggle({
12089
- title: "Toggle hover preview tooltip",
12090
- label: "Hover preview",
12091
- pathD: "M8 3.5C4.5 3.5 1.7 5.7 0 8c1.7 2.3 4.5 4.5 8 4.5s6.3-2.2 8-4.5C14.3 5.7 11.5 3.5 8 3.5Zm0 7.5a3 3 0 1 1 0-6 3 3 0 0 1 0 6Zm0-4.5a1.5 1.5 0 1 0 0 3 1.5 1.5 0 0 0 0-3Z",
12092
- extraClass: "s3-tooltip-toggle"
12093
- });
12094
- bucketRow.appendChild(tooltipToggle);
12095
12812
  sidebarSlot.appendChild(bucketRow);
12096
12813
  const searchRow = document.createElement("form");
12097
12814
  searchRow.className = "s3-search-row";
@@ -12126,7 +12843,13 @@ ${frontmatter.yaml}
12126
12843
  sortKey.value = "key-asc";
12127
12844
  sortKey.textContent = text2().sortKey;
12128
12845
  sortSelect.append(sortUpdated, sortKey);
12129
- optionRow.append(modeSeg, sortSelect);
12846
+ const newObjectBtn = document.createElement("button");
12847
+ newObjectBtn.type = "button";
12848
+ newObjectBtn.className = "db-btn db-btn-sm s3-new-object-btn";
12849
+ newObjectBtn.textContent = `+ ${text2().newObject}`;
12850
+ newObjectBtn.title = text2().newObject;
12851
+ newObjectBtn.hidden = true;
12852
+ optionRow.append(modeSeg, sortSelect, newObjectBtn);
12130
12853
  sidebarSlot.appendChild(optionRow);
12131
12854
  const objectStatus = document.createElement("div");
12132
12855
  objectStatus.className = "s3-object-status";
@@ -12179,19 +12902,9 @@ ${frontmatter.yaml}
12179
12902
  return;
12180
12903
  callbacks.onSelectionChange?.(getSelection());
12181
12904
  }
12182
- let tooltipEnabled = callbacks.getTooltipEnabled?.() ?? true;
12183
- function applyTooltipToggleState() {
12184
- tooltipToggle.classList.toggle("active", tooltipEnabled);
12185
- tooltipToggle.setAttribute("aria-pressed", String(tooltipEnabled));
12186
- }
12187
- applyTooltipToggleState();
12188
- tooltipToggle.addEventListener("click", () => {
12189
- tooltipEnabled = !tooltipEnabled;
12190
- callbacks.setTooltipEnabled?.(tooltipEnabled);
12191
- applyTooltipToggleState();
12192
- if (!tooltipEnabled)
12193
- hideKeyTooltip();
12194
- });
12905
+ function tooltipEnabled() {
12906
+ return callbacks.getTooltipEnabled?.() ?? true;
12907
+ }
12195
12908
  const keyTooltip = document.createElement("div");
12196
12909
  keyTooltip.className = "s3-key-tooltip";
12197
12910
  keyTooltip.hidden = true;
@@ -12208,7 +12921,7 @@ ${frontmatter.yaml}
12208
12921
  keyTooltip.style.top = `${top}px`;
12209
12922
  }
12210
12923
  function showKeyTooltip(row, object) {
12211
- if (!tooltipEnabled)
12924
+ if (!tooltipEnabled())
12212
12925
  return;
12213
12926
  keyTooltip.innerHTML = "";
12214
12927
  if (currentDbId && currentBucket) {
@@ -12440,6 +13153,171 @@ ${frontmatter.yaml}
12440
13153
  moreBtn.disabled = false;
12441
13154
  }
12442
13155
  }
13156
+ function mkBtn(label, cls) {
13157
+ const b2 = document.createElement("button");
13158
+ b2.type = "button";
13159
+ b2.className = cls;
13160
+ b2.textContent = label;
13161
+ return b2;
13162
+ }
13163
+ async function postS3Write(body) {
13164
+ const doFetch = fetch("/_db/s3/write", {
13165
+ method: "POST",
13166
+ headers: {
13167
+ "Content-Type": "application/json",
13168
+ "X-Code-Viewer-Action": "1"
13169
+ },
13170
+ body: JSON.stringify(body)
13171
+ });
13172
+ const res = await (callbacks.trackLoad ? callbacks.trackLoad(doFetch) : doFetch);
13173
+ if (!res.ok)
13174
+ throw new Error(await res.text() || res.statusText);
13175
+ }
13176
+ function refreshObjectList() {
13177
+ if (currentView === "explorer")
13178
+ loadExplorerRoot();
13179
+ else
13180
+ loadObjects(false);
13181
+ }
13182
+ async function startObjectEdit(object) {
13183
+ if (!currentDbId || !currentBucket)
13184
+ return;
13185
+ const bodyEl = previewPane.querySelector(".s3-preview-body");
13186
+ if (!bodyEl)
13187
+ return;
13188
+ setPaneStatus(bodyEl, tCommon().saving);
13189
+ let current = "";
13190
+ try {
13191
+ const params = new URLSearchParams({
13192
+ db: currentDbId,
13193
+ bucket: currentBucket,
13194
+ key: object.key
13195
+ });
13196
+ const res = await fetch(`/_db/s3/text?${params}`);
13197
+ if (!res.ok)
13198
+ throw new Error(await res.text() || res.statusText);
13199
+ current = (await res.json()).text;
13200
+ } catch (err) {
13201
+ setPaneStatus(bodyEl, tCommon().saveError(err instanceof Error ? err.message : String(err)), { error: true });
13202
+ return;
13203
+ }
13204
+ bodyEl.innerHTML = "";
13205
+ const bar = document.createElement("div");
13206
+ bar.className = "s3-edit-bar";
13207
+ const save = mkBtn(tCommon().save, "db-btn db-btn-primary db-btn-sm");
13208
+ const cancel = mkBtn(tCommon().cancel, "db-btn db-btn-sm");
13209
+ const status = document.createElement("span");
13210
+ status.className = "s3-edit-status";
13211
+ bar.append(save, cancel, status);
13212
+ const ta = document.createElement("textarea");
13213
+ ta.className = "s3-edit-textarea";
13214
+ ta.value = current;
13215
+ bodyEl.append(bar, ta);
13216
+ cancel.addEventListener("click", () => void selectObject(object));
13217
+ save.addEventListener("click", async () => {
13218
+ if (!currentDbId || !currentBucket)
13219
+ return;
13220
+ save.disabled = true;
13221
+ cancel.disabled = true;
13222
+ status.textContent = tCommon().saving;
13223
+ try {
13224
+ await postS3Write({
13225
+ db: currentDbId,
13226
+ bucket: currentBucket,
13227
+ key: object.key,
13228
+ content: ta.value,
13229
+ contentType: object.contentType || "text/plain"
13230
+ });
13231
+ await selectObject(object);
13232
+ } catch (err) {
13233
+ save.disabled = false;
13234
+ cancel.disabled = false;
13235
+ status.textContent = tCommon().saveError(err instanceof Error ? err.message : String(err));
13236
+ }
13237
+ });
13238
+ ta.focus();
13239
+ }
13240
+ async function deleteObject(object) {
13241
+ if (!currentDbId || !currentBucket)
13242
+ return;
13243
+ const ok = await showConfirmDialog({
13244
+ body: text2().confirmDeleteObject(object.key),
13245
+ confirmLabel: tCommon().delete,
13246
+ danger: true
13247
+ });
13248
+ if (!ok)
13249
+ return;
13250
+ try {
13251
+ await postS3Write({
13252
+ db: currentDbId,
13253
+ bucket: currentBucket,
13254
+ key: object.key,
13255
+ op: "delete"
13256
+ });
13257
+ currentKey = null;
13258
+ setPaneEmpty(previewPane, text2().selectObject);
13259
+ refreshObjectList();
13260
+ } catch (err) {
13261
+ setPaneStatus(previewPane, tCommon().saveError(err instanceof Error ? err.message : String(err)), { error: true });
13262
+ }
13263
+ }
13264
+ function showNewObjectForm() {
13265
+ if (!currentBucket) {
13266
+ setPaneStatus(previewPane, text2().selectObject);
13267
+ return;
13268
+ }
13269
+ previewPane.innerHTML = "";
13270
+ const form = document.createElement("form");
13271
+ form.className = "s3-new-object-form";
13272
+ const keyInput = document.createElement("input");
13273
+ keyInput.type = "text";
13274
+ keyInput.className = "s3-new-object-key";
13275
+ keyInput.placeholder = text2().newObjectKeyPlaceholder;
13276
+ keyInput.autocomplete = "off";
13277
+ const ta = document.createElement("textarea");
13278
+ ta.className = "s3-edit-textarea";
13279
+ ta.placeholder = text2().contentPlaceholder;
13280
+ const bar = document.createElement("div");
13281
+ bar.className = "s3-edit-bar";
13282
+ const create = mkBtn(text2().create, "db-btn db-btn-primary db-btn-sm");
13283
+ create.type = "submit";
13284
+ const cancel = mkBtn(tCommon().cancel, "db-btn db-btn-sm");
13285
+ const status = document.createElement("span");
13286
+ status.className = "s3-edit-status";
13287
+ bar.append(create, cancel, status);
13288
+ form.append(bar, keyInput, ta);
13289
+ previewPane.appendChild(form);
13290
+ keyInput.focus();
13291
+ cancel.addEventListener("click", () => setPaneEmpty(previewPane, text2().selectObject));
13292
+ form.addEventListener("submit", async (e2) => {
13293
+ e2.preventDefault();
13294
+ if (!currentDbId || !currentBucket)
13295
+ return;
13296
+ const key = keyInput.value.trim();
13297
+ if (!key) {
13298
+ keyInput.focus();
13299
+ return;
13300
+ }
13301
+ create.disabled = true;
13302
+ status.textContent = tCommon().saving;
13303
+ try {
13304
+ await postS3Write({
13305
+ db: currentDbId,
13306
+ bucket: currentBucket,
13307
+ key,
13308
+ content: ta.value,
13309
+ contentType: "text/plain",
13310
+ op: "create"
13311
+ });
13312
+ refreshObjectList();
13313
+ setPaneEmpty(previewPane, text2().selectObject);
13314
+ } catch (err) {
13315
+ create.disabled = false;
13316
+ status.textContent = tCommon().saveError(err instanceof Error ? err.message : String(err));
13317
+ }
13318
+ });
13319
+ }
13320
+ newObjectBtn.addEventListener("click", () => showNewObjectForm());
12443
13321
  function renderPreviewHeader(object) {
12444
13322
  const header = document.createElement("div");
12445
13323
  header.className = "s3-preview-header";
@@ -12485,6 +13363,14 @@ ${frontmatter.yaml}
12485
13363
  }
12486
13364
  });
12487
13365
  actions.append(open, download, copy);
13366
+ if (sourceDisplayKind(object.key) === "text") {
13367
+ const editBtn = mkBtn(tCommon().edit, "db-btn db-btn-sm");
13368
+ editBtn.addEventListener("click", () => void startObjectEdit(object));
13369
+ actions.appendChild(editBtn);
13370
+ }
13371
+ const delBtn = mkBtn(tCommon().delete, "db-btn db-btn-sm");
13372
+ delBtn.addEventListener("click", () => void deleteObject(object));
13373
+ actions.appendChild(delBtn);
12488
13374
  }
12489
13375
  header.append(title, meta, actions);
12490
13376
  return header;
@@ -12876,6 +13762,7 @@ ${frontmatter.yaml}
12876
13762
  currentNextToken = undefined;
12877
13763
  listLoaded = false;
12878
13764
  resetExplorer();
13765
+ newObjectBtn.hidden = false;
12879
13766
  notifySelectionChange();
12880
13767
  if (currentView === "explorer")
12881
13768
  await loadExplorerRoot();
@@ -12994,6 +13881,7 @@ ${frontmatter.yaml}
12994
13881
  return;
12995
13882
  }
12996
13883
  bucketSelect.value = selected;
13884
+ newObjectBtn.hidden = false;
12997
13885
  suppressNotify = true;
12998
13886
  try {
12999
13887
  currentBucket = selected;
@@ -13090,6 +13978,8 @@ ${frontmatter.yaml}
13090
13978
  containsModeBtn.textContent = t2.containsMode;
13091
13979
  sortUpdated.textContent = t2.sortUpdated;
13092
13980
  sortKey.textContent = t2.sortKey;
13981
+ newObjectBtn.textContent = `+ ${t2.newObject}`;
13982
+ newObjectBtn.title = t2.newObject;
13093
13983
  moreBtn.textContent = tCommon().loadMore;
13094
13984
  searchInput.placeholder = currentMode === "prefix" ? t2.prefixPlaceholder : t2.containsPlaceholder;
13095
13985
  if (!currentKey)
@@ -13292,6 +14182,276 @@ ${frontmatter.yaml}
13292
14182
  return { el, render, clear, localize };
13293
14183
  }
13294
14184
 
14185
+ // web-src/views/database/session-log.ts
14186
+ var DEFAULT_MAX_ENTRIES = 500;
14187
+ function createSessionLog(options = {}) {
14188
+ const max = options.maxEntries ?? DEFAULT_MAX_ENTRIES;
14189
+ const entries = [];
14190
+ const listeners = new Set;
14191
+ let seq = 0;
14192
+ function emit() {
14193
+ for (const fn of listeners)
14194
+ fn();
14195
+ }
14196
+ function add2(entry) {
14197
+ seq += 1;
14198
+ const next = {
14199
+ ...entry,
14200
+ id: `sl-${seq}`,
14201
+ timestamp: Date.now()
14202
+ };
14203
+ entries.push(next);
14204
+ while (entries.length > max)
14205
+ entries.shift();
14206
+ emit();
14207
+ return next;
14208
+ }
14209
+ return {
14210
+ add: add2,
14211
+ list: () => entries,
14212
+ clear() {
14213
+ if (entries.length === 0)
14214
+ return;
14215
+ entries.length = 0;
14216
+ emit();
14217
+ },
14218
+ subscribe(listener) {
14219
+ listeners.add(listener);
14220
+ return () => listeners.delete(listener);
14221
+ }
14222
+ };
14223
+ }
14224
+
14225
+ // web-src/views/database/session-log-view.ts
14226
+ function createSessionLogView(callbacks) {
14227
+ const text2 = () => callbacks.getText?.() ?? dbText("en");
14228
+ const el = document.createElement("div");
14229
+ el.className = "db-session-log";
14230
+ const toolbar = document.createElement("div");
14231
+ toolbar.className = "db-session-log-toolbar";
14232
+ const followBtn = document.createElement("button");
14233
+ followBtn.className = "db-session-log-action active";
14234
+ followBtn.type = "button";
14235
+ followBtn.textContent = text2().sessionLog.autoFollow;
14236
+ followBtn.title = text2().sessionLog.autoFollowTitle;
14237
+ followBtn.setAttribute("aria-pressed", "true");
14238
+ const clearBtn = document.createElement("button");
14239
+ clearBtn.className = "db-session-log-action db-session-log-danger";
14240
+ clearBtn.type = "button";
14241
+ clearBtn.textContent = text2().sessionLog.clear;
14242
+ clearBtn.title = text2().sessionLog.clearTitle;
14243
+ toolbar.append(followBtn, clearBtn);
14244
+ const body = document.createElement("div");
14245
+ body.className = "db-session-log-body-split";
14246
+ const listCol = document.createElement("div");
14247
+ listCol.className = "db-session-log-list-col";
14248
+ const listEl = document.createElement("div");
14249
+ listEl.className = "db-session-log-list";
14250
+ listCol.appendChild(listEl);
14251
+ const detailCol = document.createElement("div");
14252
+ detailCol.className = "db-session-log-detail-col";
14253
+ const placeholder = document.createElement("div");
14254
+ placeholder.className = "db-session-log-detail-placeholder";
14255
+ placeholder.textContent = text2().sessionLog.selectPlaceholder;
14256
+ detailCol.appendChild(placeholder);
14257
+ body.append(listCol, detailCol);
14258
+ el.append(toolbar, body);
14259
+ let selectedId = null;
14260
+ const rowById = new Map;
14261
+ let autoFollow = true;
14262
+ let suppressScrollEvent = false;
14263
+ const AUTO_FOLLOW_THRESHOLD_PX = 4;
14264
+ function refreshFollowBtn() {
14265
+ followBtn.classList.toggle("active", autoFollow);
14266
+ followBtn.setAttribute("aria-pressed", String(autoFollow));
14267
+ }
14268
+ followBtn.addEventListener("click", () => {
14269
+ autoFollow = !autoFollow;
14270
+ refreshFollowBtn();
14271
+ if (autoFollow) {
14272
+ suppressScrollEvent = true;
14273
+ listEl.scrollTop = 0;
14274
+ }
14275
+ });
14276
+ listEl.addEventListener("scroll", () => {
14277
+ if (suppressScrollEvent) {
14278
+ suppressScrollEvent = false;
14279
+ return;
14280
+ }
14281
+ if (autoFollow && listEl.scrollTop > AUTO_FOLLOW_THRESHOLD_PX) {
14282
+ autoFollow = false;
14283
+ refreshFollowBtn();
14284
+ }
14285
+ });
14286
+ let shiki = null;
14287
+ loadShikiHighlighter({
14288
+ themes: ["github-light", "github-dark"],
14289
+ langs: ["sql"]
14290
+ }).then((h) => {
14291
+ shiki = h;
14292
+ if (selectedId) {
14293
+ const cur = callbacks.store.list().find((e2) => e2.id === selectedId);
14294
+ if (cur)
14295
+ renderDetail(cur);
14296
+ }
14297
+ });
14298
+ function render() {
14299
+ listEl.innerHTML = "";
14300
+ rowById.clear();
14301
+ const entries = callbacks.store.list();
14302
+ if (entries.length === 0) {
14303
+ const empty = document.createElement("div");
14304
+ empty.className = "db-session-log-empty";
14305
+ empty.textContent = text2().sessionLog.empty;
14306
+ listEl.appendChild(empty);
14307
+ detailCol.innerHTML = "";
14308
+ detailCol.appendChild(placeholder);
14309
+ selectedId = null;
14310
+ return;
14311
+ }
14312
+ const fragment = document.createDocumentFragment();
14313
+ for (let i2 = entries.length - 1;i2 >= 0; i2--) {
14314
+ fragment.appendChild(renderEntry(entries[i2]));
14315
+ }
14316
+ listEl.appendChild(fragment);
14317
+ if (autoFollow) {
14318
+ suppressScrollEvent = true;
14319
+ listEl.scrollTop = 0;
14320
+ const latest = entries[entries.length - 1];
14321
+ selectedId = latest.id;
14322
+ renderDetail(latest);
14323
+ rowById.get(latest.id)?.classList.add("selected");
14324
+ } else if (selectedId) {
14325
+ const cur = entries.find((e2) => e2.id === selectedId);
14326
+ if (cur) {
14327
+ renderDetail(cur);
14328
+ rowById.get(cur.id)?.classList.add("selected");
14329
+ } else {
14330
+ detailCol.innerHTML = "";
14331
+ detailCol.appendChild(placeholder);
14332
+ selectedId = null;
14333
+ }
14334
+ }
14335
+ }
14336
+ function renderEntry(entry) {
14337
+ const row = document.createElement("div");
14338
+ row.className = "db-session-log-entry";
14339
+ row.dataset.id = entry.id;
14340
+ row.dataset.status = entry.status;
14341
+ row.dataset.kind = entry.kind;
14342
+ rowById.set(entry.id, row);
14343
+ const meta = document.createElement("div");
14344
+ meta.className = "db-session-log-entry-meta";
14345
+ const badge = document.createElement("span");
14346
+ badge.className = `db-session-log-badge db-session-log-badge-${entry.status}`;
14347
+ badge.textContent = entry.status === "error" ? text2().sessionLog.statusError : text2().sessionLog.statusOk;
14348
+ const kind = document.createElement("span");
14349
+ kind.className = "db-session-log-kind";
14350
+ kind.textContent = entry.kind === "query" ? text2().sessionLog.kindQuery : text2().sessionLog.kindMutate;
14351
+ const time = document.createElement("span");
14352
+ time.className = "db-session-log-time";
14353
+ time.textContent = formatTime2(entry.timestamp);
14354
+ const stats = document.createElement("span");
14355
+ stats.className = "db-session-log-stats";
14356
+ const parts = [];
14357
+ if (entry.rowCount !== undefined)
14358
+ parts.push(`${entry.rowCount} rows`);
14359
+ if (entry.elapsedMs !== undefined)
14360
+ parts.push(`${entry.elapsedMs}ms`);
14361
+ stats.textContent = parts.join(", ");
14362
+ meta.append(badge, kind, time, stats);
14363
+ const title = document.createElement("div");
14364
+ title.className = "db-session-log-entry-title";
14365
+ title.textContent = entry.label.length > 100 ? `${entry.label.slice(0, 100)}...` : entry.label;
14366
+ row.append(meta, title);
14367
+ row.addEventListener("click", () => selectEntry(entry));
14368
+ return row;
14369
+ }
14370
+ function selectEntry(entry) {
14371
+ selectedId = entry.id;
14372
+ for (const r2 of rowById.values())
14373
+ r2.classList.remove("selected");
14374
+ rowById.get(entry.id)?.classList.add("selected");
14375
+ renderDetail(entry);
14376
+ const entries = callbacks.store.list();
14377
+ const isLatest = entries[entries.length - 1]?.id === entry.id;
14378
+ if (autoFollow && !isLatest) {
14379
+ autoFollow = false;
14380
+ refreshFollowBtn();
14381
+ }
14382
+ }
14383
+ function renderDetail(entry) {
14384
+ detailCol.innerHTML = "";
14385
+ if (callbacks.copySqlToQuery && entry.detail) {
14386
+ const actions = document.createElement("div");
14387
+ actions.className = "db-session-log-detail-actions";
14388
+ const useBtn = document.createElement("button");
14389
+ useBtn.className = "db-btn db-btn-primary";
14390
+ useBtn.type = "button";
14391
+ useBtn.textContent = text2().sessionLog.useInEditor;
14392
+ useBtn.addEventListener("click", () => {
14393
+ callbacks.copySqlToQuery?.(entry.detail ?? "");
14394
+ });
14395
+ const copyBtn = document.createElement("button");
14396
+ copyBtn.className = "db-btn";
14397
+ copyBtn.type = "button";
14398
+ copyBtn.textContent = text2().sessionLog.copy;
14399
+ copyBtn.addEventListener("click", () => {
14400
+ const txt = entry.message && entry.detail ? `${entry.detail}
14401
+
14402
+ ${entry.message}` : entry.detail ?? entry.message ?? "";
14403
+ navigator.clipboard.writeText(txt).then(() => {
14404
+ copyBtn.textContent = text2().sessionLog.copied;
14405
+ setTimeout(() => {
14406
+ copyBtn.textContent = text2().sessionLog.copy;
14407
+ }, 1200);
14408
+ }, () => {});
14409
+ });
14410
+ actions.append(useBtn, copyBtn);
14411
+ detailCol.appendChild(actions);
14412
+ }
14413
+ if (entry.detail) {
14414
+ const sql = document.createElement("pre");
14415
+ sql.className = "db-session-log-sql";
14416
+ const inner = highlightSqlToInnerHtml(entry.detail, shiki);
14417
+ if (inner) {
14418
+ sql.innerHTML = inner;
14419
+ } else {
14420
+ sql.textContent = entry.detail;
14421
+ }
14422
+ detailCol.appendChild(sql);
14423
+ }
14424
+ if (entry.message) {
14425
+ const msg = document.createElement("pre");
14426
+ msg.className = `db-session-log-message db-session-log-message-${entry.status}`;
14427
+ msg.textContent = entry.message;
14428
+ detailCol.appendChild(msg);
14429
+ }
14430
+ }
14431
+ clearBtn.addEventListener("click", () => {
14432
+ callbacks.store.clear();
14433
+ });
14434
+ callbacks.store.subscribe(render);
14435
+ render();
14436
+ function clear() {
14437
+ callbacks.store.clear();
14438
+ }
14439
+ function localize() {
14440
+ clearBtn.textContent = text2().sessionLog.clear;
14441
+ clearBtn.title = text2().sessionLog.clearTitle;
14442
+ followBtn.textContent = text2().sessionLog.autoFollow;
14443
+ followBtn.title = text2().sessionLog.autoFollowTitle;
14444
+ placeholder.textContent = text2().sessionLog.selectPlaceholder;
14445
+ render();
14446
+ }
14447
+ return { el, clear, localize };
14448
+ }
14449
+ function formatTime2(timestamp) {
14450
+ const d2 = new Date(timestamp);
14451
+ const pad = (n2) => String(n2).padStart(2, "0");
14452
+ return `${pad(d2.getHours())}:${pad(d2.getMinutes())}:${pad(d2.getSeconds())}`;
14453
+ }
14454
+
13295
14455
  // web-src/views/database/snapshot-view.ts
13296
14456
  function arraysEqual(a2, b2) {
13297
14457
  if (a2.length !== b2.length)
@@ -14022,7 +15182,28 @@ ${frontmatter.yaml}
14022
15182
  closeExportMenu();
14023
15183
  triggerExport("json");
14024
15184
  });
14025
- filterBar.append(filterIcon, filterInput, filterClear, exportWrap);
15185
+ const editWrap = document.createElement("div");
15186
+ editWrap.className = "db-grid-edit-controls";
15187
+ editWrap.hidden = true;
15188
+ const newRowBtn = document.createElement("button");
15189
+ newRowBtn.type = "button";
15190
+ newRowBtn.className = "db-btn db-grid-edit-newrow";
15191
+ newRowBtn.textContent = text2().edit.newRow;
15192
+ newRowBtn.style.visibility = "hidden";
15193
+ const commitBtn = document.createElement("button");
15194
+ commitBtn.type = "button";
15195
+ commitBtn.className = "db-btn db-btn-primary db-grid-edit-commit";
15196
+ commitBtn.textContent = text2().edit.commit;
15197
+ commitBtn.style.visibility = "hidden";
15198
+ const discardBtn = document.createElement("button");
15199
+ discardBtn.type = "button";
15200
+ discardBtn.className = "db-btn db-grid-edit-discard";
15201
+ discardBtn.textContent = text2().edit.discard;
15202
+ discardBtn.style.visibility = "hidden";
15203
+ const editStatus = document.createElement("span");
15204
+ editStatus.className = "db-grid-edit-status";
15205
+ editWrap.append(newRowBtn, commitBtn, discardBtn, editStatus);
15206
+ filterBar.append(filterIcon, filterInput, filterClear, editWrap, exportWrap);
14026
15207
  const headerWrap = document.createElement("div");
14027
15208
  headerWrap.className = "db-grid-header-wrap";
14028
15209
  const headerRow = document.createElement("div");
@@ -14130,6 +15311,13 @@ ${frontmatter.yaml}
14130
15311
  let activeCellColIndex = -1;
14131
15312
  let renderStartRow = 0;
14132
15313
  const fkColumns = new Set;
15314
+ let editMode = false;
15315
+ let isComposing = false;
15316
+ const pendingEdits = new Map;
15317
+ const pendingDeletes = new Map;
15318
+ let draftRows = [];
15319
+ let editingCellRow = -1;
15320
+ let editingCellCol = -1;
14133
15321
  let relatedPanel = null;
14134
15322
  let relatedListEl = null;
14135
15323
  let relatedGridHost = null;
@@ -14275,6 +15463,9 @@ ${frontmatter.yaml}
14275
15463
  detailPanel.hidden = true;
14276
15464
  clearDetailContent();
14277
15465
  colWidths.clear();
15466
+ clearPending();
15467
+ setEditStatus("");
15468
+ refreshEditButtons();
14278
15469
  }
14279
15470
  let relatedFirstPageController = null;
14280
15471
  function relatedLookupValue(value) {
@@ -14524,8 +15715,8 @@ ${frontmatter.yaml}
14524
15715
  if (target.fk.inferred) {
14525
15716
  const badge = document.createElement("span");
14526
15717
  badge.className = "db-related-list-inferred-badge";
14527
- badge.textContent = "inferred";
14528
- badge.title = "Inferred from Rails-style naming, not declared in DB";
15718
+ badge.textContent = text2().nav.inferredBadge;
15719
+ badge.title = text2().nav.inferredBadgeTitle;
14529
15720
  name.appendChild(badge);
14530
15721
  }
14531
15722
  const via = document.createElement("span");
@@ -14795,7 +15986,7 @@ ${frontmatter.yaml}
14795
15986
  return;
14796
15987
  pageCache.set(pageStart, data.rows);
14797
15988
  totalRows = data.totalRows;
14798
- spacer.style.height = `${totalRows * ROW_HEIGHT}px`;
15989
+ syncSpacer();
14799
15990
  updateStatus();
14800
15991
  renderViewport();
14801
15992
  }).catch((err) => {
@@ -14813,98 +16004,334 @@ ${frontmatter.yaml}
14813
16004
  pendingPages.set(pageStart, tracked);
14814
16005
  return tracked;
14815
16006
  }
16007
+ function buildEditableCell(colIndex, value, opts) {
16008
+ const cell = document.createElement("div");
16009
+ cell.className = "db-grid-cell db-grid-cell-edit";
16010
+ cell.style.width = `${getColWidth(columnNames[colIndex])}px`;
16011
+ if (!opts.editing) {
16012
+ cell.classList.add("db-grid-cell-display");
16013
+ cell.textContent = value === null ? "NULL" : value === "" ? "" : value;
16014
+ if (value === null)
16015
+ cell.classList.add("null");
16016
+ else if (value === "")
16017
+ cell.classList.add("empty");
16018
+ if (opts.readonly) {
16019
+ cell.classList.add("readonly");
16020
+ }
16021
+ cell.style.cursor = "pointer";
16022
+ cell.addEventListener("click", (e2) => {
16023
+ e2.stopPropagation();
16024
+ opts.onActivate?.();
16025
+ });
16026
+ if (!opts.readonly) {
16027
+ cell.title = text2().edit.dblclickToEdit;
16028
+ cell.addEventListener("dblclick", (e2) => {
16029
+ e2.stopPropagation();
16030
+ opts.onEnterEdit?.();
16031
+ });
16032
+ }
16033
+ return cell;
16034
+ }
16035
+ const input = document.createElement("input");
16036
+ input.type = "text";
16037
+ input.className = "db-grid-cell-input";
16038
+ input.dataset.editRow = String(opts.rowIndex);
16039
+ input.dataset.editCol = String(colIndex);
16040
+ const applyNullVisual = (isNull) => {
16041
+ input.classList.toggle("is-null", isNull);
16042
+ input.placeholder = isNull ? "NULL" : "";
16043
+ };
16044
+ input.value = value === null ? "" : value;
16045
+ applyNullVisual(value === null);
16046
+ if (opts.readonly) {
16047
+ input.readOnly = true;
16048
+ input.classList.add("readonly");
16049
+ }
16050
+ input.addEventListener("input", () => {
16051
+ applyNullVisual(false);
16052
+ opts.onChange(input.value);
16053
+ });
16054
+ input.addEventListener("click", (e2) => e2.stopPropagation());
16055
+ input.addEventListener("blur", (e2) => {
16056
+ const next = e2.relatedTarget;
16057
+ if (next && cell.contains(next))
16058
+ return;
16059
+ opts.onExitEdit?.();
16060
+ });
16061
+ input.addEventListener("keydown", (e2) => {
16062
+ if (e2.key === "Enter" || e2.key === "Escape") {
16063
+ e2.preventDefault();
16064
+ opts.onExitEdit?.();
16065
+ }
16066
+ });
16067
+ cell.appendChild(input);
16068
+ if (opts.nullable && !opts.readonly) {
16069
+ const nullBtn = document.createElement("button");
16070
+ nullBtn.type = "button";
16071
+ nullBtn.className = "db-grid-cell-null-btn";
16072
+ nullBtn.textContent = "∅";
16073
+ nullBtn.title = text2().edit.setNull;
16074
+ nullBtn.addEventListener("mousedown", (e2) => e2.preventDefault());
16075
+ nullBtn.addEventListener("click", (e2) => {
16076
+ e2.stopPropagation();
16077
+ input.value = "";
16078
+ applyNullVisual(true);
16079
+ opts.onChange(null);
16080
+ input.focus();
16081
+ });
16082
+ cell.appendChild(nullBtn);
16083
+ }
16084
+ return cell;
16085
+ }
16086
+ function buildDataRow(i2) {
16087
+ const pageStart = Math.floor(i2 / PAGE_SIZE) * PAGE_SIZE;
16088
+ const pageRows = pageCache.get(pageStart);
16089
+ const rowData = pageRows ? pageRows[i2 - pageStart] : null;
16090
+ const row = document.createElement("div");
16091
+ row.className = "db-grid-row";
16092
+ if (i2 % 2 === 1)
16093
+ row.classList.add("alt");
16094
+ if (i2 === selectedRowIndex)
16095
+ row.classList.add("selected");
16096
+ const rowIndex = i2;
16097
+ const rowNum = document.createElement("div");
16098
+ rowNum.className = "db-grid-cell db-grid-rownum";
16099
+ rowNum.textContent = String(i2 + 1);
16100
+ row.appendChild(rowNum);
16101
+ if (!rowData) {
16102
+ for (let c2 = 0;c2 < columnNames.length; c2++) {
16103
+ const cell = document.createElement("div");
16104
+ cell.className = "db-grid-cell loading";
16105
+ cell.style.width = `${getColWidth(columnNames[c2])}px`;
16106
+ cell.textContent = "…";
16107
+ row.appendChild(cell);
16108
+ }
16109
+ return row;
16110
+ }
16111
+ if (editMode) {
16112
+ const key = rowKeyFor(rowData);
16113
+ const editableRow = key !== null;
16114
+ if (key !== null && pendingDeletes.has(key)) {
16115
+ row.classList.add("db-grid-row-deleted");
16116
+ }
16117
+ if (editableRow) {
16118
+ rowNum.classList.add("db-grid-rownum-deletable");
16119
+ rowNum.title = text2().edit.deleteRow;
16120
+ rowNum.style.cursor = "pointer";
16121
+ rowNum.addEventListener("click", (e2) => {
16122
+ e2.stopPropagation();
16123
+ const deleted = toggleDelete(rowData);
16124
+ row.classList.toggle("db-grid-row-deleted", deleted);
16125
+ });
16126
+ }
16127
+ const pendingForRow = key !== null ? pendingEdits.get(key) : undefined;
16128
+ if ((pendingForRow?.edits.size ?? 0) > 0) {
16129
+ row.classList.add("db-grid-row-edited");
16130
+ }
16131
+ for (let c2 = 0;c2 < columnNames.length; c2++) {
16132
+ const original = rowData[c2];
16133
+ const originalShown = original === null ? null : original instanceof Uint8Array ? formatValue3(original) : String(original);
16134
+ const hasOverride = pendingForRow?.edits.has(c2) ?? false;
16135
+ const shown = hasOverride ? pendingForRow?.edits.get(c2) ?? null : originalShown;
16136
+ const ro = !editableRow || columns[c2]?.primaryKey === true || original instanceof Uint8Array;
16137
+ const cellColIndex = c2;
16138
+ const cellColName = columnNames[c2];
16139
+ const cellValue = original;
16140
+ const rowValues = rowData;
16141
+ const fkClickable = fkColumns.has(cellColName) && (!embedded || !!callbacks.onForeignKeyCellClick);
16142
+ const isEditingThisCell = !ro && i2 === editingCellRow && c2 === editingCellCol;
16143
+ const activate = () => {
16144
+ selectedRowIndex = rowIndex;
16145
+ body.querySelectorAll(".db-grid-row.selected").forEach((r2) => {
16146
+ r2.classList.remove("selected");
16147
+ });
16148
+ row.classList.add("selected");
16149
+ setActiveCell(rowIndex, cellColIndex);
16150
+ if (fkClickable) {
16151
+ if (embedded) {
16152
+ callbacks.onForeignKeyCellClick?.(currentTable, columnNames, rowValues, cellColName);
16153
+ } else {
16154
+ openRelatedForRow(currentTable, columnNames, rowValues, cellColName);
16155
+ }
16156
+ } else {
16157
+ showCellDetail(cellColIndex, cellValue);
16158
+ }
16159
+ };
16160
+ const enterEdit = () => {
16161
+ editingCellRow = i2;
16162
+ editingCellCol = cellColIndex;
16163
+ renderViewport();
16164
+ requestAnimationFrame(() => {
16165
+ const inp = body.querySelector(`.db-grid-cell-input[data-edit-row="${i2}"][data-edit-col="${cellColIndex}"]`);
16166
+ if (!inp)
16167
+ return;
16168
+ inp.focus?.();
16169
+ try {
16170
+ inp.setSelectionRange?.(inp.value.length, inp.value.length);
16171
+ } catch {}
16172
+ });
16173
+ };
16174
+ const exitEdit = () => {
16175
+ if (editingCellRow === i2 && editingCellCol === cellColIndex) {
16176
+ editingCellRow = -1;
16177
+ editingCellCol = -1;
16178
+ renderViewport();
16179
+ }
16180
+ };
16181
+ const cellEl = buildEditableCell(c2, shown, {
16182
+ readonly: ro,
16183
+ nullable: columns[c2]?.nullable === true,
16184
+ editing: isEditingThisCell,
16185
+ rowIndex: i2,
16186
+ onChange: (v) => recordEdit(rowData, c2, v),
16187
+ onActivate: activate,
16188
+ onEnterEdit: enterEdit,
16189
+ onExitEdit: exitEdit
16190
+ });
16191
+ if (fkClickable) {
16192
+ cellEl.classList.add("db-grid-cell-fk");
16193
+ }
16194
+ if (hasOverride) {
16195
+ cellEl.classList.add("db-grid-cell-edited");
16196
+ }
16197
+ if (!isEditingThisCell && i2 === activeCellRowIndex && c2 === activeCellColIndex) {
16198
+ cellEl.classList.add("db-grid-cell-active");
16199
+ }
16200
+ row.appendChild(cellEl);
16201
+ }
16202
+ return row;
16203
+ }
16204
+ row.addEventListener("click", () => {
16205
+ selectedRowIndex = rowIndex;
16206
+ body.querySelectorAll(".db-grid-row.selected").forEach((r2) => {
16207
+ r2.classList.remove("selected");
16208
+ });
16209
+ row.classList.add("selected");
16210
+ });
16211
+ for (let c2 = 0;c2 < columnNames.length; c2++) {
16212
+ const cell = document.createElement("div");
16213
+ cell.className = "db-grid-cell";
16214
+ cell.style.width = `${getColWidth(columnNames[c2])}px`;
16215
+ const val = rowData[c2];
16216
+ cell.textContent = formatValue3(val);
16217
+ if (val === null)
16218
+ cell.classList.add("null");
16219
+ else if (val instanceof Uint8Array)
16220
+ cell.classList.add("blob");
16221
+ else if (typeof val === "string" && val === "")
16222
+ cell.classList.add("empty");
16223
+ cell.style.cursor = "pointer";
16224
+ const cellValue = val;
16225
+ const cellColIndex = c2;
16226
+ const cellColName = columnNames[c2];
16227
+ const rowValues = rowData;
16228
+ const fkClickable = fkColumns.has(cellColName) && (!embedded || !!callbacks.onForeignKeyCellClick);
16229
+ if (fkClickable) {
16230
+ cell.classList.add("db-grid-cell-fk");
16231
+ }
16232
+ cell.addEventListener("click", (e2) => {
16233
+ e2.stopPropagation();
16234
+ selectedRowIndex = rowIndex;
16235
+ body.querySelectorAll(".db-grid-row.selected").forEach((r2) => {
16236
+ r2.classList.remove("selected");
16237
+ });
16238
+ row.classList.add("selected");
16239
+ setActiveCell(rowIndex, cellColIndex);
16240
+ if (fkClickable) {
16241
+ if (embedded) {
16242
+ callbacks.onForeignKeyCellClick?.(currentTable, columnNames, rowValues, cellColName);
16243
+ } else {
16244
+ openRelatedForRow(currentTable, columnNames, rowValues, cellColName);
16245
+ }
16246
+ } else {
16247
+ showCellDetail(cellColIndex, cellValue);
16248
+ }
16249
+ });
16250
+ if (i2 === activeCellRowIndex && c2 === activeCellColIndex) {
16251
+ cell.classList.add("db-grid-cell-active");
16252
+ }
16253
+ row.appendChild(cell);
16254
+ }
16255
+ return row;
16256
+ }
16257
+ function buildDraftRow(draftIndex) {
16258
+ const draft = draftRows[draftIndex];
16259
+ const row = document.createElement("div");
16260
+ row.className = "db-grid-row db-grid-row-draft";
16261
+ const rowNum = document.createElement("div");
16262
+ rowNum.className = "db-grid-cell db-grid-rownum db-grid-rownum-draft";
16263
+ rowNum.textContent = "+";
16264
+ rowNum.title = text2().edit.newRow;
16265
+ rowNum.style.cursor = "pointer";
16266
+ rowNum.addEventListener("click", (e2) => {
16267
+ e2.stopPropagation();
16268
+ draftRows.splice(draftIndex, 1);
16269
+ syncSpacer();
16270
+ refreshEditButtons();
16271
+ showPendingStatus();
16272
+ renderViewport();
16273
+ });
16274
+ row.appendChild(rowNum);
16275
+ for (let c2 = 0;c2 < columnNames.length; c2++) {
16276
+ const dv = draft.has(c2) ? draft.get(c2) : "";
16277
+ row.appendChild(buildEditableCell(c2, dv, {
16278
+ readonly: false,
16279
+ nullable: columns[c2]?.nullable === true,
16280
+ editing: true,
16281
+ rowIndex: totalRows + draftIndex,
16282
+ onChange: (v) => {
16283
+ if (v === "")
16284
+ draft.delete(c2);
16285
+ else
16286
+ draft.set(c2, v);
16287
+ refreshEditButtons();
16288
+ showPendingStatus();
16289
+ }
16290
+ }));
16291
+ }
16292
+ return row;
16293
+ }
14816
16294
  function renderViewport() {
14817
16295
  cancelAnimationFrame(rafId);
14818
16296
  rafId = requestAnimationFrame(() => {
16297
+ if (isComposing)
16298
+ return;
14819
16299
  const scrollTop = viewport.scrollTop;
14820
16300
  const viewHeight = viewport.clientHeight;
14821
16301
  const startRow = Math.max(0, Math.floor(scrollTop / ROW_HEIGHT) - OVERSCAN);
14822
16302
  renderStartRow = startRow;
14823
- const endRow = Math.min(totalRows, Math.ceil((scrollTop + viewHeight) / ROW_HEIGHT) + OVERSCAN);
14824
- const neededPageStart = Math.floor(startRow / PAGE_SIZE) * PAGE_SIZE;
14825
- const neededPageEnd = Math.floor(endRow / PAGE_SIZE) * PAGE_SIZE;
14826
- for (let p2 = neededPageStart;p2 <= neededPageEnd; p2 += PAGE_SIZE) {
14827
- ensurePage(p2);
16303
+ const endRow = Math.min(displayRowCount(), Math.ceil((scrollTop + viewHeight) / ROW_HEIGHT) + OVERSCAN);
16304
+ const dataEnd = Math.min(endRow, totalRows);
16305
+ if (startRow < totalRows && dataEnd > startRow) {
16306
+ const neededPageStart = Math.floor(startRow / PAGE_SIZE) * PAGE_SIZE;
16307
+ const neededPageEnd = Math.floor((dataEnd - 1) / PAGE_SIZE) * PAGE_SIZE;
16308
+ for (let p2 = neededPageStart;p2 <= neededPageEnd; p2 += PAGE_SIZE) {
16309
+ ensurePage(p2);
16310
+ }
16311
+ }
16312
+ const active = document.activeElement;
16313
+ let focusRestore = null;
16314
+ if (active?.classList?.contains("db-grid-cell-input") && body.contains(active)) {
16315
+ focusRestore = {
16316
+ row: active.dataset?.editRow ?? "",
16317
+ col: active.dataset?.editCol ?? "",
16318
+ start: active.selectionStart ?? null,
16319
+ end: active.selectionEnd ?? null
16320
+ };
14828
16321
  }
14829
16322
  body.innerHTML = "";
14830
16323
  body.style.transform = `translateY(${startRow * ROW_HEIGHT}px)`;
14831
16324
  for (let i2 = startRow;i2 < endRow; i2++) {
14832
- const pageStart = Math.floor(i2 / PAGE_SIZE) * PAGE_SIZE;
14833
- const pageRows = pageCache.get(pageStart);
14834
- const rowData = pageRows ? pageRows[i2 - pageStart] : null;
14835
- const row = document.createElement("div");
14836
- row.className = "db-grid-row";
14837
- if (i2 % 2 === 1)
14838
- row.classList.add("alt");
14839
- if (i2 === selectedRowIndex)
14840
- row.classList.add("selected");
14841
- const rowIndex = i2;
14842
- row.addEventListener("click", () => {
14843
- selectedRowIndex = rowIndex;
14844
- body.querySelectorAll(".db-grid-row.selected").forEach((r2) => {
14845
- r2.classList.remove("selected");
14846
- });
14847
- row.classList.add("selected");
14848
- });
14849
- const rowNum = document.createElement("div");
14850
- rowNum.className = "db-grid-cell db-grid-rownum";
14851
- rowNum.textContent = String(i2 + 1);
14852
- row.appendChild(rowNum);
14853
- if (rowData) {
14854
- for (let c2 = 0;c2 < columnNames.length; c2++) {
14855
- const cell = document.createElement("div");
14856
- cell.className = "db-grid-cell";
14857
- cell.style.width = `${getColWidth(columnNames[c2])}px`;
14858
- const val = rowData[c2];
14859
- cell.textContent = formatValue3(val);
14860
- if (val === null)
14861
- cell.classList.add("null");
14862
- else if (val instanceof Uint8Array)
14863
- cell.classList.add("blob");
14864
- else if (typeof val === "string" && val === "")
14865
- cell.classList.add("empty");
14866
- cell.style.cursor = "pointer";
14867
- const cellValue = val;
14868
- const cellColIndex = c2;
14869
- const cellColName = columnNames[c2];
14870
- const rowValues = rowData;
14871
- const fkClickable = fkColumns.has(cellColName) && (!embedded || !!callbacks.onForeignKeyCellClick);
14872
- if (fkClickable) {
14873
- cell.classList.add("db-grid-cell-fk");
14874
- }
14875
- cell.addEventListener("click", (e2) => {
14876
- e2.stopPropagation();
14877
- selectedRowIndex = rowIndex;
14878
- body.querySelectorAll(".db-grid-row.selected").forEach((r2) => {
14879
- r2.classList.remove("selected");
14880
- });
14881
- row.classList.add("selected");
14882
- setActiveCell(rowIndex, cellColIndex);
14883
- if (fkClickable) {
14884
- if (embedded) {
14885
- callbacks.onForeignKeyCellClick?.(currentTable, columnNames, rowValues, cellColName);
14886
- } else {
14887
- openRelatedForRow(currentTable, columnNames, rowValues, cellColName);
14888
- }
14889
- } else {
14890
- showCellDetail(cellColIndex, cellValue);
14891
- }
14892
- });
14893
- if (i2 === activeCellRowIndex && c2 === activeCellColIndex) {
14894
- cell.classList.add("db-grid-cell-active");
14895
- }
14896
- row.appendChild(cell);
14897
- }
14898
- } else {
14899
- for (let c2 = 0;c2 < columnNames.length; c2++) {
14900
- const cell = document.createElement("div");
14901
- cell.className = "db-grid-cell loading";
14902
- cell.style.width = `${getColWidth(columnNames[c2])}px`;
14903
- cell.textContent = "…";
14904
- row.appendChild(cell);
14905
- }
16325
+ body.appendChild(i2 < totalRows ? buildDataRow(i2) : buildDraftRow(i2 - totalRows));
16326
+ }
16327
+ if (focusRestore) {
16328
+ const next = body.querySelector(`.db-grid-cell-input[data-edit-row="${focusRestore.row}"][data-edit-col="${focusRestore.col}"]`);
16329
+ if (next) {
16330
+ next.focus?.();
16331
+ try {
16332
+ next.setSelectionRange?.(focusRestore.start ?? next.value.length, focusRestore.end ?? next.value.length);
16333
+ } catch {}
14906
16334
  }
14907
- body.appendChild(row);
14908
16335
  }
14909
16336
  });
14910
16337
  }
@@ -14978,9 +16405,11 @@ ${frontmatter.yaml}
14978
16405
  }
14979
16406
  rebuildFkColumnsForCurrentTable();
14980
16407
  loadColWidths();
14981
- spacer.style.height = `${totalRows * ROW_HEIGHT}px`;
16408
+ syncSpacer();
14982
16409
  renderHeader();
14983
16410
  updateStatus();
16411
+ refreshEditButtons();
16412
+ showPendingStatus();
14984
16413
  if (!initialData) {
14985
16414
  ensurePage(0);
14986
16415
  } else {
@@ -15045,11 +16474,22 @@ ${frontmatter.yaml}
15045
16474
  renderViewport();
15046
16475
  };
15047
16476
  viewport.addEventListener("scroll", onViewportScroll, { passive: true });
16477
+ const onCompositionStart = () => {
16478
+ isComposing = true;
16479
+ };
16480
+ const onCompositionEnd = () => {
16481
+ isComposing = false;
16482
+ renderViewport();
16483
+ };
16484
+ body.addEventListener("compositionstart", onCompositionStart);
16485
+ body.addEventListener("compositionend", onCompositionEnd);
15048
16486
  function destroy() {
15049
16487
  clear();
15050
16488
  embeddedGrid?.destroy();
15051
16489
  embeddedGrid = null;
15052
16490
  viewport.removeEventListener("scroll", onViewportScroll);
16491
+ body.removeEventListener("compositionstart", onCompositionStart);
16492
+ body.removeEventListener("compositionend", onCompositionEnd);
15053
16493
  detailResizeCleanup?.();
15054
16494
  }
15055
16495
  function localize() {
@@ -15057,6 +16497,10 @@ ${frontmatter.yaml}
15057
16497
  filterInput.placeholder = t2.grid.searchPlaceholder;
15058
16498
  exportBtn.title = t2.grid.exportAction;
15059
16499
  exportBtn.setAttribute("aria-label", t2.grid.exportAction);
16500
+ newRowBtn.textContent = t2.edit.newRow;
16501
+ commitBtn.textContent = t2.edit.commit;
16502
+ discardBtn.textContent = t2.edit.discard;
16503
+ showPendingStatus();
15060
16504
  if (relatedEmptyEl)
15061
16505
  relatedEmptyEl.textContent = t2.grid.relatedEmpty;
15062
16506
  if (currentTable) {
@@ -15077,6 +16521,250 @@ ${frontmatter.yaml}
15077
16521
  fkColumns.add(fk.toColumn);
15078
16522
  }
15079
16523
  }
16524
+ function isEditable() {
16525
+ return !embedded && callbacks.getEditable?.() === true && typeof callbacks.applyMutations === "function";
16526
+ }
16527
+ function pkColumnIndices() {
16528
+ const idx = [];
16529
+ for (let i2 = 0;i2 < columns.length; i2++) {
16530
+ if (columns[i2].primaryKey)
16531
+ idx.push(i2);
16532
+ }
16533
+ return idx;
16534
+ }
16535
+ function hasPrimaryKey() {
16536
+ return columns.some((c2) => c2.primaryKey);
16537
+ }
16538
+ function rowKeyFor(rowData) {
16539
+ const idx = pkColumnIndices();
16540
+ if (idx.length === 0)
16541
+ return null;
16542
+ const parts = [];
16543
+ for (const i2 of idx) {
16544
+ const v = rowData[i2];
16545
+ if (v === null || v instanceof Uint8Array)
16546
+ return null;
16547
+ parts.push(String(v));
16548
+ }
16549
+ return JSON.stringify(parts);
16550
+ }
16551
+ function pkCellsFor(rowData) {
16552
+ return pkColumnIndices().map((i2) => ({
16553
+ column: columnNames[i2],
16554
+ value: String(rowData[i2])
16555
+ }));
16556
+ }
16557
+ function nonEmptyDraftCount() {
16558
+ return draftRows.filter((d2) => Array.from(d2.values()).some((v) => v === null || v !== "")).length;
16559
+ }
16560
+ function pendingCount() {
16561
+ let updates = 0;
16562
+ for (const [key, entry] of pendingEdits) {
16563
+ if (pendingDeletes.has(key))
16564
+ continue;
16565
+ if (entry.edits.size > 0)
16566
+ updates++;
16567
+ }
16568
+ return updates + pendingDeletes.size + nonEmptyDraftCount();
16569
+ }
16570
+ function hasPending() {
16571
+ return pendingCount() > 0;
16572
+ }
16573
+ function clearPending() {
16574
+ pendingEdits.clear();
16575
+ pendingDeletes.clear();
16576
+ draftRows = [];
16577
+ }
16578
+ function displayRowCount() {
16579
+ return totalRows + (editMode ? draftRows.length : 0);
16580
+ }
16581
+ function syncSpacer() {
16582
+ spacer.style.height = `${displayRowCount() * ROW_HEIGHT}px`;
16583
+ }
16584
+ function setEditStatus(message) {
16585
+ editStatus.textContent = message;
16586
+ }
16587
+ function refreshEditButtons() {
16588
+ const editable = isEditable();
16589
+ editWrap.hidden = !editable;
16590
+ if (!editable) {
16591
+ notifyEditableIfChanged();
16592
+ return;
16593
+ }
16594
+ const inert = !editMode;
16595
+ setControlVisibility(newRowBtn, !inert);
16596
+ setControlVisibility(commitBtn, !inert);
16597
+ setControlVisibility(discardBtn, !inert);
16598
+ setControlVisibility(editStatus, !inert);
16599
+ const count = pendingCount();
16600
+ commitBtn.disabled = count === 0;
16601
+ discardBtn.disabled = count === 0;
16602
+ notifyEditableIfChanged();
16603
+ }
16604
+ function setControlVisibility(el2, visible) {
16605
+ el2.style.visibility = visible ? "" : "hidden";
16606
+ if ("tabIndex" in el2) {
16607
+ el2.tabIndex = visible ? 0 : -1;
16608
+ }
16609
+ el2.setAttribute("aria-hidden", visible ? "false" : "true");
16610
+ }
16611
+ const editableListeners = new Set;
16612
+ const editModeListeners = new Set;
16613
+ let lastEditableNotified = null;
16614
+ function notifyEditableIfChanged() {
16615
+ const v = isEditable();
16616
+ if (v === lastEditableNotified)
16617
+ return;
16618
+ lastEditableNotified = v;
16619
+ for (const fn of editableListeners)
16620
+ fn(v);
16621
+ }
16622
+ function notifyEditMode() {
16623
+ for (const fn of editModeListeners)
16624
+ fn(editMode);
16625
+ }
16626
+ function onEditableChange(listener) {
16627
+ editableListeners.add(listener);
16628
+ listener(isEditable());
16629
+ return () => editableListeners.delete(listener);
16630
+ }
16631
+ function onEditModeChange(listener) {
16632
+ editModeListeners.add(listener);
16633
+ listener(editMode);
16634
+ return () => editModeListeners.delete(listener);
16635
+ }
16636
+ function showPendingStatus() {
16637
+ const count = pendingCount();
16638
+ setEditStatus(editMode && count > 0 ? text2().edit.pending(count) : editMode && !hasPrimaryKey() ? text2().edit.noPrimaryKey : "");
16639
+ }
16640
+ function recordEdit(rowData, colIndex, value) {
16641
+ const key = rowKeyFor(rowData);
16642
+ if (key === null)
16643
+ return;
16644
+ let entry = pendingEdits.get(key);
16645
+ if (!entry) {
16646
+ entry = { pk: pkCellsFor(rowData), edits: new Map };
16647
+ pendingEdits.set(key, entry);
16648
+ }
16649
+ const original = rowData[colIndex];
16650
+ const originalStr = original === null || original instanceof Uint8Array ? null : String(original);
16651
+ if (value === originalStr) {
16652
+ entry.edits.delete(colIndex);
16653
+ if (entry.edits.size === 0)
16654
+ pendingEdits.delete(key);
16655
+ } else {
16656
+ entry.edits.set(colIndex, value);
16657
+ }
16658
+ refreshEditButtons();
16659
+ showPendingStatus();
16660
+ }
16661
+ function toggleDelete(rowData) {
16662
+ const key = rowKeyFor(rowData);
16663
+ if (key === null)
16664
+ return false;
16665
+ if (pendingDeletes.has(key))
16666
+ pendingDeletes.delete(key);
16667
+ else
16668
+ pendingDeletes.set(key, pkCellsFor(rowData));
16669
+ refreshEditButtons();
16670
+ showPendingStatus();
16671
+ return pendingDeletes.has(key);
16672
+ }
16673
+ function buildMutations() {
16674
+ const muts = [];
16675
+ for (const pk of pendingDeletes.values()) {
16676
+ muts.push({ kind: "delete", pk });
16677
+ }
16678
+ for (const [key, entry] of pendingEdits) {
16679
+ if (pendingDeletes.has(key))
16680
+ continue;
16681
+ if (entry.edits.size === 0)
16682
+ continue;
16683
+ const values = Array.from(entry.edits).map(([c2, v]) => ({
16684
+ column: columnNames[c2],
16685
+ value: v
16686
+ }));
16687
+ muts.push({ kind: "update", pk: entry.pk, values });
16688
+ }
16689
+ for (const draft of draftRows) {
16690
+ const values = Array.from(draft).filter(([, v]) => v === null || v !== "").map(([c2, v]) => ({ column: columnNames[c2], value: v }));
16691
+ if (values.length > 0)
16692
+ muts.push({ kind: "insert", values });
16693
+ }
16694
+ return muts;
16695
+ }
16696
+ async function setEditMode(on) {
16697
+ if (on === editMode)
16698
+ return true;
16699
+ if (!on && hasPending()) {
16700
+ const ok = await showConfirmDialog({
16701
+ body: text2().edit.confirmDiscard,
16702
+ confirmLabel: text2().edit.discard,
16703
+ danger: true
16704
+ });
16705
+ if (!ok)
16706
+ return false;
16707
+ }
16708
+ editMode = on;
16709
+ if (!on)
16710
+ clearPending();
16711
+ notifyEditMode();
16712
+ editingCellRow = -1;
16713
+ editingCellCol = -1;
16714
+ syncSpacer();
16715
+ refreshEditButtons();
16716
+ showPendingStatus();
16717
+ renderViewport();
16718
+ }
16719
+ async function commitPending() {
16720
+ if (!callbacks.applyMutations)
16721
+ return;
16722
+ const mutations = buildMutations();
16723
+ if (mutations.length === 0)
16724
+ return;
16725
+ setEditStatus(text2().edit.committing);
16726
+ commitBtn.disabled = true;
16727
+ discardBtn.disabled = true;
16728
+ try {
16729
+ await callbacks.applyMutations(mutations);
16730
+ clearPending();
16731
+ await invalidateData();
16732
+ syncSpacer();
16733
+ setEditStatus("");
16734
+ } catch (err) {
16735
+ setEditStatus(text2().edit.commitError(err instanceof Error ? err.message : String(err)));
16736
+ } finally {
16737
+ refreshEditButtons();
16738
+ }
16739
+ }
16740
+ newRowBtn.addEventListener("click", () => {
16741
+ draftRows.push(new Map);
16742
+ syncSpacer();
16743
+ refreshEditButtons();
16744
+ showPendingStatus();
16745
+ renderViewport();
16746
+ viewport.scrollTop = viewport.scrollHeight;
16747
+ renderViewport();
16748
+ });
16749
+ discardBtn.addEventListener("click", async () => {
16750
+ if (!hasPending())
16751
+ return;
16752
+ const ok = await showConfirmDialog({
16753
+ body: text2().edit.confirmDiscard,
16754
+ confirmLabel: text2().edit.discard,
16755
+ danger: true
16756
+ });
16757
+ if (!ok)
16758
+ return;
16759
+ clearPending();
16760
+ syncSpacer();
16761
+ refreshEditButtons();
16762
+ setEditStatus("");
16763
+ renderViewport();
16764
+ });
16765
+ commitBtn.addEventListener("click", () => {
16766
+ commitPending();
16767
+ });
15080
16768
  function refreshForeignKeys() {
15081
16769
  rebuildFkColumnsForCurrentTable();
15082
16770
  if (currentTable) {
@@ -15094,7 +16782,12 @@ ${frontmatter.yaml}
15094
16782
  clear,
15095
16783
  destroy,
15096
16784
  localize,
15097
- refreshForeignKeys
16785
+ refreshForeignKeys,
16786
+ setEditMode,
16787
+ getEditMode: () => editMode,
16788
+ isEditable,
16789
+ onEditableChange,
16790
+ onEditModeChange
15098
16791
  };
15099
16792
  }
15100
16793
  function formatValue3(value) {
@@ -15476,7 +17169,7 @@ ${frontmatter.yaml}
15476
17169
  const historyVisible = sqlMode && userPrefersHistoryOpen;
15477
17170
  return {
15478
17171
  toolsHidden: !sqlMode,
15479
- historyToggleHidden: !sqlMode,
17172
+ historyTabStripHidden: !sqlMode,
15480
17173
  historyPaneHidden: !historyVisible,
15481
17174
  historyResizerHidden: !historyVisible,
15482
17175
  tableListHidden: !sqlMode,
@@ -15651,25 +17344,7 @@ ${frontmatter.yaml}
15651
17344
  snapshotView.refresh();
15652
17345
  }
15653
17346
  });
15654
- const inferFkToggle = makePrefToggle({
15655
- title: "Infer FK from Rails-style <name>_id → <names>.id",
15656
- label: "Rails FK 推測",
15657
- pathD: ICON_PATH_INFER_FK
15658
- });
15659
- inferFkToggle.addEventListener("click", () => {
15660
- const next = !outerDeps.getDbUiPref("inferFkRails", false);
15661
- outerDeps.setDbUiPref("inferFkRails", next);
15662
- applyInferFkBtnState();
15663
- grid.refreshForeignKeys();
15664
- });
15665
- function applyInferFkBtnState() {
15666
- const on = outerDeps.getDbUiPref("inferFkRails", false);
15667
- inferFkToggle.classList.toggle("active", on);
15668
- inferFkToggle.setAttribute("aria-pressed", String(on));
15669
- }
15670
- applyInferFkBtnState();
15671
17347
  const disposeInferFkPrefSub = outerDeps.onDbUiPrefChange(() => {
15672
- applyInferFkBtnState();
15673
17348
  grid.refreshForeignKeys();
15674
17349
  });
15675
17350
  reloc(() => {
@@ -15682,7 +17357,6 @@ ${frontmatter.yaml}
15682
17357
  toolsSection.append(queryBtn, erBtn, searchBtn, snapshotBtn);
15683
17358
  const prefsBar = document.createElement("div");
15684
17359
  prefsBar.className = "db-prefs-bar";
15685
- prefsBar.append(inferFkToggle);
15686
17360
  const explorerSidebarHost = document.createElement("div");
15687
17361
  explorerSidebarHost.className = "db-explorer-sidebar-host";
15688
17362
  sidebar.append(toolsSection, dbToolbar, prefsBar, explorerSidebarHost, tableList.el);
@@ -15727,7 +17401,31 @@ ${frontmatter.yaml}
15727
17401
  detailPanelHeightPx = h;
15728
17402
  cb.onStateChange();
15729
17403
  },
15730
- getText: () => paneText()
17404
+ getText: () => paneText(),
17405
+ getEditable: () => isSqlKind(currentDbInfo?.kind),
17406
+ applyMutations: (mutations) => applyRowMutations(mutations)
17407
+ });
17408
+ const editModeToggle = makePrefToggle({
17409
+ title: paneText().edit.editModeTitle,
17410
+ label: paneText().edit.editMode,
17411
+ pathD: ICON_PATH_EDIT_MODE,
17412
+ extraClass: "db-edit-mode-toggle"
17413
+ });
17414
+ editModeToggle.hidden = true;
17415
+ editModeToggle.addEventListener("click", () => {
17416
+ grid.setEditMode(!grid.getEditMode());
17417
+ });
17418
+ prefsBar.append(editModeToggle);
17419
+ grid.onEditableChange((editable) => {
17420
+ editModeToggle.hidden = !editable;
17421
+ });
17422
+ grid.onEditModeChange((on) => {
17423
+ editModeToggle.classList.toggle("active", on);
17424
+ editModeToggle.setAttribute("aria-pressed", String(on));
17425
+ });
17426
+ reloc(() => {
17427
+ const e2 = paneText().edit;
17428
+ localizePrefToggle(editModeToggle, e2.editMode, e2.editModeTitle);
15731
17429
  });
15732
17430
  const queryEditor = createQueryEditor({
15733
17431
  executeQuery: (sql) => executeQuery(sql),
@@ -15759,23 +17457,33 @@ ${frontmatter.yaml}
15759
17457
  setActiveTab("query");
15760
17458
  }
15761
17459
  });
17460
+ const sessionLogView = createSessionLogView({
17461
+ store: outerDeps.sessionLog,
17462
+ getText: () => paneText(),
17463
+ copySqlToQuery: (sql) => {
17464
+ queryEditor.setSql(sql);
17465
+ setActiveTab("query");
17466
+ }
17467
+ });
15762
17468
  const redisExplorer = createRedisExplorer({
15763
17469
  onSelectionChange: () => cb.onStateChange(),
15764
- getText: () => paneText()
17470
+ getText: () => paneText(),
17471
+ trackLoad: (p2) => deps.trackLoad(p2)
15765
17472
  });
15766
17473
  redisExplorer.el.hidden = true;
15767
17474
  redisExplorer.sidebarSlot.hidden = true;
15768
17475
  const esExplorer = createElasticsearchExplorer({
15769
17476
  onSelectionChange: () => cb.onStateChange(),
15770
- getText: () => paneText()
17477
+ getText: () => paneText(),
17478
+ trackLoad: (p2) => deps.trackLoad(p2)
15771
17479
  });
15772
17480
  esExplorer.el.hidden = true;
15773
17481
  esExplorer.sidebarSlot.hidden = true;
15774
17482
  const s3Explorer = createS3Explorer({
15775
17483
  onSelectionChange: () => cb.onStateChange(),
15776
17484
  getText: () => paneText(),
15777
- getTooltipEnabled: () => outerDeps.getDbUiPref("s3TooltipEnabled", true),
15778
- setTooltipEnabled: (enabled) => outerDeps.setDbUiPref("s3TooltipEnabled", enabled)
17485
+ trackLoad: (p2) => deps.trackLoad(p2),
17486
+ getTooltipEnabled: () => outerDeps.getDbUiPref("s3TooltipEnabled", true)
15779
17487
  });
15780
17488
  s3Explorer.el.hidden = true;
15781
17489
  s3Explorer.sidebarSlot.hidden = true;
@@ -15795,7 +17503,76 @@ ${frontmatter.yaml}
15795
17503
  historyPane.className = "db-history-pane";
15796
17504
  if (initial.historyHeight)
15797
17505
  historyPane.style.height = initial.historyHeight;
15798
- historyPane.appendChild(historyView.el);
17506
+ const historyTabContent = document.createElement("div");
17507
+ historyTabContent.className = "db-history-pane-content";
17508
+ historyTabContent.appendChild(historyView.el);
17509
+ const logTabContent = document.createElement("div");
17510
+ logTabContent.className = "db-history-pane-content";
17511
+ logTabContent.hidden = true;
17512
+ logTabContent.appendChild(sessionLogView.el);
17513
+ historyPane.append(historyTabContent, logTabContent);
17514
+ const historyDock = document.createElement("div");
17515
+ historyDock.className = "db-history-dock";
17516
+ historyDock.setAttribute("role", "tablist");
17517
+ const historyTabBtn = document.createElement("button");
17518
+ historyTabBtn.type = "button";
17519
+ historyTabBtn.className = "db-history-dock-tab";
17520
+ historyTabBtn.setAttribute("role", "tab");
17521
+ historyTabBtn.textContent = paneText().sessionLog.historyTabLabel;
17522
+ historyTabBtn.classList.add("active");
17523
+ historyTabBtn.setAttribute("aria-selected", "true");
17524
+ const logTabBtn = document.createElement("button");
17525
+ logTabBtn.type = "button";
17526
+ logTabBtn.className = "db-history-dock-tab";
17527
+ logTabBtn.setAttribute("role", "tab");
17528
+ logTabBtn.textContent = paneText().sessionLog.tabLabel;
17529
+ logTabBtn.setAttribute("aria-selected", "false");
17530
+ const historyDockSpacer = document.createElement("div");
17531
+ historyDockSpacer.className = "db-history-dock-spacer";
17532
+ const historyDockClose = document.createElement("button");
17533
+ historyDockClose.type = "button";
17534
+ historyDockClose.className = "db-history-dock-close";
17535
+ historyDockClose.setAttribute("aria-label", paneText().nav.queryHistory);
17536
+ historyDockClose.title = paneText().nav.queryHistoryTitle;
17537
+ historyDockClose.textContent = "×";
17538
+ historyDock.append(historyTabBtn, logTabBtn, historyDockSpacer, historyDockClose);
17539
+ let activeHistoryTab = initial.activeHistoryTab === "log" ? "log" : "history";
17540
+ function setActiveHistoryTab(which) {
17541
+ activeHistoryTab = which;
17542
+ const isHistory = which === "history";
17543
+ historyTabBtn.classList.toggle("active", isHistory);
17544
+ logTabBtn.classList.toggle("active", !isHistory);
17545
+ historyTabBtn.setAttribute("aria-selected", String(isHistory));
17546
+ logTabBtn.setAttribute("aria-selected", String(!isHistory));
17547
+ historyTabContent.hidden = !isHistory;
17548
+ logTabContent.hidden = isHistory;
17549
+ }
17550
+ setActiveHistoryTab(activeHistoryTab);
17551
+ function handleHistoryTabClick(which) {
17552
+ if (userPrefersHistoryOpen && activeHistoryTab === which) {
17553
+ userPrefersHistoryOpen = false;
17554
+ } else {
17555
+ setActiveHistoryTab(which);
17556
+ userPrefersHistoryOpen = true;
17557
+ }
17558
+ applyVisibility();
17559
+ cb.onStateChange();
17560
+ }
17561
+ historyTabBtn.addEventListener("click", () => handleHistoryTabClick("history"));
17562
+ logTabBtn.addEventListener("click", () => handleHistoryTabClick("log"));
17563
+ historyDockClose.addEventListener("click", () => {
17564
+ if (!userPrefersHistoryOpen)
17565
+ return;
17566
+ userPrefersHistoryOpen = false;
17567
+ applyVisibility();
17568
+ cb.onStateChange();
17569
+ });
17570
+ reloc(() => {
17571
+ historyTabBtn.textContent = paneText().sessionLog.historyTabLabel;
17572
+ logTabBtn.textContent = paneText().sessionLog.tabLabel;
17573
+ historyDockClose.setAttribute("aria-label", paneText().nav.queryHistory);
17574
+ historyDockClose.title = paneText().nav.queryHistoryTitle;
17575
+ });
15799
17576
  let historyResizing = false;
15800
17577
  historyResizer.addEventListener("mousedown", (e2) => {
15801
17578
  e2.preventDefault();
@@ -15820,21 +17597,13 @@ ${frontmatter.yaml}
15820
17597
  window.addEventListener("mousemove", onMove);
15821
17598
  window.addEventListener("mouseup", onUp);
15822
17599
  });
15823
- const historyToggle = document.createElement("button");
15824
- historyToggle.className = "db-history-toggle";
15825
- historyToggle.type = "button";
15826
- reloc(() => {
15827
- historyToggle.textContent = paneText().nav.queryHistory;
15828
- historyToggle.title = paneText().nav.queryHistoryTitle;
15829
- });
15830
- sidebar.appendChild(historyToggle);
15831
17600
  let userPrefersHistoryOpen = initial.historyOpen ?? true;
15832
17601
  function applyVisibility() {
15833
17602
  const sqlMode = isSqlKind(currentDbInfo?.kind);
15834
17603
  const visibility = computeVisibility(currentDbInfo?.kind, currentTab, userPrefersHistoryOpen);
15835
17604
  toolsSection.hidden = visibility.toolsHidden;
15836
17605
  prefsBar.hidden = visibility.toolsHidden;
15837
- historyToggle.hidden = visibility.historyToggleHidden;
17606
+ historyDock.hidden = visibility.historyTabStripHidden;
15838
17607
  historyResizer.hidden = visibility.historyResizerHidden;
15839
17608
  historyPane.hidden = visibility.historyPaneHidden;
15840
17609
  tableList.el.hidden = visibility.tableListHidden;
@@ -15858,19 +17627,14 @@ ${frontmatter.yaml}
15858
17627
  searchBtn.classList.remove("active");
15859
17628
  snapshotBtn.classList.remove("active");
15860
17629
  }
15861
- historyToggle.classList.toggle("active", userPrefersHistoryOpen);
17630
+ historyDock.classList.toggle("open", userPrefersHistoryOpen);
15862
17631
  if (!visibility.historyPaneHidden && cb.isActive())
15863
17632
  historyView.refresh();
15864
17633
  }
15865
17634
  applyVisibility();
15866
- historyToggle.addEventListener("click", () => {
15867
- userPrefersHistoryOpen = !userPrefersHistoryOpen;
15868
- applyVisibility();
15869
- cb.onStateChange();
15870
- });
15871
17635
  const container = document.createElement("div");
15872
17636
  container.className = "db-container";
15873
- container.append(upperArea, historyResizer, historyPane);
17637
+ container.append(upperArea, historyResizer, historyPane, historyDock);
15874
17638
  function createInnerTab(text2, active) {
15875
17639
  const btn = document.createElement("button");
15876
17640
  btn.className = `db-tab${active ? " active" : ""}`;
@@ -15975,11 +17739,12 @@ ${frontmatter.yaml}
15975
17739
  const params = new URLSearchParams({ db: dbId });
15976
17740
  if (preferredSchema)
15977
17741
  params.set("schema", preferredSchema);
15978
- const res = await deps.trackLoad(fetch(`/_db/schemas?${params}`));
15979
- if (!res.ok) {
15980
- throw new Error(await responseErrorMessage(res, "failed to fetch schemas"));
15981
- }
15982
- return await res.json();
17742
+ return logSqlFetch({
17743
+ url: `/_db/schemas?${params}`,
17744
+ kind: "query",
17745
+ label: `_db/schemas db=${dbId}`,
17746
+ errorPrefix: "failed to fetch schemas"
17747
+ });
15983
17748
  }
15984
17749
  function renderSchemaOptions(schemas, selected) {
15985
17750
  schemaSelect.innerHTML = "";
@@ -15999,11 +17764,12 @@ ${frontmatter.yaml}
15999
17764
  }
16000
17765
  async function fetchSchema(dbId) {
16001
17766
  const params = withCurrentSchema(new URLSearchParams({ db: dbId, includeColumns: "1" }));
16002
- const res = await deps.trackLoad(fetch(`/_db/schema?${params}`));
16003
- if (!res.ok) {
16004
- throw new Error(await responseErrorMessage(res, "failed to fetch schema"));
16005
- }
16006
- return await res.json();
17767
+ return logSqlFetch({
17768
+ url: `/_db/schema?${params}`,
17769
+ kind: "query",
17770
+ label: `_db/schema db=${dbId}`,
17771
+ errorPrefix: "failed to fetch schema"
17772
+ });
16007
17773
  }
16008
17774
  async function fetchTablePage(table2, offset, limit, sort, filters, signal, eq = []) {
16009
17775
  if (!currentDbInfo)
@@ -16025,38 +17791,128 @@ ${frontmatter.yaml}
16025
17791
  if (eq.length > 0) {
16026
17792
  params.set("eq", JSON.stringify(eq));
16027
17793
  }
16028
- const res = await fetch(`/_db/table?${params}`, signal ? { signal } : undefined);
16029
- if (!res.ok) {
16030
- throw new Error(await responseErrorMessage(res, "failed to fetch table"));
16031
- }
16032
- return await res.json();
17794
+ return logSqlFetch({
17795
+ url: `/_db/table?${params}`,
17796
+ init: signal ? { signal } : undefined,
17797
+ kind: "query",
17798
+ label: `SELECT FROM ${table2}`,
17799
+ trackLoad: false,
17800
+ errorPrefix: "failed to fetch table",
17801
+ rowCountOf: (r2) => r2.rows.length
17802
+ });
16033
17803
  }
16034
17804
  function fetchRelatedPage(table2, offset, limit, sort, filters, eq, signal) {
16035
17805
  return fetchTablePage(table2, offset, limit, sort, filters, signal, eq);
16036
17806
  }
17807
+ async function logSqlFetch(opts) {
17808
+ const startedAt = Date.now();
17809
+ const trackEnabled = opts.trackLoad !== false;
17810
+ try {
17811
+ const fetchPromise = fetch(opts.url, opts.init);
17812
+ const res = await (trackEnabled ? deps.trackLoad(fetchPromise) : fetchPromise);
17813
+ const elapsedMs = Date.now() - startedAt;
17814
+ if (!res.ok) {
17815
+ const message = await responseErrorMessage(res, opts.errorPrefix);
17816
+ outerDeps.sessionLog.add({
17817
+ kind: opts.kind,
17818
+ status: "error",
17819
+ label: opts.label,
17820
+ detail: opts.fallbackDetail,
17821
+ message,
17822
+ elapsedMs
17823
+ });
17824
+ const err = new Error(message);
17825
+ err.__sessionLogged = true;
17826
+ throw err;
17827
+ }
17828
+ const data = await res.json();
17829
+ const detail = data.executedSql && data.executedSql.length > 0 ? data.executedSql.join(`;
17830
+ `) : opts.fallbackDetail;
17831
+ outerDeps.sessionLog.add({
17832
+ kind: opts.kind,
17833
+ status: "ok",
17834
+ label: opts.label,
17835
+ detail,
17836
+ elapsedMs,
17837
+ rowCount: opts.rowCountOf?.(data)
17838
+ });
17839
+ return data;
17840
+ } catch (err) {
17841
+ if (!(err instanceof Error))
17842
+ throw err;
17843
+ if (err.__sessionLogged)
17844
+ throw err;
17845
+ if (err.name === "AbortError")
17846
+ throw err;
17847
+ outerDeps.sessionLog.add({
17848
+ kind: opts.kind,
17849
+ status: "error",
17850
+ label: opts.label,
17851
+ detail: opts.fallbackDetail,
17852
+ message: err.message,
17853
+ elapsedMs: Date.now() - startedAt
17854
+ });
17855
+ throw err;
17856
+ }
17857
+ }
16037
17858
  async function executeQuery(sql) {
16038
17859
  if (!currentDbInfo)
16039
17860
  throw new Error("no database selected");
16040
- const res = await fetch("/_db/query", {
16041
- method: "POST",
16042
- headers: {
16043
- "Content-Type": "application/json",
16044
- "X-Code-Viewer-Action": "1"
17861
+ const label = sql.split(`
17862
+ `)[0]?.trim() || sql;
17863
+ return logSqlFetch({
17864
+ url: "/_db/query",
17865
+ init: {
17866
+ method: "POST",
17867
+ headers: {
17868
+ "Content-Type": "application/json",
17869
+ "X-Code-Viewer-Action": "1"
17870
+ },
17871
+ body: JSON.stringify({
17872
+ db: currentDbInfo.id,
17873
+ ...currentSchema ? { schema: currentSchema } : {},
17874
+ sql,
17875
+ saveHistory: true,
17876
+ source: "browser",
17877
+ executedBy: "user"
17878
+ })
16045
17879
  },
16046
- body: JSON.stringify({
16047
- db: currentDbInfo.id,
16048
- ...currentSchema ? { schema: currentSchema } : {},
16049
- sql,
16050
- saveHistory: true,
16051
- source: "browser",
16052
- executedBy: "user"
16053
- })
17880
+ kind: "query",
17881
+ label,
17882
+ fallbackDetail: sql,
17883
+ trackLoad: false,
17884
+ errorPrefix: "failed to execute query",
17885
+ rowCountOf: (r2) => r2.rowCount ?? r2.rows.length
17886
+ });
17887
+ }
17888
+ async function applyRowMutations(mutations) {
17889
+ if (!currentDbInfo)
17890
+ throw new Error("no database selected");
17891
+ if (!currentTable)
17892
+ throw new Error("no table selected");
17893
+ const label = paneText().sessionLog.commitLabel(mutations.length);
17894
+ const fallback = JSON.stringify({ table: currentTable, mutations }, null, 2);
17895
+ await logSqlFetch({
17896
+ url: "/_db/mutate",
17897
+ init: {
17898
+ method: "POST",
17899
+ headers: {
17900
+ "Content-Type": "application/json",
17901
+ "X-Code-Viewer-Action": "1"
17902
+ },
17903
+ body: JSON.stringify({
17904
+ db: currentDbInfo.id,
17905
+ ...currentSchema ? { schema: currentSchema } : {},
17906
+ table: currentTable,
17907
+ mutations
17908
+ })
17909
+ },
17910
+ kind: "mutate",
17911
+ label,
17912
+ fallbackDetail: fallback,
17913
+ errorPrefix: "failed to save changes",
17914
+ rowCountOf: () => mutations.length
16054
17915
  });
16055
- if (!res.ok) {
16056
- throw new Error(await responseErrorMessage(res, "failed to execute query"));
16057
- }
16058
- const result = await res.json();
16059
- return result;
16060
17916
  }
16061
17917
  async function selectDb(dbId, explorerInitial, generation = loadGeneration, preferredSchema, preferredTable, targetView) {
16062
17918
  if (generation !== loadGeneration || currentDbInfo?.id !== dbId)
@@ -16235,11 +18091,18 @@ ${frontmatter.yaml}
16235
18091
  }
16236
18092
  if (!currentDbInfo)
16237
18093
  return [];
16238
- const res = await fetch(`/_db/columns?${withCurrentSchema(new URLSearchParams({ db: currentDbInfo.id, table: table2 }))}`);
16239
- if (!res.ok)
18094
+ try {
18095
+ const data = await logSqlFetch({
18096
+ url: `/_db/columns?${withCurrentSchema(new URLSearchParams({ db: currentDbInfo.id, table: table2 }))}`,
18097
+ kind: "query",
18098
+ label: `_db/columns ${table2}`,
18099
+ trackLoad: false,
18100
+ errorPrefix: "failed to fetch columns"
18101
+ });
18102
+ return data.columns;
18103
+ } catch {
16240
18104
  return [];
16241
- const data = await res.json();
16242
- return data.columns;
18105
+ }
16243
18106
  }
16244
18107
  async function showSchema(table2) {
16245
18108
  setActiveTab("schema");
@@ -16253,10 +18116,13 @@ ${frontmatter.yaml}
16253
18116
  return;
16254
18117
  setActiveTab("schema");
16255
18118
  try {
16256
- const res = await fetch(`/_db/ddl?${withCurrentSchema(new URLSearchParams({ db: currentDbInfo.id, table: table2 }))}`);
16257
- if (!res.ok)
16258
- return;
16259
- const data = await res.json();
18119
+ const data = await logSqlFetch({
18120
+ url: `/_db/ddl?${withCurrentSchema(new URLSearchParams({ db: currentDbInfo.id, table: table2 }))}`,
18121
+ kind: "query",
18122
+ label: `_db/ddl ${table2}`,
18123
+ trackLoad: false,
18124
+ errorPrefix: "failed to fetch DDL"
18125
+ });
16260
18126
  const columns = await fetchColumns(table2);
16261
18127
  schemaView.render(table2, columns, schemaCache?.indexes || [], {
16262
18128
  foreignKeys: schemaCache?.foreignKeys,
@@ -16503,6 +18369,8 @@ ${frontmatter.yaml}
16503
18369
  state.historyOpen = false;
16504
18370
  if (historyPane.style.height)
16505
18371
  state.historyHeight = historyPane.style.height;
18372
+ if (activeHistoryTab === "log")
18373
+ state.activeHistoryTab = "log";
16506
18374
  if (sidebar.style.width)
16507
18375
  state.sidebarWidth = sidebar.style.width;
16508
18376
  if (relatedPanelHeightPx != null)
@@ -16619,7 +18487,7 @@ ${frontmatter.yaml}
16619
18487
  var ICON_PATH_ER = "M1.5 1.75A.75.75 0 0 1 2.25 1h4.5a.75.75 0 0 1 .75.75v3.5h2v-1.5A.75.75 0 0 1 10.25 3h3.5a.75.75 0 0 1 .75.75v3.5a.75.75 0 0 1-.75.75h-3.5a.75.75 0 0 1-.75-.75V6.5h-2v3h2v-.75A.75.75 0 0 1 10.25 8h3.5a.75.75 0 0 1 .75.75v3.5a.75.75 0 0 1-.75.75h-3.5a.75.75 0 0 1-.75-.75v-1.5h-2v3.5a.75.75 0 0 1-.75.75h-4.5a.75.75 0 0 1-.75-.75v-3.5A.75.75 0 0 1 2.25 9h4.5a.75.75 0 0 1 .75.75v.75h-.5v-1H3v3h3V11h.5v-.75a.75.75 0 0 0-.75-.75H3V6.5h2.5V2.5H3Z";
16620
18488
  var ICON_PATH_SEARCH = "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";
16621
18489
  var ICON_PATH_SNAPSHOT = "M3.5 1.75A1.75 1.75 0 0 1 5.25 0h5.5A1.75 1.75 0 0 1 12.5 1.75v.5h1.75A1.75 1.75 0 0 1 16 4v9.25A1.75 1.75 0 0 1 14.25 15H1.75A1.75 1.75 0 0 1 0 13.25V4a1.75 1.75 0 0 1 1.75-1.75H3.5v-.5Zm1.5.5v.5h6v-.5a.25.25 0 0 0-.25-.25h-5.5a.25.25 0 0 0-.25.25Zm-3.25 2a.25.25 0 0 0-.25.25v9.25c0 .138.112.25.25.25h12.5a.25.25 0 0 0 .25-.25V4a.25.25 0 0 0-.25-.25H1.75ZM8 6a2.75 2.75 0 1 0 0 5.5 2.75 2.75 0 0 0 0-5.5Z";
16622
- var ICON_PATH_INFER_FK = "M7.775 3.275a.75.75 0 0 0 1.06 1.06l1.25-1.25a2 2 0 1 1 2.83 2.83l-2.5 2.5a2 2 0 0 1-2.83 0 .75.75 0 0 0-1.06 1.06 3.5 3.5 0 0 0 4.95 0l2.5-2.5a3.5 3.5 0 0 0-4.95-4.95l-1.25 1.25Zm-4.69 9.64a2 2 0 0 1 0-2.83l2.5-2.5a2 2 0 0 1 2.83 0 .75.75 0 0 0 1.06-1.06 3.5 3.5 0 0 0-4.95 0l-2.5 2.5a3.5 3.5 0 0 0 4.95 4.95l1.25-1.25a.75.75 0 0 0-1.06-1.06l-1.25 1.25a2 2 0 0 1-2.83 0Z";
18490
+ var ICON_PATH_EDIT_MODE = "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.61Zm.176 4.823L9.75 4.81l-6.286 6.287a.247.247 0 0 0-.064.108l-.558 1.953 1.953-.558a.249.249 0 0 0 .108-.064l6.286-6.286Zm1.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-.354L12.427 2.49Z";
16623
18491
  function makeIconButton(opts) {
16624
18492
  const btn = document.createElement("button");
16625
18493
  btn.type = "button";
@@ -16666,6 +18534,7 @@ ${frontmatter.yaml}
16666
18534
  let dbFilesCache = null;
16667
18535
  let dbUiState = { version: 1, columnWidths: {} };
16668
18536
  let dbUiLoadPromise = null;
18537
+ const sessionLog = createSessionLog();
16669
18538
  function isRestoring() {
16670
18539
  return restoringDepth > 0;
16671
18540
  }
@@ -17168,10 +19037,11 @@ ${frontmatter.yaml}
17168
19037
  const closeBtn = document.createElement("button");
17169
19038
  closeBtn.type = "button";
17170
19039
  closeBtn.className = "db-tabs-chip-close";
17171
- closeBtn.title = "閉じる";
17172
19040
  closeBtn.tabIndex = -1;
17173
19041
  closeBtn.textContent = "×";
17174
- closeBtn.setAttribute("aria-label", outerText().nav.closeTab(initialLabel));
19042
+ const closeTabLabel = outerText().nav.closeTab(initialLabel);
19043
+ closeBtn.title = closeTabLabel;
19044
+ closeBtn.setAttribute("aria-label", closeTabLabel);
17175
19045
  closeBtn.addEventListener("click", (e2) => {
17176
19046
  e2.stopPropagation();
17177
19047
  closeTab(id);
@@ -17204,7 +19074,8 @@ ${frontmatter.yaml}
17204
19074
  getDbUiPref,
17205
19075
  setDbUiPref,
17206
19076
  onDbUiPrefChange,
17207
- loadSqlHistory
19077
+ loadSqlHistory,
19078
+ sessionLog
17208
19079
  }, {
17209
19080
  tabId: id,
17210
19081
  isActive: () => activeTabId === id,
@@ -17223,6 +19094,7 @@ ${frontmatter.yaml}
17223
19094
  sqlDraft: initial?.sqlDraft,
17224
19095
  historyOpen: initial?.historyOpen,
17225
19096
  historyHeight: initial?.historyHeight,
19097
+ activeHistoryTab: initial?.activeHistoryTab,
17226
19098
  sidebarWidth: initial?.sidebarWidth,
17227
19099
  relatedPanelHeight: initial?.relatedPanelHeight,
17228
19100
  detailPanelHeight: initial?.detailPanelHeight,
@@ -17588,7 +19460,10 @@ ${frontmatter.yaml}
17588
19460
  suspend,
17589
19461
  leave,
17590
19462
  handleSse,
17591
- localize
19463
+ localize,
19464
+ getDbUiPref,
19465
+ setDbUiPref,
19466
+ onDbUiPrefChange
17592
19467
  };
17593
19468
  }
17594
19469
  function formatSize(bytes) {
@@ -19052,7 +20927,21 @@ ${frontmatter.yaml}
19052
20927
  // web-src/views/history-view.ts
19053
20928
  var HISTORY_BODY_COLLAPSE_LINES = 10;
19054
20929
  var HISTORY_WORKTREE_COMMIT = "worktree";
19055
- var HISTORY_WORKTREE_LABEL = "未コミット変更 (Working tree)";
20930
+ var HISTORY_TEXT = {
20931
+ en: {
20932
+ worktreeLabel: "Uncommitted changes (Working tree)",
20933
+ bodyExpandClose: "Collapse",
20934
+ bodyExpandMore: (n2) => `Show more (${n2} lines)`
20935
+ },
20936
+ ja: {
20937
+ worktreeLabel: "未コミット変更 (Working tree)",
20938
+ bodyExpandClose: "閉じる",
20939
+ bodyExpandMore: (n2) => `もっと見る (${n2} 行)`
20940
+ }
20941
+ };
20942
+ function historyWorktreeLabel(lang) {
20943
+ return HISTORY_TEXT[lang].worktreeLabel;
20944
+ }
19056
20945
  function buildHistoryPanelDom(options = {}) {
19057
20946
  const variant = options.variant || "page";
19058
20947
  const page = variant === "page";
@@ -19161,10 +21050,11 @@ ${frontmatter.yaml}
19161
21050
  return 0;
19162
21051
  return rawText.split(/\r?\n/).length;
19163
21052
  }
19164
- function historyBodyToggleLabel(expanded, remainingLines) {
19165
- return expanded ? "閉じる" : `もっと見る (${remainingLines} 行)`;
21053
+ function historyBodyToggleLabel(expanded, remainingLines, lang) {
21054
+ const t2 = HISTORY_TEXT[lang];
21055
+ return expanded ? t2.bodyExpandClose : t2.bodyExpandMore(remainingLines);
19166
21056
  }
19167
- function buildExpandableHistoryBody(rendered, rawText) {
21057
+ function buildExpandableHistoryBody(rendered, rawText, lang) {
19168
21058
  const lineCount = historyBodyLineCount(rawText);
19169
21059
  if (lineCount <= HISTORY_BODY_COLLAPSE_LINES)
19170
21060
  return rendered;
@@ -19180,7 +21070,7 @@ ${frontmatter.yaml}
19180
21070
  const remainingLines = lineCount - HISTORY_BODY_COLLAPSE_LINES;
19181
21071
  const sync = () => {
19182
21072
  const expanded = collapsible.classList.contains("expanded");
19183
- button.textContent = historyBodyToggleLabel(expanded, remainingLines);
21073
+ button.textContent = historyBodyToggleLabel(expanded, remainingLines, lang);
19184
21074
  button.setAttribute("aria-expanded", expanded ? "true" : "false");
19185
21075
  };
19186
21076
  const toggle = () => {
@@ -19339,7 +21229,7 @@ ${frontmatter.yaml}
19339
21229
  }
19340
21230
  function worktreeRow() {
19341
21231
  const active = selectedSha === HISTORY_WORKTREE_COMMIT ? " active" : "";
19342
- return `<li class="history-item history-item-worktree${active}" data-sha="${HISTORY_WORKTREE_COMMIT}">` + `<span class="subject" title="${deps.escapeHtml(HISTORY_WORKTREE_LABEL)}">${deps.escapeHtml(HISTORY_WORKTREE_LABEL)}</span>` + `<span class="meta2">` + `<span class="sha">HEAD..worktree</span>` + `<span class="author">Working tree</span>` + `</span>` + `</li>`;
21232
+ return `<li class="history-item history-item-worktree${active}" data-sha="${HISTORY_WORKTREE_COMMIT}">` + `<span class="subject" title="${deps.escapeHtml(historyWorktreeLabel(deps.getLanguage()))}">${deps.escapeHtml(historyWorktreeLabel(deps.getLanguage()))}</span>` + `<span class="meta2">` + `<span class="sha">HEAD..worktree</span>` + `<span class="author">Working tree</span>` + `</span>` + `</li>`;
19343
21233
  }
19344
21234
  function renderList() {
19345
21235
  const now = new Date;
@@ -19390,7 +21280,7 @@ ${frontmatter.yaml}
19390
21280
  const rendered = await renderMarkdownPreview(commit.body, { path: "COMMIT_MSG", ref: commit.sha }, { syntaxHighlight: deps.getSyntaxHighlight() });
19391
21281
  if (gen !== generation || selectedSha !== commit.sha)
19392
21282
  return;
19393
- body.replaceChildren(buildExpandableHistoryBody(rendered, commit.body));
21283
+ body.replaceChildren(buildExpandableHistoryBody(rendered, commit.body, deps.getLanguage()));
19394
21284
  }
19395
21285
  }
19396
21286
  info.hidden = false;
@@ -19401,8 +21291,9 @@ ${frontmatter.yaml}
19401
21291
  return;
19402
21292
  info.querySelector(".hci-head")?.setAttribute("hidden", "");
19403
21293
  const subject = info.querySelector(".hci-subject");
19404
- if (subject)
19405
- subject.textContent = HISTORY_WORKTREE_LABEL;
21294
+ if (subject) {
21295
+ subject.textContent = historyWorktreeLabel(deps.getLanguage());
21296
+ }
19406
21297
  const body = info.querySelector(".hci-body");
19407
21298
  if (body) {
19408
21299
  body.hidden = true;
@@ -20141,7 +22032,7 @@ code-viewer annotate add-db --db app.db --tab query \\
20141
22032
  database: {
20142
22033
  nav: "Datastores",
20143
22034
  title: "Datastore Viewer",
20144
- intro: "Browse SQLite files, Docker-hosted databases, Redis, Elasticsearch, and S3-compatible object stores from one local viewer.",
22035
+ intro: "Browse and edit SQLite files, Docker-hosted databases, Redis, Elasticsearch, and S3-compatible object stores from one local viewer.",
20145
22036
  groups: [
20146
22037
  {
20147
22038
  title: "Supported datastores",
@@ -20151,27 +22042,27 @@ code-viewer annotate add-db --db app.db --tab query \\
20151
22042
  rows: [
20152
22043
  [
20153
22044
  "SQLite",
20154
- "Automatically discovered from .db, .sqlite, .sqlite3, and .s3db files in the repository."
22045
+ "Automatically discovered from .db, .sqlite, .sqlite3, and .s3db files in the repository. Inline row edit / insert / delete via the grid Edit mode (atomic per-commit batch)."
20155
22046
  ],
20156
22047
  [
20157
22048
  "MySQL / MariaDB",
20158
- "Detected from docker-compose.yml / compose.yml (or .yaml). Multiple databases per server are listed."
22049
+ "Detected from docker-compose.yml / compose.yml (or .yaml). Multiple databases per server are listed. Same inline row edit / insert / delete as SQLite."
20159
22050
  ],
20160
22051
  [
20161
22052
  "PostgreSQL",
20162
- "Detected from compose files. Multiple databases per server, plus a schema selector for switching schemas without reopening."
22053
+ "Detected from compose files. Multiple databases per server, plus a schema selector for switching schemas without reopening. Same inline row edit / insert / delete as SQLite."
20163
22054
  ],
20164
22055
  [
20165
22056
  "Redis",
20166
- "Detected from compose files. Read-only browse over DB 0-15, SCAN keys, dedicated string/hash/list panes, JSON view for set/zset/stream, and participates in snapshots and diffs."
22057
+ "Detected from compose files. Browse DB 0-15, SCAN keys, dedicated string/hash/list panes, JSON view for set/zset/stream. Edit values, delete keys, and create new keys (all types) via in-pane editors. Participates in snapshots and diffs."
20167
22058
  ],
20168
22059
  [
20169
22060
  "Elasticsearch",
20170
- "Detected from compose files. List indices, view mappings, paginate with search_after, run lucene q= or DSL queries on an allowlist, and take snapshots/diffs."
22061
+ "Detected from compose files. List indices, view mappings, paginate with search_after, run lucene q= or DSL queries on an allowlist. Edit / create / delete documents with _seq_no / _primary_term optimistic concurrency. Take snapshots/diffs."
20171
22062
  ],
20172
22063
  [
20173
22064
  "S3 / MinIO / LocalStack",
20174
- "Detected from compose files. Folder-tree browse, prefix/filename search, updated-time sort, and previews for images, video, audio, PDF, Markdown, HTML, and text. LocalStack falls back to `docker exec curl` when no host port is published; MinIO requires a published host port."
22065
+ "Detected from compose files. Folder-tree browse, prefix/filename search, updated-time sort, and previews for images, video, audio, PDF, Markdown, HTML, and text. Edit text/markdown/JSON object bodies inline, upload new objects, and delete existing ones. LocalStack falls back to `docker exec curl` when no host port is published; MinIO requires a published host port."
20175
22066
  ]
20176
22067
  ]
20177
22068
  }
@@ -20193,7 +22084,7 @@ code-viewer annotate add-db --db app.db --tab query \\
20193
22084
  ],
20194
22085
  [
20195
22086
  "Data tab",
20196
- "Paginated grid with sort, filter, cell copy, and CSV/JSON export (capped at 100k rows; export respects the current filter and sort)."
22087
+ "Paginated grid with sort, filter, cell copy, and CSV/JSON export (capped at 100k rows; export respects the current filter and sort). Toggle Edit mode in the prefs bar to enable inline editing — double-click a cell to edit, queue insert/delete via row actions, and commit the batch atomically. Edited rows/cells highlight in yellow until committed."
20197
22088
  ],
20198
22089
  [
20199
22090
  "Detail footer & related panel",
@@ -20205,7 +22096,7 @@ code-viewer annotate add-db --db app.db --tab query \\
20205
22096
  ],
20206
22097
  [
20207
22098
  "Query editor",
20208
- "SQL syntax highlighting, Tab indent, auto-resize, Ctrl+Enter to run. Allowlist depends on the engine (SQLite: SELECT/PRAGMA/EXPLAIN/WITH; PostgreSQL and MySQL also accept SHOW/DESCRIBE)."
22099
+ "SQL syntax highlighting (shiki), Tab indent, auto-resize, Ctrl+Enter to run. Allowlist depends on the engine (SQLite: SELECT/PRAGMA/EXPLAIN/WITH; PostgreSQL and MySQL also accept SHOW/DESCRIBE)."
20209
22100
  ],
20210
22101
  [
20211
22102
  "ER Diagram",
@@ -20220,12 +22111,12 @@ code-viewer annotate add-db --db app.db --tab query \\
20220
22111
  "Capture point-in-time snapshots of selected tables/indices/key spaces and diff any two to see inserts, updates, and deletes with full before/after values."
20221
22112
  ],
20222
22113
  [
20223
- "Query History",
20224
- "Master-detail panel with history list and result preview. Each entry records whether it came from the browser or the CLI, and updates live over SSE."
22114
+ "Footer dock (Query History / Session log)",
22115
+ "JetBrains-style bottom dock with two tabs. Query History: per-DB master/detail of saved queries, SSE-synced across tabs. Session log: every SQL the server runs in this session (read fetches, user queries, write commits) with timing, row count, and the executed SQL syntax-highlighted. Auto-follow keeps the newest entry pinned; scroll up or click a non-latest entry to pause. The active tab and open/closed state persist in tabs.json."
20225
22116
  ],
20226
22117
  [
20227
22118
  "Datastore explorers",
20228
- "Redis / Elasticsearch / S3 keep the same Multi-DB tab UI but swap the table tree for a key-space / index-tree / folder-tree explorer."
22119
+ "Redis / Elasticsearch / S3 keep the same Multi-DB tab UI but swap the table tree for a key-space / index-tree / folder-tree explorer. Each supports value editing, document/key/object creation, and deletion via in-pane editors and confirmation dialogs."
20229
22120
  ]
20230
22121
  ]
20231
22122
  }
@@ -20704,7 +22595,7 @@ code-viewer annotate add-db --db app.db --tab query \\
20704
22595
  database: {
20705
22596
  nav: "データストア",
20706
22597
  title: "データストアビューア",
20707
- intro: "SQLite ファイル、Docker 上のデータベース、Redis、Elasticsearch、S3 互換オブジェクトストアをローカルビューアで閲覧できます。",
22598
+ intro: "SQLite ファイル、Docker 上のデータベース、Redis、Elasticsearch、S3 互換オブジェクトストアをローカルビューアで閲覧・編集できます。",
20708
22599
  groups: [
20709
22600
  {
20710
22601
  title: "対応データストア",
@@ -20714,27 +22605,27 @@ code-viewer annotate add-db --db app.db --tab query \\
20714
22605
  rows: [
20715
22606
  [
20716
22607
  "SQLite",
20717
- "リポジトリ内の .db, .sqlite, .sqlite3, .s3db ファイルを自動検出します。"
22608
+ "リポジトリ内の .db, .sqlite, .sqlite3, .s3db ファイルを自動検出します。グリッドの Edit モードで行のインライン編集 / 追加 / 削除に対応 (コミット単位でアトミックに適用)。"
20718
22609
  ],
20719
22610
  [
20720
22611
  "MySQL / MariaDB",
20721
- "docker-compose.yml / compose.yml (.yaml 含む) のサービスから検出。同一サーバー上の複数データベースを一覧表示します。"
22612
+ "docker-compose.yml / compose.yml (.yaml 含む) のサービスから検出。同一サーバー上の複数データベースを一覧表示します。SQLite と同じく Edit モードで行のインライン編集 / 追加 / 削除に対応。"
20722
22613
  ],
20723
22614
  [
20724
22615
  "PostgreSQL",
20725
- "compose ファイルから検出。同一サーバー上の複数データベースに対応し、スキーマ切替セレクターで再オープンせずスキーマを切り替えられます。"
22616
+ "compose ファイルから検出。同一サーバー上の複数データベースに対応し、スキーマ切替セレクターで再オープンせずスキーマを切り替えられます。SQLite と同じく行のインライン編集 / 追加 / 削除に対応。"
20726
22617
  ],
20727
22618
  [
20728
22619
  "Redis",
20729
- "compose ファイルから検出。DB 0-15 を read-only で SCAN し、string/hash/list は専用ペイン、set/zset/stream は JSON ビュー。スナップショット/差分にも参加します。"
22620
+ "compose ファイルから検出。DB 0-15 を SCAN し、string/hash/list は専用ペイン、set/zset/stream は JSON ビュー。値の編集 / キー削除 / 新規キー作成 (全タイプ) に対応。スナップショット/差分にも参加します。"
20730
22621
  ],
20731
22622
  [
20732
22623
  "Elasticsearch",
20733
- "compose ファイルから検出。インデックス一覧、マッピング、search_after ページング、lucene q= と許可リスト経由の DSL、スナップショット/差分に対応します。"
22624
+ "compose ファイルから検出。インデックス一覧、マッピング、search_after ページング、lucene q= と許可リスト経由の DSL、スナップショット/差分に対応。_seq_no / _primary_term 楽観ロックでドキュメントの編集 / 新規作成 / 削除も可能。"
20734
22625
  ],
20735
22626
  [
20736
22627
  "S3 / MinIO / LocalStack",
20737
- "compose ファイルから検出。フォルダツリー型ブラウザ、prefix/ファイル名検索、更新日時順表示、画像/動画/音声/PDF/Markdown/HTML/テキストのプレビューに対応。LocalStack はホストポート未公開時 `docker exec curl` にフォールバックしますが、MinIO はホストポート公開が必須です。"
22628
+ "compose ファイルから検出。フォルダツリー型ブラウザ、prefix/ファイル名検索、更新日時順表示、画像/動画/音声/PDF/Markdown/HTML/テキストのプレビューに対応。テキスト/Markdown/JSON のインライン編集、新規オブジェクトアップロード、オブジェクト削除も可能。LocalStack はホストポート未公開時 `docker exec curl` にフォールバックしますが、MinIO はホストポート公開が必須です。"
20738
22629
  ]
20739
22630
  ]
20740
22631
  }
@@ -20756,7 +22647,7 @@ code-viewer annotate add-db --db app.db --tab query \\
20756
22647
  ],
20757
22648
  [
20758
22649
  "Data タブ",
20759
- "ページネーション付きグリッド。ソート、フィルター、セルコピー、CSV/JSON エクスポート(最大10万行、現在のフィルター/ソートを反映)。"
22650
+ "ページネーション付きグリッド。ソート、フィルター、セルコピー、CSV/JSON エクスポート(最大10万行、現在のフィルター/ソートを反映)。設定バーで Edit モードを ON にするとインライン編集が可能 — セルをダブルクリックで編集、行操作で挿入/削除を予約、コミット単位で一括適用。未コミットの編集行/セルは黄色でハイライト。"
20760
22651
  ],
20761
22652
  [
20762
22653
  "詳細フッタ・関連パネル",
@@ -20768,7 +22659,7 @@ code-viewer annotate add-db --db app.db --tab query \\
20768
22659
  ],
20769
22660
  [
20770
22661
  "クエリエディター",
20771
- "SQL シンタックスハイライト、Tab インデント、自動リサイズ、Ctrl+Enter で実行。許可文は DB 種別により異なります(SQLite: SELECT/PRAGMA/EXPLAIN/WITH。PostgreSQL と MySQL は SHOW/DESCRIBE も可)。"
22662
+ "SQL シンタックスハイライト (shiki)、Tab インデント、自動リサイズ、Ctrl+Enter で実行。許可文は DB 種別により異なります(SQLite: SELECT/PRAGMA/EXPLAIN/WITH。PostgreSQL と MySQL は SHOW/DESCRIBE も可)。"
20772
22663
  ],
20773
22664
  [
20774
22665
  "ER 図",
@@ -20783,12 +22674,12 @@ code-viewer annotate add-db --db app.db --tab query \\
20783
22674
  "選んだテーブル / インデックス / キー空間の状態を保存し、任意の 2 つの差分(insert / update / delete + before/after)を表示します。"
20784
22675
  ],
20785
22676
  [
20786
- "クエリ履歴",
20787
- "マスター/ディテール表示。各エントリは browser / CLI どちらから来たかを記録し、SSE でライブ更新されます。"
22677
+ "フッタ Dock (クエリ履歴 / ログ)",
22678
+ "JetBrains 風の常駐 bottom dock に 2 タブ。「クエリ履歴」: DB ごとに保存されたクエリのマスター/ディテール、SSE で全タブにライブ同期。「ログ」: このセッションでサーバが実行した SQL すべて (テーブル読み込み / ユーザークエリ / 編集コミット) を所要時間・行数・実行 SQL (シンタックスハイライト) 付きで時系列表示。自動追従 ON で常に最新を表示、下スクロール or 最新以外をクリックすると追従解除。アクティブタブと開閉状態は tabs.json に永続化されます。"
20788
22679
  ],
20789
22680
  [
20790
22681
  "データストア専用エクスプローラ",
20791
- "Redis / Elasticsearch / S3 はマルチ DB タブ UI を共有しつつ、テーブルツリーをキー空間ツリー / インデックスツリー / フォルダツリーに差し替えます。"
22682
+ "Redis / Elasticsearch / S3 はマルチ DB タブ UI を共有しつつ、テーブルツリーをキー空間ツリー / インデックスツリー / フォルダツリーに差し替えます。それぞれインライン編集ペインと確認ダイアログによる値編集 / ドキュメント・キー・オブジェクト作成 / 削除に対応。"
20792
22683
  ]
20793
22684
  ]
20794
22685
  }
@@ -23490,176 +25381,25 @@ code-viewer query diff rows --id diff-xyz789 --table users --type inserted --lim
23490
25381
  function closeRepoContextMenu() {
23491
25382
  document.querySelector(".gdp-context-menu")?.remove();
23492
25383
  }
23493
- function closeTrashDialog() {
23494
- document.querySelector(".gdp-trash-dialog-backdrop")?.remove();
23495
- }
23496
- function createTrashDialog(title, body, actions) {
23497
- closeTrashDialog();
23498
- const backdrop = document.createElement("div");
23499
- backdrop.className = "gdp-trash-dialog-backdrop";
23500
- const dialog = document.createElement("div");
23501
- dialog.className = "gdp-trash-dialog";
23502
- const titleId = "gdp-trash-dialog-title";
23503
- const bodyId = "gdp-trash-dialog-body";
23504
- dialog.setAttribute("role", "dialog");
23505
- dialog.setAttribute("aria-modal", "true");
23506
- dialog.setAttribute("aria-labelledby", titleId);
23507
- dialog.setAttribute("aria-describedby", bodyId);
23508
- const heading2 = document.createElement("div");
23509
- heading2.id = titleId;
23510
- heading2.className = "gdp-trash-dialog-title";
23511
- heading2.textContent = title;
23512
- const message = document.createElement("div");
23513
- message.id = bodyId;
23514
- message.className = "gdp-trash-dialog-body";
23515
- message.textContent = body;
23516
- const actionRow = document.createElement("div");
23517
- actionRow.className = "gdp-trash-dialog-actions";
23518
- actionRow.append(...actions);
23519
- dialog.append(heading2, message, actionRow);
23520
- backdrop.appendChild(dialog);
23521
- document.body.appendChild(backdrop);
23522
- return backdrop;
23523
- }
23524
25384
  function confirmMoveToTrash(path, focusReturnTarget) {
23525
- return new Promise((resolve) => {
23526
- const previousFocus = focusReturnTarget || document.activeElement;
23527
- const cancel = document.createElement("button");
23528
- cancel.type = "button";
23529
- cancel.className = "gdp-btn gdp-btn-sm";
23530
- cancel.textContent = "Cancel";
23531
- const move = document.createElement("button");
23532
- move.type = "button";
23533
- move.className = "gdp-btn gdp-btn-sm gdp-trash-dialog-danger";
23534
- move.textContent = "Move to Trash";
23535
- const done = (ok) => {
23536
- document.removeEventListener("keydown", onKeydown);
23537
- closeTrashDialog();
23538
- previousFocus?.focus?.();
23539
- resolve(ok);
23540
- };
23541
- const onKeydown = (event) => {
23542
- if (isImeComposing(event))
23543
- return;
23544
- if (event.key === "Escape") {
23545
- event.preventDefault();
23546
- event.stopPropagation();
23547
- done(false);
23548
- return;
23549
- }
23550
- if (event.key !== "Tab")
23551
- return;
23552
- const focusables = [cancel, move];
23553
- const index = focusables.indexOf(document.activeElement);
23554
- if (index < 0) {
23555
- event.preventDefault();
23556
- focusables[0].focus();
23557
- return;
23558
- }
23559
- if (event.shiftKey && index <= 0) {
23560
- event.preventDefault();
23561
- focusables[focusables.length - 1].focus();
23562
- } else if (!event.shiftKey && index === focusables.length - 1) {
23563
- event.preventDefault();
23564
- focusables[0].focus();
23565
- }
23566
- };
23567
- cancel.addEventListener("click", () => done(false));
23568
- move.addEventListener("click", () => done(true));
23569
- const backdrop = createTrashDialog("Move to Trash?", `Move "${path}" to Trash?`, [cancel, move]);
23570
- backdrop.addEventListener("pointerdown", (event) => {
23571
- if (event.target === backdrop)
23572
- done(false);
23573
- });
23574
- document.addEventListener("keydown", onKeydown);
23575
- cancel.focus();
25385
+ return showConfirmDialog({
25386
+ title: "Move to Trash?",
25387
+ body: `Move "${path}" to Trash?`,
25388
+ confirmLabel: "Move to Trash",
25389
+ danger: true,
25390
+ focusReturnTarget
23576
25391
  });
23577
25392
  }
23578
25393
  function askNewDirectoryName(path, focusReturnTarget) {
23579
- return new Promise((resolve) => {
23580
- const previousFocus = focusReturnTarget || document.activeElement;
23581
- const cancel = document.createElement("button");
23582
- cancel.type = "button";
23583
- cancel.className = "gdp-btn gdp-btn-sm";
23584
- cancel.textContent = "Cancel";
23585
- const create = document.createElement("button");
23586
- create.type = "button";
23587
- create.className = "gdp-btn gdp-btn-sm";
23588
- create.textContent = "Create";
23589
- const input = document.createElement("input");
23590
- input.className = "gdp-create-dir-input";
23591
- input.type = "text";
23592
- input.autocomplete = "off";
23593
- input.placeholder = "Folder name";
23594
- input.setAttribute("aria-label", "Folder name");
23595
- const error2 = document.createElement("div");
23596
- error2.className = "gdp-create-dir-error";
23597
- error2.setAttribute("role", "alert");
23598
- const syncValidity = () => {
23599
- const valid = !!normalizeNewDirectoryName(input.value);
23600
- create.disabled = !valid;
23601
- error2.textContent = input.value && !valid ? "Use a folder name without slashes, control characters, . or .." : "";
23602
- return valid;
23603
- };
23604
- const done = (name) => {
23605
- document.removeEventListener("keydown", onKeydown);
23606
- closeTrashDialog();
23607
- previousFocus?.focus?.();
23608
- resolve(name);
23609
- };
23610
- const submit = () => {
23611
- const name = normalizeNewDirectoryName(input.value);
23612
- if (!name) {
23613
- syncValidity();
23614
- input.focus();
23615
- return;
23616
- }
23617
- done(name);
23618
- };
23619
- const onKeydown = (event) => {
23620
- if (isImeComposing(event))
23621
- return;
23622
- if (event.key === "Escape") {
23623
- event.preventDefault();
23624
- event.stopPropagation();
23625
- done(null);
23626
- return;
23627
- }
23628
- if (event.key === "Enter") {
23629
- event.preventDefault();
23630
- submit();
23631
- return;
23632
- }
23633
- if (event.key !== "Tab")
23634
- return;
23635
- const focusables = [input, cancel, create];
23636
- const index = focusables.indexOf(document.activeElement);
23637
- if (index < 0) {
23638
- event.preventDefault();
23639
- focusables[0].focus();
23640
- return;
23641
- }
23642
- if (event.shiftKey && index <= 0) {
23643
- event.preventDefault();
23644
- focusables[focusables.length - 1].focus();
23645
- } else if (!event.shiftKey && index === focusables.length - 1) {
23646
- event.preventDefault();
23647
- focusables[0].focus();
23648
- }
23649
- };
23650
- cancel.addEventListener("click", () => done(null));
23651
- create.addEventListener("click", submit);
23652
- input.addEventListener("input", syncValidity);
23653
- create.disabled = true;
23654
- const backdrop = createTrashDialog("New Folder", `Create a folder in "${path || getProjectName() || "repository"}".`, [cancel, create]);
23655
- const body = backdrop.querySelector(".gdp-trash-dialog-body");
23656
- body?.append(input, error2);
23657
- backdrop.addEventListener("pointerdown", (event) => {
23658
- if (event.target === backdrop)
23659
- done(null);
23660
- });
23661
- document.addEventListener("keydown", onKeydown);
23662
- input.focus();
25394
+ return showPromptDialog({
25395
+ title: "New Folder",
25396
+ body: `Create a folder in "${path || getProjectName() || "repository"}".`,
25397
+ placeholder: "Folder name",
25398
+ ariaLabel: "Folder name",
25399
+ confirmLabel: "Create",
25400
+ validate: (v) => normalizeNewDirectoryName(v),
25401
+ invalidMessage: "Use a folder name without slashes, control characters, . or ..",
25402
+ focusReturnTarget
23663
25403
  });
23664
25404
  }
23665
25405
  async function requestCreateDirectory(path, onCreated, options = {}) {
@@ -23730,7 +25470,7 @@ code-viewer query diff rows --id diff-xyz789 --table users --type inserted --lim
23730
25470
  return button;
23731
25471
  }
23732
25472
  function showRepoContextMenu(event, entry, ref, onChanged) {
23733
- if (document.querySelector(".gdp-trash-dialog-backdrop"))
25473
+ if (document.querySelector(".gdp-dialog-backdrop"))
23734
25474
  return false;
23735
25475
  if (!canTrashWorktreeRef(ref))
23736
25476
  return false;
@@ -24364,22 +26104,10 @@ code-viewer query diff rows --id diff-xyz789 --table users --type inserted --lim
24364
26104
  }
24365
26105
  let creatingDirectory = false;
24366
26106
  function showTrashError(message) {
24367
- const ok = document.createElement("button");
24368
- ok.type = "button";
24369
- ok.className = "gdp-btn gdp-btn-sm";
24370
- ok.textContent = "OK";
24371
- ok.addEventListener("click", closeTrashDialog);
24372
- createTrashDialog("Trash failed", message, [ok]);
24373
- ok.focus();
26107
+ showAlertDialog({ title: "Trash failed", body: message });
24374
26108
  }
24375
26109
  function showCreateDirectoryError(message) {
24376
- const ok = document.createElement("button");
24377
- ok.type = "button";
24378
- ok.className = "gdp-btn gdp-btn-sm";
24379
- ok.textContent = "OK";
24380
- ok.addEventListener("click", closeTrashDialog);
24381
- createTrashDialog("New folder failed", message, [ok]);
24382
- ok.focus();
26110
+ showAlertDialog({ title: "New folder failed", body: message });
24383
26111
  }
24384
26112
  async function moveRepoPathToTrash(path) {
24385
26113
  const res = await fetch("/_trash_path", {
@@ -24404,7 +26132,12 @@ code-viewer query diff rows --id diff-xyz789 --table users --type inserted --lim
24404
26132
  if (!list2.length)
24405
26133
  return;
24406
26134
  const label = path || getProjectName() || "repository root";
24407
- if (!window.confirm("Upload " + list2.length + " file" + (list2.length === 1 ? "" : "s") + " into " + label + "?"))
26135
+ const ok = await showConfirmDialog({
26136
+ title: "Upload files?",
26137
+ body: `Upload ${list2.length} file${list2.length === 1 ? "" : "s"} into ${label}?`,
26138
+ confirmLabel: "Upload"
26139
+ });
26140
+ if (!ok)
24408
26141
  return;
24409
26142
  const form = new FormData;
24410
26143
  form.set("dir", path);
@@ -27440,6 +29173,11 @@ code-viewer query diff rows --id diff-xyz789 --table users --type inserted --lim
27440
29173
  uploadsTitle: "Uploads",
27441
29174
  uploadEnabledLabel: "Allow file uploads into worktree folders",
27442
29175
  uploadEnabledHelp: "Disable to make the worktree read-only for everyone using this server.",
29176
+ datastoreTitle: "Datastores",
29177
+ datastoreInferFkLabel: "Infer FK from Rails-style naming (<name>_id → <names>.id)",
29178
+ datastoreInferFkHelp: "Show inferred foreign-key links in the related-data panel for SQL tables.",
29179
+ datastoreS3TooltipLabel: "Show S3 object preview tooltip on hover",
29180
+ datastoreS3TooltipHelp: "Hovering an S3 object row shows the full key path and a content preview.",
27443
29181
  watchTitle: "File change watcher",
27444
29182
  watchLimit: "Maximum directories to watch",
27445
29183
  watchLimitHelp: (defaultLimit) => `Higher values reduce missed updates in deep trees at the cost of file handles. Combine with the Skip list above to keep heavy folders (node_modules, .git, dist...) out of the watch budget. Default: ${defaultLimit}.`
@@ -27541,6 +29279,11 @@ code-viewer query diff rows --id diff-xyz789 --table users --type inserted --lim
27541
29279
  uploadsTitle: "アップロード",
27542
29280
  uploadEnabledLabel: "ワークツリーへのファイルアップロードを許可する",
27543
29281
  uploadEnabledHelp: "オフにすると、このサーバを使う全員に対してワークツリーは読み取り専用になります。",
29282
+ datastoreTitle: "データストア",
29283
+ datastoreInferFkLabel: "Rails 命名規約 (<name>_id → <names>.id) から FK を推測",
29284
+ datastoreInferFkHelp: "SQL テーブルの関連データパネルに Rails 命名規約由来の仮想 FK リンクを表示します。",
29285
+ datastoreS3TooltipLabel: "S3 オブジェクトの hover プレビューを表示",
29286
+ datastoreS3TooltipHelp: "S3 オブジェクト行にホバーすると、完全な key とコンテンツプレビューを表示します。",
27544
29287
  watchTitle: "ファイル変更の監視",
27545
29288
  watchLimit: "監視するディレクトリ数の上限",
27546
29289
  watchLimitHelp: (defaultLimit) => `値を大きくすると深いツリーの変更を取りこぼしにくくなりますが、ファイルハンドル数を消費します。上の Skip リストと併用すると、重いフォルダ(node_modules, .git, dist など)を監視枠から外せます。既定値: ${defaultLimit}。`
@@ -27662,7 +29405,13 @@ code-viewer query diff rows --id diff-xyz789 --table users --type inserted --lim
27662
29405
  if (settingsSections[2])
27663
29406
  settingsSections[2].textContent = text2.settings.excludedDirectories;
27664
29407
  if (settingsSections[3])
27665
- settingsSections[3].textContent = text2.settings.watchTitle;
29408
+ settingsSections[3].textContent = text2.settings.datastoreTitle;
29409
+ if (settingsSections[4])
29410
+ settingsSections[4].textContent = text2.settings.watchTitle;
29411
+ setElementText("#datastore-infer-fk-label", text2.settings.datastoreInferFkLabel);
29412
+ setElementText("#datastore-infer-fk-help", text2.settings.datastoreInferFkHelp);
29413
+ setElementText("#datastore-s3-tooltip-label", text2.settings.datastoreS3TooltipLabel);
29414
+ setElementText("#datastore-s3-tooltip-help", text2.settings.datastoreS3TooltipHelp);
27666
29415
  setElementText("#upload-enabled-label", text2.settings.uploadEnabledLabel);
27667
29416
  setElementText("#upload-help", text2.settings.uploadEnabledHelp);
27668
29417
  setElementText("#scope-omit-dirs-help", text2.settings.omitDirsHelp);
@@ -27874,6 +29623,7 @@ code-viewer query diff rows --id diff-xyz789 --table users --type inserted --lim
27874
29623
  const uploadToggle = document.querySelector("#upload-enabled");
27875
29624
  if (uploadToggle)
27876
29625
  uploadToggle.checked = APP_SETTINGS.uploadEnabled !== false;
29626
+ syncDatastoreToggles();
27877
29627
  source.textContent = uiText().settings.scopeSource(PROJECT_NAME || "default", scopeOmitSourceLabel());
27878
29628
  pop.hidden = false;
27879
29629
  viewerLanguage.focus();
@@ -28491,6 +30241,22 @@ code-viewer query diff rows --id diff-xyz789 --table users --type inserted --lim
28491
30241
  $("#upload-enabled")?.addEventListener("change", (event) => {
28492
30242
  saveUploadEnabled(event.currentTarget.checked);
28493
30243
  });
30244
+ $("#datastore-infer-fk")?.addEventListener("change", (event) => {
30245
+ DATABASE_VIEW.setDbUiPref("inferFkRails", event.currentTarget.checked);
30246
+ });
30247
+ $("#datastore-s3-tooltip")?.addEventListener("change", (event) => {
30248
+ DATABASE_VIEW.setDbUiPref("s3TooltipEnabled", event.currentTarget.checked);
30249
+ });
30250
+ function syncDatastoreToggles() {
30251
+ const inferToggle = document.querySelector("#datastore-infer-fk");
30252
+ if (inferToggle) {
30253
+ inferToggle.checked = DATABASE_VIEW.getDbUiPref("inferFkRails", false);
30254
+ }
30255
+ const tooltipToggle = document.querySelector("#datastore-s3-tooltip");
30256
+ if (tooltipToggle) {
30257
+ tooltipToggle.checked = DATABASE_VIEW.getDbUiPref("s3TooltipEnabled", true);
30258
+ }
30259
+ }
28494
30260
  $("#scope-omit-dirs")?.addEventListener("change", (event) => {
28495
30261
  saveScopeOmitDirsField(event.currentTarget.value);
28496
30262
  });
@@ -28912,6 +30678,7 @@ code-viewer query diff rows --id diff-xyz789 --table users --type inserted --lim
28912
30678
  if (!isCurrentDiffRequest())
28913
30679
  return null;
28914
30680
  const result = renderShell(data, options.changedPaths);
30681
+ applyHideTestsToMeta();
28915
30682
  setStatus("live");
28916
30683
  return result;
28917
30684
  }).catch(() => {
@@ -29022,6 +30789,7 @@ code-viewer query diff rows --id diff-xyz789 --table users --type inserted --lim
29022
30789
  });
29023
30790
  },
29024
30791
  getSyntaxHighlight: () => STATE.syntaxHighlight,
30792
+ getLanguage: () => STATE.language,
29025
30793
  trackLoad
29026
30794
  });
29027
30795
  const DATABASE_VIEW = createDatabaseView({
@@ -29033,6 +30801,11 @@ code-viewer query diff rows --id diff-xyz789 --table users --type inserted --lim
29033
30801
  getLanguage: () => STATE.language
29034
30802
  });
29035
30803
  relocalizeDatabase = () => DATABASE_VIEW.localize();
30804
+ DATABASE_VIEW.onDbUiPrefChange(() => {
30805
+ if (!document.querySelector("#scope-settings-popover")?.hidden) {
30806
+ syncDatastoreToggles();
30807
+ }
30808
+ });
29036
30809
  const REF_PICKER = createRefPicker({
29037
30810
  $,
29038
30811
  escapeHtml: escapeHtml3,
@@ -29203,6 +30976,28 @@ code-viewer query diff rows --id diff-xyz789 --table users --type inserted --lim
29203
30976
  updateTreeDirVisibility();
29204
30977
  if (typeof applyViewedState === "function")
29205
30978
  applyViewedState();
30979
+ applyHideTestsToMeta();
30980
+ }
30981
+ function applyHideTestsToMeta() {
30982
+ const meta = window._lastMeta;
30983
+ if (!meta || !meta.totals)
30984
+ return;
30985
+ const effective = STATE.hideTests && !isRepositorySidebarMode();
30986
+ if (!effective) {
30987
+ renderMeta(meta);
30988
+ return;
30989
+ }
30990
+ let additions = 0;
30991
+ let deletions = 0;
30992
+ let files = 0;
30993
+ for (const f2 of STATE.files) {
30994
+ if (TEST_RE.test(f2.path || ""))
30995
+ continue;
30996
+ additions += f2.additions || 0;
30997
+ deletions += f2.deletions || 0;
30998
+ files += 1;
30999
+ }
31000
+ renderMeta({ ...meta, totals: { files, additions, deletions } });
29206
31001
  }
29207
31002
  applyHideTests();
29208
31003
  $("#hide-tests").addEventListener("click", () => {