Skip to content

feat: restore documents and Git Sync support - #2491

Merged
yohamta0 merged 16 commits into
mainfrom
agent/restore-docs-feature
Aug 4, 2026
Merged

yohamta0 merged 16 commits into
mainfrom
agent/restore-docs-feature

Conversation

@yohamta0

@yohamta0 yohamta0 commented Aug 4, 2026 •

Copy link
Copy Markdown
Member

Summary

  • restore the Documents feature with Markdown editing, preview, Mermaid rendering, outline navigation, tabs, search, and complete file-tree operations
  • restore document persistence, API, workspace, audit, runtime-context, and SSE integration
  • extend the current Git Sync implementation to synchronize Markdown documents under docs/** alongside DAG definitions
  • prevent save echoes from being reported as external edit conflicts

Why

Documents previously provided an integrated place to capture business logic and scripts next to workflows. This restores that workflow while preserving the current Git Sync safety model.

Closes #2368

Testing

  • make test
  • go test ./internal/gitsync ./internal/service/frontend/api/v1
  • make bin
  • cd ui && pnpm test
  • cd ui && pnpm typecheck
  • cd ui && pnpm build
  • .local/bin/golangci-lint run --timeout=10m ./...
  • GOOS=windows .local/bin/golangci-lint run --timeout=10m ./...
  • browser QA for document creation, Markdown/Mermaid preview, rename, drag-and-drop move, deletion, search, and save-conflict handling

Summary by cubic

Restores Documents with a workspace-aware Markdown editor and tree, plus Git Sync for docs. Adds doc search, SSE live updates with a 30s fallback, and explicit config/runtime support.

  • New Features

    • Documents: create/rename/move/delete, drag-and-drop, search, audit logs; workspace-scoped under paths.docs_dir (defaults to <dags_dir>/docs).
    • UI: Docs page with tree (react-arborist), tabbed Markdown + Mermaid preview, outline panel, external-change dialog, drafts/unsaved handling; System Paths and dagu config show “Docs directory”; global search includes docs with match fetching.
    • API & SSE: docs endpoints; /search/docs and match endpoints; SSE topics doc and doctree with recursive watching and a 30s polling fallback.
    • Runtime & Config: exposes context.paths.docs_dir and env DAG_DOCS_DIR; adds paths.docs_dir and env DAGU_DOCS_DIR.
    • Git Sync: syncs .md under docs/** as kind doc, writes to the configured docs dir; CLI/UI use generic “sync item” labels and filters.
  • Stability & Refactors

    • Editor: accepts server echo of a pending save, explicit begin/cancel save, preserves state across navigation/saves; drafts scoped by workspace and cleared reliably.
    • Paths & Renames: normalizes IDs/paths; rename-without-overwrite across platforms; moves doc directories on workspace rename; stable created-at metadata.
    • SSE & Watchers: topic parsing for doc/doctree; recursive watch includes nested dirs; low-volume invalidations use a 30s polling fallback.
    • Initialization & Safety: document store initialization via DocStoreFactory; document mutations centralized; rejects invalid path segments; filters DAG_DOCS_DIR in shared containers; clarifies document search case behavior.

Written for commit ff871a4. Summary will update on new commits.

Review in cubic

Summary by CodeRabbit

  • New Features
    • Added a Docs area for browsing, creating, editing, renaming, moving, deleting, and searching Markdown documents.
    • Added document tabs, previews, outlines, drafts, unsaved-change protection, and external-change handling.
    • Search now supports workflows and documentation with match results and pagination.
    • Git Sync supports documentation alongside workflows, including item-type filtering and validation.
  • Configuration
    • Added configurable documentation paths and workflow context access.
  • UI
    • Added real-time document updates with polling fallback and Docs navigation.
  • Documentation
    • Updated Quick Look screenshots to showcase documentation.

Copilot AI lite review requested due to automatic review settings August 4, 2026 01:15
@coderabbitai

coderabbitai Bot commented Aug 4, 2026 •

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 007a7cce-dc23-42e1-9729-e83dcc23ed14

📥 Commits

Reviewing files that changed from the base of the PR and between 691e28b and ff871a4.

📒 Files selected for processing (11)
  • internal/cmd/sync.go
  • internal/gitsync/service_test.go
  • internal/gitsync/state.go
  • internal/persis/file/doc/store_test.go
  • internal/persis/file/doc/tree_test.go
  • internal/persis/file/service_stores.go
  • internal/service/frontend/api/v1/docs_search_test.go
  • internal/service/frontend/api/v1/sync.go
  • internal/service/frontend/api/v1/sync_test.go
  • ui/src/pages/docs/hooks/__tests__/useDocDraftPersistence.test.tsx
  • ui/src/pages/docs/hooks/__tests__/useDocMutations.test.tsx
🚧 Files skipped from review as they are similar to previous changes (11)
  • internal/service/frontend/api/v1/docs_search_test.go
  • internal/persis/file/doc/tree_test.go
  • internal/service/frontend/api/v1/sync_test.go
  • internal/gitsync/state.go
  • internal/gitsync/service_test.go
  • internal/cmd/sync.go
  • internal/service/frontend/api/v1/sync.go
  • ui/src/pages/docs/hooks/tests/useDocDraftPersistence.test.tsx
  • ui/src/pages/docs/hooks/tests/useDocMutations.test.tsx
  • internal/persis/file/service_stores.go
  • internal/persis/file/doc/store_test.go

📝 Walkthrough

Walkthrough

The change adds Markdown document storage, workspace-aware APIs, search, synchronization, SSE updates, runtime paths, and a document-management UI. Git Sync now models DAGs and documents as sync items.

Changes

Document management and sync-item support

Layer / File(s) Summary
Document contracts and storage
internal/core/docs/*, internal/persis/file/doc/*
Adds document models, validation, Markdown parsing, metadata, indexed listing, CRUD, safe renaming and deletion, tree construction, and cursor-based search.
Backend APIs and synchronization
api/v1/*, internal/service/frontend/api/v1/*, internal/gitsync/*
Adds document API contracts and handlers. Git Sync scans Markdown documents, stores item kinds, resolves separate document paths, and rejects cross-kind moves.
Runtime paths and SSE
internal/cmn/config/*, internal/core/exec/*, internal/service/frontend/server.go, internal/service/frontend/sse/*
Adds configurable documentation paths, DAG_DOCS_DIR, built-in context bindings, document-store wiring, recursive watchers, polling fallback, and document SSE topics.
Document UI and search
ui/src/pages/docs/*, ui/src/contexts/*, ui/src/hooks/*, ui/src/pages/search/*
Adds the Docs route, workspace-aware tree browsing, tabbed editing, Markdown preview, outlines, mutations, conflict handling, document search, and SSE subscriptions.
Sync-item UI and presentation
ui/src/pages/git-sync/*, ui/src/pages/home/*, ui/src/menu.tsx, ui/src/layouts/*, README.md
Adds DAG/document filtering and labels, Docs navigation, breadcrumbs, audit filtering, system path display, and a documentation screenshot.

Estimated code review effort: 5 (Critical) | ~120 minutes

Possibly related PRs

  • dagucloud/dagu#2015: Adds workspace scoping to document APIs and related UI.
  • dagucloud/dagu#2331: Removes the document-management feature across related APIs, storage, sync, SSE, configuration, and UI.
  • dagucloud/dagu#2413: Overlaps with the Git Sync SyncItem state model and related API, CLI, service, and UI changes.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 9.80% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title 'feat: restore documents and Git Sync support' accurately summarizes the main change—restoring the Documents feature with Git Sync integration for Markdown files.
Description check ✅ Passed The description covers the Summary, Changes, Related Issues sections, and testing details; the Checklist is not fully completed but the core required sections are present and comprehensive.
Linked Issues check ✅ Passed The PR fully addresses issue #2368 by restoring the Documents feature for documenting business logic and scripts alongside workflows with Markdown editing, search, and Git Sync integration.
Out of Scope Changes check ✅ Passed All changes align with the PR objectives: document feature restoration, Git Sync extension, configuration paths, workspace integration, API/UI additions, and save-conflict prevention.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch agent/restore-docs-feature

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

yohamta0 commented Aug 4, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Aug 4, 2026 •

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 18

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
internal/service/frontend/sse/app_stream.go (1)

244-266: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Close the file watcher on the early error returns in Start.

Two error paths return without releasing the watcher:

  • Line 248-250: w.addWatch(w.root) fails and returns. w.watcher stays open.
  • Line 262-264: w.addWatch(path) fails and returns. w.watcher stays open.

The watchPathsForScope path at Line 254-257 does call _ = w.watcher.Close(), so the handling is inconsistent. Stop() cannot recover the handle either, because the loop goroutine has not started yet, so Stop() only closes done and waits on an empty WaitGroup.

This path is now reachable on every fallback. docsDirectoryWatcher.Start at Line 375-382 starts a recursive event watcher, and on failure calls eventWatcher.Stop() before switching to the Markdown poller. The inotify handle from the failed attempt leaks.

🔒️ Proposed fix
 	w.watcher, err = w.newFileWatcher()
 	if err != nil {
 		return err
 	}
 	if err := w.addWatch(w.root); err != nil {
+		_ = w.watcher.Close()
 		return err
 	}
 
 	if w.scope == watchScopeOneLevel || w.scope == watchScopeRecursive {
 		paths, err := watchPathsForScope(w.root, w.scope)
 		if err != nil {
 			_ = w.watcher.Close()
 			return err
 		}
 		for _, path := range paths {
 			if path == w.root {
 				continue
 			}
 			if err := w.addWatch(path); err != nil {
+				_ = w.watcher.Close()
 				return err
 			}
 		}
 	}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/service/frontend/sse/app_stream.go` around lines 244 - 266, Update
Start to close w.watcher before returning when addWatch(w.root) fails or when
adding any path from watchPathsForScope fails, matching the existing cleanup for
watchPathsForScope errors; preserve the original error after cleanup.
🧹 Nitpick comments (18)
internal/service/frontend/sse/app_stream_test.go (1)

108-112: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Consider not starting the poll loop in this test.

Start spawns the polling goroutine, and the test then calls watcher.check() directly. Both touch w.snapshot without synchronization. The test is safe today only because the interval is time.Hour, so the loop goroutine never calls check. A future change to the default interval or to Start would make this a real race under go test -race.

Seed the snapshot without starting the loop, or keep the current form and add a short comment stating that the one-hour interval is what keeps the loop idle.

As per coding guidelines: "Go tests use race detection".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/service/frontend/sse/app_stream_test.go` around lines 108 - 112,
Update the test around watcher.Start and watcher.check to avoid concurrent
snapshot access: seed the watcher snapshot directly without starting the polling
loop, or explicitly document that the one-hour interval keeps the loop idle.
Prefer the no-loop setup so the test remains race-safe if the default interval
or Start behavior changes.

Source: Coding guidelines

internal/service/frontend/api/v1/docs.go (1)

465-482: 🚀 Performance & Scalability | 🔵 Trivial | ⚖️ Poor tradeoff

Consider pushing workspace scoping into the store call.

SearchDocs calls a.docStore.Search without any scope, then discards non-matching and non-visible results in Go. SearchDocFeed in internal/service/frontend/api/v1/search.go passes PathPrefix and ExcludePathRoots to the store instead. Authorization is still correct here, because every hidden result is filtered before the response is built. However, the store reads and scores documents that the caller can never see.

If docs.DocStore gains a scoped variant of Search, align this handler with SearchDocFeed.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/service/frontend/api/v1/docs.go` around lines 465 - 482, Update
SearchDocs to use a scoped docs.DocStore search when the store exposes the
scoped Search variant, passing workspaceName as PathPrefix and the
visibility-derived roots as ExcludePathRoots, consistent with SearchDocFeed.
Preserve the existing filtering and ID normalization behavior as a safety check
before constructing the response.
internal/service/frontend/sse/app_stream.go (1)

343-358: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Ignore os.ErrNotExist during the recursive walk.

addCreatedDirWatches walks a directory immediately after a Create event. If the directory or a child is removed before the walk reaches it, WalkDir returns the error. The caller at Line 319-321 then calls w.onReset, which forces a full client refresh for a transient directory.

Skip missing entries so a short-lived directory does not trigger a reset.

♻️ Proposed change
 	return filepath.WalkDir(path, func(childPath string, entry os.DirEntry, err error) error {
 		if err != nil {
+			if errors.Is(err, os.ErrNotExist) {
+				return nil
+			}
 			return err
 		}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/service/frontend/sse/app_stream.go` around lines 343 - 358, Update
the WalkDir callback in addCreatedDirWatches to ignore errors wrapping
os.ErrNotExist and continue the recursive walk, including when the created
directory or a child disappears before traversal. Preserve returning all other
errors so genuine failures still reach the caller without triggering unnecessary
behavior changes.
internal/service/frontend/api/v1/search.go (1)

296-306: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Document why the aggregate-scope retry exists.

The retry compensates for a matches cursor that SearchDocFeed minted in aggregate scope and that the client then replays with workspace set. The behavior depends on the store returning exec.ErrInvalidCursor when the cursor PathPrefix does not match the request. That coupling is not obvious from the code.

Authorization is preserved, because workspaceName is only non-empty after docPointReadScopeForParams authorizes it, and aggregatePath keeps that prefix. Add a short comment so a future change to cursor encoding does not remove this branch by accident.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/service/frontend/api/v1/search.go` around lines 296 - 306, Add a
short comment above the retry branch in the search handler around SearchMatches
explaining that SearchDocFeed may mint an aggregate-scope cursor, which can be
replayed with a workspace and yield exec.ErrInvalidCursor when PathPrefix
differs; document that the retry restores aggregate scope while preserving the
authorized aggregatePath.
ui/src/contexts/DocTabContext.tsx (1)

196-233: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Move persistence and ref writes out of state updater functions.

closeTab calls setActiveTabId, mutates draftsRef/unsavedTabIdsRef, and calls persistState inside the setTabs, setDrafts, and setUnsavedTabIds updater functions. The same pattern appears in closeOtherTabs, setDraft, clearDraft, markTabUnsaved, and markTabSaved. React expects updater functions to be pure. Under StrictMode the updaters run twice, so these writes run twice per call.

The effect on Line 158 already persists state after every change to tabs, activeTabId, drafts, and unsavedTabIds, and the refs are refreshed on every render at Lines 135-142. The in-updater persistence is therefore redundant. Compute the next value first, then call the setters and persist outside the updater.

♻️ Example for setDraft
 const setDraft = useCallback(
   (tabId: string, content: string) => {
-    setDrafts((prev) => {
-      const next = new Map(prev);
-      next.set(tabId, content);
-      draftsRef.current = next;
-      persistState({ drafts: Array.from(next.entries()) });
-      return next;
-    });
+    const next = new Map(draftsRef.current);
+    next.set(tabId, content);
+    setDrafts(next);
   },
-  [persistState]
+  []
 );
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@ui/src/contexts/DocTabContext.tsx` around lines 196 - 233, Update closeTab
and the related mutators closeOtherTabs, setDraft, clearDraft, markTabUnsaved,
and markTabSaved so their state updater callbacks only compute and return next
values. Move setActiveTabId calls, draftsRef/unsavedTabIdsRef writes, and
persistState calls outside updater functions, relying on the existing
state-persistence effect and render-time ref synchronization to handle updates
without duplicate side effects.
ui/src/pages/docs/index.tsx (3)

203-205: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low value

Depend on setTitle instead of the whole context object.

AppBarContext is provided as an inline object literal in ui/src/App.tsx (lines 578-592), so its identity changes on every AppInner render. This effect then re-runs on every parent render. ui/src/pages/home/index.tsx line 79 uses [setTitle], which is stable.

♻️ Proposed refactor
-  // Set page title
-  useEffect(() => {
-    appBarContext.setTitle('Docs');
-  }, [appBarContext]);
+  // Set page title
+  const setTitle = appBarContext.setTitle;
+  useEffect(() => {
+    setTitle('Docs');
+  }, [setTitle]);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@ui/src/pages/docs/index.tsx` around lines 203 - 205, Update the Docs page
useEffect dependency array to depend on the stable setTitle function rather than
the entire appBarContext object, matching the existing pattern in the Home page
while preserving the title-setting behavior.

495-498: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Surface the server error message on delete failure.

handleRenameModal (line 366) and handlePathChange (line 429) show error?.message. This branch discards it. Users lose the reason for the failure.

♻️ Proposed refactor
       if (error) {
-        showToast('Failed to delete document');
+        showToast(error?.message || 'Failed to delete document');
         return;
       }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@ui/src/pages/docs/index.tsx` around lines 495 - 498, Update the delete
failure branch in the document deletion handler to include the server-provided
error message via error?.message, matching handleRenameModal and
handlePathChange, while retaining the existing fallback toast text when no
message is available.

705-745: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Extract the duplicated modal block.

Lines 705-745 and lines 763-803 declare the same four modals with identical props. Two copies can diverge when a prop changes. Assign the block to a variable once, then render it in both layouts.

♻️ Proposed refactor
+  const modals = (
+    <>
+      <CreateDocModal
+        isOpen={createModalOpen}
+        onClose={() => setCreateModalOpen(false)}
+        onSubmit={handleCreate}
+        parentDir={createParentDir}
+        isLoading={createLoading}
+        externalError={createError}
+      />
+      <RenameDocModal
+        isOpen={renameModalOpen}
+        onClose={() => setRenameModalOpen(false)}
+        onSubmit={handleRenameModal}
+        currentPath={renameDocPath}
+        isLoading={renameLoading}
+        externalError={renameError}
+      />
+      <ConfirmModal
+        title="Delete Document"
+        buttonText="Delete"
+        visible={deleteConfirmOpen}
+        dismissModal={() => setDeleteConfirmOpen(false)}
+        onSubmit={handleDelete}
+      >
+        <p className="text-sm text-muted-foreground">
+          Are you sure you want to delete <strong>{deleteDocTitle}</strong>?
+          This action cannot be undone.
+        </p>
+      </ConfirmModal>
+      <ConfirmModal
+        title="Delete Docs"
+        buttonText={`Delete ${batchDeleteTargets.length} items`}
+        visible={batchDeleteConfirmOpen}
+        dismissModal={() => setBatchDeleteConfirmOpen(false)}
+        onSubmit={handleBatchDelete}
+      >
+        <p className="text-sm text-muted-foreground">
+          Are you sure you want to delete {batchDeleteTargets.length} items?
+          This cannot be undone.
+        </p>
+      </ConfirmModal>
+    </>
+  );

Then render {modals} in the mobile branch and in the desktop branch.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@ui/src/pages/docs/index.tsx` around lines 705 - 745, Extract the shared
CreateDocModal, RenameDocModal, and both ConfirmModal declarations from the page
component into a single modals variable, preserving all existing props and
handlers. Replace the duplicated modal blocks in the mobile and desktop layout
branches with {modals} so both branches render the same definitions.
ui/src/pages/docs/lib/doc-validation.ts (1)

11-31: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Validate trimmed consistently, and name the length limit.

Line 11 computes trimmed, but lines 15, 18, 24, and 31 validate the raw path. Both current callers trim before calling, so behavior is correct today. A future caller that passes an untrimmed value would get a misleading "cannot end with a space" error. Also extract 252 into a named constant that states it mirrors the backend document ID limit.

♻️ Proposed refactor
+const MAX_DOC_PATH_LENGTH = 252; // Mirrors the backend document ID limit.
+
 export function validateDocPath(path: string): {
   isValid: boolean;
   error?: string;
 } {
   const trimmed = path.trim();
   if (!trimmed) {
     return { isValid: false, error: 'Path is required' };
   }
-  if (path.length > 252) {
-    return { isValid: false, error: 'Path must be 252 characters or fewer' };
+  if (trimmed.length > MAX_DOC_PATH_LENGTH) {
+    return {
+      isValid: false,
+      error: `Path must be ${MAX_DOC_PATH_LENGTH} characters or fewer`,
+    };
   }
-  if (path.toLowerCase().endsWith('.md')) {
+  if (trimmed.toLowerCase().endsWith('.md')) {
     return {
       isValid: false,
       error: 'Path should not include the .md extension.',
     };
   }
-  if (!DOC_PATH_PATTERN.test(path)) {
+  if (!DOC_PATH_PATTERN.test(trimmed)) {
     return {
       isValid: false,
       error:
         'Invalid path. Use letters, numbers, underscores, dots, hyphens, and spaces. Use / for directories.',
     };
   }
-  if (path.split('/').some((segment) => /[ .]$/.test(segment))) {
+  if (trimmed.split('/').some((segment) => /[ .]$/.test(segment))) {

If you apply this, update ui/src/pages/docs/lib/__tests__/doc-validation.test.ts line 23, because validateDocPath('guide ') would then return { isValid: true }.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@ui/src/pages/docs/lib/doc-validation.ts` around lines 11 - 31, Update the doc
path validation function to use the existing trimmed value for all subsequent
length, extension, pattern, and segment checks, while preserving the
required-path check. Extract the 252-character limit into a named constant
documenting that it mirrors the backend document ID limit, and update the
affected test expectation for validateDocPath('guide ') to reflect the trimmed
valid path result.
ui/src/pages/docs/components/DocEditor.tsx (2)

334-347: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Improve the accessible state and name of the toolbar controls.

The copy-file-path button renders an icon only and relies on title for its name. Add aria-label. The mode toggle buttons expose no selected state to assistive technology. Add aria-pressed.

♿ Proposed fix
           <button
             type="button"
             onClick={copyFilePath}
             className="inline-flex items-center gap-1 px-1.5 py-0.5 text-xs rounded-md text-muted-foreground hover:text-foreground hover:bg-muted transition-all shrink-0"
             title={`Copy file path: ${doc.filePath}`}
+            aria-label="Copy file path"
           >
           <button
             type="button"
+            aria-pressed={mode === 'edit'}
             className={cn(
           <button
             type="button"
+            aria-pressed={mode === 'preview'}
             className={cn(

Also applies to: 368-393

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@ui/src/pages/docs/components/DocEditor.tsx` around lines 334 - 347, Improve
accessibility for the toolbar controls in DocEditor: add an aria-label to the
icon-only copy-file-path button using copyFilePath, and add aria-pressed to both
mode toggle buttons in the related controls, reflecting each button’s
active/selected state.

284-320: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Extract the duplicated clipboard routine.

copyFilePath and copyContent repeat the same logic: write to the clipboard, fall back to a hidden textarea, then reset a flag after 2000 ms. Extract one helper and call it from both callbacks.

♻️ Suggested shape
// e.g. ui/src/lib/clipboard.ts
export async function copyText(text: string): Promise<void> {
  try {
    await navigator.clipboard.writeText(text);
    return;
  } catch {
    const textArea = document.createElement('textarea');
    textArea.value = text;
    document.body.appendChild(textArea);
    textArea.select();
    document.execCommand('copy');
    document.body.removeChild(textArea);
  }
}

Each callback then reduces to await copyText(value); setCopied(true); setTimeout(() => setCopied(false), 2000);.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@ui/src/pages/docs/components/DocEditor.tsx` around lines 284 - 320, Extract
the shared clipboard write and textarea fallback from copyFilePath and
copyContent into a reusable copyText helper, then have both callbacks call it
while retaining their existing empty-value guards and copied-state reset
behavior. Keep the helper responsible only for copying text; leave the file-path
and content-specific state updates in their respective callbacks.
ui/src/hooks/__tests__/useContentEditor.test.ts (1)

8-31: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add a test for cancelSave and for the conflict path.

The suite covers the echo-accept path only. cancelSave is new public API and has no coverage. A failed save must restore conflict detection, so an external change after cancelSave must set conflict.hasConflict.

💚 Suggested additional test
   });
+
+  it('restores conflict detection after a cancelled save', () => {
+    const { result, rerender } = renderHook(
+      ({ serverContent }) => useContentEditor({ key: 'doc', serverContent }),
+      { initialProps: { serverContent: '' } }
+    );
+
+    act(() => {
+      result.current.setCurrentValue('# Local');
+      result.current.beginSave('# Local');
+      result.current.cancelSave();
+    });
+
+    rerender({ serverContent: '# External' });
+
+    expect(result.current.conflict.hasConflict).toBe(true);
+  });
 });

As per coding guidelines: "Add or update tests appropriate to the changed code; ... frontend unit tests use Vitest".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@ui/src/hooks/__tests__/useContentEditor.test.ts` around lines 8 - 31, Extend
the useContentEditor test suite with Vitest coverage for cancelSave: begin a
save, call result.current.cancelSave(), then simulate an external serverContent
change and assert conflict.hasConflict becomes true. Keep the existing
server-echo acceptance test unchanged and use the hook’s public API.

Source: Coding guidelines

ui/src/pages/docs/components/DocExternalChangeDialog.tsx (1)

25-50: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

The global Enter handler duplicates native button activation.

ignoreButtonRef and discardButtonRef point to native <button> elements. A focused button already fires onClick on Enter, so onIgnore and onDiscard run without this listener. The handler works today only because e.preventDefault() suppresses the generated click. Remove the effect and rely on the buttons.

♻️ Proposed simplification
 function DocExternalChangeDialog({ visible, onDiscard, onIgnore }: Props) {
-  const ignoreButtonRef = React.useRef<HTMLButtonElement>(null);
-  const discardButtonRef = React.useRef<HTMLButtonElement>(null);
-
-  React.useEffect(() => {
-    const handleKeyDown = (e: KeyboardEvent) => {
-      if (!visible) return;
-
-      if (e.key === 'Enter') {
-        const activeElement = document.activeElement;
-
-        if (activeElement === ignoreButtonRef.current) {
-          e.preventDefault();
-          onIgnore();
-          return;
-        }
-
-        if (activeElement === discardButtonRef.current) {
-          e.preventDefault();
-          onDiscard();
-          return;
-        }
-
-        // No default action — Enter only triggers when a specific button is focused
-      }
-    };
-
-    document.addEventListener('keydown', handleKeyDown);
-    return () => document.removeEventListener('keydown', handleKeyDown);
-  }, [visible, onDiscard, onIgnore]);
-
   return (

Then drop the ref props on both Button elements.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@ui/src/pages/docs/components/DocExternalChangeDialog.tsx` around lines 25 -
50, Remove the global Enter-handling React.useEffect and its related keyboard
logic from DocExternalChangeDialog, relying on native button activation for
onIgnore and onDiscard. Remove the now-unused ignoreButtonRef and
discardButtonRef declarations and ref props from both Button elements.
internal/persis/file/doc/store.go (2)

81-89: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Normalize baseDir in New.

New stores the caller-supplied path verbatim. safePath (line 103) and cleanEmptyParents (line 1710) compare raw strings against s.baseDir. If the caller passes a path with a trailing separator or a . element, those comparisons no longer match the values produced by filepath.Dir and filepath.Join. cleanEmptyParents deletes directories, so an implicit invariant here has a wide blast radius. Clean the path once at construction and remove the assumption.

♻️ Proposed normalization
 func New(baseDir string) *Store {
+	baseDir = filepath.Clean(baseDir)
 	_ = os.MkdirAll(baseDir, docDirPermissions) // best effort
 	return &Store{
 		baseDir:            baseDir,
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/persis/file/doc/store.go` around lines 81 - 89, Normalize baseDir in
New with filepath.Clean before storing it in Store and before creating the
directory, so safePath and cleanEmptyParents compare canonical paths
consistently. Remove the implicit assumption that callers provide an
already-clean path while preserving the existing initialization behavior.

242-267: 🚀 Performance & Scalability | 🔵 Trivial

Index refresh blocks all readers during a full directory walk.

ensureFreshIndex takes the exclusive s.mu write lock and calls refreshIndexLocked, which walks the whole document tree and re-parses every file with a changed fingerprint. Every read path (List, ListFlat, listSearchCandidates) goes through this function. Once the 5-second interval expires, the next request performs the scan while all concurrent readers block on the same lock.

This is acceptable for small document sets. For large trees consider one of these options:

  • Run the refresh in a single background goroutine and let readers serve the current snapshot.
  • Build the new index into a local map, then swap it in under a short write lock.
  • Add a metric for refresh duration so the cost becomes visible before it degrades request latency.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/persis/file/doc/store.go` around lines 242 - 267, Refactor
ensureFreshIndex so refreshIndexLocked does not hold the exclusive s.mu lock
while walking and re-parsing the document tree. Build the refreshed index
outside the write lock, then acquire s.mu only to atomically swap the completed
snapshot and update its freshness metadata, while preserving safe behavior for
concurrent readers and avoiding duplicate refresh work.
internal/core/docs/doc.go (1)

138-155: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Consider rejecting Windows reserved device names in ValidateDocID.

The pattern accepts segments such as con, nul, aux, com1, and lpt1. On Windows these names are reserved. Creating docs/con.md fails or resolves to a device, so document creation and rename produce confusing errors on Windows builds. The trailing space and dot checks already handle the other Windows-specific naming rules, so this check completes the set.

♻️ Proposed addition of a reserved-name check
+// windowsReservedNames lists device names that Windows rejects as file names.
+var windowsReservedNames = map[string]struct{}{
+	"con": {}, "prn": {}, "aux": {}, "nul": {},
+	"com1": {}, "com2": {}, "com3": {}, "com4": {}, "com5": {},
+	"com6": {}, "com7": {}, "com8": {}, "com9": {},
+	"lpt1": {}, "lpt2": {}, "lpt3": {}, "lpt4": {}, "lpt5": {},
+	"lpt6": {}, "lpt7": {}, "lpt8": {}, "lpt9": {},
+}
+
 // ValidateDocID validates that id is a safe, well-formed doc identifier.
 func ValidateDocID(id string) error {
 	if id == "" {
 		return ErrInvalidDocID
 	}
@@
 	for segment := range strings.SplitSeq(id, "/") {
 		if strings.HasSuffix(segment, " ") || strings.HasSuffix(segment, ".") {
 			return fmt.Errorf("%w: path segments must not end with spaces or dots", ErrInvalidDocID)
 		}
+		base, _, _ := strings.Cut(segment, ".")
+		if _, reserved := windowsReservedNames[strings.ToLower(base)]; reserved {
+			return fmt.Errorf("%w: %q is a reserved name", ErrInvalidDocID, base)
+		}
 	}
 	return nil
 }

As per coding guidelines: "Use Go 1.26 and keep Go code compatible with Windows builds, since linting runs under GOOS=windows."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/core/docs/doc.go` around lines 138 - 155, Update ValidateDocID to
reject path segments matching Windows reserved device names, including CON, NUL,
AUX, COM1–COM9, and LPT1–LPT9, case-insensitively. Apply the check alongside the
existing trailing-space and trailing-dot validation for every segment, returning
ErrInvalidDocID with a clear reserved-name message while preserving all existing
validation behavior.

Source: Coding guidelines

internal/persis/file/doc/store_test.go (1)

955-966: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Assert the expected case-insensitivity behavior instead of discarding the result.

The test discards results with _ = results and asserts nothing beyond the absence of an error. It cannot fail if case handling regresses. The store's cursor search builds its pattern with a (?i) prefix in docSearchPattern, so the intended behavior is defined. Pin the expectation for Search here, or remove the test and cover case-insensitivity through SearchCursor, where the behavior is explicit.

💚 Proposed assertion
-	// Search with different case — verify no error even if no match.
-	results, err := store.Search(ctx, "hello")
-	require.NoError(t, err)
-	// Grep may or may not be case-insensitive depending on implementation.
-	_ = results
+	// Cursor search lowercases the pattern with (?i), so a case difference must match.
+	results, err := store.SearchCursor(ctx, docs.SearchDocsOptions{
+		Query:      "hello",
+		Limit:      10,
+		MatchLimit: 1,
+	})
+	require.NoError(t, err)
+	require.Len(t, results.Items, 1)
+	assert.Equal(t, "doc1", results.Items[0].ID)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/persis/file/doc/store_test.go` around lines 955 - 966, Update
TestSearchCaseDifferenceDoesNotError to assert that searching for “hello”
returns the previously created “Hello World” document, using the expected result
assertion helpers. Do not discard results; preserve the no-error check and pin
the case-insensitive behavior implemented by docSearchPattern, or move coverage
to SearchCursor if that is the explicit API path.
internal/persis/file/doc/birthtime_linux.go (1)

24-27: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Reconsider the Ctim fallback.

Ctim is the inode change time. It updates on every content or metadata change, so it is usually later than ModTime and is not a creation time. When statx does not report STATX_BTIME (older kernels, or filesystems such as older ext4 or NFS), this branch reports a "created at" value that moves forward on each edit. The other platform variants fall back to ModTime. Use ModTime here for consistent behavior.

♻️ Proposed change
-	if stat, ok := info.Sys().(*syscall.Stat_t); ok {
-		return time.Unix(int64(stat.Ctim.Sec), int64(stat.Ctim.Nsec))
-	}
 	return info.ModTime()

Remove the now-unused syscall import if you apply this.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/persis/file/doc/birthtime_linux.go` around lines 24 - 27, Update the
birth-time fallback in the shown file to return info.ModTime() instead of
reading stat.Ctim, matching the behavior of other platform variants. Remove the
now-unused syscall import.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@internal/gitsync/service.go`:
- Around line 535-567: Update scanDocFiles so itemID removes the matched file’s
actual extension by slicing filepath.ToSlash(relPath) using the length of
filepath.Ext(filePath), rather than strings.TrimSuffix with the lowercase
docExtension constant. Preserve case-insensitive document matching while
ensuring files such as deploy.MD produce an ID without the extension.
- Line 536: Update newSyncService and the Git Sync service configuration to
carry ctx.Config.Paths.DocsDir into serviceImpl, then use that configured path
when assigning docsDir in scanDocFiles instead of constructing it from s.dagsDir
and docsDir. Preserve scanning behavior for the configured documents directory,
including paths outside the DAGs directory.

In `@internal/gitsync/state.go`:
- Around line 217-227: Update normalizeTrackedItems to stop deleting entries
when itemState.Kind is nonempty but unrecognized. Preserve the existing state,
including sync and conflict metadata, and let the subsequent
KindForDAGID(itemID) assignment normalize the kind; retain deletion only for
other explicitly supported removal cases, if any.

In `@internal/persis/file/doc/metadata.go`:
- Around line 132-149: Update Store.renameDocCreatedAtPrefix to avoid inserting
or deleting entries in metadata.CreatedAt while ranging over it. Build a
separate result map from the original entries, remapping oldID and
oldID-prefixed keys to their newID equivalents, then assign the completed map
back to metadata.CreatedAt before calling saveMetadata.
- Around line 88-130: Guard all document-createdAt metadata mutations with the
existing Store.mutationMu by updating the callers or the methods
setDocCreatedAt, deleteDocCreatedAt, deleteDocCreatedAtPrefix,
renameDocCreatedAt, and renameDocCreatedAtPrefix to use the same in-process lock
around the full load, map mutation, and save sequence. Prefer the existing
public guarded API when available, and preserve the required local file-lock
behavior for shared document directories.

In `@internal/persis/file/doc/rename_noreplace_darwin.go`:
- Around line 15-21: Update renameNoReplace to detect unix.ENOTSUP from
unix.RenameatxNp and fall back to fileutil.Rename(oldPath, newPath) for
unsupported filesystems. Preserve the existing docs.ErrDocAlreadyExists handling
for unix.EEXIST and return other errors unchanged.

In `@internal/persis/file/doc/rename_noreplace_other.go`:
- Around line 15-21: Update renameNoReplace to use an atomic no-replace rename
primitive instead of fileutil.Rename, ensuring an existing destination is never
replaced; retain the existing pre-check for cross-process TOCTOU protection and
continue mapping destination-exists failures to docs.ErrDocAlreadyExists.

In `@internal/persis/file/doc/store_test.go`:
- Around line 1276-1285: Update the comment in TestListFlatMissingBaseDir to
describe the current behavior: rebuildIndexLocked returns early when os.Stat
finds that the base directory is missing, so ListFlat returns empty results
without an error. Remove the outdated reference to filepath.WalkDir and
swallowed errors.

In `@internal/persis/file/doc/store.go`:
- Around line 865-867: In internal/persis/file/doc/store.go at Create (865-867),
Update (903-905), Rename (1133-1135), and Rename (1182-1184), treat created-at
sidecar errors as best effort: log each setDocCreatedAt, renameDocCreatedAt, or
renameDocCreatedAtPrefix error with logger.Warn, then continue the existing
mutation cleanup and index-update calls instead of returning the error.

In `@internal/service/frontend/api/v1/docs.go`:
- Around line 705-716: Update GetDocTreeData to return the error from
url.ParseQuery instead of replacing invalid results with empty url.Values. Match
the existing error-handling behavior in GetDocContentData, while preserving the
current parameter parsing and docReadScopeForParams flow for successful queries.

In `@ui/src/api/v1/schema.ts`:
- Around line 15095-15114: Add a 400 response definition to the deleteDocBatch
operation alongside its existing 200 and default responses, using the
appropriate Error schema, then regenerate the API-derived files with make api so
clients distinguish validation failures.

In `@ui/src/hooks/useDocSSE.ts`:
- Around line 15-25: Update the useDocSSE call to useSSE so it forwards the
remoteNode argument along with the endpoint and enabled condition. Preserve the
existing endpoint construction and ensure the SSE subscription and docs endpoint
resolve the same node.

In `@ui/src/layouts/ContentNavigation.tsx`:
- Around line 114-122: Update the docs breadcrumb construction in the segments
loop so the final docs segment is added without a to value, matching the dags
and generic breadcrumb branches; retain links for preceding segments and the
existing Docs breadcrumb.

In `@ui/src/pages/docs/components/DocArboristNode.tsx`:
- Around line 177-192: Add an aria-label to the inline rename input in the
DocArboristNode component, matching the accessible naming approach used by the
nearby dropdown trigger. Keep the existing input behavior and styling unchanged.

In `@ui/src/pages/docs/components/DocEditor.tsx`:
- Around line 270-279: Update the keyboard shortcut effect in DocEditor to
recognize both lowercase and uppercase S, and only call preventDefault or
handleSaveRef.current when the current user can edit. Add and keep canEditRef
synchronized with canEdit alongside handleSaveRef so the stable keydown listener
uses the latest permission state without blocking the browser save shortcut for
read-only users.

In `@ui/src/pages/docs/components/DocTabBar.tsx`:
- Around line 54-82: Update the tab keyboard navigation in handleKeyDown and the
tab rendering logic to store refs for each tab element and focus the newly
selected tab after ArrowLeft or ArrowRight changes activeTab. Preserve the
existing wrapping behavior and ensure the ref targets remain synchronized with
the roving tabIndex so visible focus follows the active tab.

In `@ui/src/pages/docs/components/DocTreeSidebar.tsx`:
- Around line 424-452: Update handleMove to wrap each awaited onMove call in
try/catch, report failures through the component’s established error-reporting
mechanism, and deliberately choose whether to continue processing later dragIds
or stop after the first failure. Ensure the caught rejection does not escape the
react-arborist callback as an unhandled promise rejection.

In `@ui/src/pages/git-sync/sync-kind.ts`:
- Around line 58-60: Update deriveSyncKindFromItemId to accept the configured
document directory and classify IDs using that directory instead of hardcoded
docs/. Thread paths.docsDir through the caller into MoveDialog and its
classification logic, preserving the backend’s configured-path behavior. Add
Vitest coverage for both the default document directory and a non-default
directory.

---

Outside diff comments:
In `@internal/service/frontend/sse/app_stream.go`:
- Around line 244-266: Update Start to close w.watcher before returning when
addWatch(w.root) fails or when adding any path from watchPathsForScope fails,
matching the existing cleanup for watchPathsForScope errors; preserve the
original error after cleanup.

---

Nitpick comments:
In `@internal/core/docs/doc.go`:
- Around line 138-155: Update ValidateDocID to reject path segments matching
Windows reserved device names, including CON, NUL, AUX, COM1–COM9, and
LPT1–LPT9, case-insensitively. Apply the check alongside the existing
trailing-space and trailing-dot validation for every segment, returning
ErrInvalidDocID with a clear reserved-name message while preserving all existing
validation behavior.

In `@internal/persis/file/doc/birthtime_linux.go`:
- Around line 24-27: Update the birth-time fallback in the shown file to return
info.ModTime() instead of reading stat.Ctim, matching the behavior of other
platform variants. Remove the now-unused syscall import.

In `@internal/persis/file/doc/store_test.go`:
- Around line 955-966: Update TestSearchCaseDifferenceDoesNotError to assert
that searching for “hello” returns the previously created “Hello World”
document, using the expected result assertion helpers. Do not discard results;
preserve the no-error check and pin the case-insensitive behavior implemented by
docSearchPattern, or move coverage to SearchCursor if that is the explicit API
path.

In `@internal/persis/file/doc/store.go`:
- Around line 81-89: Normalize baseDir in New with filepath.Clean before storing
it in Store and before creating the directory, so safePath and cleanEmptyParents
compare canonical paths consistently. Remove the implicit assumption that
callers provide an already-clean path while preserving the existing
initialization behavior.
- Around line 242-267: Refactor ensureFreshIndex so refreshIndexLocked does not
hold the exclusive s.mu lock while walking and re-parsing the document tree.
Build the refreshed index outside the write lock, then acquire s.mu only to
atomically swap the completed snapshot and update its freshness metadata, while
preserving safe behavior for concurrent readers and avoiding duplicate refresh
work.

In `@internal/service/frontend/api/v1/docs.go`:
- Around line 465-482: Update SearchDocs to use a scoped docs.DocStore search
when the store exposes the scoped Search variant, passing workspaceName as
PathPrefix and the visibility-derived roots as ExcludePathRoots, consistent with
SearchDocFeed. Preserve the existing filtering and ID normalization behavior as
a safety check before constructing the response.

In `@internal/service/frontend/api/v1/search.go`:
- Around line 296-306: Add a short comment above the retry branch in the search
handler around SearchMatches explaining that SearchDocFeed may mint an
aggregate-scope cursor, which can be replayed with a workspace and yield
exec.ErrInvalidCursor when PathPrefix differs; document that the retry restores
aggregate scope while preserving the authorized aggregatePath.

In `@internal/service/frontend/sse/app_stream_test.go`:
- Around line 108-112: Update the test around watcher.Start and watcher.check to
avoid concurrent snapshot access: seed the watcher snapshot directly without
starting the polling loop, or explicitly document that the one-hour interval
keeps the loop idle. Prefer the no-loop setup so the test remains race-safe if
the default interval or Start behavior changes.

In `@internal/service/frontend/sse/app_stream.go`:
- Around line 343-358: Update the WalkDir callback in addCreatedDirWatches to
ignore errors wrapping os.ErrNotExist and continue the recursive walk, including
when the created directory or a child disappears before traversal. Preserve
returning all other errors so genuine failures still reach the caller without
triggering unnecessary behavior changes.

In `@ui/src/contexts/DocTabContext.tsx`:
- Around line 196-233: Update closeTab and the related mutators closeOtherTabs,
setDraft, clearDraft, markTabUnsaved, and markTabSaved so their state updater
callbacks only compute and return next values. Move setActiveTabId calls,
draftsRef/unsavedTabIdsRef writes, and persistState calls outside updater
functions, relying on the existing state-persistence effect and render-time ref
synchronization to handle updates without duplicate side effects.

In `@ui/src/hooks/__tests__/useContentEditor.test.ts`:
- Around line 8-31: Extend the useContentEditor test suite with Vitest coverage
for cancelSave: begin a save, call result.current.cancelSave(), then simulate an
external serverContent change and assert conflict.hasConflict becomes true. Keep
the existing server-echo acceptance test unchanged and use the hook’s public
API.

In `@ui/src/pages/docs/components/DocEditor.tsx`:
- Around line 334-347: Improve accessibility for the toolbar controls in
DocEditor: add an aria-label to the icon-only copy-file-path button using
copyFilePath, and add aria-pressed to both mode toggle buttons in the related
controls, reflecting each button’s active/selected state.
- Around line 284-320: Extract the shared clipboard write and textarea fallback
from copyFilePath and copyContent into a reusable copyText helper, then have
both callbacks call it while retaining their existing empty-value guards and
copied-state reset behavior. Keep the helper responsible only for copying text;
leave the file-path and content-specific state updates in their respective
callbacks.

In `@ui/src/pages/docs/components/DocExternalChangeDialog.tsx`:
- Around line 25-50: Remove the global Enter-handling React.useEffect and its
related keyboard logic from DocExternalChangeDialog, relying on native button
activation for onIgnore and onDiscard. Remove the now-unused ignoreButtonRef and
discardButtonRef declarations and ref props from both Button elements.

In `@ui/src/pages/docs/index.tsx`:
- Around line 203-205: Update the Docs page useEffect dependency array to depend
on the stable setTitle function rather than the entire appBarContext object,
matching the existing pattern in the Home page while preserving the
title-setting behavior.
- Around line 495-498: Update the delete failure branch in the document deletion
handler to include the server-provided error message via error?.message,
matching handleRenameModal and handlePathChange, while retaining the existing
fallback toast text when no message is available.
- Around line 705-745: Extract the shared CreateDocModal, RenameDocModal, and
both ConfirmModal declarations from the page component into a single modals
variable, preserving all existing props and handlers. Replace the duplicated
modal blocks in the mobile and desktop layout branches with {modals} so both
branches render the same definitions.

In `@ui/src/pages/docs/lib/doc-validation.ts`:
- Around line 11-31: Update the doc path validation function to use the existing
trimmed value for all subsequent length, extension, pattern, and segment checks,
while preserving the required-path check. Extract the 252-character limit into a
named constant documenting that it mirrors the backend document ID limit, and
update the affected test expectation for validateDocPath('guide ') to reflect
the trimmed valid path result.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: c1c6b783-bc1d-4ced-8ee5-4beaa859cb29

📥 Commits

Reviewing files that changed from the base of the PR and between d5ea66a and be0186e.

⛔ Files ignored due to path filters (2)
  • assets/images/readme-documents-dark.png is excluded by !**/*.png
  • ui/pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (100)
  • README.md
  • api/v1/api.gen.go
  • api/v1/api.yaml
  • internal/cmd/config.go
  • internal/cmd/config_test.go
  • internal/cmd/process/frontend_store_factories.go
  • internal/cmn/config/config.go
  • internal/cmn/config/definition.go
  • internal/cmn/config/loader.go
  • internal/cmn/config/loader_test.go
  • internal/cmn/value/builtin_context_test.go
  • internal/cmn/value/template.go
  • internal/core/docs/doc.go
  • internal/core/exec/context.go
  • internal/core/exec/context_env.go
  • internal/core/exec/context_test.go
  • internal/core/exec/env.go
  • internal/core/spec/builder.go
  • internal/gitsync/errors.go
  • internal/gitsync/service.go
  • internal/gitsync/service_test.go
  • internal/gitsync/state.go
  • internal/persis/file/doc/birthtime_darwin.go
  • internal/persis/file/doc/birthtime_linux.go
  • internal/persis/file/doc/birthtime_other.go
  • internal/persis/file/doc/birthtime_windows.go
  • internal/persis/file/doc/metadata.go
  • internal/persis/file/doc/rename_noreplace_darwin.go
  • internal/persis/file/doc/rename_noreplace_linux.go
  • internal/persis/file/doc/rename_noreplace_other.go
  • internal/persis/file/doc/store.go
  • internal/persis/file/doc/store_test.go
  • internal/persis/file/service_stores.go
  • internal/runtime/builtin/harness/harness.go
  • internal/runtime/builtin/harness/root_container_external_test.go
  • internal/runtime/builtin_context_test.go
  • internal/runtime/env_test.go
  • internal/runtime/eval.go
  • internal/service/audit/entry.go
  • internal/service/frontend/api/v1/api.go
  • internal/service/frontend/api/v1/docs.go
  • internal/service/frontend/api/v1/docs_test.go
  • internal/service/frontend/api/v1/search.go
  • internal/service/frontend/api/v1/sync.go
  • internal/service/frontend/api/v1/sync_test.go
  • internal/service/frontend/persistence.go
  • internal/service/frontend/server.go
  • internal/service/frontend/sse/app_stream.go
  • internal/service/frontend/sse/app_stream_test.go
  • internal/service/frontend/sse/topic_parse.go
  • internal/service/frontend/sse/types.go
  • internal/service/frontend/templates.go
  • internal/service/frontend/templates/base.gohtml
  • ui/package.json
  • ui/src/App.tsx
  • ui/src/__tests__/App.test.tsx
  • ui/src/api/v1/schema.ts
  • ui/src/contexts/ConfigContext.tsx
  • ui/src/contexts/DocTabContext.tsx
  • ui/src/features/search/components/SearchResult.tsx
  • ui/src/features/search/components/__tests__/SearchResult.test.tsx
  • ui/src/features/system-status/components/PathsCard.tsx
  • ui/src/hooks/SSEManager.ts
  • ui/src/hooks/__tests__/SSEManager.test.ts
  • ui/src/hooks/__tests__/useContentEditor.test.ts
  • ui/src/hooks/useContentEditor.ts
  • ui/src/hooks/useDocSSE.ts
  • ui/src/hooks/useDocTreeSSE.ts
  • ui/src/layouts/ContentNavigation.tsx
  • ui/src/menu.tsx
  • ui/src/pages/__tests__/DocArboristNode.test.tsx
  • ui/src/pages/audit-logs/index.tsx
  • ui/src/pages/docs/components/CreateDocModal.tsx
  • ui/src/pages/docs/components/DocArboristNode.tsx
  • ui/src/pages/docs/components/DocEditor.tsx
  • ui/src/pages/docs/components/DocExternalChangeDialog.tsx
  • ui/src/pages/docs/components/DocOutlinePanel.tsx
  • ui/src/pages/docs/components/DocTabBar.tsx
  • ui/src/pages/docs/components/DocTabEditorPanel.tsx
  • ui/src/pages/docs/components/DocTreeSidebar.tsx
  • ui/src/pages/docs/components/RenameDocModal.tsx
  • ui/src/pages/docs/index.tsx
  • ui/src/pages/docs/lib/__tests__/doc-mutation.test.ts
  • ui/src/pages/docs/lib/__tests__/doc-url.test.ts
  • ui/src/pages/docs/lib/__tests__/doc-validation.test.ts
  • ui/src/pages/docs/lib/doc-mutation.ts
  • ui/src/pages/docs/lib/doc-polling.ts
  • ui/src/pages/docs/lib/doc-url.ts
  • ui/src/pages/docs/lib/doc-validation.ts
  • ui/src/pages/git-sync/BatchDeleteDialog.tsx
  • ui/src/pages/git-sync/CleanupDialog.tsx
  • ui/src/pages/git-sync/DeleteDialog.tsx
  • ui/src/pages/git-sync/DeleteMissingDialog.tsx
  • ui/src/pages/git-sync/ForgetDialog.tsx
  • ui/src/pages/git-sync/MoveDialog.tsx
  • ui/src/pages/git-sync/index.tsx
  • ui/src/pages/git-sync/sync-kind.ts
  • ui/src/pages/git-sync/useSyncReconcile.ts
  • ui/src/pages/home/index.tsx
  • ui/src/pages/search/index.tsx

