@ycniuqton/devlens 0.1.2 → 0.1.4

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.
@@ -0,0 +1 @@
1
+ export declare const browserRouter: import("express-serve-static-core").Router;
@@ -0,0 +1,60 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.browserRouter = void 0;
4
+ const express_1 = require("express");
5
+ const files_1 = require("../services/files");
6
+ exports.browserRouter = (0, express_1.Router)();
7
+ // GET /api/browser/branch — current branch name
8
+ exports.browserRouter.get('/branch', async (req, res) => {
9
+ const git = req.app.locals.gitService;
10
+ try {
11
+ const branch = await git.getCurrentBranch();
12
+ res.json({ branch });
13
+ }
14
+ catch (err) {
15
+ res.status(500).json({ error: err.message });
16
+ }
17
+ });
18
+ // GET /api/browser/commits?limit=50 — recent commits in current branch
19
+ exports.browserRouter.get('/commits', async (req, res) => {
20
+ const git = req.app.locals.gitService;
21
+ try {
22
+ const limit = parseInt(req.query.limit) || 50;
23
+ const log = await git.getLog(limit);
24
+ res.json(log);
25
+ }
26
+ catch (err) {
27
+ res.status(500).json({ error: err.message });
28
+ }
29
+ });
30
+ // GET /api/browser/commit/:hash — show commit details (files + diff)
31
+ exports.browserRouter.get('/commit/:hash', async (req, res) => {
32
+ const git = req.app.locals.gitService;
33
+ try {
34
+ const files = await git.getCommitFiles(req.params.hash);
35
+ const diff = await git.getCommitDiff(req.params.hash);
36
+ res.json({ hash: req.params.hash, files, diff });
37
+ }
38
+ catch (err) {
39
+ res.status(500).json({ error: err.message });
40
+ }
41
+ });
42
+ // GET /api/browser/files?path=... — list directory contents
43
+ exports.browserRouter.get('/files', (req, res) => {
44
+ const projectDir = req.app.locals.projectDir;
45
+ const relPath = req.query.path || '';
46
+ const entries = (0, files_1.listDirectory)(projectDir, relPath);
47
+ res.json({ path: relPath, entries });
48
+ });
49
+ // GET /api/browser/file?path=... — read file content
50
+ exports.browserRouter.get('/file', (req, res) => {
51
+ const projectDir = req.app.locals.projectDir;
52
+ const relPath = req.query.path || '';
53
+ if (!relPath)
54
+ return res.status(400).json({ error: 'path required' });
55
+ const result = (0, files_1.readFile)(projectDir, relPath);
56
+ if (!result)
57
+ return res.status(404).json({ error: 'not found' });
58
+ res.json({ path: relPath, ...result });
59
+ });
60
+ //# sourceMappingURL=browser.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"browser.js","sourceRoot":"","sources":["../../src/routes/browser.ts"],"names":[],"mappings":"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"}
package/dist/server.js CHANGED
@@ -17,6 +17,7 @@ const diff_1 = require("./routes/diff");
17
17
  const tasks_1 = require("./routes/tasks");
18
18
  const integrations_1 = require("./routes/integrations");
19
19
  const rules_2 = require("./routes/rules");
