Merge remote-tracking branch 'origin/main' into activity
This commit is contained in:
@@ -44,6 +44,14 @@ export const FOCUS_PANE_EVENT = "catalog-focus-pane";
|
||||
* ("Spawn similar"). Detail is the session being cloned. */
|
||||
export const SPAWN_SIMILAR_EVENT = "catalog-spawn-similar";
|
||||
|
||||
/** A session row was clicked (issue #133). The row's `href` drives normal
|
||||
* hash navigation, but a split/drag can move focus without ever touching
|
||||
* `location.hash` — so a click back onto a session whose hash still
|
||||
* matches fires no `hashchange` and the app never hears about it. This
|
||||
* event is the click-driven path that doesn't depend on the hash actually
|
||||
* changing. */
|
||||
export const SESSION_SELECT_EVENT = "catalog-session-select";
|
||||
|
||||
/** The rows a keyboard walk stops on, in visual (document) order. Expanded
|
||||
* folder headers are skipped: a directory is not something to focus, and the
|
||||
* cards under it are one row away anyway. A *collapsed* folder still counts —
|
||||
@@ -192,6 +200,9 @@ function renderNode(node: TreeNode, depth: number): HTMLElement {
|
||||
ev.stopPropagation();
|
||||
void requestKill(s, name);
|
||||
});
|
||||
row.addEventListener("click", () => {
|
||||
document.dispatchEvent(new CustomEvent(SESSION_SELECT_EVENT, { detail: s.id }));
|
||||
});
|
||||
row.addEventListener("contextmenu", (ev) => {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
|
||||
@@ -47,6 +47,30 @@ describe("resolveFastForward (#67)", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("prefers the focused session's project over the freshest one", () => {
|
||||
const projects = [
|
||||
project({ path: "/repos/fresh", name: "fresh", last_command: "claude" }),
|
||||
project({
|
||||
path: "/repos/current",
|
||||
name: "current",
|
||||
last_activity: "2020-01-01T00:00:00Z",
|
||||
last_command: "zsh",
|
||||
}),
|
||||
];
|
||||
const res = resolveFastForward(projects, presets, "n", "/repos/current");
|
||||
expect(res.project?.path).toBe("/repos/current");
|
||||
expect(res.command).toBe("zsh");
|
||||
});
|
||||
|
||||
it("falls back to the freshest project when the focused cwd is unknown", () => {
|
||||
const projects = [
|
||||
project({ path: "/repos/old", name: "old", last_activity: "2020-01-01T00:00:00Z" }),
|
||||
project({ path: "/repos/fresh", name: "fresh" }),
|
||||
];
|
||||
const res = resolveFastForward(projects, presets, "n", "/repos/gone");
|
||||
expect(res.project?.path).toBe("/repos/fresh");
|
||||
});
|
||||
|
||||
it("falls back to the first non-custom preset when there is no last command", () => {
|
||||
const res = resolveFastForward([project({ last_command: undefined })], presets, "n");
|
||||
expect(res.command).toBe("claude");
|
||||
|
||||
@@ -13,15 +13,20 @@ export interface FastForwardResolution {
|
||||
name?: string;
|
||||
}
|
||||
|
||||
/** The defaults: most recent project (fresh-first, same ordering the picker
|
||||
* shows), its last command falling back to the first non-custom preset, and
|
||||
* a freshly generated name when the daemon supplied one. */
|
||||
/** The defaults: the currently focused session's project when it's still a
|
||||
* known project, else the most recent one (fresh-first, same ordering the
|
||||
* picker shows) — so `N` lands where the operator currently is, matching
|
||||
* what plain `n` seeds from `spawnSimilarProjectSeed`. Its last command
|
||||
* falls back to the first non-custom preset, and the name is freshly
|
||||
* generated when the daemon supplied one. */
|
||||
export function resolveFastForward(
|
||||
projects: Project[],
|
||||
presets: SpawnPreset[],
|
||||
name?: string,
|
||||
focusedCwd?: string,
|
||||
): FastForwardResolution {
|
||||
const project = sortProjects(projects, new Date())[0];
|
||||
const project =
|
||||
projects.find((p) => p.path === focusedCwd) ?? sortProjects(projects, new Date())[0];
|
||||
const lastCommand = project?.last_command?.trim();
|
||||
const command =
|
||||
lastCommand || presets.find((p) => p.command !== "")?.command || undefined;
|
||||
|
||||
+39
-7
@@ -29,7 +29,12 @@ import { mountHud, type Hud } from "./hud.js";
|
||||
import { emptyLayout, type Layout, openSessions } from "./layout.js";
|
||||
import { createMultiView, type MultiView } from "./multiview.js";
|
||||
import { loadLayout, saveLayout } from "./persistence.js";
|
||||
import { FOCUS_PANE_EVENT, OPEN_SPAWN_EVENT, SPAWN_SIMILAR_EVENT } from "./catalog.js";
|
||||
import {
|
||||
FOCUS_PANE_EVENT,
|
||||
OPEN_SPAWN_EVENT,
|
||||
SESSION_SELECT_EVENT,
|
||||
SPAWN_SIMILAR_EVENT,
|
||||
} from "./catalog.js";
|
||||
import { initTheme } from "./theme.js";
|
||||
import { sessionName, type Project, type Session, type SpawnPreset } from "./types.js";
|
||||
import { installViewportSizing } from "./viewport.js";
|
||||
@@ -257,13 +262,28 @@ function requestOpenSpawn(): void {
|
||||
// beside it is the point.
|
||||
// The session you were last on preselects its project: `n` then Enter spawns
|
||||
// another session there without touching the picker.
|
||||
const focused = mv?.focusedSessionId() ?? null;
|
||||
const current = focused ? store.snapshot().sessions.get(focused) : undefined;
|
||||
const current = focusedSession();
|
||||
openSpawnModal({
|
||||
seed: current ? spawnSimilarProjectSeed(current) : undefined,
|
||||
onSuccess: (session) => mountSpawned(session, { newTab: true }),
|
||||
});
|
||||
}
|
||||
|
||||
/** The session the operator was last on, if any. Shared by the plain spawn
|
||||
* modal seed and fast-forward's project pick: both should land on "where I
|
||||
* currently am". */
|
||||
function focusedSession(): Session | undefined {
|
||||
const focused = mv?.focusedSessionId() ?? null;
|
||||
return focused ? store.snapshot().sessions.get(focused) : undefined;
|
||||
}
|
||||
|
||||
/** The focused session's project — its base repo for a worktree session, its
|
||||
* cwd otherwise (same rule as spawnSimilarProjectSeed). */
|
||||
function focusedProjectCwd(): string | undefined {
|
||||
const s = focusedSession();
|
||||
if (!s) return undefined;
|
||||
return s.worktree ? s.worktree.base_repo : s.cwd;
|
||||
}
|
||||
document.addEventListener(OPEN_SPAWN_EVENT, () => requestOpenSpawn());
|
||||
document.addEventListener(SPAWN_SIMILAR_EVENT, (ev) => {
|
||||
const s = (ev as CustomEvent<Session>).detail;
|
||||
@@ -298,7 +318,7 @@ async function requestFastForward(): Promise<void> {
|
||||
} catch {
|
||||
/* keep fallbacks */
|
||||
}
|
||||
const resolved = resolveFastForward(discovered, presets, name);
|
||||
const resolved = resolveFastForward(discovered, presets, name, focusedProjectCwd());
|
||||
const req = buildFastForwardRequest(resolved);
|
||||
if (req) {
|
||||
try {
|
||||
@@ -355,6 +375,20 @@ window.addEventListener("keydown", (e) => {
|
||||
// Escape in the session list hands the keyboard back to the pane.
|
||||
document.addEventListener(FOCUS_PANE_EVENT, () => pool.focusTerminal());
|
||||
|
||||
/** Open/focus a session and reflect it in the shell chrome. Shared by the
|
||||
* hash router and the sidebar's click-driven path (#133), which can't rely
|
||||
* on `hashchange` alone — a split or drag can move focus without the hash
|
||||
* changing, leaving a click back onto that session's row silently ignored. */
|
||||
function focusSession(sessionId: string): void {
|
||||
showMultiView();
|
||||
mv.openSession(sessionId);
|
||||
updateShellRoute();
|
||||
}
|
||||
|
||||
document.addEventListener(SESSION_SELECT_EVENT, (e) => {
|
||||
focusSession((e as CustomEvent<string>).detail);
|
||||
});
|
||||
|
||||
type Route =
|
||||
| { kind: "catalog" }
|
||||
| { kind: "attach"; sessionId: string }
|
||||
@@ -373,9 +407,7 @@ function dispatch(): void {
|
||||
switch (route.kind) {
|
||||
case "attach":
|
||||
// Deep-link: open/focus the session, then show the layout.
|
||||
showMultiView();
|
||||
mv.openSession(route.sessionId);
|
||||
updateShellRoute();
|
||||
focusSession(route.sessionId);
|
||||
return;
|
||||
case "not-found":
|
||||
shellEl.dataset.route = "notfound";
|
||||
|
||||
Reference in New Issue
Block a user