Comment thread internal/gitsync/service.go
Comment thread internal/gitsync/service.go Outdated
Comment thread internal/gitsync/state.go
Comment thread internal/persis/file/doc/metadata.go
Comment thread internal/persis/file/doc/metadata.go
Comment thread ui/src/pages/docs/components/DocArboristNode.tsx
Comment thread ui/src/pages/docs/components/DocEditor.tsx
Comment thread ui/src/pages/docs/components/DocTabBar.tsx
Comment thread ui/src/pages/docs/components/DocTreeSidebar.tsx
Comment thread ui/src/pages/git-sync/sync-kind.ts
@yohamta0
yohamta0 marked this pull request as ready for review August 4, 2026 01:36
Copilot AI review requested due to automatic review settings August 4, 2026 02:17

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

Copilot AI review requested due to automatic review settings August 4, 2026 02:19

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

yohamta0 commented Aug 4, 2026

Copy link
Copy Markdown
Member Author

Addressed the review in a5874db41 and 2ba6a421e.

Top-level findings:

  • Fixed the race-prone polling test setup, transient directory-removal handling, aggregate-cursor rationale, tab-state persistence side effects, app-title dependency, delete error detail, duplicated modal markup, path validation constants/normalization, editor accessibility and clipboard duplication, cancelled-save conflict coverage, redundant dialog key handling, cleaned store roots, Windows reserved names, explicit case-sensitive search coverage, and Linux creation-time fallback.
  • Kept the legacy unpaginated SearchDocs implementation unchanged: authorization filtering is correct, and the suggested scoped API does not exist on that store contract. The cursor-based search path already performs scoped storage queries.
  • Kept index rebuilding under the write lock: publishing a rebuilt snapshot outside the lock needs generation/version coordination to avoid overwriting concurrent mutation updates. The current implementation favors correctness; a lock-duration redesign should be separate and measured.
  • The outside-diff watcher leak does not reproduce: addWatch closes w.watcher before returning any Add error, covering both root and child calls. Adding another close in Start would only duplicate cleanup.

