@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.
Files changed (37) hide show
  1. package/dist/assets/CardDetailView-BZE_MfjM.js +24 -0
  2. package/dist/assets/{DiffPane-bQsGoT7y.js → DiffPane-2WnoRvZ0.js} +2 -2
  3. package/dist/assets/KnowledgeView-C4WPFAUM.js +3 -0
  4. package/dist/assets/ListBoardView-DEE_eK4h.js +3 -0
  5. package/dist/assets/NewTaskDialog-D8cpc7Bc.js +1 -0
  6. package/dist/assets/OnboardingWizard-9ZkFPGvH.js +1 -0
  7. package/dist/assets/{ProcessManagerView-BIYjQpoP.js → ProcessManagerView-DJOr44DY.js} +1 -1
  8. package/dist/assets/ProposalCard-B8jqFtL8.js +1 -0
  9. package/dist/assets/{SearchOverlay-B4aywpIh.js → SearchOverlay-qGxzmM_v.js} +1 -1
  10. package/dist/assets/SettingsView-C3GkCbTz.js +1 -0
  11. package/dist/assets/SetupProjectView-CSx6ax5r.js +1 -0
  12. package/dist/assets/TaskDescriptionEditor-D0_b81zY.js +58 -0
  13. package/dist/assets/{Terminal-DJJcg-t6.js → Terminal-yq90NOBX.js} +1 -1
  14. package/dist/assets/deepEqual-BiJVGDfM.js +1 -0
  15. package/dist/assets/index-BtXUncAU.css +1 -0
  16. package/dist/assets/index-CQnL8JJ5.js +18 -0
  17. package/dist/assets/plus-Cecrm8sZ.js +1 -0
  18. package/dist/assets/x-B6wVesqk.js +1 -0
  19. package/dist/index.html +2 -2
  20. package/package.json +2 -2
  21. package/server-dist/server/commands.js +5 -39
  22. package/server-dist/server/fsBrowse.js +143 -0
  23. package/server-dist/server/transcripts.js +21 -22
  24. package/dist/assets/CardDetailView-CVWOsYyA.js +0 -24
  25. package/dist/assets/KnowledgeView-C5KHiBRG.js +0 -3
  26. package/dist/assets/ListBoardView-CzJcCLXS.js +0 -3
  27. package/dist/assets/NewTaskDialog-BuxvfgNU.js +0 -1
  28. package/dist/assets/OnboardingWizard-8WLFjTd8.js +0 -1
  29. package/dist/assets/ProposalCard-BbPobl1f.js +0 -1
  30. package/dist/assets/SettingsView-spBB3EwB.js +0 -1
  31. package/dist/assets/SetupProjectView-BuUDqDYU.js +0 -1
  32. package/dist/assets/TaskDescriptionEditor-Cv0nlU3w.js +0 -58
  33. package/dist/assets/chevron-right-BsVThPz4.js +0 -1
  34. package/dist/assets/index-DNKB6XwG.css +0 -1
  35. package/dist/assets/index-gWSenOqD.js +0 -18
  36. package/dist/assets/plus-XSPxArj5.js +0 -1
  37. 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-gWSenOqD.js"></script>
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-DNKB6XwG.css">
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.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, IS_WINDOWS } from "./util.js";
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
- export async function readTranscript(filePath, offset) {
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 all = [];
61
+ const turns = [];
61
62
  let index = 0;
62
63
  for await (const obj of turnObjects(filePath)) {
63
- const contentBlocks = parseContentBlocks(obj);
64
- const textPreview = takeChars(contentBlocks
65
- .filter((b) => b.kind === "text")
66
- .map((b) => b.text)
67
- .join(" "), 200);
68
- all.push({
69
- index: index++,
70
- role: obj.type,
71
- textPreview,
72
- timestamp: typeof obj.timestamp === "string" ? obj.timestamp : undefined,
73
- contentBlocks,
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 = all.length;
77
- const hasMore = offset + PAGE_SIZE < total;
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))