Skip to content

Commit 0115347

Browse files
committed
fix: complete browser work feedback loops
Keep feed-level instructions cancellable and visible through completion, and give web editors and shortcut actions clear accessible names.
1 parent 7576e3e commit 0115347

6 files changed

Lines changed: 75 additions & 19 deletions

File tree

src/App.tsx

Lines changed: 40 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,15 @@ import { api, post } from "./app/api";
55
import type { AttentionScreen, Inspector, Tab, WorkspaceTab } from "./app/types";
66
import { CardView } from "./feed/CardView";
77
import { RoutineActionGroupView } from "./feed/RoutineActionGroupView";
8-
import { countFor, visibleCardActions, visibleCards, visibleRoutineActions } from "./feed/selectors";
8+
import { countFor, visibleCardActions, visibleCards, visibleFeedWork, visibleRoutineActions } from "./feed/selectors";
99
import { Dock } from "./shell/Dock";
1010
import { InspectorPanel } from "./shell/InspectorPanel";
1111
import { TopBar } from "./shell/TopBar";
1212
import { useActiveCard } from "./state/activeCard";
1313
import { RealtimeProvider } from "./state/realtime";
1414
import { preferredTarget, sameTarget } from "./state/voiceTarget";
1515
import type { Card, CardAction, RevisionProposal, RoutineActionGroup, VoiceTarget, WorkItem, WorkspaceRevision, WorkspaceView } from "./types";
16+
import { FormattedText } from "./ui/FormattedText";
1617
import { LearningReview, RevisionProposals } from "./workspace/LearningReview";
1718
import { PromptWorkspace } from "./workspace/PromptWorkspace";
1819

