pinokiod 3.169.0 → 3.180.0
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/package.json +1 -1
- package/server/index.js +123 -3
- package/server/public/files-app/app.css +73 -1
- package/server/public/files-app/app.js +255 -2
- package/server/public/layout.js +1 -1
- package/server/public/style.css +5 -1
- package/server/routes/files.js +96 -0
- package/server/views/app.ejs +402 -22
- package/server/views/file_browser.ejs +9 -2
- package/server/views/index.ejs +2 -2
- package/server/views/layout.ejs +5 -5
- package/server/views/review.ejs +4 -3
package/package.json
CHANGED
package/server/index.js
CHANGED
|
@@ -230,8 +230,40 @@ class Server {
|
|
|
230
230
|
}
|
|
231
231
|
running_dynamic.push(obj)
|
|
232
232
|
} else {
|
|
233
|
+
const normalizedFilepath = path.normalize(filepath)
|
|
234
|
+
const hasMenuCwd = typeof cwd === 'string'
|
|
235
|
+
const normalizedMenuCwd = hasMenuCwd ? (cwd.length === 0 ? '' : path.normalize(cwd)) : null
|
|
236
|
+
const matchesRunningEntry = (runningKey) => {
|
|
237
|
+
if (typeof runningKey !== 'string' || runningKey.length === 0) {
|
|
238
|
+
return false
|
|
239
|
+
}
|
|
240
|
+
const questionIndex = runningKey.indexOf('?')
|
|
241
|
+
const runningPath = questionIndex >= 0 ? runningKey.slice(0, questionIndex) : runningKey
|
|
242
|
+
if (path.normalize(runningPath) !== normalizedFilepath) {
|
|
243
|
+
return false
|
|
244
|
+
}
|
|
245
|
+
if (!hasMenuCwd) {
|
|
246
|
+
return questionIndex === -1
|
|
247
|
+
}
|
|
248
|
+
if (questionIndex === -1) {
|
|
249
|
+
return normalizedMenuCwd === ''
|
|
250
|
+
}
|
|
251
|
+
const params = querystring.parse(runningKey.slice(questionIndex + 1))
|
|
252
|
+
const rawCwd = typeof params.cwd === 'string' ? params.cwd : null
|
|
253
|
+
if (normalizedMenuCwd === '') {
|
|
254
|
+
return rawCwd !== null && rawCwd.length === 0
|
|
255
|
+
}
|
|
256
|
+
if (!rawCwd || rawCwd.length === 0) {
|
|
257
|
+
return false
|
|
258
|
+
}
|
|
259
|
+
try {
|
|
260
|
+
return path.normalize(rawCwd) === normalizedMenuCwd
|
|
261
|
+
} catch (_) {
|
|
262
|
+
return false
|
|
263
|
+
}
|
|
264
|
+
}
|
|
233
265
|
for(let running_id in this.kernel.api.running) {
|
|
234
|
-
if (running_id
|
|
266
|
+
if (matchesRunningEntry(running_id)) {
|
|
235
267
|
let obj2 = structuredClone(obj)
|
|
236
268
|
obj2.running = true
|
|
237
269
|
obj2.display = "indent"
|
|
@@ -526,9 +558,13 @@ class Server {
|
|
|
526
558
|
}
|
|
527
559
|
|
|
528
560
|
let currentBranch = null
|
|
561
|
+
let isDetached = false
|
|
529
562
|
try {
|
|
530
563
|
currentBranch = await git.currentBranch({ fs, dir, fullname: false })
|
|
531
564
|
} catch (_) {}
|
|
565
|
+
if (!currentBranch) {
|
|
566
|
+
isDetached = true
|
|
567
|
+
}
|
|
532
568
|
|
|
533
569
|
let branches = []
|
|
534
570
|
if (branchList.length > 0) {
|
|
@@ -595,6 +631,7 @@ class Server {
|
|
|
595
631
|
branch: currentBranch,
|
|
596
632
|
branches,
|
|
597
633
|
dir,
|
|
634
|
+
detached: isDetached,
|
|
598
635
|
logError: logError ? String(logError.message || logError) : null
|
|
599
636
|
}
|
|
600
637
|
}
|
|
@@ -812,6 +849,7 @@ class Server {
|
|
|
812
849
|
let run_tab = "/p/" + name
|
|
813
850
|
let dev_tab = "/p/" + name + "/dev"
|
|
814
851
|
let review_tab = "/p/" + name + "/review"
|
|
852
|
+
let files_tab = "/p/" + name + "/files"
|
|
815
853
|
|
|
816
854
|
let editor_tab = `/pinokio/fileview/${encodeURIComponent(name)}`
|
|
817
855
|
let savedTabs = []
|
|
@@ -867,6 +905,7 @@ class Server {
|
|
|
867
905
|
run_tab,
|
|
868
906
|
dev_tab,
|
|
869
907
|
review_tab,
|
|
908
|
+
files_tab,
|
|
870
909
|
// paths,
|
|
871
910
|
theme: this.theme,
|
|
872
911
|
agent: req.agent,
|
|
@@ -6246,8 +6285,84 @@ class Server {
|
|
|
6246
6285
|
res.json(response)
|
|
6247
6286
|
}))
|
|
6248
6287
|
this.app.get("/info/git/:ref/*", ex(async (req, res) => {
|
|
6249
|
-
|
|
6250
|
-
|
|
6288
|
+
const repoParam = req.params[0]
|
|
6289
|
+
const ref = req.params.ref || 'HEAD'
|
|
6290
|
+
const summary = await this.getGit(ref, repoParam)
|
|
6291
|
+
|
|
6292
|
+
const repoDir = summary && summary.dir ? summary.dir : this.kernel.path('api', repoParam)
|
|
6293
|
+
|
|
6294
|
+
if (ref === 'HEAD') {
|
|
6295
|
+
try {
|
|
6296
|
+
const { changes: headChanges, git_commit_url } = await this.getRepoHeadStatus(repoParam)
|
|
6297
|
+
summary.changes = headChanges || []
|
|
6298
|
+
summary.git_commit_url = git_commit_url || null
|
|
6299
|
+
} catch (error) {
|
|
6300
|
+
console.error('[git-info] head status error', repoParam, error)
|
|
6301
|
+
summary.changes = []
|
|
6302
|
+
}
|
|
6303
|
+
} else {
|
|
6304
|
+
let changes = []
|
|
6305
|
+
try {
|
|
6306
|
+
const commitOid = await this.kernel.git.resolveCommitOid(repoDir, ref)
|
|
6307
|
+
const parentOid = await this.kernel.git.getParentCommit(repoDir, commitOid)
|
|
6308
|
+
let entries
|
|
6309
|
+
if (parentOid !== commitOid) {
|
|
6310
|
+
entries = await git.walk({
|
|
6311
|
+
fs,
|
|
6312
|
+
dir: repoDir,
|
|
6313
|
+
trees: [git.TREE({ ref: parentOid }), git.TREE({ ref: commitOid })],
|
|
6314
|
+
map: async (filepath, [A, B]) => {
|
|
6315
|
+
if (filepath === '.') return
|
|
6316
|
+
if (!A && B) return { filepath, type: 'added' }
|
|
6317
|
+
if (A && !B) return { filepath, type: 'deleted' }
|
|
6318
|
+
if (A && B) {
|
|
6319
|
+
const Aoid = await A.oid()
|
|
6320
|
+
const Boid = await B.oid()
|
|
6321
|
+
if (Aoid !== Boid) return { filepath, type: 'modified' }
|
|
6322
|
+
}
|
|
6323
|
+
},
|
|
6324
|
+
})
|
|
6325
|
+
} else {
|
|
6326
|
+
entries = await git.walk({
|
|
6327
|
+
fs,
|
|
6328
|
+
dir: repoDir,
|
|
6329
|
+
trees: [git.TREE({ ref: commitOid })],
|
|
6330
|
+
map: async (filepath, [B]) => {
|
|
6331
|
+
if (filepath === '.') return
|
|
6332
|
+
return { filepath, type: 'added' }
|
|
6333
|
+
},
|
|
6334
|
+
})
|
|
6335
|
+
}
|
|
6336
|
+
const diffFiles = (entries || []).filter(Boolean)
|
|
6337
|
+
for (const { filepath, type } of diffFiles) {
|
|
6338
|
+
const fullPath = path.join(repoDir, filepath)
|
|
6339
|
+
const stats = await fs.promises.stat(fullPath).catch(() => null)
|
|
6340
|
+
if (!stats || stats.isDirectory()) {
|
|
6341
|
+
continue
|
|
6342
|
+
}
|
|
6343
|
+
const relpath = path.relative(this.kernel.path('api'), fullPath)
|
|
6344
|
+
changes.push({
|
|
6345
|
+
ref,
|
|
6346
|
+
webpath: "/asset/" + path.relative(this.kernel.homedir, fullPath),
|
|
6347
|
+
file: filepath,
|
|
6348
|
+
path: fullPath,
|
|
6349
|
+
diffpath: `/gitdiff/${ref}/${repoParam}/${filepath}`,
|
|
6350
|
+
status: type,
|
|
6351
|
+
relpath,
|
|
6352
|
+
})
|
|
6353
|
+
}
|
|
6354
|
+
} catch (error) {
|
|
6355
|
+
console.error('[git-info] diff error', repoParam, ref, error)
|
|
6356
|
+
}
|
|
6357
|
+
summary.changes = changes
|
|
6358
|
+
}
|
|
6359
|
+
|
|
6360
|
+
if (!summary.git_commit_url) {
|
|
6361
|
+
summary.git_commit_url = `/run/scripts/git/commit.json?cwd=${repoDir}&callback_target=parent&callback=$location.href`
|
|
6362
|
+
}
|
|
6363
|
+
summary.dir = repoDir
|
|
6364
|
+
|
|
6365
|
+
res.json(summary)
|
|
6251
6366
|
}))
|
|
6252
6367
|
this.app.get("/info/gitstatus/:name", ex(async (req, res) => {
|
|
6253
6368
|
try {
|
|
@@ -7052,10 +7167,12 @@ class Server {
|
|
|
7052
7167
|
let run_tab = "/p/" + name
|
|
7053
7168
|
let dev_tab = "/p/" + name + "/dev"
|
|
7054
7169
|
let review_tab = "/p/" + name + "/review"
|
|
7170
|
+
let files_tab = "/p/" + name + "/files"
|
|
7055
7171
|
res.render("review", {
|
|
7056
7172
|
run_tab,
|
|
7057
7173
|
dev_tab,
|
|
7058
7174
|
review_tab,
|
|
7175
|
+
files_tab,
|
|
7059
7176
|
name: req.params.name,
|
|
7060
7177
|
type: "review",
|
|
7061
7178
|
title: name,
|
|
@@ -7070,6 +7187,9 @@ class Server {
|
|
|
7070
7187
|
this.app.get("/p/:name/dev", ex(async (req, res) => {
|
|
7071
7188
|
await this.chrome(req, res, "browse")
|
|
7072
7189
|
}))
|
|
7190
|
+
this.app.get("/p/:name/files", ex(async (req, res) => {
|
|
7191
|
+
await this.chrome(req, res, "files")
|
|
7192
|
+
}))
|
|
7073
7193
|
this.app.get("/p/:name/browse", ex(async (req, res) => {
|
|
7074
7194
|
await this.chrome(req, res, "browse")
|
|
7075
7195
|
}))
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
display: flex;
|
|
36
36
|
align-items: center;
|
|
37
37
|
justify-content: space-between;
|
|
38
|
-
padding:
|
|
38
|
+
padding: 8px 18px;
|
|
39
39
|
background: var(--surface-color);
|
|
40
40
|
border-bottom: 1px solid var(--border-color);
|
|
41
41
|
gap: 12px;
|
|
@@ -46,16 +46,28 @@
|
|
|
46
46
|
align-items: center;
|
|
47
47
|
gap: 12px;
|
|
48
48
|
font-size: 15px;
|
|
49
|
+
min-width: 0; /* allow ellipsis children */
|
|
49
50
|
}
|
|
50
51
|
|
|
51
52
|
.files-app__header-title i {
|
|
52
53
|
font-size: 18px;
|
|
53
54
|
}
|
|
54
55
|
|
|
56
|
+
.files-app__header-icon {
|
|
57
|
+
width: 28px;
|
|
58
|
+
height: 28px;
|
|
59
|
+
border-radius: 6px;
|
|
60
|
+
object-fit: cover;
|
|
61
|
+
display: block;
|
|
62
|
+
background: #fff;
|
|
63
|
+
box-shadow: 0 1px 2px rgba(0,0,0,0.08);
|
|
64
|
+
}
|
|
65
|
+
|
|
55
66
|
.files-app__header-text {
|
|
56
67
|
display: flex;
|
|
57
68
|
flex-direction: column;
|
|
58
69
|
gap: 2px;
|
|
70
|
+
min-width: 0; /* allow ellipsis children */
|
|
59
71
|
}
|
|
60
72
|
|
|
61
73
|
.files-app__header-primary {
|
|
@@ -68,6 +80,15 @@
|
|
|
68
80
|
word-break: break-all;
|
|
69
81
|
}
|
|
70
82
|
|
|
83
|
+
.files-app__header-desc {
|
|
84
|
+
font-size: 12px;
|
|
85
|
+
color: var(--muted-color);
|
|
86
|
+
display: block;
|
|
87
|
+
white-space: nowrap;
|
|
88
|
+
overflow: hidden;
|
|
89
|
+
text-overflow: ellipsis;
|
|
90
|
+
}
|
|
91
|
+
|
|
71
92
|
.files-app__header-actions {
|
|
72
93
|
display: flex;
|
|
73
94
|
gap: 8px;
|
|
@@ -262,12 +283,34 @@ body.dark .files-app__tab--stale .files-app__tab-label::after {
|
|
|
262
283
|
transition: background 0.1s ease;
|
|
263
284
|
}
|
|
264
285
|
|
|
286
|
+
.files-app__tree-label {
|
|
287
|
+
flex: 1;
|
|
288
|
+
min-width: 0;
|
|
289
|
+
overflow: hidden;
|
|
290
|
+
text-overflow: ellipsis;
|
|
291
|
+
white-space: nowrap;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
.files-app__tree-actions {
|
|
295
|
+
display: inline-flex;
|
|
296
|
+
align-items: center;
|
|
297
|
+
gap: 4px;
|
|
298
|
+
opacity: 0;
|
|
299
|
+
transition: opacity 0.15s ease;
|
|
300
|
+
}
|
|
301
|
+
|
|
265
302
|
.files-app__tree-row:hover,
|
|
266
303
|
.files-app__tree-row:focus-visible {
|
|
267
304
|
background: rgba(127, 91, 243, 0.12);
|
|
268
305
|
outline: none;
|
|
269
306
|
}
|
|
270
307
|
|
|
308
|
+
.files-app__tree-row:hover .files-app__tree-actions,
|
|
309
|
+
.files-app__tree-row:focus-visible .files-app__tree-actions,
|
|
310
|
+
.files-app__tree-row[data-selected='true'] .files-app__tree-actions {
|
|
311
|
+
opacity: 1;
|
|
312
|
+
}
|
|
313
|
+
|
|
271
314
|
.files-app__tree-row[data-selected='true'] {
|
|
272
315
|
background: rgba(127, 91, 243, 0.2);
|
|
273
316
|
}
|
|
@@ -277,6 +320,35 @@ body.dark .files-app__tab--stale .files-app__tab-label::after {
|
|
|
277
320
|
text-align: center;
|
|
278
321
|
}
|
|
279
322
|
|
|
323
|
+
.files-app__tree-action {
|
|
324
|
+
display: inline-flex;
|
|
325
|
+
align-items: center;
|
|
326
|
+
justify-content: center;
|
|
327
|
+
width: 22px;
|
|
328
|
+
height: 22px;
|
|
329
|
+
border-radius: 4px;
|
|
330
|
+
color: var(--muted-color);
|
|
331
|
+
cursor: pointer;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
.files-app__tree-action--rename {
|
|
335
|
+
font-size: 13px;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
.files-app__tree-action:hover {
|
|
339
|
+
background: rgba(127, 91, 243, 0.15);
|
|
340
|
+
color: var(--text-color);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
.files-app__tree-action:focus-visible {
|
|
344
|
+
outline: 2px solid var(--accent-color);
|
|
345
|
+
outline-offset: 2px;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
.files-app__tree-action i {
|
|
349
|
+
pointer-events: none;
|
|
350
|
+
}
|
|
351
|
+
|
|
280
352
|
.files-app__tree-children {
|
|
281
353
|
list-style: none;
|
|
282
354
|
margin: 0;
|
|
@@ -263,6 +263,28 @@
|
|
|
263
263
|
return parseJsonResponse(response);
|
|
264
264
|
};
|
|
265
265
|
|
|
266
|
+
const remove = async (pathPosix) => {
|
|
267
|
+
const response = await fetch('/api/files/delete', {
|
|
268
|
+
method: 'POST',
|
|
269
|
+
headers: {
|
|
270
|
+
'Content-Type': 'application/json',
|
|
271
|
+
},
|
|
272
|
+
body: JSON.stringify({ workspace, path: pathPosix, root: workspaceRoot }),
|
|
273
|
+
});
|
|
274
|
+
return parseJsonResponse(response);
|
|
275
|
+
};
|
|
276
|
+
|
|
277
|
+
const rename = async (pathPosix, newName) => {
|
|
278
|
+
const response = await fetch('/api/files/rename', {
|
|
279
|
+
method: 'POST',
|
|
280
|
+
headers: {
|
|
281
|
+
'Content-Type': 'application/json',
|
|
282
|
+
},
|
|
283
|
+
body: JSON.stringify({ workspace, path: pathPosix, name: newName, root: workspaceRoot }),
|
|
284
|
+
});
|
|
285
|
+
return parseJsonResponse(response);
|
|
286
|
+
};
|
|
287
|
+
|
|
266
288
|
const stat = async (pathPosix) => {
|
|
267
289
|
const params = new URLSearchParams({ workspace });
|
|
268
290
|
if (workspaceRoot) {
|
|
@@ -276,7 +298,7 @@
|
|
|
276
298
|
return parseJsonResponse(response);
|
|
277
299
|
};
|
|
278
300
|
|
|
279
|
-
return { list, read, save, stat };
|
|
301
|
+
return { list, read, save, remove, rename, stat };
|
|
280
302
|
}
|
|
281
303
|
|
|
282
304
|
async function parseJsonResponse(response) {
|
|
@@ -340,10 +362,52 @@
|
|
|
340
362
|
icon.className = entry.type === 'directory' ? 'fa-regular fa-folder' : 'fa-regular fa-file-lines';
|
|
341
363
|
row.appendChild(icon);
|
|
342
364
|
|
|
343
|
-
const label = createElement('span');
|
|
365
|
+
const label = createElement('span', 'files-app__tree-label');
|
|
344
366
|
label.textContent = entry.name;
|
|
345
367
|
row.appendChild(label);
|
|
346
368
|
|
|
369
|
+
if (!entry.isRoot) {
|
|
370
|
+
const actions = createElement('span', 'files-app__tree-actions');
|
|
371
|
+
const renameBtn = createElement('span', 'files-app__tree-action files-app__tree-action--rename');
|
|
372
|
+
renameBtn.setAttribute('role', 'button');
|
|
373
|
+
renameBtn.setAttribute('aria-label', entry.type === 'directory' ? 'Rename folder' : 'Rename file');
|
|
374
|
+
renameBtn.tabIndex = 0;
|
|
375
|
+
renameBtn.title = entry.type === 'directory' ? 'Rename folder' : 'Rename file';
|
|
376
|
+
renameBtn.innerHTML = '<i class="fa-regular fa-pen-to-square"></i>';
|
|
377
|
+
const triggerRename = (event) => {
|
|
378
|
+
event.preventDefault();
|
|
379
|
+
event.stopPropagation();
|
|
380
|
+
requestRenameEntry.call(this, entry);
|
|
381
|
+
};
|
|
382
|
+
renameBtn.addEventListener('click', triggerRename);
|
|
383
|
+
renameBtn.addEventListener('keydown', (event) => {
|
|
384
|
+
if (event.key === 'Enter' || event.key === ' ') {
|
|
385
|
+
triggerRename(event);
|
|
386
|
+
}
|
|
387
|
+
});
|
|
388
|
+
actions.appendChild(renameBtn);
|
|
389
|
+
|
|
390
|
+
const deleteBtn = createElement('span', 'files-app__tree-action files-app__tree-action--delete');
|
|
391
|
+
deleteBtn.setAttribute('role', 'button');
|
|
392
|
+
deleteBtn.setAttribute('aria-label', entry.type === 'directory' ? 'Delete folder' : 'Delete file');
|
|
393
|
+
deleteBtn.tabIndex = 0;
|
|
394
|
+
deleteBtn.title = entry.type === 'directory' ? 'Delete folder' : 'Delete file';
|
|
395
|
+
deleteBtn.innerHTML = '<i class="fa-regular fa-trash-can"></i>';
|
|
396
|
+
const triggerDelete = (event) => {
|
|
397
|
+
event.preventDefault();
|
|
398
|
+
event.stopPropagation();
|
|
399
|
+
requestDeleteEntry.call(this, entry);
|
|
400
|
+
};
|
|
401
|
+
deleteBtn.addEventListener('click', triggerDelete);
|
|
402
|
+
deleteBtn.addEventListener('keydown', (event) => {
|
|
403
|
+
if (event.key === 'Enter' || event.key === ' ') {
|
|
404
|
+
triggerDelete(event);
|
|
405
|
+
}
|
|
406
|
+
});
|
|
407
|
+
actions.appendChild(deleteBtn);
|
|
408
|
+
row.appendChild(actions);
|
|
409
|
+
}
|
|
410
|
+
|
|
347
411
|
if (entry.type === 'directory') {
|
|
348
412
|
row.addEventListener('click', (event) => {
|
|
349
413
|
event.preventDefault();
|
|
@@ -418,6 +482,195 @@
|
|
|
418
482
|
container.style.display = parentItem.dataset.expanded === 'true' ? 'block' : 'none';
|
|
419
483
|
}
|
|
420
484
|
|
|
485
|
+
function requestDeleteEntry(entry) {
|
|
486
|
+
if (!entry || !entry.path) {
|
|
487
|
+
return;
|
|
488
|
+
}
|
|
489
|
+
const path = entry.path;
|
|
490
|
+
const displayName = entry.name || path.split('/').pop() || this.state.workspaceLabel;
|
|
491
|
+
const impacted = [];
|
|
492
|
+
for (const [sessionPath, info] of this.state.sessions.entries()) {
|
|
493
|
+
if (sessionPath === path || sessionPath.startsWith(`${path}/`)) {
|
|
494
|
+
impacted.push({ path: sessionPath, info });
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
let message = entry.type === 'directory'
|
|
498
|
+
? `Delete folder "${displayName}" and all of its contents?`
|
|
499
|
+
: `Delete file "${displayName}"?`;
|
|
500
|
+
if (impacted.some(({ info }) => info && info.session && !isSessionClean(info.session))) {
|
|
501
|
+
message += '\n\nUnsaved changes in open editors will be lost.';
|
|
502
|
+
}
|
|
503
|
+
const confirmed = window.confirm(message);
|
|
504
|
+
if (!confirmed) {
|
|
505
|
+
return;
|
|
506
|
+
}
|
|
507
|
+
deleteEntry.call(this, entry, impacted.map(({ path }) => path));
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
function requestRenameEntry(entry) {
|
|
511
|
+
if (!entry || !entry.path) {
|
|
512
|
+
return;
|
|
513
|
+
}
|
|
514
|
+
const path = entry.path;
|
|
515
|
+
const displayName = entry.name || path.split('/').pop() || this.state.workspaceLabel;
|
|
516
|
+
let nextName = window.prompt('Rename to:', displayName);
|
|
517
|
+
if (nextName === null) {
|
|
518
|
+
return;
|
|
519
|
+
}
|
|
520
|
+
nextName = nextName.trim();
|
|
521
|
+
if (!nextName) {
|
|
522
|
+
setStatus.call(this, 'Name cannot be empty', 'error');
|
|
523
|
+
return;
|
|
524
|
+
}
|
|
525
|
+
if (nextName.includes('/') || nextName.includes('\\')) {
|
|
526
|
+
setStatus.call(this, 'Name cannot contain path separators', 'error');
|
|
527
|
+
return;
|
|
528
|
+
}
|
|
529
|
+
if (nextName === displayName) {
|
|
530
|
+
return;
|
|
531
|
+
}
|
|
532
|
+
renameEntry.call(this, entry, nextName);
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
async function renameEntry(entry, newName) {
|
|
536
|
+
if (!entry || !entry.path) {
|
|
537
|
+
return;
|
|
538
|
+
}
|
|
539
|
+
const path = entry.path;
|
|
540
|
+
const displayName = entry.name || path.split('/').pop() || this.state.workspaceLabel;
|
|
541
|
+
if (!this.api || typeof this.api.rename !== 'function') {
|
|
542
|
+
setStatus.call(this, 'Rename is not available', 'error');
|
|
543
|
+
return;
|
|
544
|
+
}
|
|
545
|
+
setStatus.call(this, `Renaming ${displayName}…`);
|
|
546
|
+
try {
|
|
547
|
+
const result = await this.api.rename(path, newName);
|
|
548
|
+
const targetPath = result && typeof result.target === 'string' ? result.target : path;
|
|
549
|
+
|
|
550
|
+
const oldPrefix = `${path}/`;
|
|
551
|
+
|
|
552
|
+
const updates = [];
|
|
553
|
+
for (const [sessionPath, entryRef] of this.state.sessions.entries()) {
|
|
554
|
+
if (sessionPath === path || sessionPath.startsWith(oldPrefix)) {
|
|
555
|
+
const suffix = sessionPath.slice(path.length);
|
|
556
|
+
const newSessionPath = `${targetPath}${suffix}`;
|
|
557
|
+
updates.push({ oldPath: sessionPath, newPath: newSessionPath, ref: entryRef, suffix });
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
if (updates.length > 0) {
|
|
562
|
+
for (const { oldPath, newPath, ref, suffix } of updates) {
|
|
563
|
+
this.state.sessions.delete(oldPath);
|
|
564
|
+
this.state.sessions.set(newPath, ref);
|
|
565
|
+
if (this.state.activePath === oldPath) {
|
|
566
|
+
this.state.activePath = newPath;
|
|
567
|
+
}
|
|
568
|
+
const tab = ref.tabEl;
|
|
569
|
+
if (tab) {
|
|
570
|
+
tab.dataset.path = newPath;
|
|
571
|
+
const label = tab.querySelector('.files-app__tab-label');
|
|
572
|
+
if (label && suffix.length === 0) {
|
|
573
|
+
label.textContent = newName;
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
if (suffix.length === 0) {
|
|
577
|
+
ref.name = newName;
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
this.state.openOrder = this.state.openOrder.map((existing) => {
|
|
581
|
+
if (existing === path) {
|
|
582
|
+
return targetPath;
|
|
583
|
+
}
|
|
584
|
+
if (existing.startsWith(oldPrefix)) {
|
|
585
|
+
return `${targetPath}${existing.slice(path.length)}`;
|
|
586
|
+
}
|
|
587
|
+
return existing;
|
|
588
|
+
});
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
if (this.state.selectedTreePath) {
|
|
592
|
+
if (this.state.selectedTreePath === path) {
|
|
593
|
+
this.state.selectedTreePath = targetPath;
|
|
594
|
+
} else if (this.state.selectedTreePath.startsWith(oldPrefix)) {
|
|
595
|
+
this.state.selectedTreePath = `${targetPath}${this.state.selectedTreePath.slice(path.length)}`;
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
pruneTreeCache.call(this, path);
|
|
600
|
+
|
|
601
|
+
const parentPath = path.split('/').slice(0, -1).join('/');
|
|
602
|
+
const parentItem = this.state.treeElements.get(parentPath) || this.state.treeElements.get('');
|
|
603
|
+
if (parentItem) {
|
|
604
|
+
parentItem.dataset.loaded = 'false';
|
|
605
|
+
await loadDirectory.call(this, parentPath, parentItem);
|
|
606
|
+
if (this.state.treeElements.has(targetPath)) {
|
|
607
|
+
setTreeSelection.call(this, targetPath);
|
|
608
|
+
} else {
|
|
609
|
+
setTreeSelection.call(this, parentPath);
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
setStatus.call(this, `${displayName} renamed`, 'success');
|
|
614
|
+
} catch (error) {
|
|
615
|
+
console.error(error);
|
|
616
|
+
setStatus.call(this, error.message || 'Failed to rename', 'error');
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
async function deleteEntry(entry, impactedPaths) {
|
|
621
|
+
if (!entry || !entry.path) {
|
|
622
|
+
return;
|
|
623
|
+
}
|
|
624
|
+
const path = entry.path;
|
|
625
|
+
const displayName = entry.name || path.split('/').pop() || this.state.workspaceLabel;
|
|
626
|
+
if (!this.api || typeof this.api.remove !== 'function') {
|
|
627
|
+
setStatus.call(this, 'Delete is not available', 'error');
|
|
628
|
+
return;
|
|
629
|
+
}
|
|
630
|
+
setStatus.call(this, `Deleting ${displayName}…`);
|
|
631
|
+
try {
|
|
632
|
+
await this.api.remove(path);
|
|
633
|
+
|
|
634
|
+
if (Array.isArray(impactedPaths)) {
|
|
635
|
+
for (const sessionPath of impactedPaths) {
|
|
636
|
+
this.closeFile(sessionPath, { force: true });
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
pruneTreeCache.call(this, path);
|
|
641
|
+
|
|
642
|
+
const parentPath = path.split('/').slice(0, -1).join('/');
|
|
643
|
+
const parentItem = this.state.treeElements.get(parentPath) || this.state.treeElements.get('');
|
|
644
|
+
if (parentItem) {
|
|
645
|
+
parentItem.dataset.loaded = 'false';
|
|
646
|
+
await loadDirectory.call(this, parentPath, parentItem);
|
|
647
|
+
setTreeSelection.call(this, parentPath);
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
if (this.state.selectedTreePath && (this.state.selectedTreePath === path || this.state.selectedTreePath.startsWith(`${path}/`))) {
|
|
651
|
+
setTreeSelection.call(this, null);
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
setStatus.call(this, `${displayName} deleted`, 'success');
|
|
655
|
+
} catch (error) {
|
|
656
|
+
console.error(error);
|
|
657
|
+
setStatus.call(this, error.message || 'Failed to delete', 'error');
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
function pruneTreeCache(path) {
|
|
662
|
+
if (typeof path !== 'string') {
|
|
663
|
+
return;
|
|
664
|
+
}
|
|
665
|
+
const prefix = path ? `${path}/` : '';
|
|
666
|
+
for (const key of Array.from(this.state.treeElements.keys())) {
|
|
667
|
+
if (!key) continue;
|
|
668
|
+
if (key === path || (prefix && key.startsWith(prefix))) {
|
|
669
|
+
this.state.treeElements.delete(key);
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
|
|
421
674
|
async function toggleDirectory(treeItem, relativePath) {
|
|
422
675
|
if (!treeItem || treeItem.dataset.loading === 'true') {
|
|
423
676
|
return;
|
package/server/public/layout.js
CHANGED
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
const HOST_ORIGIN = window.location.origin;
|
|
24
24
|
const STORAGE_PREFIX = 'pinokio:layout:';
|
|
25
25
|
const MIN_PANEL_SIZE = 120;
|
|
26
|
-
const GUTTER_SIZE =
|
|
26
|
+
const GUTTER_SIZE = 6;
|
|
27
27
|
|
|
28
28
|
const state = {
|
|
29
29
|
sessionId: typeof parsedConfig.sessionId === 'string' && parsedConfig.sessionId.trim() ? parsedConfig.sessionId.trim() : null,
|
package/server/public/style.css
CHANGED
|
@@ -821,6 +821,10 @@ body {
|
|
|
821
821
|
color: var(--light-color);
|
|
822
822
|
position: relative;
|
|
823
823
|
}
|
|
824
|
+
/* Reserve scrollbar space to prevent header layout shift */
|
|
825
|
+
html {
|
|
826
|
+
scrollbar-gutter: stable both-edges;
|
|
827
|
+
}
|
|
824
828
|
body.dark {
|
|
825
829
|
color: var(--dark-color);
|
|
826
830
|
background: var(--dark-bg);
|
|
@@ -2371,7 +2375,7 @@ body.dark .mode-selector .btn2.selected {
|
|
|
2371
2375
|
justify-content: center;
|
|
2372
2376
|
padding: 5px 10px;
|
|
2373
2377
|
box-sizing: border-box;
|
|
2374
|
-
width:
|
|
2378
|
+
width: 70px;
|
|
2375
2379
|
gap: 5px;
|
|
2376
2380
|
border-radius: 5px;
|
|
2377
2381
|
}
|