diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 0130f416..dea9530c 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -13,7 +13,7 @@ Add exactly one matching GitHub label before merge. ## Release Notes -- Write the changelog line you want if the PR title is not good enough. +- Write a single sentence or bullet to use in the automated changelog if the PR title is not good enough. ## Validation diff --git a/.github/workflows/prepare-release.yml b/.github/workflows/prepare-release.yml index 4d34f973..7fbc93c1 100644 --- a/.github/workflows/prepare-release.yml +++ b/.github/workflows/prepare-release.yml @@ -1,6 +1,9 @@ name: Prepare Release on: + push: + branches: + - main workflow_dispatch: concurrency: @@ -13,6 +16,7 @@ permissions: jobs: prepare: + if: github.event_name != 'push' || !startsWith(github.event.head_commit.message, 'chore(release): ') runs-on: ubuntu-latest timeout-minutes: 20 steps: diff --git a/scripts/release/prepare-release.mjs b/scripts/release/prepare-release.mjs index d8fca2d8..2350f2c0 100644 --- a/scripts/release/prepare-release.mjs +++ b/scripts/release/prepare-release.mjs @@ -117,6 +117,37 @@ function groupPullRequestsByLabel(pullRequests) { return groups; } +function extractReleaseNotesOverride(body) { + if (typeof body !== "string" || !body.trim()) { + return null; + } + + const match = body.match( + /(^|\n)##\s+Release Notes\s*\n([\s\S]*?)(?=\n##\s+|\n#\s+|$)/i, + ); + + if (!match?.[2]) { + return null; + } + + const lines = match[2] + .split(/\r?\n/) + .map((line) => line.trim()) + .filter(Boolean) + .map((line) => line.replace(/^[-*]\s+/, "").trim()) + .filter( + (line) => + line && + line !== "Write the changelog line you want if the PR title is not good enough.", + ); + + if (lines.length === 0) { + return null; + } + + return lines.join(" "); +} + function formatChangelogSection({ pullRequests, repositoryUrl, version }) { const date = new Date().toISOString().slice(0, 10); const groups = groupPullRequestsByLabel(pullRequests); @@ -138,7 +169,7 @@ function formatChangelogSection({ pullRequests, repositoryUrl, version }) { for (const pullRequest of entries) { sections.push( - `- ${pullRequest.title} ([#${pullRequest.number}](${repositoryUrl}/pull/${pullRequest.number}))`, + `- ${pullRequest.changelogLine ?? pullRequest.title} ([#${pullRequest.number}](${repositoryUrl}/pull/${pullRequest.number}))`, ); } @@ -295,6 +326,8 @@ for (const pullRequest of uniquePullRequests) { } pullRequest.releaseLabel = releaseLabels[0]; + pullRequest.changelogLine = + extractReleaseNotesOverride(pullRequest.body) ?? pullRequest.title; } if (invalidPullRequests.length > 0) { diff --git a/src/app/(app)/settings/models/page.tsx b/src/app/(app)/settings/models/page.tsx index f10400a7..03fdbcc2 100644 --- a/src/app/(app)/settings/models/page.tsx +++ b/src/app/(app)/settings/models/page.tsx @@ -87,7 +87,15 @@ function ModelsSkeleton() { export default function ModelsPage() { const { data: models, isPending } = api.models.list.useQuery(); + const enginesQuery = api.engines.list.useQuery(); const utils = api.useUtils(); + const codexEngine = enginesQuery.data?.find( + (engine) => engine.engine === "codex", + ); + const codexStatus = + codexEngine?.engine === "codex" && "status" in codexEngine + ? codexEngine.status + : null; const enable = api.models.enable.useMutation({ onMutate: async ({ modelId, provider }) => { @@ -276,6 +284,140 @@ export default function ModelsPage() { ) : null}
+ Sentinel reads the local Codex CLI state from this machine. No + Codex credentials are stored here. +
++ {codexEngine.error} +
+ ) : null} ++ {model.description} +
+{skill.description} @@ -213,13 +262,17 @@ function InstalledSkillRow({
Install a recommended skill below, or add one under{" "}
- .sentinel/skills.
+ .sentinel/skills{" "}
+ or ~/.codex/skills
+ .
- You have installed every skill from the curated registry. + You have installed every skill from the curated registry for + both runtimes.
{attachmentWarning}
-+ Drop files to attach +
++ {fileInput.reason} +
+ ) : null} ++ {imagePath} +
+
+ {text}
+
+
+ {resultText}
+
+ Input
+
+ {renderJson(part.input)}
+
+ Output
+
+ {renderJson(part.output)}
+
+ + {shellInput.reason} +
+ )} +
+ {input.review}
+
+ {input.prompt}
+ )} +{input.prompt}
++ Query: {action.query} +
+ {action.queries && action.queries.length > 1 && ( +Related queries:
++ Opened{" "} + + {action.url} + +
++ Searched page for{" "} + {action.pattern} +
+ ) : null; + case "other": + default: + return null; + } +} + +function buildSummary( + input: CodexWebSearchInput, + output: CodexWebSearchOutput | null, + flags: { + isDenied: boolean; + isDone: boolean; + isError: boolean; + isRunning: boolean; + }, +) { + if (flags.isDenied) return "Search denied"; + if (flags.isError) return "Search failed"; + + if (output?.action.type === "openPage" && output.action.url) { + const urlLabel = (() => { + try { + return new URL(output.action.url).hostname; + } catch { + return output.action.url; + } + })(); + if (flags.isDone) return `Opened ${urlLabel}`; + return `Opening ${urlLabel}`; + } + + if (output?.action.type === "findInPage") { + if (flags.isDone) return `Found in page`; + return `Finding in page`; + } + + const queryLabel = input.query ? `\u201c${input.query}\u201d` : "the web"; + if (flags.isDone) return `Searched for ${queryLabel}`; + if (flags.isRunning) return `Searching for ${queryLabel}`; + return `Searching for ${queryLabel}`; +} + +export const CodexWebSearchTool = memo(function CodexWebSearchTool({ + part, +}: RendererProps) { + const isRunning = + part.state === "approval-responded" || + part.state === "input-streaming" || + part.state === "input-available"; + const isDenied = part.state === "output-denied"; + const isError = part.state === "output-error" || isDenied; + const isDone = part.state === "output-available"; + + const input = + "input" in part && isWebSearchInput(part.input) ? part.input : null; + const output = + "output" in part && isWebSearchOutput(part.output) ? part.output : null; + + if (!input) return null; + + const hasDetails = + output?.action && + output.action.type !== "other" && + (output.action.type !== "openPage" || output.action.url != null); + const [isExpanded, setIsExpanded] = useState(false); + + const summary = ( + <> +- {chatError} + {visibleChatError}
- {chatError} + {visibleChatError}