Gateway: changed files, search across hosts, a richer complication summary, and question options in pushes
This commit is contained in:
1 parent
45251b1a43
commit
b08fd9bdb1
13 files changed
+500
-19
No files matched your search
+98
-3
@@ -7,6 +7,7 @@ import { resolvePhase, threadPhase, type Phase } from "./awareness.ts";
|
||||
import type {
|
||||
Activity,
|
||||
AgentProvider,
|
||||
ChangedFile,
|
||||
Feed,
|
||||
FeedItem,
|
||||
HostStatusDetail,
|
||||
@@ -19,6 +20,7 @@ import type {
|
||||
ThreadDetail,
|
||||
ThreadRow,
|
||||
HistoryResponse,
|
||||
SearchResponse,
|
||||
} from "./contract.ts";
|
||||
import type { T3Host } from "./t3/host.ts";
|
||||
import {
|
||||
@@ -46,6 +48,8 @@ export interface ThreadSummary {
|
||||
lastAssistantMessage: string | null;
|
||||
request: PendingRequest | null;
|
||||
activity: Activity | null;
|
||||
/** The files the latest run in the window changed. */
|
||||
changedFiles: ChangedFile[];
|
||||
/** The history route's cursor for messages before `messages`. */
|
||||
earlierCursor: string | null;
|
||||
}
|
||||
@@ -70,6 +74,7 @@ export function summarize(bounded: BoundedThread, thread: ThreadShell): ThreadSu
|
||||
lastAssistantMessage: lastAssistant ? trimText(lastAssistant.text, LAST_MESSAGE_CHARS) : null,
|
||||
request: pendingRequest(thread, turnItems),
|
||||
activity: currentActivity(turnItems),
|
||||
changedFiles: latestChanges(turnItems),
|
||||
earlierCursor: windowCursor(start, bounded),
|
||||
};
|
||||
}
|
||||
@@ -128,6 +133,21 @@ export async function loadHistory(host: T3Host, thread: ThreadShell, cursor: str
|
||||
return { messages: [], earlierCursor: next && `h${next}` };
|
||||
}
|
||||
|
||||
/** The files the latest run changed, summed per file, most lines changed first. */
|
||||
export function latestChanges(turnItems: TurnItem[]): ChangedFile[] {
|
||||
const runId = turnItems.findLast((item) => item.runId)?.runId;
|
||||
if (!runId) return [];
|
||||
const files = new Map<string, ChangedFile>();
|
||||
for (const item of turnItems) {
|
||||
if (item.type !== "file_change" || item.runId !== runId || !item.fileName) continue;
|
||||
const file = files.get(item.fileName) ?? { path: item.fileName, name: item.fileName.split("/").pop()!, additions: 0, deletions: 0 };
|
||||
file.additions += item.additions ?? 0;
|
||||
file.deletions += item.deletions ?? 0;
|
||||
files.set(item.fileName, file);
|
||||
}
|
||||
return [...files.values()].sort((a, b) => b.additions + b.deletions - (a.additions + a.deletions));
|
||||
}
|
||||
|
||||
/** The newest step still in progress, said briefly. Monitors are left out, as they run for hours. */
|
||||
export function currentActivity(turnItems: TurnItem[]): Activity | null {
|
||||
for (let i = turnItems.length - 1; i >= 0; i--) {
|
||||
@@ -341,6 +361,13 @@ function feedItem(
|
||||
updatedAt: thread.updatedAt,
|
||||
lastMessage: summary?.lastAssistantMessage ?? null,
|
||||
activity: summary?.activity ?? null,
|
||||
changes: summary?.changedFiles.length
|
||||
? {
|
||||
files: summary.changedFiles.length,
|
||||
additions: summary.changedFiles.reduce((sum, file) => sum + file.additions, 0),
|
||||
deletions: summary.changedFiles.reduce((sum, file) => sum + file.deletions, 0),
|
||||
}
|
||||
: null,
|
||||
request: (via ? via.summary : summary)?.request ?? null,
|
||||
via: via ? { hostId: via.host.id, threadId: via.thread.id, title: via.thread.title } : null,
|
||||
};
|
||||
@@ -362,25 +389,55 @@ function waitingDelegate(hosts: T3Host[], parentHost: T3Host, parent: ThreadShel
|
||||
}
|
||||
|
||||
/** Counts for the complication, from thread shells alone. */
|
||||
export function buildSummary(hosts: T3Host[]): Summary {
|
||||
/**
|
||||
* Counts for the complication, plus the longest waiting request and the busiest working thread.
|
||||
* Reads at most those two threads, waiting briefly as the feed does.
|
||||
*/
|
||||
export async function buildSummary(hosts: T3Host[], cache: SummaryCache): Promise<Summary> {
|
||||
let needsYou = 0;
|
||||
let working = 0;
|
||||
let next: { host: T3Host; thread: ThreadShell } | null = null;
|
||||
let current: { host: T3Host; thread: ThreadShell } | null = null;
|
||||
for (const host of hosts) {
|
||||
for (const thread of host.threads.values()) {
|
||||
if (thread.archivedAt || thread.deletedAt) continue;
|
||||
const phase = threadPhase(thread);
|
||||
if (phase === "starting" || phase === "running") working++;
|
||||
if (phase === "starting" || phase === "running") {
|
||||
working++;
|
||||
if (!current || thread.updatedAt > current.thread.updatedAt) current = { host, thread };
|
||||
}
|
||||
if (phase !== "waiting_for_approval" && phase !== "waiting_for_input") continue;
|
||||
needsYou++;
|
||||
const waitingSince = thread.pendingRuntimeRequest?.createdAt ?? "";
|
||||
if (!next || waitingSince < (next.thread.pendingRuntimeRequest?.createdAt ?? "")) next = { host, thread };
|
||||
}
|
||||
}
|
||||
const read = [next, current].filter((pick) => pick !== null);
|
||||
const missing = read.filter(({ host, thread }) => !cache.peek(host, thread));
|
||||
if (missing.length > 0) {
|
||||
await Promise.race([
|
||||
Promise.all(missing.map(({ host, thread }) => cache.get(host, thread).catch(() => undefined))),
|
||||
Bun.sleep(SUMMARY_WAIT_MS),
|
||||
]);
|
||||
}
|
||||
const request = next && cache.peek(next.host, next.thread)?.request;
|
||||
const asked = request?.prompt ?? request?.questions[0]?.question;
|
||||
return {
|
||||
needsYou,
|
||||
working,
|
||||
next: next && { hostId: next.host.id, threadId: next.thread.id, title: next.thread.title, hostLabel: next.host.label },
|
||||
next: next && {
|
||||
hostId: next.host.id,
|
||||
threadId: next.thread.id,
|
||||
title: next.thread.title,
|
||||
hostLabel: next.host.label,
|
||||
request: asked ? trimText(asked, ACTIVITY_CHARS) : null,
|
||||
},
|
||||
current: current && {
|
||||
hostId: current.host.id,
|
||||
threadId: current.thread.id,
|
||||
title: current.thread.title,
|
||||
activity: cache.peek(current.host, current.thread)?.activity ?? null,
|
||||
},
|
||||
hostsOffline: hosts.filter((host) => host.status !== "online").length,
|
||||
};
|
||||
}
|
||||
@@ -464,6 +521,7 @@ export async function buildThreadDetail(
|
||||
askFirst: thread.runtimeMode === ASK_FIRST,
|
||||
earlierCursor: summary.earlierCursor,
|
||||
subagents: subagents(hosts, host, thread),
|
||||
changedFiles: summary.changedFiles,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -497,6 +555,43 @@ function subagents(hosts: T3Host[], parentHost: T3Host, parent: ThreadShell): Th
|
||||
return rows.sort(newestFirst);
|
||||
}
|
||||
|
||||
/**
|
||||
* Searches every online host at once and merges the hits, newest match first. Hosts that are
|
||||
* offline or fail are named in `unsearched`.
|
||||
*/
|
||||
export async function searchThreads(hosts: T3Host[], query: string): Promise<SearchResponse> {
|
||||
const results = await Promise.all(
|
||||
hosts.map(async (host) => {
|
||||
if (host.status !== "online") return null;
|
||||
try {
|
||||
return { host, matches: await host.search(query) };
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}),
|
||||
);
|
||||
const hits = results.flatMap((result) =>
|
||||
(result?.matches ?? []).flatMap((match) => {
|
||||
const thread = result!.host.threads.get(match.threadId);
|
||||
if (!thread || !isLive(thread)) return [];
|
||||
const snippet = match.snippet.trim();
|
||||
return [
|
||||
{
|
||||
thread: threadRow(result!.host, thread),
|
||||
projectTitle: result!.host.projects.get(thread.projectId)?.title ?? "",
|
||||
hostLabel: result!.host.label,
|
||||
snippet: snippet && snippet !== thread.title ? trimText(snippet, 160) : null,
|
||||
at: match.messageCreatedAt ?? thread.updatedAt,
|
||||
},
|
||||
];
|
||||
}),
|
||||
);
|
||||
return {
|
||||
hits: hits.sort((a, b) => b.at.localeCompare(a.at)).map(({ at: _, ...hit }) => hit),
|
||||
unsearched: hosts.filter((host, i) => results[i] === null).map((host) => host.label),
|
||||
};
|
||||
}
|
||||
|
||||
/** A project's newest 50 threads, settled ones included. Subagents are listed under their parents. */
|
||||
export function projectThreads(host: T3Host, projectId: string): ThreadRow[] {
|
||||
return [...host.threads.values()]
|
||||
|
||||
Reference in new issue
Block a user