@youtyan/code-viewer 0.8.9 → 0.9.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@youtyan/code-viewer",
3
- "version": "0.8.9",
3
+ "version": "0.9.0",
4
4
  "description": "Local browser-based code and git diff viewer",
5
5
  "type": "module",
6
6
  "bin": {
@@ -41,7 +41,9 @@ browser's Database > Search tab, so the human can review the same workflow.
41
41
  the sidebar — it lists every SQLite file plus PostgreSQL / MySQL / Redis
42
42
  / Elasticsearch / S3 service that any nearby `docker-compose` exposes, as
43
43
  well as any local Supabase CLI (`supabase start`) project found via a
44
- `supabase/config.toml` (id form `supabase:<project_id>`).
44
+ `supabase/config.toml` (id form `supabase:<project_id>`) and any saved
45
+ connection added from the browser UI (id form `connection:<uuid>`,
46
+ including Cloudflare D1 and R2).
45
47
  Use the printed `id` as `--db` on every other command. Credentials and
46
48
  internal config are stripped server-side.
47
49
 
package/web/app.js CHANGED
@@ -7669,15 +7669,24 @@ ${lines.join(`
7669
7669
  function markdownSlugify(text2) {
7670
7670
  return text2.trim().toLowerCase().replace(/[\s ]+/g, "-").replace(/[^\p{L}\p{N}\-_]/gu, "").slice(0, 80) || "section";
7671
7671
  }
7672
- function resolveMarkdownRelativePath(currentPath, href) {
7672
+ function resolveMarkdownLinkTarget(currentPath, href) {
7673
7673
  if (!href || href.startsWith("#"))
7674
7674
  return null;
7675
7675
  if (/^(?:[a-z][a-z0-9+.-]*:|\/\/)/i.test(href))
7676
7676
  return null;
7677
- const cleanHref = href.replace(/[?#].*$/, "");
7678
- if (!/\.(md|markdown|mdown|mkd|mkdn|mdx)$/i.test(cleanHref))
7677
+ const hashAt = href.indexOf("#");
7678
+ const hash = hashAt < 0 ? "" : href.slice(hashAt + 1);
7679
+ const cleanHref = (hashAt < 0 ? href : href.slice(0, hashAt)).replace(/\?.*$/, "");
7680
+ if (!cleanHref)
7679
7681
  return null;
7680
- return resolveRepoRelative(currentPath, decodeURIComponent(cleanHref));
7682
+ const path = resolveRepoRelative(currentPath, decodeUriComponentSafe(cleanHref));
7683
+ if (path == null)
7684
+ return null;
7685
+ return {
7686
+ path,
7687
+ hash: decodeUriComponentSafe(hash),
7688
+ directory: cleanHref.endsWith("/")
7689
+ };
7681
7690
  }
7682
7691
  function resolveMarkdownAssetPath(currentPath, src) {
7683
7692
  if (!src || src.startsWith("#") || /^(?:[a-z][a-z0-9+.-]*:|\/\/)/i.test(src))
@@ -7685,6 +7694,13 @@ ${lines.join(`
7685
7694
  const cleanSrc = src.split(/[?#]/, 1)[0];
7686
7695
  return resolveRepoRelative(currentPath, cleanSrc);
7687
7696
  }
7697
+ function decodeUriComponentSafe(value) {
7698
+ try {
7699
+ return decodeURIComponent(value);
7700
+ } catch {
7701
+ return value;
7702
+ }
7703
+ }
7688
7704
  function resolveRepoRelative(currentPath, requestedPath) {
7689
7705
  const base2 = currentPath.split("/").slice(0, -1);
7690
7706
  const parts = [
@@ -7780,11 +7796,15 @@ ${lines.join(`
7780
7796
  md.renderer.rules.link_open = (tokens, idx, options, env, self) => {
7781
7797
  const token = tokens[idx];
7782
7798
  const href = token.attrGet("href") || "";
7783
- const mdPath = resolveMarkdownRelativePath(target.path, href);
7784
- if (mdPath) {
7799
+ const link2 = resolveMarkdownLinkTarget(target.path, href);
7800
+ if (link2) {
7785
7801
  token.attrSet("href", "#");
7786
- token.attrSet("data-gdp-md-link", mdPath);
7802
+ token.attrSet("data-gdp-md-link", link2.path);
7787
7803
  token.attrSet("data-gdp-md-ref", target.ref || "worktree");
7804
+ if (link2.hash)
7805
+ token.attrSet("data-gdp-md-hash", link2.hash);
7806
+ if (link2.directory)
7807
+ token.attrSet("data-gdp-md-dir", "1");
7788
7808
  } else if (/^(?:https?:)?\/\//i.test(href)) {
7789
7809
  token.attrSet("target", "_blank");
7790
7810
  token.attrSet("rel", "noopener noreferrer");
@@ -7899,11 +7919,15 @@ ${frontmatter.yaml}
7899
7919
  if (!link2)
7900
7920
  return;
7901
7921
  const path = link2.dataset.gdpMdLink;
7902
- const ref = link2.dataset.gdpMdRef || target.ref;
7903
- if (!path)
7922
+ if (path == null)
7904
7923
  return;
7905
7924
  e2.preventDefault();
7906
- options.onNavigateMarkdown?.(path, ref);
7925
+ options.onNavigateMarkdown?.({
7926
+ path,
7927
+ ref: link2.dataset.gdpMdRef || target.ref,
7928
+ hash: link2.dataset.gdpMdHash || "",
7929
+ directory: link2.dataset.gdpMdDir === "1"
7930
+ });
7907
7931
  });
7908
7932
  setupMarkdownScrollSpy(root);
7909
7933
  setupMermaidLightbox(root);
@@ -7986,12 +8010,7 @@ ${frontmatter.yaml}
7986
8010
  scrollMarkdownSectionIntoView(section, "auto");
7987
8011
  }
7988
8012
  function decodeHashFragment(hash) {
7989
- const value = hash.startsWith("#") ? hash.slice(1) : hash;
7990
- try {
7991
- return decodeURIComponent(value);
7992
- } catch {
7993
- return value;
7994
- }
8013
+ return decodeUriComponentSafe(hash.startsWith("#") ? hash.slice(1) : hash);
7995
8014
  }
7996
8015
  function scrollMarkdownSectionIntoView(section, behavior) {
7997
8016
  const top = section.getBoundingClientRect().top + window.scrollY - markdownAnchorOffset() - 12;
@@ -10309,11 +10328,21 @@ ${frontmatter.yaml}
10309
10328
  var KINDS = [
10310
10329
  "postgresql",
10311
10330
  "mysql",
10331
+ "d1",
10312
10332
  "redis",
10313
10333
  "elasticsearch",
10314
10334
  "s3",
10315
10335
  "dynamodb"
10316
10336
  ];
10337
+ var S3_PROVIDERS = ["custom", "r2"];
10338
+ var R2_ENDPOINT_RE = /^https:\/\/([a-z0-9-]+)\.r2\.cloudflarestorage\.com$/i;
10339
+ var R2_REGION = "auto";
10340
+ function r2EndpointFor(accountId) {
10341
+ return `https://${accountId}.r2.cloudflarestorage.com`;
10342
+ }
10343
+ function r2AccountIdFromEndpoint(endpoint) {
10344
+ return endpoint ? R2_ENDPOINT_RE.exec(endpoint)?.[1] ?? null : null;
10345
+ }
10317
10346
  function text2(language) {
10318
10347
  return language === "ja" ? {
10319
10348
  titleAdd: "データストア接続を追加",
@@ -10327,6 +10356,12 @@ ${frontmatter.yaml}
10327
10356
  password: "パスワード",
10328
10357
  database: "データベース",
10329
10358
  schema: "既定スキーマ(任意)",
10359
+ provider: "プロバイダ",
10360
+ providerCustom: "S3 互換(AWS / MinIO / LocalStack)",
10361
+ providerR2: "Cloudflare R2",
10362
+ accountId: "Cloudflare アカウント ID",
10363
+ databaseId: "データベース ID",
10364
+ apiToken: "API トークン",
10330
10365
  endpoint: "エンドポイント URL",
10331
10366
  region: "リージョン",
10332
10367
  accessKeyId: "アクセスキー ID",
@@ -10345,7 +10380,9 @@ ${frontmatter.yaml}
10345
10380
  requestFailed: "接続情報を保存できませんでした",
10346
10381
  deleteTitle: "保存済み接続を削除",
10347
10382
  deleteBody: "この接続情報を削除します。タブやスナップショットのデータは削除されません。",
10348
- delete: "削除"
10383
+ delete: "削除",
10384
+ secretsLeftTitle: "資格情報がキーチェーンに残りました",
10385
+ secretsLeftBody: "接続は削除しましたが、キーチェーンから資格情報を削除できませんでした。キーチェーンがロックされている可能性があります。ロックを解除して「キーチェーンアクセス」から code-viewer の項目を削除してください。"
10349
10386
  } : {
10350
10387
  titleAdd: "Add datastore connection",
10351
10388
  titleEdit: "Edit datastore connection",
@@ -10358,6 +10395,12 @@ ${frontmatter.yaml}
10358
10395
  password: "Password",
10359
10396
  database: "Database",
10360
10397
  schema: "Default schema (optional)",
10398
+ provider: "Provider",
10399
+ providerCustom: "S3-compatible (AWS / MinIO / LocalStack)",
10400
+ providerR2: "Cloudflare R2",
10401
+ accountId: "Cloudflare account ID",
10402
+ databaseId: "Database ID",
10403
+ apiToken: "API token",
10361
10404
  endpoint: "Endpoint URL",
10362
10405
  region: "Region",
10363
10406
  accessKeyId: "Access key ID",
@@ -10376,7 +10419,9 @@ ${frontmatter.yaml}
10376
10419
  requestFailed: "Failed to save the connection",
10377
10420
  deleteTitle: "Delete saved connection",
10378
10421
  deleteBody: "This removes the saved connection. Tabs and snapshot data are not deleted.",
10379
- delete: "Delete"
10422
+ delete: "Delete",
10423
+ secretsLeftTitle: "Credentials left in the keychain",
10424
+ secretsLeftBody: "The connection was removed, but its credentials could not be deleted from the keychain — it may be locked. Unlock it and remove the code-viewer item from Keychain Access."
10380
10425
  };
10381
10426
  }
10382
10427
  function field(labelText, input, required = false, requiredLabel = "Required") {
@@ -10440,6 +10485,21 @@ ${frontmatter.yaml}
10440
10485
  database.value = current?.database ?? "";
10441
10486
  const schema = input();
10442
10487
  schema.value = current?.schema ?? "";
10488
+ const provider = document.createElement("select");
10489
+ for (const value of S3_PROVIDERS) {
10490
+ const option = document.createElement("option");
10491
+ option.value = value;
10492
+ option.textContent = value === "r2" ? labels.providerR2 : labels.providerCustom;
10493
+ provider.appendChild(option);
10494
+ }
10495
+ const currentR2AccountId = r2AccountIdFromEndpoint(current?.endpoint);
10496
+ provider.value = currentR2AccountId ? "r2" : "custom";
10497
+ const accountId = input();
10498
+ accountId.value = current?.accountId ?? currentR2AccountId ?? "";
10499
+ const databaseId = input();
10500
+ databaseId.value = current?.databaseId ?? "";
10501
+ const apiToken = input("password");
10502
+ apiToken.autocomplete = "new-password";
10443
10503
  const endpoint = input("url");
10444
10504
  endpoint.value = current?.endpoint ?? "http://127.0.0.1:9200";
10445
10505
  const region = input();
@@ -10461,6 +10521,10 @@ ${frontmatter.yaml}
10461
10521
  password: field(labels.password, password),
10462
10522
  database: field(labels.database, database, true, labels.requiredField),
10463
10523
  schema: field(labels.schema, schema),
10524
+ provider: field(labels.provider, provider),
10525
+ accountId: field(labels.accountId, accountId, true, labels.requiredField),
10526
+ databaseId: field(labels.databaseId, databaseId, true, labels.requiredField),
10527
+ apiToken: field(labels.apiToken, apiToken, true, labels.requiredField),
10464
10528
  endpoint: field(labels.endpoint, endpoint, true, labels.requiredField),
10465
10529
  region: field(labels.region, region, true, labels.requiredField),
10466
10530
  accessKeyId: field(labels.accessKeyId, accessKeyId, true, labels.requiredField),
@@ -10469,21 +10533,28 @@ ${frontmatter.yaml}
10469
10533
  tls: field(labels.tls, tls)
10470
10534
  };
10471
10535
  form.append(...Object.values(rows));
10536
+ const isR2 = () => kind.value === "s3" && provider.value === "r2";
10472
10537
  const syncKind = () => {
10473
10538
  const value = kind.value;
10474
10539
  const sql = value === "postgresql" || value === "mysql";
10475
10540
  const redis = value === "redis";
10476
10541
  const http = value === "elasticsearch";
10477
10542
  const aws = value === "s3" || value === "dynamodb";
10543
+ const d1 = value === "d1";
10544
+ const r2 = isR2();
10478
10545
  rows.host.hidden = !sql && !redis;
10479
10546
  rows.port.hidden = !sql && !redis;
10480
10547
  rows.user.hidden = !sql;
10481
10548
  rows.username.hidden = !redis && !http;
10482
- rows.password.hidden = aws;
10549
+ rows.password.hidden = aws || d1;
10483
10550
  rows.database.hidden = !sql;
10484
10551
  rows.schema.hidden = value !== "postgresql";
10485
- rows.endpoint.hidden = !http && !aws;
10486
- rows.region.hidden = !aws;
10552
+ rows.provider.hidden = value !== "s3";
10553
+ rows.accountId.hidden = !d1 && !r2;
10554
+ rows.databaseId.hidden = !d1;
10555
+ rows.apiToken.hidden = !d1;
10556
+ rows.endpoint.hidden = !http && !aws || r2;
10557
+ rows.region.hidden = !aws || r2;
10487
10558
  rows.accessKeyId.hidden = !aws;
10488
10559
  rows.secretAccessKey.hidden = !aws;
10489
10560
  rows.sessionToken.hidden = !aws;
@@ -10498,8 +10569,11 @@ ${frontmatter.yaml}
10498
10569
  if (!current && (http || aws)) {
10499
10570
  endpoint.value = value === "elasticsearch" ? "http://127.0.0.1:9200" : "http://127.0.0.1:4566";
10500
10571
  }
10572
+ if (!current && aws)
10573
+ region.value = r2 ? R2_REGION : "us-east-1";
10501
10574
  };
10502
10575
  kind.addEventListener("change", syncKind);
10576
+ provider.addEventListener("change", syncKind);
10503
10577
  syncKind();
10504
10578
  const validateConnection = () => {
10505
10579
  const value = kind.value;
@@ -10516,10 +10590,16 @@ ${frontmatter.yaml}
10516
10590
  if ((value === "postgresql" || value === "mysql") && (!user.value.trim() || !database.value.trim())) {
10517
10591
  return labels.required;
10518
10592
  }
10519
- if ((value === "elasticsearch" || value === "s3" || value === "dynamodb") && !endpoint.value.trim()) {
10593
+ if (value === "d1" && (!accountId.value.trim() || !databaseId.value.trim() || !apiToken.value)) {
10594
+ return labels.required;
10595
+ }
10596
+ if (isR2() && !accountId.value.trim()) {
10597
+ return labels.required;
10598
+ }
10599
+ if ((value === "elasticsearch" || value === "s3" && !isR2() || value === "dynamodb") && !endpoint.value.trim()) {
10520
10600
  return labels.required;
10521
10601
  }
10522
- if ((value === "s3" || value === "dynamodb") && (!region.value.trim() || !accessKeyId.value.trim())) {
10602
+ if ((value === "s3" || value === "dynamodb") && (!isR2() && !region.value.trim() || !accessKeyId.value.trim())) {
10523
10603
  return labels.required;
10524
10604
  }
10525
10605
  return null;
@@ -10555,10 +10635,16 @@ ${frontmatter.yaml}
10555
10635
  ...username.value.trim() || !current ? { username: username.value.trim() || undefined } : {},
10556
10636
  ...password.value || !current ? { password: password.value } : {}
10557
10637
  });
10638
+ } else if (value === "d1") {
10639
+ Object.assign(payload, {
10640
+ accountId: accountId.value.trim(),
10641
+ databaseId: databaseId.value.trim(),
10642
+ apiToken: apiToken.value
10643
+ });
10558
10644
  } else {
10559
10645
  Object.assign(payload, {
10560
- endpoint: endpoint.value.trim(),
10561
- region: region.value.trim(),
10646
+ endpoint: isR2() ? r2EndpointFor(accountId.value.trim()) : endpoint.value.trim(),
10647
+ region: isR2() ? R2_REGION : region.value.trim(),
10562
10648
  ...accessKeyId.value.trim() || !current ? { accessKeyId: accessKeyId.value.trim() } : {},
10563
10649
  ...secretAccessKey.value || !current ? { secretAccessKey: secretAccessKey.value } : {},
10564
10650
  ...sessionToken.value ? { sessionToken: sessionToken.value } : {}
@@ -10668,6 +10754,13 @@ ${frontmatter.yaml}
10668
10754
  }));
10669
10755
  if (!response.ok)
10670
10756
  throw new Error(await response.text() || labels.requestFailed);
10757
+ const body = await response.json().catch(() => ({}));
10758
+ if (body.secretsRemoved === false) {
10759
+ await showAlertDialog({
10760
+ title: labels.secretsLeftTitle,
10761
+ body: labels.secretsLeftBody
10762
+ });
10763
+ }
10671
10764
  return true;
10672
10765
  }
10673
10766
 
@@ -20343,8 +20436,22 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
20343
20436
  function isAbortError2(err) {
20344
20437
  return err instanceof DOMException && err.name === "AbortError" || err instanceof Error && err.name === "AbortError";
20345
20438
  }
20439
+ var SQL_KINDS = new Set([
20440
+ "sqlite",
20441
+ "postgresql",
20442
+ "mysql",
20443
+ "d1"
20444
+ ]);
20445
+ var ROW_EDITABLE_KINDS = new Set([
20446
+ "sqlite",
20447
+ "postgresql",
20448
+ "mysql"
20449
+ ]);
20346
20450
  function isSqlKind(kind) {
20347
- return kind === "sqlite" || kind === "postgresql" || kind === "mysql";
20451
+ return !!kind && SQL_KINDS.has(kind);
20452
+ }
20453
+ function isRowEditableKind(kind) {
20454
+ return !!kind && ROW_EDITABLE_KINDS.has(kind);
20348
20455
  }
20349
20456
  function isPostgresKind(kind) {
20350
20457
  return kind === "postgresql";
@@ -20700,7 +20807,7 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
20700
20807
  cb.onStateChange();
20701
20808
  },
20702
20809
  getText: () => paneText(),
20703
- getEditable: () => isSqlKind(currentDbInfo?.kind),
20810
+ getEditable: () => isRowEditableKind(currentDbInfo?.kind),
20704
20811
  applyMutations: (mutations) => applyRowMutations(mutations),
20705
20812
  onRefreshComplete: ({ table: table2, filters }) => {
20706
20813
  if (filters.length === 0) {
@@ -26304,6 +26411,10 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
26304
26411
  kind: "paragraph",
26305
26412
  text: "A file detail page exposes up to four tabs — Preview, Code, Blame, History. For text files Code is the default and ?preview=1 opts in to the Markdown / HTML preview; media files (images, video, audio, PDF) show a Preview tab only, with no Code tab. Blame and History have their own canonical URLs (view=blame, view=history) so deep links and the browser back/forward stay in sync, and they keep the Repository sidebar visible. Opening another file from the repository tree keeps the active tab (a file that cannot be previewed falls back to Code)."
26306
26413
  },
26414
+ {
26415
+ kind: "paragraph",
26416
+ text: "Relative links inside a Markdown preview lead to the same destinations as they do on GitHub: another Markdown file opens its file page, an #anchor opens the preview and scrolls to that heading, a non-Markdown file opens in the Code view, and a link to a directory opens that folder in the repository tree."
26417
+ },
26307
26418
  {
26308
26419
  kind: "paragraph",
26309
26420
  text: "When the repository remote is on GitHub, repository and file headers can open the current path there. Selecting source lines also exposes separate actions to copy the AI reference, open the exact GitHub line range, or copy its URL. Markdown and HTML diff cards include a direct Preview shortcut."
@@ -26555,7 +26666,7 @@ code-viewer annotate add-db --db app.db --tab query \\
26555
26666
  database: {
26556
26667
  nav: "Datastores",
26557
26668
  title: "Datastore Viewer",
26558
- intro: "Browse SQLite files, Docker-hosted databases, Redis, Elasticsearch, DynamoDB, and S3-compatible object stores from one local viewer.",
26669
+ intro: "Browse SQLite files, Docker-hosted databases, Cloudflare D1, Redis, Elasticsearch, DynamoDB, and S3-compatible object stores (including Cloudflare R2) from one local viewer.",
26559
26670
  groups: [
26560
26671
  {
26561
26672
  title: "Supported datastores",
@@ -26565,7 +26676,7 @@ code-viewer annotate add-db --db app.db --tab query \\
26565
26676
  rows: [
26566
26677
  [
26567
26678
  "Saved connections",
26568
- "Use + beside the datastore selector to add an arbitrary PostgreSQL, MySQL, Redis, Elasticsearch, S3-compatible, or DynamoDB endpoint. Required fields are marked, and Test connection verifies the current values before saving. Drivers are included, so no database CLI or curl is required. Non-secret settings are saved locally; authentication values stay in server memory and must be entered again after a restart."
26679
+ "Use + beside the datastore selector to add an arbitrary PostgreSQL, MySQL, Cloudflare D1, Redis, Elasticsearch, S3-compatible (including a Cloudflare R2 preset), or DynamoDB endpoint. Required fields are marked, and Test connection verifies the current values before saving. Drivers are included, so no database CLI or curl is required. Non-secret settings are saved locally; credentials are never written into the repository — on macOS they are kept in the Keychain so they survive a restart, and elsewhere they stay in server memory and must be entered again."
26569
26680
  ],
26570
26681
  [
26571
26682
  "SQLite",
@@ -26579,6 +26690,10 @@ code-viewer annotate add-db --db app.db --tab query \\
26579
26690
  "PostgreSQL",
26580
26691
  "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. Local Supabase CLI (`supabase start`) projects are also auto-discovered from `supabase/config.toml`, without needing a docker-compose file."
26581
26692
  ],
26693
+ [
26694
+ "Cloudflare D1",
26695
+ "Added as a saved connection with an account ID, database ID, and API token (needs D1:Read). Browsed over the D1 REST API and reuses the SQL screens — table list, row grid, query editor, schema, ER diagram, snapshots and diffs. Read-only: the query editor accepts SELECT / PRAGMA / EXPLAIN / WITH only, and grid Edit mode is not offered."
26696
+ ],
26582
26697
  [
26583
26698
  "Redis",
26584
26699
  "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."
@@ -26592,8 +26707,8 @@ code-viewer annotate add-db --db app.db --tab query \\
26592
26707
  "Detected when DynamoDB is enabled on a LocalStack compose service. List tables, browse a Structure tab (key schema, GSI/LSI, and non-key attribute types inferred from loaded items), scan or query items, follow pagination tokens, and open item details with a copyable key. Browsing is read-only."
26593
26708
  ],
26594
26709
  [
26595
- "S3 / MinIO / LocalStack",
26596
- "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."
26710
+ "S3 / MinIO / LocalStack / Cloudflare R2",
26711
+ "Detected from compose files, or added as a saved connection. R2 uses the Cloudflare R2 provider preset: enter the account ID and an R2 access key pair, and the endpoint and the required auto signing region are filled in. 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."
26597
26712
  ]
26598
26713
  ]
26599
26714
  }
@@ -26987,6 +27102,10 @@ code-viewer query agent-help`
26987
27102
  kind: "paragraph",
26988
27103
  text: "ファイル詳細ページには最大 4 つのタブ (Preview / Code / Blame / History) があります。テキストファイルは Code がデフォルトで、?preview=1 を付けると Markdown / HTML プレビューに切り替わります。画像・動画・音声・PDF などのメディアファイルは Preview タブのみ表示され、Code タブは表示されません。Blame と History はそれぞれ専用 URL (view=blame, view=history) を持つため、ディープリンクとブラウザの戻る / 進むが同期し、いずれも Repository サイドバーは表示されたままです。ツリーから別のファイルを開いても選択中のタブは維持されます (プレビューできないファイルでは Code に戻ります)。"
26989
27104
  },
27105
+ {
27106
+ kind: "paragraph",
27107
+ text: "Markdown プレビュー内の相対リンクは GitHub と同じ行き先に解決されます。別の Markdown ファイルはそのファイルページを開き、#見出し 付きのリンクはプレビューを開いて該当見出しまでスクロールし、Markdown 以外のファイルは Code ビュー、ディレクトリへのリンクはリポジトリツリーのそのフォルダを開きます。"
27108
+ },
26990
27109
  {
26991
27110
  kind: "paragraph",
26992
27111
  text: "リポジトリの remote が GitHub の場合、リポジトリとファイルのヘッダーから現在のパスを GitHub で開けます。ソース行を選択すると、AI 参照のコピー、選択行範囲を GitHub で開く、GitHub URL のコピーを個別に選べます。Markdown / HTML の diff カードには直接 Preview を開く導線も表示されます。"
@@ -27238,7 +27357,7 @@ code-viewer annotate add-db --db app.db --tab query \\
27238
27357
  database: {
27239
27358
  nav: "データストア",
27240
27359
  title: "データストアビューア",
27241
- intro: "SQLite ファイル、Docker 上のデータベース、Redis、Elasticsearch、DynamoDB、S3 互換オブジェクトストアをローカルビューアで閲覧できます。",
27360
+ intro: "SQLite ファイル、Docker 上のデータベース、Cloudflare D1、Redis、Elasticsearch、DynamoDB、S3 互換オブジェクトストア (Cloudflare R2 を含む) をローカルビューアで閲覧できます。",
27242
27361
  groups: [
27243
27362
  {
27244
27363
  title: "対応データストア",
@@ -27248,7 +27367,7 @@ code-viewer annotate add-db --db app.db --tab query \\
27248
27367
  rows: [
27249
27368
  [
27250
27369
  "保存済み接続",
27251
- "データストア選択の横にある + から、任意の PostgreSQL、MySQL、Redis、Elasticsearch、S3 互換、DynamoDB エンドポイントを追加できます。必須項目にはマークが付き、保存前に「接続テスト」で入力内容を確認できます。ドライバーは同梱されているため、データベース CLI や curl は不要です。非機密設定はローカルに保存されますが、認証情報はサーバーメモリだけに保持され、再起動後は再入力が必要です。"
27370
+ "データストア選択の横にある + から、任意の PostgreSQL、MySQL、Cloudflare D1、Redis、Elasticsearch、S3 互換 (Cloudflare R2 プリセットあり)、DynamoDB エンドポイントを追加できます。必須項目にはマークが付き、保存前に「接続テスト」で入力内容を確認できます。ドライバーは同梱されているため、データベース CLI や curl は不要です。非機密設定はローカルに保存されます。資格情報はリポジトリ配下には一切書かれず、macOS ではキーチェーンに保存されるため再起動をまたいで保持されます (それ以外の OS ではサーバーメモリのみで、再起動後は再入力が必要です)。"
27252
27371
  ],
27253
27372
  [
27254
27373
  "SQLite",
@@ -27262,6 +27381,10 @@ code-viewer annotate add-db --db app.db --tab query \\
27262
27381
  "PostgreSQL",
27263
27382
  "compose ファイルから検出。同一サーバー上の複数データベースに対応し、スキーマ切替セレクターで再オープンせずスキーマを切り替えられます。SQLite と同じく行のインライン編集 / 追加 / 削除に対応。ローカルの Supabase CLI (`supabase start`) プロジェクトも `supabase/config.toml` から自動検出され、docker-compose ファイルは不要です。"
27264
27383
  ],
27384
+ [
27385
+ "Cloudflare D1",
27386
+ "アカウント ID・データベース ID・API トークン (D1:Read 権限が必要) を入力して保存済み接続として追加します。D1 REST API 経由で閲覧し、SQL 系の画面 (テーブル一覧・行グリッド・クエリエディタ・スキーマ・ER 図・スナップショット/差分) をそのまま使えます。閲覧専用で、クエリエディタは SELECT / PRAGMA / EXPLAIN / WITH のみ受け付け、グリッドの Edit モードは表示されません。"
27387
+ ],
27265
27388
  [
27266
27389
  "Redis",
27267
27390
  "compose ファイルから検出。DB 0-15 を SCAN し、string/hash/list は専用ペイン、set/zset/stream は JSON ビュー。値の編集 / キー削除 / 新規キー作成 (全タイプ) に対応。スナップショット/差分にも参加します。"
@@ -27275,8 +27398,8 @@ code-viewer annotate add-db --db app.db --tab query \\
27275
27398
  "LocalStack の compose サービスで DynamoDB が有効な場合に検出。テーブル一覧、構造タブ(キースキーマ・GSI/LSI・読み込み済みアイテムから推測した非キー属性の型)、scan / query、継続トークンによるページング、コピー可能なキー付きのアイテム詳細を表示します。閲覧専用です。"
27276
27399
  ],
27277
27400
  [
27278
- "S3 / MinIO / LocalStack",
27279
- "compose ファイルから検出。フォルダツリー型ブラウザ、prefix/ファイル名検索、更新日時順表示、画像/動画/音声/PDF/Markdown/HTML/テキストのプレビューに対応。テキスト/Markdown/JSON のインライン編集、新規オブジェクトアップロード、オブジェクト削除も可能。LocalStack はホストポート未公開時 `docker exec curl` にフォールバックしますが、MinIO はホストポート公開が必須です。"
27401
+ "S3 / MinIO / LocalStack / Cloudflare R2",
27402
+ "compose ファイルから検出するほか、保存済み接続としても追加できます。R2 は「Cloudflare R2」プロバイダプリセットを選び、アカウント ID と R2 のアクセスキーを入力すればエンドポイントと必須リージョン auto が自動で入ります。フォルダツリー型ブラウザ、prefix/ファイル名検索、更新日時順表示、画像/動画/音声/PDF/Markdown/HTML/テキストのプレビューに対応。テキスト/Markdown/JSON のインライン編集、新規オブジェクトアップロード、オブジェクト削除も可能。LocalStack はホストポート未公開時 `docker exec curl` にフォールバックしますが、MinIO はホストポート公開が必須です。"
27280
27403
  ]
27281
27404
  ]
27282
27405
  }
@@ -30831,6 +30954,59 @@ code-viewer query agent-help`
30831
30954
  return trimmed;
30832
30955
  }
30833
30956
 
30957
+ // web-src/views/markdown-link-navigation.ts
30958
+ async function openMarkdownLink(target, deps) {
30959
+ const ref = target.ref || "worktree";
30960
+ const routeAtClick = currentRouteKey();
30961
+ const directory = await isMarkdownDirectoryLink(target, deps);
30962
+ if (directory === null || currentRouteKey() !== routeAtClick)
30963
+ return;
30964
+ if (directory) {
30965
+ deps.setRoute(deps.repoRoute(ref, target.path));
30966
+ await deps.loadRepo();
30967
+ return;
30968
+ }
30969
+ const anchorInMarkdown = !!target.hash && sourcePreviewKind(target.path) === "markdown";
30970
+ deps.setRoute({
30971
+ screen: "file",
30972
+ path: target.path,
30973
+ ref,
30974
+ view: "blob",
30975
+ ...anchorInMarkdown ? { preview: true } : {},
30976
+ range: deps.currentRange()
30977
+ });
30978
+ applyMarkdownLinkHash(target.hash);
30979
+ await deps.renderStandaloneSource({ path: target.path, ref });
30980
+ }
30981
+ function currentRouteKey() {
30982
+ return window.location.pathname + window.location.search;
30983
+ }
30984
+ async function isMarkdownDirectoryLink(target, deps) {
30985
+ if (target.directory)
30986
+ return true;
30987
+ if (!target.path)
30988
+ return true;
30989
+ const name = target.path.split("/").pop() || "";
30990
+ if (/.\.[^.]+$/.test(name))
30991
+ return false;
30992
+ try {
30993
+ const res = await deps.trackLoad(fetch(buildRawFileUrl({ path: target.path, ref: target.ref }), {
30994
+ method: "HEAD"
30995
+ }));
30996
+ return res.status === 404;
30997
+ } catch (err) {
30998
+ if (deps.isAbortError(err))
30999
+ return null;
31000
+ return false;
31001
+ }
31002
+ }
31003
+ function applyMarkdownLinkHash(hash) {
31004
+ if (!hash)
31005
+ return;
31006
+ const base2 = window.location.pathname + window.location.search;
31007
+ history.replaceState(history.state, "", `${base2}#${encodeURIComponent(hash)}`);
31008
+ }
31009
+
30834
31010
  // web-src/views/repository-web-link.ts
30835
31011
  function createRepositoryWebLink(target, label) {
30836
31012
  const link2 = document.createElement("a");
@@ -32533,6 +32709,7 @@ code-viewer query agent-help`
32533
32709
  renderStandaloneSource,
32534
32710
  repoFileTargetFromRoute,
32535
32711
  trackLoad,
32712
+ isAbortError: isAbortError3,
32536
32713
  syncSidebarHeaderHeight,
32537
32714
  clearLoadQueue,
32538
32715
  getProjectName,
@@ -32894,6 +33071,17 @@ code-viewer query agent-help`
32894
33071
  range: currentRange()
32895
33072
  };
32896
33073
  }
33074
+ function markdownLinkNavigationDeps() {
33075
+ return {
33076
+ setRoute,
33077
+ currentRange,
33078
+ loadRepo,
33079
+ repoRoute,
33080
+ renderStandaloneSource,
33081
+ trackLoad,
33082
+ isAbortError: isAbortError3
33083
+ };
33084
+ }
32897
33085
  function createRepoBreadcrumb(target, path) {
32898
33086
  const nav = document.createElement("nav");
32899
33087
  nav.className = "gdp-file-breadcrumb gdp-repo-breadcrumb";
@@ -33087,16 +33275,7 @@ code-viewer query agent-help`
33087
33275
  try {
33088
33276
  wrapper.appendChild(await renderMarkdownPreview(meta.readme.text, { path: meta.readme.path, ref: meta.ref }, {
33089
33277
  syntaxHighlight: STATE.syntaxHighlight,
33090
- onNavigateMarkdown: (path, ref) => {
33091
- setRoute({
33092
- screen: "file",
33093
- path,
33094
- ref,
33095
- view: "blob",
33096
- range: currentRange()
33097
- });
33098
- renderStandaloneSource({ path, ref });
33099
- }
33278
+ onNavigateMarkdown: (link2) => void openMarkdownLink(link2, markdownLinkNavigationDeps())
33100
33279
  }));
33101
33280
  } catch {
33102
33281
  const fallback = document.createElement("pre");
@@ -34321,6 +34500,17 @@ code-viewer query agent-help`
34321
34500
  focusMainSurface,
34322
34501
  isPaletteOpen
34323
34502
  } = deps;
34503
+ function markdownLinkNavigationDeps() {
34504
+ return {
34505
+ setRoute,
34506
+ currentRange,
34507
+ loadRepo,
34508
+ repoRoute,
34509
+ renderStandaloneSource,
34510
+ trackLoad,
34511
+ isAbortError: isAbortError3
34512
+ };
34513
+ }
34324
34514
  const VIRTUAL_SOURCE_LINE_THRESHOLD = 3000;
34325
34515
  const VIRTUAL_SOURCE_SIZE_THRESHOLD = 1024 * 1024;
34326
34516
  const VIRTUAL_SOURCE_PAGE_SIZE = 2000;
@@ -34562,16 +34752,7 @@ code-viewer query agent-help`
34562
34752
  (deps.renderMarkdownPreview ?? renderMarkdownPreview)(textValue, target, {
34563
34753
  syntaxHighlight: true,
34564
34754
  signal,
34565
- onNavigateMarkdown: (path, ref) => {
34566
- setRoute({
34567
- screen: "file",
34568
- path,
34569
- ref,
34570
- view: "blob",
34571
- range: currentRange()
34572
- });
34573
- renderStandaloneSource({ path, ref });
34574
- }
34755
+ onNavigateMarkdown: (link2) => void openMarkdownLink(link2, markdownLinkNavigationDeps())
34575
34756
  }).then((next) => {
34576
34757
  if (signal?.aborted || !preview.isConnected || !sourceTargetsEqual(sourceTargetFromRoute(), target))
34577
34758
  return;
@@ -34856,16 +35037,7 @@ code-viewer query agent-help`
34856
35037
  let preview = previewKind === "html" ? renderHtmlPreview(target, textValue) : await (deps.renderMarkdownPreview ?? renderMarkdownPreview)(textValue, target, {
34857
35038
  syntaxHighlight: false,
34858
35039
  signal,
34859
- onNavigateMarkdown: (path, ref) => {
34860
- setRoute({
34861
- screen: "file",
34862
- path,
34863
- ref,
34864
- view: "blob",
34865
- range: currentRange()
34866
- });
34867
- renderStandaloneSource({ path, ref });
34868
- }
35040
+ onNavigateMarkdown: (link2) => void openMarkdownLink(link2, markdownLinkNavigationDeps())
34869
35041
  });
34870
35042
  if (signal?.aborted)
34871
35043
  return false;
@@ -34956,16 +35128,7 @@ code-viewer query agent-help`
34956
35128
  let preview = previewKind === "html" ? renderHtmlPreview(target, textValue) : await (deps.renderMarkdownPreview ?? renderMarkdownPreview)(textValue, target, {
34957
35129
  syntaxHighlight: false,
34958
35130
  signal,
34959
- onNavigateMarkdown: (path, ref) => {
34960
- setRoute({
34961
- screen: "file",
34962
- path,
34963
- ref,
34964
- view: "blob",
34965
- range: currentRange()
34966
- });
34967
- renderStandaloneSource({ path, ref });
34968
- }
35131
+ onNavigateMarkdown: (link2) => void openMarkdownLink(link2, markdownLinkNavigationDeps())
34969
35132
  });
34970
35133
  if (signal?.aborted)
34971
35134
  return false;
@@ -36754,6 +36917,7 @@ code-viewer query agent-help`
36754
36917
  renderStandaloneSource,
36755
36918
  repoFileTargetFromRoute,
36756
36919
  trackLoad,
36920
+ isAbortError: isAbortError3,
36757
36921
  syncSidebarHeaderHeight,
36758
36922
  clearLoadQueue: () => DIFF_VIEW.clearLoadQueue(),
36759
36923
  getProjectName: () => PROJECT_NAME,
package/web/style.css CHANGED
@@ -3766,10 +3766,12 @@ table.d2h-diff-table td.d2h-code-side-linenumber {
3766
3766
  border-radius: 6px;
3767
3767
  }
3768
3768
  .gdp-upload-panel {
3769
- display: flex;
3769
+ /* Grid, not space-between flex: the always-present error node would
3770
+ otherwise take the right edge and strand the button in the middle. */
3771
+ display: grid;
3772
+ grid-template-columns: minmax(0, 1fr) auto;
3770
3773
  align-items: center;
3771
- justify-content: space-between;
3772
- gap: 12px;
3774
+ gap: 6px 12px;
3773
3775
  margin: 12px 14px;
3774
3776
  padding: 10px 14px;
3775
3777
  border: 1px dashed var(--border);
@@ -3789,11 +3791,15 @@ table.d2h-diff-table td.d2h-code-side-linenumber {
3789
3791
  border-color: var(--danger);
3790
3792
  }
3791
3793
  .gdp-upload-error {
3792
- min-height: 18px;
3794
+ /* Second row, full width, so showing an error never shifts the button. */
3795
+ grid-column: 1 / -1;
3793
3796
  color: var(--danger);
3794
3797
  font-size: var(--ui-font-base);
3795
3798
  line-height: 18px;
3796
3799
  }
3800
+ .gdp-upload-error:empty {
3801
+ display: none;
3802
+ }
3797
3803
  .gdp-upload-copy {
3798
3804
  min-width: 0;
3799
3805
  color: var(--fg-muted);