Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .github/workflows/deploy-web.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ on:
description: "Git ref to build and deploy. Defaults to the caller's commit."
required: false
type: string
version:
description: "Application version to embed in the web build. Defaults to the version derived from the Git ref."
required: false
type: string
channel:
description: "Hosted app channel: latest deploys app.threadlines.dev, nightly deploys nightly.app.threadlines.dev."
required: false
Expand All @@ -18,6 +22,10 @@ on:
description: "Git ref (tag or sha) to build and deploy. Use a release tag so the hosted app version matches the desktop build."
required: true
type: string
version:
description: "Application version to embed in the web build. Defaults to the version derived from the Git ref."
required: false
type: string
channel:
description: "Hosted app channel: latest deploys app.threadlines.dev, nightly deploys nightly.app.threadlines.dev."
required: true
Expand Down Expand Up @@ -45,6 +53,7 @@ jobs:
VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }}
VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }}
VERCEL_PROJECT_ID: ${{ inputs.channel == 'nightly' && secrets.VERCEL_PROJECT_ID_NIGHTLY || secrets.VERCEL_PROJECT_ID }}
APP_VERSION: ${{ inputs.version }}
VITE_HOSTED_APP_CHANNEL: ${{ inputs.channel == 'nightly' && 'nightly' || 'latest' }}
VITE_HOSTED_APP_URL: ${{ inputs.channel == 'nightly' && 'https://nightly.app.threadlines.dev' || 'https://app.threadlines.dev' }}
HOSTED_APP_URL: ${{ inputs.channel == 'nightly' && 'https://nightly.app.threadlines.dev' || 'https://app.threadlines.dev' }}
Expand Down
14 changes: 2 additions & 12 deletions .github/workflows/prepare-stable-release-content.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ on:

permissions:
contents: write
models: read
pull-requests: write

concurrency:
Expand All @@ -33,9 +32,6 @@ jobs:
runs-on: ubuntu-24.04
timeout-minutes: 15
env:
GITHUB_TOKEN: ${{ github.token }}
RELEASE_MODELS_TOKEN: ${{ secrets.RELEASE_MODELS_TOKEN }}
GITHUB_RELEASE_SUMMARY_MODEL: ${{ vars.GITHUB_RELEASE_SUMMARY_MODEL }}
VERSION_INPUT: ${{ inputs.version }}
CURRENT_REF_INPUT: ${{ inputs.current_ref }}
RELEASE_DATE_INPUT: ${{ inputs.release_date }}
Expand All @@ -58,15 +54,9 @@ jobs:
- name: Install dependencies
run: vp install --frozen-lockfile

- name: Verify release drafting token
run: |
set -euo pipefail
if [[ -z "${RELEASE_MODELS_TOKEN:-}" ]]; then
echo "::error::The RELEASE_MODELS_TOKEN repository secret is unavailable. Add a fine-grained personal access token with Models: read permission before preparing stable release content."
exit 1
fi

- name: Generate stable release draft
env:
COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }}
run: |
set -euo pipefail

Expand Down
33 changes: 33 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -306,20 +306,52 @@ jobs:

- name: Generate release notes
env:
GH_TOKEN: ${{ github.token }}
CHANNEL: ${{ needs.preflight.outputs.channel }}
TAG: ${{ needs.preflight.outputs.tag }}
CURRENT_REF: ${{ needs.preflight.outputs.ref }}
REPOSITORY: ${{ github.repository }}
run: |
set -euo pipefail

previous_tag="$(
node scripts/generate-release-notes.ts \
--channel "$CHANNEL" \
--current-tag "$TAG" \
--print-baseline-tag
)"

api_args=(
--method POST
"repos/$REPOSITORY/releases/generate-notes"
--field tag_name="$TAG"
--field target_commitish="$CURRENT_REF"
)
if [[ -n "$previous_tag" ]]; then
api_args+=(--field previous_tag_name="$previous_tag")
fi

github_notes_file=""
if gh api "${api_args[@]}" --jq '.body' > github-generated-notes.md; then
if [[ -s github-generated-notes.md ]]; then
github_notes_file="github-generated-notes.md"
else
echo "::warning::GitHub generated an empty release-notes body; using local release-note metadata instead."
fi
else
echo "::warning::GitHub's generated release-notes API failed; using local release-note metadata instead."
fi