Validation completed: full race-enabled Go suite, full UI suite, production UI build, TypeScript typecheck, macOS and Windows Go lint, and Windows cross-compilation.

yohamta0 commented Aug 4, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Aug 4, 2026 •

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copilot AI review requested due to automatic review settings August 4, 2026 02:26

yohamta0 commented Aug 4, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Aug 4, 2026 •

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
ui/src/hooks/__tests__/useContentEditor.test.ts (1)

20-29: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Test markAsSaved independently.

The matching server echo already makes the editor clean before markAsSaved runs. The final assertion passes even if markAsSaved stops updating the saved-content reference.

Assert that the echo clears hasUnsavedChanges. Then add a separate local edit, assert it is unsaved, call markAsSaved, rerender without changing serverContent, and assert that it is clean.

Proposed test update
     rerender({ serverContent: '# Saved' });

     expect(result.current.conflict.hasConflict).toBe(false);
+    expect(result.current.hasUnsavedChanges).toBe(false);
+
+    act(() => {
+      result.current.setCurrentValue('# Saved again');
+    });
+    expect(result.current.hasUnsavedChanges).toBe(true);

     act(() => {
-      result.current.markAsSaved('# Saved');
+      result.current.markAsSaved('# Saved again');
     });
     rerender({ serverContent: '# Saved' });

     expect(result.current.hasUnsavedChanges).toBe(false);

