1- import type { EnvironmentId , RunId } from "@t3tools/contracts" ;
1+ import type { EnvironmentId , OrchestrationV2Run , RunId } from "@t3tools/contracts" ;
22import { useSyncExternalStore } from "react" ;
33
44import type { Thread } from "../../types" ;
55import { useMediaQuery } from "../../hooks/useMediaQuery" ;
66import { useServerConfigs } from "../../state/entities" ;
77import { orchestrationEnvironment } from "../../state/orchestration" ;
88import { useEnvironmentQuery } from "../../state/query" ;
9+ import { announcedRun } from "./threadActivityAnnouncement" ;
910
1011// Dismissed suggestions, keyed by environment and run, shared by every mounted composer and kept
1112// across remounts. Run ids derive from thread ids, which can repeat across environments.
@@ -18,13 +19,43 @@ const subscribeDismissed = (listener: () => void) => {
1819const readDismissed = ( ) => dismissedKeys ;
1920const dismissKey = ( environmentId : EnvironmentId , runId : RunId ) => `${ environmentId } \u0000${ runId } ` ;
2021
22+ /**
23+ * The suggestion while the run that produced it is the thread's latest completed run. A message
24+ * promoted to Steer leaves its queued run cancelled before it started, still the latest run, so
25+ * the comparison skips to the last run that ran. Detail can lag the shell, so it skips only once
26+ * the runs include that cancelled run.
27+ */
28+ export function offeredPromptSuggestion < Suggestion extends { readonly runId : RunId } > (
29+ suggestion : Suggestion | null | undefined ,
30+ latestRun : Thread [ "latestRun" ] | undefined ,
31+ runs : ReadonlyArray < OrchestrationV2Run > ,
32+ ) : Suggestion | null {
33+ const run = announcedRun ( latestRun ?? null , runs ) ;
34+ const skippedUnseenRun =
35+ latestRun != null &&
36+ run ?. runId !== latestRun . runId &&
37+ ! runs . some (
38+ ( candidate ) =>
39+ candidate . id === latestRun . runId &&
40+ candidate . status === "cancelled" &&
41+ candidate . startedAt === null ,
42+ ) ;
43+ return suggestion &&
44+ ! skippedUnseenRun &&
45+ run ?. runId === suggestion . runId &&
46+ run . status === "completed"
47+ ? suggestion
48+ : null ;
49+ }
50+
2151/**
2252 * The provider's predicted next prompt, while the run that produced it is the thread's latest
2353 * completed run and Escape has not dismissed it.
2454 */
2555export function usePromptSuggestion (
2656 environmentId : EnvironmentId ,
2757 thread : Thread | undefined ,
58+ runs : ReadonlyArray < OrchestrationV2Run > ,
2859) : { readonly suggestion : string | null ; readonly dismiss : ( ) => void } {
2960 // Older servers decode the missing setting as false, so true also proves the method exists.
3061 const enabled = useServerConfigs ( ) . get ( environmentId ) ?. settings . promptSuggestionsEnabled === true ;
@@ -40,9 +71,7 @@ export function usePromptSuggestion(
4071 : null ,
4172 ) ;
4273 const dismissed = useSyncExternalStore ( subscribeDismissed , readDismissed ) ;
43- const latestRun = thread ?. latestRun ;
44- const offered =
45- data && latestRun ?. runId === data . runId && latestRun . status === "completed" ? data : null ;
74+ const offered = offeredPromptSuggestion ( data , thread ?. latestRun , runs ) ;
4675 return {
4776 suggestion :
4877 offered && ! dismissed . has ( dismissKey ( environmentId , offered . runId ) ) ? offered . text : null ,
0 commit comments