191 lines
10 KiB
Markdown
191 lines
10 KiB
Markdown
# xboct-flow — VS Code Extension
|
||
|
||
## Хранение
|
||
|
||
Формат: Markdown + YAML frontmatter (один `.task.md` файл на задачу)
|
||
|
||
```markdown
|
||
---
|
||
id: "..."
|
||
title: "Пофиксить баг логина"
|
||
status: todo # todo | in-progress | done | cancelled
|
||
priority: high # low | medium | high | critical
|
||
tags: [bug, frontend]
|
||
assignee: ""
|
||
created: 2026-07-12T10:00:00Z
|
||
updated: 2026-07-12T10:00:00Z
|
||
---
|
||
|
||
Описание задачи в markdown...
|
||
```
|
||
|
||
**Структура папок:**
|
||
|
||
- `.vscode/tasks/` — проектные задачи
|
||
- глобальная папка — настраивается (`xboctFlow.globalPath`)
|
||
|
||
Оба пути настраиваются через VS Code settings.
|
||
|
||
## UI в основном окне (Split-редактор)
|
||
|
||
Через **Custom Editor** + **WebView**:
|
||
|
||
1. **Activity Bar** — вкладка XBOCT flow
|
||
2. **Task Dashboard** — WebView в editor area со split layout:
|
||
- Левая панель — список задач с фильтрацией/группировкой
|
||
- Правая панель — содержимое выбранной задачи (редактируемый markdown)
|
||
3. **TreeView** — быстрый просмотр по статусу
|
||
|
||
## Архитектура кода
|
||
|
||
Актуальная раскладка (после рефакторинга портов/адаптеров):
|
||
|
||
```txt
|
||
src/
|
||
├── extension.ts # composition root (DI, register)
|
||
├── ports.ts # IFileSystem, ITaskRepository, IConfigProvider
|
||
├── error.ts # AppErrorVariant + AppError { variant, message }
|
||
├── model/task.ts
|
||
├── storage/ # infrastructure adapters
|
||
├── commands/ # use-cases (no vscode)
|
||
├── dashboard/ # pure view-model helpers
|
||
├── ui/ # thin vscode adapters (input/quickpick/errors)
|
||
│ ├── presentError.ts
|
||
│ ├── resolveTaskId.ts
|
||
│ ├── taskLabels.ts
|
||
│ └── taskCommands.ts # register + run* handlers
|
||
├── views/
|
||
│ ├── taskTreeItem.ts
|
||
│ ├── taskTreeProvider.ts
|
||
│ ├── taskDashboardPanel.ts # WebviewPanel host
|
||
│ └── taskDashboardHtml.ts # split layout document
|
||
└── utils/
|
||
|
||
tests/helpers/InMemoryFileSystem.ts
|
||
```
|
||
|
||
Слои: use-cases + ports — без vscode; `ui/*` — только ввод/вывод host API;
|
||
`extension.ts` — сборка зависимостей и регистрация.
|
||
|
||
## План реализации
|
||
|
||
| Фаза | Что делаем |
|
||
| ------------------------ | ---------------------------------------------------------------------- |
|
||
| **1. Scaffold** | Extension, TypeScript, сборка, package.json |
|
||
| **2. Storage** | CRUD `.task.md`, парсинг frontmatter, unit-тесты storage |
|
||
| **3. TreeView** | sidebar по статусу, file watcher |
|
||
| **4. Команды** | createTask, deleteTask, changeStatus, openTask (use-cases → wiring UI) |
|
||
| **5. Task Dashboard** | WebView split layout |
|
||
| **6. Глобальные задачи** | использование globalPath |
|
||
| **7. Kanban** | drag-n-drop |
|
||
| **8. Публикация** | vsce publish |
|
||
|
||
### Фаза 4 — команды (use-cases + UI wiring)
|
||
|
||
| Команда | Use-case | UI adapter |
|
||
| -------------- | -------------------------------------------------------------- | ------------------------------------------- |
|
||
| `createTask` | title + folder → defaults todo/medium; empty title / no folder | InputBox → use-case → tree.refresh |
|
||
| `deleteTask` | folder + id → delete; not found | confirm + TreeItem/QuickPick → use-case |
|
||
| `changeStatus` | folder + id + status → update; not found | status QuickPick → use-case |
|
||
| `openTask` | folder + id → path + task | TreeItem click / command → showTextDocument |
|
||
|
||
Ошибки use-case → `presentError` (`AppError.message`). Cancel UI ≠ `AppError`.
|
||
|
||
### Фаза 5 — Task Dashboard
|
||
|
||
Split layout: список (filter/group) + detail (editable markdown body).
|
||
|
||
| Модуль | Поведение |
|
||
| ------------------------------ | -------------------------------------------------------------------------------- |
|
||
| `filterTasks` | statuses / priorities / query (title+body) / tags (AND); пустой = all |
|
||
| `groupTasks` | `none` \| `status` \| `priority`; пустые группы не отдаём; фиксированный порядок |
|
||
| `buildDashboardView` | filter → group; `selected` по id из полного списка (даже если отфильтрован) |
|
||
| `parseDashboardInboundMessage` | ready / selectTask / setFilter / setGroupBy / saveDescription / refresh |
|
||
| `loadDashboard` | folder → `{ tasks }`; `AppError.NO_FOLDER` |
|
||
| `saveTaskDescription` | folder + id + description → update body; `NOT_FOUND` / validation |
|
||
|
||
**Host (сделано, unit-тестами не покрываем):**
|
||
|
||
- `TaskDashboardPanel` — create/reveal/dispose, state, inbound/outbound
|
||
- HTML split: filter/group/list | detail + save description
|
||
- `openDashboard` + status bar; watcher / command mutations → `refreshIfOpen`
|
||
|
||
**По-прежнему только smoke руками:** визуал, CSP, postMessage round-trip
|
||
|
||
### Фаза 6 — globalPath
|
||
|
||
| Модуль | Поведение |
|
||
| ------------------------------ | ---------------------------------------------------------- |
|
||
| `TaskLocation` / `LocatedTask` | runtime scope + folderPath (не в frontmatter) |
|
||
| `resolveTaskLocations` | project и/или global из config |
|
||
| `listLocatedTasks` | list всех locations → `LocatedTask[]`; empty → `NO_FOLDER` |
|
||
| Tree | Project / Global → status → task (location на item) |
|
||
| Create | QuickPick scope, если оба path заданы |
|
||
| Dashboard | multi-folder list; save по location из host cache |
|
||
| Watchers | на каждый active folder |
|
||
|
||
### Фаза 7 — Kanban
|
||
|
||
| Модуль | Поведение |
|
||
| --------------------------- | ------------------------------------------------------------------- |
|
||
| `buildKanbanBoard` | 4 колонки status (пустые остаются); tasks by status; `updated` desc |
|
||
| `parseKanbanInboundMessage` | ready / refresh / moveTask |
|
||
| Host | `TaskKanbanPanel`; move → `changeStatus` + location cache |
|
||
| UI | HTML5 DnD; command `openKanban` |
|
||
|
||
## Зависимости
|
||
|
||
- `front-matter` — разбор YAML frontmatter (body + attributes)
|
||
- `js-yaml` — сериализация frontmatter обратно в YAML (`dump`)
|
||
- `uuid` — генерация ID
|
||
- `neverthrow` — `Result` / `ResultAsync` для use-cases
|
||
- `vitest` / `eslint` — dev
|
||
|
||
## История изменений (решения, не чеклист)
|
||
|
||
Краткая летопись смен относительно **изначального** наброска плана. Детали
|
||
рефакторинга storage/тестов — в архиве `docs/REFACTOR-AND-TEST.md` (документ
|
||
закрыт, не ведём).
|
||
|
||
### Изначальный набросок (v0)
|
||
|
||
- Парсинг: **`gray-matter`**
|
||
- Storage: `taskStore.ts` (CRUD), `configStore.ts` (settings)
|
||
- Сборка/скелет: `yo code`-ориентированный scaffold
|
||
- Дерево модулей: storage + views (tree/list/dashboard) + commands + utils
|
||
|
||
### Рефакторинг storage / чистая архитектура
|
||
|
||
- `taskStore` / `configStore` → **порты** (`IFileSystem`, `ITaskRepository`,
|
||
`IConfigProvider`) и адаптеры:
|
||
- `FsTaskRepository`, `NodeFileSystem`, `VscodeConfigProvider`
|
||
- `extension.ts` — composition root (DI), tree зависит от портов
|
||
- Unit-тесты: Vitest, colocation, `InMemoryFileSystem` в `tests/helpers/`
|
||
|
||
### `gray-matter` → `front-matter` + `js-yaml`
|
||
|
||
- **Было в плане:** один пакет `gray-matter` на parse (+ stringify при
|
||
необходимости).
|
||
- **Стало:** `front-matter` для чтения attributes/body; `js-yaml` (`dump`) для
|
||
записи frontmatter в `serializeTask`.
|
||
- **Зачем:** `gray-matter` даёт лишние возможности парсинга (eval для JS и
|
||
прочая «магия»), которые для задач не нужны; Vite на eval ругается, с точки
|
||
зрения безопасности это нежелательно. Поэтому parse через `front-matter`,
|
||
serialize через `js-yaml` (`dump`). Формат файла тот же: Markdown + YAML
|
||
frontmatter.
|
||
|
||
### neverthrow + `AppError`
|
||
|
||
- Use-cases возвращают `ResultAsync<T, AppError>`.
|
||
- `AppErrorVariant` — const-object кодов (`NOT_FOUND: "not-found"`).
|
||
- `AppError` — `{ variant, message }`; `message` из шаблона с `{placeholders}`
|
||
через `appError(variant, params?)`.
|
||
|
||
### Прочее относительно v0
|
||
|
||
- Сборка: Vite (extension bundle), не классический `tsc`-only из yo code
|
||
- В модели/файле задачи зафиксирован **`id`** в frontmatter (нужен для
|
||
list/get/update/delete)
|
||
- Фаза 4: use-cases в `src/commands/*` тестируются без vscode; UI — отдельные
|
||
адаптеры
|