As per coding guidelines, add or update tests appropriate to the changed code.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@ui/src/hooks/__tests__/useContentEditor.test.ts` around lines 20 - 29, Update
the test around the hook’s server-echo handling and markAsSaved flow so it first
asserts the matching server content clears hasUnsavedChanges without calling
markAsSaved. Then perform a separate local edit, verify it becomes unsaved, call
result.current.markAsSaved with that content, rerender while keeping
serverContent unchanged, and assert hasUnsavedChanges is false to independently
validate markAsSaved.

Source: Coding guidelines

🧹 Nitpick comments (1)
ui/src/hooks/__tests__/useContentEditor.test.ts (1)

10-12: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Add a props type for the renderHook callbacks.

The test callbacks both destructure { serverContent } without a TypeScript annotation. Define one shared props type and use it for both callbacks.

  • ui/src/hooks/__tests__/useContentEditor.test.ts#L10-L12
  • ui/src/hooks/__tests__/useContentEditor.test.ts#L33-L35
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@ui/src/hooks/__tests__/useContentEditor.test.ts` around lines 10 - 12, Define
one shared props type for the renderHook callbacks in
ui/src/hooks/__tests__/useContentEditor.test.ts at lines 10-12 and 33-35, then
annotate both destructured callback parameters with that type while preserving
their existing serverContent behavior.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@ui/src/contexts/DocTabContext.tsx`:
- Around line 281-287: Update setDraft in DocTabContext to ignore drafts when
the direct tabId or its encoded key does not match any tab in tabsRef.current.
Only add the draft to the copied drafts map for an open tab, preventing
DocEditor unmount cleanup from persisting discarded content after closure.

---

Outside diff comments:
In `@ui/src/hooks/__tests__/useContentEditor.test.ts`:
- Around line 20-29: Update the test around the hook’s server-echo handling and
markAsSaved flow so it first asserts the matching server content clears
hasUnsavedChanges without calling markAsSaved. Then perform a separate local
edit, verify it becomes unsaved, call result.current.markAsSaved with that
content, rerender while keeping serverContent unchanged, and assert
hasUnsavedChanges is false to independently validate markAsSaved.

---

Nitpick comments:
In `@ui/src/hooks/__tests__/useContentEditor.test.ts`:
- Around line 10-12: Define one shared props type for the renderHook callbacks
in ui/src/hooks/__tests__/useContentEditor.test.ts at lines 10-12 and 33-35,
then annotate both destructured callback parameters with that type while
preserving their existing serverContent behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 78456a52-69ea-42f7-9d22-0ab3c3cfca1c

📥 Commits

Reviewing files that changed from the base of the PR and between be0186e and b163f59.

📒 Files selected for processing (34)
  • api/v1/api.gen.go
  • api/v1/api.yaml
  • internal/cmd/sync.go
  • internal/core/docs/doc.go
  • internal/gitsync/pull_external_test.go
  • internal/gitsync/service.go
  • internal/gitsync/service_test.go
  • internal/persis/file/doc/birthtime_linux.go
  • internal/persis/file/doc/metadata.go
  • internal/persis/file/doc/rename_noreplace_darwin.go
  • internal/persis/file/doc/rename_noreplace_fallback.go
  • internal/persis/file/doc/rename_noreplace_linux.go
  • internal/persis/file/doc/rename_noreplace_other.go
  • internal/persis/file/doc/rename_noreplace_windows.go
  • internal/persis/file/doc/store.go
  • internal/persis/file/doc/store_test.go
  • internal/service/frontend/api/v1/docs.go
  • internal/service/frontend/api/v1/docs_test.go
  • internal/service/frontend/api/v1/search.go
  • internal/service/frontend/server.go
  • internal/service/frontend/sse/app_stream.go
  • internal/service/frontend/sse/app_stream_test.go
  • ui/src/api/v1/schema.ts
  • ui/src/contexts/DocTabContext.tsx
  • ui/src/hooks/__tests__/useContentEditor.test.ts
  • ui/src/hooks/useDocSSE.ts
  • ui/src/pages/docs/components/DocArboristNode.tsx
  • ui/src/pages/docs/components/DocEditor.tsx
  • ui/src/pages/docs/components/DocExternalChangeDialog.tsx
  • ui/src/pages/docs/components/DocTabBar.tsx
  • ui/src/pages/docs/components/DocTabEditorPanel.tsx
  • ui/src/pages/docs/index.tsx
  • ui/src/pages/docs/lib/__tests__/doc-validation.test.ts
  • ui/src/pages/docs/lib/doc-validation.ts
💤 Files with no reviewable changes (1)
  • internal/persis/file/doc/birthtime_linux.go
🚧 Files skipped from review as they are similar to previous changes (20)
  • internal/persis/file/doc/rename_noreplace_darwin.go
  • internal/persis/file/doc/metadata.go
  • internal/service/frontend/sse/app_stream_test.go
  • internal/persis/file/doc/rename_noreplace_linux.go
  • ui/src/pages/docs/lib/doc-validation.ts
  • internal/gitsync/service_test.go
  • ui/src/pages/docs/lib/tests/doc-validation.test.ts
  • internal/core/docs/doc.go
  • internal/service/frontend/server.go
  • ui/src/hooks/useDocSSE.ts
  • internal/service/frontend/api/v1/search.go
  • ui/src/pages/docs/components/DocArboristNode.tsx
  • internal/service/frontend/sse/app_stream.go
  • ui/src/pages/docs/components/DocEditor.tsx
  • ui/src/pages/docs/index.tsx
  • internal/persis/file/doc/store.go
  • internal/service/frontend/api/v1/docs.go
  • ui/src/api/v1/schema.ts
  • internal/service/frontend/api/v1/docs_test.go
  • api/v1/api.gen.go

Comment thread ui/src/contexts/DocTabContext.tsx Outdated
Copilot AI review requested due to automatic review settings August 4, 2026 04:05

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

yohamta0 commented Aug 4, 2026

Copy link
Copy Markdown
Member Author

Follow-up on the latest pre-merge warnings:

  • Issue feat: The docs feature is an useful feature #2368 asks for the removed Docs experience to return. The script example in the issue uses a subdirectory of docs_dir as a DAG working_dir and invokes a user-defined $exec command; it does not depend on a removed Dagu document-script executor. Restoring the configurable Docs directory, editor, and Git Sync preserves that workflow without adding a new execution mechanism.
  • The license status, activation, and deactivation endpoints are already present on main. This PR inserts the restored Docs endpoints after that existing section; it does not add or alter the license API.
  • The blanket docstring-coverage suggestion is not actionable for this mixed Go/TypeScript restoration and conflicts with the repository's contract-focused comment convention. Public contracts have focused documentation where callers need it; adding boilerplate comments solely to satisfy an external percentage would reduce signal.

yohamta0 commented Aug 4, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Aug 4, 2026 •

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@yohamta0

yohamta0 commented Aug 4, 2026

Copy link
Copy Markdown
Member Author

Addressed all CodeRabbit findings from the e573eb1 review in 824a5ee.

  • Reject document-directory moves into the same path or any descendant in both the tree resolver and backend store; API and store regressions verify that source documents remain unchanged.
  • Clear persisted drafts and pending persistence timers when a user discards an external-change conflict; the component regression persists a draft, discards the conflict, remounts, and verifies the draft does not return.
  • Check DocsDir creation during document-store construction and propagate startup errors, with success and failure coverage.
  • Canonicalize direct and scoped draft keys for lookup and cleanup, with compatibility coverage.
  • Assert the batch path-conflict error, log failed workspace-document rollback paths, cover occupied workspace rename targets, and use the requested descriptive filePath local.

Validation: make fmt; full race-enabled make test; all UI tests; TypeScript typecheck; changed-file ESLint; and production UI build. Individual replies were posted on both inline threads and both threads are resolved.

@yohamta0

yohamta0 commented Aug 4, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Aug 4, 2026 •

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@yohamta0

yohamta0 commented Aug 4, 2026

Copy link
Copy Markdown
Member Author

Pre-merge warning clarification:

  • Issue feat: The docs feature is an useful feature #2368 describes editing script content in Docs and invoking it from DAG steps through the users existing custom $exec command. This PR restores that workflow: document editing is back, and each run receives the workspace-aware document path through DAG_DOCS_DIR and context.paths.docs_dir. It does not require a new built-in document-script executor.
  • The DAG-run spec test change is test-only and was made because the full race suite exposed an unrelated subprocess timing dependency. The replacement seeds the stored run state that the endpoint contract actually reads, removing the flake without changing production behavior.
  • The blanket docstring-coverage suggestion is not a repository requirement. Public APIs added here retain contract comments; adding narrative comments to internal helpers would conflict with the repository comment guidance.

The final incremental CodeRabbit review reported no actionable comments, and all review threads are resolved.

Copilot AI review requested due to automatic review settings August 4, 2026 09:41

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot wasn't able to review this pull request because it exceeds the maximum number of lines (20,000). Try reducing the number of changed lines and requesting a review from Copilot again.

@yohamta0

yohamta0 commented Aug 4, 2026

Copy link
Copy Markdown
Member Author

Cleanup follow-up pushed in e9550ac..691e28b.

  • document-store construction now propagates initialization failures
  • document mutations and draft persistence are centralized and covered by focused frontend tests
  • document store/API/search/tree code and tests are split by responsibility
  • GitSync internals now use sync-item terminology while preserving existing state and JSON keys
  • removed the inaccurate license-endpoints release note from the PR description

Validation: make fmt; make check; full race-enabled make test; frontend typecheck; all frontend unit tests; targeted Docs ESLint; and production frontend build.

@yohamta0

yohamta0 commented Aug 4, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Aug 4, 2026 •

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
internal/service/frontend/api/v1/sync.go (1)

696-704: 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Preserve the tracked Markdown extension for document file paths.

syncItemFilePath returns itemID + ".md" for every document. Git Sync preserves the local extension casing for documents; scanDocFiles stores .MD for deploy.MD, and TestScanLocalDAGs asserts that value. For such an item the API reports docs/operations/deploy.md, which does not match the file on disk. Use the tracked extension when it is a Markdown extension.

🔧 Proposed fix
 func syncItemFilePath(itemID, fileExtension string) string {
 	if gitsync.SyncItemKindForID(itemID) == gitsync.SyncItemKindDoc {
+		if strings.EqualFold(fileExtension, ".md") {
+			return itemID + fileExtension
+		}
 		return itemID + ".md"
 	}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/service/frontend/api/v1/sync.go` around lines 696 - 704, Update
