feat: v0.8.2 clickable paths, alert sessions, ops guard and trail
build-deb / Lint, test, package (push) Successful in 2m45s
build-deb / Deploy to CT 126 (push) Successful in 5s
build-deb / Notify on failure (push) Skipped

- 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:
2026-10-10 20:37:46 -04:00
co-authored by Claude Opus 5.5
parent a92d10ace3
commit f82c13612d
21 changed files with 389 additions and 23 deletions
+52 -2
View File
@@ -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)