Skip to content

Commit 7f4bde6

Browse files
committed
feat: echo job-specific output promise in action session loading state
1 parent b35e6cb commit 7f4bde6

File tree

6 files changed

+217
-1
lines changed

6 files changed

+217
-1
lines changed
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import assert from "node:assert/strict";
2+
import test from "node:test";
3+
import { readFileSync } from "node:fs";
4+
import { resolve } from "node:path";
5+
6+
const src = readFileSync(
7+
resolve(import.meta.dirname, "ActionSessionProgress.tsx"),
8+
"utf-8",
9+
);
10+
11+
// ---------------------------------------------------------------------------
12+
// ActionSessionProgress: accepts actionPromise and actionOutputType props
13+
// ---------------------------------------------------------------------------
14+
15+
void test("ActionSessionProgress accepts actionPromise prop", () => {
16+
assert.ok(
17+
src.includes("actionPromise"),
18+
"Expected actionPromise prop in the component interface",
19+
);
20+
});
21+
22+
void test("ActionSessionProgress accepts actionOutputType prop", () => {
23+
assert.ok(
24+
src.includes("actionOutputType"),
25+
"Expected actionOutputType prop in the component interface",
26+
);
27+
});
28+
29+
// ---------------------------------------------------------------------------
30+
// Starting state: shows job-specific loading text when actionOutputType is set
31+
// ---------------------------------------------------------------------------
32+
33+
void test("Starting state shows job-specific preparing message when actionOutputType is provided", () => {
34+
assert.ok(
35+
src.includes("Preparing your"),
36+
"Expected 'Preparing your' prefix for job-specific loading message",
37+
);
38+
assert.ok(
39+
src.includes("actionOutputType"),
40+
"Expected actionOutputType used in the loading message",
41+
);
42+
});
43+
44+
// ---------------------------------------------------------------------------
45+
// Starting state: shows deliverables list when actionPromise is provided
46+
// ---------------------------------------------------------------------------
47+
48+
void test("Starting state shows deliverables from actionPromise", () => {
49+
assert.ok(
50+
src.includes("actionPromise"),
51+
"Expected actionPromise rendered as deliverables list",
52+
);
53+
});
54+
55+
// ---------------------------------------------------------------------------
56+
// Starting state: falls back to generic message when no job data
57+
// ---------------------------------------------------------------------------
58+
59+
void test("Starting state falls back to generic loading message", () => {
60+
assert.ok(
61+
src.includes("Analyzing your company and preparing outputs"),
62+
"Expected generic fallback message preserved",
63+
);
64+
});
65+
66+
// ---------------------------------------------------------------------------
67+
// Starting state: preserves time estimate
68+
// ---------------------------------------------------------------------------
69+
70+
void test("Starting state preserves time estimate", () => {
71+
assert.ok(
72+
src.includes("First output in"),
73+
"Expected time estimate preserved in starting state",
74+
);
75+
});

apps/desktop/src/features/action-session/components/ActionSessionProgress.tsx

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,10 @@ interface ActionSessionProgressProps {
55
state: ActionSessionState;
66
hasOutputs: boolean;
77
outputCount?: number;
8+
/** Output promise text from the job card, e.g. "Tagline, one-liner, launch post draft, checklist" */
9+
actionPromise?: string | undefined;
10+
/** Short deliverable label from the job card, e.g. "Launch copy bundle" */
11+
actionOutputType?: string | undefined;
812
}
913

