feat: integrate mobile work chat and Gitea delivery
This commit is contained in:
@@ -13,24 +13,45 @@ import { STATUS_COPY } from "@/lib/chat/constants";
|
||||
import { getMessageText } from "@/lib/chat/transforms";
|
||||
import { buildMobileWorkspaceView } from "@/lib/mobile-workspace/build-mobile-workspace-view";
|
||||
|
||||
const WORK_UNIT_SCOPE_PATTERN =
|
||||
/^<zopu-work-unit id="(?<workUnitId>[^"]+)">[\s\S]*?<message>(?<message>[\s\S]*?)<\/message>\s*<\/zopu-work-unit>$/u;
|
||||
|
||||
const toAssistantMessages = (
|
||||
messages: ReturnType<typeof useOrganizationChatAgent>["messages"]
|
||||
): MobileAssistantMessageView[] =>
|
||||
messages.flatMap((message) => {
|
||||
messages: ReturnType<typeof useOrganizationChatAgent>["messages"],
|
||||
selectedWorkUnitId?: string
|
||||
): MobileAssistantMessageView[] => {
|
||||
const result: MobileAssistantMessageView[] = [];
|
||||
for (const message of messages) {
|
||||
if (message.role !== "assistant" && message.role !== "user") {
|
||||
return [];
|
||||
continue;
|
||||
}
|
||||
const text = getMessageText(message);
|
||||
return text
|
||||
? [
|
||||
{
|
||||
id: message.id,
|
||||
role: message.role,
|
||||
text,
|
||||
},
|
||||
]
|
||||
: [];
|
||||
});
|
||||
const rawText = getMessageText(message);
|
||||
if (!rawText) {
|
||||
continue;
|
||||
}
|
||||
if (!selectedWorkUnitId) {
|
||||
result.push({ id: message.id, role: message.role, text: rawText });
|
||||
continue;
|
||||
}
|
||||
if (message.role === "user") {
|
||||
const match = WORK_UNIT_SCOPE_PATTERN.exec(rawText);
|
||||
if (match?.groups?.workUnitId !== selectedWorkUnitId) {
|
||||
result.length = 0;
|
||||
continue;
|
||||
}
|
||||
result.push({
|
||||
id: message.id,
|
||||
role: message.role,
|
||||
text: match.groups.message?.trim() ?? rawText,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
if (result.length > 0) {
|
||||
result.push({ id: message.id, role: message.role, text: rawText });
|
||||
}
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
const getStatusTone = (
|
||||
status: ReturnType<typeof useOrganizationChatAgent>["status"]
|
||||
@@ -57,12 +78,23 @@ export const useMobileProjectWorkspace = ({
|
||||
? { organizationId: organization.organizationId }
|
||||
: "skip"
|
||||
);
|
||||
const selectedIssue = projectWorkspace.issues?.find(
|
||||
(issue) => String(issue._id) === selectedWorkUnitId
|
||||
);
|
||||
const sendMessage = (message: string) => {
|
||||
if (!selectedIssue) {
|
||||
return chatAgent.sendMessage(message);
|
||||
}
|
||||
return chatAgent.sendMessage(
|
||||
`<zopu-work-unit id="${selectedIssue._id}">\n<context>Issue #${selectedIssue.number}: ${selectedIssue.title}</context>\n<message>${message}</message>\n</zopu-work-unit>`
|
||||
);
|
||||
};
|
||||
const assistant: MobileAssistantView = {
|
||||
isBusy:
|
||||
chatAgent.status === "connecting" ||
|
||||
chatAgent.status === "streaming" ||
|
||||
chatAgent.status === "submitted",
|
||||
messages: toAssistantMessages(chatAgent.messages),
|
||||
messages: toAssistantMessages(chatAgent.messages, selectedWorkUnitId),
|
||||
statusLabel: STATUS_COPY[chatAgent.status],
|
||||
statusTone: getStatusTone(chatAgent.status),
|
||||
};
|
||||
@@ -88,7 +120,7 @@ export const useMobileProjectWorkspace = ({
|
||||
raiseIssue: projectWorkspace.raiseIssue,
|
||||
raiseIssueFromSignal: (signalId: string) =>
|
||||
projectWorkspace.raiseIssueFromSignal(signalId as Id<"signals">),
|
||||
sendMessage: chatAgent.sendMessage,
|
||||
sendMessage,
|
||||
startWorkUnit: projectWorkspace.startWorkUnit,
|
||||
} as const;
|
||||
};
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
import type { MobileWorkUnitView } from "@code/ui/components/mobile-product";
|
||||
import { useCallback, useMemo, useState } from "react";
|
||||
|
||||
export const useMobileWorkStack = (
|
||||
workUnits: readonly MobileWorkUnitView[]
|
||||
) => {
|
||||
const [order, setOrder] = useState<readonly string[]>([]);
|
||||
const [checkedIds, setCheckedIds] = useState<ReadonlySet<string>>(
|
||||
() => new Set()
|
||||
);
|
||||
|
||||
const workUnitsById = useMemo(
|
||||
() => new Map(workUnits.map((workUnit) => [workUnit.id, workUnit])),
|
||||
[workUnits]
|
||||
);
|
||||
const availableIds = useMemo(
|
||||
() => new Set(workUnitsById.keys()),
|
||||
[workUnitsById]
|
||||
);
|
||||
const visibleWorkUnits = useMemo(() => {
|
||||
const retainedOrder = order.filter(
|
||||
(workUnitId) =>
|
||||
availableIds.has(workUnitId) && !checkedIds.has(workUnitId)
|
||||
);
|
||||
const retainedIds = new Set(retainedOrder);
|
||||
const addedIds = workUnits
|
||||
.map((workUnit) => workUnit.id)
|
||||
.filter(
|
||||
(workUnitId) =>
|
||||
!retainedIds.has(workUnitId) && !checkedIds.has(workUnitId)
|
||||
);
|
||||
return [...retainedOrder, ...addedIds].flatMap((workUnitId) => {
|
||||
const workUnit = workUnitsById.get(workUnitId);
|
||||
return workUnit ? [workUnit] : [];
|
||||
});
|
||||
}, [availableIds, checkedIds, order, workUnits, workUnitsById]);
|
||||
|
||||
const sendToBack = useCallback(
|
||||
(workUnitId: string) => {
|
||||
if (visibleWorkUnits[0]?.id !== workUnitId) {
|
||||
return;
|
||||
}
|
||||
setOrder([...visibleWorkUnits.slice(1).map(({ id }) => id), workUnitId]);
|
||||
},
|
||||
[visibleWorkUnits]
|
||||
);
|
||||
|
||||
const markChecked = useCallback((workUnitId: string) => {
|
||||
setCheckedIds((current) => new Set(current).add(workUnitId));
|
||||
}, []);
|
||||
|
||||
const restoreChecked = useCallback(() => {
|
||||
setCheckedIds(new Set());
|
||||
setOrder([]);
|
||||
}, []);
|
||||
|
||||
return {
|
||||
checkedCount: [...checkedIds].filter((workUnitId) =>
|
||||
availableIds.has(workUnitId)
|
||||
).length,
|
||||
markChecked,
|
||||
restoreChecked,
|
||||
sendToBack,
|
||||
visibleWorkUnits,
|
||||
} as const;
|
||||
};
|
||||
Reference in New Issue
Block a user