syncItemFilePath to preserve the tracked Markdown extension for document items
instead of always appending lowercase ".md". When fileExtension represents
Markdown, return itemID with that extension and retain its casing; preserve the
existing YAML/YML handling for non-document items.
🧹 Nitpick comments (7)
ui/src/pages/docs/hooks/__tests__/useDocDraftPersistence.test.tsx (1)

19-22: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Both new test files rely on vi.clearAllMocks() to reset mock behavior. vi.clearAllMocks() clears call history only. Return values and resolved values set with mockReturnValue or mockResolvedValue persist into later tests, so the tests become order dependent.

  • ui/src/pages/docs/hooks/__tests__/useDocDraftPersistence.test.tsx#L19-L22: replace vi.clearAllMocks() with vi.resetAllMocks() so the mocks.getDraft return value from the restore test does not leak.
  • ui/src/pages/docs/hooks/__tests__/useDocMutations.test.tsx#L40-L44: replace vi.clearAllMocks() with vi.resetAllMocks() so the mocks.client.POST resolved value from the rename test does not leak.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@ui/src/pages/docs/hooks/__tests__/useDocDraftPersistence.test.tsx` around
lines 19 - 22, Replace vi.clearAllMocks() with vi.resetAllMocks() in the
beforeEach blocks of
ui/src/pages/docs/hooks/__tests__/useDocDraftPersistence.test.tsx lines 19-22
and ui/src/pages/docs/hooks/__tests__/useDocMutations.test.tsx lines 40-44,
ensuring mock return and resolved values from tests such as the restore and
rename tests do not persist between cases.
internal/persis/file/doc/tree_test.go (1)

