feat: v0.8.2 clickable paths, alert sessions, ops guard and trail
- Terminal link provider: printed paths that are files of the session open in Files (at the line); the file list is preloaded so the first hover works. - Alerts: "In a new session" opens alert-<name> and types the prompt once Claude is up. - devdesk-guard PreToolUse(Bash) hook: permissionDecision "ask" for destructive commands (verified to prompt in auto mode); DEVDESK_GUARD and guard.local. - State hook logs Bash commands and derives host tags (ssh targets, CT/VM ids); --list carries tags, --peek the last commands; UI shows both. Closes #21 Closes #18 Closes #27 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+52
-2
@@ -437,6 +437,7 @@ export class FilesPane {
|
||||
async loadFileList() {
|
||||
if (this.fileList && Date.now() - this.fileList.at < 60000 && this.fileList.session === this.session) return this.fileList.items;
|
||||
const list = parseListOutput(await runCommand(['--fs-find', this.session], 45000));
|
||||
this.fileListRoot = list.root;
|
||||
const items = list.files.map((path) => {
|
||||
const name = base(path);
|
||||
const [icon] = iconFor(name);
|
||||
@@ -462,6 +463,9 @@ export class FilesPane {
|
||||
}
|
||||
|
||||
async claudeTurnEnded() {
|
||||
// New files may exist now: refresh the list terminal links are checked against.
|
||||
this.fileList = null;
|
||||
this.knownFiles();
|
||||
if (!this.isOpen) {
|
||||
// Next time it opens it revalidates anyway; open tabs re-check on activation.
|
||||
for (const v of this.views.values()) v.stale = true;
|
||||
@@ -707,8 +711,9 @@ export class FilesPane {
|
||||
|
||||
// Single click opens a preview tab (italic) that the next click replaces;
|
||||
// double click, Enter or go-to-file keeps it.
|
||||
openFile(path, { mode, pin = false } = {}) {
|
||||
const wanted = mode ?? (isMarkdown(path) ? 'preview' : 'view');
|
||||
openFile(path, { mode, pin = false, line } = {}) {
|
||||
// A line number means source: markdown opens as text, not as a preview.
|
||||
const wanted = mode ?? (isMarkdown(path) && !line ? 'preview' : 'view');
|
||||
const key = `${wanted}:${path}`;
|
||||
let tab = this.tabs.find((t) => t.key === key) ?? this.tabs.find((t) => t.path === path && !mode);
|
||||
if (!tab) {
|
||||
@@ -729,6 +734,11 @@ export class FilesPane {
|
||||
tab.pinned = true;
|
||||
}
|
||||
this.pushRecent(path);
|
||||
if (line) {
|
||||
const loaded = this.views.get(tab.key)?.editor;
|
||||
if (loaded?.goto) setTimeout(() => loaded.goto(line), 0);
|
||||
else tab.line = line;
|
||||
}
|
||||
this.activate(tab.key);
|
||||
if (this.root.classList.contains('is-narrow')) this.setExplorer(false);
|
||||
}
|
||||
@@ -1030,9 +1040,49 @@ export class FilesPane {
|
||||
view.node.replaceChildren(this.bar(tab, header, view), content);
|
||||
if (banner) this.oldDraft(tab.key, banner);
|
||||
if (view.dirty) this.renderTabs();
|
||||
this.gotoPending(tab, view);
|
||||
return;
|
||||
}
|
||||
view.node.replaceChildren(this.bar(tab, header, view), content);
|
||||
this.gotoPending(tab, view);
|
||||
}
|
||||
|
||||
gotoPending(tab, view) {
|
||||
if (!tab.line || !view.editor?.goto) return;
|
||||
const line = tab.line;
|
||||
delete tab.line;
|
||||
setTimeout(() => view.editor.goto(line), 0);
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------ paths in the terminal (devdesk#21)
|
||||
|
||||
// The session's file list (cached by go-to-file). Loaded ahead of time (on
|
||||
// connect and after Claude's turns): xterm asks for a line's links once and
|
||||
// caches the answer, so it has to be ready before the first hover.
|
||||
knownFiles() {
|
||||
if (this.session !== this.hooks.session()) this.syncSession();
|
||||
if (this.fileList?.session === this.session) {
|
||||
if (!this.fileSet || this.fileSet.list !== this.fileList.items) {
|
||||
this.fileSet = { list: this.fileList.items, set: new Set(this.fileList.items.map((x) => x.id)) };
|
||||
}
|
||||
return this.fileSet.set;
|
||||
}
|
||||
if (!this.loadingList && this.session) {
|
||||
this.loadingList = true;
|
||||
this.loadFileList().catch(() => {}).finally(() => { this.loadingList = false; });
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// A path as printed in the terminal -> the session-relative path, if it's a
|
||||
// file of this session (absolute paths inside the folder count too).
|
||||
resolvePrinted(text) {
|
||||
const known = this.knownFiles();
|
||||
if (!known) return null;
|
||||
let path = text.replace(/^\.\//, '');
|
||||
const root = this.fileListRoot;
|
||||
if (path.startsWith('/') && root && path.startsWith(`${root}/`)) path = path.slice(root.length + 1);
|
||||
return known.has(path) ? path : null;
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------ editing (devdesk#20)
|
||||
|
||||
Reference in New Issue
Block a user