@saadnvd1/agent-os 0.3.647 → 0.3.653
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/app/api/bus/peers/route.ts +1 -1
- package/components/Chat/ChatPanel.tsx +49 -52
- package/components/Chat/QuoteButton.tsx +24 -7
- package/components/Chat/composer/editor.test.ts +39 -2
- package/components/Chat/quote-selection.test.ts +184 -2
- package/components/Chat/quote-selection.ts +70 -0
- package/components/Chat/useChatCommands.ts +19 -3
- package/components/SessionList/OrchestratorPinRow.tsx +7 -2
- package/components/SessionList/RowParts.tsx +32 -7
- package/components/SessionList/RowRename.test.ts +102 -0
- package/components/SessionList/RowShell.tsx +9 -1
- package/components/SessionList/SessionRow.tsx +2 -1
- package/components/SessionList/SessionShelves.tsx +9 -6
- package/components/SessionList/index.tsx +15 -16
- package/components/SessionList/useSessionSwitching.ts +55 -0
- package/hooks/useKeybindings.ts +27 -0
- package/lib/agents/brief.ts +1 -1
- package/lib/bus/index.ts +77 -11
- package/lib/chat/background.ts +0 -22
- package/lib/keybindings/chord.ts +114 -0
- package/lib/keybindings/focus.ts +29 -0
- package/lib/keybindings/registry.ts +84 -0
- package/lib/sidebar/visible-order.ts +56 -0
- package/package.json +1 -1
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
const NON_TEXT = new Set([
|
|
2
|
+
"checkbox",
|
|
3
|
+
"radio",
|
|
4
|
+
"button",
|
|
5
|
+
"submit",
|
|
6
|
+
"reset",
|
|
7
|
+
"range",
|
|
8
|
+
"color",
|
|
9
|
+
"file",
|
|
10
|
+
"image",
|
|
11
|
+
]);
|
|
12
|
+
|
|
13
|
+
// Whether a vertical-arrow chord should be left to the focused field: it has
|
|
14
|
+
// text, so ⌘↑/⌘↓ move its caret. An empty field, a terminal, or no field
|
|
15
|
+
// at all lets the app have the key. A dialog (the palette) keeps its keys.
|
|
16
|
+
export function fieldOwnsKey(target: EventTarget | null): boolean {
|
|
17
|
+
const el = target as HTMLElement | null;
|
|
18
|
+
if (!el || typeof el.closest !== "function") return false;
|
|
19
|
+
if (el.closest(".xterm")) return false;
|
|
20
|
+
if (el.closest('[role="dialog"]')) return true;
|
|
21
|
+
const tag = el.tagName;
|
|
22
|
+
if (tag === "TEXTAREA") return (el as HTMLTextAreaElement).value.length > 0;
|
|
23
|
+
if (tag === "INPUT") {
|
|
24
|
+
const input = el as HTMLInputElement;
|
|
25
|
+
return !NON_TEXT.has(input.type) && input.value.length > 0;
|
|
26
|
+
}
|
|
27
|
+
if (el.isContentEditable) return (el.textContent ?? "").trim().length > 0;
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { matchChord, parseChord, type Chord, type KeyLike } from "./chord";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Key chords tied to commands. Features register theirs under a source name
|
|
5
|
+
* and take them back the same way; one keydown listener serves them all.
|
|
6
|
+
*
|
|
7
|
+
* To add a shortcut, add one line to the `useKeybindings` call of the
|
|
8
|
+
* feature that owns the action: { id, keys: "ctrl+b", when, run }.
|
|
9
|
+
*/
|
|
10
|
+
export interface Keybinding {
|
|
11
|
+
id: string;
|
|
12
|
+
// "ctrl+b", "shift+tab", "esc": modifiers must match exactly.
|
|
13
|
+
keys: string;
|
|
14
|
+
// Whether it applies right now. The key is left alone when it doesn't.
|
|
15
|
+
when?: (e: KeyboardEvent) => boolean;
|
|
16
|
+
// Also fire while the key is held down.
|
|
17
|
+
repeat?: boolean;
|
|
18
|
+
run: () => void;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
interface Compiled {
|
|
22
|
+
binding: Keybinding;
|
|
23
|
+
chord: Chord;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export class KeybindingRegistry {
|
|
27
|
+
private sources = new Map<string, Compiled[]>();
|
|
28
|
+
private attached = false;
|
|
29
|
+
|
|
30
|
+
constructor(
|
|
31
|
+
private target: Pick<Window, "addEventListener" | "removeEventListener">
|
|
32
|
+
) {}
|
|
33
|
+
|
|
34
|
+
// Replaces what a source binds; returns how to take it back.
|
|
35
|
+
register(source: string, bindings: Keybinding[]): () => void {
|
|
36
|
+
const compiled = bindings.map((binding) => ({
|
|
37
|
+
binding,
|
|
38
|
+
chord: parseChord(binding.keys),
|
|
39
|
+
}));
|
|
40
|
+
this.sources.set(source, compiled);
|
|
41
|
+
this.attach();
|
|
42
|
+
return () => {
|
|
43
|
+
if (this.sources.get(source) !== compiled) return;
|
|
44
|
+
this.sources.delete(source);
|
|
45
|
+
if (!this.sources.size) this.detach();
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Runs the first binding whose chord matches and whose `when` holds, and
|
|
50
|
+
// reports whether one did (the caller then stops the key).
|
|
51
|
+
handle(e: KeyboardEvent): boolean {
|
|
52
|
+
for (const list of this.sources.values())
|
|
53
|
+
for (const { binding, chord } of list) {
|
|
54
|
+
if (!matchChord(chord, e as KeyLike)) continue;
|
|
55
|
+
if (e.isComposing || (e.repeat && !binding.repeat)) continue;
|
|
56
|
+
if (binding.when && !binding.when(e)) continue;
|
|
57
|
+
e.preventDefault();
|
|
58
|
+
binding.run();
|
|
59
|
+
return true;
|
|
60
|
+
}
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
private onKey = (e: Event) => {
|
|
65
|
+
this.handle(e as KeyboardEvent);
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
private attach() {
|
|
69
|
+
if (this.attached) return;
|
|
70
|
+
this.attached = true;
|
|
71
|
+
this.target.addEventListener("keydown", this.onKey, true);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
private detach() {
|
|
75
|
+
this.attached = false;
|
|
76
|
+
this.target.removeEventListener("keydown", this.onKey, true);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export const keybindings = new KeybindingRegistry(
|
|
81
|
+
typeof window === "undefined"
|
|
82
|
+
? { addEventListener() {}, removeEventListener() {} }
|
|
83
|
+
: window
|
|
84
|
+
);
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import type { MachineGroup } from "./machines";
|
|
2
|
+
import { doneLimit, type Shelves, type SidebarRow } from "./shelves";
|
|
3
|
+
|
|
4
|
+
export interface ShelfFolds {
|
|
5
|
+
doneCollapsed: boolean;
|
|
6
|
+
snoozedCollapsed: boolean;
|
|
7
|
+
donePages: number;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
// The Snoozed and Done rows the sidebar draws: none while folded, Done cut
|
|
11
|
+
// to the pages shown. SessionShelves renders from this too, so the two agree.
|
|
12
|
+
export function shownShelves(shelves: Shelves, folds: ShelfFolds) {
|
|
13
|
+
return {
|
|
14
|
+
snoozed: folds.snoozedCollapsed ? [] : shelves.snoozed,
|
|
15
|
+
done: folds.doneCollapsed
|
|
16
|
+
? []
|
|
17
|
+
: shelves.done.slice(0, doneLimit(folds.donePages)),
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const flatten = (rows: SidebarRow[]): string[] =>
|
|
22
|
+
rows.flatMap((r) => [r.session.id, ...flatten(r.workers)]);
|
|
23
|
+
|
|
24
|
+
// Session ids top to bottom, as the sidebar draws them: folded shelves are
|
|
25
|
+
// out, a conductor's workers follow it. The one order for switching sessions.
|
|
26
|
+
export function visibleSessionIds(
|
|
27
|
+
shelves: Shelves,
|
|
28
|
+
machines: MachineGroup[],
|
|
29
|
+
folds: ShelfFolds
|
|
30
|
+
): string[] {
|
|
31
|
+
const shown = shownShelves(shelves, folds);
|
|
32
|
+
return flatten([
|
|
33
|
+
...shelves.orchestrators,
|
|
34
|
+
...shelves.pinned,
|
|
35
|
+
...shelves.needsYou,
|
|
36
|
+
...shelves.working,
|
|
37
|
+
...shelves.inReview,
|
|
38
|
+
...shelves.ongoing,
|
|
39
|
+
...shown.snoozed,
|
|
40
|
+
...shown.done,
|
|
41
|
+
...machines.flatMap((g) => g.rows),
|
|
42
|
+
]);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// The neighbour of `activeId` one step up (-1) or down (1); null at either
|
|
46
|
+
// end (no wrap), and the first or last row when nothing is active yet.
|
|
47
|
+
export function neighbourSession(
|
|
48
|
+
ids: string[],
|
|
49
|
+
activeId: string | undefined,
|
|
50
|
+
step: 1 | -1
|
|
51
|
+
): string | null {
|
|
52
|
+
if (!ids.length) return null;
|
|
53
|
+
const at = activeId ? ids.indexOf(activeId) : -1;
|
|
54
|
+
if (at < 0) return step === 1 ? ids[0] : ids[ids.length - 1];
|
|
55
|
+
return ids[at + step] ?? null;
|
|
56
|
+
}
|