126-143: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Make the directory mtime assertion distinguish the two orderings.

dir-new precedes dir-old both by descending propagated mtime and by ascending name. TestListTreeSortMtimeKeepsDirectoriesAlphabetical shows that directories keep alphabetical order under mtime sort, so this test passes under either rule and does not prove propagation. Choose names whose alphabetical order opposes the expected mtime order, for example a-dir as the oldest and b-dir as the newest.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/persis/file/doc/tree_test.go` around lines 126 - 143, Update
TestListTreeSortMtimeWithDirectories to use directory names whose alphabetical
order opposes their modification-time order, such as making a-dir the older
directory and b-dir the newer one. Adjust the created paths, setModTime targets,
expected item IDs, and ModTime assertion so the test specifically verifies
descending propagated directory mtime rather than alphabetical ordering.
internal/cmd/sync.go (1)

246-257: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Use one sort helper in this file.

Line 252 uses sort.Strings, while the new code at Lines 123, 433, and 527 uses slices.Sort. Use slices.Sort here for consistency.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/cmd/sync.go` around lines 246 - 257, Update the itemIDs sorting in
the sync command flow around PublishAll to use slices.Sort instead of
sort.Strings, matching the sorting approach already used elsewhere in the file.
Ensure the corresponding sort package import is removed if no longer referenced.
internal/persis/file/service_stores.go (1)