@@ -344,7 +345,7 @@ export default function App({ feedId, screen, workspaceTab }: { feedId: string;
344345

345346
const updated = cards.filter((card) => card.status === "to_review_updated");
346347
const fresh = cards.filter((card) => card.status !== "to_review_updated");
347-
const feedWork = feed.work.filter((work) => work.cardId === "__feed__" && work.status === tab);
348+
const feedWork = visibleFeedWork(feed, tab);
348349
return withRealtime(
349350
<>
350351
<TopBar state={state} onMind={openMind} onFeed={changeFeed} onInspector={setInspector} onWorkspace={openWorkspace} />
@@ -374,7 +375,43 @@ export default function App({ feedId, screen, workspaceTab }: { feedId: string;
374375
<span className="kind-dot proposal" />
375376
<div><div className="eyebrow">Feed instruction · {work.status}</div><h2>{work.instruction}</h2></div>
376377
</header>
377-
<p className="why">{work.status === "queued" ? "Ready for the home Codex thread to drain." : "The home Codex thread is working through this feed-level instruction."}</p>
378+
<p className="why">
379+
{work.status === "queued"
380+
? "Ready for the home Codex thread to drain."
381+
: work.status === "working"
382+
? "The home Codex thread is working through this feed-level instruction."
383+
: "The home Codex thread completed this feed-level instruction."}
384+
</p>
385+
{work.status === "completed" && work.response && (
386+
<div className="blocks">
387+
<section className="block block-rich_text">
388+
<h3>Codex response</h3>
389+
<p><FormattedText text={work.response} /></p>
390+
</section>
391+
</div>
392+
)}
393+
{work.status === "queued" && (
394+
<footer className="card-action">
395+
<div>
396+
<span className="action-label">Queued for Codex</span>
397+
<b>Waiting for the feed thread</b>
398+
</div>
399+
<div className="action-buttons">
400+
<button className="button ghost" onClick={() => void withRefresh(
401+
() => post(`/api/feeds/${work.feedId}/work/${work.id}/cancel`),
402+
"Instruction cancelled",
403+
)}>Cancel instruction</button>
404+
</div>
405+
</footer>
406+
)}
407+
{work.status === "completed" && (
408+
<footer className="card-action">
409+
<div>
410+
<span className="action-label">Done</span>
411+
<b>Completed</b>
412+
</div>
413+
</footer>
414+
)}
378415
</article>
379416
))}
380417
{!cards.length && !routineActions.length && !feedWork.length && <div className="empty"><h2>Nothing here right now.</h2><p>{tab === "review" ? "A quiet feed is allowed. Wake the feed thread when you want Codex to collect or drain pending work." : "Move back to To review when you are ready for the next pass."}</p></div>}

src/feed/CardView.tsx

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,14 @@ function Block({ feedId, cardId, block, onChanged }: { feedId: string; cardId: s
9494
return (
9595
<section className="block block-editor">
9696
{block.label && <h3>{block.label}</h3>}
97-
<textarea data-block-id={block.id} value={value} onChange={(event) => setValue(event.target.value)} onBlur={() => void save()} rows={Math.max(4, value.split("\n").length + 1)} />
97+
<textarea
98+
aria-label={block.label ?? "Editable card content"}
99+
data-block-id={block.id}
100+
value={value}
101+
onChange={(event) => setValue(event.target.value)}
102+
onBlur={() => void save()}
103+
rows={Math.max(4, value.split("\n").length + 1)}
104+
/>
98105
</section>
99106
);
100107
}
@@ -228,7 +235,7 @@ function QueuedNoteEditor({ work, onChanged }: { work: WorkItem; onChanged: () =
228235
return (
229236
<section className="queued-note">
230237
<span className="action-label">Queued note</span>
231-
<textarea value={value} onChange={(event) => setValue(event.target.value)} onBlur={() => void save()} rows={Math.max(2, value.split("\n").length)} />
238+
<textarea aria-label="Queued note" value={value} onChange={(event) => setValue(event.target.value)} onBlur={() => void save()} rows={Math.max(2, value.split("\n").length)} />
232239
<small>{saving ? "Saving..." : "Edit before Codex claims it."}</small>
233240
</section>
234241
);
@@ -313,12 +320,14 @@ export function CardView({
313320
<div className="action-buttons">
314321
{actions.map((action) => (
315322
<button
323+
aria-keyshortcuts={action.shortcut}
324+
aria-label={action.label}
316325
className={`button ${action.variant === "primary" ? "primary" : "ghost"}`}
317326
key={action.id}
318327
onPointerDown={(event) => event.preventDefault()}
319328
onClick={(event) => { event.stopPropagation(); onAction(action); }}
320329
>
321-
{action.label}{action.shortcut && <kbd>{action.shortcut.toUpperCase()}</kbd>}
330+
{action.label}{action.shortcut && <kbd aria-hidden="true">{action.shortcut.toUpperCase()}</kbd>}
322331
</button>
323332
))}
324333
</div>

src/feed/selectors.ts

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import type { Tab } from "../app/types";
2-
import type { Card, CardAction, FeedView, RoutineActionGroup } from "../types";
2+
import type { Card, CardAction, FeedView, RoutineActionGroup, WorkItem } from "../types";
33

44
export function visibleCards(feed: FeedView, tab: Tab): Card[] {
55
const pass = feed.config.currentPass;
@@ -22,11 +22,14 @@ export function visibleRoutineActions(feed: FeedView, tab: Tab): RoutineActionGr
2222
return feed.routineActions.filter((group) => group.status === status);
2323
}
2424

25+
export function visibleFeedWork(feed: FeedView, tab: Tab): WorkItem[] {
26+
if (tab === "review") return [];
27+
const status = tab === "done" ? "completed" : tab;
28+
return feed.work.filter((work) => work.cardId === "__feed__" && work.status === status);
29+
}
30+
2531
export function countFor(feed: FeedView, tab: Tab): number {
26-
const feedWork = tab === "queued" || tab === "working"
27-
? feed.work.filter((work) => work.cardId === "__feed__" && work.status === tab).length
28-
: 0;
29-
return visibleCards(feed, tab).length + visibleRoutineActions(feed, tab).length + feedWork;
32+
return visibleCards(feed, tab).length + visibleRoutineActions(feed, tab).length + visibleFeedWork(feed, tab).length;
3033
}
3134

3235
export function visibleCardActions(card: Card): CardAction[] {

src/shell/Dock.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -79,8 +79,8 @@ export function Dock({
7979
</div>
8080
</div>
8181
<div className="dock-row">
82-
<textarea ref={inputRef} value={value} onChange={(event) => setValue(event.target.value)} onKeyDown={onDockKeyDown} rows={1} placeholder="Tell Codex what to notice, change, or do…" />
83-
<button className="button primary" type="submit">Send</button>
82+
<textarea aria-label="Instruction for Codex" ref={inputRef} value={value} onChange={(event) => setValue(event.target.value)} onKeyDown={onDockKeyDown} rows={1} placeholder="Tell Codex what to notice, change, or do…" />
83+
<button className="button primary" type="submit" disabled={!value.trim()}>Send</button>
8484
</div>
8585
<div className="dock-footer">
8686
<div className="dock-hints"><kbd></kbd>/<kbd></kbd> change scope when empty · hold <kbd>{state.dictation.activationLabel}</kbd> to dictate · <kbd>Enter</kbd> send</div>

src/shell/InspectorPanel.tsx

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,12 +27,19 @@ export function InspectorPanel({ value, state, onClose, onChanged }: { value: In
2727
return (
2828
<div className="overlay" onMouseDown={onClose}>
2929
<section className="inspector setup-panel" onMouseDown={(event) => event.stopPropagation()}>
30-
<button className="close" onClick={onClose}>×</button>
30+
<button aria-label="Close setup panel" className="close" onClick={onClose}>×</button>
3131
<div className="panel-kicker">{create ? "New feed" : "New source"}</div>
3232
<h2>{create ? "What should this feed notice?" : "What else should this feed pay attention to?"}</h2>
3333
<p>Describe it naturally. Codex can refine the recipe with you in the feed thread.</p>
34-
<textarea autoFocus rows={8} value={text} onChange={(event) => setText(event.target.value)} placeholder={create ? "Track the models I am actually using and show me meaningful changes in where each one is winning…" : "Also look at the product planning Slack channel and pull in decisions or unresolved questions that affect Q3…"} />
35-
<button className="button primary large" onClick={() => void submit()}>{create ? "Create feed" : "Add source"}</button>
34+
<textarea
35+
aria-label={create ? "Feed brief" : "Source brief"}
36+
autoFocus
37+
rows={8}
38+
value={text}
39+
onChange={(event) => setText(event.target.value)}
40+
placeholder={create ? "Track the models I am actually using and show me meaningful changes in where each one is winning…" : "Also look at the product planning Slack channel and pull in decisions or unresolved questions that affect Q3…"}
41+
/>
42+
<button className="button primary large" disabled={!text.trim()} onClick={() => void submit()}>{create ? "Create feed" : "Add source"}</button>
3643
</section>
3744
</div>
3845
);

src/workspace/PromptWorkspace.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ function WorkspaceEditor({
2626
<div className="workspace-editor-head">
2727
<h3>{label}</h3>
2828
<div className="workspace-editor-actions">
29-
{undoRevision && <button className="button text" onClick={() => void (async () => {
29+
{undoRevision && <button aria-label={`Undo last save for ${label}`} className="button text" onClick={() => void (async () => {
3030
setSaving(true);
3131
try {
3232
await onUndo(undoRevision);
@@ -37,7 +37,7 @@ function WorkspaceEditor({
3737
setSaving(false);
3838
}
3939
})()}>Undo last save</button>}
40-
<button className="button ghost" disabled={!changed || saving} onClick={() => void (async () => {
40+
<button aria-label={`Save ${label}`} className="button ghost" disabled={!changed || saving} onClick={() => void (async () => {
4141
setSaving(true);
4242
try {
4343
const revision = await onSave(value);
@@ -50,7 +50,7 @@ function WorkspaceEditor({
5050
})()}>{saving ? "Saving…" : "Save"}</button>
5151
</div>
5252
</div>
53-
<textarea value={value} onFocus={onFocus} onClick={onFocus} onChange={(event) => setValue(event.target.value)} rows={Math.max(7, Math.min(20, value.split("\n").length + 2))} />
53+
<textarea aria-label={label} value={value} onFocus={onFocus} onClick={onFocus} onChange={(event) => setValue(event.target.value)} rows={Math.max(7, Math.min(20, value.split("\n").length + 2))} />
5454
</section>
5555
);
5656
}

0 commit comments

Comments
 (0)