Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
85 changes: 85 additions & 0 deletions backlog/tasks/back-481 - Add-wiki-to-web-search.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
---
id: BACK-481
title: Add wiki to web search
status: Done
assignee: []
created_date: '2026-05-22'
updated_date: '2026-05-22'
labels:
- web-ui
- search
- wiki
- enhancement
dependencies: []
priority: medium
---

## Description

Currently the web UI search (top-left search box in the browser) only supports three result types: **task**, **document**, and **decision**.
Wiki pages are accessible via their own routes (`/wiki/*`) but are **not indexed** by the shared `SearchService`, so they cannot be found through the unified search.

This task adds wiki pages to the web search scope, allowing users to quickly find wiki content from the global search bar.

## Acceptance Criteria

- [x] #1 Wiki pages are indexed by `SearchService` alongside tasks, documents, and decisions
- [x] #2 `SearchResultType` includes `"wiki"` in type definitions
- [x] #3 Server `/api/search` endpoint accepts `type=wiki` and returns wiki results
- [x] #4 Web search UI (`SideNavigation.tsx`) renders wiki results with correct title/path
- [x] #5 Clicking a wiki search result navigates to the corresponding `/wiki/${path}` page
- [x] #6 Search query syntax `type:wiki <keyword>` works in the web search box
- [x] #7 Existing search behavior for task/document/decision remains unchanged

## Implementation Plan

<!-- SECTION:PLAN:BEGIN -->
1. **Extend type definitions** (`src/types/index.ts`): add `"wiki"` to `SearchResultType`, create `WikiSearchResult` interface.
2. **Add bulk wiki loading** (`src/file-system/operations.ts`): implement `listWikiPages()` to recursively read all `.md` files under `backlog/wiki/` and parse frontmatter.
3. **Extend ContentStore** (`src/core/content-store.ts`): add `wikis` to `ContentSnapshot`, load wiki pages during initialization, wire wiki file watcher to refresh the cache and emit `"wikis"` events.
4. **Extend SearchService** (`src/core/search-service.ts`): add `WikiSearchEntity` with `title` (from frontmatter or filename), `bodyText` (content), and `fileName` (for filename search); include wiki in Fuse index with `fileName` key at weight 0.25.
5. **Update server search endpoint** (`src/server/index.ts`): expand allowed `SearchResultType` list to include `"wiki"`.
6. **Update CLI search** (`src/cli.ts`): add `"wiki"` to `allowedTypes`, skip wiki results in plain-text CLI output (web-only scope).
7. **Update web search UI** (`src/web/components/SideNavigation.tsx`): import `WikiSearchResult`, add wiki icon, render wiki results with title/path metadata, link to `/wiki/${path}`.
8. **Validate**: run `bunx tsc --noEmit`, `bun test` (search-related suites), `bun run check --write`.
<!-- SECTION:PLAN:END -->

## Implementation Notes

<!-- SECTION:NOTES:BEGIN -->
**Architecture decision — chose Option A (extend ContentStore)**
Rather than having SearchService read wiki data directly from FileSystem, wiki pages were integrated into `ContentStore`'s existing snapshot/event pipeline. This keeps the data flow consistent with tasks/documents/decisions and gives SearchService automatic cache invalidation via the wiki file watcher.

**Title resolution for wiki pages**
Wiki files don't have a mandatory `title` field. SearchService derives the display title with this priority:
1. `frontmatter.title` if present and non-empty
2. Filename without `.md` extension

**Search weights**
Wiki pages use the same Fuse.js configuration as other entities, with one additional key:
- `fileName` (weight 0.25) — enables searching by filename even when it doesn't appear in the body text

**CLI scope note**
The `search` CLI command now recognizes `--type wiki`, but wiki results are intentionally skipped in plain-text output because the terminal TUI does not have a wiki viewer. The scope remains web-only per acceptance criteria.

**Verification evidence**
- `bun test src/test/search-service.test.ts` → pass (6 passed, 0 failed)
- `bun test src/test/server-search-endpoint.test.ts src/test/cli-search-command.test.ts` → pass (22 passed, 0 failed)
- `bunx tsc --noEmit` → no new errors (2 pre-existing unrelated warnings in PathAutocomplete.tsx)
- `bun run check --write` → 3 files auto-formatted, no lint errors introduced
<!-- SECTION:NOTES:END -->

## Definition of Done

<!-- DOD:BEGIN -->
- [x] #1 `bunx tsc --noEmit` passes with no new type errors
- [x] #2 `bun run check --write` passes with no new lint/format errors
- [x] #3 `bun test` passes for search-related test suites
- [x] #4 All acceptance criteria are satisfied
<!-- DOD:END -->

## Notes