161-165: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Rename the local variable to avoid shadowing the store package.

The file imports a package named store and uses it in NewDAGSettingsStore. The local variable store here shadows that package inside this function. Rename it for clarity.

♻️ Proposed change
-	store, err := filedoc.New(cfg.Paths.DocsDir)
+	docStore, err := filedoc.New(cfg.Paths.DocsDir)
 	if err != nil {
 		return nil, fmt.Errorf("document store: %w", err)
 	}
-	return store, nil
+	return docStore, nil
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/persis/file/service_stores.go` around lines 161 - 165, Rename the
local variable assigned from filedoc.New in the document store constructor, and
update the return statement to use the new name, avoiding shadowing of the
imported store package while preserving the existing error handling.
internal/service/frontend/api/v1/docs_search_test.go (1)

132-132: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Use checked type assertions for the response values.

Lines 132 and 150 use unchecked type assertions. If the handler returns an error response, the test panics instead of reporting a clear failure. The other tests in this file use require.True(t, ok). Apply the same pattern here.

♻️ Proposed change
-	feedPage := feedResp.(apigen.SearchDocFeed200JSONResponse)
+	feedPage, ok := feedResp.(apigen.SearchDocFeed200JSONResponse)
+	require.True(t, ok)
-	matchesPage := matchesResp.(apigen.SearchDocMatches200JSONResponse)
+	matchesPage, ok := matchesResp.(apigen.SearchDocMatches200JSONResponse)
+	require.True(t, ok)

Also applies to: 150-150

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/service/frontend/api/v1/docs_search_test.go` at line 132, Update the
response assertions around feedPage and the corresponding response at the second
unchecked assertion to use checked type assertions, then call require.True(t,
ok) before using the asserted values so unexpected error responses produce a
clear test failure instead of panicking.
internal/gitsync/service_test.go (1)

