Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 8 additions & 8 deletions apps/web/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -728,16 +728,16 @@ export function App() {
? opening.text
: undefined;

const reportRefusal = async (cause: ProductApiError) => {
const code = await responseCode(cause);
const refused =
code === undefined ? undefined : wording.library.refused[code];
toast.error(refused ?? humanFault(cause, wording.library.actionFailed));
void refresh();
};
const reportProjectError = (cause: unknown) => {
if (cause instanceof ApiFailure) toast.error(cause.detail.message);
else if (cause instanceof ProductApiError)
void responseCode(cause).then((code) => {
toast.error(
wording.library.refused[code ?? ""] ??
humanFault(cause, wording.library.actionFailed),
);
void refresh();
});
else if (cause instanceof ProductApiError) void reportRefusal(cause);
else void refresh();
};

Expand Down
6 changes: 6 additions & 0 deletions apps/web/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -423,6 +423,7 @@ export const wording = {
},
activity: {
label: "Who is working",
notRead: "Who is working could not be read.",
role: {
coordinator: "Coordinator",
writer: "Writer",
Expand Down Expand Up @@ -811,6 +812,10 @@ export const wording = {
instruct_coder: () => "Send this change to the app builder?",
} as Record<string, (subject?: string) => string>,
document: (document: string) => `Happy with the ${document}?`,
/** An ask that names neither a document nor a tool. */
unnamed: "Happy for this to go ahead?",
/** The settled line of an ask that names neither a document nor a tool. */
settledUnnamed: "the request",
/** A request_approval ask that covers several documents, by their short names. */
documents: (names: readonly string[]) =>
`Happy with the ${names.slice(0, -1).join(", ")} and ${names.at(-1) ?? ""}?`,
Expand Down Expand Up @@ -974,6 +979,7 @@ export const wording = {
coordinatorStopped: "The coordinator stopped before it replied.",
coordinatorNotStarted: "The coordinator for this project did not start.",
projectNotOpened: "The project could not be opened.",
projectLoading: "Loading this project…",
projectTooShort: "At least ten characters to start a project.",
stepFailed: "That step could not finish.",
},
Expand Down
11 changes: 6 additions & 5 deletions apps/web/src/pages/collaborators.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,11 +42,12 @@ function expiresInLabel(expiresAt: string): string {
async function refusal(cause: unknown): Promise<string> {
if (!(cause instanceof ProductApiError))
return cause instanceof Error ? cause.message : String(cause);
const code = (await responseCode(cause)) ?? "";
return (
wording.settings.collaborators.refused[code] ??
wording.settings.collaborators.failed
);
const code = await responseCode(cause);
const refused =
code === undefined
? undefined
: wording.settings.collaborators.refused[code];
return refused ?? wording.settings.collaborators.failed;
}

export function Collaborators({ user }: { user: HubUser | null }) {
Expand Down
30 changes: 21 additions & 9 deletions apps/web/src/pages/workspace/agent-activity.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
* carried. A phrase is a fixed reading of the latest call, never a guess at
* progress.
*/
import { useQuery } from "@tanstack/react-query";
import { queryOptions, useQuery } from "@tanstack/react-query";
import type { AgentActivity } from "@corbits-builder/client";
import {
readProjectActivity,
Expand Down Expand Up @@ -135,8 +135,8 @@ export function activityPhrase(agent: AgentActivity): {
};
}

export function useProjectActivity(projectId: string) {
return useQuery({
export function projectActivityQuery(projectId: string) {
return queryOptions({
queryKey: keys.projectActivity.of(projectId),
queryFn: ({ signal }) => readProjectActivity(projectId, signal),
// While anyone is mid-turn, a hand-off and each step read sooner.
Expand All @@ -146,12 +146,16 @@ export function useProjectActivity(projectId: string) {
});
}

export function useProjectActivity(projectId: string) {
return useQuery(projectActivityQuery(projectId));
}

/**
* Each live run's state by its latest committed event, polled while any run is live. A run that
* parks on an approval raises no room event, so this is how the page learns the person's turn came.
*/
export function useProjectRunStates(projectId: string) {
return useQuery({
export function projectRunStatesQuery(projectId: string) {
return queryOptions({
queryKey: keys.projectRunStates.of(projectId),
queryFn: ({ signal }) => readProjectRunStates(projectId, signal),
refetchInterval: (query) =>
Expand All @@ -162,6 +166,10 @@ export function useProjectRunStates(projectId: string) {
});
}

export function useProjectRunStates(projectId: string) {
return useQuery(projectRunStatesQuery(projectId));
}

/** The runs held on an approval, as one string a React effect can depend on. */
export function heldOnApproval(
states: { runs: readonly { runId: string; state: string }[] } | undefined,
Expand All @@ -174,8 +182,12 @@ export function heldOnApproval(
.join(",");
}

/** The roles with a run mid-turn right now, from the hub's native activity read. */
export function useRolesAtWork(projectId: string): ReadonlySet<string> {
const agents = useProjectActivity(projectId).data?.agents ?? [];
return new Set(agents.map((agent) => agent.role));
/** The roles with a run mid-turn right now, from the hub's native activity read; undefined until it is read. */
export function useRolesAtWork(
projectId: string,
): ReadonlySet<string> | undefined {
const agents = useProjectActivity(projectId).data?.agents;
return agents === undefined
? undefined
: new Set(agents.map((agent) => agent.role));
}
1 change: 1 addition & 0 deletions apps/web/src/pages/workspace/artifact-pane.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ const document: DeliverableState = {
content: "Previous content",
filedAt: "2026-10-30T11:00:00.000Z",
},
versions: [],
},
};
function render(
Expand Down
6 changes: 3 additions & 3 deletions apps/web/src/pages/workspace/collaboration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -132,14 +132,14 @@ export function shortDocumentName(kind: string, title: string): string {
*/
export function approvalQuestion(
toolName: string | undefined,
document: string,
document: string | undefined,
subject?: string,
): string {
const asked =
toolName === undefined ? undefined : wording.approval.question[toolName];
if (asked !== undefined) return asked(subject);
if (document !== "" || toolName === undefined)
return wording.approval.document(document);
if (document !== undefined) return wording.approval.document(document);
Comment thread
TheGreatAxios marked this conversation as resolved.
Resolved
if (toolName === undefined) return wording.approval.unnamed;
const tool = toolName
.slice(toolName.lastIndexOf(".") + 1)
.replaceAll("_", " ");
Expand Down
8 changes: 4 additions & 4 deletions apps/web/src/pages/workspace/deliverable-block.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ export type LoadedDeliverable = ReadVersion & {
sha256: string;
before: (ReadVersion & { version: number }) | null;
historyError?: string;
versions?: readonly RetainedVersion[];
versions: readonly RetainedVersion[];
};

export type DeliverableState =
Expand Down Expand Up @@ -238,7 +238,7 @@ function ReadyDocument({
const compareWith = changesOpen ? (baseline ?? predecessor) : null;
const afterText = textFor(viewing);
const beforeText = compareWith === null ? null : textFor(compareWith);
const viewed = document.versions?.find((item) => item.version === viewing);
const viewed = document.versions.find((item) => item.version === viewing);
const mediaType = viewed?.mediaType ?? document.mediaType;
const title = viewed?.title ?? document.title;
const kind = mediaKind(mediaType, title);
Expand Down Expand Up @@ -381,7 +381,7 @@ function ReadyDocument({
const barGroups = [
approvalGroup,
versionGroup({
versions: document.versions ?? [],
versions: document.versions,
viewing,
pending: pendingVersion !== null,
canCompare:
Expand Down Expand Up @@ -526,7 +526,7 @@ function ReadyDocument({
<ContextualApprovalRounds
packageVersion={viewing}
packageContent={body}
packageVersions={document.versions?.map((item) => item.version)}
packageVersions={document.versions.map((item) => item.version)}
onReadPackageVersion={fetchVersion}
shareOpen={shareOpen}
onShareOpenChange={setShareOpen}
Expand Down
16 changes: 9 additions & 7 deletions apps/web/src/pages/workspace/dispatch-card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -77,9 +77,10 @@ export function dispatchesOf(
const role = ROLE_OF[part.name];
if (role === undefined) return [];
const pending = part.status !== "output-available";
const args = record(part.input) ?? {};
const instruction = str(args["instruction"]);
const kind = str(args["kind"]);
// A call still streaming its arguments has none to read yet.
const args = record(part.input);
const instruction = str(args?.["instruction"]);
const kind = str(args?.["kind"]);
const base = {
call: part.toolCallId,
tool: part.name,
Expand All @@ -97,8 +98,9 @@ export function dispatchesOf(
refused: str(result.content) ?? text.refusedUnknown,
},
];
const concepts = Array.isArray(args["concepts"])
? args["concepts"].flatMap((item) => {
const named = args?.["concepts"];
const concepts = Array.isArray(named)
? named.flatMap((item) => {
const label = str(record(item)?.["label"]);
return label === undefined ? [] : [label];
})
Expand Down Expand Up @@ -162,8 +164,8 @@ export type DispatchRoom = {
>;
/** The native approvals, pending or as their records settled them. */
readonly approvals: readonly DocumentApproval[];
/** The runs mid-turn, with the tool calls and words their own inference events carried. */
readonly activity: readonly AgentActivity[];
/** The runs mid-turn, with the tool calls and words their own inference events carried; undefined until read. */
readonly activity: readonly AgentActivity[] | undefined;
readonly onOpenDocument?: (kind: string) => void;
};

Expand Down
18 changes: 9 additions & 9 deletions apps/web/src/pages/workspace/document-decision.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,10 @@ const FOLDED_INSTRUCTION_ASKS = new Set(["design_app"]);

function shortName(approval: DocumentApproval): string | undefined {
const { document } = approval;
return document === undefined
? undefined
: shortDocumentName(document.kind, document.title);
if (document === undefined) return undefined;
const name = shortDocumentName(document.kind, document.title);
// A blank title has no name to ask about, so the card falls back to the tool's wording.
return name === "" ? undefined : name;
}

// The documents a request_approval ask approves together, when it covers more than one.
Expand All @@ -62,7 +63,7 @@ function decisionTitle(approval: DocumentApproval, shown: boolean): string {
);
const question = approvalQuestion(
approval.toolName,
shortName(approval) ?? "",
shortName(approval),
approval.subject,
);
return shown ? `${question} ${text.openOnRight}` : question;
Expand Down Expand Up @@ -136,12 +137,11 @@ function settledLine(approval: DocumentApproval): string {
.map(({ title, version }) => `${title} v${String(version)}`)
.join(", "),
);
const settled =
approval.toolName === undefined
? undefined
: text.settled[approval.toolName];
if (approval.toolName === undefined)
return text.approved(text.settledUnnamed);
const settled = text.settled[approval.toolName];
return text.approved(
settled ?? approvalQuestion(approval.toolName, "", approval.subject),
settled ?? approvalQuestion(approval.toolName, undefined, approval.subject),
);
}

Expand Down
1 change: 1 addition & 0 deletions apps/web/src/pages/workspace/empty-project.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ function ready(title: string, content: string): DeliverableState {
version: 1,
sha256: sha,
before: null,
versions: [],
},
};
}
Expand Down
8 changes: 3 additions & 5 deletions apps/web/src/pages/workspace/filed-versions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,10 @@ export type FiledVersion = {

export function useFiledVersions(input: {
readonly projectId: string;
/** The room has its first read; versions before it are history, not news. */
readonly ready: boolean;
/** From the room's first read on; versions in that read are history, not news. */
readonly documents: readonly FiledVersion[];
}): readonly FiledVersion[] {
const { projectId, ready, documents } = input;
const { projectId, documents } = input;
const seen = useRef<Map<string, number> | null>(null);
const [filed, setFiled] = useState<readonly FiledVersion[]>([]);

Expand All @@ -27,15 +26,14 @@ export function useFiledVersions(input: {
}, [projectId]);

useEffect(() => {
if (!ready) return;
const known = seen.current;
seen.current = new Map(documents.map((item) => [item.kind, item.version]));
if (known === null) return;
Comment thread
TheGreatAxios marked this conversation as resolved.
Resolved
const fresh = documents
.filter((item) => item.version > (known.get(item.kind) ?? 0))
.map(({ kind, title, version }) => ({ kind, title, version }));
if (fresh.length > 0) setFiled((current) => [...current, ...fresh]);
}, [ready, documents]);
}, [documents]);

return filed;
}
Loading