- The terminal (CLI/TUI) does not support wiki currently; this scope is **web-only**.
- `SearchService` lives in `src/core/search-service.ts` and uses Fuse.js for fuzzy matching.
- Wiki API endpoints (`/api/wiki/*`) already exist; the main work is wiring wiki content into the search index.
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
---
id: BACK-482
title: Fix wikilink and Markdown relative-link preview in wiki pages
status: Done
assignee:
- '@agent'
created_date: '2026-05-22 02:02'
updated_date: '2026-05-22 07:22'
labels:
- web-ui
- bug
- wiki
dependencies: []
priority: medium
ordinal: 27001
---

## Description

<!-- SECTION:DESCRIPTION:BEGIN -->
Clicking a [[wikilink]] that contains '..' (e.g. [[../developer-notes/security-gotchas]]) shows 'Failed to fetch wiki page' error. The backend readWikiPage() treats the raw relative path as wikiRoot-relative, causing the containment check to reject valid parent-directory references.

Additionally, relative Markdown links (e.g. [子任务与依赖](10-任务管理/03-子任务与依赖.md)) in usermanual/ pages navigate to broken URLs instead of opening a preview modal.
<!-- SECTION:DESCRIPTION:END -->

## Acceptance Criteria
<!-- AC:BEGIN -->
- [x] #1 Clicking a [[wikilink]] with '..' resolves the relative path against the current page's directory, then passes the resolved wikiRoot-relative path to the backend.
- [x] #2 Backend containment check still rejects actual directory-traversal attacks (paths escaping wikiRoot).
- [x] #3 Absolute paths in wikilinks are rejected on the frontend before fetching.
- [x] #4 Tests cover parent-directory wikilinks, sibling-directory wikilinks, and traversal-attack wikilinks.
- [x] #5 Relative Markdown links in all wiki pages are intercepted and resolved against the current page directory, opening a preview modal instead of navigating away.
- [x] #6 Markdown links that escape the project root are blocked with a console warning.
<!-- AC:END -->



## Implementation Plan

<!-- SECTION:PLAN:BEGIN -->
1. Modify readWikiPage() to accept an optional rootDir parameter, keeping wiki/ as the default.
2. Update handleGetWikiPage() to strip wiki/ prefix from resolved wiki-internal paths, and fallback to backlog project root for sibling-directory references (e.g. wiki_output/).
3. Update resolveWikiPath() to treat the current page as residing under wiki/ for correct relative resolution against the project root; return null only when traversal escapes the project root.
4. Add resolveMarkdownLink() helper for standard Markdown relative paths (no wiki/ prefix).
5. Wire resolveWikiPath() into WikiDetail sanitizedContent and WikiLinkPreview previewContent so wikilinks are resolved at render time; illegal links become strikethrough text (~~text~~).
6. Add click interceptors in both WikiDetail and WikiLinkPreview that resolve relative links before navigation; use useNavigate for SPA transitions in the preview modal.
7. Expand Markdown link interception from usermanual/ only to all wiki pages.
8. Add unit tests and run checks.
<!-- SECTION:PLAN:END -->

## Implementation Notes

<!-- SECTION:NOTES:BEGIN -->
Root cause: readWikiPage() was locked to the wiki/ subdirectory, so wikilinks like ../wiki_output/... failed even though the target files live inside the same backlog project. The frontend also passed raw relative paths straight through without resolving them against the current page.

Fix: expanded readWikiPage() containment to the backlog project root via an optional rootDir parameter (still rejects paths escaping the project). Frontend now resolves .. segments using the current page path as base, treating the page as under wiki/ so ../ reaches sibling directories inside the project.

Security: write operations (saveWikiPage/createWikiPage) remain restricted to wiki/; only read access was expanded. Paths that resolve above the project root return null and are rendered as strikethrough text so users cannot click them.

Regression fix: initial change to readWikiPage broke all wiki pages because it switched the root from wiki/ to backlog/. Fixed by making rootDir optional (defaults to wiki/) and adding smart fallback in handleGetWikiPage. All filesystem tests pass.

Additional fix for relative Markdown links in wiki pages:
- Added resolveMarkdownLink() helper for standard Markdown relative paths (no wiki/ prefix).
- Both WikiDetail and WikiLinkPreview now intercept relative links in all wiki pages, decode URL-encoded hrefs, resolve them against the current page directory, and open a preview modal or SPA-navigate instead of letting the browser jump to a broken URL.

Files modified:
- src/file-system/operations.ts
- src/server/index.ts
- src/web/components/WikiDetail.tsx
- src/test/resolve-wiki-path.test.ts
<!-- SECTION:NOTES:END -->

## Definition of Done
<!-- DOD:BEGIN -->
- [x] #1 bunx tsc --noEmit passes when TypeScript touched
- [x] #2 bun run check . passes when formatting/linting touched
- [x] #3 bun test (or scoped test) passes
<!-- DOD:END -->
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
---
id: BACK-483
title: 'Web UI: Sidebar resize and search type dropdown'
status: Done
assignee: []
created_date: '2026-05-22 15:17'
updated_date: '2026-05-22 16:20'
labels:
- web-ui
- ui
- ux
dependencies: []
priority: medium
ordinal: 33000
---

