@luuthuong/devboard 1.0.0 → 1.0.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/dist/assets/CardDetailView-BZE_MfjM.js +24 -0
- package/dist/assets/{DiffPane-bQsGoT7y.js → DiffPane-2WnoRvZ0.js} +2 -2
- package/dist/assets/KnowledgeView-C4WPFAUM.js +3 -0
- package/dist/assets/ListBoardView-DEE_eK4h.js +3 -0
- package/dist/assets/NewTaskDialog-D8cpc7Bc.js +1 -0
- package/dist/assets/OnboardingWizard-9ZkFPGvH.js +1 -0
- package/dist/assets/{ProcessManagerView-BIYjQpoP.js → ProcessManagerView-DJOr44DY.js} +1 -1
- package/dist/assets/ProposalCard-B8jqFtL8.js +1 -0
- package/dist/assets/{SearchOverlay-B4aywpIh.js → SearchOverlay-qGxzmM_v.js} +1 -1
- package/dist/assets/SettingsView-C3GkCbTz.js +1 -0
- package/dist/assets/SetupProjectView-CSx6ax5r.js +1 -0
- package/dist/assets/TaskDescriptionEditor-D0_b81zY.js +58 -0
- package/dist/assets/{Terminal-DJJcg-t6.js → Terminal-yq90NOBX.js} +1 -1
- package/dist/assets/deepEqual-BiJVGDfM.js +1 -0
- package/dist/assets/index-BtXUncAU.css +1 -0
- package/dist/assets/index-CQnL8JJ5.js +18 -0
- package/dist/assets/plus-Cecrm8sZ.js +1 -0
- package/dist/assets/x-B6wVesqk.js +1 -0
- package/dist/index.html +2 -2
- package/package.json +2 -2
- package/server-dist/server/commands.js +5 -39
- package/server-dist/server/fsBrowse.js +143 -0
- package/server-dist/server/transcripts.js +21 -22
- package/dist/assets/CardDetailView-CVWOsYyA.js +0 -24
- package/dist/assets/KnowledgeView-C5KHiBRG.js +0 -3
- package/dist/assets/ListBoardView-CzJcCLXS.js +0 -3
- package/dist/assets/NewTaskDialog-BuxvfgNU.js +0 -1
- package/dist/assets/OnboardingWizard-8WLFjTd8.js +0 -1
- package/dist/assets/ProposalCard-BbPobl1f.js +0 -1
- package/dist/assets/SettingsView-spBB3EwB.js +0 -1
- package/dist/assets/SetupProjectView-BuUDqDYU.js +0 -1
- package/dist/assets/TaskDescriptionEditor-Cv0nlU3w.js +0 -58
- package/dist/assets/chevron-right-BsVThPz4.js +0 -1
- package/dist/assets/index-DNKB6XwG.css +0 -1
- package/dist/assets/index-gWSenOqD.js +0 -18
- package/dist/assets/plus-XSPxArj5.js +0 -1
- package/dist/assets/x-CdEDbLMR.js +0 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{Ct as e}from"./index-CQnL8JJ5.js";var t={name:`plus`,size:24,node:[[`path`,{d:`M5 12h14`,key:`1ays0h`}],[`path`,{d:`M12 5v14`,key:`s699le`}]]};t.node;var n=e(t);export{n as t};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{Ct as e}from"./index-CQnL8JJ5.js";var t={name:`x`,size:24,node:[[`path`,{d:`M18 6 6 18`,key:`1bl5f8`}],[`path`,{d:`m6 6 12 12`,key:`d8bk6v`}]]};t.node;var n=e(t);export{n as t};
|
package/dist/index.html
CHANGED
|
@@ -10,11 +10,11 @@
|
|
|
10
10
|
rel="stylesheet"
|
|
11
11
|
/>
|
|
12
12
|
<title>DevBoard</title>
|
|
13
|
-
<script type="module" crossorigin src="/assets/index-
|
|
13
|
+
<script type="module" crossorigin src="/assets/index-CQnL8JJ5.js"></script>
|
|
14
14
|
<link rel="modulepreload" crossorigin href="/assets/jsx-runtime-QmcfnBgR.js">
|
|
15
15
|
<link rel="modulepreload" crossorigin href="/assets/react-dom-DjtJUrx3.js">
|
|
16
16
|
<link rel="modulepreload" crossorigin href="/assets/Surface-BQKeigoq.js">
|
|
17
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
17
|
+
<link rel="stylesheet" crossorigin href="/assets/index-BtXUncAU.css">
|
|
18
18
|
</head>
|
|
19
19
|
<body>
|
|
20
20
|
<div id="root"></div>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@luuthuong/devboard",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.1",
|
|
4
4
|
"description": "DevBoard — a local devboard board for orchestrating Claude Code sessions",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"agents",
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
"build": "tsc && vite build",
|
|
34
34
|
"start": "tsx server/index.ts",
|
|
35
35
|
"typecheck": "tsc --noEmit && tsc --noEmit -p tsconfig.server.json",
|
|
36
|
-
"test": "tsx --test --test-reporter=spec 'server/**/*.test.ts'",
|
|
36
|
+
"test": "tsx --test --test-reporter=spec 'server/**/*.test.ts' 'src/**/*.test.ts'",
|
|
37
37
|
"preview": "vite preview",
|
|
38
38
|
"prepack": "node scripts/prepack.mjs",
|
|
39
39
|
"build:server": "node scripts/build-server.mjs"
|
|
@@ -1,10 +1,8 @@
|
|
|
1
1
|
// The RPC surface: every command the UI used to `invoke()` through Tauri.
|
|
2
2
|
// Arguments arrive as the same camelCase objects; return values are JSON.
|
|
3
|
-
import fs from "node:fs";
|
|
4
|
-
import os from "node:os";
|
|
5
|
-
import path from "node:path";
|
|
6
3
|
import { notifyUser } from "./context.js";
|
|
7
4
|
import { emit } from "./events.js";
|
|
5
|
+
import { fsList, fsSearch } from "./fsBrowse.js";
|
|
8
6
|
import { openInEditor, openInTerminal } from "./editor.js";
|
|
9
7
|
import { commandExists, createWorktree, getDiff, ghIsAuthed, githubBaseUrl, listWorktrees, removeWorktree, } from "./git.js";
|
|
10
8
|
import { currentContextTokens, installStatusline, mergeBlocked, mergeIntoTarget, readAllContextUsage, readContextUsage, ensureLauncher, saveAttachmentBytes, saveImageBytes, statuslineInstalled, uninstallStatusline, } from "./integrations.js";
|
|
@@ -16,7 +14,7 @@ import { killCardSession } from "./sessionReaper.js";
|
|
|
16
14
|
import * as tmux from "./tmux.js";
|
|
17
15
|
const backendFor = (runtime) => (runtime === "windows" ? "psmux" : "wsl");
|
|
18
16
|
import { forkSession, moveSession, readTranscript, readTranscriptUsage, searchSessions, searchTranscriptTurns, truncateSession, } from "./transcripts.js";
|
|
19
|
-
import { errorMessage, ksuid, log, runShell
|
|
17
|
+
import { errorMessage, ksuid, log, runShell } from "./util.js";
|
|
20
18
|
const nowIso = () => new Date().toISOString();
|
|
21
19
|
/** Resolve a session's transcript path: the card's link first, then discovery. */
|
|
22
20
|
async function sessionPathFor(ctx, sessionId) {
|
|
@@ -29,39 +27,6 @@ async function sessionPathFor(ctx, sessionId) {
|
|
|
29
27
|
throw new Error(`Session ${sessionId} not found`);
|
|
30
28
|
return found;
|
|
31
29
|
}
|
|
32
|
-
/** Directories-only listing for the in-browser folder picker. */
|
|
33
|
-
async function fsList(dir) {
|
|
34
|
-
if (!dir) {
|
|
35
|
-
const roots = IS_WINDOWS
|
|
36
|
-
? "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
|
37
|
-
.split("")
|
|
38
|
-
.map((l) => `${l}:\\`)
|
|
39
|
-
.filter((r) => fs.existsSync(r))
|
|
40
|
-
: ["/"];
|
|
41
|
-
return {
|
|
42
|
-
path: "",
|
|
43
|
-
parent: null,
|
|
44
|
-
home: os.homedir(),
|
|
45
|
-
entries: roots.map((r) => ({ name: r, path: r, isDir: true })),
|
|
46
|
-
};
|
|
47
|
-
}
|
|
48
|
-
const resolved = path.resolve(dir);
|
|
49
|
-
const parent = path.dirname(resolved);
|
|
50
|
-
const dirents = await fs.promises.readdir(resolved, { withFileTypes: true });
|
|
51
|
-
const entries = dirents
|
|
52
|
-
.filter((d) => d.isDirectory() || (d.isSymbolicLink() && safeIsDir(path.join(resolved, d.name))))
|
|
53
|
-
.map((d) => ({ name: d.name, path: path.join(resolved, d.name), isDir: true }))
|
|
54
|
-
.sort((a, b) => a.name.localeCompare(b.name, undefined, { sensitivity: "base" }));
|
|
55
|
-
return { path: resolved, parent: parent === resolved ? "" : parent, home: os.homedir(), entries };
|
|
56
|
-
}
|
|
57
|
-
function safeIsDir(p) {
|
|
58
|
-
try {
|
|
59
|
-
return fs.statSync(p).isDirectory();
|
|
60
|
-
}
|
|
61
|
-
catch {
|
|
62
|
-
return false;
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
30
|
async function configuredProjectPaths(settings) {
|
|
66
31
|
const s = await settings.read().catch(() => null);
|
|
67
32
|
return (s?.projects ?? []).flatMap((p) => [p.path, p.repoRoot].filter((x) => !!x));
|
|
@@ -222,8 +187,8 @@ export function buildCommands(ctx) {
|
|
|
222
187
|
async search_sessions({ query }) {
|
|
223
188
|
return searchSessions(await discovery.discoverSessions(), query);
|
|
224
189
|
},
|
|
225
|
-
async get_transcript({ sessionId, offset }) {
|
|
226
|
-
return readTranscript(await sessionPathFor(ctx, sessionId), offset);
|
|
190
|
+
async get_transcript({ sessionId, offset, limit }) {
|
|
191
|
+
return readTranscript(await sessionPathFor(ctx, sessionId), offset, typeof limit === "number" ? limit : undefined);
|
|
227
192
|
},
|
|
228
193
|
async search_transcript({ sessionId, query }) {
|
|
229
194
|
return searchTranscriptTurns(await sessionPathFor(ctx, sessionId), query);
|
|
@@ -414,6 +379,7 @@ export function buildCommands(ctx) {
|
|
|
414
379
|
},
|
|
415
380
|
// ── Web-only helpers ────────────────────────────────────────────────────
|
|
416
381
|
fs_list: ({ path: dir }) => fsList(dir),
|
|
382
|
+
fs_search: ({ query, root }) => fsSearch(String(query ?? ""), root),
|
|
417
383
|
launcher_info: () => ensureLauncher(),
|
|
418
384
|
};
|
|
419
385
|
}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
// Directory browsing for the in-browser folder picker: listing, quick places
|
|
2
|
+
// and a bounded name search. Directories only; never reads file contents.
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import os from "node:os";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
const IS_WINDOWS = process.platform === "win32";
|
|
7
|
+
function isDir(p) {
|
|
8
|
+
try {
|
|
9
|
+
return fs.statSync(p).isDirectory();
|
|
10
|
+
}
|
|
11
|
+
catch {
|
|
12
|
+
return false;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
function hasGit(dir) {
|
|
16
|
+
return fs.promises.access(path.join(dir, ".git")).then(() => true, () => false);
|
|
17
|
+
}
|
|
18
|
+
const PLACE_NAMES = ["Desktop", "Documents", "Downloads", "repos", "source", "code", "dev", "projects", "workspace"];
|
|
19
|
+
function places() {
|
|
20
|
+
const home = os.homedir();
|
|
21
|
+
const out = [{ name: "Home", path: home }];
|
|
22
|
+
const seen = new Set();
|
|
23
|
+
for (const name of PLACE_NAMES) {
|
|
24
|
+
const p = path.join(home, name);
|
|
25
|
+
// Case-insensitive filesystems report "Repos" and "repos" as the same folder.
|
|
26
|
+
if (seen.has(p.toLowerCase()) || !isDir(p))
|
|
27
|
+
continue;
|
|
28
|
+
seen.add(p.toLowerCase());
|
|
29
|
+
out.push({ name, path: p });
|
|
30
|
+
}
|
|
31
|
+
return out;
|
|
32
|
+
}
|
|
33
|
+
function roots() {
|
|
34
|
+
const list = IS_WINDOWS
|
|
35
|
+
? "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
|
36
|
+
.split("")
|
|
37
|
+
.map((l) => `${l}:\\`)
|
|
38
|
+
.filter((r) => fs.existsSync(r))
|
|
39
|
+
: ["/"];
|
|
40
|
+
return list.map((r) => ({ name: r, path: r, isDir: true }));
|
|
41
|
+
}
|
|
42
|
+
/** `~`, `~/x` and an empty path resolve against the user's home; anything else is taken as given. */
|
|
43
|
+
function expand(dir) {
|
|
44
|
+
if (dir === "~")
|
|
45
|
+
return os.homedir();
|
|
46
|
+
if (dir.startsWith("~/") || dir.startsWith("~\\"))
|
|
47
|
+
return path.join(os.homedir(), dir.slice(2));
|
|
48
|
+
return dir;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* List the subfolders of `dir`. A missing `dir` is the home folder (so the
|
|
52
|
+
* picker opens where the user works); `""` is the machine's roots (drives).
|
|
53
|
+
*/
|
|
54
|
+
export async function fsList(dir) {
|
|
55
|
+
const home = os.homedir();
|
|
56
|
+
if (dir === "")
|
|
57
|
+
return { path: "", parent: null, home, places: places(), entries: roots() };
|
|
58
|
+
const resolved = path.resolve(expand(dir || home));
|
|
59
|
+
const dirents = await fs.promises.readdir(resolved, { withFileTypes: true });
|
|
60
|
+
const entries = dirents
|
|
61
|
+
.filter((d) => d.isDirectory() || (d.isSymbolicLink() && isDir(path.join(resolved, d.name))))
|
|
62
|
+
.map((d) => ({ name: d.name, path: path.join(resolved, d.name), isDir: true }))
|
|
63
|
+
.sort((a, b) => a.name.localeCompare(b.name, undefined, { sensitivity: "base" }));
|
|
64
|
+
if (entries.length <= 400) {
|
|
65
|
+
await Promise.all(entries.map(async (e) => {
|
|
66
|
+
if (await hasGit(e.path))
|
|
67
|
+
e.isRepo = true;
|
|
68
|
+
}));
|
|
69
|
+
}
|
|
70
|
+
const parent = path.dirname(resolved);
|
|
71
|
+
return { path: resolved, parent: parent === resolved ? "" : parent, home, places: places(), entries };
|
|
72
|
+
}
|
|
73
|
+
const SKIP = new Set([
|
|
74
|
+
"node_modules",
|
|
75
|
+
"appdata",
|
|
76
|
+
"library",
|
|
77
|
+
"target",
|
|
78
|
+
"dist",
|
|
79
|
+
"build",
|
|
80
|
+
"venv",
|
|
81
|
+
"__pycache__",
|
|
82
|
+
"vendor",
|
|
83
|
+
"windows",
|
|
84
|
+
"program files",
|
|
85
|
+
"program files (x86)",
|
|
86
|
+
"programdata",
|
|
87
|
+
"$recycle.bin",
|
|
88
|
+
"system volume information",
|
|
89
|
+
]);
|
|
90
|
+
/**
|
|
91
|
+
* Find folders under `root` (default: home) whose name contains every word of
|
|
92
|
+
* `query`. Breadth-first, so shallow matches win, and bounded by depth, folder
|
|
93
|
+
* count and time so a huge disk can't stall the server.
|
|
94
|
+
*/
|
|
95
|
+
export async function fsSearch(query, root) {
|
|
96
|
+
const tokens = query.toLowerCase().split(/\s+/).filter(Boolean);
|
|
97
|
+
if (tokens.length === 0)
|
|
98
|
+
return [];
|
|
99
|
+
const start = path.resolve(expand(root || os.homedir()));
|
|
100
|
+
const deadline = Date.now() + 1500;
|
|
101
|
+
const MAX_VISITED = 8000;
|
|
102
|
+
const MAX_DEPTH = 5;
|
|
103
|
+
const hits = [];
|
|
104
|
+
let level = [start];
|
|
105
|
+
let visited = 0;
|
|
106
|
+
for (let depth = 0; depth <= MAX_DEPTH && level.length > 0; depth++) {
|
|
107
|
+
const next = [];
|
|
108
|
+
// Read one level in small parallel batches, checking the clock between them.
|
|
109
|
+
for (let i = 0; i < level.length; i += 24) {
|
|
110
|
+
if (Date.now() > deadline || visited > MAX_VISITED)
|
|
111
|
+
break;
|
|
112
|
+
const batch = level.slice(i, i + 24);
|
|
113
|
+
const results = await Promise.all(batch.map((d) => fs.promises.readdir(d, { withFileTypes: true }).catch(() => [])));
|
|
114
|
+
results.forEach((dirents, j) => {
|
|
115
|
+
for (const d of dirents) {
|
|
116
|
+
if (!d.isDirectory())
|
|
117
|
+
continue;
|
|
118
|
+
const lower = d.name.toLowerCase();
|
|
119
|
+
if (d.name.startsWith(".") || SKIP.has(lower))
|
|
120
|
+
continue;
|
|
121
|
+
visited++;
|
|
122
|
+
const full = path.join(batch[j], d.name);
|
|
123
|
+
if (tokens.every((t) => lower.includes(t))) {
|
|
124
|
+
const score = (lower === tokens.join(" ") ? 0 : lower.startsWith(tokens[0]) ? 1 : 2) + depth * 0.25;
|
|
125
|
+
hits.push({ name: d.name, path: full, parent: batch[j], isRepo: false, score });
|
|
126
|
+
}
|
|
127
|
+
next.push(full);
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
if (hits.length >= 60 || Date.now() > deadline || visited > MAX_VISITED)
|
|
132
|
+
break;
|
|
133
|
+
level = next;
|
|
134
|
+
}
|
|
135
|
+
hits.sort((a, b) => a.score - b.score || a.name.localeCompare(b.name));
|
|
136
|
+
const top = hits.slice(0, 30);
|
|
137
|
+
await Promise.all(top.map(async (h) => {
|
|
138
|
+
h.isRepo = await hasGit(h.path);
|
|
139
|
+
}));
|
|
140
|
+
// Project roots first within the same relevance band.
|
|
141
|
+
top.sort((a, b) => Math.floor(a.score) - Math.floor(b.score) || Number(b.isRepo) - Number(a.isRepo) || a.score - b.score);
|
|
142
|
+
return top.map(({ score: _score, ...h }) => h);
|
|
143
|
+
}
|
|
@@ -54,33 +54,32 @@ async function* turnObjects(filePath) {
|
|
|
54
54
|
yield obj;
|
|
55
55
|
}
|
|
56
56
|
}
|
|
57
|
-
|
|
57
|
+
/** Turns `offset`..`offset + limit` of a transcript. Only that window is materialised; the rest is just counted. */
|
|
58
|
+
export async function readTranscript(filePath, offset, limit = PAGE_SIZE) {
|
|
58
59
|
if (!fs.existsSync(filePath))
|
|
59
60
|
return { turns: [], totalTurns: 0, hasMore: false, nextOffset: 0 };
|
|
60
|
-
const
|
|
61
|
+
const turns = [];
|
|
61
62
|
let index = 0;
|
|
62
63
|
for await (const obj of turnObjects(filePath)) {
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
64
|
+
if (index >= offset && index < offset + limit) {
|
|
65
|
+
const contentBlocks = parseContentBlocks(obj);
|
|
66
|
+
const textPreview = takeChars(contentBlocks
|
|
67
|
+
.filter((b) => b.kind === "text")
|
|
68
|
+
.map((b) => b.text)
|
|
69
|
+
.join(" "), 200);
|
|
70
|
+
turns.push({
|
|
71
|
+
index,
|
|
72
|
+
role: obj.type,
|
|
73
|
+
textPreview,
|
|
74
|
+
timestamp: typeof obj.timestamp === "string" ? obj.timestamp : undefined,
|
|
75
|
+
contentBlocks,
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
index++;
|
|
75
79
|
}
|
|
76
|
-
const total =
|
|
77
|
-
const hasMore = offset +
|
|
78
|
-
return {
|
|
79
|
-
turns: all.slice(offset, offset + PAGE_SIZE),
|
|
80
|
-
totalTurns: total,
|
|
81
|
-
hasMore,
|
|
82
|
-
nextOffset: hasMore ? offset + PAGE_SIZE : total,
|
|
83
|
-
};
|
|
80
|
+
const total = index;
|
|
81
|
+
const hasMore = offset + limit < total;
|
|
82
|
+
return { turns, totalTurns: total, hasMore, nextOffset: hasMore ? offset + limit : total };
|
|
84
83
|
}
|
|
85
84
|
export async function searchTranscriptTurns(filePath, query) {
|
|
86
85
|
if (!fs.existsSync(filePath))
|