1014
/** Indeterminate progress bar with shimmer animation */
@@ -38,6 +42,8 @@ export function ActionSessionProgress({
3842
state,
3943
hasOutputs,
4044
outputCount = 0,
45+
actionPromise,
46+
actionOutputType,
4147
}: ActionSessionProgressProps) {
4248
if (state === "ready-to-review" || state === "saved-to-board" || state === "done") {
4349
return null;
@@ -53,8 +59,15 @@ export function ActionSessionProgress({
5359
</div>
5460
<div className="flex-1 space-y-0.5">
5561
<p className="text-sm font-medium text-foreground">
56-
Analyzing your company and preparing outputs…
62+
{actionOutputType
63+
? `Preparing your ${actionOutputType.toLowerCase()}…`
64+
: "Analyzing your company and preparing outputs…"}
5765
</p>
66+
{actionPromise && (
67+
<p className="text-xs text-muted-foreground/70">
68+
{actionPromise}
69+
</p>
70+
)}
5871
<p className="text-xs text-muted-foreground">
5972
First output in ~30–90s
6073
</p>

apps/desktop/src/features/action-session/components/ActionSessionView.tsx

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import { ActionSessionFooter } from "./ActionSessionFooter";
2929
import {
3030
clearPersistedActionContext,
3131
readPersistedActionContext,
32+
readActionOutputPromise,
3233
} from "../lib/action-session-persistence";
3334
import { useAutoArtifacts } from "@/features/chat/hooks/useAutoArtifacts";
3435

@@ -76,6 +77,9 @@ export function ActionSessionView({
7677
const effectivePrompt = pendingActionPrompt ?? recovered?.prompt ?? null;
7778
const effectiveTitle = actionTitle ?? recovered?.title ?? "Action";
7879

80+
// Read output promise data from sessionStorage (persisted by useIntakeForm)
81+
const outputPromise = useMemo(() => readActionOutputPromise(), []);
82+
7983
// If there's absolutely no session ID (no props AND nothing persisted),
8084
// redirect to dashboard — we can't render an action session without one.
8185
useEffect(() => {
@@ -174,6 +178,8 @@ export function ActionSessionView({
174178
client={client}
175179
pendingActionPrompt={effectivePrompt}
176180
actionTitle={effectiveTitle}
181+
actionPromise={outputPromise?.promise}
182+
actionOutputType={outputPromise?.outputType}
177183
onPendingPromptConsumed={() => {
178184
clearPersistedActionContext();
179185
onPendingPromptConsumed?.();
@@ -197,6 +203,8 @@ function ActionSessionInner({
197203
client,
198204
pendingActionPrompt,
199205
actionTitle,
206+
actionPromise,
207+
actionOutputType,
200208
onPendingPromptConsumed,
201209
onViewChat,
202210
onBackToDashboard,
@@ -206,6 +214,8 @@ function ActionSessionInner({
206214
client: SidecarClient | null;
207215
pendingActionPrompt?: string | null | undefined;
208216
actionTitle: string;
217+
actionPromise?: string | undefined;
218+
actionOutputType?: string | undefined;
209219
onPendingPromptConsumed?: (() => void) | undefined;
210220
onViewChat: (sessionId: string) => void;
211221
onBackToDashboard: () => void;
@@ -419,6 +429,8 @@ function ActionSessionInner({
419429
state={sessionState}
420430
hasOutputs={outputs.length > 0}
421431
outputCount={outputs.length}
432+
actionPromise={actionPromise}
433+
actionOutputType={actionOutputType}
422434
/>
423435

424436
{/* Error banner when sendMessage fails */}
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import assert from "node:assert/strict";
2+
import test from "node:test";
3+
import { readFileSync } from "node:fs";
4+
import { resolve } from "node:path";
5+
6+
const src = readFileSync(
7+
resolve(import.meta.dirname, "action-session-persistence.ts"),
8+
"utf-8",
9+
);
10+
11+
// ---------------------------------------------------------------------------
12+
// Output promise persistence: sessionStorage keys exist
13+
// ---------------------------------------------------------------------------
14+
15+
void test("persistence module defines sessionStorage key for action promise", () => {
16+
assert.ok(
17+
src.includes("opengoat:actionPromise"),
18+
"Expected sessionStorage key for actionPromise",
19+
);
20+
});
21+
22+
void test("persistence module defines sessionStorage key for action output type", () => {
23+
assert.ok(
24+
src.includes("opengoat:actionOutputType"),
25+
"Expected sessionStorage key for actionOutputType",
26+
);
27+
});
28+
29+
// ---------------------------------------------------------------------------
30+
// persistActionOutputPromise: stores promise and outputType
31+
// ---------------------------------------------------------------------------
32+
33+
void test("persistActionOutputPromise function exists and stores both fields", () => {
34+
assert.ok(
35+
src.includes("export function persistActionOutputPromise"),
36+
"Expected persistActionOutputPromise export",
37+
);
38+
// Should accept promise and outputType parameters
39+
assert.ok(
40+
src.includes("promise: string"),
41+
"Expected promise parameter of type string",
42+
);
43+
assert.ok(
44+
src.includes("outputType: string"),
45+
"Expected outputType parameter of type string",
46+
);
47+
});
48+
49+
// ---------------------------------------------------------------------------
50+
// readActionOutputPromise: reads promise and outputType
51+
// ---------------------------------------------------------------------------
52+
53+
void test("readActionOutputPromise function exists and returns both fields", () => {
54+
assert.ok(
55+
src.includes("export function readActionOutputPromise"),
56+
"Expected readActionOutputPromise export",
57+
);
58+
});
59+
60+
// ---------------------------------------------------------------------------
61+
// clearPersistedActionContext: also clears output promise keys
62+
// ---------------------------------------------------------------------------
63+
64+
void test("clearPersistedActionContext removes output promise keys", () => {
65+
assert.ok(
66+
src.includes("SS_ACTION_PROMISE"),
67+
"Expected SS_ACTION_PROMISE constant used in clear",
68+
);
69+
assert.ok(
70+
src.includes("SS_ACTION_OUTPUT_TYPE"),
71+
"Expected SS_ACTION_OUTPUT_TYPE constant used in clear",
72+
);
73+
});

apps/desktop/src/features/action-session/lib/action-session-persistence.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212
const SS_PENDING_PROMPT = "opengoat:pendingActionPrompt";
1313
const SS_ACTION_SESSION_ID = "opengoat:actionSessionId";
1414
const SS_ACTION_TITLE = "opengoat:actionTitle";
15+
const SS_ACTION_PROMISE = "opengoat:actionPromise";
16+
const SS_ACTION_OUTPUT_TYPE = "opengoat:actionOutputType";
1517

1618
/** Persist action context so it survives HMR / page reloads. */
1719
export function persistActionContext(
@@ -34,6 +36,8 @@ export function clearPersistedActionContext(): void {
3436
sessionStorage.removeItem(SS_PENDING_PROMPT);
3537
sessionStorage.removeItem(SS_ACTION_SESSION_ID);
3638
sessionStorage.removeItem(SS_ACTION_TITLE);
39+
sessionStorage.removeItem(SS_ACTION_PROMISE);
40+
sessionStorage.removeItem(SS_ACTION_OUTPUT_TYPE);
3741
} catch {
3842
// best-effort
3943
}
@@ -57,3 +61,33 @@ export function readPersistedActionContext(): {
5761
}
5862
return null;
5963
}
64+
65+
/** Persist the output promise from the action card so the loading state can echo it. */
66+
export function persistActionOutputPromise(
67+
promise: string,
68+
outputType: string,
69+
): void {
70+
try {
71+
sessionStorage.setItem(SS_ACTION_PROMISE, promise);
72+
sessionStorage.setItem(SS_ACTION_OUTPUT_TYPE, outputType);
73+
} catch {
74+
// best-effort
75+
}
76+
}
77+
78+
/** Read persisted output promise (returns null if nothing stored). */
79+
export function readActionOutputPromise(): {
80+
promise: string;
81+
outputType: string;
82+
} | null {
83+
try {
84+
const promise = sessionStorage.getItem(SS_ACTION_PROMISE);
85+
const outputType = sessionStorage.getItem(SS_ACTION_OUTPUT_TYPE);
86+
if (promise && outputType) {
87+
return { promise, outputType };
88+
}
89+
} catch {
90+
// best-effort
91+
}
92+
return null;
93+
}

apps/desktop/src/features/dashboard/hooks/useIntakeForm.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import type { ActionCard } from "@/features/dashboard/data/actions";
33
import { starterActions } from "@/features/dashboard/data/actions";
44
import { getIntakeFields, type IntakeFieldSet } from "@/features/dashboard/data/intake-fields";
55
import { buildActionPromptWithIntake } from "@/features/dashboard/data/prompt-builder";
6+
import { persistActionOutputPromise } from "@/features/action-session/lib/action-session-persistence";
67

78
export interface UseIntakeFormOptions {
89
suggestedActions: ActionCard[];
@@ -37,6 +38,9 @@ export function useIntakeForm({
3738
if (!pendingIntakeAction) return;
3839
const enrichedPrompt = buildActionPromptWithIntake(pendingIntakeAction, values);
3940
closeIntakeForm();
41+
if (pendingIntakeAction.promise && pendingIntakeAction.outputType) {
42+
persistActionOutputPromise(pendingIntakeAction.promise, pendingIntakeAction.outputType);
43+
}
4044
void onSubmitAction(pendingIntakeAction.id, enrichedPrompt, pendingIntakeAction.title);
4145
},
4246
[pendingIntakeAction, onSubmitAction, closeIntakeForm],
@@ -58,6 +62,11 @@ export function useIntakeForm({
5862
}
5963
}
6064

65+
// Persist output promise for the action session loading state
66+
if (card?.promise && card?.outputType) {
67+
persistActionOutputPromise(card.promise, card.outputType);
68+
}
69+
6170
void onSubmitAction(actionId, prompt, label);
6271
},
6372
[suggestedActions, onSubmitAction],

0 commit comments

Comments
 (0)