Compaction fixes (observed in a real long run): - the summary message's zeroed tokens froze the goal counter at its pre-compaction value; segments now close with the previously displayed total as a continuity floor - audits and continuations after a summary tail now take execution params (provider/model/agent/variant) from the newest non-summary assistant turn instead of inheriting agent 'compaction' and the summarize model File-backed objectives: - the objective text lives in <data-dir>/goals/<sessionId>.md, keyed by session id (one goal per session, a new goal overwrites the file); metadata carries only an objectiveFile flag so session.updated fanout stays light, and never a path — ids are pattern-validated before any filesystem access - limit raised to 5000 chars, no snapshot field: the UI fetches content via PUT/GET/DELETE /api/goals/objective/:sessionId (behind the blanket /api auth gate), writes the file before stamping metadata, and falls back to an inline objective when the write fails - the loop reads the file fresh on every tick, so objectives are live-editable mid-goal; a missing file falls back to the inline text - scheduled goal tasks write the objective file server-side; VS Code degrades to the audit note (route unavailable there by design)
51 lines
1.9 KiB
TypeScript
51 lines
1.9 KiB
TypeScript
import React from 'react';
|
|
import { useSession } from '@/sync/sync-context';
|
|
import { getSessionGoal, type SessionGoalPayload } from '@/lib/sessionGoalMetadata';
|
|
import { fetchGoalObjectiveContent } from '@/lib/sessionGoalActions';
|
|
import { useUIStore } from '@/stores/useUIStore';
|
|
|
|
export interface SessionGoalState {
|
|
/** Parsed goal payload, or null when the session has no goal. */
|
|
goal: SessionGoalPayload | null;
|
|
/** The Settings → Chat toggle; when off, goal UI stays hidden. */
|
|
enabled: boolean;
|
|
}
|
|
|
|
// Live goal state: the payload rides session.updated, so subscribing to the
|
|
// session record is all the plumbing needed.
|
|
export function useSessionGoal(sessionId: string, directory?: string): SessionGoalState {
|
|
const session = useSession(sessionId, directory);
|
|
const enabled = useUIStore((state) => state.sessionGoalEnabled);
|
|
return {
|
|
goal: getSessionGoal(session),
|
|
enabled,
|
|
};
|
|
}
|
|
|
|
// Effective objective text for display. Inline goals return the metadata
|
|
// text directly; file-backed goals fetch the server-side file once per
|
|
// goal edit (keyed by id + updatedAt). Display-only: a failed fetch yields
|
|
// null and callers degrade gracefully (e.g. VS Code, where the OpenChamber
|
|
// route is unavailable — the strip then shows only the audit note).
|
|
export function useGoalObjectiveContent(sessionId: string, goal: SessionGoalPayload | null): string | null {
|
|
const [fetched, setFetched] = React.useState<string | null>(null);
|
|
const fetchKey = goal?.objectiveFile ? `${sessionId}:${goal.id}:${goal.updatedAt}` : '';
|
|
|
|
React.useEffect(() => {
|
|
if (!fetchKey) {
|
|
setFetched(null);
|
|
return undefined;
|
|
}
|
|
let alive = true;
|
|
void fetchGoalObjectiveContent(sessionId).then((content) => {
|
|
if (alive) setFetched(content);
|
|
});
|
|
return () => {
|
|
alive = false;
|
|
};
|
|
}, [fetchKey, sessionId]);
|
|
|
|
if (!goal) return null;
|
|
return goal.objectiveFile ? fetched : goal.objective;
|
|
}
|