beads-kanban-ui 0.1.0 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +16 -222
- package/package.json +18 -55
- package/.designs/beads-kanban-ui-bj0.md +0 -73
- package/.designs/beads-kanban-ui-qxq.md +0 -144
- package/.designs/epic-support.md +0 -282
- package/.env.local.example +0 -2
- package/.eslintrc.json +0 -3
- package/.gitattributes +0 -3
- package/.github/workflows/release.yml +0 -123
- package/.history/README_20260121193710.md +0 -227
- package/.history/README_20260121193918.md +0 -227
- package/.history/README_20260121193921.md +0 -227
- package/.history/README_20260121193933.md +0 -227
- package/.history/README_20260121193934.md +0 -227
- package/.history/README_20260121193944.md +0 -227
- package/.history/README_20260121193953.md +0 -227
- package/.history/src/app/page_20260121133429.tsx +0 -134
- package/.history/src/app/page_20260121133928.tsx +0 -134
- package/.history/src/app/page_20260121144850.tsx +0 -138
- package/.history/src/app/page_20260121144854.tsx +0 -138
- package/.history/src/app/page_20260121144858.tsx +0 -138
- package/.history/src/app/page_20260121144902.tsx +0 -138
- package/.history/src/app/page_20260121144906.tsx +0 -138
- package/.history/src/app/page_20260121144911.tsx +0 -138
- package/.history/src/app/page_20260121144928.tsx +0 -138
- package/.playwright-mcp/.playwright-mcp/morphing-dialog-wheel-scroll-fix.png +0 -0
- package/.playwright-mcp/beams-test.png +0 -0
- package/.playwright-mcp/card-verification.png +0 -0
- package/.playwright-mcp/design-doc-dialog-fix-verification.png +0 -0
- package/.playwright-mcp/dialog-width-test.png +0 -0
- package/.playwright-mcp/homepage.png +0 -0
- package/.playwright-mcp/morphing-dialog-expanded.png +0 -0
- package/.playwright-mcp/morphing-dialog-fixes-final.png +0 -0
- package/.playwright-mcp/morphing-dialog-open.png +0 -0
- package/.playwright-mcp/page-2026-01-21T14-08-31-529Z.png +0 -0
- package/.playwright-mcp/page-2026-01-21T14-09-23-431Z.png +0 -0
- package/.playwright-mcp/page-2026-01-21T14-10-28-773Z.png +0 -0
- package/.playwright-mcp/page-2026-01-21T14-10-47-432Z.png +0 -0
- package/.playwright-mcp/page-2026-01-21T14-11-12-350Z.png +0 -0
- package/.playwright-mcp/screenshot-after-click.png +0 -0
- package/.playwright-mcp/screenshot-after-dialog-click.png +0 -0
- package/.playwright-mcp/sheet-restored-after-dialog-close.png +0 -0
- package/.playwright-mcp/test-1-sheet-open-with-overlay.png +0 -0
- package/.playwright-mcp/test-2-morphing-dialog-with-overlay.png +0 -0
- package/.playwright-mcp/test-3-sheet-open-dark-overlay.png +0 -0
- package/.playwright-mcp/test-4-morphing-dialog-with-dark-overlay.png +0 -0
- package/.playwright-mcp/test-5-morphing-dialog-scrolled.png +0 -0
- package/.playwright-mcp/test-6-sheet-restored-after-dialog-close.png +0 -0
- package/.playwright-mcp/wheel-scroll-fixed.png +0 -0
- package/Screenshots/bead-detail.png +0 -0
- package/Screenshots/dashboard.png +0 -0
- package/Screenshots/kanban-board.png +0 -0
- package/components.json +0 -27
- package/logo/logo.svg +0 -1
- package/next.config.js +0 -9
- package/npm/README.md +0 -37
- package/npm/package.json +0 -20
- package/postcss.config.js +0 -6
- package/public/logo.svg +0 -1
- package/restart.sh +0 -5
- package/server/Cargo.lock +0 -1685
- package/server/Cargo.toml +0 -24
- package/server/src/db.rs +0 -570
- package/server/src/main.rs +0 -141
- package/server/src/routes/beads.rs +0 -413
- package/server/src/routes/cli.rs +0 -150
- package/server/src/routes/fs.rs +0 -360
- package/server/src/routes/git.rs +0 -169
- package/server/src/routes/mod.rs +0 -107
- package/server/src/routes/projects.rs +0 -177
- package/server/src/routes/watch.rs +0 -211
- package/src/app/globals.css +0 -101
- package/src/app/layout.tsx +0 -36
- package/src/app/page.tsx +0 -348
- package/src/app/project/kanban-board.tsx +0 -356
- package/src/app/project/page.tsx +0 -18
- package/src/app/settings/page.tsx +0 -224
- package/src/components/Beams.css +0 -5
- package/src/components/Beams.jsx +0 -307
- package/src/components/Galaxy.css +0 -5
- package/src/components/Galaxy.jsx +0 -333
- package/src/components/activity-timeline.tsx +0 -172
- package/src/components/add-project-dialog.tsx +0 -219
- package/src/components/bead-card.tsx +0 -196
- package/src/components/bead-detail.tsx +0 -306
- package/src/components/color-picker.tsx +0 -101
- package/src/components/comment-input.tsx +0 -155
- package/src/components/comment-list.tsx +0 -147
- package/src/components/dependency-badge.tsx +0 -106
- package/src/components/design-doc-dialog.tsx +0 -58
- package/src/components/design-doc-preview.tsx +0 -97
- package/src/components/design-doc-viewer.tsx +0 -199
- package/src/components/editable-project-name.tsx +0 -178
- package/src/components/epic-card.tsx +0 -263
- package/src/components/folder-browser.tsx +0 -273
- package/src/components/footer.tsx +0 -27
- package/src/components/kanban/default.tsx +0 -184
- package/src/components/kanban-column.tsx +0 -167
- package/src/components/project-card.tsx +0 -191
- package/src/components/quick-filter-bar.tsx +0 -279
- package/src/components/scan-directory-dialog.tsx +0 -368
- package/src/components/status-donut.tsx +0 -197
- package/src/components/subtask-list.tsx +0 -128
- package/src/components/tag-picker.tsx +0 -252
- package/src/components/ui/.gitkeep +0 -0
- package/src/components/ui/alert-dialog.tsx +0 -141
- package/src/components/ui/avatar.tsx +0 -67
- package/src/components/ui/badge.tsx +0 -230
- package/src/components/ui/button.tsx +0 -433
- package/src/components/ui/card/index.tsx +0 -24
- package/src/components/ui/card/roiui-card.module.css +0 -197
- package/src/components/ui/card/roiui-card.tsx +0 -154
- package/src/components/ui/card/shadcn-card.tsx +0 -76
- package/src/components/ui/chart.tsx +0 -369
- package/src/components/ui/dialog.tsx +0 -122
- package/src/components/ui/dropdown-menu.tsx +0 -201
- package/src/components/ui/input.tsx +0 -22
- package/src/components/ui/kanban.tsx +0 -522
- package/src/components/ui/morphing-dialog.tsx +0 -457
- package/src/components/ui/popover.tsx +0 -33
- package/src/components/ui/progress.tsx +0 -28
- package/src/components/ui/scroll-area.tsx +0 -48
- package/src/components/ui/select.tsx +0 -159
- package/src/components/ui/separator.tsx +0 -31
- package/src/components/ui/sheet.tsx +0 -142
- package/src/components/ui/skeleton.tsx +0 -15
- package/src/components/ui/toast.tsx +0 -129
- package/src/components/ui/toaster.tsx +0 -35
- package/src/components/ui/tooltip.tsx +0 -30
- package/src/hooks/.gitkeep +0 -0
- package/src/hooks/use-bead-filters.ts +0 -261
- package/src/hooks/use-beads.ts +0 -162
- package/src/hooks/use-branch-statuses.ts +0 -161
- package/src/hooks/use-epics.ts +0 -173
- package/src/hooks/use-file-watcher.ts +0 -111
- package/src/hooks/use-keyboard-navigation.ts +0 -282
- package/src/hooks/use-project.ts +0 -61
- package/src/hooks/use-projects.ts +0 -93
- package/src/hooks/use-toast.ts +0 -194
- package/src/hooks/useClickOutside.tsx +0 -26
- package/src/lib/.gitkeep +0 -0
- package/src/lib/api.ts +0 -186
- package/src/lib/beads-parser.ts +0 -252
- package/src/lib/cli.ts +0 -193
- package/src/lib/db.ts +0 -145
- package/src/lib/design-doc.ts +0 -74
- package/src/lib/epic-parser.ts +0 -242
- package/src/lib/git.ts +0 -102
- package/src/lib/utils.ts +0 -12
- package/src/types/index.ts +0 -107
- package/tailwind.config.ts +0 -85
- package/tsconfig.json +0 -26
- /package/{npm/bin → bin}/cli.js +0 -0
- /package/{npm/scripts → scripts}/postinstall.js +0 -0
|
@@ -1,177 +0,0 @@
|
|
|
1
|
-
//! Project and Tag REST API routes
|
|
2
|
-
//!
|
|
3
|
-
//! Provides CRUD endpoints for projects, tags, and project-tag relationships.
|
|
4
|
-
|
|
5
|
-
use axum::{
|
|
6
|
-
extract::{Path, State},
|
|
7
|
-
http::StatusCode,
|
|
8
|
-
Json,
|
|
9
|
-
};
|
|
10
|
-
use serde::Serialize;
|
|
11
|
-
use std::sync::Arc;
|
|
12
|
-
|
|
13
|
-
use crate::db::{
|
|
14
|
-
CreateProjectInput, CreateTagInput, Database, DbError, ProjectTagInput, ProjectWithTags, Tag,
|
|
15
|
-
UpdateProjectInput,
|
|
16
|
-
};
|
|
17
|
-
|
|
18
|
-
/// Application state containing the database
|
|
19
|
-
pub type AppState = Arc<Database>;
|
|
20
|
-
|
|
21
|
-
/// Error response structure
|
|
22
|
-
#[derive(Serialize)]
|
|
23
|
-
pub struct ErrorResponse {
|
|
24
|
-
pub error: String,
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
/// Success response structure for operations that don't return data
|
|
28
|
-
#[derive(Serialize)]
|
|
29
|
-
pub struct SuccessResponse {
|
|
30
|
-
pub success: bool,
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
impl DbError {
|
|
34
|
-
fn status_code(&self) -> StatusCode {
|
|
35
|
-
match self {
|
|
36
|
-
DbError::ProjectNotFound(_) | DbError::TagNotFound(_) => StatusCode::NOT_FOUND,
|
|
37
|
-
DbError::Sqlite(_) | DbError::PathError => StatusCode::INTERNAL_SERVER_ERROR,
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
fn db_error_response(err: DbError) -> (StatusCode, Json<ErrorResponse>) {
|
|
43
|
-
let status = err.status_code();
|
|
44
|
-
(
|
|
45
|
-
status,
|
|
46
|
-
Json(ErrorResponse {
|
|
47
|
-
error: err.to_string(),
|
|
48
|
-
}),
|
|
49
|
-
)
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
// ===== Project Routes =====
|
|
53
|
-
|
|
54
|
-
/// GET /api/projects - List all projects with their tags
|
|
55
|
-
pub async fn list_projects(
|
|
56
|
-
State(db): State<AppState>,
|
|
57
|
-
) -> Result<Json<Vec<ProjectWithTags>>, (StatusCode, Json<ErrorResponse>)> {
|
|
58
|
-
db.get_projects_with_tags()
|
|
59
|
-
.map(Json)
|
|
60
|
-
.map_err(db_error_response)
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
/// POST /api/projects - Create a new project
|
|
64
|
-
pub async fn create_project(
|
|
65
|
-
State(db): State<AppState>,
|
|
66
|
-
Json(input): Json<CreateProjectInput>,
|
|
67
|
-
) -> Result<(StatusCode, Json<ProjectWithTags>), (StatusCode, Json<ErrorResponse>)> {
|
|
68
|
-
let project = db.create_project(input).map_err(db_error_response)?;
|
|
69
|
-
|
|
70
|
-
// Return project with empty tags array
|
|
71
|
-
let project_with_tags = ProjectWithTags {
|
|
72
|
-
id: project.id,
|
|
73
|
-
name: project.name,
|
|
74
|
-
path: project.path,
|
|
75
|
-
tags: vec![],
|
|
76
|
-
last_opened: project.last_opened,
|
|
77
|
-
created_at: project.created_at,
|
|
78
|
-
};
|
|
79
|
-
|
|
80
|
-
Ok((StatusCode::CREATED, Json(project_with_tags)))
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
/// PATCH /api/projects/:id - Update a project
|
|
84
|
-
pub async fn update_project(
|
|
85
|
-
State(db): State<AppState>,
|
|
86
|
-
Path(id): Path<String>,
|
|
87
|
-
Json(input): Json<UpdateProjectInput>,
|
|
88
|
-
) -> Result<Json<ProjectWithTags>, (StatusCode, Json<ErrorResponse>)> {
|
|
89
|
-
let project = db.update_project(&id, input).map_err(db_error_response)?;
|
|
90
|
-
let tags = db.get_project_tags(&id).map_err(db_error_response)?;
|
|
91
|
-
|
|
92
|
-
Ok(Json(ProjectWithTags {
|
|
93
|
-
id: project.id,
|
|
94
|
-
name: project.name,
|
|
95
|
-
path: project.path,
|
|
96
|
-
tags,
|
|
97
|
-
last_opened: project.last_opened,
|
|
98
|
-
created_at: project.created_at,
|
|
99
|
-
}))
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
/// DELETE /api/projects/:id - Delete a project
|
|
103
|
-
pub async fn delete_project(
|
|
104
|
-
State(db): State<AppState>,
|
|
105
|
-
Path(id): Path<String>,
|
|
106
|
-
) -> Result<StatusCode, (StatusCode, Json<ErrorResponse>)> {
|
|
107
|
-
db.delete_project(&id).map_err(db_error_response)?;
|
|
108
|
-
Ok(StatusCode::NO_CONTENT)
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
// ===== Tag Routes =====
|
|
112
|
-
|
|
113
|
-
/// GET /api/tags - List all tags
|
|
114
|
-
pub async fn list_tags(
|
|
115
|
-
State(db): State<AppState>,
|
|
116
|
-
) -> Result<Json<Vec<Tag>>, (StatusCode, Json<ErrorResponse>)> {
|
|
117
|
-
db.get_tags().map(Json).map_err(db_error_response)
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
/// POST /api/tags - Create a new tag
|
|
121
|
-
pub async fn create_tag(
|
|
122
|
-
State(db): State<AppState>,
|
|
123
|
-
Json(input): Json<CreateTagInput>,
|
|
124
|
-
) -> Result<(StatusCode, Json<Tag>), (StatusCode, Json<ErrorResponse>)> {
|
|
125
|
-
let tag = db.create_tag(input).map_err(db_error_response)?;
|
|
126
|
-
Ok((StatusCode::CREATED, Json(tag)))
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
/// DELETE /api/tags/:id - Delete a tag
|
|
130
|
-
pub async fn delete_tag(
|
|
131
|
-
State(db): State<AppState>,
|
|
132
|
-
Path(id): Path<String>,
|
|
133
|
-
) -> Result<StatusCode, (StatusCode, Json<ErrorResponse>)> {
|
|
134
|
-
db.delete_tag(&id).map_err(db_error_response)?;
|
|
135
|
-
Ok(StatusCode::NO_CONTENT)
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
// ===== Project-Tag Relationship Routes =====
|
|
139
|
-
|
|
140
|
-
/// POST /api/project-tags - Add a tag to a project
|
|
141
|
-
pub async fn add_project_tag(
|
|
142
|
-
State(db): State<AppState>,
|
|
143
|
-
Json(input): Json<ProjectTagInput>,
|
|
144
|
-
) -> Result<(StatusCode, Json<SuccessResponse>), (StatusCode, Json<ErrorResponse>)> {
|
|
145
|
-
db.add_tag_to_project(&input.project_id, &input.tag_id)
|
|
146
|
-
.map_err(db_error_response)?;
|
|
147
|
-
Ok((StatusCode::CREATED, Json(SuccessResponse { success: true })))
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
/// DELETE /api/project-tags/:project_id/:tag_id - Remove a tag from a project
|
|
151
|
-
pub async fn remove_project_tag(
|
|
152
|
-
State(db): State<AppState>,
|
|
153
|
-
Path((project_id, tag_id)): Path<(String, String)>,
|
|
154
|
-
) -> Result<Json<SuccessResponse>, (StatusCode, Json<ErrorResponse>)> {
|
|
155
|
-
db.remove_tag_from_project(&project_id, &tag_id)
|
|
156
|
-
.map_err(db_error_response)?;
|
|
157
|
-
Ok(Json(SuccessResponse { success: true }))
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
/// Creates the project/tag router with all routes
|
|
161
|
-
pub fn project_routes() -> axum::Router<AppState> {
|
|
162
|
-
use axum::routing::{delete, get, patch, post};
|
|
163
|
-
|
|
164
|
-
axum::Router::new()
|
|
165
|
-
// Project routes
|
|
166
|
-
.route("/projects", get(list_projects).post(create_project))
|
|
167
|
-
.route(
|
|
168
|
-
"/projects/:id",
|
|
169
|
-
patch(update_project).delete(delete_project),
|
|
170
|
-
)
|
|
171
|
-
// Tag routes
|
|
172
|
-
.route("/tags", get(list_tags).post(create_tag))
|
|
173
|
-
.route("/tags/:id", delete(delete_tag))
|
|
174
|
-
// Project-tag relationship routes
|
|
175
|
-
.route("/project-tags", post(add_project_tag))
|
|
176
|
-
.route("/project-tags/:project_id/:tag_id", delete(remove_project_tag))
|
|
177
|
-
}
|
|
@@ -1,211 +0,0 @@
|
|
|
1
|
-
//! File watcher SSE endpoint for real-time file change notifications.
|
|
2
|
-
//!
|
|
3
|
-
//! Provides Server-Sent Events for monitoring changes to beads issue files.
|
|
4
|
-
|
|
5
|
-
use axum::{
|
|
6
|
-
extract::Query,
|
|
7
|
-
response::sse::{Event, Sse},
|
|
8
|
-
};
|
|
9
|
-
use futures::stream::Stream;
|
|
10
|
-
use notify::{
|
|
11
|
-
event::ModifyKind, Config, EventKind, RecommendedWatcher, RecursiveMode, Watcher,
|
|
12
|
-
};
|
|
13
|
-
use serde::{Deserialize, Serialize};
|
|
14
|
-
use std::{convert::Infallible, path::PathBuf, time::Duration};
|
|
15
|
-
use tokio::sync::mpsc;
|
|
16
|
-
use tokio_stream::wrappers::ReceiverStream;
|
|
17
|
-
use tracing::{error, info, warn};
|
|
18
|
-
|
|
19
|
-
/// Query parameters for the watch endpoint.
|
|
20
|
-
#[derive(Debug, Deserialize)]
|
|
21
|
-
pub struct WatchParams {
|
|
22
|
-
/// The project path to watch for changes.
|
|
23
|
-
pub path: String,
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
/// File change event sent to clients.
|
|
27
|
-
#[derive(Debug, Serialize)]
|
|
28
|
-
pub struct FileChangeEvent {
|
|
29
|
-
/// The path of the changed file.
|
|
30
|
-
pub path: String,
|
|
31
|
-
/// The type of change (modified, created, removed).
|
|
32
|
-
#[serde(rename = "type")]
|
|
33
|
-
pub change_type: String,
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
/// SSE endpoint for watching beads file changes.
|
|
37
|
-
///
|
|
38
|
-
/// Monitors the `.beads/issues.jsonl` file in the specified project path
|
|
39
|
-
/// and sends SSE events when changes are detected.
|
|
40
|
-
///
|
|
41
|
-
/// # Query Parameters
|
|
42
|
-
///
|
|
43
|
-
/// - `path`: The project directory path to monitor
|
|
44
|
-
///
|
|
45
|
-
/// # Returns
|
|
46
|
-
///
|
|
47
|
-
/// A Server-Sent Events stream of file change notifications.
|
|
48
|
-
pub async fn watch_beads(
|
|
49
|
-
Query(params): Query<WatchParams>,
|
|
50
|
-
) -> Sse<impl Stream<Item = Result<Event, Infallible>>> {
|
|
51
|
-
let project_path = PathBuf::from(¶ms.path);
|
|
52
|
-
let beads_file = project_path.join(".beads").join("issues.jsonl");
|
|
53
|
-
|
|
54
|
-
info!("Starting file watcher for: {:?}", beads_file);
|
|
55
|
-
|
|
56
|
-
// Create channel for events with buffer for debouncing
|
|
57
|
-
let (tx, rx) = mpsc::channel::<Result<Event, Infallible>>(100);
|
|
58
|
-
|
|
59
|
-
// Spawn the watcher task
|
|
60
|
-
tokio::spawn(async move {
|
|
61
|
-
if let Err(e) = run_watcher(beads_file, tx).await {
|
|
62
|
-
error!("File watcher error: {}", e);
|
|
63
|
-
}
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
let stream = ReceiverStream::new(rx);
|
|
67
|
-
Sse::new(stream).keep_alive(
|
|
68
|
-
axum::response::sse::KeepAlive::new()
|
|
69
|
-
.interval(Duration::from_secs(30))
|
|
70
|
-
.text("ping"),
|
|
71
|
-
)
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
/// Runs the file watcher and sends events through the channel.
|
|
75
|
-
async fn run_watcher(
|
|
76
|
-
beads_file: PathBuf,
|
|
77
|
-
tx: mpsc::Sender<Result<Event, Infallible>>,
|
|
78
|
-
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
|
79
|
-
// Create a channel for notify events
|
|
80
|
-
let (notify_tx, mut notify_rx) = mpsc::channel(100);
|
|
81
|
-
|
|
82
|
-
// Create the watcher
|
|
83
|
-
let mut watcher = RecommendedWatcher::new(
|
|
84
|
-
move |res: notify::Result<notify::Event>| {
|
|
85
|
-
if let Ok(event) = res {
|
|
86
|
-
// Only forward relevant events
|
|
87
|
-
let _ = notify_tx.blocking_send(event);
|
|
88
|
-
}
|
|
89
|
-
},
|
|
90
|
-
Config::default().with_poll_interval(Duration::from_millis(100)),
|
|
91
|
-
)?;
|
|
92
|
-
|
|
93
|
-
// Watch the parent directory (.beads) since the file might not exist yet
|
|
94
|
-
let watch_path = beads_file
|
|
95
|
-
.parent()
|
|
96
|
-
.map(|p| p.to_path_buf())
|
|
97
|
-
.unwrap_or_else(|| beads_file.clone());
|
|
98
|
-
|
|
99
|
-
// Create the .beads directory if it doesn't exist
|
|
100
|
-
if !watch_path.exists() {
|
|
101
|
-
warn!(
|
|
102
|
-
"Watch path does not exist, waiting for creation: {:?}",
|
|
103
|
-
watch_path
|
|
104
|
-
);
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
// Try to watch the path, or watch parent if it doesn't exist
|
|
108
|
-
let actual_watch_path = if watch_path.exists() {
|
|
109
|
-
watch_path.clone()
|
|
110
|
-
} else if let Some(parent) = watch_path.parent() {
|
|
111
|
-
if parent.exists() {
|
|
112
|
-
parent.to_path_buf()
|
|
113
|
-
} else {
|
|
114
|
-
error!("Neither watch path nor parent exists: {:?}", watch_path);
|
|
115
|
-
return Ok(());
|
|
116
|
-
}
|
|
117
|
-
} else {
|
|
118
|
-
error!("No valid path to watch: {:?}", watch_path);
|
|
119
|
-
return Ok(());
|
|
120
|
-
};
|
|
121
|
-
|
|
122
|
-
watcher.watch(&actual_watch_path, RecursiveMode::Recursive)?;
|
|
123
|
-
info!("File watcher active on: {:?}", actual_watch_path);
|
|
124
|
-
|
|
125
|
-
// Send initial connection event
|
|
126
|
-
let connect_event = Event::default().data(
|
|
127
|
-
serde_json::to_string(&FileChangeEvent {
|
|
128
|
-
path: beads_file.to_string_lossy().to_string(),
|
|
129
|
-
change_type: "connected".to_string(),
|
|
130
|
-
})
|
|
131
|
-
.unwrap_or_default(),
|
|
132
|
-
);
|
|
133
|
-
let _ = tx.send(Ok(connect_event)).await;
|
|
134
|
-
|
|
135
|
-
// Debounce state
|
|
136
|
-
let mut last_event_time = std::time::Instant::now();
|
|
137
|
-
let debounce_duration = Duration::from_millis(100);
|
|
138
|
-
|
|
139
|
-
// Process events
|
|
140
|
-
while let Some(event) = notify_rx.recv().await {
|
|
141
|
-
// Check if the event is for our target file
|
|
142
|
-
let is_relevant = event.paths.iter().any(|p| {
|
|
143
|
-
p.ends_with("issues.jsonl")
|
|
144
|
-
|| p.ends_with(".beads")
|
|
145
|
-
|| p == &beads_file
|
|
146
|
-
});
|
|
147
|
-
|
|
148
|
-
if !is_relevant {
|
|
149
|
-
continue;
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
// Debounce rapid changes
|
|
153
|
-
let now = std::time::Instant::now();
|
|
154
|
-
if now.duration_since(last_event_time) < debounce_duration {
|
|
155
|
-
continue;
|
|
156
|
-
}
|
|
157
|
-
last_event_time = now;
|
|
158
|
-
|
|
159
|
-
// Determine event type
|
|
160
|
-
let change_type = match event.kind {
|
|
161
|
-
EventKind::Create(_) => "created",
|
|
162
|
-
EventKind::Modify(ModifyKind::Data(_)) => "modified",
|
|
163
|
-
EventKind::Modify(_) => "modified",
|
|
164
|
-
EventKind::Remove(_) => "removed",
|
|
165
|
-
_ => continue, // Ignore other events
|
|
166
|
-
};
|
|
167
|
-
|
|
168
|
-
let file_event = FileChangeEvent {
|
|
169
|
-
path: beads_file.to_string_lossy().to_string(),
|
|
170
|
-
change_type: change_type.to_string(),
|
|
171
|
-
};
|
|
172
|
-
|
|
173
|
-
info!("File change detected: {:?}", file_event);
|
|
174
|
-
|
|
175
|
-
let sse_event = Event::default()
|
|
176
|
-
.data(serde_json::to_string(&file_event).unwrap_or_default());
|
|
177
|
-
|
|
178
|
-
// If send fails, client disconnected
|
|
179
|
-
if tx.send(Ok(sse_event)).await.is_err() {
|
|
180
|
-
info!("Client disconnected, stopping watcher");
|
|
181
|
-
break;
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
// Watcher is automatically dropped and cleaned up here
|
|
186
|
-
info!("File watcher stopped");
|
|
187
|
-
Ok(())
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
#[cfg(test)]
|
|
191
|
-
mod tests {
|
|
192
|
-
use super::*;
|
|
193
|
-
|
|
194
|
-
#[test]
|
|
195
|
-
fn test_file_change_event_serialization() {
|
|
196
|
-
let event = FileChangeEvent {
|
|
197
|
-
path: "/test/path".to_string(),
|
|
198
|
-
change_type: "modified".to_string(),
|
|
199
|
-
};
|
|
200
|
-
let json = serde_json::to_string(&event).unwrap();
|
|
201
|
-
assert!(json.contains("\"path\":\"/test/path\""));
|
|
202
|
-
assert!(json.contains("\"type\":\"modified\""));
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
#[test]
|
|
206
|
-
fn test_watch_params_deserialization() {
|
|
207
|
-
let params: WatchParams =
|
|
208
|
-
serde_json::from_str(r#"{"path": "/test/project"}"#).unwrap();
|
|
209
|
-
assert_eq!(params.path, "/test/project");
|
|
210
|
-
}
|
|
211
|
-
}
|
package/src/app/globals.css
DELETED
|
@@ -1,101 +0,0 @@
|
|
|
1
|
-
@tailwind base;
|
|
2
|
-
@tailwind components;
|
|
3
|
-
@tailwind utilities;
|
|
4
|
-
|
|
5
|
-
@layer base {
|
|
6
|
-
:root {
|
|
7
|
-
--background: 0 0% 100%;
|
|
8
|
-
--foreground: 240 10% 3.9%;
|
|
9
|
-
--card: 0 0% 100%;
|
|
10
|
-
--card-foreground: 240 10% 3.9%;
|
|
11
|
-
--popover: 0 0% 100%;
|
|
12
|
-
--popover-foreground: 240 10% 3.9%;
|
|
13
|
-
--primary: 240 5.9% 10%;
|
|
14
|
-
--primary-foreground: 0 0% 98%;
|
|
15
|
-
--secondary: 240 4.8% 95.9%;
|
|
16
|
-
--secondary-foreground: 240 5.9% 10%;
|
|
17
|
-
--muted: 240 4.8% 95.9%;
|
|
18
|
-
--muted-foreground: 240 3.8% 46.1%;
|
|
19
|
-
--accent: 240 4.8% 95.9%;
|
|
20
|
-
--accent-foreground: 240 5.9% 10%;
|
|
21
|
-
--destructive: 0 84.2% 60.2%;
|
|
22
|
-
--destructive-foreground: 0 0% 98%;
|
|
23
|
-
--border: 240 5.9% 90%;
|
|
24
|
-
--input: 240 5.9% 90%;
|
|
25
|
-
--ring: 240 5.9% 10%;
|
|
26
|
-
--chart-1: 12 76% 61%;
|
|
27
|
-
--chart-2: 173 58% 39%;
|
|
28
|
-
--chart-3: 197 37% 24%;
|
|
29
|
-
--chart-4: 43 74% 66%;
|
|
30
|
-
--chart-5: 27 87% 67%;
|
|
31
|
-
--radius: 0.5rem;
|
|
32
|
-
|
|
33
|
-
/* Beads Status colors (HSL) */
|
|
34
|
-
--status-open: 217 91% 60%;
|
|
35
|
-
--status-in_progress: 38 92% 50%;
|
|
36
|
-
--status-inreview: 271 81% 56%;
|
|
37
|
-
--status-closed: 142 71% 45%;
|
|
38
|
-
|
|
39
|
-
/* Beads Priority colors (HSL) */
|
|
40
|
-
--priority-p0: 0 84% 60%;
|
|
41
|
-
--priority-p1: 25 95% 53%;
|
|
42
|
-
--priority-p2: 240 5% 65%;
|
|
43
|
-
--priority-p3: 240 5% 84%;
|
|
44
|
-
--priority-p4: 240 5% 84%;
|
|
45
|
-
|
|
46
|
-
/* Beads Accents (HSL) */
|
|
47
|
-
--blocked: 0 84% 60%;
|
|
48
|
-
--branch: 142 76% 36%;
|
|
49
|
-
|
|
50
|
-
/* roiui Card CSS Variables */
|
|
51
|
-
--shadow-border-stack: 0 1px 2px hsl(var(--foreground) / 0.04);
|
|
52
|
-
--mix-card-33-bg: hsl(var(--card) / 0.33);
|
|
53
|
-
--ease-in-out-quad: cubic-bezier(0.455, 0.03, 0.515, 0.955);
|
|
54
|
-
--background-muted: hsl(var(--muted));
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
.dark {
|
|
58
|
-
--background: 240 10% 3.9%;
|
|
59
|
-
--foreground: 0 0% 98%;
|
|
60
|
-
--card: 240 10% 3.9%;
|
|
61
|
-
--card-foreground: 0 0% 98%;
|
|
62
|
-
--popover: 240 10% 3.9%;
|
|
63
|
-
--popover-foreground: 0 0% 98%;
|
|
64
|
-
--primary: 0 0% 98%;
|
|
65
|
-
--primary-foreground: 240 5.9% 10%;
|
|
66
|
-
--secondary: 240 3.7% 15.9%;
|
|
67
|
-
--secondary-foreground: 0 0% 98%;
|
|
68
|
-
--muted: 240 3.7% 15.9%;
|
|
69
|
-
--muted-foreground: 240 5% 64.9%;
|
|
70
|
-
--accent: 240 3.7% 15.9%;
|
|
71
|
-
--accent-foreground: 0 0% 98%;
|
|
72
|
-
--destructive: 0 62.8% 30.6%;
|
|
73
|
-
--destructive-foreground: 0 0% 98%;
|
|
74
|
-
--border: 240 3.7% 15.9%;
|
|
75
|
-
--input: 240 3.7% 15.9%;
|
|
76
|
-
--ring: 240 4.9% 83.9%;
|
|
77
|
-
--chart-1: 220 70% 50%;
|
|
78
|
-
--chart-2: 160 60% 45%;
|
|
79
|
-
--chart-3: 30 80% 55%;
|
|
80
|
-
--chart-4: 280 65% 60%;
|
|
81
|
-
--chart-5: 340 75% 55%;
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
@layer base {
|
|
86
|
-
* {
|
|
87
|
-
@apply border-border;
|
|
88
|
-
}
|
|
89
|
-
body {
|
|
90
|
-
@apply bg-background text-foreground;
|
|
91
|
-
font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
.font-heading {
|
|
96
|
-
font-family: var(--font-space-grotesk), system-ui, sans-serif;
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
.font-project-name {
|
|
100
|
-
font-family: var(--font-plus-jakarta), system-ui, sans-serif;
|
|
101
|
-
}
|
package/src/app/layout.tsx
DELETED
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
import type { Metadata } from 'next';
|
|
2
|
-
import { Space_Grotesk, Plus_Jakarta_Sans } from 'next/font/google';
|
|
3
|
-
import { Toaster } from '@/components/ui/toaster';
|
|
4
|
-
import './globals.css';
|
|
5
|
-
|
|
6
|
-
const spaceGrotesk = Space_Grotesk({
|
|
7
|
-
subsets: ['latin'],
|
|
8
|
-
display: 'swap',
|
|
9
|
-
variable: '--font-space-grotesk',
|
|
10
|
-
});
|
|
11
|
-
|
|
12
|
-
const plusJakartaSans = Plus_Jakarta_Sans({
|
|
13
|
-
subsets: ['latin'],
|
|
14
|
-
display: 'swap',
|
|
15
|
-
variable: '--font-plus-jakarta',
|
|
16
|
-
});
|
|
17
|
-
|
|
18
|
-
export const metadata: Metadata = {
|
|
19
|
-
title: 'Beads',
|
|
20
|
-
description: 'Kanban interface for beads - git-backed distributed issue tracker',
|
|
21
|
-
};
|
|
22
|
-
|
|
23
|
-
export default function RootLayout({
|
|
24
|
-
children,
|
|
25
|
-
}: {
|
|
26
|
-
children: React.ReactNode;
|
|
27
|
-
}) {
|
|
28
|
-
return (
|
|
29
|
-
<html lang="en" className={`dark ${spaceGrotesk.variable} ${plusJakartaSans.variable}`}>
|
|
30
|
-
<body className="flex min-h-screen flex-col bg-background antialiased">
|
|
31
|
-
<div className="flex-1">{children}</div>
|
|
32
|
-
<Toaster />
|
|
33
|
-
</body>
|
|
34
|
-
</html>
|
|
35
|
-
);
|
|
36
|
-
}
|