args=(
--channel "$CHANNEL"
--current-tag "$TAG"
--current-ref "$CURRENT_REF"
--repository "$REPOSITORY"
--output release-notes.md
)
if [[ -n "$github_notes_file" ]]; then
args+=(--github-notes-file "$github_notes_file")
fi

if [[ "$CHANNEL" == "stable" ]]; then
highlights_file="apps/marketing/src/content/changelog/${TAG}.md"
Expand Down Expand Up @@ -1039,6 +1071,7 @@ jobs:
uses: ./.github/workflows/deploy-web.yml
with:
ref: ${{ needs.preflight.outputs.ref }}
version: ${{ needs.preflight.outputs.version }}
channel: ${{ needs.preflight.outputs.channel == 'nightly' && 'nightly' || 'latest' }}
secrets: inherit

Expand Down
1 change: 1 addition & 0 deletions apps/marketing/src/content.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ const changelog = defineCollection({
}),
schema: z.object({
version: z.string().regex(/^\d+\.\d+\.\d+$/),
reviewRequired: z.literal(true).optional(),
date: z.coerce.date(),
title: z.string().min(1).max(90),
summary: z.string().min(1).max(280),
Expand Down
13 changes: 13 additions & 0 deletions apps/web/src/components/ChatView.logic.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { type ChatMessage, type Thread } from "../types";

import {
buildRevertConfirmView,
resolveRemoteBehindCount,
resolveWorkingTreeDiffStat,
MAX_HIDDEN_MOUNTED_TERMINAL_THREADS,
buildExpiredTerminalContextToastCopy,
Expand Down Expand Up @@ -2548,3 +2549,15 @@ describe("resolveWorkingTreeDiffStat", () => {
expect(resolveWorkingTreeDiffStat(null)).toBeNull();
});
});

describe("resolveRemoteBehindCount", () => {
it("reports the count only when a repository branch is behind", () => {
expect(resolveRemoteBehindCount({ isRepo: true, behindCount: 2 })).toBe(2);
});

it("stays quiet for an up-to-date branch, a non-repo, and an unloaded status", () => {
expect(resolveRemoteBehindCount({ isRepo: true, behindCount: 0 })).toBeNull();
expect(resolveRemoteBehindCount({ isRepo: false, behindCount: 3 })).toBeNull();
expect(resolveRemoteBehindCount(null)).toBeNull();
});
});
14 changes: 14 additions & 0 deletions apps/web/src/components/ChatView.logic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2029,3 +2029,17 @@ export function resolveWorkingTreeDiffStat(
const { insertions, deletions } = status.workingTree;
return insertions === 0 && deletions === 0 ? null : { insertions, deletions };
}

/**
* Commits the branch is behind its upstream, surfaced on a closed source
* control toggle as a pull-available hint, or null when there is nothing to
* pull. An unloaded or non-repo status must not read as "up to date".
*/
export function resolveRemoteBehindCount(
status: { readonly isRepo: boolean; readonly behindCount: number } | null,
): number | null {
if (status === null || !status.isRepo) {
return null;
}
return status.behindCount > 0 ? status.behindCount : null;
}
6 changes: 6 additions & 0 deletions apps/web/src/components/ChatView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -253,6 +253,7 @@ import {
waitForStartedServerThread,
mergeLocalDraftThreadWithServerThread,
buildRevertConfirmView,
resolveRemoteBehindCount,
resolveWorkingTreeDiffStat,
type RevertConfirmView,
} from "./ChatView.logic";
Expand Down Expand Up @@ -2557,6 +2558,10 @@ export default function ChatView(props: ChatViewProps) {
() => resolveWorkingTreeDiffStat(gitStatusQuery.data ?? null),
[gitStatusQuery.data],
);
const remoteBehindCount = useMemo(
() => resolveRemoteBehindCount(gitStatusQuery.data ?? null),
[gitStatusQuery.data],
);
const terminalShortcutLabelOptions = useMemo(
() => ({
context: {
Expand Down Expand Up @@ -6123,6 +6128,7 @@ export default function ChatView(props: ChatViewProps) {
browserOpen={browserOpen}
onToggleBrowser={handleToggleBrowser}
workingTreeDiffStat={workingTreeDiffStat}
remoteBehindCount={remoteBehindCount}
fileBrowserAvailable={!isGeneralChatThread}
taskProgress={taskProgress}
subagentProgress={subagentProgress}
Expand Down
21 changes: 21 additions & 0 deletions apps/web/src/components/chat/ChatHeader.render.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ function renderChatHeader(overrides: Partial<ComponentProps<typeof ChatHeader>>
browserAvailable: true,
browserOpen: false,
workingTreeDiffStat: null,
remoteBehindCount: null,
fileBrowserAvailable: false,
taskProgress: null,
subagentProgress: null,
Expand Down Expand Up @@ -107,4 +108,24 @@ describe("ChatHeader", () => {
expect(markup).not.toContain("+38");
expect(markup).not.toContain("−12");
});

it("shows the behind-remote count on the closed source control toggle", () => {
const markup = renderChatHeader({
sourceControlAvailable: true,
sourceControlOpen: false,
remoteBehindCount: 2,
});

expect(markup).toContain("↓2");
});

it("drops the behind-remote count once the panel is open", () => {
const markup = renderChatHeader({
sourceControlAvailable: true,
sourceControlOpen: true,
remoteBehindCount: 2,
});

expect(markup).not.toContain("↓2");
});
});
48 changes: 42 additions & 6 deletions apps/web/src/components/chat/ChatHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,12 @@ interface ChatHeaderProps {
* the tree is clean or the status has not loaded.
*/
workingTreeDiffStat: { readonly insertions: number; readonly deletions: number } | null;
/**
* Commits the branch is behind its upstream, surfaced on the closed source
* control toggle as a pull-available hint. Null when there is nothing to
* pull or the status has not loaded.
*/
remoteBehindCount: number | null;
/** False for General Chats: their scratch workspace has no files worth browsing. */
fileBrowserAvailable: boolean;
taskProgress: ThreadTaskProgressState | null;
Expand Down Expand Up @@ -132,6 +138,7 @@ export const ChatHeader = memo(function ChatHeader({
browserOpen,
onToggleBrowser,
workingTreeDiffStat,
remoteBehindCount,
fileBrowserAvailable,
taskProgress,
subagentProgress,
Expand Down Expand Up @@ -355,7 +362,9 @@ export const ChatHeader = memo(function ChatHeader({
// on both sides -- the icon otherwise sits against the
// hover fill -- and less between them: the base gap is
// sized for icons, not for a label that belongs to one.
workingTreeDiffStat !== null && !sourceControlOpen && "gap-1 px-1.5",
(workingTreeDiffStat !== null || remoteBehindCount !== null) &&
!sourceControlOpen &&
"gap-1 px-1.5",
)}
pressed={sourceControlOpen}
onPressedChange={onToggleSourceControl}
Expand All @@ -367,12 +376,31 @@ export const ChatHeader = memo(function ChatHeader({
<SourceControlIcon className="size-[11px]" />
{/* Only while closed: once the panel is open it shows the
per-file counts, and repeating the total is noise. */}
{!sourceControlOpen && workingTreeDiffStat ? (
{!sourceControlOpen && (workingTreeDiffStat || remoteBehindCount !== null) ? (
<span className="font-mono text-[10px] leading-none">
<span className="text-success">+{workingTreeDiffStat.insertions}</span>
<span className="ps-1 text-destructive">
−{workingTreeDiffStat.deletions}
</span>
{workingTreeDiffStat ? (
<>
<span className="text-success">
+{workingTreeDiffStat.insertions}
</span>
<span className="ps-1 text-destructive">
−{workingTreeDiffStat.deletions}
</span>
</>
) : null}
{/* Deliberately hue-less: the arrow is the signal, and a
third color next to the green/red counts would crowd
an icon-sized control. */}
{remoteBehindCount !== null ? (
<span
className={cn(
"text-muted-foreground",
workingTreeDiffStat !== null && "ps-1",
)}
>
↓{remoteBehindCount}
</span>
) : null}
</span>
) : null}
</Toggle>
Expand All @@ -384,6 +412,14 @@ export const ChatHeader = memo(function ChatHeader({
: sourceControlToggleShortcutLabel
? `Toggle source control panel (${sourceControlToggleShortcutLabel})`
: "Toggle source control panel"}
{!sourceControlOpen && sourceControlAvailable && remoteBehindCount !== null ? (
<div className="text-muted-foreground">
{remoteBehindCount === 1
? "1 commit behind the remote."
: `${remoteBehindCount} commits behind the remote.`}{" "}
Pull from the source control panel.
</div>
) : null}
</TooltipPopup>
</Tooltip>
) : null}
Expand Down
Loading
Loading