## Description

<!-- SECTION:DESCRIPTION:BEGIN -->
Optimize three areas of the Web UI:
1. The left sidebar supports dragging a divider to resize its width, with a ghost bar preview during drag and persistence to localStorage.
2. The search bar has a type dropdown inside the input, using consistent sidebar icons without added colors. 'All' is the default.
3. Wiki paths in URLs no longer encode '/' as '%2F', keeping subdirectory paths readable.
<!-- SECTION:DESCRIPTION:END -->

## Acceptance Criteria
<!-- AC:BEGIN -->
- [x] #1 Add a draggable resize handle on the right edge of the sidebar
- [x] #2 Adjust sidebar width in real-time while dragging the divider, with visual feedback
- [x] #3 Persist sidebar width to localStorage and restore it on page reload
- [x] #4 Enforce minimum and maximum width limits to prevent the sidebar from becoming invisible or too wide
- [x] #5 Add a dropdown menu next to the search bar listing available search type options
- [x] #6 Use a magnifying glass icon for the "All" option, and use respective icons for other types (tasks, docs, decisions, etc.)
- [x] #7 Default to "All"; users can switch to a specific type
- [x] #8 Trigger the corresponding search filter immediately when the type is switched
- [x] #9 Fix wiki URL encoding so that `/` separators remain readable instead of being encoded as `%2F`
- [x] #10 All changes pass type-checking and linting; related interactive components have test coverage
<!-- AC:END -->

## Implementation Plan

<!-- SECTION:PLAN:BEGIN -->
1. Sidebar resize:
- Add sidebarWidth state + isResizing flag.
- Add a 1px resize handle on the right edge.
- During drag, move a blue ghost bar via DOM ref (no React re-render) for smoothness.
- On mouseup, apply the final width to state + localStorage.
- Enforce min 200px / max 500px.
2. Search type dropdown:
- Add searchType state ('all' | SearchResultType).
- Place a clickable icon button inside the search input (left side, absolute).
- Dropdown lists All/Tasks/Documents/Decisions/Wiki with the same icons used in the sidebar.
- Pass selected type to apiClient.search; fallback to parsed query types when 'all'.
3. Wiki URL encoding:
- Introduce encodeWikiPath() in urlHelpers.ts: split by '/', encodeURIComponent each segment, join back with '/'.
- Replace all wiki encodeURIComponent calls in SideNavigation, WikiDetail, and api.ts.
<!-- SECTION:PLAN:END -->

## Implementation Notes

<!-- SECTION:NOTES:BEGIN -->
- Ghost bar approach chosen over live width mutation because React re-rendering on every mousemove caused noticeable lag.
- encodeWikiPath keeps '/' separators readable while still safely encoding special characters like spaces or CJK in individual path segments.
- Type-check and search-command-query tests pass; build blocked only by Windows file lock on dist/backlog.exe.
<!-- SECTION:NOTES:END -->

## Definition of Done
<!-- DOD:BEGIN -->
- [ ] #1 bunx tsc --noEmit passes when TypeScript touched
- [ ] #2 bun run check . passes when formatting/linting touched
- [ ] #3 bun test (or scoped test) passes
<!-- DOD:END -->
93 changes: 93 additions & 0 deletions backlog/tasks/back-484 - Web-UI-sort-optimization.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
---
id: BACK-484
title: 'Web UI sort optimization'
status: Done
assignee:
- '@kimi'
created_date: '2026-05-23 10:50'
updated_date: '2026-05-23 11:15'
labels:
- web-ui
- ui
- ux
dependencies: []
priority: low
ordinal: 33100
---

## Description

<!-- SECTION:DESCRIPTION:BEGIN -->
Unify and polish sort indicators across the Web UI for a consistent look and feel.

1. **Task List page (`/tasks`)** — Replaced the plain-text sort glyphs (`↕` `▲` `▼`) with a modern split-arrow design: left arrow for ascending, right arrow for descending. Inactive arrows are dimmed; the active direction lights up while the other stays muted. All three states share an identical outer frame so column headers never jitter when switching sort direction.

2. **Milestones page (`/milestones`)** — Added sortable table headers inside every milestone group (including "Unassigned Tasks" and individual milestones). Each group maintains its own independent sort state so sorting within one milestone does not affect others. Header columns: ID, Title, Status, Priority. Uses the same split-arrow icon design as the Task List.