20
+ const browser_1 = require("./routes/browser");
20
21
  function createServer(options) {
21
22
  const app = (0, express_1.default)();
22
23
  const httpServer = http_1.default.createServer(app);
@@ -35,10 +36,18 @@ function createServer(options) {
35
36
  app.locals.projectDir = options.projectDir;
36
37
  app.locals.port = options.port;
37
38
  // API routes
39
+ app.get('/api/info', (_req, res) => {
40
+ res.json({
41
+ projectDir: options.projectDir,
42
+ projectName: path_1.default.basename(options.projectDir),
43
+ port: options.port,
44
+ });
45
+ });
38
46
  app.use('/api', diff_1.diffRouter);
39
47
  app.use('/api/tasks', tasks_1.tasksRouter);
40
48
  app.use('/api/integrations', integrations_1.integrationsRouter);
41
49
  app.use('/api/rules', rules_2.rulesRouter);
50
+ app.use('/api/browser', browser_1.browserRouter);
42
51
  // Static files
43
52
  const publicDir = path_1.default.resolve(__dirname, '../public');
44
53
  app.use(express_1.default.static(publicDir));
@@ -1 +1 @@
1
- {"version":3,"file":"server.js","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"server.js","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,12 @@
1
+ export interface FileEntry {
2
+ name: string;
3
+ path: string;
4
+ type: 'file' | 'dir';
5
+ size?: number;
6
+ }
7
+ export declare function listDirectory(projectDir: string, relPath: string): FileEntry[];
8
+ export declare function readFile(projectDir: string, relPath: string, maxBytes?: number): {
9
+ content: string;
10
+ truncated: boolean;
11
+ size: number;
12
+ } | null;
@@ -0,0 +1,64 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.listDirectory = listDirectory;
7
+ exports.readFile = readFile;
8
+ const fs_1 = __importDefault(require("fs"));
9
+ const path_1 = __importDefault(require("path"));
10
+ const IGNORE = new Set(['.git', 'node_modules', '.devlens', 'dist', '.next', '.nuxt', '.cache']);
11
+ function isSafePath(projectDir, target) {
12
+ const abs = path_1.default.resolve(projectDir, target);
13
+ const root = path_1.default.resolve(projectDir);
14
+ return abs === root || abs.startsWith(root + path_1.default.sep);
15
+ }
16
+ function listDirectory(projectDir, relPath) {
17
+ if (!isSafePath(projectDir, relPath))
18
+ return [];
19
+ const abs = path_1.default.resolve(projectDir, relPath);
20
+ if (!fs_1.default.existsSync(abs) || !fs_1.default.statSync(abs).isDirectory())
21
+ return [];
22
+ const entries = fs_1.default.readdirSync(abs, { withFileTypes: true });
23
+ const result = [];
24
+ for (const entry of entries) {
25
+ if (IGNORE.has(entry.name))
26
+ continue;
27
+ if (entry.name.startsWith('.') && entry.name !== '.gitignore' && entry.name !== '.env.example')
28
+ continue;
29
+ const entryRel = relPath ? path_1.default.posix.join(relPath, entry.name) : entry.name;
30
+ const entryAbs = path_1.default.join(abs, entry.name);
31
+ if (entry.isDirectory()) {
32
+ result.push({ name: entry.name, path: entryRel, type: 'dir' });
33
+ }
34
+ else if (entry.isFile()) {
35
+ let size = 0;
36
+ try {
37
+ size = fs_1.default.statSync(entryAbs).size;
38
+ }
39
+ catch { }
40
+ result.push({ name: entry.name, path: entryRel, type: 'file', size });
41
+ }
42
+ }
43
+ // Folders first, then alphabetical
44
+ result.sort((a, b) => {
45
+ if (a.type !== b.type)
46
+ return a.type === 'dir' ? -1 : 1;
47
+ return a.name.localeCompare(b.name);
48
+ });
49
+ return result;
50
+ }
51
+ function readFile(projectDir, relPath, maxBytes = 200000) {
52
+ if (!isSafePath(projectDir, relPath))
53
+ return null;
54
+ const abs = path_1.default.resolve(projectDir, relPath);
55
+ if (!fs_1.default.existsSync(abs) || !fs_1.default.statSync(abs).isFile())
56
+ return null;
57
+ const stat = fs_1.default.statSync(abs);
58
+ const size = stat.size;
59
+ const truncated = size > maxBytes;
60
+ const buf = fs_1.default.readFileSync(abs);
61
+ const content = truncated ? buf.subarray(0, maxBytes).toString('utf-8') + '\n\n... [truncated]' : buf.toString('utf-8');
62
+ return { content, truncated, size };
63
+ }
64
+ //# sourceMappingURL=files.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"files.js","sourceRoot":"","sources":["../../src/services/files.ts"],"names":[],"mappings":"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"}
@@ -4,5 +4,11 @@ export interface GitService {
4
4
  getStatus(): Promise<FileStatus[]>;
5
5
  getLog(limit?: number): Promise<LogEntry[]>;
6
6
  isRepo(): Promise<boolean>;
7
+ getCurrentBranch(): Promise<string>;
8
+ getCommitDiff(hash: string): Promise<string>;
9
+ getCommitFiles(hash: string): Promise<{
10
+ path: string;
11
+ status: string;
12
+ }[]>;
7
13
  }
8
14
  export declare function createGitService(projectDir: string): GitService;
@@ -85,6 +85,43 @@ function createGitService(projectDir) {
85
85
  async isRepo() {
86
86
  return git.checkIsRepo();
87
87
  },
88
+ async getCurrentBranch() {
89
+ try {
90
+ const status = await git.status();
91
+ return status.current || 'HEAD';
92
+ }
93
+ catch {
94
+ return 'unknown';
95
+ }
96
+ },
97
+ async getCommitDiff(hash) {
98
+ try {
99
+ return await git.show([hash]);
100
+ }
101
+ catch {
102
+ return '';
103
+ }
104
+ },
105
+ async getCommitFiles(hash) {
106
+ try {
107
+ const raw = await git.raw(['show', '--name-status', '--format=', hash]);
108
+ const lines = raw.split('\n').filter(Boolean);
109
+ const result = [];
110
+ for (const line of lines) {
111
+ const parts = line.split('\t');
112
+ if (parts.length < 2)
113
+ continue;
114
+ const code = parts[0].trim();
115
+ const file = parts[parts.length - 1];
116
+ const statusMap = { A: 'added', M: 'modified', D: 'deleted', R: 'renamed' };
117
+ result.push({ path: file, status: statusMap[code[0]] || 'modified' });
118
+ }
119
+ return result;
120
+ }
121
+ catch {
122
+ return [];
123
+ }
124
+ },
88
125
  };
89
126
  }
90
127
  //# sourceMappingURL=git.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"git.js","sourceRoot":"","sources":["../../src/services/git.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"git.js","sourceRoot":"","sources":["../../src/services/git.ts"],"names":[],"mappings":";;;;;AAeA,4CAyHC;AAxID,4DAAkD;AAClD,4CAAoB;AACpB,gDAAwB;AAaxB,SAAgB,gBAAgB,CAAC,UAAkB;IACjD,MAAM,GAAG,GAAc,IAAA,oBAAS,EAAC,UAAU,CAAC,CAAC;IAE7C,kEAAkE;IAClE,SAAS,iBAAiB,CAAC,QAAgB;QACzC,IAAI,CAAC;YACH,MAAM,QAAQ,GAAG,cAAI,CAAC,IAAI,CAAC,UAAU,EAAE,QAAQ,CAAC,CAAC;YACjD,MAAM,OAAO,GAAG,YAAE,CAAC,YAAY,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC;YACnD,MAAM,KAAK,GAAG,OAAO,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;YAClC,MAAM,KAAK,GAAG,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,GAAG,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;YACjD,OAAO,gBAAgB,QAAQ,MAAM,QAAQ,gDAAgD,QAAQ,gBAAgB,KAAK,CAAC,MAAM,QAAQ,KAAK,EAAE,CAAC;QACnJ,CAAC;QAAC,MAAM,CAAC;YACP,OAAO,EAAE,CAAC;QACZ,CAAC;IACH,CAAC;IAED,OAAO;QACL,KAAK,CAAC,OAAO,CAAC,MAAe;YAC3B,IAAI,IAAI,GAAG,EAAE,CAAC;YACd,IAAI,MAAM,KAAK,QAAQ,EAAE,CAAC;gBACxB,IAAI,GAAG,MAAM,GAAG,CAAC,IAAI,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC;YACtC,CAAC;iBAAM,IAAI,MAAM,KAAK,UAAU,EAAE,CAAC;gBACjC,IAAI,GAAG,MAAM,GAAG,CAAC,IAAI,EAAE,CAAC;YAC1B,CAAC;iBAAM,CAAC;gBACN,MAAM,QAAQ,GAAG,MAAM,GAAG,CAAC,IAAI,EAAE,CAAC;gBAClC,MAAM,MAAM,GAAG,MAAM,GAAG,CAAC,IAAI,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC;gBAC5C,IAAI,GAAG,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;YACvD,CAAC;YAED,4CAA4C;YAC5C,IAAI,MAAM,KAAK,QAAQ,EAAE,CAAC;gBACxB,MAAM,MAAM,GAAG,MAAM,GAAG,CAAC,MAAM,EAAE,CAAC;gBAClC,KAAK,MAAM,CAAC,IAAI,MAAM,CAAC,SAAS,EAAE,CAAC;oBACjC,MAAM,aAAa,GAAG,iBAAiB,CAAC,CAAC,CAAC,CAAC;oBAC3C,IAAI,aAAa,EAAE,CAAC;wBAClB,IAAI,GAAG,IAAI,CAAC,CAAC,CAAC,IAAI,GAAG,IAAI,GAAG,aAAa,CAAC,CAAC,CAAC,aAAa,CAAC;oBAC5D,CAAC;gBACH,CAAC;YACH,CAAC;YAED,OAAO,IAAI,CAAC;QACd,CAAC;QAED,KAAK,CAAC,SAAS;YACb,MAAM,MAAM,GAAG,MAAM,GAAG,CAAC,MAAM,EAAE,CAAC;YAClC,MAAM,KAAK,GAAiB,EAAE,CAAC;YAE/B,KAAK,MAAM,CAAC,IAAI,MAAM,CAAC,QAAQ,EAAE,CAAC;gBAChC,KAAK,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,CAAC,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC,CAAC;YAC7D,CAAC;YACD,KAAK,MAAM,CAAC,IAAI,MAAM,CAAC,SAAS,EAAE,CAAC;gBACjC,KAAK,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,CAAC,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC,CAAC;YAC9D,CAAC;YACD,KAAK,MAAM,CAAC,IAAI,MAAM,CAAC,OAAO,EAAE,CAAC;gBAC/B,KAAK,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,CAAC,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC,CAAC;YAC5D,CAAC;YACD,KAAK,MAAM,CAAC,IAAI,MAAM,CAAC,OAAO,EAAE,CAAC;gBAC/B,KAAK,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,CAAC,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC;YACzD,CAAC;YACD,KAAK,MAAM,CAAC,IAAI,MAAM,CAAC,MAAM,EAAE,CAAC;gBAC9B,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,CAAC,CAAC,EAAE,CAAC;oBACnC,KAAK,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,CAAC,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC;gBAC5D,CAAC;YACH,CAAC;YACD,KAAK,MAAM,CAAC,IAAI,MAAM,CAAC,OAAO,EAAE,CAAC;gBAC/B,KAAK,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,CAAC,CAAC,EAAE,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC;YAC9D,CAAC;YAED,OAAO,KAAK,CAAC;QACf,CAAC;QAED,KAAK,CAAC,MAAM,CAAC,KAAK,GAAG,EAAE;YACrB,MAAM,GAAG,GAAG,MAAM,GAAG,CAAC,GAAG,CAAC,EAAE,QAAQ,EAAE,KAAK,EAAE,CAAC,CAAC;YAC/C,OAAO,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;gBAC7B,IAAI,EAAE,KAAK,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC,EAAE,CAAC,CAAC;gBAChC,OAAO,EAAE,KAAK,CAAC,OAAO;gBACtB,MAAM,EAAE,KAAK,CAAC,WAAW;gBACzB,IAAI,EAAE,KAAK,CAAC,IAAI;aACjB,CAAC,CAAC,CAAC;QACN,CAAC;QAED,KAAK,CAAC,MAAM;YACV,OAAO,GAAG,CAAC,WAAW,EAAE,CAAC;QAC3B,CAAC;QAED,KAAK,CAAC,gBAAgB;YACpB,IAAI,CAAC;gBACH,MAAM,MAAM,GAAG,MAAM,GAAG,CAAC,MAAM,EAAE,CAAC;gBAClC,OAAO,MAAM,CAAC,OAAO,IAAI,MAAM,CAAC;YAClC,CAAC;YAAC,MAAM,CAAC;gBACP,OAAO,SAAS,CAAC;YACnB,CAAC;QACH,CAAC;QAED,KAAK,CAAC,aAAa,CAAC,IAAY;YAC9B,IAAI,CAAC;gBACH,OAAO,MAAM,GAAG,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC;YAChC,CAAC;YAAC,MAAM,CAAC;gBACP,OAAO,EAAE,CAAC;YACZ,CAAC;QACH,CAAC;QAED,KAAK,CAAC,cAAc,CAAC,IAAY;YAC/B,IAAI,CAAC;gBACH,MAAM,GAAG,GAAG,MAAM,GAAG,CAAC,GAAG,CAAC,CAAC,MAAM,EAAE,eAAe,EAAE,WAAW,EAAE,IAAI,CAAC,CAAC,CAAC;gBACxE,MAAM,KAAK,GAAG,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;gBAC9C,MAAM,MAAM,GAAuC,EAAE,CAAC;gBACtD,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;oBACzB,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;oBAC/B,IAAI,KAAK,CAAC,MAAM,GAAG,CAAC;wBAAE,SAAS;oBAC/B,MAAM,IAAI,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;oBAC7B,MAAM,IAAI,GAAG,KAAK,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;oBACrC,MAAM,SAAS,GAA2B,EAAE,CAAC,EAAE,OAAO,EAAE,CAAC,EAAE,UAAU,EAAE,CAAC,EAAE,SAAS,EAAE,CAAC,EAAE,SAAS,EAAE,CAAC;oBACpG,MAAM,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,EAAE,SAAS,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,UAAU,EAAE,CAAC,CAAC;gBACxE,CAAC;gBACD,OAAO,MAAM,CAAC;YAChB,CAAC;YAAC,MAAM,CAAC;gBACP,OAAO,EAAE,CAAC;YACZ,CAAC;QACH,CAAC;KACF,CAAC;AACJ,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ycniuqton/devlens",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -29,6 +29,7 @@
29
29
  },
30
30
  "homepage": "https://github.com/ycniuqton/Devlens#readme",
31
31
  "dependencies": {
32
+ "@ycniuqton/devlens": "^0.1.3",
32
33
  "better-sqlite3": "^12.8.0",
33
34
  "chokidar": "^3.6.0",
34
35
  "commander": "^12.0.0",
@@ -144,6 +144,7 @@ body {
144
144
  gap: var(--sp-3);
145
145
  padding: var(--sp-2) var(--sp-2);
146
146
  margin-bottom: var(--sp-8);
147
+ min-width: 0;
147
148
  }
148
149
 
149
150
  .brand-icon {
@@ -158,6 +159,13 @@ body {
158
159
  box-shadow: var(--shadow-glow);
159
160
  }
160
161
 
162
+ .brand-text {
163
+ display: flex;
164
+ flex-direction: column;
165
+ min-width: 0;
166
+ line-height: 1.15;
167
+ }
168
+
161
169
  .brand-name {
162
170
  font-size: var(--text-lg);
163
171
  font-weight: 700;
@@ -165,6 +173,23 @@ body {
165
173
  color: var(--color-text);
166
174
  }
167
175
 
176
+ .brand-project {
177
+ font-size: var(--text-xs);
178
+ font-weight: 600;
179
+ color: var(--color-primary-hover);
180
+ font-family: var(--font-mono);
181
+ white-space: nowrap;
182
+ overflow: hidden;
183
+ text-overflow: ellipsis;
184
+ margin-top: 3px;
185
+ padding: 2px 6px;
186
+ background: var(--color-primary-subtle);
187
+ border-radius: var(--radius-sm);
188
+ display: block;
189
+ max-width: 100%;
190
+ min-width: 0;
191
+ }
192
+
168
193
  .sidebar-nav {
169
194
  display: flex;
170
195
  flex-direction: column;
@@ -1450,6 +1475,398 @@ body {
1450
1475
  .file-status-untracked .file-name { color: var(--color-text-muted); font-style: italic; }
1451
1476
  .file-status-added .file-name { color: var(--color-success); }
1452
1477
 
1478
+ /* ============================================================
1479
+ Files Tab — VSCode-style Explorer
1480
+ ============================================================ */
1481
+ .file-breadcrumb {
1482
+ font-family: var(--font-mono);
1483
+ font-size: var(--text-xs);
1484
+ color: var(--color-text-secondary);
1485
+ background: var(--color-surface-2);
1486
+ padding: 4px 10px;
1487
+ border-radius: var(--radius-sm);
1488
+ max-width: 480px;
1489
+ overflow: hidden;
1490
+ text-overflow: ellipsis;
1491
+ white-space: nowrap;
1492
+ }
1493
+
1494
+ .explorer-container {
1495
+ display: flex;
1496
+ gap: var(--sp-3);
1497
+ flex: 1;
1498
+ min-height: calc(100vh - 200px);
1499
+ height: calc(100vh - 200px);
1500
+ }
1501
+
1502
+ .explorer-tree-pane {
1503
+ width: 280px;
1504
+ flex-shrink: 0;
1505
+ background: var(--color-surface);
1506
+ border: 1px solid var(--color-border);
1507
+ border-radius: var(--radius-lg);
1508
+ display: flex;
1509
+ flex-direction: column;
1510
+ overflow: hidden;
1511
+ }
1512
+
1513
+ .explorer-pane-header {
1514
+ padding: var(--sp-3) var(--sp-4);
1515
+ border-bottom: 1px solid var(--color-border-subtle);
1516
+ display: flex;
1517
+ align-items: center;
1518
+ justify-content: space-between;
1519
+ }
1520
+
1521
+ .explorer-pane-header h3 {
1522
+ font-size: 10px;
1523
+ font-weight: 700;
1524
+ letter-spacing: 0.1em;
1525
+ color: var(--color-text-muted);
1526
+ }
1527
+
1528
+ .explorer-tree {
1529
+ flex: 1;
1530
+ overflow-y: auto;
1531
+ padding: var(--sp-2) 0;
1532
+ }
1533
+
1534
+ .explorer-row {
1535
+ display: flex;
1536
+ align-items: center;
1537
+ gap: 6px;
1538
+ padding-right: var(--sp-3);
1539
+ cursor: pointer;
1540
+ font-size: var(--text-sm);
1541
+ color: var(--color-text-secondary);
1542
+ height: 24px;
1543
+ transition: background var(--duration-fast) var(--ease);
1544
+ user-select: none;
1545
+ }
1546
+
1547
+ .explorer-row:hover { background: var(--color-surface-hover); }
1548
+
1549
+ .explorer-row.folder-row { color: var(--color-text); font-weight: 500; }
1550
+
1551
+ .explorer-row.file-row.active {
1552
+ background: var(--color-primary-subtle);
1553
+ color: var(--color-primary-hover);
1554
+ }
1555
+
1556
+ .explorer-row .explorer-name {
1557
+ flex: 1;
1558
+ overflow: hidden;
1559
+ text-overflow: ellipsis;
1560
+ white-space: nowrap;
1561
+ }
1562
+
1563
+ .explorer-row svg { flex-shrink: 0; }
1564
+
1565
+ .explorer-chevron {
1566
+ color: var(--color-text-muted);
1567
+ transition: transform var(--duration-fast) var(--ease);
1568
+ }
1569
+
1570
+ .explorer-chevron.expanded { transform: rotate(90deg); }
1571
+
1572
+ .explorer-viewer-pane {
1573
+ flex: 1;
1574
+ background: var(--color-surface);
1575
+ border: 1px solid var(--color-border);
1576
+ border-radius: var(--radius-lg);
1577
+ display: flex;
1578
+ flex-direction: column;
1579
+ overflow: hidden;
1580
+ min-width: 0;
1581
+ }
1582
+
1583
+ .explorer-viewer {
1584
+ flex: 1;
1585
+ overflow: auto;
1586
+ display: flex;
1587
+ flex-direction: column;
1588
+ }
1589
+
1590
+ .code-viewer {
1591
+ display: flex;
1592
+ flex-direction: column;
1593
+ height: 100%;
1594
+ }
1595
+
1596
+ .code-viewer-header {
1597
+ padding: var(--sp-3) var(--sp-4);
1598
+ background: var(--color-surface-2);
1599
+ border-bottom: 1px solid var(--color-border-subtle);
1600
+ display: flex;
1601
+ align-items: center;
1602
+ justify-content: space-between;
1603
+ gap: var(--sp-3);
1604
+ flex-shrink: 0;
1605
+ }
1606
+
1607
+ .code-file-path {
1608
+ font-family: var(--font-mono);
1609
+ font-size: var(--text-xs);
1610
+ color: var(--color-text);
1611
+ overflow: hidden;
1612
+ text-overflow: ellipsis;
1613
+ white-space: nowrap;
1614
+ }
1615
+
1616
+ .code-file-meta {
1617
+ font-size: var(--text-xs);
1618
+ color: var(--color-text-muted);
1619
+ flex-shrink: 0;
1620
+ }
1621
+
1622
+ .code-viewer-body {
1623
+ flex: 1;
1624
+ overflow: auto;
1625
+ font-family: var(--font-mono);
1626
+ font-size: var(--text-sm);
1627
+ line-height: 1.5;
1628
+ background: var(--color-bg);
1629
+ }
1630
+
1631
+ .code-line {
1632
+ display: flex;
1633
+ min-height: 21px;
1634
+ }
1635
+
1636
+ .code-line:hover { background: var(--color-surface-hover); }
1637
+
1638
+ .code-line-num {
1639
+ display: inline-block;
1640
+ min-width: 50px;
1641
+ padding: 0 12px;
1642
+ text-align: right;
1643
+ color: var(--color-text-muted);
1644
+ user-select: none;
1645
+ flex-shrink: 0;
1646
+ border-right: 1px solid var(--color-border-subtle);
1647
+ }
1648
+
1649
+ .code-line-text {
1650
+ padding: 0 12px;
1651
+ color: var(--color-text);
1652
+ white-space: pre;
1653
+ flex: 1;
1654
+ overflow-x: auto;
1655
+ }
1656
+
1657
+ /* ============================================================
1658
+ History Tab — Git Commits
1659
+ ============================================================ */
1660
+ .branch-pill {
1661
+ display: inline-flex;
1662
+ align-items: center;
1663
+ gap: var(--sp-2);
1664
+ padding: 4px 10px;
1665
+ background: var(--color-primary-subtle);
1666
+ color: var(--color-primary-hover);
1667
+ border-radius: var(--radius-full);
1668
+ font-family: var(--font-mono);
1669
+ font-size: var(--text-xs);
1670
+ font-weight: 600;
1671
+ }
1672
+
1673
+ .history-container {
1674
+ display: flex;
1675
+ gap: var(--sp-3);
1676
+ flex: 1;
1677
+ min-height: calc(100vh - 200px);
1678
+ height: calc(100vh - 200px);
1679
+ }
1680
+
1681
+ .history-list-pane {
1682
+ width: 360px;
1683
+ flex-shrink: 0;
1684
+ background: var(--color-surface);
1685
+ border: 1px solid var(--color-border);
1686
+ border-radius: var(--radius-lg);
1687
+ display: flex;
1688
+ flex-direction: column;
1689
+ overflow: hidden;
1690
+ }
1691
+
1692
+ .commit-count {
1693
+ font-size: 10px;
1694
+ font-weight: 600;
1695
+ color: var(--color-text-muted);
1696
+ background: var(--color-surface-3);
1697
+ padding: 2px 8px;
1698
+ border-radius: var(--radius-full);
1699
+ }
1700
+
1701
+ .commits-list {
1702
+ list-style: none;
1703
+ flex: 1;
1704
+ overflow-y: auto;
1705
+ padding: var(--sp-2);
1706
+ }
1707
+
1708
+ .commit-item {
1709
+ display: flex;
1710
+ gap: var(--sp-3);
1711
+ padding: var(--sp-3) var(--sp-3);
1712
+ border-radius: var(--radius-md);
1713
+ cursor: pointer;
1714
+ margin-bottom: 2px;
1715
+ transition: background var(--duration-fast) var(--ease);
1716
+ position: relative;
1717
+ }
1718
+
1719
+ .commit-item:hover { background: var(--color-surface-hover); }
1720
+
1721
+ .commit-item.selected {
1722
+ background: var(--color-primary-subtle);
1723
+ }
1724
+
1725
+ .commit-item.selected .commit-marker { background: var(--color-primary); }
1726
+
1727
+ .commit-marker {
1728
+ width: 8px;
1729
+ height: 8px;
1730
+ border-radius: var(--radius-full);
1731
+ background: var(--color-text-muted);
1732
+ flex-shrink: 0;
1733
+ margin-top: 6px;
1734
+ }
1735
+
1736
+ .commit-content { flex: 1; min-width: 0; }
1737
+
1738
+ .commit-message {
1739
+ font-size: var(--text-sm);
1740
+ color: var(--color-text);
1741
+ font-weight: 500;
1742
+ margin-bottom: 4px;
1743
+ overflow: hidden;
1744
+ text-overflow: ellipsis;
1745
+ white-space: nowrap;
1746
+ }
1747
+
1748
+ .commit-meta {
1749
+ display: flex;
1750
+ gap: var(--sp-2);
1751
+ font-size: var(--text-xs);
1752
+ color: var(--color-text-muted);
1753
+ align-items: center;
1754
+ }
1755
+
1756
+ .commit-hash {
1757
+ font-family: var(--font-mono);
1758
+ background: var(--color-surface-3);
1759
+ padding: 1px 6px;
1760
+ border-radius: var(--radius-sm);
1761
+ }
1762
+
1763
+ .commit-author { font-weight: 500; }
1764
+
1765
+ .history-detail-pane {
1766
+ flex: 1;
1767
+ background: var(--color-surface);
1768
+ border: 1px solid var(--color-border);
1769
+ border-radius: var(--radius-lg);
1770
+ display: flex;
1771
+ flex-direction: column;
1772
+ overflow: hidden;
1773
+ min-width: 0;
1774
+ }
1775
+
1776
+ .commit-detail-header {
1777
+ padding: var(--sp-4) var(--sp-5);
1778
+ background: var(--color-surface-2);
1779
+ border-bottom: 1px solid var(--color-border-subtle);
1780
+ flex-shrink: 0;
1781
+ }
1782
+
1783
+ .commit-header-content {
1784
+ display: flex;
1785
+ align-items: center;
1786
+ gap: var(--sp-3);
1787
+ }
1788
+
1789
+ .commit-header-hash {
1790
+ font-family: var(--font-mono);
1791
+ font-size: var(--text-sm);
1792
+ font-weight: 600;
1793
+ color: var(--color-primary-hover);
1794
+ background: var(--color-primary-subtle);
1795
+ padding: 4px 10px;
1796
+ border-radius: var(--radius-sm);
1797
+ }
1798
+
1799
+ .commit-header-files {
1800
+ font-size: var(--text-xs);
1801
+ color: var(--color-text-muted);
1802
+ }
1803
+
1804
+ .commit-header-actions {
1805
+ margin-left: auto;
1806
+ display: flex;
1807
+ gap: var(--sp-2);
1808
+ }
1809
+
1810
+ .commit-detail-diff {
1811
+ flex: 1;
1812
+ overflow: auto;
1813
+ padding: var(--sp-3);
1814
+ }
1815
+
1816
+ .commit-file-section {
1817
+ border: 1px solid var(--color-border);
1818
+ border-radius: var(--radius-md);
1819
+ margin-bottom: var(--sp-2);
1820
+ overflow: hidden;
1821
+ }
1822
+
1823
+ .commit-file-header {
1824
+ display: flex;
1825
+ align-items: center;
1826
+ gap: var(--sp-3);
1827
+ padding: var(--sp-3) var(--sp-4);
1828
+ background: var(--color-surface-2);
1829
+ cursor: pointer;
1830
+ user-select: none;
1831
+ transition: background var(--duration-fast) var(--ease);
1832
+ min-height: 40px;
1833
+ }
1834
+
1835
+ .commit-file-header:hover { background: var(--color-surface-3); }
1836
+
1837
+ .commit-file-chevron {
1838
+ color: var(--color-text-muted);
1839
+ flex-shrink: 0;
1840
+ transition: transform var(--duration-fast) var(--ease);
1841
+ }
1842
+
1843
+ .commit-file-section.expanded .commit-file-chevron { transform: rotate(90deg); }
1844
+
1845
+ .commit-file-name {
1846
+ font-family: var(--font-mono);
1847
+ font-size: var(--text-sm);
1848
+ color: var(--color-text);
1849
+ overflow: hidden;
1850
+ text-overflow: ellipsis;
1851
+ white-space: nowrap;
1852
+ }
1853
+
1854
+ .commit-file-body {
1855
+ display: none;
1856
+ overflow-x: auto;
1857
+ }
1858
+
1859
+ .commit-file-section.expanded .commit-file-body { display: block; }
1860
+
1861
+ .raw-diff {
1862
+ margin: 0;
1863
+ padding: var(--sp-4);
1864
+ font-family: var(--font-mono);
1865
+ font-size: var(--text-sm);
1866
+ white-space: pre-wrap;
1867
+ color: var(--color-text);
1868
+ }
1869
+
1453
1870
  /* ============================================================
1454
1871
  Rules Tab
1455
1872
  ============================================================ */
@@ -1599,7 +2016,7 @@ body {
1599
2016
  ============================================================ */
1600
2017
  @media (max-width: 900px) {
1601
2018
  #sidebar { width: var(--sidebar-collapsed); padding: var(--sp-2); }
1602
- .brand-name, .nav-item span, .status-label { display: none; }
2019
+ .brand-name, .brand-project, .nav-item span, .status-label { display: none; }
1603
2020
  .sidebar-brand { justify-content: center; margin-bottom: var(--sp-4); }
1604
2021
  .nav-item { justify-content: center; padding: var(--sp-3); }
1605
2022
  #main-content { margin-left: var(--sidebar-collapsed); width: calc(100vw - var(--sidebar-collapsed)); }
package/public/index.html CHANGED
@@ -20,7 +20,10 @@
20
20
  <circle cx="12" cy="12" r="3"/><path d="M12 1v4M12 19v4M4.22 4.22l2.83 2.83M16.95 16.95l2.83 2.83M1 12h4M19 12h4M4.22 19.78l2.83-2.83M16.95 7.05l2.83-2.83"/>
21
21
  </svg>
22
22
  </div>
23
- <span class="brand-name">Devlens</span>
23
+ <div class="brand-text">
24
+ <span class="brand-name">Devlens</span>
25
+ <span class="brand-project" id="brand-project" title=""></span>
26
+ </div>
24
27
  </div>
25
28
 
26
29
  <nav class="sidebar-nav">
@@ -36,6 +39,18 @@
36
39
  </svg>
37
40
  <span>Tasks</span>
38
41
  </button>
42
+ <button class="nav-item" data-tab="browser" aria-label="File explorer">
43
+ <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
44
+ <path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/>
45
+ </svg>
46
+ <span>Files</span>
47
+ </button>
48
+ <button class="nav-item" data-tab="history" aria-label="Git history">
49
+ <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
50
+ <circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/>
51
+ </svg>
52
+ <span>History</span>
53
+ </button>
39
54
  <button class="nav-item" data-tab="rules" aria-label="View rules">
40
55
  <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
41
56
  <path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="9" y1="13" x2="15" y2="13"/><line x1="9" y1="17" x2="15" y2="17"/>
@@ -189,6 +204,81 @@
189
204
 
190
205
  </section>
191
206
 
207
+ <!-- Files View — VSCode-style file explorer -->
208
+ <section id="browser-view" class="tab-content">
209
+ <header class="view-header">
210
+ <h1>Files</h1>
211
+ <div class="header-actions">
212
+ <span class="file-breadcrumb" id="file-breadcrumb">No file selected</span>
213
+ <button class="btn btn-ghost btn-sm" id="browser-refresh-btn" title="Refresh tree">Refresh</button>
214
+ </div>
215
+ </header>
216
+
217
+ <div class="explorer-container">
218
+ <!-- Left: persistent file tree -->
219
+ <aside class="explorer-tree-pane">
220
+ <div class="explorer-pane-header">
221
+ <h3>EXPLORER</h3>
222
+ </div>
223
+ <div id="explorer-tree" class="explorer-tree">
224
+ <div class="empty-state-inline">Loading...</div>
225
+ </div>
226
+ </aside>
227
+
228
+ <!-- Right: file viewer -->
229
+ <main class="explorer-viewer-pane">
230
+ <div id="explorer-viewer" class="explorer-viewer">
231
+ <div class="empty-state">
232
+ <svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" opacity="0.3">
233
+ <path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/>
234
+ </svg>
235
+ <p>Select a file to view</p>
236
+ <span>Click any file in the tree on the left</span>
237
+ </div>
238
+ </div>
239
+ </main>
240
+ </div>
241
+ </section>
242
+
243
+ <!-- History View — git commits + diff viewer -->
244
+ <section id="history-view" class="tab-content">
245
+ <header class="view-header">
246
+ <h1>History</h1>
247
+ <div class="header-actions">
248
+ <span class="branch-pill" id="current-branch">
249
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="6" y1="3" x2="6" y2="15"/><circle cx="18" cy="6" r="3"/><circle cx="6" cy="18" r="3"/><path d="M18 9a9 9 0 0 1-9 9"/></svg>
250
+ <span id="current-branch-name">—</span>
251
+ </span>
252
+ <button class="btn btn-ghost btn-sm" id="history-refresh-btn">Refresh</button>
253
+ </div>
254
+ </header>
255
+
256
+ <div class="history-container">
257
+ <aside class="history-list-pane">
258
+ <div class="explorer-pane-header">
259
+ <h3>COMMITS</h3>
260
+ <span class="commit-count" id="commit-count">0</span>
261
+ </div>
262
+ <ul id="commits-list" class="commits-list">
263
+ <li class="empty-state-inline">Loading commits...</li>
264
+ </ul>
265
+ </aside>
266
+
267
+ <main class="history-detail-pane">
268
+ <div id="commit-detail-header" class="commit-detail-header">
269
+ <div class="empty-state">
270
+ <svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" opacity="0.3">
271
+ <circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/>
272
+ </svg>
273
+ <p>Select a commit</p>
274
+ <span>Click any commit on the left to see its diff</span>
275
+ </div>
276
+ </div>
277
+ <div id="commit-detail-diff" class="commit-detail-diff"></div>
278
+ </main>
279
+ </div>
280
+ </section>
281
+
192
282
  <!-- Rules View -->
193
283
  <section id="rules-view" class="tab-content">
194
284
  <header class="view-header">
@@ -390,6 +480,8 @@
390
480
  <script src="/js/diff.js"></script>
391
481
  <script src="/js/tasks.js"></script>
392
482
  <script src="/js/rules.js"></script>
483
+ <script src="/js/browser.js"></script>
484
+ <script src="/js/history.js"></script>
393
485
  <script src="/js/integrations.js"></script>
394
486
  </body>
395
487
  </html>
package/public/js/app.js CHANGED
@@ -1,3 +1,15 @@
1
+ // Load project info into sidebar + page title
2
+ fetch('/api/info').then(r => r.json()).then(info => {
3
+ if (info.projectName) {
4
+ document.title = `${info.projectName} — Devlens`;
5
+ const el = document.getElementById('brand-project');
6
+ if (el) {
7
+ el.textContent = info.projectName;
8
+ el.title = info.projectDir || info.projectName;
9
+ }
10
+ }
11
+ }).catch(() => {});
12
+
1
13
  // Tab switching — sidebar nav with URL routing
2
14
  const navItems = document.querySelectorAll('.nav-item');
3
15
  const tabContents = document.querySelectorAll('.tab-content');
@@ -28,7 +40,7 @@ window.addEventListener('popstate', () => {
28
40
  // Load initial tab from URL — redirect / to /diff
29
41
  (function() {
30
42
  const path = location.pathname.replace('/', '');
31
- const tab = ['diff', 'tasks', 'rules', 'integrations'].includes(path) ? path : 'diff';
43
+ const tab = ['diff', 'tasks', 'browser', 'history', 'rules', 'integrations'].includes(path) ? path : 'diff';
32
44
  if (!path || path === '') {
33
45
  history.replaceState(null, '', '/diff');
34
46
  }
@@ -0,0 +1,163 @@
1
+ // Files tab — VSCode-style file explorer
2
+
3
+ var explorerExpanded = new Set(JSON.parse(localStorage.getItem('devlens-explorer-expanded') || '[]'));
4
+ var explorerActiveFile = null;
5
+ var explorerCache = {}; // path → entries[]
6
+
7
+ function saveExplorerState() {
8
+ localStorage.setItem('devlens-explorer-expanded', JSON.stringify(Array.from(explorerExpanded)));
9
+ }
10
+
11
+ async function fetchDir(path) {
12
+ if (explorerCache[path]) return explorerCache[path];
13
+ try {
14
+ const res = await fetch(`/api/browser/files?path=${encodeURIComponent(path)}`);
15
+ const data = await res.json();
16
+ explorerCache[path] = data.entries || [];
17
+ return explorerCache[path];
18
+ } catch {
19
+ return [];
20
+ }
21
+ }
22
+
23
+ function fileIconSvg() {
24
+ return '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/></svg>';
25
+ }
26
+
27
+ function chevronSvg(expanded) {
28
+ return `<svg class="explorer-chevron ${expanded ? 'expanded' : ''}" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="9 18 15 12 9 6"/></svg>`;
29
+ }
30
+
31
+ function folderIconSvg() {
32
+ return '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/></svg>';
33
+ }
34
+
35
+ async function renderTree() {
36
+ const tree = document.getElementById('explorer-tree');
37
+ if (!tree) return;
38
+ const html = await renderNode('', 0);
39
+ tree.innerHTML = html || '<div class="empty-state-inline">Empty</div>';
40
+ attachTreeHandlers();
41
+ }
42
+
43
+ async function renderNode(path, depth) {
44
+ const entries = await fetchDir(path);
45
+ if (!entries.length) return '';
46
+
47
+ let html = '';
48
+ for (const entry of entries) {
49
+ const indent = depth * 12;
50
+ if (entry.type === 'dir') {
51
+ const expanded = explorerExpanded.has(entry.path);
52
+ html += `<div class="explorer-row folder-row" data-path="${escapeAttr(entry.path)}" data-type="dir" style="padding-left:${indent + 8}px">
53
+ ${chevronSvg(expanded)}
54
+ ${folderIconSvg()}
55
+ <span class="explorer-name">${escapeHtmlBrowser(entry.name)}</span>
56
+ </div>`;
57
+ if (expanded) {
58
+ html += await renderNode(entry.path, depth + 1);
59
+ }
60
+ } else {
61
+ const isActive = explorerActiveFile === entry.path;
62
+ html += `<div class="explorer-row file-row ${isActive ? 'active' : ''}" data-path="${escapeAttr(entry.path)}" data-type="file" style="padding-left:${indent + 24}px">
63
+ ${fileIconSvg()}
64
+ <span class="explorer-name">${escapeHtmlBrowser(entry.name)}</span>
65
+ </div>`;
66
+ }
67
+ }
68
+ return html;
69
+ }
70
+
71
+ function attachTreeHandlers() {
72
+ const tree = document.getElementById('explorer-tree');
73
+ if (!tree) return;
74
+ tree.querySelectorAll('.explorer-row').forEach(row => {
75
+ row.addEventListener('click', async (e) => {
76
+ e.stopPropagation();
77
+ const path = row.dataset.path;
78
+ const type = row.dataset.type;
79
+ if (type === 'dir') {
80
+ if (explorerExpanded.has(path)) explorerExpanded.delete(path);
81
+ else explorerExpanded.add(path);
82
+ saveExplorerState();
83
+ renderTree();
84
+ } else {
85
+ explorerActiveFile = path;
86
+ loadFileContent(path);
87
+ // Update active highlight without full re-render
88
+ tree.querySelectorAll('.file-row.active').forEach(r => r.classList.remove('active'));
89
+ row.classList.add('active');
90
+ }
91
+ });
92
+ });
93
+ }
94
+
95
+ async function loadFileContent(path) {
96
+ const viewer = document.getElementById('explorer-viewer');
97
+ const breadcrumb = document.getElementById('file-breadcrumb');
98
+ if (!viewer) return;
99
+
100
+ viewer.innerHTML = '<div class="empty-state"><span>Loading...</span></div>';
101
+ breadcrumb.textContent = path;
102
+
103
+ try {
104
+ const res = await fetch(`/api/browser/file?path=${encodeURIComponent(path)}`);
105
+ if (!res.ok) {
106
+ viewer.innerHTML = '<div class="empty-state"><p>Failed to load</p></div>';
107
+ return;
108
+ }
109
+ const data = await res.json();
110
+ const lines = data.content.split('\n');
111
+
112
+ const linesHtml = lines.map((line, i) => `
113
+ <div class="code-line">
114
+ <span class="code-line-num">${i + 1}</span>
115
+ <span class="code-line-text">${escapeHtmlBrowser(line) || ' '}</span>
116
+ </div>
117
+ `).join('');
118
+
119
+ viewer.innerHTML = `
120
+ <div class="code-viewer">
121
+ <div class="code-viewer-header">
122
+ <span class="code-file-path">${escapeHtmlBrowser(path)}</span>
123
+ <span class="code-file-meta">${formatSize(data.size)}${data.truncated ? ' · truncated' : ''}</span>
124
+ </div>
125
+ <div class="code-viewer-body">${linesHtml}</div>
126
+ </div>
127
+ `;
128
+ } catch {
129
+ viewer.innerHTML = '<div class="empty-state"><p>Failed to load file</p></div>';
130
+ }
131
+ }
132
+
133
+ function escapeHtmlBrowser(str) {
134
+ if (str == null) return '';
135
+ const div = document.createElement('div');
136
+ div.textContent = str;
137
+ return div.innerHTML;
138
+ }
139
+
140
+ function escapeAttr(str) {
141
+ return String(str).replace(/"/g, '&quot;');
142
+ }
143
+
144
+ function formatSize(bytes) {
145
+ if (bytes == null) return '';
146
+ if (bytes < 1024) return bytes + ' B';
147
+ if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB';
148
+ return (bytes / (1024 * 1024)).toFixed(1) + ' MB';
149
+ }
150
+
151
+ document.getElementById('browser-refresh-btn')?.addEventListener('click', () => {
152
+ explorerCache = {};
153
+ renderTree();
154
+ });
155
+
156
+ // Lazy load when tab opens
157
+ document.querySelector('[data-tab="browser"]')?.addEventListener('click', () => {
158
+ if (!Object.keys(explorerCache).length) renderTree();
159
+ });
160
+
161
+ if (location.pathname === '/browser') {
162
+ renderTree();
163
+ }
@@ -0,0 +1,219 @@
1
+ // History tab — git commits + diff viewer
2
+
3
+ var historyLoaded = false;
4
+ var historySelectedHash = null;
5
+ var historyViewMode = localStorage.getItem('devlens-history-view') || 'line-by-line';
6
+ var historyCachedData = null; // last loaded commit { hash, files, diff }
7
+
8
+ async function loadBranchInfo() {
9
+ try {
10
+ const res = await fetch('/api/browser/branch');
11
+ const data = await res.json();
12
+ document.getElementById('current-branch-name').textContent = data.branch || '—';
13
+ } catch {}
14
+ }
15
+
16
+ async function loadCommits() {
17
+ const list = document.getElementById('commits-list');
18
+ if (!list) return;
19
+ list.innerHTML = '<li class="empty-state-inline">Loading commits...</li>';
20
+ try {
21
+ const res = await fetch('/api/browser/commits?limit=100');
22
+ const commits = await res.json();
23
+ document.getElementById('commit-count').textContent = commits.length;
24
+
25
+ if (!commits.length) {
26
+ list.innerHTML = '<li class="empty-state-inline">No commits</li>';
27
+ return;
28
+ }
29
+
30
+ list.innerHTML = commits.map(c => `
31
+ <li class="commit-item" data-hash="${c.hash}">
32
+ <div class="commit-marker"></div>
33
+ <div class="commit-content">
34
+ <div class="commit-message">${escapeHtmlHistory(c.message)}</div>
35
+ <div class="commit-meta">
36
+ <span class="commit-hash">${c.hash}</span>
37
+ <span class="commit-author">${escapeHtmlHistory(c.author)}</span>
38
+ <span class="commit-date">${formatRelativeDate(c.date)}</span>
39
+ </div>
40
+ </div>
41
+ </li>
42
+ `).join('');
43
+
44
+ list.querySelectorAll('.commit-item').forEach(item => {
45
+ item.addEventListener('click', () => {
46
+ list.querySelectorAll('.commit-item').forEach(i => i.classList.remove('selected'));
47
+ item.classList.add('selected');
48
+ historySelectedHash = item.dataset.hash;
49
+ loadCommitDetails(item.dataset.hash);
50
+ });
51
+ });
52
+
53
+ historyLoaded = true;
54
+ } catch {
55
+ list.innerHTML = '<li class="empty-state-inline">Failed to load</li>';
56
+ }
57
+ }
58
+
59
+ // Split a unified diff into per-file chunks (reused pattern from diff.js)
60
+ function historySplitDiffByFile(rawDiff) {
61
+ if (!rawDiff || !rawDiff.trim()) return [];
62
+ const files = [];
63
+ const lines = rawDiff.split('\n');
64
+ let current = null;
65
+ for (const line of lines) {
66
+ if (line.startsWith('diff --git')) {
67
+ if (current) files.push(current);
68
+ const match = line.match(/diff --git a\/(.*) b\/(.*)/);
69
+ current = { name: match ? match[2] : 'unknown', lines: [line] };
70
+ } else if (current) {
71
+ current.lines.push(line);
72
+ }
73
+ }
74
+ if (current) files.push(current);
75
+ return files.map(f => ({ name: f.name, diff: f.lines.join('\n') }));
76
+ }
77
+
78
+ async function loadCommitDetails(hash) {
79
+ const headerEl = document.getElementById('commit-detail-header');
80
+ const diffEl = document.getElementById('commit-detail-diff');
81
+ if (!headerEl || !diffEl) return;
82
+
83
+ headerEl.innerHTML = '<div class="empty-state"><span>Loading...</span></div>';
84
+ diffEl.innerHTML = '';
85
+
86
+ try {
87
+ const res = await fetch(`/api/browser/commit/${hash}`);
88
+ const data = await res.json();
89
+ historyCachedData = data;
90
+ renderCommitDetails(hash, data);
91
+ } catch {
92
+ headerEl.innerHTML = '<div class="empty-state"><p>Failed to load commit</p></div>';
93
+ }
94
+ }
95
+
96
+ function renderCommitDetails(hash, data) {
97
+ const headerEl = document.getElementById('commit-detail-header');
98
+ const diffEl = document.getElementById('commit-detail-diff');
99
+ if (!headerEl || !diffEl) return;
100
+
101
+ const isUnified = historyViewMode === 'line-by-line';
102
+
103
+ headerEl.innerHTML = `
104
+ <div class="commit-header-content">
105
+ <div class="commit-header-hash">${hash}</div>
106
+ <div class="commit-header-files">${data.files.length} file(s) changed</div>
107
+ <div class="commit-header-actions">
108
+ <div class="btn-group">
109
+ <button class="btn btn-ghost btn-sm ${!isUnified ? 'active' : ''}" id="history-view-split" title="Side by side">
110
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="8" height="18" rx="1"/><rect x="13" y="3" width="8" height="18" rx="1"/></svg>
111
+ Split
112
+ </button>
113
+ <button class="btn btn-ghost btn-sm ${isUnified ? 'active' : ''}" id="history-view-unified" title="Unified">
114
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="18" height="18" rx="1"/><line x1="3" y1="12" x2="21" y2="12"/></svg>
115
+ Unified
116
+ </button>
117
+ </div>
118
+ <button class="btn btn-ghost btn-sm" id="commit-expand-all" title="Expand all files">
119
+ <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 12 15 18 9"/></svg>
120
+ Expand all
121
+ </button>
122
+ <button class="btn btn-ghost btn-sm" id="commit-collapse-all" title="Collapse all files">
123
+ <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="18 15 12 9 6 15"/></svg>
124
+ Collapse all
125
+ </button>
126
+ </div>
127
+ </div>
128
+ `;
129
+
130
+ if (!data.diff) {
131
+ diffEl.innerHTML = `<pre class="raw-diff">(no diff)</pre>`;
132
+ return;
133
+ }
134
+
135
+ const files = historySplitDiffByFile(data.diff);
136
+ diffEl.innerHTML = files.map((file) => {
137
+ const html = Diff2Html.html(file.diff, {
138
+ drawFileList: false,
139
+ matching: 'lines',
140
+ outputFormat: historyViewMode,
141
+ colorScheme: 'dark',
142
+ });
143
+ return `
144
+ <div class="commit-file-section" data-file="${escapeAttrHistory(file.name)}">
145
+ <div class="commit-file-header">
146
+ <svg class="commit-file-chevron" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="9 18 15 12 9 6"/></svg>
147
+ <span class="commit-file-name">${escapeHtmlHistory(file.name)}</span>
148
+ </div>
149
+ <div class="commit-file-body">${html}</div>
150
+ </div>
151
+ `;
152
+ }).join('');
153
+
154
+ // Wire interactions
155
+ diffEl.querySelectorAll('.commit-file-header').forEach(h => {
156
+ h.addEventListener('click', () => {
157
+ h.closest('.commit-file-section').classList.toggle('expanded');
158
+ });
159
+ });
160
+
161
+ document.getElementById('commit-expand-all')?.addEventListener('click', () => {
162
+ diffEl.querySelectorAll('.commit-file-section').forEach(s => s.classList.add('expanded'));
163
+ });
164
+ document.getElementById('commit-collapse-all')?.addEventListener('click', () => {
165
+ diffEl.querySelectorAll('.commit-file-section').forEach(s => s.classList.remove('expanded'));
166
+ });
167
+ document.getElementById('history-view-split')?.addEventListener('click', () => {
168
+ if (historyViewMode === 'side-by-side') return;
169
+ historyViewMode = 'side-by-side';
170
+ localStorage.setItem('devlens-history-view', historyViewMode);
171
+ if (historyCachedData) renderCommitDetails(hash, historyCachedData);
172
+ });
173
+ document.getElementById('history-view-unified')?.addEventListener('click', () => {
174
+ if (historyViewMode === 'line-by-line') return;
175
+ historyViewMode = 'line-by-line';
176
+ localStorage.setItem('devlens-history-view', historyViewMode);
177
+ if (historyCachedData) renderCommitDetails(hash, historyCachedData);
178
+ });
179
+ }
180
+
181
+ function escapeAttrHistory(str) {
182
+ return String(str).replace(/"/g, '&quot;');
183
+ }
184
+
185
+ function escapeHtmlHistory(str) {
186
+ if (str == null) return '';
187
+ const div = document.createElement('div');
188
+ div.textContent = str;
189
+ return div.innerHTML;
190
+ }
191
+
192
+ function formatRelativeDate(iso) {
193
+ const now = Date.now();
194
+ const then = new Date(iso).getTime();
195
+ const diff = (now - then) / 1000;
196
+ if (diff < 60) return 'just now';
197
+ if (diff < 3600) return `${Math.floor(diff / 60)}m ago`;
198
+ if (diff < 86400) return `${Math.floor(diff / 3600)}h ago`;
199
+ if (diff < 86400 * 7) return `${Math.floor(diff / 86400)}d ago`;
200
+ return new Date(iso).toLocaleDateString();
201
+ }
202
+
203
+ document.getElementById('history-refresh-btn')?.addEventListener('click', () => {
204
+ loadBranchInfo();
205
+ loadCommits();
206
+ });
207
+
208
+ // Lazy load when tab opens
209
+ document.querySelector('[data-tab="history"]')?.addEventListener('click', () => {
210
+ if (!historyLoaded) {
211
+ loadBranchInfo();
212
+ loadCommits();
213
+ }
214
+ });
215
+
216
+ if (location.pathname === '/history') {
217
+ loadBranchInfo();
218
+ loadCommits();
219
+ }