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
7 changes: 4 additions & 3 deletions kits/workplace/app/src/App.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ function Workplace() {
const [bootErr, setBootErr] = useState('');
const [teammates, setTeammates] = useState(null); // null = loading
const [cards, setCards] = useState([]);
const [cardsAt, setCardsAt] = useState(0); // when the list was first read: a direct message is found in it, so nothing is sent before
const [docs, setDocs] = useState({}); // group session id -> document
const docsAt = useRef({});
const [liveLocal, setLiveLocal] = useState({}); // teammate id -> true while this tab runs its turn
Expand All @@ -108,7 +109,7 @@ function Workplace() {
const harnessIds = useMemo(() => [rec?.id, ...(teammates || []).map((t) => t.id)].filter(Boolean), [rec, teammates]);
const refreshCards = useCallback(async () => {
if (!me || !harnessIds.length) return;
try { setCards(await listCards(me, harnessIds)); } catch { /* keep the last list */ }
try { setCards(await listCards(me, harnessIds)); setCardsAt((t) => t || Date.now()); } catch { /* keep the last list */ }
}, [me, harnessIds]);

const ready = !!rec && !!me;
Expand Down Expand Up @@ -150,9 +151,9 @@ function Workplace() {
const unread = useCallback((key, marker) => !!marker && seen[key] !== undefined && seen[key] !== marker, [seen]);

const value = useMemo(() => ({
route, navigate, rec, me, teammates, refreshTeammates, cards, refreshCards, groupCards, docs, setDoc,
route, navigate, rec, me, teammates, refreshTeammates, cards, cardsLoaded: cardsAt > 0, refreshCards, groupCards, docs, setDoc,
working, markLive, markSeen, unread, openDrawer: () => setDrawer(true), closeDrawer: () => setDrawer(false), tabId: TAB_ID,
}), [route, rec, me, teammates, refreshTeammates, cards, refreshCards, groupCards, docs, setDoc, working, markLive, markSeen, unread]);
}), [route, rec, me, teammates, refreshTeammates, cards, cardsAt, refreshCards, groupCards, docs, setDoc, working, markLive, markSeen, unread]);

useEffect(() => { setDrawer(false); }, [route.kind, route.id]);

Expand Down
10 changes: 7 additions & 3 deletions kits/workplace/app/src/components/DmView.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ function turnFiles(t, sid) {

export default function DmView({ teammate }) {
const wp = useWorkplace();
const { me, cards, teammates, navigate, markLive, markSeen, refreshCards, refreshTeammates, openDrawer } = wp;
const { me, cards, cardsLoaded, teammates, navigate, markLive, markSeen, refreshCards, refreshTeammates, openDrawer } = wp;
const dialog = useDialog();
const card = findDm(cards, teammate.id, me);
const [sid, setSid] = useState(card?.id || '');
Expand All @@ -60,7 +60,11 @@ export default function DmView({ teammate }) {

useEffect(() => { if (!sid && card?.id) setSid(card.id); }, [card?.id, sid]);
const externalBusy = (!!(card && isLive(card)) || !!extLive) && !live;
const busy = !!live || externalBusy;
// Nothing is sent before the card list has been read once: the direct message is found in that
// list, and a message sent before it arrived opened a second conversation with the same teammate
// (hosted, where the list lands later, 2026-10-01).
const finding = !sid && !cardsLoaded;
const busy = !!live || externalBusy || finding;

const load = useCallback(async () => {
if (!sid) { setTurns([]); return; }
Expand Down Expand Up @@ -198,7 +202,7 @@ export default function DmView({ teammate }) {
</div>

<Composer placeholder={`Message ${teammate.name}`} disabled={busy} onSend={send} autoFocus
hint={busy ? `${teammate.name} is working on your last message.` : ''} />
hint={finding ? 'Opening your conversation…' : busy ? `${teammate.name} is working on your last message.` : ''} />
</div>
{files.pane}
<Rail open={railOpen} onClose={() => setRailOpen(false)} teammate={teammate} sessions={sid ? [sid] : []} title={teammate.name}
Expand Down
8 changes: 7 additions & 1 deletion kits/workplace/app/src/lib/dm.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import { patchSession, sessionTurns, streamTurn, turnsToMessages } from 'reifyui/harness';
import { DM_TITLE } from './kit.js';
import { mine } from './auth.js';
import { turnInput } from './api.js';
import { listCards, turnInput } from './api.js';

export const dmTitle = (me) => `${DM_TITLE} · ${me?.name || 'me'}`;

Expand All @@ -31,6 +31,12 @@ export async function loadDm(sid) {
* and not the finish; a follow-up turn renamed the session again). The name is how the direct
* message is found next time, so it is put back when the turn is over. */
export async function sendDm({ sid, bot, me, text, files = [], handlers = {} }) {
// Before opening a new conversation, look once more for the existing one: the caller's list may
// be older than the conversation (another window opened it a moment ago).
if (!sid) {
const found = findDm(await listCards(me, [bot.id]).catch(() => []), bot.id, me);
if (found) { sid = found.id; handlers.onSession?.(sid); }
}
let session = sid || '';
const r = await streamTurn({
sessionId: sid, harnessId: bot.id, input: turnInput(text, files), instructions: dmInstructions(bot, me),
Expand Down
Loading