Worktree insights

A worktree insight source publishes typed facts about each Git worktree of an open project: its pull request and a summary of its CI checks. Terminay renders them in the Git sidebar. The bundled Gitea extension is built on this contribution.

Manifest contribution

Available since API 3.1. It requires the worktree-observation permission.

JSON
"api": "^3.1.0",
"permissions": ["network", "worktree-observation"],
"contributes": {
  "worktreeInsights": [{
    "id": "com.terminay.gitea/gitea",
    "displayName": "Gitea",
    "description": "Pull requests and commit statuses from Gitea servers your remotes point at."
  }]
}

A manifest may declare up to 8 sources. Each id is namespaced by the extension ID.

Runtime contract

Register with context.worktrees.registerInsightSource(sourceId, runtime). The runtime has one method, start().

TypeScript
import { defineExtension, defineWorktreeInsightSource } from '@terminay/extension-api';

const source = defineWorktreeInsightSource({
  start({ contexts, onContextsChanged, publisher, credentials, signal }) {
    if (signal.aborted) return;
    const changes = onContextsChanged((next) => refresh(next));
    const available = credentials.onAvailable((origin) => refreshOrigin(origin));
    signal.addEventListener(
      'abort',
      () => {
        void changes.dispose();
        void available.dispose();
      },
      { once: true },
    );
    refresh(contexts);
  },
});

export default defineExtension({
  activate(context) {
    context.subscriptions.add(
      context.worktrees.registerInsightSource('com.terminay.gitea/gitea', source),
    );
  },
});
Start memberMeaning
contextsThe repository contexts live when the source starts.
onContextsChanged(listener)Called with every live context whenever one is issued, re-issued, or cancelled.
publisherpublish() and requestSignIn().
credentialstoken(), reject(), and onAvailable().
signalAborts when the source is disposed or the extension stops. Later calls are ignored.

Repository contexts

Terminay issues one context per open project's repository. It re-issues the context with the same id when its worktrees, branches, upstreams, or heads change, and when the project's activity changes.

TypeScript
interface RepositoryContext {
  id: string;
  repositoryRoot: string;
  remotes: { name: string; url: string }[];
  worktrees: RepositoryWorktree[];
  active?: boolean;
}

interface RepositoryWorktree {
  id: string;
  path: string;
  branch: string | null;
  upstream: { remote: string; branch: string } | null;
  head: string | null;
}
  • Context and worktree IDs are opaque and host-issued. A worktree ID is the only key properties can be published under.
  • active is true while a client has the project active. A source may refresh an active project more often.
  • Stop all work for a context once it no longer appears in the list.

Publish properties

TypeScript
publisher.publish(context.id, worktree.id, {
  pullRequest: {
    number: 285,
    title: 'Add worktree status',
    url: 'https://git.example.net/acme/app/pulls/285',
    state: 'open',
    mergeable: true,
  },
  checks: {
    passed: 2,
    failed: 1,
    pending: 0,
    skipped: 0,
    total: 3,
    items: [
      { name: 'build', state: 'passed' },
      { name: 'lint', state: 'passed' },
      { name: 'test', state: 'failed', url: 'https://git.example.net/acme/app/actions/runs/91' },
    ],
  },
});

// Clear a worktree's properties.
publisher.publish(context.id, worktree.id, null);

publish() replaces one worktree's properties. A call for a context or worktree the host did not issue is dropped. WorktreeProperties is a closed object with two optional members.

FieldRule
pullRequest.numberPositive integer.
pullRequest.titleNon-blank, at most 512 characters.
pullRequest.urlCredential-free HTTPS URL, at most 2,048 characters.
pullRequest.stateopen, draft, merged, or closed.
pullRequest.mergeableOptional boolean.
checks.passed, failed, pending, skipped, totalIntegers from 0 to 10,000. The four counts must sum to total.
checks.urlOptional credential-free HTTPS URL.
checks.itemsRequired array of at most 100 items, and no more than total. Counts cover every check; items may be truncated.
checks.items[]name (at most 256 characters), state of passed, failed, pending, or skipped, and an optional HTTPS url.

validateWorktreeProperties(value) applies the same rules and returns either a clean copy or a reason. isSafeHttpsUrl() and isHttpsOrigin() are exported too.

Sign-in and credentials

TypeScript
const token = await credentials.token(origin);
if (token === undefined) {
  publisher.requestSignIn({
    origin, // 'https://git.example.net'
    provider: 'Gitea',
    tokenPageUrl: `${origin}/user/settings/applications`,
  });
  return;
}
// On a 401 from the forge:
await credentials.reject(origin);
  • requestSignIn() asks Terminay to prompt the user for a token for one HTTPS origin. Terminay owns the prompt and whether it is shown.
  • origin must be an HTTPS origin with no path, query, fragment, or credentials. provider is at most 64 characters. tokenPageUrl is an optional HTTPS page where the user can create a token.
  • The token is stored in the server vault per extension and origin. credentials.token(origin) returns it, or undefined.
  • credentials.reject(origin) tells Terminay the origin refused the token, so it is forgotten and sign-in can recur.
  • credentials.onAvailable(listener) fires when the user stores a token for an origin.

What Terminay owns

The property model is closed. An extension supplies facts and never presentation: Terminay alone decides how a pull request or checks summary looks and what activating it does.

Remote forge state has no file to watch. The bundled Gitea extension refreshes at once when a context is issued, re-issued, or becomes active, then every 10 seconds for an active project and every 45 seconds for any other, widening the interval after consecutive failures. It publishes only when properties change.