3. **Board page (`/board`)** — Expanded the column-actions menu to 6 local sort actions (ID ↑/↓, Title ↑/↓, Priority ↑/↓) plus the original "Apply Priority Order" save action at the bottom. Local sorts only affect display order; active sort is highlighted in the menu with an X to clear. Dragging a task or applying priority order clears the local sort and restores ordinal-based ordering. Dropdown width is dynamic to prevent text wrapping.
<!-- SECTION:DESCRIPTION:END -->

## Acceptance Criteria
<!-- AC:BEGIN -->
- [x] #1 Task List sort icons use split-arrow glyphs (`↑` left, `↓` right) inside a fixed-width container
- [x] #2 Task List inactive state shows both arrows dimmed; active state highlights the matching arrow while keeping the opposite arrow dimmed
- [x] #3 Task List sort icon frame size is identical across all three states (asc, desc, inactive) to prevent layout shift
- [x] #4 Milestones page displays sortable column headers (ID / Title / Status / Priority) inside every milestone group
- [x] #5 Each milestone group maintains independent sort state (column + direction) that does not affect other groups
- [x] #6 Milestones sort icons use the same split-arrow design as Task List
- [x] #7 Board column-actions menu provides 6 sort options (ID / Title / Priority × asc / desc)
- [x] #8 Board sort actions use the same split-arrow icon style as TaskList/Milestones
- [x] #9 All changes pass type-checking and linting
<!-- AC:END -->

## Implementation Plan

<!-- SECTION:PLAN:BEGIN -->
1. **Task List sort icons**
- Replace `renderSortIcon` in `TaskList.tsx` with split-arrow glyphs (`↑` left / `↓` right) inside a fixed `w-4` frame.
- Inactive: both dimmed. Active: matching arrow lit, opposite dimmed.

2. **Milestones sortable headers**
- Add `bucketSorts` state (`Record<string, BucketSortConfig>`) keyed by `bucket.key`.
- `__unassigned` gets its own entry for independent sort.
- Extract `renderBucketTableHeader(bucketKey)` reused by milestone cards and unassigned section.
- Replace `getSortedTasks()` with version accepting `(tasks, bucketKey)` supporting `id | title | status | priority`.
- Align `MilestoneTaskRow` grid columns with header grid.

3. **Board column-actions menu**
- Add 6 local sort options (ID / Title / Priority × asc / desc) to `TaskColumn.tsx`.
- Local sorts only affect display order via `columnSort` state; cleared on drag or when "Apply Priority Order" is invoked.
- Active sort highlighted in menu with an X button to clear.
- Rename original menu item to "Apply Priority Order" / "按优先级重排(保存)".
- Make dropdown width dynamic (`min-w-[12rem] w-max`) with `whitespace-nowrap`.
<!-- SECTION:PLAN:END -->

## Implementation Notes

<!-- SECTION:NOTES:BEGIN -->
- **Task List** (`src/web/components/TaskList.tsx`): `renderSortIcon` now renders a fixed `w-4` container with two arrows (`↑` `↓`). Inactive = both dimmed; asc = left lit; desc = right lit. Prevents layout shift when switching directions.

- **Milestones** (`src/web/components/MilestonesPage.tsx`):
- `bucketSorts` state (`Record<string, BucketSortConfig>`) stores per-bucket sort config keyed by `bucket.key`; `__unassigned` uses `"__unassigned"`.
- `renderBucketTableHeader(bucketKey)` renders a clickable header row (ID / Title / Status / Priority) with the same split-arrow icon style.
- `getSortedTasks(tasks, bucketKey)` applies column+direction sort; falls back to `compareTaskIds` for tie-breaking.
- Previous "done-to-bottom" behavior removed — user-selected sort has full control.

- **MilestoneTaskRow** (`src/web/components/MilestoneTaskRow.tsx`): Grid updated to `grid-cols-[1.5rem_6rem_1fr_6rem_5rem]` matching the header layout.

- **Board** (`src/web/components/TaskColumn.tsx`):
- 6 local sort options (ID / Title / Priority × asc / desc) stored in `columnSort` state; only affects display order.
- Active option highlighted in dropdown menu; X button on the active row clears `columnSort`.
- Dragging a task or clicking "Apply Priority Order" clears `columnSort` and restores ordinal-based display.
- "Apply Priority Order" renamed from "Sort by Priority" to disambiguate from view-only sorting; uses `SortAscendingIcon` (list + up arrow) to indicate persistence.
- Dropdown changed from fixed `w-48` to `min-w-[12rem] w-max` with `whitespace-nowrap`.

- All changes pass `bunx tsc --noEmit` and `bun run check .`.
<!-- SECTION:NOTES:END -->

## Definition of Done
<!-- DOD:BEGIN -->
- [x] #1 bunx tsc --noEmit passes when TypeScript touched
- [x] #2 bun run check . passes when formatting/linting touched
- [x] #3 bun test (or scoped test) passes
<!-- DOD:END -->
Loading
Loading