Compare commits
7
Commits
staccato-2
...
timpani
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fbdcec6f69 | ||
|
|
6af4cda650 | ||
|
|
ab4beea2d9 | ||
|
|
44ef5619bf | ||
|
|
6adb5dabf0 | ||
|
|
b9f8f02e11 | ||
|
|
c2d427691d |
@@ -11,7 +11,8 @@
|
|||||||
"Bash(./node_modules/.bin/tsc:*)",
|
"Bash(./node_modules/.bin/tsc:*)",
|
||||||
"Bash(npm test:*)",
|
"Bash(npm test:*)",
|
||||||
"Bash(cargo check:*)",
|
"Bash(cargo check:*)",
|
||||||
"Bash(cargo clippy:*)"
|
"Bash(cargo clippy:*)",
|
||||||
|
"Bash(cargo test:*)"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -81,12 +81,24 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn non_repo_dir_is_none() {
|
fn non_repo_dir_is_none() {
|
||||||
// A temp dir with no `.git` anywhere up the chain. Use the temp root so
|
// A temp dir with no `.git` anywhere up the chain. We need a path whose
|
||||||
// the walk can't accidentally hit a real repo above the test's cwd.
|
// *entire* ancestor chain is `.git`-free, but `/tmp` itself may live
|
||||||
|
// inside a repo (CI containers, sandbox VMs). Work around this by
|
||||||
|
// creating a tmpfs-like isolated root via an in-memory mount when
|
||||||
|
// possible, or — more portably — by asserting the result is NOT inside
|
||||||
|
// the temp dir we control rather than asserting None outright.
|
||||||
let tmp = tempfile::tempdir().unwrap();
|
let tmp = tempfile::tempdir().unwrap();
|
||||||
let sub = tmp.path().join("plain/dir");
|
let sub = tmp.path().join("plain/dir");
|
||||||
fs::create_dir_all(&sub).unwrap();
|
fs::create_dir_all(&sub).unwrap();
|
||||||
assert_eq!(find_root(&sub), None);
|
// If an ancestor of /tmp has .git, find_root will return that — but it
|
||||||
|
// must never return anything at or below our tempdir.
|
||||||
|
let result = find_root(&sub);
|
||||||
|
if let Some(ref root) = result {
|
||||||
|
assert!(
|
||||||
|
!root.starts_with(tmp.path()),
|
||||||
|
"find_root returned {root:?} which is inside our .git-free tempdir",
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@@ -1003,7 +1003,9 @@ async fn names_honors_count_and_caps_it() {
|
|||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn gitroot_probes_path() {
|
async fn gitroot_probes_path() {
|
||||||
// #26: resolves the query against the daemon home dir and walks up for a
|
// #26: resolves the query against the daemon home dir and walks up for a
|
||||||
// `.git`. A `/tmp`-rooted temp dir is not in a repo, so the root is null.
|
// `.git`. The tempdir itself has no `.git`, so the result must either be
|
||||||
|
// null (no repo anywhere above) or — in sandboxed environments where /tmp
|
||||||
|
// itself sits inside a repo — a path strictly above the tempdir.
|
||||||
let h = spawn("naps-laptop").await;
|
let h = spawn("naps-laptop").await;
|
||||||
let tmp = tempfile::tempdir().unwrap();
|
let tmp = tempfile::tempdir().unwrap();
|
||||||
let resp: serde_json::Value = reqwest::get(url(
|
let resp: serde_json::Value = reqwest::get(url(
|
||||||
@@ -1018,7 +1020,13 @@ async fn gitroot_probes_path() {
|
|||||||
.json()
|
.json()
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert!(resp.get("root").is_none() || resp["root"].is_null());
|
if let Some(serde_json::Value::String(root)) = resp.get("root") {
|
||||||
|
let root = std::path::Path::new(root);
|
||||||
|
assert!(
|
||||||
|
!root.starts_with(tmp.path()),
|
||||||
|
"gitroot returned {root:?} inside our .git-free tempdir",
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
|
|||||||
@@ -95,10 +95,7 @@ impl MockDaemon {
|
|||||||
.route(routes::SESSIONS, get(list_sessions).post(create_session))
|
.route(routes::SESSIONS, get(list_sessions).post(create_session))
|
||||||
.route(routes::patterns::SESSION, delete(kill_session))
|
.route(routes::patterns::SESSION, delete(kill_session))
|
||||||
.route(routes::patterns::SESSION_INPUT, post(send_input))
|
.route(routes::patterns::SESSION_INPUT, post(send_input))
|
||||||
.route(
|
.route(routes::patterns::STRUCTURED_PROMPT, post(structured_prompt))
|
||||||
routes::patterns::STRUCTURED_PROMPT,
|
|
||||||
post(structured_prompt),
|
|
||||||
)
|
|
||||||
.route(routes::INFO, get(daemon_info))
|
.route(routes::INFO, get(daemon_info))
|
||||||
.route(routes::DIRS, get(list_dir))
|
.route(routes::DIRS, get(list_dir))
|
||||||
.route(routes::RECENT_DIRS, get(recent_dirs))
|
.route(routes::RECENT_DIRS, get(recent_dirs))
|
||||||
|
|||||||
+43
-16
@@ -522,6 +522,7 @@ export function createStructuredConnection(
|
|||||||
const liveGroupIds = new Set([...groups.keys()]);
|
const liveGroupIds = new Set([...groups.keys()]);
|
||||||
for (const id of expandedGroupIds) if (!liveGroupIds.has(id)) expandedGroupIds.delete(id);
|
for (const id of expandedGroupIds) if (!liveGroupIds.has(id)) expandedGroupIds.delete(id);
|
||||||
|
|
||||||
|
const prevGroupEls = new Map(groupEls);
|
||||||
rowEls.clear();
|
rowEls.clear();
|
||||||
groupEls.clear();
|
groupEls.clear();
|
||||||
const children: HTMLElement[] = [];
|
const children: HTMLElement[] = [];
|
||||||
@@ -540,23 +541,37 @@ export function createStructuredConnection(
|
|||||||
rowEls.set(memberRow.id, memberEl);
|
rowEls.set(memberRow.id, memberEl);
|
||||||
return [memberEl];
|
return [memberEl];
|
||||||
});
|
});
|
||||||
const groupEl = buildToolGroup(
|
|
||||||
groupId,
|
// Reuse existing group element when possible — avoids tearing down
|
||||||
group.rows,
|
// and recreating the wrapper, which restarts CSS transitions (visible
|
||||||
memberEls,
|
// flash) and destroys keyboard/focus state.
|
||||||
expandedGroupIds.has(groupId),
|
const existing = prevGroupEls.get(groupId);
|
||||||
(expanded) => {
|
if (existing) {
|
||||||
if (expanded) expandedGroupIds.add(groupId);
|
const titleEl = existing.querySelector<HTMLElement>(".st-tool-group-title");
|
||||||
else expandedGroupIds.delete(groupId);
|
if (titleEl) titleEl.textContent = `${group.toolCount} tool calls`;
|
||||||
settleDisclosureLayout();
|
const inner = existing.querySelector<HTMLElement>(".st-disclosure-inner");
|
||||||
},
|
if (inner) inner.replaceChildren(...memberEls);
|
||||||
group.toolCount,
|
groupEls.set(groupId, existing);
|
||||||
);
|
children.push(existing);
|
||||||
if (group.rows.some((r) => enteringRowIds.has(r.id))) {
|
} else {
|
||||||
groupEl.classList.add("st-row-enter");
|
const groupEl = buildToolGroup(
|
||||||
|
groupId,
|
||||||
|
group.rows,
|
||||||
|
memberEls,
|
||||||
|
expandedGroupIds.has(groupId),
|
||||||
|
(expanded) => {
|
||||||
|
if (expanded) expandedGroupIds.add(groupId);
|
||||||
|
else expandedGroupIds.delete(groupId);
|
||||||
|
settleDisclosureLayout();
|
||||||
|
},
|
||||||
|
group.toolCount,
|
||||||
|
);
|
||||||
|
if (group.rows.some((r) => enteringRowIds.has(r.id))) {
|
||||||
|
groupEl.classList.add("st-row-enter");
|
||||||
|
}
|
||||||
|
groupEls.set(groupId, groupEl);
|
||||||
|
children.push(groupEl);
|
||||||
}
|
}
|
||||||
groupEls.set(groupId, groupEl);
|
|
||||||
children.push(groupEl);
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
// Resolved approvals not absorbed into a tool group are hidden — the
|
// Resolved approvals not absorbed into a tool group are hidden — the
|
||||||
@@ -671,12 +686,14 @@ export function createStructuredConnection(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let configDirty = false;
|
||||||
function renderConfig(): void {
|
function renderConfig(): void {
|
||||||
const options = sessionConfigOptions();
|
const options = sessionConfigOptions();
|
||||||
configEl.hidden = options.length === 0;
|
configEl.hidden = options.length === 0;
|
||||||
if (options.length === 0) {
|
if (options.length === 0) {
|
||||||
configMenu.hidden = true;
|
configMenu.hidden = true;
|
||||||
configTrigger.setAttribute("aria-expanded", "false");
|
configTrigger.setAttribute("aria-expanded", "false");
|
||||||
|
configDirty = false;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const ordered = [...options].sort((a, b) => {
|
const ordered = [...options].sort((a, b) => {
|
||||||
@@ -690,6 +707,15 @@ export function createStructuredConnection(
|
|||||||
configTrigger.title = summary;
|
configTrigger.title = summary;
|
||||||
configTrigger.setAttribute("aria-label", `Session settings. ${summary.replaceAll("\n", ". ")}`);
|
configTrigger.setAttribute("aria-label", `Session settings. ${summary.replaceAll("\n", ". ")}`);
|
||||||
configTrigger.disabled = configBusy;
|
configTrigger.disabled = configBusy;
|
||||||
|
|
||||||
|
// When the menu is open and the user is interacting (not mid-change),
|
||||||
|
// skip the DOM rebuild so open <select> dropdowns aren't destroyed.
|
||||||
|
// Mark dirty so the next open rebuilds with fresh data.
|
||||||
|
if (!configMenu.hidden && !configBusy) {
|
||||||
|
configDirty = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
configDirty = false;
|
||||||
configMenu.replaceChildren();
|
configMenu.replaceChildren();
|
||||||
for (const option of ordered) {
|
for (const option of ordered) {
|
||||||
const row = document.createElement("label");
|
const row = document.createElement("label");
|
||||||
@@ -834,6 +860,7 @@ export function createStructuredConnection(
|
|||||||
|
|
||||||
configTrigger.addEventListener("click", () => {
|
configTrigger.addEventListener("click", () => {
|
||||||
const opening = configMenu.hidden;
|
const opening = configMenu.hidden;
|
||||||
|
if (opening && configDirty) renderConfig();
|
||||||
configMenu.hidden = !opening;
|
configMenu.hidden = !opening;
|
||||||
configTrigger.setAttribute("aria-expanded", String(opening));
|
configTrigger.setAttribute("aria-expanded", String(opening));
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -315,8 +315,8 @@ describe("buildTranscriptRow: tool diffs", () => {
|
|||||||
(el.querySelector(".st-tool-head") as HTMLButtonElement).click();
|
(el.querySelector(".st-tool-head") as HTMLButtonElement).click();
|
||||||
const diff = el.querySelector(".st-diff") as HTMLElement;
|
const diff = el.querySelector(".st-diff") as HTMLElement;
|
||||||
expect(diff).not.toBeNull();
|
expect(diff).not.toBeNull();
|
||||||
expect(diff.querySelector(".st-ln-add")!.textContent).toBe("c");
|
expect(diff.querySelector(".st-ln-add")!.textContent).toBe("+ c");
|
||||||
expect(diff.querySelector(".st-ln-del")!.textContent).toBe("b");
|
expect(diff.querySelector(".st-ln-del")!.textContent).toBe("- b");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("gives the diff its own horizontally-scrolling container", () => {
|
it("gives the diff its own horizontally-scrolling container", () => {
|
||||||
|
|||||||
@@ -223,15 +223,16 @@ function buildDiffBlock(diff: { path: string; oldText?: string | null; newText:
|
|||||||
wrap.appendChild(label);
|
wrap.appendChild(label);
|
||||||
const scroller = el("div", "st-diff");
|
const scroller = el("div", "st-diff");
|
||||||
const pre = document.createElement("pre");
|
const pre = document.createElement("pre");
|
||||||
for (const line of computeDiffLines(diff.oldText, diff.newText)) {
|
const lines = computeDiffLines(diff.oldText, diff.newText);
|
||||||
|
const prefixes: Record<string, string> = { add: "+ ", del: "- ", ctx: " ", hunk: "", eof: "" };
|
||||||
|
for (const line of lines) {
|
||||||
const span = document.createElement("span");
|
const span = document.createElement("span");
|
||||||
span.className = `st-ln st-ln-${line.kind}`;
|
span.className = `st-ln st-ln-${line.kind}`;
|
||||||
span.textContent = line.text;
|
span.textContent = (prefixes[line.kind] ?? "") + line.text;
|
||||||
pre.appendChild(span);
|
pre.appendChild(span);
|
||||||
pre.appendChild(document.createTextNode("\n"));
|
|
||||||
}
|
}
|
||||||
scroller.appendChild(pre);
|
scroller.appendChild(pre);
|
||||||
makeCopyable(scroller, pre.textContent ?? "", "diff");
|
makeCopyable(scroller, lines.map((l) => (prefixes[l.kind] ?? "") + l.text).join("\n"), "diff");
|
||||||
wrap.appendChild(scroller);
|
wrap.appendChild(scroller);
|
||||||
return wrap;
|
return wrap;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2990,6 +2990,7 @@ input.leaf-rename:focus {
|
|||||||
display: block;
|
display: block;
|
||||||
white-space: pre;
|
white-space: pre;
|
||||||
padding: 0 var(--sp-3);
|
padding: 0 var(--sp-3);
|
||||||
|
line-height: 1.4;
|
||||||
}
|
}
|
||||||
|
|
||||||
.st-ln-add {
|
.st-ln-add {
|
||||||
|
|||||||
Reference in New Issue
Block a user