130-157: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Rename the test to match scanLocalItems.

The test now exercises scanLocalItems and asserts document classification, but the name still says TestScanLocalDAGs. Rename it to TestScanLocalItems so the coverage target is clear.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/gitsync/service_test.go` around lines 130 - 157, Rename the test
function TestScanLocalDAGs to TestScanLocalItems so its name matches the
scanLocalItems method it exercises; leave the test body unchanged.
internal/persis/file/doc/search.go (1)

41-44: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Align Search with SearchCursor match semantics.

DefaultGrepOptions uses regexp matching, but Search sends the raw query while SearchCursor uses docSearchPattern() with (?i) plus regexp.QuoteMeta. Use the same pattern construction in both paths.

♻️ Proposed change for `Search`
-		matches, err := grep.Grep(data, query, grep.DefaultGrepOptions)
+		matches, err := grep.Grep(data, docSearchPattern(query), grep.GrepOptions{
+			IsRegexp: true,
+			Before:   grep.DefaultGrepOptions.Before,
+			After:    grep.DefaultGrepOptions.After,
+		})
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/persis/file/doc/search.go` around lines 41 - 44, Update Search’s
grep pattern construction around grep.Grep to use docSearchPattern(query),
matching SearchCursor’s case-insensitive, regexp-escaped semantics instead of
passing the raw query; leave the existing match handling unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@internal/cmd/sync.go`:
- Line 660: Update the completion message in the sync item move flow to start
with an uppercase letter, changing it to “Sync item moved successfully” while
leaving the surrounding behavior unchanged.
- Around line 110-112: Update the counts-section header in the sync command to
replace “DAG Status Counts” with wording that reflects all included items,
including documents, and remains consistent with “Sync items with pending
changes.” Locate the header near the pending-change output in the sync flow.

In `@internal/gitsync/state.go`:
- Around line 217-228: Update normalizeTrackedItems to delete entries whose
itemState is nil from state.Items before continuing, rather than retaining them.
Preserve the existing validation and Kind normalization behavior for non-nil
entries.

In `@internal/persis/file/doc/store_test.go`:
- Around line 1280-1284: Update TestNewReturnsBaseDirCreationError to create a
temporary regular file, then pass a child path beneath that file to New so
directory creation reliably fails on Windows and other platforms. Replace the
POSIX-specific "/dev/null/impossible" fixture while preserving the existing
error and nil-store assertions.

---

Outside diff comments:
In `@internal/service/frontend/api/v1/sync.go`:
- Around line 696-704: Update syncItemFilePath to preserve the tracked Markdown
extension for document items instead of always appending lowercase ".md". When
fileExtension represents Markdown, return itemID with that extension and retain
its casing; preserve the existing YAML/YML handling for non-document items.

---

Nitpick comments:
In `@internal/cmd/sync.go`:
- Around line 246-257: Update the itemIDs sorting in the sync command flow
around PublishAll to use slices.Sort instead of sort.Strings, matching the
sorting approach already used elsewhere in the file. Ensure the corresponding
sort package import is removed if no longer referenced.

In `@internal/gitsync/service_test.go`:
- Around line 130-157: Rename the test function TestScanLocalDAGs to
TestScanLocalItems so its name matches the scanLocalItems method it exercises;
leave the test body unchanged.

In `@internal/persis/file/doc/search.go`:
- Around line 41-44: Update Search’s grep pattern construction around grep.Grep
to use docSearchPattern(query), matching SearchCursor’s case-insensitive,
regexp-escaped semantics instead of passing the raw query; leave the existing
match handling unchanged.

In `@internal/persis/file/doc/tree_test.go`:
- Around line 126-143: Update TestListTreeSortMtimeWithDirectories to use
directory names whose alphabetical order opposes their modification-time order,
such as making a-dir the older directory and b-dir the newer one. Adjust the
created paths, setModTime targets, expected item IDs, and ModTime assertion so
the test specifically verifies descending propagated directory mtime rather than
alphabetical ordering.

In `@internal/persis/file/service_stores.go`:
- Around line 161-165: Rename the local variable assigned from filedoc.New in
the document store constructor, and update the return statement to use the new
name, avoiding shadowing of the imported store package while preserving the
existing error handling.

In `@internal/service/frontend/api/v1/docs_search_test.go`:
- Line 132: Update the response assertions around feedPage and the corresponding
response at the second unchecked assertion to use checked type assertions, then
call require.True(t, ok) before using the asserted values so unexpected error
responses produce a clear test failure instead of panicking.

In `@ui/src/pages/docs/hooks/__tests__/useDocDraftPersistence.test.tsx`:
- Around line 19-22: Replace vi.clearAllMocks() with vi.resetAllMocks() in the
beforeEach blocks of
ui/src/pages/docs/hooks/__tests__/useDocDraftPersistence.test.tsx lines 19-22
and ui/src/pages/docs/hooks/__tests__/useDocMutations.test.tsx lines 40-44,
ensuring mock return and resolved values from tests such as the restore and
rename tests do not persist between cases.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 0b742bae-c9ce-4d66-a301-ed573c6a218a

📥 Commits

Reviewing files that changed from the base of the PR and between 824a5ee and 691e28b.

📒 Files selected for processing (25)
  • internal/cmd/sync.go
  • internal/gitsync/pull_external_test.go
  • internal/gitsync/service.go
  • internal/gitsync/service_test.go
  • internal/gitsync/state.go
  • internal/persis/file/doc/search.go
  • internal/persis/file/doc/store.go
  • internal/persis/file/doc/store_test.go
  • internal/persis/file/doc/tree.go
  • internal/persis/file/doc/tree_test.go
  • internal/persis/file/service_stores.go
  • internal/service/frontend/api/v1/docs.go
  • internal/service/frontend/api/v1/docs_response.go
  • internal/service/frontend/api/v1/docs_search_test.go
  • internal/service/frontend/api/v1/docs_test.go
  • internal/service/frontend/api/v1/docs_visibility.go
  • internal/service/frontend/api/v1/sync.go
  • internal/service/frontend/api/v1/sync_test.go
  • ui/src/contexts/DocTabContext.tsx
  • ui/src/pages/docs/components/DocEditor.tsx
  • ui/src/pages/docs/hooks/__tests__/useDocDraftPersistence.test.tsx
  • ui/src/pages/docs/hooks/__tests__/useDocMutations.test.tsx
  • ui/src/pages/docs/hooks/useDocDraftPersistence.ts
  • ui/src/pages/docs/hooks/useDocMutations.ts
  • ui/src/pages/docs/index.tsx
💤 Files with no reviewable changes (2)
  • internal/service/frontend/api/v1/docs_test.go
  • internal/service/frontend/api/v1/docs.go
🚧 Files skipped from review as they are similar to previous changes (5)
  • internal/gitsync/pull_external_test.go
  • ui/src/pages/docs/components/DocEditor.tsx
  • ui/src/contexts/DocTabContext.tsx
  • internal/service/frontend/api/v1/sync_test.go
  • ui/src/pages/docs/index.tsx

Comment thread internal/cmd/sync.go
Comment thread internal/cmd/sync.go Outdated
Comment thread internal/gitsync/state.go
Comment thread internal/persis/file/doc/store_test.go
Copilot AI review requested due to automatic review settings August 4, 2026 09:58

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot wasn't able to review this pull request because it exceeds the maximum number of lines (20,000). Try reducing the number of changed lines and requesting a review from Copilot again.

@yohamta0

yohamta0 commented Aug 4, 2026

Copy link
Copy Markdown
Member Author

Addressed the cleanup review in ff871a4.

  • fixed all four inline findings and replied on/resolved each thread
  • preserved tracked Markdown extension casing in Sync item file paths
  • reset mock behavior between both new hook test suites
  • made the document-store construction failure fixture portable
  • pruned null persisted Sync items and added load-level regression coverage
  • standardized CLI sorting/naming, removed store-package shadowing, checked response assertions, and renamed the scan test
  • rewrote the directory-mtime test to verify propagated mtimes independently of ordering; applying the suggested mtime ordering would conflict with the existing contract that directories stay alphabetic

The Search suggestion was intentionally not applied: Store.Search is the restored legacy pattern-search API (regex and case-sensitive), while SearchCursor/SearchMatches power global search and intentionally treat the user query as a case-insensitive literal. Existing contract tests cover that distinction.

Validation: focused Go packages; all frontend unit tests; targeted ESLint; and clean Linux/Windows make check.

@yohamta0

yohamta0 commented Aug 4, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Aug 4, 2026 •

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@yohamta0

yohamta0 commented Aug 4, 2026

Copy link
Copy Markdown
Member Author

Final cleanup and review pass is complete.

  • CodeRabbit re-reviewed ff871a45d; no additional actionable feedback was posted
  • all review threads are resolved (0 unresolved)
  • frontend build, Go lint, Linux/Windows tests, Linux/Windows conformance, and all three browser E2E shards are green
  • PR is mergeable with a clean merge state, and the branch HEAD matches the pushed remote
  • the PR body retains Closes #2368

@yohamta0
yohamta0 merged commit e9afa5e into main Aug 4, 2026
14 checks passed
@yohamta0
yohamta0 deleted the agent/restore-docs-feature branch August 4, 2026 11:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat: The docs feature is an useful feature

2 participants