g1t is described as the product it is in alpha, and its shell takes the new shape: the README, the docs home and docs/PLAN.md label every feature Live, Preview or Coming; a floating dock with the g1t mark, Today, Chat, Notifications, Agents, Code, Artifacts, your pinned apps and the Apps launcher, with People, Workspace and the account menu (now holding help) at its foot; the workspace's logo and switcher heading a flat sidebar that folds away with Ctrl B, or leading the header's breadcrumbs where there is none; the page in a rounded panel with a full-width header; Today as the front page, with first-time acceptance of agents' pull requests, what needs you and what's at stake, spend today and one item to start with, every number from a service; Notifications in place of the Inbox at /notifications; a bottom bar and More sheet on phones; and sign-in, sign-up, two-factor, reset, invites and choosing a workspace on standalone pages with no app around them.
| 1 | 1 | # g1t | |
| 2 | 2 | ||
| 3 | − | One open-source workspace where a team and its agents talk, work, write | |
| 4 | − | things down and ship. No separate chat app, wiki or forge to stitch | |
| 5 | − | together. It runs on Cloudflare Workers and Artifacts. | |
| 3 | + | g1t is where a company's people and agents work together. People and agents | |
| 4 | + | are members of the same workspace: they talk in channels and direct | |
| 5 | + | messages and sit on the same teams. You hand an agent work, it does it, and | |
| 6 | + | it comes back with something you can act on: a pull request to merge, a | |
| 7 | + | document, a reply to approve. Today says what happened and what is waiting | |
| 8 | + | on you. Code and Deployments are built in, because what agents make has to | |
| 9 | + | be reviewed, tested and shipped. Everything specific to your business will | |
| 10 | + | come from extensions and integrations. | |
| 11 | + | ||
| 12 | + | g1t is MIT-licensed. Use it on [g1t.sh](https://g1t.sh), or run it | |
| 13 | + | yourself. It runs on Cloudflare Workers, and the core runs in Docker. | |
| 6 | 14 | ||
| 7 | − | - **Chat.** Channels, direct messages and threads, live. People and agents | |
| 8 | − | are members alike: DM an agent, or mention it in a thread, and it answers | |
| 9 | − | there. Chat is included on every plan, with no seats and no history | |
| 10 | − | cutoff. | |
| 11 | − | - **Agents.** A workspace's own agents, each with a role, a job, a | |
| 12 | − | personality, limits on which models Auto may route it to (including the | |
| 13 | − | workspace's own providers) and a budget. Start from templates: planner, | |
| 14 | − | implementer, reviewer, triage, documenter, release manager, on-call. | |
| 15 | − | - **Docs** (coming soon). Specs, runbooks and decisions, written together, | |
| 16 | − | read by agents and kept current by them. | |
| 17 | − | - **Code.** Git over HTTPS, issues, pull requests and reviews. Assign an | |
| 18 | − | issue to g1t or connect any coding agent over MCP; hand g1t an outcome and | |
| 19 | − | a planner splits it into issues that agents take up as they unblock. | |
| 20 | − | Checks run by g1t in clean sandboxes, workflows from `.g1t/workflows`, a | |
| 21 | − | merge queue that tests changes together, why-blame from any line to the | |
| 22 | − | session that wrote it, and a preview of every pull request on `g1t.page`. | |
| 23 | − | - **Rails.** Budgets per workspace, agent and task; agents act with the | |
| 24 | − | asker's access and answer only with what their audience may see; | |
| 25 | − | approvals for merges and production deploys; an audit log on every | |
| 26 | − | workspace. | |
| 27 | − | - **Open and fair.** MIT licensed and self-hostable (an early Docker Compose | |
| 28 | − | version of the core forge, in `deploy/self-host`). People chat free and | |
| 29 | − | the forge is free; agents pay the model's price plus a flat agent rate, | |
| 30 | − | and other compute is what it costs plus 20%, never per seat. | |
| 15 | + | **g1t is in alpha.** Accounts are by invite. Each feature below is marked: | |
| 31 | 16 | ||
| 32 | − | The plan for the workspace is [docs/WORKSPACE.md](docs/WORKSPACE.md). | |
| 17 | + | - **Live**: works on g1t.sh for every workspace. | |
| 18 | + | - **Preview**: works on g1t.sh, but is limited to some workspaces or | |
| 19 | + | unfinished. | |
| 20 | + | - **Coming**: planned, not built. | |
| 33 | 21 | ||
| 34 | − | g1t is made by Flagon, Inc. It is also an entry in Cloudflare's **Build the | |
| 35 | − | Next-Gen Git Platform** competition, which asks what a git platform looks | |
| 36 | − | like when many of the people using it are agents | |
| 37 | − | ([the challenge](https://blog.cloudflare.com/next-git-platform-on-cloudflare/), | |
| 38 | − | [rules and dates](https://www.cloudflare.com/git-competition/)). | |
| 39 | − | [docs/PLAN.md](docs/PLAN.md) says how g1t answers the brief and what is | |
| 40 | − | built so far. | |
| 22 | + | The plan behind this is [docs/PLAN.md](docs/PLAN.md). | |
| 41 | 23 | ||
| 42 | 24 | ## Where things are | |
| 43 | 25 | ||
| 44 | 26 | - Site: <https://g1t.sh> | |
| 45 | 27 | - Docs: <https://docs.g1t.sh> | |
| 46 | 28 | - API: <https://api.g1t.sh> · MCP: <https://mcp.g1t.sh> | |
| 47 | − | - Plan and design: [docs/PLAN.md](docs/PLAN.md) | |
| 48 | − | - Demo walk-through: [docs/DEMO.md](docs/DEMO.md) | |
| 29 | + | - Plan: [docs/PLAN.md](docs/PLAN.md) | |
| 30 | + | - Limits you can hit today: [docs.g1t.sh/about/limitations](https://docs.g1t.sh/about/limitations/) | |
| 49 | 31 | ||
| 50 | − | ## Status | |
| 32 | + | ## What's in it | |
| 51 | 33 | ||
| 52 | − | Working today: | |
| 34 | + | ### The workspace | |
| 53 | 35 | ||
| 54 | − | - Accounts with email verification and password reset. Applications sign | |
| 55 | − | in through the browser with OAuth 2.1, so connecting an MCP client needs | |
| 56 | − | no pasted token; tools without a browser use a device code. | |
| 57 | − | - Workspaces that own repositories, with members and roles. Every account | |
| 58 | − | creates one before anything else, and usernames and workspaces share one | |
| 59 | − | namespace. | |
| 60 | − | - Access tokens that belong to a workspace instead of a person, for CI and | |
| 61 | − | integrations, so nothing needs a shared service account. | |
| 62 | − | - Public and private repositories, and git over HTTPS, including creating a | |
| 63 | − | repository by pushing to it. | |
| 64 | − | - Issues with labels and comments; a description can say what done means, | |
| 65 | − | under a Definition of done. | |
| 66 | − | - Pull requests with a diff and a recorded agent session: in a | |
| 67 | − | fork of their own, which is how agents work, or from a branch pushed to | |
| 68 | − | the repository. Several can be made for one issue. | |
| 69 | − | - Checks: the repository's workflows run on every pull request, a | |
| 70 | − | person's or an agent's, and report a check each. The default branch | |
| 71 | − | names the required checks a merge needs; an agent whose change fails a | |
| 72 | − | check is sent back with the failing jobs' logs. A repository with no | |
| 73 | − | workflows gets a starter CI workflow in one click. | |
| 74 | − | - Review: comments on lines of a change, and approve or request-changes | |
| 75 | − | verdicts, from people and from agents. | |
| 76 | − | - Overlap: each pull request shows which others in progress change the | |
| 77 | − | same files, while the work is still going on. | |
| 78 | − | - Catch-up: when `main` has moved under a pull request, g1t merges it in, | |
| 79 | − | and g1t resolves any conflict. | |
| 80 | − | - Reviews written by g1t, on request: line comments, a summary and | |
| 81 | − | a verdict. | |
| 82 | − | - Importing a public repository from any git host by its address, and | |
| 83 | − | public or private repositories through g1t's GitHub App, imported once, | |
| 84 | − | mirrored, or pushed back to GitHub. | |
| 85 | − | - Merging: lands a pull request on `main`, closes its issue naming the pull | |
| 86 | − | request that resolved it, and closes the others for that issue as | |
| 87 | − | superseded. When `main` has moved, the pull request is brought up to date | |
| 88 | − | first, or refused where the repository requires that, so no commit is | |
| 89 | − | lost. | |
| 90 | − | - g1t agents: g1t's own agents working on an issue in sandboxes on | |
| 91 | − | Cloudflare Containers, seeing each pull request through checks, an | |
| 92 | − | agent's review, revisions and catch-up. | |
| 93 | − | - Outcomes: a brief planned into issues with dependencies, which agents | |
| 94 | − | take up as their dependencies land. | |
| 95 | − | - The merge queue: pull requests tested together with what is ahead of | |
| 96 | − | them before they land, with failures sent back to the agent that wrote | |
| 97 | − | them. Required approvals and checks per repository. | |
| 98 | − | - Checks in detail on every pull request, and conflicts worked out on | |
| 99 | − | every push, before a merge is tried. | |
| 100 | − | - Agents as records: every run with its live steps, cost and session, Stop | |
| 101 | − | and Message, and memory at two levels (project and workspace) that | |
| 102 | − | agents write and read. | |
| 103 | − | - Projects with deployments on g1t.page: a preview for every pull request, | |
| 104 | − | production on merge, custom domains. | |
| 105 | − | - GitHub Actions workflows from `.g1t/workflows`, secrets and variables, | |
| 106 | − | webhooks and integrations (Sentry, Datadog, Jira, Linear). | |
| 107 | − | - Profiles, workspaces with display names, icons and renameable slugs. | |
| 108 | − | - Usage billing with no seats: what it costs g1t plus a markup, a public | |
| 109 | − | price book, usage limits and itemised invoices. | |
| 110 | − | - A REST API, an OpenAPI document and an MCP server over the same operations. | |
| 111 | − | - An event bus: every state change is published, logged and delivered to | |
| 112 | − | subscribers. | |
| 36 | + | | Feature | Status | | |
| 37 | + | | --- | --- | | |
| 38 | + | | The dock: Today, Chat, Notifications, Agents, Code, Artifacts and your pinned apps, with People, Workspace and your account at its foot; a bottom bar on phones | Live | | |
| 39 | + | | Today: what agents finished, how much was accepted the first time, what's waiting on you, what was spent, and where to start | Preview | | |
| 40 | + | | Chat: channels, direct messages and threads, live, with people and agents as members | Live | | |
| 41 | + | | Notifications: mentions, reviews, approvals and alerts, with browser push | Live | | |
| 42 | + | | People and teams: members, roles, nested teams, invites | Live | | |
| 43 | + | | A directory of people and agents, profiles, org chart, teams with agent members | Coming | | |
| 44 | + | | Artifacts: documents written together, live, by people and agents | Live | | |
| 45 | + | | Slides, designs and dashboards as artifacts | Coming | | |
| 46 | + | | Scratchpads: tools, reports and agents anyone can build on a copy of the data, promoted after review | Coming | | |
| 47 | + | | Desktop app (Tauri, with a runner inside) and phone app (Expo) | Coming | | |
| 113 | 48 | ||
| 114 | − | Not built yet: what the Soon pages in each project's menu describe. Git | |
| 115 | − | over SSH waits on inbound TCP on port 22, which on Cloudflare | |
| 116 | − | means Workers inbound TCP, a beta g1t has applied for and is waiting on. | |
| 117 | − | Use HTTPS until then. See the build order in the plan. | |
| 49 | + | ### Agents | |
| 118 | 50 | ||
| 51 | + | | Feature | Status | | |
| 52 | + | | --- | --- | | |
| 53 | + | | Agents hired into roles from templates, each with responsibilities, a voice, model limits and a budget | Live | | |
| 54 | + | | `@g1t`, the orchestrator every workspace has, which hands work to the right agent with `hand_off` | Live | | |
| 55 | + | | Your own model providers' keys (14 providers), and the AI Gateway | Live | | |
| 56 | + | | g1t's hosted models (open to invited workspaces during the alpha) | Preview | | |
| 57 | + | | Budgets per workspace, agent and task; guardrails; agents act with the asker's access | Live | | |
| 58 | + | | Memory, sessions with live steps and cost, routines on a schedule | Live | | |
| 59 | + | | An agent catalog of specialists, and foundational skills (documents, research, data, code, communication, files) | Coming | | |
| 60 | + | | Agents on runners anywhere (g1t's, yours, your desktop), with sessions that persist between tasks | Coming | | |
| 61 | + | | Agents answering in Slack and Teams | Coming | | |
| 62 | + | ||
| 63 | + | ### Code and Deployments | |
| 64 | + | ||
| 65 | + | | Feature | Status | | |
| 66 | + | | --- | --- | | |
| 67 | + | | Public and private repositories, git over HTTPS, creating a repository by pushing to it | Live | | |
| 68 | + | | Issues, pull requests, line comments, reviews from people and agents | Live | | |
| 69 | + | | Required checks, rulesets, CODEOWNERS, the merge queue | Live | | |
| 70 | + | | Workflows from `.g1t/workflows` on g1t's runners or self-hosted ones (`crates/runner`) | Live | | |
| 71 | + | | Assign an issue to g1t: a pull request, checks, a review and revisions until it passes (invited workspaces) | Preview | | |
| 72 | + | | Any coding agent over MCP, recording its session onto its pull requests; why-blame | Live | | |
| 73 | + | | Packages: npm, Cargo, Composer, Maven, NuGet, RubyGems, Go modules, container images | Live | | |
| 74 | + | | Secret push protection, history scanning, dependency upgrade pull requests | Live | | |
| 75 | + | | A preview of every pull request on `g1t.page`, production on merge, custom domains | Live | | |
| 76 | + | | GitHub import, mirroring and pushing back | Live | | |
| 77 | + | | A copy of the data for every preview, instant rollback, container apps kept warm | Coming | | |
| 78 | + | | Git over SSH (waits on inbound TCP on Workers; use HTTPS) | Coming | | |
| 79 | + | ||
| 80 | + | ### Extend and run | |
| 81 | + | ||
| 82 | + | | Feature | Status | | |
| 83 | + | | --- | --- | | |
| 84 | + | | Integrations: the GitHub App, Sentry, Datadog, Jira, Linear, alerts | Live | | |
| 85 | + | | REST API, OpenAPI, MCP server, webhooks, an event bus | Live | | |
| 86 | + | | Usage billing with no seats, a public price book, spend limits, itemised invoices | Live | | |
| 87 | + | | Marketplace of extensions, shared from public repositories; Drive, Gmail, Calendar and Slack integrations; Mail | Coming | | |
| 88 | + | | Single sign-on (SAML, OIDC) and SCIM per workspace | Coming | | |
| 89 | + | | Storage: databases and buckets per team, with copies of production for anything unreviewed | Coming | | |
| 90 | + | | Self-hosting: the core forge in Docker Compose (agents, deployments and context search off) | Preview | | |
| 91 | + | ||
| 119 | 92 | ## Try it | |
| 120 | 93 | ||
| 121 | 94 | ```sh | |
| 122 | − | # 1. Create an account and a workspace at https://g1t.sh/register. | |
| 95 | + | # 1. Create an account and a workspace at https://g1t.sh/register (you need an invite). | |
| 123 | 96 | ||
| 124 | 97 | # 2. Connect Claude Code, then run /mcp in it to sign in through your browser. | |
| 125 | 98 | claude mcp add --transport http g1t https://mcp.g1t.sh | |
| ⋯ | |||
| 153 | 126 | | `services/pages` | Serves every app deployed on `g1t.page`, and custom domains. | TypeScript | | |
| 154 | 127 | | `services/models` | The model proxy at `models.g1t.sh`. | TypeScript | | |
| 155 | 128 | | `services/context` | The context hub: catalog, search and scorecards. | TypeScript | | |
| 129 | + | | `services/chat` | Channels, direct messages, threads and their live sockets. | TypeScript | | |
| 130 | + | | `services/agents` | The workspace's agents: definitions, templates, desks, memory and runs. | TypeScript | | |
| 131 | + | | `services/docs` | Artifacts: documents, their spaces, sharing and live editing. | TypeScript | | |
| 132 | + | | `services/notify` | Notifications: each person's feed, live counts and browser push. | TypeScript | | |
| 133 | + | | `services/packages` | The package registries and container images. | Rust | | |
| 156 | 134 | | `services/og` | Social cards at `og.g1t.sh`: a PNG per page, showing only what anyone may see. | TypeScript | | |
| 157 | 135 | | `apps/status` | The status page at `status.g1t.sh`. | TypeScript | | |
| 158 | 136 | | `apps/sudo` | g1t's own staff console. | TypeScript | | |
| 159 | 137 | | `crates/runner` | The program inside a sandbox: runs an agent, a workflow job or a merge queue build, and reports back. | Rust | | |
| 160 | 138 | | `crates/contracts` | Types and service interfaces for the Rust services. | Rust | | |
| 161 | 139 | | `crates/kit` | Plumbing shared by Rust services on Workers. | Rust | | |
| 140 | + | | `crates/g1t` | The `g1t` command line. | Rust | | |
| 141 | + | | `crates/rules` | The rules engine for rulesets on branches and tags. | Rust | | |
| 142 | + | | `crates/blobstore` | Object storage: R2 on Cloudflare, any S3-compatible store self-hosted. | Rust | | |
| 162 | 143 | | `crates/actions` | Reads workflows and evaluates their expressions. | Rust | | |
| 163 | 144 | | `crates/scan` | Secret and lockfile scanning, shared by services. | Rust | | |
| 164 | 145 | | `crates/secrets` | Secrets at rest and signatures. | Rust | | |
| 165 | − | | `crates/sshd` | Git over SSH, bridged to Artifacts. Not deployed yet. | Rust | | |
| 146 | + | | `crates/sshd` | Git over SSH, bridged to Cloudflare Artifacts. Not deployed yet. | Rust | | |
| 166 | 147 | | `packages/contracts` | The same interfaces for TypeScript callers. | TypeScript | | |
| 167 | 148 | | `packages/theme` | Design tokens and the logo, shared by the site and the docs. | CSS | | |
| 168 | 149 | | `deploy` | `stack.jsonc`, every deployable part and its resources; `self-host`, the Docker Compose version. | JSON, Docker Compose | | |
| ⋯ | |||
| 194 | 175 | ||
| 195 | 176 | ### On Cloudflare | |
| 196 | 177 | ||
| 197 | − | You need a Cloudflare account on the Workers Paid plan (Artifacts requires | |
| 198 | − | it), Node 22.22 or newer (`engines` in `package.json`; g1t is built on | |
| 178 | + | You need a Cloudflare account on the Workers Paid plan (Cloudflare | |
| 179 | + | Artifacts, where repositories are stored, requires it), Node 22.22 or newer (`engines` in `package.json`; g1t is built on | |
| 199 | 180 | Node 24), Rust with the `wasm32-unknown-unknown` target, and | |
| 200 | 181 | Docker to build the sandbox image. | |
| 201 | 182 | ||
| ⋯ | |||
| 207 | 188 | Then, once: | |
| 208 | 189 | ||
| 209 | 190 | 1. Create the resources each part needs: D1 databases, queues, KV | |
| 210 | − | namespaces, R2 buckets and the Artifacts namespace (`npx wrangler d1 | |
| 191 | + | namespaces, R2 buckets and the Cloudflare Artifacts namespace (`npx wrangler d1 | |
| 211 | 192 | create <name>`, `npx wrangler queues create <name>`, and so on), and set | |
| 212 | 193 | each part's secrets. `deploy/stack.jsonc` lists them all. | |
| 213 | 194 | 2. Put your own `account_id`, database ids and hostnames in each | |
| 12 | 12 | // Pages that moved, so links already shared still arrive. | |
| 13 | 13 | redirects: { | |
| 14 | 14 | '/guides/g1t-agents/': '/guides/working-with-g1t/', | |
| 15 | + | '/guides/inbox/': '/guides/notifications/', | |
| 15 | 16 | }, | |
| 16 | 17 | integrations: [ | |
| 17 | 18 | starlight({ | |
| ⋯ | |||
| 67 | 68 | items: [ | |
| 68 | 69 | { label: 'Quickstart', slug: 'quickstart' }, | |
| 69 | 70 | { label: 'How g1t works', slug: 'concepts/overview' }, | |
| 70 | − | { label: 'Your inbox', slug: 'guides/inbox' }, | |
| 71 | + | { label: 'Today', slug: 'guides/today' }, | |
| 72 | + | { label: 'Notifications', slug: 'guides/notifications' }, | |
| 71 | 73 | { label: 'Search and Explore', slug: 'guides/search' }, | |
| 72 | 74 | { label: 'Status and incidents', slug: 'guides/status' }, | |
| 73 | 75 | ], | |
| 1 | + | --- | |
| 2 | + | // Where a feature stands in the alpha: Live (on g1t.sh for every | |
| 3 | + | // workspace), Preview (on g1t.sh, but limited to some workspaces or | |
| 4 | + | // unfinished) or Coming (planned, not built). Inline, in a table cell, a | |
| 5 | + | // heading or a line. | |
| 6 | + | interface Props { | |
| 7 | + | is: 'live' | 'preview' | 'coming'; | |
| 8 | + | } | |
| 9 | + | ||
| 10 | + | const { is } = Astro.props; | |
| 11 | + | const LABEL = { live: 'Live', preview: 'Preview', coming: 'Coming' } as const; | |
| 12 | + | --- | |
| 13 | + | ||
| 14 | + | <span class:list={['status', 'not-content', is]}>{LABEL[is]}</span> | |
| 15 | + | ||
| 16 | + | <style> | |
| 17 | + | .status { | |
| 18 | + | display: inline-flex; | |
| 19 | + | align-items: center; | |
| 20 | + | padding: 0.05rem 0.5rem; | |
| 21 | + | border-radius: 999px; | |
| 22 | + | border: 1px solid transparent; | |
| 23 | + | font-family: var(--g1t-font-mono); | |
| 24 | + | font-size: 0.6875rem; | |
| 25 | + | font-weight: 500; | |
| 26 | + | letter-spacing: 0.04em; | |
| 27 | + | text-transform: uppercase; | |
| 28 | + | white-space: nowrap; | |
| 29 | + | vertical-align: middle; | |
| 30 | + | line-height: 1.5; | |
| 31 | + | } | |
| 32 | + | .live { | |
| 33 | + | color: var(--g1t-success); | |
| 34 | + | background: var(--g1t-success-low); | |
| 35 | + | } | |
| 36 | + | .preview { | |
| 37 | + | color: var(--g1t-accent); | |
| 38 | + | background: var(--g1t-accent-low); | |
| 39 | + | } | |
| 40 | + | .coming { | |
| 41 | + | color: var(--g1t-muted); | |
| 42 | + | background: var(--g1t-raised); | |
| 43 | + | border-color: var(--g1t-line-strong); | |
| 44 | + | } | |
| 45 | + | </style> |
| 1326 | 1326 | ||
| 1327 | 1327 | A run whose jobs wait shows **Waiting for review** at the top of its page, | |
| 1328 | 1328 | with the environments, the jobs each holds, its reviewers and when its | |
| 1329 | − | wait timer runs out. Reviewers are told in their [inbox](/guides/inbox/); | |
| 1329 | + | wait timer runs out. Reviewers are told in their [notifications](/guides/notifications/); | |
| 1330 | 1330 | on the run's page they choose **Approve and deploy** or **Reject**, with | |
| 1331 | 1331 | room for a comment. One review covers every job of the run that names the | |
| 1332 | 1332 | environment. A rejected job fails, and so does anything that needs it. The |
| 180 | 180 | ||
| 181 | 181 | | | | | |
| 182 | 182 | | --- | --- | | |
| 183 | − | | **Sees** | Chat, Artifacts, Agents and the inbox. The rail has no Code, and Home shows their channels, DMs, docs and inbox instead of projects. | | |
| 183 | + | | **Sees** | Chat, Artifacts, Agents and Notifications. The dock has no Code, and Today shows what needs them in chat, from agents and in their notifications, without reviews. | | |
| 184 | 184 | | **Can't open** | Any repository, issue, pull request, check or deploy page. The workspace's base permission and team grants don't apply to them. | | |
| 185 | 185 | | **Still sees work reach them** | Cards about issues, pull requests and deploys appear in their channels as summaries: title, state and who is on it. Opening one asks for Code access. | | |
| 186 | 186 | | **Can ask agents** | Anything about the product. Agents explain how things work and what changed, but never show them source code. Owners can tighten this so agents answer only from the workspace's docs. | |
| 5 | 5 | ||
| 6 | 6 | Every project has an **Agents** section, beside its pull requests: who is | |
| 7 | 7 | working on it right now, what each agent did before, and what agents have | |
| 8 | − | learned about the project. Across the workspace, **Agents** in the rail | |
| 8 | + | learned about the project. Across the workspace, **Agents** in the dock | |
| 9 | 9 | shows every agent, its [sessions](/guides/agent-sessions/) and what it | |
| 10 | 10 | costs, and **Memory** holds what agents know in every project. | |
| 11 | 11 | ||
| ⋯ | |||
| 17 | 17 | | Teach every agent something about this code | **Agents → Memory** | | |
| 18 | 18 | | Teach every agent something true in every project | **Memory**, in the workspace's sidebar | | |
| 19 | 19 | | Review what agents, reviews and docs taught | **Agents → Memory**, or **Context → Memory** for the workspace | | |
| 20 | − | | See every agent across the workspace, and its cost | **Agents** in the rail: the overview, then each agent's **Spend** | | |
| 20 | + | | See every agent across the workspace, and its cost | **Agents** in the dock: the overview, then each agent's **Spend** | | |
| 21 | 21 | ||
| 22 | 22 | ## Runs | |
| 23 | 23 | ||
| 47 | 47 | ||
| 48 | 48 | | | | | |
| 49 | 49 | | --- | --- | | |
| 50 | − | | **In every workspace** | On every surface: Chat, issues and pull requests, the inbox and MCP. It works on issues and pull requests as described in [g1t's agent](/guides/working-with-g1t/). | | |
| 50 | + | | **In every workspace** | On every surface: Chat, issues and pull requests, Notifications and MCP. It works on issues and pull requests as described in [g1t's agent](/guides/working-with-g1t/). | | |
| 51 | 51 | | **Configurable** | Set its personality, model limits, budget and what it may do alone, like any agent. Its job is fixed, and you can add instructions to it. | | |
| 52 | 52 | | **Knows the team** <Soon /> | Every colleague's role, what they are working on and their budget, and which teams own what. | | |
| 53 | 53 | | **Delegates** | When a specialist's role fits, it [hands the work off](#hand-off) with a brief: here, if the specialist is in the conversation, or in a group message with you and them. At most two hand-offs per message. | | |
| ⋯ | |||
| 64 | 64 | ||
| 65 | 65 | <Steps> | |
| 66 | 66 | ||
| 67 | − | 1. Open **Agents** in the rail, then choose **New agent**, or go to | |
| 67 | + | 1. Open **Agents** in the dock, then choose **New agent**, or go to | |
| 68 | 68 | `g1t.sh/<workspace>/-/agents/new`. | |
| 69 | 69 | 2. **Pick a role** from the templates, grouped by department, or choose | |
| 70 | 70 | **Start from nothing**. A role fills in every field below, and you can | |
| 12 | 12 | private until you share it, and agents read only what the people they work | |
| 13 | 13 | for can read. | |
| 14 | 14 | ||
| 15 | − | Open **Artifacts** in the rail on the left, or go to | |
| 15 | + | Open **Artifacts** in the dock on the left, or go to | |
| 16 | 16 | `g1t.sh/<workspace>/-/artifacts`. Artifacts is open to every member of a | |
| 17 | 17 | workspace, on every plan. | |
| 18 | 18 | ||
| ⋯ | |||
| 79 | 79 | ||
| 80 | 80 | 1. Choose people, agents or teams in the workspace, and a role. | |
| 81 | 81 | 2. Add a message if you want one, then press **Invite**. People you add get | |
| 82 | − | an inbox item with the link. | |
| 82 | + | a notification with the link. | |
| 83 | 83 | ||
| 84 | 84 | </Steps> | |
| 85 | 85 | ||
| ⋯ | |||
| 117 | 117 | ### Asking for access | |
| 118 | 118 | ||
| 119 | 119 | An artifact you can't open shows **You need access**. **Ask for access** | |
| 120 | − | tells its owner and the people with full access (up to 20) in their inbox, | |
| 120 | + | tells its owner and the people with full access (up to 20) in their notifications, | |
| 121 | 121 | with your message if you add one. You can ask once a day for each artifact. | |
| 122 | 122 | ||
| 123 | 123 | ## Private | |
| 130 | 130 | | Made from | [Settings → Invites](https://g1t.sh/settings/invites), your own | The workspace's **People** page, by its owners | | |
| 131 | 131 | | What it gives | One new account. It adds them to no workspace: the account gets a workspace of its own | An invitation to join that workspace, which they accept or decline | | |
| 132 | 132 | | Someone without an account | Makes their account with it | Makes their account with it too, while g1t is invite-only, then answers the invitation | | |
| 133 | − | | Someone already on g1t | Nothing: they have an account | The invitation, in their inbox and by email | | |
| 133 | + | | Someone already on g1t | Nothing: they have an account | The invitation, in their notifications and by email | | |
| 134 | 134 | | Exists | Only while g1t is invite-only | Always | | |
| 135 | 135 | | What its page and email say | "@syntaqx invited you to g1t" | "@syntaqx invited you to join Flagon, Inc. on g1t" | | |
| 136 | 136 | ||
| ⋯ | |||
| 306 | 306 | ### Workspace invitations | |
| 307 | 307 | ||
| 308 | 308 | A workspace invitation asks one account to join one workspace, with the | |
| 309 | − | role chosen when it was sent. You hear of it in your inbox and by email, | |
| 309 | + | role chosen when it was sent. You hear of it in your notifications and by email, | |
| 310 | 310 | and answer it at [g1t.sh/invitations](https://g1t.sh/invitations): | |
| 311 | 311 | ||
| 312 | 312 | - **Accept** joins the workspace with that role, and takes you there. | |
| 313 | − | - **Decline** joins nothing; whoever invited you is told in their inbox. | |
| 313 | + | - **Decline** joins nothing; whoever invited you is told in their notifications. | |
| 314 | 314 | ||
| 315 | 315 | An invitation works for 30 days, the same as an invite. Until it is | |
| 316 | 316 | answered, the workspace's owners see it under **Pending invitations** on | |
| ⋯ | |||
| 625 | 625 | (public repositories, read-only, and the workspace's own settings its | |
| 626 | 626 | permissions allow). | |
| 627 | 627 | - **No workspace**: your account and public repositories only, such as | |
| 628 | − | a token that reads your inbox. | |
| 628 | + | a token that reads your notifications. | |
| 629 | 629 | 5. Under **Permissions**, set each resource the token needs to a level. | |
| 630 | 630 | **Read only**, **Agent** and **CI** fill in a [preset](#presets); | |
| 631 | 631 | **Clear** sets everything back to no access. | |
| ⋯ | |||
| 638 | 638 | [requires approval](#a-workspaces-rules-for-tokens), and you are not one of | |
| 639 | 639 | its owners, the token is made **Pending approval**: it works at once, but | |
| 640 | 640 | reads public repositories only until an owner approves it. The owners hear | |
| 641 | − | of it in their [inbox](/guides/inbox/), and you hear of their answer in | |
| 641 | + | of it in their [notifications](/guides/notifications/), and you hear of their answer in | |
| 642 | 642 | yours. An owner's own token never waits. | |
| 643 | 643 | ||
| 644 | 644 | ### Change or delete a token | |
| ⋯ | |||
| 882 | 882 | | `memory:write` | Save memory for the next agent | | |
| 883 | 883 | | `account:read` | Read your email addresses, invites, invitations, pinned projects and stars | | |
| 884 | 884 | | `account:write` | Change your email addresses, make invites, answer invitations, pin projects and star repositories | | |
| 885 | − | | `notifications:read` | See your [inbox](/guides/inbox/), its threads, and what you subscribe to and watch | | |
| 885 | + | | `notifications:read` | See your [notifications](/guides/notifications/), its threads, and what you subscribe to and watch | | |
| 886 | 886 | | `notifications:write` | Mark notifications read, done, saved or snoozed, subscribe to threads and watch repositories | | |
| 887 | 887 | | `workspace:read` | Read workspace settings, invites, integrations, model routes and [teams](/guides/teams/) | | |
| 888 | 888 | | `workspace:admin` | Create and delete workspaces, invite members, manage teams, connect integrations | | |
| ⋯ | |||
| 1049 | 1049 | ||
| 1050 | 1050 | - **Waiting for approval.** Each pending token with its owner, | |
| 1051 | 1051 | permissions, repositories and expiry. Add an optional note, then select | |
| 1052 | − | **Approve** or **Deny**. Its owner hears of it in their inbox, with the | |
| 1052 | + | **Approve** or **Deny**. Its owner hears of it in their notifications, with the | |
| 1053 | 1053 | note. | |
| 1054 | 1054 | - **Tokens that can reach the workspace.** Every token made for it, and | |
| 1055 | 1055 | every token of its members and outside collaborators made for all of | |
| ⋯ | |||
| 1276 | 1276 | ||
| 1277 | 1277 | | | | | |
| 1278 | 1278 | | --- | --- | | |
| 1279 | − | | Your addresses, keys and profile | Removed: your email addresses, two-factor secret and recovery codes, GitHub link, picture, profile and security log, and your inbox and its settings. | | |
| 1279 | + | | Your addresses, keys and profile | Removed: your email addresses, two-factor secret and recovery codes, GitHub link, picture, profile and security log, and your notifications and their settings. | | |
| 1280 | 1280 | | What you wrote | Issues, pull requests, comments and reviews keep their place and their words, and show as written by `ghost`. You are taken off issues and pull requests you were assigned to or asked to review. Commits keep the name and address git recorded in them; those made with your [noreply address](#keeping-your-address-private) show as `ghost`. | | |
| 1281 | 1281 | | Workspaces you made | Name `ghost` as their creator. | | |
| 1282 | 1282 | | Statements, invoices and audit logs | Kept with your username, for the workspaces they belong to. | | |
| 60 | 60 | ## Approvals settle everywhere | |
| 61 | 61 | ||
| 62 | 62 | An approval is a button in the chat app message, a card in g1t and an item | |
| 63 | − | in your inbox. Approving in any one of them settles all three. | |
| 63 | + | in your notifications. Approving in any one of them settles all three. | |
| 64 | 64 | ||
| 65 | 65 | ## Account linking | |
| 66 | 66 |
| 22 | 22 | no history cutoff. Agents' replies are charged to the agent that wrote | |
| 23 | 23 | them; see [what an agent costs](/guides/agents/#what-an-agent-costs). | |
| 24 | 24 | ||
| 25 | − | Open **Chat** in the rail on the left, or go to | |
| 25 | + | Open **Chat** in the dock on the left, or go to | |
| 26 | 26 | `g1t.sh/<workspace>/-/chat`. | |
| 27 | 27 | ||
| 28 | 28 | ## A conversation | |
| ⋯ | |||
| 374 | 374 | ||
| 375 | 375 | When a card waits on you (a session at its cap, a draft issue you asked | |
| 376 | 376 | for), its pop-up has the card's buttons, and so does **Waiting on you in | |
| 377 | − | chat** at the top of the inbox panel (the bell), for a day or until you | |
| 377 | + | chat** at the top of the notifications panel (the bell), for a day or until you | |
| 378 | 378 | act. **Approve more** asks for the amount right there; **Stop** asks | |
| 379 | 379 | first. A browser notification has the buttons that need nothing typed, | |
| 380 | 380 | such as **Stop** and **Open**. | |
| ⋯ | |||
| 551 | 551 | ||
| 552 | 552 | ### Set a status | |
| 553 | 553 | ||
| 554 | − | Open the menu on your avatar at the bottom of the rail (on a phone, the | |
| 555 | − | workspace avatar at the top left) and choose **Set a status**. Give it an | |
| 554 | + | Open the menu on your avatar at the foot of the dock (on a phone, **More**, | |
| 555 | + | then **You and help**) and choose **Set a status**. Give it an | |
| 556 | 556 | emoji and a few words, or pick one: | |
| 557 | 557 | ||
| 558 | 558 | | | Clears after | | |
| ⋯ | |||
| 600 | 600 | | Notify me about | You hear about | | |
| 601 | 601 | | --- | --- | | |
| 602 | 602 | | **DMs and mentions** (the default) | Direct messages, mentions, replies in your threads, agents waiting on you, and approvals. | | |
| 603 | − | | **Everything** | Every message in conversations you're in, and everything in your inbox. | | |
| 603 | + | | **Everything** | Every message in conversations you're in, and everything in your notifications. | | |
| 604 | 604 | | **Nothing** | No pop-ups or browser notifications. Counts still update. | | |
| 605 | 605 | ||
| 606 | 606 | Under **For a workspace**, you can give one workspace a different level, | |
| ⋯ | |||
| 612 | 612 | From the menu on your avatar, **Pause notifications** for 30 minutes, an | |
| 613 | 613 | hour, or until tomorrow morning (9:00 your time). Until then nothing pops | |
| 614 | 614 | up and no browser notification is sent, whatever your settings; counts and | |
| 615 | − | your inbox still update, and everyone sees your amber dot. **Resume | |
| 615 | + | your notifications still update, and everyone sees your amber dot. **Resume | |
| 616 | 616 | notifications** ends it early. | |
| 617 | 617 | ||
| 618 | − | [Your inbox](/guides/inbox/) is separate: it keeps every item until you | |
| 618 | + | [Notifications](/guides/notifications/) are separate: they keep every item until you | |
| 619 | 619 | deal with it, whatever these settings say. | |
| 620 | 620 | ||
| 621 | 621 | ## Coming soon | |
| ⋯ | |||
| 627 | 627 | Pull requests, checks, deploys and approvals as cards in the channels | |
| 628 | 628 | linked to a project, with their buttons working in place. <Soon /> | |
| 629 | 629 | </Card> | |
| 630 | − | <Card title="Mentions in your inbox" icon="inbox"> | |
| 631 | − | Mentions and approvals as items in [your inbox](/guides/inbox/), so | |
| 630 | + | <Card title="Mentions in Notifications" icon="bell"> | |
| 631 | + | Mentions and approvals as items in [Notifications](/guides/notifications/), so | |
| 632 | 632 | acting in either place settles both. <Soon /> | |
| 633 | 633 | </Card> | |
| 634 | 634 | <Card title="Files" icon="paperclip"> | |
| 219 | 219 | - owners of optional sections too; | |
| 220 | 220 | - a draft once it is marked ready. | |
| 221 | 221 | ||
| 222 | − | The inbox tells them **acme/api#42 changes files you own**, or **acme/api#42 | |
| 222 | + | Notifications tell them **acme/api#42 changes files you own**, or **acme/api#42 | |
| 223 | 223 | changes files @acme/backend owns**, with the reason `review_requested`. | |
| 224 | 224 | Webhooks get `pull.review_requested` with `data.code_owners` set to `true`. | |
| 225 | 225 |
| 1 | − | --- | |
| 2 | − | title: Your inbox | |
| 3 | − | description: What needs you, and what you follow, as it happens. One thread per issue, pull request, workflow or deployment, with why you were told. Choose what you hear of with subscriptions, watching and email settings. | |
| 4 | − | --- | |
| 5 | − | ||
| 6 | − | Your **inbox** tells you when something needs you, or when something | |
| 7 | − | happens to work you answer for or follow: an agent is waiting on you, | |
| 8 | − | someone asked you to review a pull request, checks failed on your pull | |
| 9 | − | request, a deployment failed, someone mentioned you. You are never told | |
| 10 | − | about what you did yourself. | |
| 11 | − | ||
| 12 | − | Open it from the bell in the top bar. The number on the bell is what is | |
| 13 | − | unread. It is amber while something is waiting on you, red while a failure | |
| 14 | − | is unread, and green otherwise. | |
| 15 | − | ||
| 16 | − | ## Threads | |
| 17 | − | ||
| 18 | − | Your inbox holds one **thread** for each thing you were told about: | |
| 19 | − | ||
| 20 | − | - an issue | |
| 21 | − | - a pull request | |
| 22 | − | - a workflow on one branch | |
| 23 | − | - a deployment: a project's production, or one pull request's preview | |
| 24 | − | - a workflow run waiting for your review to deploy to an environment | |
| 25 | − | ||
| 26 | − | When something new happens on a thread, it comes back to the top of your | |
| 27 | − | inbox, unread, even if you had marked it done. It is not added a second | |
| 28 | − | time. A thread that is snoozed stays snoozed until its time. | |
| 29 | − | ||
| 30 | − | Each card shows the latest activity's title, why you were told (see | |
| 31 | − | [reasons](#reasons)), and, when more than one thing has happened, how many, | |
| 32 | − | such as **3 updates**. g1t keeps the last 10 activities of each thread. | |
| 33 | − | ||
| 34 | − | While a thread is unread it keeps the most urgent of what happened since | |
| 35 | − | you last read it. A failure followed by a comment still shows as a failure | |
| 36 | − | until you read it. | |
| 37 | − | ||
| 38 | − | ## Reasons | |
| 39 | − | ||
| 40 | − | Every thread says why you were told of its latest activity. When you are | |
| 41 | − | told of one thing for more than one reason, the first that applies in this | |
| 42 | − | table is shown. | |
| 43 | − | ||
| 44 | − | | Reason | Shown as | Why you were told | | |
| 45 | − | | --- | --- | --- | | |
| 46 | − | | `agent` | agent waiting | An agent is waiting on you: it asked a question, or it stopped until a person steps in. | | |
| 47 | − | | `review_requested` | review requested | Someone asked you, or a team you are in, to review a pull request; it changes files you own; or you are one of its reviewers. Also a workflow run waiting for you, as one of an [environment's reviewers](/guides/actions/#environments), to approve its deployment, and, for a workspace's owners, a member's token made for the workspace waiting for [approval](/guides/authentication/#a-workspaces-rules-for-tokens) (in the inbox only, never emailed). | | |
| 48 | − | | `assign` | assigned | You were assigned, or you are an assignee. | | |
| 49 | − | | `mention` | mentioned | Someone mentioned you with `@username`, or you were mentioned on it before. | | |
| 50 | − | | `team_mention` | team mentioned | Someone mentioned a [team](/guides/teams/#mentions) you are in with `@workspace/team`, or a team you are in was mentioned on it before. | | |
| 51 | − | | `ci_activity` | CI activity | A check, workflow or deployment on your work finished badly, or recovered. | | |
| 52 | − | | `security_alert` | security alert | A new secret, code scanning or vulnerability alert on a repository you look after, a push of yours that push protection blocked, or a [bypass request](/guides/security/secret-protection/#delegated-bypass) to review or its answer. The workspace's owners hear of new alerts; watchers who chose **Security alerts** do too, if they can see findings. | | |
| 53 | − | | `state_change` | state changed | It was closed, reopened or merged. | | |
| 54 | − | | `author` | your work | You opened it, or you asked g1t for it. Also an owner's answer to your [token made for a workspace](/guides/authentication/#create-a-token) waiting for approval, or its revocation. | | |
| 55 | − | | `comment` | commented | You commented on it. | | |
| 56 | − | | `manual` | subscribed | You subscribed to it yourself. | | |
| 57 | − | | `subscribed` | watching | You watch its repository. | | |
| 58 | − | ||
| 59 | − | ## What lands there | |
| 60 | − | ||
| 61 | − | Each item comes from something that happened on g1t. Who is told depends on | |
| 62 | − | what it was: | |
| 63 | − | ||
| 64 | − | | What happened | Who is told | Reason | Shown as | | |
| 65 | − | | --- | --- | --- | --- | | |
| 66 | − | | An agent asked a question of the agent on a pull request, or handed it work | The person the pull request belongs to, and its issue's author and assignees | `agent` | Needs you | | |
| 67 | − | | g1t stopped on a pull request until a person steps in | The same people | `agent` | Needs you | | |
| 68 | − | | Someone asked for reviews on a pull request, or opened one with reviewers | The reviewers asked | `review_requested` | Needs you | | |
| 69 | − | | Someone asked a team to review a pull request | Everyone in the team and its child teams, or, with [review assignment](/guides/teams/#review-assignment), the people picked | `review_requested` | Needs you | | |
| 70 | − | | A workflow run's jobs wait for an [environment's reviewers](/guides/actions/#environments) | Each reviewer, and everyone in a reviewing team ("Deploy is waiting for your review to deploy to production in acme/api"), but not whoever started the run when it may not approve it | `review_requested` | Needs you | | |
| 71 | − | | A pull request changes files a [CODEOWNERS file](/guides/codeowners/) gives you or your team | The owners asked: "acme/api#42 changes files you own" | `review_requested` | Needs you | | |
| 72 | − | | Someone assigned people to an issue or pull request, or opened one with assignees | The people newly assigned | `assign` | Info | | |
| 73 | − | | Checks failed, or could not run, on a pull request | The person the pull request belongs to | `ci_activity` | Error | | |
| 74 | − | | A workflow failed on a pull request | The person the pull request belongs to | `ci_activity` | Error | | |
| 75 | − | | A workflow failed on a branch | Whoever pushed the commit it ran on | `ci_activity` | Error | | |
| 76 | − | | A preview of a pull request failed to deploy | The person the pull request belongs to, and people watching deployments | `ci_activity`, or `subscribed` for watchers | Error | | |
| 77 | − | | Production failed to deploy | Whoever pushed or started it, and people watching deployments | `ci_activity`, or `subscribed` for watchers | Error | | |
| 78 | − | | A deployment went live | People watching deployments; after a failure, also whoever was told of the failure | `ci_activity`, or `subscribed` for watchers | Success | | |
| 79 | − | | g1t finished a change and marked it ready for review | The person who asked g1t for it | `author` | Success | | |
| 80 | − | | g1t reviewed a pull request | The person the pull request belongs to | `author` | Success when approved, Info when it asks for changes | | |
| 81 | − | | A pull request was merged | Everyone subscribed to it, and people watching pull requests | `state_change`, or `subscribed` for watchers | Success | | |
| 82 | − | | An issue or pull request was closed, or an issue was reopened | Everyone subscribed to it, and people watching its kind | `state_change`, or `subscribed` for watchers | Info | | |
| 83 | − | | Someone mentioned you with `@username` in a comment | You | `mention` | Info | | |
| 84 | − | | Someone mentioned a team with `@workspace/team` in a comment, or in an issue or pull request they opened | Everyone in the team and its child teams, when the team's notifications are on and the writer can see the team | `team_mention` | Info | | |
| 85 | − | | Someone commented on an issue or pull request | Everyone subscribed to it, and people watching its kind | Why each is subscribed, or `subscribed` for watchers | Info, or Success for an approval | | |
| 86 | − | | One of your workspace's agents commented on or reviewed an issue or pull request, as itself | The same people, and the person the pull request belongs to for a review; shown as from "Margo (agent)", with "(advisory)" on a review. See [agent reviews](/guides/pull-requests/#agent-reviews) | Why each is subscribed, or `author` for a review | Info, or Success for an approval | | |
| 87 | − | | An issue or pull request was opened | People watching its kind | `subscribed` | Info | | |
| 88 | − | ||
| 89 | − | "The person a pull request belongs to" is its author, or, for a change g1t | |
| 90 | − | made, the person who asked for it. An approval or a request for changes | |
| 91 | − | always reaches that person. g1t itself is never told. A mention in code or | |
| 92 | − | in a quoted line does not count. | |
| 93 | − | ||
| 94 | − | Some threads close themselves once they no longer need you: | |
| 95 | − | ||
| 96 | − | - When an agent was waiting on you, the thread moves to Done as soon as the | |
| 97 | − | agent picks back up: it resumes, its pull request changes, a merge is | |
| 98 | − | asked for, or the pull request is merged or closed. | |
| 99 | − | - When a review request to you is removed, that thread moves to Done. | |
| 100 | − | ||
| 101 | − | New activity brings either back, as with any thread. | |
| 102 | − | ||
| 103 | − | You only see items about repositories you can read. If you lose access to a | |
| 104 | − | repository, its items leave your inbox the next time you open it. | |
| 105 | − | ||
| 106 | − | ## Subscriptions | |
| 107 | − | ||
| 108 | − | You are **subscribed** to an issue or pull request, and hear of what | |
| 109 | − | happens on it, without doing anything when you: | |
| 110 | − | ||
| 111 | − | - opened it, or asked g1t for it | |
| 112 | − | - are assigned to it | |
| 113 | − | - are one of its reviewers | |
| 114 | − | - commented on it | |
| 115 | − | - were mentioned in it, by name or through a team | |
| 116 | − | ||
| 117 | − | You can also subscribe to any issue or pull request yourself, or | |
| 118 | − | unsubscribe from one. | |
| 119 | − | ||
| 120 | − | | You are | You hear of | | |
| 121 | − | | --- | --- | | |
| 122 | − | | Subscribed | Everything in [What lands there](#what-lands-there) that goes to everyone subscribed: comments, closes, reopens and merges. | | |
| 123 | − | | Unsubscribed | Only what is asked of you or is about your own work: an agent waiting on you, a review request, an assignment, a mention of you or your team, and failed checks, workflows and deployments. Commenting on it, or being mentioned in it, subscribes you again. | | |
| 124 | − | | Ignoring it | Nothing on it at all, not even a mention. Only you can undo this. | | |
| 125 | − | ||
| 126 | − | To subscribe to an issue or pull request, or unsubscribe: | |
| 127 | − | ||
| 128 | − | 1. Open the issue or pull request. | |
| 129 | − | 2. In the sidebar, under **Notifications**, select **Subscribe** or | |
| 130 | − | **Unsubscribe**. | |
| 131 | − | ||
| 132 | − | The line under the button says where you stand, such as "You're subscribed | |
| 133 | − | because you were assigned.", "You're not subscribed. You'll still hear if | |
| 134 | − | you're mentioned or asked to review." or "You ignore this thread." | |
| 135 | − | ||
| 136 | − | To ignore an issue or pull request, use the API: | |
| 137 | − | `PUT /repos/{owner}/{name}/issues/{number}/subscription` with | |
| 138 | − | `"ignored": true`, or the `notifications` tool's `subscribe` action with | |
| 139 | − | `ignored`. See [from the API and agents](#from-the-api-and-agents). While | |
| 140 | − | you ignore one, its button reads **Stop ignoring**, which puts you back to | |
| 141 | − | the default: subscribed only while you take part. | |
| 142 | − | ||
| 143 | − | ## Watching a repository | |
| 144 | − | ||
| 145 | − | How you **watch** a repository decides what you hear of on it beyond what | |
| 146 | − | you take part in. | |
| 147 | − | ||
| 148 | − | | Level | You hear of | | |
| 149 | − | | --- | --- | | |
| 150 | − | | **Participating and @mentions** | Only what you take part in or are mentioned in. The default. | | |
| 151 | − | | **All activity** | Also every issue and pull request opened, commented on, closed, reopened or merged, and every deployment. | | |
| 152 | − | | **Ignore** | Nothing on the repository at all, not even a mention or a review request. | | |
| 153 | − | | **Custom** | What you take part in, and the kinds you choose: **Issues**, **Pull requests**, **Deployments** and **Security alerts**. Security alerts reach only those with Write on the repository, who can see its findings. | | |
| 154 | − | ||
| 155 | − | To change how you watch a repository: | |
| 156 | − | ||
| 157 | − | 1. Open the repository. | |
| 158 | − | 2. In the header, open the **Watch** menu. | |
| 159 | − | 3. Select a level. For **Custom**, tick the kinds you want. Unticking | |
| 160 | − | every kind puts you back on **Participating and @mentions**. | |
| 161 | − | ||
| 162 | − | A repository you create is watched the way you choose in | |
| 163 | − | [your settings](#settings): **All activity** unless you change it. | |
| 164 | − | ||
| 165 | − | ||
| 166 | − | ||
| 167 | − | You can also be emailed when you are told of something. By default, g1t | |
| 168 | − | emails you for three reasons: **agent waiting**, **review requested** and | |
| 169 | − | **mentioned**. Each time you are told of something for a reason you chose, | |
| 170 | − | g1t sends one email with what happened and a link to it. | |
| 171 | − | ||
| 172 | − | An email is sent only when: | |
| 173 | − | ||
| 174 | − | - your account's email address is confirmed, and | |
| 175 | − | - you can still read the repository it is about. | |
| 176 | − | ||
| 177 | − | The foot of each email says why you got it, and links to | |
| 178 | − | [g1t.sh/settings/notifications](https://g1t.sh/settings/notifications). | |
| 179 | − | ||
| 180 | − | ## Settings | |
| 181 | − | ||
| 182 | − | **Settings → Notifications**, at | |
| 183 | − | [g1t.sh/settings/notifications](https://g1t.sh/settings/notifications), | |
| 184 | − | holds your choices: | |
| 185 | − | ||
| 186 | − | | Setting | What it does | Default | | |
| 187 | − | | --- | --- | --- | | |
| 188 | − | | **Email** | One checkbox per reason: you are also emailed when you are told of something for it. | agent waiting, review requested, mentioned | | |
| 189 | − | | **Repositories you create** | How you watch a new repository you create: **Participating and @mentions** or **All activity**. | All activity | | |
| 190 | − | | **Watched repositories** | Every repository you watch other than the default way, with how. | | | |
| 191 | − | ||
| 192 | − | ## Tabs | |
| 193 | − | ||
| 194 | − | | Tab | Shows | | |
| 195 | − | | --- | --- | | |
| 196 | − | | **All** | Everything, with what is waiting on you first | | |
| 197 | − | | **Needs you** | What is waiting on you: an agent, or a review asked of you | | |
| 198 | − | | **Errors** | Failed checks, workflows and deployments | | |
| 199 | − | | **Success** | Merges, approvals, finished agent work, and deployments that went live | | |
| 200 | − | | **Info** | Mentions, comments, assignments, and what you watch | | |
| 201 | − | ||
| 202 | − | The count beside each tab is what is unread under it. | |
| 203 | − | ||
| 204 | − | ## Work through it | |
| 205 | − | ||
| 206 | − | 1. Select the bell in the top bar. The inbox opens beside the page. | |
| 207 | − | 2. Select an item to open what it is about. It is marked read, and the inbox | |
| 208 | − | closes. | |
| 209 | − | 3. Point at an item (on a phone, the buttons are always there) to act on it | |
| 210 | − | without opening it: | |
| 211 | − | ||
| 212 | − | | Action | What it does | | |
| 213 | − | | --- | --- | | |
| 214 | − | | **Done** (✓) | Moves the thread out of the inbox and into Done, until something new happens on it | | |
| 215 | − | | **Mark as read** / **Mark as unread** | Changes whether it counts as unread | | |
| 216 | − | | **Save** | Keeps it under Saved, even after it is done | | |
| 217 | − | | **Snooze until** | Hides it for 3 hours, until tomorrow, or for a week, then brings it back | | |
| 218 | − | ||
| 219 | − | **Mark all read** marks everything under the tab you are on as read. | |
| 220 | − | ||
| 221 | − | ## The full inbox | |
| 222 | − | ||
| 223 | − | **Open inbox**, at the foot of the panel, goes to | |
| 224 | − | [g1t.sh/inbox](https://g1t.sh/inbox). It has the same tabs, every thread a | |
| 225 | − | page at a time, and two more views: | |
| 226 | − | ||
| 227 | − | | View | Shows | | |
| 228 | − | | --- | --- | | |
| 229 | − | | **Saved** | Threads you saved, done or not | | |
| 230 | − | | **Done** | Threads you marked done. Select ↶ on one to move it back | | |
| 231 | − | ||
| 232 | − | Beside the tabs, the **Reason** filter shows only threads told for one | |
| 233 | − | reason: select **Any reason** or one of the [reasons](#reasons). It is kept | |
| 234 | − | in the address as `?reason=`, such as | |
| 235 | − | [g1t.sh/inbox?reason=review_requested](https://g1t.sh/inbox?reason=review_requested), | |
| 236 | − | so you can bookmark it. | |
| 237 | − | ||
| 238 | − | Mission control shows a **Needs you** card with the newest unread threads | |
| 239 | − | waiting on you, then failures. It is hidden when there are none. | |
| 240 | − | ||
| 241 | − | ## From the API and agents | |
| 242 | − | ||
| 243 | − | Everything here is also in the REST API and the MCP server, for a personal | |
| 244 | − | access token or an OAuth sign-in. A workspace's token cannot use it, and | |
| 245 | − | neither can g1t's own agents: they act as `g1t`, which has no inbox. | |
| 246 | − | Reading needs the `notifications:read` scope, and changing anything | |
| 247 | − | `notifications:write`; the Agent [preset](/guides/authentication/#presets) | |
| 248 | − | has both. | |
| 249 | − | ||
| 250 | − | | Route | MCP action | What it does | | |
| 251 | − | | --- | --- | --- | | |
| 252 | − | | [`GET /notifications`](/reference/api/notifications/list-notifications/) | `list` | Your unread threads, latest first. Filter by `reason`, `severity`, `participating`, `since` and `before`; `all` adds read ones; `view` lists `saved` or `done`. | | |
| 253 | − | | [`PUT /notifications`](/reference/api/notifications/mark-notifications-read/) | `mark_all_read` | Mark everything read up to `last_read_at`. | | |
| 254 | − | | [`GET /notifications/threads/{id}`](/reference/api/notifications/get-notification-thread/) | `get` | One thread, its last 10 activities, and your subscription. | | |
| 255 | − | | [`PATCH /notifications/threads/{id}`](/reference/api/notifications/mark-thread-read/) | `mark_read` | Mark a thread read, or unread. | | |
| 256 | − | | [`DELETE /notifications/threads/{id}`](/reference/api/notifications/mark-thread-done/) | `done` | Mark a thread done. | | |
| 257 | − | | [`PUT /notifications/threads/{id}/saved`](/reference/api/notifications/save-thread/) | `save` | Save a thread; `DELETE` unsaves it. | | |
| 258 | − | | [`PUT /notifications/threads/{id}/snooze`](/reference/api/notifications/snooze-thread/) | `snooze` | Snooze a thread until `until`; `DELETE` brings it back. | | |
| 259 | − | | [`PUT /repos/{owner}/{name}/issues/{number}/subscription`](/reference/api/notifications/set-issue-subscription/) | `subscribe` | Subscribe to an issue or pull request, unsubscribe, or ignore it. `GET` reads it and `DELETE` unsubscribes. The same works at `/notifications/threads/{id}/subscription`. | | |
| 260 | − | | [`PUT /repos/{owner}/{name}/subscription`](/reference/api/notifications/set-repo-subscription/) | `watch` | Watch a repository at a `level`. `GET` reads it and `DELETE` goes back to the default. | | |
| 261 | − | | [`GET /user/subscriptions`](/reference/api/notifications/list-watched-repos/) | `watched` | The repositories you watch other than the default way. | | |
| 262 | − | ||
| 263 | − | `GET /repos/{owner}/{name}/notifications` and | |
| 264 | − | `PUT /repos/{owner}/{name}/notifications` list and mark one repository's | |
| 265 | − | threads. Every action of the `notifications` tool is in | |
| 266 | − | [MCP tools](/reference/mcp/#notifications). | |
| 267 | − | ||
| 268 | − | For example, to list the reviews waiting on you: | |
| 269 | − | ||
| 270 | − | ```sh | |
| 271 | − | curl "https://api.g1t.sh/notifications?reason=review_requested" \ | |
| 272 | − | -H "Authorization: Bearer $G1T_TOKEN" | |
| 273 | − | ``` | |
| 274 | − | ||
| 275 | − | ## How long items are kept | |
| 276 | − | ||
| 277 | − | Threads you mark done are removed 30 days after you mark them. Any other | |
| 278 | − | thread is removed once nothing has happened on it for 180 days. Saved | |
| 279 | − | threads are kept until you unsave them. |
| 21 | 21 | | **License** | The license its `LICENSE` file holds, such as **MIT license**, linked to the file. **View license** when the text is not one g1t recognizes. `LICENCE`, `COPYING` and `UNLICENSE` are read too, with or without an extension, and an `SPDX-License-Identifier` line says it outright. | | |
| 22 | 22 | | **Security policy** | A link to `SECURITY.md` at the root, or in `.g1t`, `.github` or `docs`. | | |
| 23 | 23 | | **Activity** | The repository's [activity](#activity): pushes, merges, new branches and tags. | | |
| 24 | − | | **Stars**, **watching** | How many people [starred](#stars) it, and how many watch all or some of its activity from the [Watch menu](/guides/inbox/). | | |
| 24 | + | | **Stars**, **watching** | How many people [starred](#stars) it, and how many watch all or some of its activity from the [Watch menu](/guides/notifications/). | | |
| 25 | 25 | | **Releases** | How many [releases](/guides/releases/) it has and the latest, or **Create a new release** for people who can push. | | |
| 26 | 26 | | **Packages** | [Packages](/guides/packages/) linked to it, or how to publish the first. | | |
| 27 | 27 | | **Contributors** | How many people and agents made it, and the most active. See [contributors](#contributors). | |
| 83 | 83 | ||
| 84 | 84 | | Setting | Default | Options | | |
| 85 | 85 | | --- | --- | --- | | |
| 86 | − | | When the remote stops answering | Show it on the repository | Also tell the workspace's owners in their [inbox](/guides/inbox/) | | |
| 86 | + | | When the remote stops answering | Show it on the repository | Also tell the workspace's owners in their [notifications](/guides/notifications/) | | |
| 87 | 87 | | Take over automatically | Off | After 5 to 1440 minutes unreachable | | |
| 88 | 88 | | Hand back on its own | When every branch goes back cleanly | Only when someone hands it back | | |
| 89 | 89 | | Keep CI warm | Off | Run `.g1t/workflows` on each push copied in | |
| 1 | + | --- | |
| 2 | + | title: Notifications | |
| 3 | + | description: What needs you, and what you follow, as it happens. One thread per issue, pull request, workflow or deployment, with why you were told. Choose what you hear of with subscriptions, watching and email settings. | |
| 4 | + | --- | |
| 5 | + | ||
| 6 | + | **Notifications** tell you when something needs you, or when something | |
| 7 | + | happens to work you answer for or follow: an agent is waiting on you, | |
| 8 | + | someone asked you to review a pull request, checks failed on your pull | |
| 9 | + | request, a deployment failed, someone mentioned you. You are never told | |
| 10 | + | about what you did yourself. | |
| 11 | + | ||
| 12 | + | Open them from **Notifications** in the dock, or from the bottom bar on a | |
| 13 | + | phone. The number on it is what is unread. The bell at the right of the | |
| 14 | + | page's header opens the same notifications in a panel beside the page; its | |
| 15 | + | number is amber while something is waiting on you, red while a failure is | |
| 16 | + | unread, and lavender otherwise. | |
| 17 | + | ||
| 18 | + | ## Threads | |
| 19 | + | ||
| 20 | + | Notifications hold one **thread** for each thing you were told about: | |
| 21 | + | ||
| 22 | + | - an issue | |
| 23 | + | - a pull request | |
| 24 | + | - a workflow on one branch | |
| 25 | + | - a deployment: a project's production, or one pull request's preview | |
| 26 | + | - a workflow run waiting for your review to deploy to an environment | |
| 27 | + | ||
| 28 | + | When something new happens on a thread, it comes back to the top of your | |
| 29 | + | notifications, unread, even if you had marked it done. It is not added a second | |
| 30 | + | time. A thread that is snoozed stays snoozed until its time. | |
| 31 | + | ||
| 32 | + | Each card shows the latest activity's title, why you were told (see | |
| 33 | + | [reasons](#reasons)), and, when more than one thing has happened, how many, | |
| 34 | + | such as **3 updates**. g1t keeps the last 10 activities of each thread. | |
| 35 | + | ||
| 36 | + | While a thread is unread it keeps the most urgent of what happened since | |
| 37 | + | you last read it. A failure followed by a comment still shows as a failure | |
| 38 | + | until you read it. | |
| 39 | + | ||
| 40 | + | ## Reasons | |
| 41 | + | ||
| 42 | + | Every thread says why you were told of its latest activity. When you are | |
| 43 | + | told of one thing for more than one reason, the first that applies in this | |
| 44 | + | table is shown. | |
| 45 | + | ||
| 46 | + | | Reason | Shown as | Why you were told | | |
| 47 | + | | --- | --- | --- | | |
| 48 | + | | `agent` | agent waiting | An agent is waiting on you: it asked a question, or it stopped until a person steps in. | | |
| 49 | + | | `review_requested` | review requested | Someone asked you, or a team you are in, to review a pull request; it changes files you own; or you are one of its reviewers. Also a workflow run waiting for you, as one of an [environment's reviewers](/guides/actions/#environments), to approve its deployment, and, for a workspace's owners, a member's token made for the workspace waiting for [approval](/guides/authentication/#a-workspaces-rules-for-tokens) (in Notifications only, never emailed). | | |
| 50 | + | | `assign` | assigned | You were assigned, or you are an assignee. | | |
| 51 | + | | `mention` | mentioned | Someone mentioned you with `@username`, or you were mentioned on it before. | | |
| 52 | + | | `team_mention` | team mentioned | Someone mentioned a [team](/guides/teams/#mentions) you are in with `@workspace/team`, or a team you are in was mentioned on it before. | | |
| 53 | + | | `ci_activity` | CI activity | A check, workflow or deployment on your work finished badly, or recovered. | | |
| 54 | + | | `security_alert` | security alert | A new secret, code scanning or vulnerability alert on a repository you look after, a push of yours that push protection blocked, or a [bypass request](/guides/security/secret-protection/#delegated-bypass) to review or its answer. The workspace's owners hear of new alerts; watchers who chose **Security alerts** do too, if they can see findings. | | |
| 55 | + | | `state_change` | state changed | It was closed, reopened or merged. | | |
| 56 | + | | `author` | your work | You opened it, or you asked g1t for it. Also an owner's answer to your [token made for a workspace](/guides/authentication/#create-a-token) waiting for approval, or its revocation. | | |
| 57 | + | | `comment` | commented | You commented on it. | | |
| 58 | + | | `manual` | subscribed | You subscribed to it yourself. | | |
| 59 | + | | `subscribed` | watching | You watch its repository. | | |
| 60 | + | ||
| 61 | + | ## What lands there | |
| 62 | + | ||
| 63 | + | Each item comes from something that happened on g1t. Who is told depends on | |
| 64 | + | what it was: | |
| 65 | + | ||
| 66 | + | | What happened | Who is told | Reason | Shown as | | |
| 67 | + | | --- | --- | --- | --- | | |
| 68 | + | | An agent asked a question of the agent on a pull request, or handed it work | The person the pull request belongs to, and its issue's author and assignees | `agent` | Needs you | | |
| 69 | + | | g1t stopped on a pull request until a person steps in | The same people | `agent` | Needs you | | |
| 70 | + | | Someone asked for reviews on a pull request, or opened one with reviewers | The reviewers asked | `review_requested` | Needs you | | |
| 71 | + | | Someone asked a team to review a pull request | Everyone in the team and its child teams, or, with [review assignment](/guides/teams/#review-assignment), the people picked | `review_requested` | Needs you | | |
| 72 | + | | A workflow run's jobs wait for an [environment's reviewers](/guides/actions/#environments) | Each reviewer, and everyone in a reviewing team ("Deploy is waiting for your review to deploy to production in acme/api"), but not whoever started the run when it may not approve it | `review_requested` | Needs you | | |
| 73 | + | | A pull request changes files a [CODEOWNERS file](/guides/codeowners/) gives you or your team | The owners asked: "acme/api#42 changes files you own" | `review_requested` | Needs you | | |
| 74 | + | | Someone assigned people to an issue or pull request, or opened one with assignees | The people newly assigned | `assign` | Info | | |
| 75 | + | | Checks failed, or could not run, on a pull request | The person the pull request belongs to | `ci_activity` | Error | | |
| 76 | + | | A workflow failed on a pull request | The person the pull request belongs to | `ci_activity` | Error | | |
| 77 | + | | A workflow failed on a branch | Whoever pushed the commit it ran on | `ci_activity` | Error | | |
| 78 | + | | A preview of a pull request failed to deploy | The person the pull request belongs to, and people watching deployments | `ci_activity`, or `subscribed` for watchers | Error | | |
| 79 | + | | Production failed to deploy | Whoever pushed or started it, and people watching deployments | `ci_activity`, or `subscribed` for watchers | Error | | |
| 80 | + | | A deployment went live | People watching deployments; after a failure, also whoever was told of the failure | `ci_activity`, or `subscribed` for watchers | Success | | |
| 81 | + | | g1t finished a change and marked it ready for review | The person who asked g1t for it | `author` | Success | | |
| 82 | + | | g1t reviewed a pull request | The person the pull request belongs to | `author` | Success when approved, Info when it asks for changes | | |
| 83 | + | | A pull request was merged | Everyone subscribed to it, and people watching pull requests | `state_change`, or `subscribed` for watchers | Success | | |
| 84 | + | | An issue or pull request was closed, or an issue was reopened | Everyone subscribed to it, and people watching its kind | `state_change`, or `subscribed` for watchers | Info | | |
| 85 | + | | Someone mentioned you with `@username` in a comment | You | `mention` | Info | | |
| 86 | + | | Someone mentioned a team with `@workspace/team` in a comment, or in an issue or pull request they opened | Everyone in the team and its child teams, when the team's notifications are on and the writer can see the team | `team_mention` | Info | | |
| 87 | + | | Someone commented on an issue or pull request | Everyone subscribed to it, and people watching its kind | Why each is subscribed, or `subscribed` for watchers | Info, or Success for an approval | | |
| 88 | + | | One of your workspace's agents commented on or reviewed an issue or pull request, as itself | The same people, and the person the pull request belongs to for a review; shown as from "Margo (agent)", with "(advisory)" on a review. See [agent reviews](/guides/pull-requests/#agent-reviews) | Why each is subscribed, or `author` for a review | Info, or Success for an approval | | |
| 89 | + | | An issue or pull request was opened | People watching its kind | `subscribed` | Info | | |
| 90 | + | ||
| 91 | + | "The person a pull request belongs to" is its author, or, for a change g1t | |
| 92 | + | made, the person who asked for it. An approval or a request for changes | |
| 93 | + | always reaches that person. g1t itself is never told. A mention in code or | |
| 94 | + | in a quoted line does not count. | |
| 95 | + | ||
| 96 | + | Some threads close themselves once they no longer need you: | |
| 97 | + | ||
| 98 | + | - When an agent was waiting on you, the thread moves to Done as soon as the | |
| 99 | + | agent picks back up: it resumes, its pull request changes, a merge is | |
| 100 | + | asked for, or the pull request is merged or closed. | |
| 101 | + | - When a review request to you is removed, that thread moves to Done. | |
| 102 | + | ||
| 103 | + | New activity brings either back, as with any thread. | |
| 104 | + | ||
| 105 | + | You only see items about repositories you can read. If you lose access to a | |
| 106 | + | repository, its items leave your notifications the next time you open them. | |
| 107 | + | ||
| 108 | + | ## Subscriptions | |
| 109 | + | ||
| 110 | + | You are **subscribed** to an issue or pull request, and hear of what | |
| 111 | + | happens on it, without doing anything when you: | |
| 112 | + | ||
| 113 | + | - opened it, or asked g1t for it | |
| 114 | + | - are assigned to it | |
| 115 | + | - are one of its reviewers | |
| 116 | + | - commented on it | |
| 117 | + | - were mentioned in it, by name or through a team | |
| 118 | + | ||
| 119 | + | You can also subscribe to any issue or pull request yourself, or | |
| 120 | + | unsubscribe from one. | |
| 121 | + | ||
| 122 | + | | You are | You hear of | | |
| 123 | + | | --- | --- | | |
| 124 | + | | Subscribed | Everything in [What lands there](#what-lands-there) that goes to everyone subscribed: comments, closes, reopens and merges. | | |
| 125 | + | | Unsubscribed | Only what is asked of you or is about your own work: an agent waiting on you, a review request, an assignment, a mention of you or your team, and failed checks, workflows and deployments. Commenting on it, or being mentioned in it, subscribes you again. | | |
| 126 | + | | Ignoring it | Nothing on it at all, not even a mention. Only you can undo this. | | |
| 127 | + | ||
| 128 | + | To subscribe to an issue or pull request, or unsubscribe: | |
| 129 | + | ||
| 130 | + | 1. Open the issue or pull request. | |
| 131 | + | 2. In the sidebar, under **Notifications**, select **Subscribe** or | |
| 132 | + | **Unsubscribe**. | |
| 133 | + | ||
| 134 | + | The line under the button says where you stand, such as "You're subscribed | |
| 135 | + | because you were assigned.", "You're not subscribed. You'll still hear if | |
| 136 | + | you're mentioned or asked to review." or "You ignore this thread." | |
| 137 | + | ||
| 138 | + | To ignore an issue or pull request, use the API: | |
| 139 | + | `PUT /repos/{owner}/{name}/issues/{number}/subscription` with | |
| 140 | + | `"ignored": true`, or the `notifications` tool's `subscribe` action with | |
| 141 | + | `ignored`. See [from the API and agents](#from-the-api-and-agents). While | |
| 142 | + | you ignore one, its button reads **Stop ignoring**, which puts you back to | |
| 143 | + | the default: subscribed only while you take part. | |
| 144 | + | ||
| 145 | + | ## Watching a repository | |
| 146 | + | ||
| 147 | + | How you **watch** a repository decides what you hear of on it beyond what | |
| 148 | + | you take part in. | |
| 149 | + | ||
| 150 | + | | Level | You hear of | | |
| 151 | + | | --- | --- | | |
| 152 | + | | **Participating and @mentions** | Only what you take part in or are mentioned in. The default. | | |
| 153 | + | | **All activity** | Also every issue and pull request opened, commented on, closed, reopened or merged, and every deployment. | | |
| 154 | + | | **Ignore** | Nothing on the repository at all, not even a mention or a review request. | | |
| 155 | + | | **Custom** | What you take part in, and the kinds you choose: **Issues**, **Pull requests**, **Deployments** and **Security alerts**. Security alerts reach only those with Write on the repository, who can see its findings. | | |
| 156 | + | ||
| 157 | + | To change how you watch a repository: | |
| 158 | + | ||
| 159 | + | 1. Open the repository. | |
| 160 | + | 2. In the header, open the **Watch** menu. | |
| 161 | + | 3. Select a level. For **Custom**, tick the kinds you want. Unticking | |
| 162 | + | every kind puts you back on **Participating and @mentions**. | |
| 163 | + | ||
| 164 | + | A repository you create is watched the way you choose in | |
| 165 | + | [your settings](#settings): **All activity** unless you change it. | |
| 166 | + | ||
| 167 | + | ||
| 168 | + | ||
| 169 | + | You can also be emailed when you are told of something. By default, g1t | |
| 170 | + | emails you for three reasons: **agent waiting**, **review requested** and | |
| 171 | + | **mentioned**. Each time you are told of something for a reason you chose, | |
| 172 | + | g1t sends one email with what happened and a link to it. | |
| 173 | + | ||
| 174 | + | An email is sent only when: | |
| 175 | + | ||
| 176 | + | - your account's email address is confirmed, and | |
| 177 | + | - you can still read the repository it is about. | |
| 178 | + | ||
| 179 | + | The foot of each email says why you got it, and links to | |
| 180 | + | [g1t.sh/settings/notifications](https://g1t.sh/settings/notifications). | |
| 181 | + | ||
| 182 | + | ## Settings | |
| 183 | + | ||
| 184 | + | **Settings → Notifications**, at | |
| 185 | + | [g1t.sh/settings/notifications](https://g1t.sh/settings/notifications), | |
| 186 | + | holds your choices: | |
| 187 | + | ||
| 188 | + | | Setting | What it does | Default | | |
| 189 | + | | --- | --- | --- | | |
| 190 | + | | **Email** | One checkbox per reason: you are also emailed when you are told of something for it. | agent waiting, review requested, mentioned | | |
| 191 | + | | **Repositories you create** | How you watch a new repository you create: **Participating and @mentions** or **All activity**. | All activity | | |
| 192 | + | | **Watched repositories** | Every repository you watch other than the default way, with how. | | | |
| 193 | + | ||
| 194 | + | ## Tabs | |
| 195 | + | ||
| 196 | + | | Tab | Shows | | |
| 197 | + | | --- | --- | | |
| 198 | + | | **All** | Everything, with what is waiting on you first | | |
| 199 | + | | **Needs you** | What is waiting on you: an agent, or a review asked of you | | |
| 200 | + | | **Errors** | Failed checks, workflows and deployments | | |
| 201 | + | | **Success** | Merges, approvals, finished agent work, and deployments that went live | | |
| 202 | + | | **Info** | Mentions, comments, assignments, and what you watch | | |
| 203 | + | ||
| 204 | + | The count beside each tab is what is unread under it. | |
| 205 | + | ||
| 206 | + | ## The Notifications page | |
| 207 | + | ||
| 208 | + | **Notifications** in the dock opens | |
| 209 | + | [g1t.sh/notifications](https://g1t.sh/notifications): every thread, a page | |
| 210 | + | at a time, under the same tabs. Its sidebar chooses what you look at: | |
| 211 | + | ||
| 212 | + | | In the sidebar | Shows | | |
| 213 | + | | --- | --- | | |
| 214 | + | | **Everything** | Threads you have not marked done, with how many are unread | | |
| 215 | + | | **Saved** | Threads you saved, done or not | | |
| 216 | + | | **Done** | Threads you marked done. Select ↶ on one to move it back | | |
| 217 | + | | **Why you were told** | Only threads told for one [reason](#reasons), or **Any reason** | | |
| 218 | + | ||
| 219 | + | The reason is kept in the address as `?reason=`, such as | |
| 220 | + | [g1t.sh/notifications?reason=review_requested](https://g1t.sh/notifications?reason=review_requested), | |
| 221 | + | so you can bookmark it. On a phone, and on a narrow window, where the | |
| 222 | + | sidebar is a drawer, the page shows the views as tabs and the reason as a | |
| 223 | + | menu above the list. | |
| 224 | + | ||
| 225 | + | ## Work through it | |
| 226 | + | ||
| 227 | + | 1. Open the Notifications page, or select the bell in the page's header to | |
| 228 | + | work through them in a panel beside the page. | |
| 229 | + | 2. Select an item to open what it is about. It is marked read, and the | |
| 230 | + | panel closes. | |
| 231 | + | 3. Point at an item (on a phone, the buttons are always there) to act on it | |
| 232 | + | without opening it: | |
| 233 | + | ||
| 234 | + | | Action | What it does | | |
| 235 | + | | --- | --- | | |
| 236 | + | | **Done** (✓) | Moves the thread out of Everything and into Done, until something new happens on it | | |
| 237 | + | | **Mark as read** / **Mark as unread** | Changes whether it counts as unread | | |
| 238 | + | | **Save** | Keeps it under Saved, even after it is done | | |
| 239 | + | | **Snooze until** | Hides it for 3 hours, until tomorrow, or for a week, then brings it back | | |
| 240 | + | ||
| 241 | + | **Mark all read** marks everything under the tab you are on as read. | |
| 242 | + | **Open Notifications**, at the foot of the panel, goes to the page. | |
| 243 | + | ||
| 244 | + | [Today](/guides/today/) shows the newest unread threads waiting on you, with | |
| 245 | + | what else needs you across the workspace. | |
| 246 | + | ||
| 247 | + | ## From the API and agents | |
| 248 | + | ||
| 249 | + | Everything here is also in the REST API and the MCP server, for a personal | |
| 250 | + | access token or an OAuth sign-in. A workspace's token cannot use it, and | |
| 251 | + | neither can g1t's own agents: they act as `g1t`, which has no notifications. | |
| 252 | + | Reading needs the `notifications:read` scope, and changing anything | |
| 253 | + | `notifications:write`; the Agent [preset](/guides/authentication/#presets) | |
| 254 | + | has both. | |
| 255 | + | ||
| 256 | + | | Route | MCP action | What it does | | |
| 257 | + | | --- | --- | --- | | |
| 258 | + | | [`GET /notifications`](/reference/api/notifications/list-notifications/) | `list` | Your unread threads, latest first. Filter by `reason`, `severity`, `participating`, `since` and `before`; `all` adds read ones; `view` lists `saved` or `done`. | | |
| 259 | + | | [`PUT /notifications`](/reference/api/notifications/mark-notifications-read/) | `mark_all_read` | Mark everything read up to `last_read_at`. | | |
| 260 | + | | [`GET /notifications/threads/{id}`](/reference/api/notifications/get-notification-thread/) | `get` | One thread, its last 10 activities, and your subscription. | | |
| 261 | + | | [`PATCH /notifications/threads/{id}`](/reference/api/notifications/mark-thread-read/) | `mark_read` | Mark a thread read, or unread. | | |
| 262 | + | | [`DELETE /notifications/threads/{id}`](/reference/api/notifications/mark-thread-done/) | `done` | Mark a thread done. | | |
| 263 | + | | [`PUT /notifications/threads/{id}/saved`](/reference/api/notifications/save-thread/) | `save` | Save a thread; `DELETE` unsaves it. | | |
| 264 | + | | [`PUT /notifications/threads/{id}/snooze`](/reference/api/notifications/snooze-thread/) | `snooze` | Snooze a thread until `until`; `DELETE` brings it back. | | |
| 265 | + | | [`PUT /repos/{owner}/{name}/issues/{number}/subscription`](/reference/api/notifications/set-issue-subscription/) | `subscribe` | Subscribe to an issue or pull request, unsubscribe, or ignore it. `GET` reads it and `DELETE` unsubscribes. The same works at `/notifications/threads/{id}/subscription`. | | |
| 266 | + | | [`PUT /repos/{owner}/{name}/subscription`](/reference/api/notifications/set-repo-subscription/) | `watch` | Watch a repository at a `level`. `GET` reads it and `DELETE` goes back to the default. | | |
| 267 | + | | [`GET /user/subscriptions`](/reference/api/notifications/list-watched-repos/) | `watched` | The repositories you watch other than the default way. | | |
| 268 | + | ||
| 269 | + | `GET /repos/{owner}/{name}/notifications` and | |
| 270 | + | `PUT /repos/{owner}/{name}/notifications` list and mark one repository's | |
| 271 | + | threads. Every action of the `notifications` tool is in | |
| 272 | + | [MCP tools](/reference/mcp/#notifications). | |
| 273 | + | ||
| 274 | + | For example, to list the reviews waiting on you: | |
| 275 | + | ||
| 276 | + | ```sh | |
| 277 | + | curl "https://api.g1t.sh/notifications?reason=review_requested" \ | |
| 278 | + | -H "Authorization: Bearer $G1T_TOKEN" | |
| 279 | + | ``` | |
| 280 | + | ||
| 281 | + | ## How long items are kept | |
| 282 | + | ||
| 283 | + | Threads you mark done are removed 30 days after you mark them. Any other | |
| 284 | + | thread is removed once nothing has happened on it for 180 days. Saved | |
| 285 | + | threads are kept until you unsave them. |
| 214 | 214 | person's does. | |
| 215 | 215 | ||
| 216 | 216 | People mentioned in an agent's comment, and everyone subscribed to the | |
| 217 | − | pull request, hear of it in their [inbox](/guides/inbox/) as from | |
| 217 | + | pull request, hear of it in their [notifications](/guides/notifications/) as from | |
| 218 | 218 | **Margo (agent)**, with "(advisory)" on a review. It publishes | |
| 219 | 219 | `comment.created` as a person's comment does, with `agent` (its `id`, | |
| 220 | 220 | `handle`, `display_name` and `avatar_seed`), `acting_for`, and for a |
| 118 | 118 | With **Delegated bypass** on, in the workspace's Security settings | |
| 119 | 119 | (`g1t.sh/<owner>/-/security/settings`), someone who pushed a blocked | |
| 120 | 120 | secret asks instead: **Ask to bypass** records a request, and the | |
| 121 | − | workspace's owners are told in their [inbox](/guides/inbox/). The | |
| 121 | + | workspace's owners are told in their [notifications](/guides/notifications/). The | |
| 122 | 122 | workspace's owners and the repository's admins review requests at | |
| 123 | 123 | `g1t.sh/<owner>/-/security/bypass-requests`: | |
| 124 | 124 |
| 190 | 190 | Write `@<workspace>/<team>`, such as `@acme/backend`, in a comment, or in | |
| 191 | 191 | the description of an issue or pull request when you open it, and the | |
| 192 | 192 | people of that team and its child teams are told in their | |
| 193 | − | [inbox](/guides/inbox/), with the reason `team_mention`: | |
| 193 | + | [notifications](/guides/notifications/), with the reason `team_mention`: | |
| 194 | 194 | ||
| 195 | 195 | ```text | |
| 196 | 196 | ana mentioned @acme/backend on acme/api#42 | |
| ⋯ | |||
| 289 | 289 | ||
| 290 | 290 | The timeline records who asked whom: **ana requested a review from | |
| 291 | 291 | @acme/backend**, and, when people are picked, **requested a review from | |
| 292 | − | cy**. The inbox says **ana asked @acme/backend to review acme/api#42** to | |
| 292 | + | cy**. Notifications say **ana asked @acme/backend to review acme/api#42** to | |
| 293 | 293 | the team's people, and **ana asked you to review acme/api#42** to each | |
| 294 | 294 | person picked. A request made by a CODEOWNERS file reads **acme/api#42 | |
| 295 | 295 | changes files @acme/backend owns**. | |
| 1 | + | --- | |
| 2 | + | title: Today | |
| 3 | + | description: Your workspace's front page. How the agents' work went today, what is waiting on you with the action beside it, what the day cost, and the one item to start with. How every number is worked out. | |
| 4 | + | --- | |
| 5 | + | ||
| 6 | + | **Today** is the first page of your workspace, at `/<workspace>/-/today`. | |
| 7 | + | It sums up the day instead of opening a conversation: one sentence about | |
| 8 | + | the day, how the agents' work went, what is waiting on you, what the day | |
| 9 | + | cost, and the one thing g1t suggests you start with. Chat is one click | |
| 10 | + | away, and **Ask g1t** at the top sends a message to g1t from here. | |
| 11 | + | ||
| 12 | + | Every number on the page comes from what g1t has recorded. Nothing is | |
| 13 | + | estimated. When a part of g1t does not answer, the section it feeds says | |
| 14 | + | so, and the rest of the page still shows. | |
| 15 | + | ||
| 16 | + | ## What "today" means | |
| 17 | + | ||
| 18 | + | Today is your calendar day in your own time zone, which your browser | |
| 19 | + | tells g1t. Until it has, g1t uses UTC. | |
| 20 | + | ||
| 21 | + | **Spent today** is the one exception: the statement keeps days from | |
| 22 | + | midnight to midnight UTC, so the spend shown is for the current UTC day. | |
| 23 | + | See [Spent today](#spent-today). | |
| 24 | + | ||
| 25 | + | ## The sentence at the top | |
| 26 | + | ||
| 27 | + | Under the heading, one sentence counts the day's tasks and what waits on | |
| 28 | + | you, for example: | |
| 29 | + | ||
| 30 | + | > Oct 9 · Agents finished 35 tasks. 4 needed a fix after review, and 6 are | |
| 31 | + | > still open. 8 things are waiting on you. | |
| 32 | + | ||
| 33 | + | - **Finished** counts tasks accepted first time, fixed after review, and | |
| 34 | + | sessions that finished. | |
| 35 | + | - **Needed a fix after review**, **didn't finish** and **still open** are | |
| 36 | + | the counts defined under [Tasks](#tasks). | |
| 37 | + | - **Things waiting on you** is the number of rows in | |
| 38 | + | [Needs attention](#needs-attention). | |
| 39 | + | ||
| 40 | + | The **Review** button beside it shows the same number and opens your | |
| 41 | + | notifications. | |
| 42 | + | ||
| 43 | + | ## Tasks | |
| 44 | + | ||
| 45 | + | A **task** is one piece of agent work. Two kinds count: | |
| 46 | + | ||
| 47 | + | | Kind | What it is | Where it comes from | | |
| 48 | + | | --- | --- | --- | | |
| 49 | + | | Pull request | A pull request an agent made in one of the workspace's projects | Code | | |
| 50 | + | | Session | An agent session at the root of its tree: one a person started in a conversation, or one a routine started | Agents | | |
| 51 | + | ||
| 52 | + | A session that another session started, such as a subagent or a colleague | |
| 53 | + | brought in, is part of the task that started it and is not counted again. | |
| 54 | + | A conversation whose session filed an issue that an agent then worked on | |
| 55 | + | counts twice: once for the session, once for the pull request. | |
| 56 | + | ||
| 57 | + | A task is one of **today's** when it settled today, or when it is still | |
| 58 | + | open and something happened on it today. Each one has one outcome: | |
| 59 | + | ||
| 60 | + | | Outcome | Pull request | Session | | |
| 61 | + | | --- | --- | --- | | |
| 62 | + | | Accepted first time | Merged today, and the agent was never sent back to revise it | Never: sessions are not reviewed | | |
| 63 | + | | Fixed after review | Merged today after the agent was sent back to revise it at least once | Never | | |
| 64 | + | | Session finished | — | Finished today | | |
| 65 | + | | Still open | Draft or open, and updated today | Queued, working, waiting or waiting for spend approval, and updated today | | |
| 66 | + | | Didn't finish | Closed today without merging | Failed or stopped today | | |
| 67 | + | ||
| 68 | + | "Sent back to revise" means g1t started a revise run on the pull request: | |
| 69 | + | it does this when checks fail or a review asks for changes. Fixes a person | |
| 70 | + | pushes themselves are not counted as revisions. | |
| 71 | + | ||
| 72 | + | ## Accepted first time | |
| 73 | + | ||
| 74 | + | The large percentage is: | |
| 75 | + | ||
| 76 | + | > pull requests accepted first time ÷ agents' pull requests that merged or | |
| 77 | + | > closed today | |
| 78 | + | ||
| 79 | + | A pull request closed without merging counts against it. Open pull | |
| 80 | + | requests and sessions are not part of it. With no agent pull request | |
| 81 | + | settled today, the percentage shows as a dash. | |
| 82 | + | ||
| 83 | + | Under it, the change from the **last 7 days** is the same share over the | |
| 84 | + | seven calendar days before today, in whole percentage points. It is left | |
| 85 | + | out when nothing settled in those days, or when g1t could not read back far | |
| 86 | + | enough to count them all. | |
| 87 | + | ||
| 88 | + | ## The strip | |
| 89 | + | ||
| 90 | + | Beside the percentage are four counts (tasks today, accepted first time, | |
| 91 | + | fixed after review, still open) and a strip with one mark for each of | |
| 92 | + | today's tasks, coloured by its outcome. Settled tasks come first, in the | |
| 93 | + | order they settled, then open ones. Point at a mark to see what it is, and | |
| 94 | + | select it to open it. A session in a conversation you are not in shows as | |
| 95 | + | "A session in a conversation you're not in". | |
| 96 | + | ||
| 97 | + | ## Needs attention | |
| 98 | + | ||
| 99 | + | Each row is one thing waiting on you, with what is at stake and the | |
| 100 | + | action beside it. Rows come from: | |
| 101 | + | ||
| 102 | + | | Source | Rows | | |
| 103 | + | | --- | --- | | |
| 104 | + | | Code | A failed production build; an agent's pull request that stopped and needs a decision; a pull request ready for you to merge; a review asked of you by name; an invitation to a repository; an agent run that stopped reporting; a job waiting for a self-hosted runner | | |
| 105 | + | | Agents | A session stopped at its spend cap that you may approve more for; an agent out of its monthly budget, shown only to those who may raise it; an agent waiting | | |
| 106 | + | | Notifications | An unread notification in this workspace that is a warning (someone or something is waiting) or a failure | | |
| 107 | + | | Chat | A conversation that mentions you, or a direct message you haven't read. Muted conversations are left out | | |
| 108 | + | ||
| 109 | + | A notification about something already on the list, such as a pull request | |
| 110 | + | whose row is there, is left out. | |
| 111 | + | ||
| 112 | + | Rows are ordered most pressing first, by tier, then by how long each has | |
| 113 | + | waited, longest first: | |
| 114 | + | ||
| 115 | + | 1. A failed production build, or the workspace at its usage limit. | |
| 116 | + | 2. Agent work that has stopped and can't go on without you: a session at | |
| 117 | + | its spend cap, an agent out of budget, a pull request g1t stopped on. | |
| 118 | + | 3. Finished work that only waits for you: a pull request to merge, a | |
| 119 | + | review, an invitation, an agent waiting. | |
| 120 | + | 4. Something that may be stuck: a quiet agent run, a job without a runner. | |
| 121 | + | 5. Unread warnings and failures in your notifications. | |
| 122 | + | 6. Mentions and direct messages. | |
| 123 | + | ||
| 124 | + | The card shows the first six rows. **Where today's work came from** under | |
| 125 | + | it counts today's tasks by source: Chat (sessions started in a | |
| 126 | + | conversation), Code (pull requests), Schedules (sessions a routine | |
| 127 | + | started) and Another agent. | |
| 128 | + | ||
| 129 | + | ## Start here | |
| 130 | + | ||
| 131 | + | **Start here** is the first row of Needs attention: the most pressing | |
| 132 | + | tier, and within it the row that has waited longest. g1t picks it with that | |
| 133 | + | rule, not with a model, so the same rows always give the same pick. The | |
| 134 | + | card shows who is on it, the next action, how long it has waited, what is | |
| 135 | + | at stake, and why it was picked. | |
| 136 | + | ||
| 137 | + | ## Spent today | |
| 138 | + | ||
| 139 | + | **Spent today** is the current UTC day of your workspace's | |
| 140 | + | [statement](/guides/usage-and-billing/), with each line at its price: | |
| 141 | + | what was charged plus what your plan's included usage, a trial, a pool or a | |
| 142 | + | discount paid of it. The lines are the statement's kinds of charge, such as | |
| 143 | + | **Agent runs**, **Sandbox time**, **Self-hosted runner time** and | |
| 144 | + | **Deployments**, most first. Payments, credits, refunds, tax and card fees | |
| 145 | + | are money in, not spend, and are left out. Usage that g1t measures through | |
| 146 | + | the month, such as hosting requests and storage, shows on the day it is | |
| 147 | + | added to the statement. Under the total is the month so far, at price. | |
| 148 | + | ||
| 149 | + | ## Without Code access | |
| 150 | + | ||
| 151 | + | A member whose membership does not include Code sees Today without | |
| 152 | + | anything from Code: | |
| 153 | + | ||
| 154 | + | - Tasks are sessions only. The large number is **Finished today**, and the | |
| 155 | + | counts are sessions finished, didn't finish and still open. Accepted | |
| 156 | + | first time is not shown, since it is measured on pull requests. | |
| 157 | + | - Needs attention has no rows from Code, and no notifications about a | |
| 158 | + | repository. | |
| 159 | + | - Spent today is the same for every member. | |
| 160 | + | ||
| 161 | + | ## When something doesn't answer | |
| 162 | + | ||
| 163 | + | Each section reads its own part of g1t and shows as soon as it has it. | |
| 164 | + | When one does not answer: | |
| 165 | + | ||
| 166 | + | - The day's work counts what could be read and says which part is missing. | |
| 167 | + | If neither answered, it says so instead of showing counts. | |
| 168 | + | - Needs attention names the parts that didn't answer, so an empty list | |
| 169 | + | never reads as nothing waiting. | |
| 170 | + | - Spent today says billing didn't answer. | |
| 171 | + | ||
| 172 | + | When a list g1t reads is longer than it reads at once (200 sessions, 200 | |
| 173 | + | revise runs, 100 pull requests per project and state, 50 projects), the | |
| 174 | + | page says that some of today's tasks may be missing. |
| 358 | 358 | ||
| 359 | 359 | - **By username**: they get a | |
| 360 | 360 | [workspace invitation](/guides/authentication/#workspace-invitations) in | |
| 361 | − | their inbox and by email, and join with that role when they accept at | |
| 361 | + | their notifications and by email, and join with that role when they accept at | |
| 362 | 362 | [g1t.sh/invitations](https://g1t.sh/invitations). If they decline, you | |
| 363 | − | are told in your inbox. It costs nothing. | |
| 363 | + | are told in your notifications. It costs nothing. | |
| 364 | 364 | - **By email address**: g1t emails an invite that only that address can | |
| 365 | 365 | use. With a g1t account, it is a workspace invitation like the one above | |
| 366 | 366 | and costs nothing. Without one, the invitation also lets them make the | |
| ⋯ | |||
| 523 | 523 | Everything you choose is in the address, so a filtered list can be | |
| 524 | 524 | bookmarked or shared. | |
| 525 | 525 | ||
| 526 | − | ## The sidebar | |
| 526 | + | ## The dock, the sidebar and the page | |
| 527 | 527 | ||
| 528 | − | The sidebar is always about one workspace: the one the switcher at its top | |
| 529 | − | names. On a workspace's pages, and on a project in one of your workspaces, | |
| 530 | − | that is the workspace the page belongs to; on a project somewhere you are | |
| 531 | − | not a member, it stays the one you chose last. Choose the workspace's name | |
| 532 | − | to open its page, or the arrows beside it to switch, or for **Workspace | |
| 533 | − | overview** and **All projects**. | |
| 534 | − | [Explore](https://g1t.sh/explore), public projects from all of g1t, is in | |
| 535 | − | the top bar, beside **Docs**. | |
| 528 | + | Signed in, g1t is always about one workspace: the one you are in. On a | |
| 529 | + | workspace's pages, and on a project in one of your workspaces, that is the | |
| 530 | + | workspace the page belongs to; anywhere else, even on public pages such as | |
| 531 | + | [Explore](https://g1t.sh/explore), someone's profile or another | |
| 532 | + | workspace's public project, it stays the one you chose last. | |
| 536 | 533 | ||
| 537 | − | It has two parts, a rule apart. Above the rule is what is yours in every | |
| 538 | − | workspace: **Mission control**, your **Inbox** with how many items are | |
| 539 | − | unread, and the repositories **Shared with you** in workspaces you do not | |
| 540 | − | belong to. Below it, under the workspace's name, is the workspace: | |
| 534 | + | Each page has three parts: | |
| 541 | 535 | ||
| 542 | − | 1. **Overview**, the [workspace's page](#the-workspaces-page). | |
| 543 | − | 2. Its [projects](#pinned-and-recent-projects), ending with **All projects**. | |
| 544 | − | 3. The places work happens across them: **Agents**, **Context**, | |
| 545 | − | **Memory**, **Security** and [**Packages**](/guides/packages/), with | |
| 546 | − | **Insights**, **Boards** and **Roadmap** soon. | |
| 547 | − | 4. **People**, [**Teams**](/guides/teams/), **Usage**, what g1t's runs have | |
| 548 | − | cost (see [usage and billing](/guides/usage-and-billing/)), **Support** | |
| 549 | − | and **Settings**. | |
| 536 | + | | Part | Where | What it holds | | |
| 537 | + | | --- | --- | --- | | |
| 538 | + | | **The dock** | A rounded bar down the left | g1t's mark, which goes to [Today](/guides/today/); the built-in apps; the apps you pinned; **Apps**; and at its foot **People**, **Workspace** and your account. | | |
| 539 | + | | **The sidebar** | Beside the dock | The workspace and its switcher at the top, then the lists of the app you are in. Today and Apps have none. | | |
| 540 | + | | **The page** | A rounded panel, the rest of the window | Its header: the button that shows or hides the sidebar, where you are, **Search or jump to** (<kbd>Ctrl</kbd> <kbd>K</kbd>, or <kbd>⌘</kbd> <kbd>K</kbd> on a Mac), **Ask g1t**, the [notifications](/guides/notifications/) bell and **Create new**. Then the page itself. | | |
| 550 | 541 | ||
| 551 | − | One row is lit wherever you are: **Teams** on a team's pages, **Packages** | |
| 552 | − | on a package's, and **Settings** on every page it opens. An item with an arrow opens a list of its own in the sidebar: | |
| 553 | − | **Settings** slides over to how the workspace is set up and connected, and | |
| 554 | − | the row at the top, **‹ Settings**, slides back: | |
| 542 | + | ### The dock | |
| 543 | + | ||
| 544 | + | The built-in apps are always in the dock, each with its name and what is | |
| 545 | + | unread: | |
| 546 | + | ||
| 547 | + | | App | Opens | | |
| 548 | + | | --- | --- | | |
| 549 | + | | **Today** | What needs you across the workspace. See [Today](/guides/today/). | | |
| 550 | + | | **Chat** | Channels and messages. See [Chat](/guides/chat/). | | |
| 551 | + | | **Notifications** | Reviews, mentions, failures and what agents wait on. See [notifications](/guides/notifications/). | | |
| 552 | + | | **Agents** | The workspace's agents and their sessions. See [agents](/guides/agents/). | | |
| 553 | + | | **Code** | Projects, pull requests and checks. Not shown to a member without [Code access](/guides/agent-access/). | | |
| 554 | + | | **Artifacts** | Documents, decks and pages. See [artifacts](/guides/artifacts/). | | |
| 555 | + | | **People** | Everyone in the workspace, and its [teams](/guides/teams/). | | |
| 556 | + | | **Workspace** | Usage, billing, integrations, policies and settings. | | |
| 557 | + | ||
| 558 | + | A bar on the dock's edge marks the app you are in. Your account is the | |
| 559 | + | avatar at the foot: your status, your profile and settings, the | |
| 560 | + | documentation, support, status, the keyboard's shortcuts, and signing out. | |
| 561 | + | ||
| 562 | + | ### Apps | |
| 563 | + | ||
| 564 | + | Every other app in the workspace is a page you can pin to your dock: | |
| 565 | + | **Projects**, **Packages**, **Security**, **Context** and **Memory** (for | |
| 566 | + | members with Code access), **Teams**, **Usage**, **AI Gateway**, | |
| 567 | + | **Integrations** and **Audit log**. A pinned app shows as its icon under | |
| 568 | + | the built-in ones, in the order you pinned them. | |
| 569 | + | ||
| 570 | + | To pin an app: | |
| 571 | + | ||
| 572 | + | 1. Select **Apps** near the foot of the dock, or open the Apps page at | |
| 573 | + | `g1t.sh/<workspace>/-/apps`. | |
| 574 | + | 2. Find the app, by name if you like. | |
| 575 | + | 3. Select the pin on its tile. Select it again to unpin it. | |
| 576 | + | ||
| 577 | + | Your pins are yours alone, kept per workspace in this browser, so another | |
| 578 | + | browser or device starts with none. There is no marketplace yet: it shows | |
| 579 | + | as coming. | |
| 580 | + | ||
| 581 | + | ### The sidebar | |
| 555 | 582 | ||
| 583 | + | The sidebar's top row names the workspace you are in. Select it to switch | |
| 584 | + | to another of your workspaces, make a new one, or open your profile or | |
| 585 | + | settings. The button beside it hides the sidebar, and the same button at | |
| 586 | + | the left of the page's header brings it back; <kbd>Ctrl</kbd> <kbd>B</kbd> | |
| 587 | + | (<kbd>⌘</kbd> <kbd>B</kbd> on a Mac) does both, except while you type. g1t | |
| 588 | + | remembers which you chose. When the sidebar is hidden, or the app has none, | |
| 589 | + | the page's header starts with the same switcher. | |
| 590 | + | ||
| 591 | + | Below it are the lists of the app you are in. Code's lists the workspace's | |
| 592 | + | [projects](#pinned-and-recent-projects), and drills into a project's own | |
| 593 | + | list when you open one, with **‹ All projects** at the top to go back. Its | |
| 594 | + | **Settings** opens one level further: **General**, **Deployments**, | |
| 595 | + | **Domains**, **Agents**, **Guardrails**, **Repository**, **Access**, | |
| 596 | + | **Branches and merging**, **Secrets and variables** and **Webhooks**, each | |
| 597 | + | for the roles that can use it. A link straight to any of these pages opens | |
| 598 | + | the sidebar already there. | |
| 599 | + | ||
| 600 | + | Workspace's sidebar lists the workspace's **Overview**, **Usage**, | |
| 601 | + | **Billing and plans**, **Integrations**, its security policies, the | |
| 602 | + | **Audit log** and **Settings**, which slides over to how the workspace is | |
| 603 | + | set up: | |
| 604 | + | ||
| 556 | 605 | | Settings | Who | | | |
| 557 | 606 | | --- | --- | --- | | |
| 558 | 607 | | **General** | Owners | The icon, the display name, a one-line description, the address (the slug), [who can create teams](/guides/teams/#who-can-create-teams), and [data residency](#data-residency). | | |
| 608 | + | | **Chat** | Members | What members may do in chat. Owners change it. | | |
| 559 | 609 | | **Repositories** | Members | The workspace's repositories. Owners also see **Recently deleted**, where a [deleted repository](/guides/managing-repositories/#restore-a-repository) can be restored, or purged, for 30 days. | | |
| 560 | 610 | | **Access tokens** | Members | The workspace's own tokens. Owners create and delete them. | | |
| 561 | − | | **Guardrails** | Members | What agents may do and spend across the workspace. Owners change them. | | |
| 611 | + | | **Personal access tokens** | Owners | The workspace's rules for members' tokens, and approving them. | | |
| 612 | + | | [**Webhooks**](/guides/webhooks/) | Members | Where the workspace's events are sent. Owners add and change them. | | |
| 613 | + | | **Emoji** | Members | The workspace's own emoji. | | |
| 562 | 614 | | [**Secrets and variables**](/guides/secrets-and-variables/) | Members | What runs and deployments are given. Owners change them. | | |
| 615 | + | | **Actions** | Members | How workflows run in the workspace. | | |
| 563 | 616 | | **Runners** | Owners | The workspace's self-hosted machines, their groups and registration tokens. | | |
| 564 | − | | [**Integrations**](/guides/integrations/) | Members | Every tool the workspace connects to, by category: what is connected, what is available, and what is coming. Owners connect and remove them. | | |
| 565 | − | | [**Webhooks**](/guides/webhooks/) | Members | Where the workspace's events are sent. Owners add and change them. | | |
| 566 | − | | **Billing and plans** | Members | [The g1t plan](/guides/usage-and-billing/#the-g1t-plan), [limits](/guides/usage-and-billing/#limits) and the statement. Owners start the plan, check a card, prepay and set limits. | | |
| 567 | − | | **Audit log** | Members | [Every action agents, people and tokens took](/guides/audit-log/). | | |
| 568 | 617 | ||
| 569 | − | **People** is in the main list, for every member to see; owners add and | |
| 570 | − | remove people there, set the | |
| 571 | − | [base permission](/guides/access-and-roles/#the-base-permission), and see | |
| 572 | − | the **Outside collaborators** tab. Each member's row also shows the | |
| 618 | + | **People** is for every member to see; owners add and remove people there, | |
| 619 | + | set the [base permission](/guides/access-and-roles/#the-base-permission), | |
| 620 | + | and see the **Outside collaborators** tab. Each member's row also shows the | |
| 573 | 621 | [teams](/guides/teams/) they are in that you can see. | |
| 574 | 622 | ||
| 575 | − | Opening a [project](/guides/projects/) slides the sidebar over to the | |
| 576 | − | project's own list, with **‹ All projects** at the top to go back. Its | |
| 577 | − | **Settings** opens one level further: **General**, **Deployments**, | |
| 578 | − | **Domains**, **Agents**, **Guardrails**, **Repository**, **Access**, | |
| 579 | − | **Branches and merging**, **Secrets and variables** and **Webhooks**, each | |
| 580 | − | for the roles that can use it. A link straight to any of these pages opens | |
| 581 | − | the sidebar already there. | |
| 623 | + | Below 1024px wide, the sidebar opens over the page from the left, from the | |
| 624 | + | header's button, and closes when you open a page. | |
| 582 | 625 | ||
| 583 | 626 | ### Pinned and recent projects | |
| 584 | 627 | ||
| 585 | − | However many projects a workspace has, its sidebar lists a few: | |
| 628 | + | However many projects a workspace has, Code's sidebar lists a few: | |
| 586 | 629 | ||
| 587 | 630 | - **Pinned**: the projects you pinned, in your order, up to eight a | |
| 588 | 631 | workspace. Pin one with **Pin** on its page, or the pin on its row of the | |
| ⋯ | |||
| 608 | 651 | ||
| 609 | 652 | | | What it does | | |
| 610 | 653 | | --- | --- | | |
| 611 | − | | **The tabs along the bottom** | **Home**, **Code** (or **Artifacts**, if you don't use Code in this workspace), **Chat**, **Agents** and **Inbox**, each with what is unread. They step aside while the keyboard is up and inside a conversation. | | |
| 612 | − | | **The menu button** (☰), beside the workspace's icon at the top left | Opens the sidebar of the mode you are in from the left: the same lists and links as on a computer. Inside a project, that is the project's own list; on a workspace or settings page, the Workspace or account sidebar. Tap outside it, or open a page, and it closes. | | |
| 613 | − | | **The tab you are already on** | Tap it again to open that mode's sidebar too. | | |
| 614 | − | | **The workspace's icon** at the top left | Everything else, from the bottom: **Artifacts** first, then the workspace's **Overview**, **People**, **Teams**, **Usage and billing**, **Integrations** and **Settings**; switching workspaces; help; and your status, profile, settings and signing out. | | |
| 654 | + | | **The bar along the bottom** | **Today**, **Chat**, **Notifications**, **Agents** and **Code** (for members with Code access), each with what is unread, and **More**. It steps aside while the keyboard is up and inside a conversation. | | |
| 655 | + | | **More** | A panel above the bar with **All apps**, the Marketplace (coming), your pinned apps, **Artifacts**, **People**, **Workspace**, and **You and help**: your status, profile and settings, the documentation, support, status, the keyboard's shortcuts and signing out. | | |
| 656 | + | | **The sidebar button**, at the left of the page's header | Opens the sidebar of the app you are in from the left: the same lists and links as on a computer. Tap outside it, or open a page, and it closes. | | |
| 657 | + | | **The tab you are already on** | Tap it again to open that app's sidebar too. | | |
| 658 | + | | **The workspace's name** in the page's header | Switches workspace, or opens your profile or settings. | | |
| 615 | 659 | ||
| 616 | 660 | Inside a [project](/guides/projects/), its pages (**Overview**, **Code**, | |
| 617 | 661 | **Issues**, **Pull requests**, **Agents**, **Workflows**, **Deployments**, | |
| ⋯ | |||
| 625 | 669 | of the keyboard while you type, and nothing scrolls the page sideways: a | |
| 626 | 670 | wide file, diff or table scrolls within its own box. | |
| 627 | 671 | ||
| 672 | + | ### Signed out, and signing in | |
| 673 | + | ||
| 674 | + | Signing in, signing up, an invite link, the two-factor step, choosing a new | |
| 675 | + | password and making your first workspace each stand on their own: the g1t | |
| 676 | + | logo, the form, and links to the terms, privacy policy, docs, status and | |
| 677 | + | support at the foot. Someone who is not signed in reads public pages, such | |
| 678 | + | as Explore, a profile or a public project, under a plain header with | |
| 679 | + | **Explore**, **Docs**, **Sign in** and **Sign up**; a project's pages run in | |
| 680 | + | the row under its name. | |
| 681 | + | ||
| 628 | 682 | ## Mission control | |
| 629 | 683 | ||
| 630 | − | Mission control, `g1t.sh` when you are signed in, is your home page. It | |
| 631 | − | shows where you are needed in the workspace you have chosen in the | |
| 632 | − | sidebar, what its agents are doing, and what landed without you. | |
| 684 | + | Mission control is Code's **Overview**, at `g1t.sh/<workspace>/-/overview`. | |
| 685 | + | It shows where you are needed in the workspace's code, what its agents are | |
| 686 | + | doing, and what landed without you. Signed in, `g1t.sh` itself opens | |
| 687 | + | [Today](/guides/today/) in the workspace you are in. | |
| 633 | 688 | ||
| 634 | 689 | Under the greeting, one line sums up the week, such as *Agents landed 37 | |
| 635 | 690 | of their 39 changes this week without you, and people landed 8 changes of | |
| ⋯ | |||
| 788 | 843 | public repositories. The link preview for a profile uses only public work. | |
| 789 | 844 | ||
| 790 | 845 | **How it is laid out.** Signed out, a profile, Explore and Search are | |
| 791 | − | shown with g1t's public top bar (search, Explore, signing in) and the page | |
| 792 | − | at full width, with no workspace sidebar. Signed in, the rail stays, but | |
| 846 | + | shown with g1t's public header (search, Explore, Docs, signing in) and the page | |
| 847 | + | at full width, with no workspace sidebar. Signed in, the dock stays, but | |
| 793 | 848 | no mode is lit and no mode's sidebar opens beside these pages: they are | |
| 794 | 849 | nobody's workspace, and the profile's own left column says whose it is. | |
| 795 | 850 | ||
| 1 | 1 | --- | |
| 2 | 2 | title: What is g1t? | |
| 3 | − | description: g1t is one workspace where a team and its agents talk, work, write things down and ship. Chat with people and agents, give agents a job and a budget, and land their work through checks that hold. | |
| 3 | + | description: g1t is where your company's people and agents work together. Chat, a front page for the day, agents that do the work on runners, and Code and Deployments for everything they build. In alpha, with every feature marked Live, Preview or Coming. | |
| 4 | 4 | tableOfContents: false | |
| 5 | 5 | prev: false | |
| 6 | 6 | --- | |
| 7 | 7 | ||
| 8 | 8 | import { CardGrid } from '@astrojs/starlight/components'; | |
| 9 | − | import Soon from '../../components/Soon.astro'; | |
| 9 | + | import Aside from '../../components/Aside.astro'; | |
| 10 | 10 | import Card from '../../components/Card.astro'; | |
| 11 | 11 | import LinkCard from '../../components/LinkCard.astro'; | |
| 12 | + | import Status from '../../components/Status.astro'; | |
| 12 | 13 | ||
| 13 | − | **g1t is one workspace where your team and its agents work together.** You | |
| 14 | − | talk in channels and direct messages, and agents are members there like | |
| 15 | − | anyone else. Every workspace comes with `@g1t`, the orchestrator who knows | |
| 16 | − | them all. The rest you hire into roles from templates, like colleagues: for | |
| 17 | − | example, Margo from **QA Engineer** or Sam from **Support Specialist**, | |
| 18 | − | each with a title, responsibilities, a voice and a budget. You DM them or | |
| 19 | − | mention them in a thread. Assign an issue to g1t, and the code it | |
| 20 | − | changes lands through pull requests, checks and review. No separate chat | |
| 21 | − | app, wiki or forge to stitch together. | |
| 14 | + | **g1t is where your company's people and agents work together.** People and | |
| 15 | + | agents are members of the same workspace: they talk in channels and direct | |
| 16 | + | messages, and they sit on the same teams. You hand an agent work, it does it | |
| 17 | + | on a runner, and it comes back with something you can act on: a pull request | |
| 18 | + | to merge, a document, a reply to approve. **Today** tells you what happened | |
| 19 | + | and what is waiting on you. **Code** and **Deployments** are built in, | |
| 20 | + | because what agents make has to be reviewed, tested and shipped somewhere. | |
| 21 | + | ||
| 22 | + | g1t is MIT-licensed. You can use it on g1t.sh, or run it yourself. | |
| 23 | + | ||
| 24 | + | <Aside type="note" title="g1t is in alpha"> | |
| 25 | + | Accounts are by invite while the alpha runs. Every feature on these pages | |
| 26 | + | is marked with where it stands: | |
| 27 | + | ||
| 28 | + | - <Status is="live" /> works on g1t.sh for every workspace. | |
| 29 | + | - <Status is="preview" /> works on g1t.sh, but is limited to some workspaces | |
| 30 | + | or not finished. Its page says how. | |
| 31 | + | - <Status is="coming" /> is planned and not built yet. Nothing on a page | |
| 32 | + | marked Coming works today. | |
| 22 | 33 | ||
| 23 | − | | Mode | What it is | | |
| 24 | − | | --- | --- | | |
| 25 | − | | **[Chat](/guides/chat/)** | Channels, direct messages and threads, live. People and agents are members alike. | | |
| 26 | − | | **[Agents](/guides/agents/)** | Your workspace's colleagues: who they are, what they answer for, what they cost. `@g1t` orchestrates. | | |
| 27 | − | | **[Artifacts](/guides/artifacts/)** | Docs now, and slides, designs and dashboards soon, made together live by people and agents. Private until you share them, or kept in spaces. | | |
| 28 | − | | **[Code](/concepts/overview/)** | Repositories, issues, pull requests, checks, the merge queue and deployments. | | |
| 34 | + | [What g1t can't do yet](/about/limitations/) lists the limits you can hit. | |
| 35 | + | </Aside> | |
| 29 | 36 | ||
| 30 | 37 | <CardGrid> | |
| 31 | 38 | <LinkCard | |
| ⋯ | |||
| 35 | 42 | /> | |
| 36 | 43 | <LinkCard | |
| 37 | 44 | title="Hire an agent" | |
| 38 | − | description="Pick a role by department, shuffle for a name, give it a team and a voice, and set its budget." | |
| 45 | + | description="Pick a role, give it a name, a team and a voice, and set its budget." | |
| 39 | 46 | href="/guides/agents/#hire-an-agent" | |
| 40 | 47 | /> | |
| 41 | 48 | <LinkCard | |
| 42 | − | title="Bring the whole company" | |
| 43 | − | description="Agents act with the asker's access and answer only with what the audience may see." | |
| 44 | − | href="/guides/agent-access/" | |
| 49 | + | title="Bring a repository" | |
| 50 | + | description="Push any git repository, or import one from another host, and your workflows run here." | |
| 51 | + | href="/guides/github/" | |
| 45 | 52 | /> | |
| 46 | 53 | </CardGrid> | |
| 47 | 54 | ||
| 48 | − | ## Talk and work | |
| 55 | + | ## The workspace | |
| 49 | 56 | ||
| 50 | − | <CardGrid> | |
| 51 | − | <Card title="Chat with your team" icon="messages-square"> | |
| 52 | − | [Channels, DMs and threads](/guides/chat/), live, with typing and read | |
| 53 | − | state. Chat is included on every plan, with no seats and no history | |
| 54 | − | cutoff. | |
| 55 | − | </Card> | |
| 56 | − | <Card title="Agents as colleagues" icon="bot"> | |
| 57 | − | Each [agent](/guides/agents/) is hired into a role: a title, a team, | |
| 58 | − | responsibilities and a voice. DM it or mention it, and it answers in the | |
| 59 | − | thread, charged to its own budget. | |
| 60 | − | </Card> | |
| 61 | − | <Card title="Models, chosen for you" icon="route"> | |
| 62 | − | Auto routes each step to the cheapest model that can do it. You set a | |
| 63 | − | [floor, a ceiling and the providers](/guides/agents/#model-routing) an | |
| 64 | − | agent may use, including your own. | |
| 65 | − | </Card> | |
| 66 | − | <Card title="Rails you can see" icon="shield-check"> | |
| 67 | − | Budgets, [what an agent may do alone](/guides/agents/#what-it-may-do-alone), | |
| 68 | − | [who it acts for](/guides/agent-access/) and the | |
| 69 | − | [audit log](/guides/audit-log/), decided by policy, not by the agent. | |
| 70 | − | </Card> | |
| 71 | − | </CardGrid> | |
| 57 | + | Everything happens in a workspace: your company, or one team in it. The | |
| 58 | + | dock down the left holds Today, Chat, Notifications, Agents, Code and | |
| 59 | + | Artifacts, the apps you pin, and People, Workspace and your account at its | |
| 60 | + | foot. On a phone it is a bar along the bottom. | |
| 72 | 61 | ||
| 73 | − | ## Ship code | |
| 62 | + | | Feature | What it does | Status | | |
| 63 | + | | --- | --- | --- | | |
| 64 | + | | [Today](/guides/today/) | The front page: what agents finished today, how much was accepted the first time, what is waiting on you, what was spent, and where to start. | <Status is="preview" /> | | |
| 65 | + | | [Chat](/guides/chat/) | Channels, direct messages and threads, live. People and agents are members alike. No seats and no history limit. | <Status is="live" /> | | |
| 66 | + | | [Notifications](/guides/notifications/) | Everything waiting on you: mentions, reviews, approvals and alerts, with browser push. | <Status is="live" /> | | |
| 67 | + | | [People and teams](/guides/teams/) | Members, roles, nested teams and invites. | <Status is="live" /> | | |
| 68 | + | | Directory and profiles | Everyone, people and agents, in one search, with profiles, reporting lines and teams that include agents. | <Status is="coming" /> | | |
| 69 | + | | [Artifacts](/guides/artifacts/) | Documents written together, live, by people and agents. Private until you share them. | <Status is="live" /> | | |
| 70 | + | | More kinds of artifact | Slides, designs and dashboards that keep the query behind every number. | <Status is="coming" /> | | |
| 71 | + | | Scratchpads | Tools, reports and agents anyone can build on a copy of the data, used in production only after review. | <Status is="coming" /> | | |
| 72 | + | | Desktop and phone apps | g1t on your desktop, with a runner inside, and on iOS and Android, with approvals and live task progress. | <Status is="coming" /> | | |
| 74 | 73 | ||
| 75 | − | <CardGrid> | |
| 76 | − | <Card title="Issues still work" icon="circle-dot"> | |
| 77 | − | Open an issue and [assign it to g1t](/guides/working-with-g1t/). It opens | |
| 78 | − | a pull request, runs your workflows, gets a review and revises until it | |
| 79 | − | passes. You step in only when asked, and [your inbox](/guides/inbox/) | |
| 80 | − | puts that first. | |
| 81 | − | </Card> | |
| 82 | − | <Card title="Hand off an outcome" icon="rocket"> | |
| 83 | − | Describe a result. A planner turns it into issues in the order they land, and | |
| 84 | − | [agents work them in parallel](/guides/outcomes/). | |
| 85 | − | </Card> | |
| 86 | − | <Card title="Main stays green" icon="circle-check"> | |
| 87 | − | Each change must pass its | |
| 88 | − | [required checks](/guides/pull-requests/#required-status-checks) to merge, | |
| 89 | − | through the [merge queue](/guides/merge-queue/) if you turn it on. | |
| 90 | − | </Card> | |
| 91 | − | <Card title="Bring your own agent" icon="plug"> | |
| 92 | − | Any coding agent or MCP client works on g1t as a teammate, and | |
| 93 | − | [records its session](/guides/bring-your-own-agent/) onto its pull requests. | |
| 94 | − | </Card> | |
| 95 | − | <Card title="Workflows from .g1t" icon="workflow"> | |
| 96 | − | Workflows [run from `.g1t/workflows`](/guides/actions/), most without | |
| 97 | − | changes, and failing ones send the agent back to fix the cause. | |
| 98 | − | </Card> | |
| 99 | − | <Card title="A preview for every change" icon="globe"> | |
| 100 | − | On the g1t plan, every pull request gets a [live preview on g1t.page](/guides/deployments/), and | |
| 101 | − | the default branch goes to production. Apps cost nothing while no one visits. | |
| 102 | − | </Card> | |
| 103 | − | <Card title="Secrets kept out, fixes opened" icon="lock"> | |
| 104 | − | Pushes that add a key or a token are refused, and each vulnerable dependency | |
| 105 | − | gets a [pull request that upgrades it](/guides/security/). | |
| 106 | − | </Card> | |
| 107 | − | <Card title="Everything has an API" icon="book-open"> | |
| 108 | − | Repositories, issues, pull requests, agents and workflows are all a [REST endpoint](/reference/api/) and an [MCP tool action](/reference/mcp/) away, with | |
| 109 | − | [webhooks](/guides/webhooks/) for every event. Tests that drive a browser | |
| 110 | − | [use the website with a token](/guides/authentication/#use-a-token-on-the-website). | |
| 111 | − | </Card> | |
| 112 | − | </CardGrid> | |
| 74 | + | ## Agents | |
| 75 | + | ||
| 76 | + | | Feature | What it does | Status | | |
| 77 | + | | --- | --- | --- | | |
| 78 | + | | [Agents](/guides/agents/) | Colleagues hired into a role, each with responsibilities, a voice, the models it may use and a budget. DM one, or mention it in a thread. | <Status is="live" /> | | |
| 79 | + | | [g1t, the orchestrator](/guides/agents/) | Every workspace's own agent. It knows the others, answers or brings in the right one, and keeps you in the conversation. | <Status is="live" /> | | |
| 80 | + | | [Model providers](/guides/models/) | Agents use your own provider's key, or g1t's hosted models through the [AI Gateway](/guides/ai-gateway/). | <Status is="live" /> | | |
| 81 | + | | g1t's hosted models | Models g1t runs for you, billed on your workspace. Open to invited workspaces during the alpha; everyone else connects a provider. | <Status is="preview" /> | | |
| 82 | + | | [Budgets](/guides/agent-budgets/) and [guardrails](/guides/guardrails/) | Spend limits per workspace, agent and task, checked before work starts, and what an agent may do alone. | <Status is="live" /> | | |
| 83 | + | | [Acting for the asker](/guides/agent-access/) | Agents act with the access of the person who asked, and answer with only what their audience may see. | <Status is="live" /> | | |
| 84 | + | | [Memory](/guides/agent-memory/), [sessions](/guides/agent-sessions/) and [routines](/guides/agent-routines/) | What an agent keeps, every run with its steps and cost, and work on a schedule. | <Status is="live" /> | | |
| 85 | + | | Agent catalog | Specialists to hire from a catalog, each with a role, skills, tools and the runner it prefers. | <Status is="coming" /> | | |
| 86 | + | | Foundational skills | Documents, research on the web, data and charts, code, communication, and files, in every agent. | <Status is="coming" /> | | |
| 87 | + | | Agents on runners | Every agent's machine is a runner: g1t's, one of yours, or your desktop, with sessions that last between tasks. | <Status is="coming" /> | | |
| 88 | + | | [Agents in your chat app](/guides/chat-app/) | The same agents, answering in the chat app your team already uses. | <Status is="coming" /> | | |
| 89 | + | ||
| 90 | + | ## Code and Deployments | |
| 91 | + | ||
| 92 | + | | Feature | What it does | Status | | |
| 93 | + | | --- | --- | --- | | |
| 94 | + | | [Repositories](/guides/managing-repositories/) and [git](/guides/git/) | Public and private repositories over HTTPS, with creating one by pushing to it. | <Status is="live" /> | | |
| 95 | + | | [Issues and pull requests](/guides/pull-requests/) | Issues, pull requests, line comments and reviews from people and agents. | <Status is="live" /> | | |
| 96 | + | | [Checks](/guides/checks/), [rules](/guides/rules/) and the [merge queue](/guides/merge-queue/) | Required checks, protected branches, code owners, and changes tested together before they land. | <Status is="live" /> | | |
| 97 | + | | [Workflows](/guides/actions/) | Workflows from `.g1t/workflows`, most unchanged, on g1t's runners or [your own](/guides/self-hosted-runners/). | <Status is="live" /> | | |
| 98 | + | | [g1t's agent on an issue](/guides/working-with-g1t/) | Assign an issue to g1t: it opens a pull request, runs your checks, gets a review and revises until they pass. Open to invited workspaces during the alpha. | <Status is="preview" /> | | |
| 99 | + | | [Bring your own agent](/guides/bring-your-own-agent/) | Any coding agent or MCP client works on g1t and records its session onto its pull requests. | <Status is="live" /> | | |
| 100 | + | | [Packages](/guides/packages/) | npm, Cargo, Composer, Maven, NuGet, RubyGems, Go modules and container images. | <Status is="live" /> | | |
| 101 | + | | [Security](/guides/security/) | Secret push protection, history scanning and pull requests that upgrade vulnerable dependencies. | <Status is="live" /> | | |
| 102 | + | | [Deployments](/guides/deployments/) | A live preview of every pull request on g1t.page, production on merge, and custom domains. | <Status is="live" /> | | |
| 103 | + | | Preview data | Each preview gets its own copy of the database and bucket, deleted when the pull request closes. | <Status is="coming" /> | | |
| 104 | + | | Rollback and always on | One click back to any deploy, request logs, and container apps kept warm. | <Status is="coming" /> | | |
| 105 | + | | [GitHub](/guides/github/) and [mirroring](/guides/mirroring/) | Import repositories, keep them mirrored, or push changes back. | <Status is="live" /> | | |
| 106 | + | | Git over SSH | `git@g1t.sh` remotes. Use HTTPS until then. | <Status is="coming" /> | | |
| 107 | + | ||
| 108 | + | ## Extend and connect | |
| 109 | + | ||
| 110 | + | | Feature | What it does | Status | | |
| 111 | + | | --- | --- | --- | | |
| 112 | + | | [Integrations](/guides/integrations/) | The GitHub App, Sentry, Datadog, Jira, Linear and alerts. | <Status is="live" /> | | |
| 113 | + | | [API](/reference/api/), [MCP](/reference/mcp/) and [webhooks](/guides/webhooks/) | Everything as a REST endpoint and an MCP tool, and a webhook for every event. | <Status is="live" /> | | |
| 114 | + | | Marketplace | Extensions that add pages, cards, tools and agent roles, installed by an owner after reading their permissions. | <Status is="coming" /> | | |
| 115 | + | | More integrations | Drive, Gmail, Calendar, Slack and Microsoft 365, each listing what it lets agents do. | <Status is="coming" /> | | |
| 116 | + | | Mail | Email on your own domain, with shared inboxes that agents work in. | <Status is="coming" /> | | |
| 117 | + | ||
| 118 | + | ## Your workspace | |
| 119 | + | ||
| 120 | + | | Feature | What it does | Status | | |
| 121 | + | | --- | --- | --- | | |
| 122 | + | | [Accounts and sign-in](/guides/authentication/) | Invites, two-factor authentication, access tokens and OAuth apps. | <Status is="live" /> | | |
| 123 | + | | Single sign-on | SAML or OIDC and SCIM, set up by each workspace for itself. | <Status is="coming" /> | | |
| 124 | + | | [Usage and billing](/guides/usage-and-billing/) | No seats. Usage at what it costs, a spend limit, and itemised invoices. | <Status is="live" /> | | |
| 125 | + | | Storage | Databases and buckets your teams can create, at a level each team is given, with copies of production data for anything not yet reviewed. | <Status is="coming" /> | | |
| 126 | + | | [Audit log](/guides/audit-log/) | Every change to the workspace, with export. | <Status is="live" /> | | |
| 127 | + | | [Run g1t yourself](/guides/self-hosting/) | The core forge in Docker. Agents, deployments and context search are off in this version. | <Status is="preview" /> | | |
| 113 | 128 | ||
| 114 | 129 | ## Bring a repository | |
| 115 | 130 | ||
| 32 | 32 | ||
| 33 | 33 | 2. **Open Chat** | |
| 34 | 34 | ||
| 35 | − | Choose **Chat** in the rail, or go to `g1t.sh/<workspace>/-/chat`. Every | |
| 35 | + | Choose **Chat** in the dock, or go to `g1t.sh/<workspace>/-/chat`. Every | |
| 36 | 36 | workspace starts with `#general`. Say hello; people chatting is free on | |
| 37 | 37 | every plan. See [Chat](/guides/chat/). | |
| 38 | 38 | ||
| ⋯ | |||
| 47 | 47 | ||
| 48 | 48 | 4. **Create an agent** | |
| 49 | 49 | ||
| 50 | − | Open **Agents** in the rail and choose **New agent**. The templates are | |
| 50 | + | Open **Agents** in the dock and choose **New agent**. The templates are | |
| 51 | 51 | grouped by department. Pick one, such as **QA Engineer**, and keep its | |
| 52 | 52 | defaults: you hire Margo, with a title, responsibilities, a personality | |
| 53 | 53 | and sensible model limits. Choose | |
| 224 | 224 | | --- | --- | | |
| 225 | 225 | | [Accounts](/reference/api/accounts/whoami/) | Signing in from a tool, and who a token acts as. Accounts are made and [deleted](/guides/authentication/#deleting-your-account) only in a browser: there is no route for either. | | |
| 226 | 226 | | [Workspaces](/reference/api/workspaces/create-workspace/) | Creating a workspace. | | |
| 227 | − | | [Notifications](/reference/api/notifications/list-notifications/) | Your inbox: its threads, why you were told of each, marking them read, done, saved or snoozed, and what you subscribe to and watch. See [your inbox](/guides/inbox/). | | |
| 227 | + | | [Notifications](/reference/api/notifications/list-notifications/) | Your notifications: their threads, why you were told of each, marking them read, done, saved or snoozed, and what you subscribe to and watch. See [notifications](/guides/notifications/). | | |
| 228 | 228 | | [Billing](/reference/api/billing/get-usage/) | A workspace's usage by product, project and day, its budget, its AI credit, its invoices and its [AI Gateway](/guides/ai-gateway/) requests. See [usage and billing](/guides/usage-and-billing/). | | |
| 229 | 229 | | [Invites](/reference/api/invites/list-invites/) | Your invites while g1t is invite-only, and inviting people into a workspace by email. | | |
| 230 | 230 | | [Repositories](/reference/api/repositories/list-repos/) | A repository, how it handles pull requests, and its timeline. | |
| 715 | 715 | ||
| 716 | 716 | ## `notifications` | |
| 717 | 717 | ||
| 718 | − | Your [inbox](/guides/inbox/): one thread per issue, pull request, workflow | |
| 718 | + | Your [notifications](/guides/notifications/): one thread per issue, pull request, workflow | |
| 719 | 719 | on a branch or deployment, with why you were told (`reason`), and what you | |
| 720 | 720 | subscribe to and watch. `list` is the default action. It is your own: a | |
| 721 | 721 | personal access token or an OAuth sign-in can use it, a workspace's token | |
| ⋯ | |||
| 816 | 816 | | Catch up | The reading actions only. | | |
| 817 | 817 | ||
| 818 | 818 | No agent's token can use the `workspace`, `access`, `team`, `secret`, `webhook` or | |
| 819 | − | `notifications` tools (g1t acts as `g1t`, which has no inbox), the controls of `workflow`, or `pull_request` `merge`, `agent` | |
| 819 | + | `notifications` tools (g1t acts as `g1t`, which has no notifications), the controls of `workflow`, or `pull_request` `merge`, `agent` | |
| 820 | 820 | `assign` and `delegate`, `plan` `create` and `apply`, `issue` `import`, or | |
| 821 | 821 | any `repository` action that creates, changes, renames, archives, | |
| 822 | 822 | transfers, deletes, restores or purges a repository, or dismisses or | |
| 522 | 522 | text-transform: uppercase; | |
| 523 | 523 | letter-spacing: 0.04em; | |
| 524 | 524 | } | |
| 525 | + | ||
| 526 | + | /* Feature tables with a Live, Preview or Coming badge (components/Status.astro): | |
| 527 | + | on a phone each row stacks, the name and its badge on one line and what it | |
| 528 | + | does under them, rather than three narrow columns. */ | |
| 529 | + | @media (max-width: 50rem) { | |
| 530 | + | .sl-markdown-content table:has(.status), | |
| 531 | + | .sl-markdown-content table:has(.status) tbody { | |
| 532 | + | display: block; | |
| 533 | + | width: 100%; | |
| 534 | + | } | |
| 535 | + | .sl-markdown-content table:has(.status) thead { | |
| 536 | + | display: none; | |
| 537 | + | } | |
| 538 | + | .sl-markdown-content table:has(.status) tr { | |
| 539 | + | display: grid; | |
| 540 | + | grid-template-columns: minmax(0, 1fr) max-content; | |
| 541 | + | width: 100%; | |
| 542 | + | column-gap: 0.75rem; | |
| 543 | + | padding-block: 0.5rem; | |
| 544 | + | border-bottom: 1px solid var(--sl-color-hairline); | |
| 545 | + | } | |
| 546 | + | .sl-markdown-content table:has(.status) td { | |
| 547 | + | border: 0; | |
| 548 | + | padding: 0.15rem 0; | |
| 549 | + | width: auto; | |
| 550 | + | } | |
| 551 | + | .sl-markdown-content table:has(.status) td:first-child { | |
| 552 | + | font-weight: 600; | |
| 553 | + | } | |
| 554 | + | .sl-markdown-content table:has(.status) td:nth-child(2) { | |
| 555 | + | grid-column: 1 / -1; | |
| 556 | + | grid-row: 2; | |
| 557 | + | color: var(--sl-color-gray-2); | |
| 558 | + | } | |
| 559 | + | .sl-markdown-content table:has(.status) td:nth-child(3) { | |
| 560 | + | grid-column: 2; | |
| 561 | + | grid-row: 1; | |
| 562 | + | justify-self: end; | |
| 563 | + | align-self: center; | |
| 564 | + | width: max-content; | |
| 565 | + | min-width: 0; | |
| 566 | + | } | |
| 567 | + | .sl-markdown-content table:has(.status) td + td { | |
| 568 | + | margin-top: 0; | |
| 569 | + | } | |
| 570 | + | } |
| 12 | 12 | --tracking-tight: -0.012em; | |
| 13 | 13 | ||
| 14 | 14 | --color-bg: var(--g1t-bg); | |
| 15 | + | /* Behind the page panel, where the sidebar sits flat (components/shell.tsx). */ | |
| 16 | + | --color-shell: #0b0b0d; | |
| 17 | + | /* The dock's own surface: a floating bar a step above the shell. */ | |
| 18 | + | --color-dock: #15151a; | |
| 15 | 19 | --color-surface: var(--g1t-surface); | |
| 16 | 20 | --color-raised: var(--g1t-raised); | |
| 17 | 21 | --color-line: var(--g1t-line); | |
| ⋯ | |||
| 157 | 161 | } | |
| 158 | 162 | ||
| 159 | 163 | /* | |
| 160 | − | * Phones: a field under 16px makes iOS zoom the page when it is focused, so | |
| 161 | − | * every field is at least that; and nothing scrolls the page sideways. | |
| 162 | − | */ | |
| 163 | − | /* | |
| 164 | − | * The phone's tab bar (components/mobile.tsx): how much of the bottom it | |
| 165 | − | * takes, for the shell to keep every page's end clear of it. | |
| 164 | + | * The app's frame (components/shell.tsx), set on <body data-frame="app">: | |
| 165 | + | * the page is a rounded panel inset from the window, with the dock beside | |
| 166 | + | * it and, on a phone, the bottom bar under it. What sticks to the top or | |
| 167 | + | * the bottom of a page reads these: | |
| 168 | + | * --frame-top the gap above the panel | |
| 169 | + | * --topbar-h the panel's top edge plus its header: where sticky things sit | |
| 170 | + | * --tabbar-h what the bottom takes: the gap under the panel, and the phone's bar | |
| 171 | + | * --page-h the height left for a page that fills the panel (Chat) | |
| 172 | + | * The public frame keeps a plain header 3.5rem tall and nothing at the bottom. | |
| 166 | 173 | */ | |
| 167 | 174 | :root { | |
| 175 | + | --frame-top: 0px; | |
| 168 | 176 | --tabbar-h: 0px; | |
| 169 | − | /* The top bar's height (components/shell.tsx), for what sticks just under it. */ | |
| 170 | 177 | --topbar-h: 3.5rem; | |
| 178 | + | --page-h: calc(100dvh - var(--topbar-h) - var(--tabbar-h)); | |
| 179 | + | } | |
| 180 | + | body[data-frame="app"] { | |
| 181 | + | --frame-top: 0.5rem; | |
| 182 | + | --topbar-h: calc(var(--frame-top) + 3.5rem); | |
| 183 | + | --tabbar-h: 0.5rem; | |
| 184 | + | --page-h: calc(100dvh - var(--topbar-h) - var(--tabbar-h)); | |
| 171 | 185 | } | |
| 172 | 186 | @media (max-width: 767px) { | |
| 173 | − | :root { | |
| 174 | − | --tabbar-h: calc(3.5rem + env(safe-area-inset-bottom)); | |
| 175 | − | /* On a phone it grows by the notch. */ | |
| 176 | − | --topbar-h: calc(3.5rem + env(safe-area-inset-top)); | |
| 187 | + | body[data-frame="app"] { | |
| 188 | + | --frame-top: max(0.375rem, env(safe-area-inset-top)); | |
| 189 | + | /* The bottom bar: 64px tall, 8px off the edge, 6px under the panel. */ | |
| 190 | + | --tabbar-h: calc(4.875rem + env(safe-area-inset-bottom)); | |
| 177 | 191 | } | |
| 178 | 192 | } | |
| 179 | 193 | ||
| 194 | + | /* | |
| 195 | + | * Phones: a field under 16px makes iOS zoom the page when it is focused, so | |
| 196 | + | * every field is at least that; and nothing scrolls the page sideways. | |
| 197 | + | */ | |
| 180 | 198 | @media (max-width: 767px) { | |
| 181 | 199 | input:not([type="checkbox"]):not([type="radio"]):not([type="range"]), | |
| 182 | 200 | textarea, | |
| ⋯ | |||
| 191 | 209 | body { | |
| 192 | 210 | @apply bg-bg font-sans text-fg antialiased; | |
| 193 | 211 | } | |
| 212 | + | body[data-frame="app"] { | |
| 213 | + | @apply bg-shell; | |
| 214 | + | } | |
| 194 | 215 | ||
| 195 | 216 | ::selection { | |
| 196 | 217 | background: color-mix(in srgb, var(--color-accent) 35%, transparent); | |
| 98 | 98 | <span className="relative shrink-0"> | |
| 99 | 99 | <AgentFace agent={{ ...agent, builtin: isOrchestrator(agent) }} size={26} /> | |
| 100 | 100 | {!isOrchestrator(agent) && ( | |
| 101 | − | <StatusDot status={agent.status} className="absolute -right-0.5 -bottom-0.5 ring-2 ring-[color-mix(in_srgb,var(--color-surface)_70%,var(--color-bg))]" /> | |
| 101 | + | <StatusDot status={agent.status} className="absolute -right-0.5 -bottom-0.5 ring-2 ring-shell" /> | |
| 102 | 102 | )} | |
| 103 | 103 | </span> | |
| 104 | 104 | <span className="min-w-0 grow leading-tight"> | |
| ⋯ | |||
| 121 | 121 | ); | |
| 122 | 122 | return ( | |
| 123 | 123 | <div className="flex h-full flex-col"> | |
| 124 | − | <div className={`flex h-14 shrink-0 items-center justify-between border-b border-line pr-2.5 pl-4 ${phone ? "hidden" : ""}`}> | |
| 125 | − | <h2 className="text-[0.9375rem] font-semibold">Agents</h2> | |
| 124 | + | <div className={`flex h-9 shrink-0 items-center justify-between pr-1 pl-3 ${phone ? "hidden" : ""}`}> | |
| 125 | + | <h2 className="text-xs font-medium text-faint">Agents</h2> | |
| 126 | 126 | <NavLink | |
| 127 | 127 | to={`/${slug}/-/agents/new`} | |
| 128 | 128 | aria-label="New agent" | |
| 1 | + | import { | |
| 2 | + | BarChart3, | |
| 3 | + | Bell, | |
| 4 | + | Boxes, | |
| 5 | + | Brain, | |
| 6 | + | Building2, | |
| 7 | + | Code2, | |
| 8 | + | History, | |
| 9 | + | LayoutGrid, | |
| 10 | + | MessagesSquare, | |
| 11 | + | Network, | |
| 12 | + | Package, | |
| 13 | + | Pin, | |
| 14 | + | Plug, | |
| 15 | + | Search, | |
| 16 | + | Shapes, | |
| 17 | + | ShieldCheck, | |
| 18 | + | Sparkles, | |
| 19 | + | Store, | |
| 20 | + | Sun, | |
| 21 | + | Users, | |
| 22 | + | UsersRound, | |
| 23 | + | Waypoints, | |
| 24 | + | } from "lucide-react"; | |
| 25 | + | import { type ReactNode, useMemo, useState } from "react"; | |
| 26 | + | import { Link, useFetcher, useFetchers } from "react-router"; | |
| 27 | + | ||
| 28 | + | import { type AppInfo, type AppKey, type PinnableApp, appPinFromForm, appsFor, withPin } from "../lib/apps"; | |
| 29 | + | import { cn } from "../lib/cn"; | |
| 30 | + | import { Hint } from "./ui/hint"; | |
| 31 | + | ||
| 32 | + | /** Each app's icon, at the size asked for. */ | |
| 33 | + | export function appIcon(key: AppKey, size = 18): ReactNode { | |
| 34 | + | const icons: Record<AppKey, ReactNode> = { | |
| 35 | + | today: <Sun size={size} />, | |
| 36 | + | chat: <MessagesSquare size={size} />, | |
| 37 | + | notifications: <Bell size={size} />, | |
| 38 | + | agents: <Sparkles size={size} />, | |
| 39 | + | code: <Code2 size={size} />, | |
| 40 | + | artifacts: <Shapes size={size} />, | |
| 41 | + | people: <Users size={size} />, | |
| 42 | + | workspace: <Building2 size={size} />, | |
| 43 | + | projects: <Boxes size={size} />, | |
| 44 | + | packages: <Package size={size} />, | |
| 45 | + | security: <ShieldCheck size={size} />, | |
| 46 | + | context: <Network size={size} />, | |
| 47 | + | memory: <Brain size={size} />, | |
| 48 | + | teams: <UsersRound size={size} />, | |
| 49 | + | usage: <BarChart3 size={size} />, | |
| 50 | + | gateway: <Waypoints size={size} />, | |
| 51 | + | integrations: <Plug size={size} />, | |
| 52 | + | audit: <History size={size} />, | |
| 53 | + | }; | |
| 54 | + | return icons[key]; | |
| 55 | + | } | |
| 56 | + | ||
| 57 | + | /** Every pin form shares this key, so the dock, the launcher and the Apps page see one change. */ | |
| 58 | + | const PIN_FETCHER = "app-pin"; | |
| 59 | + | ||
| 60 | + | /** | |
| 61 | + | * Someone's pins in the workspace `slug`, as saved (`saved`, from the | |
| 62 | + | * root's data), with any pin or unpin on its way already applied, so the | |
| 63 | + | * dock changes the moment a pin is pressed. `toggle` pins or unpins. | |
| 64 | + | */ | |
| 65 | + | export function useAppPins(slug: string, saved: PinnableApp[]) { | |
| 66 | + | const fetcher = useFetcher({ key: PIN_FETCHER }); | |
| 67 | + | const fetchers = useFetchers(); | |
| 68 | + | const pins = useMemo(() => { | |
| 69 | + | let pins = saved; | |
| 70 | + | for (const pending of fetchers) { | |
| 71 | + | if (pending.key !== PIN_FETCHER || !pending.formData) continue; | |
| 72 | + | const change = appPinFromForm(pending.formData); | |
| 73 | + | if (change) pins = withPin(pins, change.app, change.pinned); | |
| 74 | + | } | |
| 75 | + | return pins; | |
| 76 | + | }, [saved, fetchers]); | |
| 77 | + | const toggle = (app: PinnableApp) => { | |
| 78 | + | const form = new FormData(); | |
| 79 | + | form.set("intent", pins.includes(app) ? "unpin" : "pin"); | |
| 80 | + | form.set("app", app); | |
| 81 | + | fetcher.submit(form, { method: "post", action: `/${slug}/-/apps` }); | |
| 82 | + | }; | |
| 83 | + | return { pins, toggle }; | |
| 84 | + | } | |
| 85 | + | ||
| 86 | + | /** | |
| 87 | + | * One app: its icon and name, opening it; and, for an app people pin, a | |
| 88 | + | * pin that shows on hover or focus and stays lit while it is pinned. | |
| 89 | + | */ | |
| 90 | + | export function AppTile({ | |
| 91 | + | app, | |
| 92 | + | slug, | |
| 93 | + | pinned, | |
| 94 | + | onToggle, | |
| 95 | + | onOpen, | |
| 96 | + | card = false, | |
| 97 | + | }: { | |
| 98 | + | app: AppInfo; | |
| 99 | + | slug: string; | |
| 100 | + | pinned: boolean; | |
| 101 | + | onToggle: (app: PinnableApp) => void; | |
| 102 | + | onOpen?: () => void; | |
| 103 | + | /** On the Apps page: a card with what the app is. */ | |
| 104 | + | card?: boolean; | |
| 105 | + | }) { | |
| 106 | + | return ( | |
| 107 | + | <div | |
| 108 | + | className={cn( | |
| 109 | + | "group/tile relative rounded-xl transition-colors", | |
| 110 | + | card ? "border border-line bg-surface hover:border-line-strong" : "hover:bg-raised focus-within:bg-raised", | |
| 111 | + | )} | |
| 112 | + | > | |
| 113 | + | <Link | |
| 114 | + | to={app.path(slug)} | |
| 115 | + | prefetch="intent" | |
| 116 | + | onClick={onOpen} | |
| 117 | + | className={cn( | |
| 118 | + | "flex w-full rounded-xl text-center outline-none focus-visible:ring-2 focus-visible:ring-accent", | |
| 119 | + | card ? "items-center gap-3 p-3.5 pr-11 text-left" : "flex-col items-center gap-1.5 px-1 pt-3 pb-2.5", | |
| 120 | + | )} | |
| 121 | + | > | |
| 122 | + | <span className="flex size-10 shrink-0 items-center justify-center rounded-xl bg-raised text-fg ring-1 ring-line group-hover/tile:ring-line-strong"> | |
| 123 | + | {appIcon(app.key, 19)} | |
| 124 | + | </span> | |
| 125 | + | {card ? ( | |
| 126 | + | <span className="min-w-0"> | |
| 127 | + | <span className="block truncate text-sm font-medium text-fg">{app.name}</span> | |
| 128 | + | <span className="block truncate text-xs text-muted">{app.about}</span> | |
| 129 | + | </span> | |
| 130 | + | ) : ( | |
| 131 | + | <span className="w-full truncate text-xs font-medium text-fg-soft">{app.name}</span> | |
| 132 | + | )} | |
| 133 | + | </Link> | |
| 134 | + | {!app.builtin && ( | |
| 135 | + | <Hint label={pinned ? "Unpin from your dock" : "Pin to your dock"}> | |
| 136 | + | <button | |
| 137 | + | type="button" | |
| 138 | + | aria-label={pinned ? `Unpin ${app.name} from your dock` : `Pin ${app.name} to your dock`} | |
| 139 | + | aria-pressed={pinned} | |
| 140 | + | onClick={() => onToggle(app.key as PinnableApp)} | |
| 141 | + | className={cn( | |
| 142 | + | "absolute flex size-6 items-center justify-center rounded-md outline-none transition-opacity hover:bg-line focus-visible:opacity-100 focus-visible:ring-2 focus-visible:ring-accent [@media(hover:none)]:opacity-100", | |
| 143 | + | card ? "top-1/2 right-2.5 -translate-y-1/2" : "top-1 right-1", | |
| 144 | + | pinned ? "text-accent opacity-100" : "text-faint opacity-0 group-hover/tile:opacity-100 hover:text-fg", | |
| 145 | + | )} | |
| 146 | + | > | |
| 147 | + | <Pin size={13} className={pinned ? "fill-current" : undefined} /> | |
| 148 | + | </button> | |
| 149 | + | </Hint> | |
| 150 | + | )} | |
| 151 | + | </div> | |
| 152 | + | ); | |
| 153 | + | } | |
| 154 | + | ||
| 155 | + | /** | |
| 156 | + | * The Apps launcher's body: a search, every app in the workspace the | |
| 157 | + | * person can use, and the way to all of them and to the Marketplace. | |
| 158 | + | */ | |
| 159 | + | export function AppsLauncher({ | |
| 160 | + | slug, | |
| 161 | + | code, | |
| 162 | + | pins, | |
| 163 | + | onToggle, | |
| 164 | + | onClose, | |
| 165 | + | }: { | |
| 166 | + | slug: string; | |
| 167 | + | code: boolean; | |
| 168 | + | pins: PinnableApp[]; | |
| 169 | + | onToggle: (app: PinnableApp) => void; | |
| 170 | + | onClose: () => void; | |
| 171 | + | }) { | |
| 172 | + | const [query, setQuery] = useState(""); | |
| 173 | + | const wanted = query.trim().toLowerCase(); | |
| 174 | + | const apps = appsFor(code).filter((app) => !wanted || app.name.toLowerCase().includes(wanted) || app.about.toLowerCase().includes(wanted)); | |
| 175 | + | return ( | |
| 176 | + | <div className="flex flex-col gap-2"> | |
| 177 | + | <label className="flex h-9 items-center gap-2 rounded-lg border border-line bg-bg px-2.5 text-sm focus-within:border-line-strong"> | |
| 178 | + | <Search size={15} className="shrink-0 text-faint" /> | |
| 179 | + | <input | |
| 180 | + | value={query} | |
| 181 | + | onChange={(event) => setQuery(event.target.value)} | |
| 182 | + | placeholder="Find an app" | |
| 183 | + | aria-label="Find an app" | |
| 184 | + | autoComplete="off" | |
| 185 | + | className="min-w-0 grow bg-transparent outline-none placeholder:text-faint" | |
| 186 | + | /> | |
| 187 | + | </label> | |
| 188 | + | {apps.length === 0 ? ( | |
| 189 | + | <p className="px-2 py-6 text-center text-sm text-muted">No app matches.</p> | |
| 190 | + | ) : ( | |
| 191 | + | <div className="grid max-h-[min(23.5rem,60dvh)] grid-cols-4 gap-1 overflow-y-auto max-sm:grid-cols-3"> | |
| 192 | + | {apps.map((app) => ( | |
| 193 | + | <AppTile key={app.key} app={app} slug={slug} pinned={pins.includes(app.key as PinnableApp)} onToggle={onToggle} onOpen={onClose} /> | |
| 194 | + | ))} | |
| 195 | + | </div> | |
| 196 | + | )} | |
| 197 | + | <div className="flex items-center justify-between border-t border-line pt-2"> | |
| 198 | + | <Link | |
| 199 | + | to={`/${slug}/-/apps`} | |
| 200 | + | onClick={onClose} | |
| 201 | + | className="flex h-8 items-center gap-1.5 rounded-md px-2 text-[0.8125rem] text-muted transition-colors hover:bg-line hover:text-fg" | |
| 202 | + | > | |
| 203 | + | <LayoutGrid size={14} /> | |
| 204 | + | All apps | |
| 205 | + | </Link> | |
| 206 | + | <Hint label="There is no marketplace yet." disabled> | |
| 207 | + | <button type="button" disabled className="flex h-8 items-center gap-1.5 rounded-md px-2 text-[0.8125rem] text-faint"> | |
| 208 | + | <Store size={14} /> | |
| 209 | + | Marketplace | |
| 210 | + | <span className="rounded-full bg-line px-1.5 text-[0.625rem] font-medium text-muted">Coming</span> | |
| 211 | + | </button> | |
| 212 | + | </Hint> | |
| 213 | + | </div> | |
| 214 | + | </div> | |
| 215 | + | ); | |
| 216 | + | } |
| 1 | 1 | import type { ReactNode } from "react"; | |
| 2 | 2 | ||
| 3 | − | import { Mark } from "./logo"; | |
| 4 | − | ||
| 5 | − | /** The centred panel shared by the sign-in and sign-up pages. */ | |
| 3 | + | /** | |
| 4 | + | * The column shared by the sign-in and sign-up pages, under the logo the | |
| 5 | + | * standalone frame draws (components/standalone.tsx): a short heading, a | |
| 6 | + | * line under it, the form, and what else there is to do. | |
| 7 | + | */ | |
| 6 | 8 | export function AuthCard({ | |
| 7 | 9 | title, | |
| 8 | 10 | subtitle, | |
| ⋯ | |||
| 15 | 17 | children: ReactNode; | |
| 16 | 18 | }) { | |
| 17 | 19 | return ( | |
| 18 | − | <main className="mx-auto flex max-w-sm flex-col px-4 pt-20 pb-10"> | |
| 19 | − | <Mark className="size-9" /> | |
| 20 | − | <h1 className="mt-6 text-2xl font-semibold tracking-tight">{title}</h1> | |
| 21 | − | <p className="text-2xl tracking-tight text-faint">{subtitle}</p> | |
| 22 | − | <div className="mt-8">{children}</div> | |
| 20 | + | <main className="mx-auto flex w-full max-w-85 flex-col pt-8"> | |
| 21 | + | <h1 className="text-center text-xl font-semibold tracking-tight">{title}</h1> | |
| 22 | + | <p className="mt-1 text-center text-sm text-muted">{subtitle}</p> | |
| 23 | + | <div className="mt-7">{children}</div> | |
| 23 | 24 | <p className="mt-6 text-center text-sm text-muted">{footer}</p> | |
| 24 | 25 | </main> | |
| 25 | 26 | ); | |
| 13 | 13 | GitMerge, | |
| 14 | 14 | GitPullRequest, | |
| 15 | 15 | Hash, | |
| 16 | − | Home, | |
| 17 | − | Inbox, | |
| 16 | + | Sun, | |
| 17 | + | Bell, | |
| 18 | 18 | MessagesSquare, | |
| 19 | 19 | Paperclip, | |
| 20 | 20 | SendHorizontal, | |
| ⋯ | |||
| 114 | 114 | } | |
| 115 | 115 | ||
| 116 | 116 | const RAIL: { icon: ReactNode; label: string; on?: boolean }[] = [ | |
| 117 | − | { icon: <Home size={16} />, label: "Home" }, | |
| 117 | + | { icon: <Sun size={16} />, label: "Today" }, | |
| 118 | 118 | { icon: <Code2 size={16} />, label: "Code" }, | |
| 119 | 119 | { icon: <MessagesSquare size={16} />, label: "Chat", on: true }, | |
| 120 | 120 | { icon: <BookOpen size={16} />, label: "Docs" }, | |
| 121 | 121 | { icon: <Bot size={16} />, label: "Agents" }, | |
| 122 | − | { icon: <Inbox size={16} />, label: "Inbox" }, | |
| 122 | + | { icon: <Bell size={16} />, label: "Notifications" }, | |
| 123 | 123 | ]; | |
| 124 | 124 | ||
| 125 | 125 | const CHANNELS: { name: string; on?: boolean; unread?: number }[] = [ | |
| 606 | 606 | <CardContext.Provider value={cardContext}> | |
| 607 | 607 | <div | |
| 608 | 608 | {...swipe} | |
| 609 | − | className="flex h-[calc(100dvh-3.5rem)] min-h-0 lg:h-dvh max-md:fixed max-md:inset-x-0 max-md:top-(--vv-top,0px) max-md:z-30 max-md:h-(--vv-height,100dvh) max-md:bg-bg" | |
| 609 | + | className="flex h-(--page-h) min-h-0 max-md:fixed max-md:inset-x-0 max-md:top-(--vv-top,0px) max-md:z-30 max-md:h-(--vv-height,100dvh) max-md:bg-bg" | |
| 610 | 610 | > | |
| 611 | 611 | {/* What pressing a card's action did (components/chat/card.tsx). */} | |
| 612 | 612 | <CardToasts /> |
| 7 | 7 | /** A calm full-height panel for a conversation that cannot be shown. */ | |
| 8 | 8 | function Panel({ icon, title, children }: { icon: ReactNode; title: string; children: ReactNode }) { | |
| 9 | 9 | return ( | |
| 10 | − | <div className="flex h-[calc(100dvh-3.5rem)] items-center justify-center px-6 lg:h-dvh"> | |
| 10 | + | <div className="flex h-(--page-h) items-center justify-center px-6"> | |
| 11 | 11 | <div className="max-w-sm text-center"> | |
| 12 | 12 | <div className="mx-auto flex size-12 items-center justify-center rounded-xl border border-line bg-surface text-muted">{icon}</div> | |
| 13 | 13 | <h1 className="mt-4 text-base font-semibold">{title}</h1> |
| 139 | 139 | ||
| 140 | 140 | return ( | |
| 141 | 141 | // The colour behind it, for the dots cut out of people's avatars. | |
| 142 | − | <div className="flex h-full flex-col [--chat-sidebar-bg:color-mix(in_srgb,var(--color-surface)_70%,var(--color-bg))]"> | |
| 142 | + | <div className="flex h-full flex-col [--chat-sidebar-bg:var(--color-shell)]"> | |
| 143 | 143 | {heading && ( | |
| 144 | − | <div className="flex h-14 shrink-0 items-center justify-between border-b border-line pr-2.5 pl-4"> | |
| 145 | − | <h2 className="text-[0.9375rem] font-semibold">Chat</h2> | |
| 144 | + | <div className="flex h-9 shrink-0 items-center justify-between pr-1 pl-3"> | |
| 145 | + | <h2 className="text-xs font-medium text-faint">Chat</h2> | |
| 146 | 146 | <NewMessageButton slug={slug} /> | |
| 147 | 147 | </div> | |
| 148 | 148 | )} |
| 433 | 433 | > | |
| 434 | 434 | <header | |
| 435 | 435 | // Sticks under the change's toolbar (its height is --diff-toolbar), not behind it. | |
| 436 | − | className={`sticky top-[calc(3.5rem+var(--diff-toolbar,0px))] z-20 flex items-center gap-2 border-line bg-surface/95 px-2 py-2 backdrop-blur sm:gap-2.5 sm:px-3 ${ | |
| 436 | + | className={`sticky top-[calc(var(--topbar-h)+var(--diff-toolbar,0px))] z-20 flex items-center gap-2 border-line bg-surface/95 px-2 py-2 backdrop-blur sm:gap-2.5 sm:px-3 ${ | |
| 437 | 437 | collapsed ? "rounded-xl" : "rounded-t-xl border-b" | |
| 438 | 438 | }`} | |
| 439 | 439 | > | |
| ⋯ | |||
| 869 | 869 | )} | |
| 870 | 870 | <div className="flex gap-4"> | |
| 871 | 871 | {files.length > 1 && ( | |
| 872 | − | <aside className="sticky top-28 hidden max-h-[calc(100vh-8rem)] w-60 shrink-0 flex-col self-start xl:flex"> | |
| 872 | + | <aside className="sticky top-[calc(var(--topbar-h)+3.5rem)] hidden max-h-[calc(100vh-var(--topbar-h)-4.5rem)] w-60 shrink-0 flex-col self-start xl:flex"> | |
| 873 | 873 | <div className="relative"> | |
| 874 | 874 | <Search size={13} className="pointer-events-none absolute top-1/2 left-2.5 -translate-y-1/2 text-faint" /> | |
| 875 | 875 | <input | |
| 1 | + | import { Check, ChevronDown, CircleUserRound, LayoutGrid, Plus, Settings } from "lucide-react"; | |
| 2 | + | import { type ReactNode, useState } from "react"; | |
| 3 | + | import { Link, useLocation, useNavigation } from "react-router"; | |
| 4 | + | ||
| 5 | + | import { type Membership, type User, hasCodeAccess, shownUsername } from "@g1t/contracts"; | |
| 6 | + | ||
| 7 | + | import { AppsLauncher, appIcon, useAppPins } from "./apps"; | |
| 8 | + | import { Mark } from "./logo"; | |
| 9 | + | import { Avatar } from "./ui"; | |
| 10 | + | import { Hint } from "./ui/hint"; | |
| 11 | + | import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from "./ui/dialog"; | |
| 12 | + | import { | |
| 13 | + | DropdownMenu, | |
| 14 | + | DropdownMenuContent, | |
| 15 | + | DropdownMenuItem, | |
| 16 | + | DropdownMenuLabel, | |
| 17 | + | DropdownMenuSeparator, | |
| 18 | + | DropdownMenuTrigger, | |
| 19 | + | } from "./ui/dropdown-menu"; | |
| 20 | + | import { Popover, PopoverContent, PopoverTrigger } from "./ui/popover"; | |
| 21 | + | import { type AppKey, type PinnableApp, appOf } from "../lib/apps"; | |
| 22 | + | import { cn } from "../lib/cn"; | |
| 23 | + | import { paletteKeyLabel } from "../lib/shortcut"; | |
| 24 | + | import { type ModeKey, modeHome, modeOf, todayPath } from "../lib/workspace-nav"; | |
| 25 | + | ||
| 26 | + | /** The dock's column: 8px from the window's edge, 80px wide. */ | |
| 27 | + | export const DOCK_COLUMN = "5.5rem"; | |
| 28 | + | ||
| 29 | + | type Item = { key: ModeKey; label: string; badge?: { count: number; loud: boolean } | null }; | |
| 30 | + | ||
| 31 | + | /** A count on an icon: loud (the accent) for what waits on you, quiet for the rest. */ | |
| 32 | + | export function CountBadge({ count, loud, className }: { count: number; loud: boolean; className?: string }) { | |
| 33 | + | if (count <= 0) return null; | |
| 34 | + | return ( | |
| 35 | + | <span | |
| 36 | + | className={cn( | |
| 37 | + | "absolute -top-1 -right-1.5 min-w-[1.125rem] rounded-full px-1 text-center text-[0.625rem] leading-[1.125rem] font-bold tabular-nums ring-2 ring-dock", | |
| 38 | + | loud ? "bg-accent text-bg" : "bg-line-strong text-fg", | |
| 39 | + | className, | |
| 40 | + | )} | |
| 41 | + | > | |
| 42 | + | {count > 99 ? "99+" : count} | |
| 43 | + | </span> | |
| 44 | + | ); | |
| 45 | + | } | |
| 46 | + | ||
| 47 | + | /** The square behind an icon in the dock: raised for the current one. */ | |
| 48 | + | function IconSquare({ current, children }: { current: boolean; children: ReactNode }) { | |
| 49 | + | return ( | |
| 50 | + | <span | |
| 51 | + | className={cn( | |
| 52 | + | "relative flex size-9 items-center justify-center rounded-[11px] transition-colors", | |
| 53 | + | current | |
| 54 | + | ? "bg-raised text-fg shadow-[inset_0_0_0_1px_var(--color-line-strong)]" | |
| 55 | + | : "text-muted group-hover:bg-raised group-hover:text-fg group-data-[state=open]:bg-raised group-data-[state=open]:text-fg", | |
| 56 | + | )} | |
| 57 | + | > | |
| 58 | + | {children} | |
| 59 | + | </span> | |
| 60 | + | ); | |
| 61 | + | } | |
| 62 | + | ||
| 63 | + | /** The bar on the dock's left edge beside the current item. */ | |
| 64 | + | function CurrentBar() { | |
| 65 | + | return <span aria-hidden="true" className="absolute top-[9px] -left-1.5 h-5 w-[3px] rounded-r-[3px] bg-fg" />; | |
| 66 | + | } | |
| 67 | + | ||
| 68 | + | // Each item fills the dock's width and centres its icon and name on it. | |
| 69 | + | const ITEM = "group relative flex w-full flex-col items-center justify-center gap-0.5 rounded-lg py-0.5 text-center outline-none focus-visible:ring-2 focus-visible:ring-accent"; | |
| 70 | + | ||
| 71 | + | function DockLink({ to, label, icon, current, badge, compact = false }: { to: string; label: string; icon: ReactNode; current: boolean; badge?: Item["badge"]; compact?: boolean }) { | |
| 72 | + | const link = ( | |
| 73 | + | <Link | |
| 74 | + | to={to} | |
| 75 | + | prefetch="intent" | |
| 76 | + | aria-current={current ? "page" : undefined} | |
| 77 | + | aria-label={badge && badge.count > 0 ? `${label}, ${badge.count} unread` : label} | |
| 78 | + | className={ITEM} | |
| 79 | + | > | |
| 80 | + | {current && <CurrentBar />} | |
| 81 | + | <IconSquare current={current}> | |
| 82 | + | {icon} | |
| 83 | + | {badge && <CountBadge count={badge.count} loud={badge.loud} />} | |
| 84 | + | </IconSquare> | |
| 85 | + | {!compact && <DockLabel current={current}>{label}</DockLabel>} | |
| 86 | + | </Link> | |
| 87 | + | ); | |
| 88 | + | // An icon alone says its name on hover. | |
| 89 | + | return compact ? ( | |
| 90 | + | <Hint label={label} side="right"> | |
| 91 | + | {link} | |
| 92 | + | </Hint> | |
| 93 | + | ) : ( | |
| 94 | + | link | |
| 95 | + | ); | |
| 96 | + | } | |
| 97 | + | ||
| 98 | + | /** A name under an icon: centred on the dock, whatever its length. */ | |
| 99 | + | function DockLabel({ current, children }: { current: boolean; children: ReactNode }) { | |
| 100 | + | return ( | |
| 101 | + | <span className={cn("block w-full truncate text-[0.625rem] leading-tight font-medium transition-colors", current ? "text-fg" : "text-faint group-hover:text-muted")}> | |
| 102 | + | {children} | |
| 103 | + | </span> | |
| 104 | + | ); | |
| 105 | + | } | |
| 106 | + | ||
| 107 | + | function displayName(membership: Membership): string { | |
| 108 | + | return membership.name?.trim() || membership.slug; | |
| 109 | + | } | |
| 110 | + | ||
| 111 | + | /** | |
| 112 | + | * The workspace you are in and the way to another: its avatar, its name | |
| 113 | + | * and a chevron, opening your workspaces, a new one, and your own account. | |
| 114 | + | * `row` heads the sidebar; `compact` starts the page header's trail when | |
| 115 | + | * no sidebar is open. | |
| 116 | + | */ | |
| 117 | + | export function WorkspaceSwitcher({ user, workspace, compact = false }: { user: User; workspace: Membership; compact?: boolean }) { | |
| 118 | + | return ( | |
| 119 | + | <DropdownMenu> | |
| 120 | + | <DropdownMenuTrigger | |
| 121 | + | aria-label={`${displayName(workspace)}: switch workspace`} | |
| 122 | + | className={cn( | |
| 123 | + | "flex min-w-0 items-center rounded-lg outline-none transition-colors hover:bg-raised focus-visible:ring-2 focus-visible:ring-accent data-[state=open]:bg-raised", | |
| 124 | + | compact ? "h-8 shrink-0 gap-1.5 px-1 max-sm:max-w-40" : "h-9 grow gap-2 px-1.5", | |
| 125 | + | )} | |
| 126 | + | > | |
| 127 | + | <Avatar name={workspace.slug} image={workspace.avatar} size={compact ? 22 : 26} square /> | |
| 128 | + | <span className={cn("min-w-0 truncate font-semibold", compact ? "text-sm" : "text-[0.875rem]")}>{displayName(workspace)}</span> | |
| 129 | + | <ChevronDown size={14} className="shrink-0 text-faint" /> | |
| 130 | + | </DropdownMenuTrigger> | |
| 131 | + | <DropdownMenuContent align="start" className="w-72 p-1.5"> | |
| 132 | + | <DropdownMenuItem asChild className="gap-3 px-2 py-2"> | |
| 133 | + | <Link to={`/${workspace.slug}/-/workspace`}> | |
| 134 | + | <Avatar name={workspace.slug} image={workspace.avatar} size={36} square /> | |
| 135 | + | <span className="flex min-w-0 grow flex-col leading-tight"> | |
| 136 | + | <span className="truncate text-sm font-medium text-fg">{displayName(workspace)}</span> | |
| 137 | + | <span className="truncate font-mono text-xs text-muted">g1t.sh/{workspace.slug}</span> | |
| 138 | + | <span className="mt-0.5 text-xs text-faint capitalize">{workspace.role}</span> | |
| 139 | + | </span> | |
| 140 | + | </Link> | |
| 141 | + | </DropdownMenuItem> | |
| 142 | + | <DropdownMenuSeparator className="my-1.5" /> | |
| 143 | + | <DropdownMenuLabel>Switch workspace</DropdownMenuLabel> | |
| 144 | + | {(user.workspaces ?? []).map((membership) => ( | |
| 145 | + | <DropdownMenuItem key={membership.slug} asChild> | |
| 146 | + | <Link to={todayPath(membership.slug)}> | |
| 147 | + | <Avatar name={membership.slug} image={membership.avatar} size={24} square /> | |
| 148 | + | <span className="flex min-w-0 grow flex-col leading-tight"> | |
| 149 | + | <span className="truncate">{displayName(membership)}</span> | |
| 150 | + | <span className="truncate font-mono text-[0.6875rem] text-faint">{membership.slug}</span> | |
| 151 | + | </span> | |
| 152 | + | {membership.slug === workspace.slug ? <Check className="shrink-0 text-accent" /> : <span className="size-4 shrink-0" aria-hidden="true" />} | |
| 153 | + | </Link> | |
| 154 | + | </DropdownMenuItem> | |
| 155 | + | ))} | |
| 156 | + | <DropdownMenuItem asChild> | |
| 157 | + | <Link to="/workspaces/new"> | |
| 158 | + | <Plus /> | |
| 159 | + | New workspace | |
| 160 | + | </Link> | |
| 161 | + | </DropdownMenuItem> | |
| 162 | + | <DropdownMenuSeparator className="my-1.5" /> | |
| 163 | + | <DropdownMenuLabel>Personal account</DropdownMenuLabel> | |
| 164 | + | <DropdownMenuItem asChild> | |
| 165 | + | <Link to={`/u/${user.username}`}> | |
| 166 | + | <CircleUserRound /> | |
| 167 | + | <span className="grow">Your profile</span> | |
| 168 | + | <span className="font-mono text-xs text-faint">@{shownUsername(user)}</span> | |
| 169 | + | </Link> | |
| 170 | + | </DropdownMenuItem> | |
| 171 | + | <DropdownMenuItem asChild> | |
| 172 | + | <Link to="/settings"> | |
| 173 | + | <Settings /> | |
| 174 | + | Your settings | |
| 175 | + | </Link> | |
| 176 | + | </DropdownMenuItem> | |
| 177 | + | </DropdownMenuContent> | |
| 178 | + | </DropdownMenu> | |
| 179 | + | ); | |
| 180 | + | } | |
| 181 | + | ||
| 182 | + | /** Keyboard shortcuts, as the account menu lists them. */ | |
| 183 | + | function shortcuts(palette: string, sidebar: string): [string, string][] { | |
| 184 | + | return [ | |
| 185 | + | [palette, "Search, jump anywhere, or run a command"], | |
| 186 | + | [sidebar, "Show or hide the sidebar"], | |
| 187 | + | ["Esc", "Close a menu, dialog or panel"], | |
| 188 | + | ["Enter", "Send a message in Chat"], | |
| 189 | + | ["Shift Enter", "A new line in a message"], | |
| 190 | + | ["@", "Mention a person or an agent"], | |
| 191 | + | ["↑ ↓ then Tab", "Choose a suggestion"], | |
| 192 | + | ["Alt ↑ ↓", "Move a pinned project in Code's sidebar"], | |
| 193 | + | ]; | |
| 194 | + | } | |
| 195 | + | ||
| 196 | + | /** The sidebar's shortcut, as this computer writes it. */ | |
| 197 | + | export function sidebarKeyLabel(platform: string | null): string { | |
| 198 | + | return paletteKeyLabel(platform).replace(/K$/, "B"); | |
| 199 | + | } | |
| 200 | + | ||
| 201 | + | /** The keyboard's shortcuts, opened from the account menu. */ | |
| 202 | + | export function ShortcutsDialog({ open, onOpenChange }: { open: boolean; onOpenChange: (open: boolean) => void }) { | |
| 203 | + | const platform = typeof navigator === "undefined" ? null : navigator.platform; | |
| 204 | + | return ( | |
| 205 | + | <Dialog open={open} onOpenChange={onOpenChange}> | |
| 206 | + | <DialogContent className="max-w-md"> | |
| 207 | + | <DialogHeader> | |
| 208 | + | <DialogTitle>Keyboard shortcuts</DialogTitle> | |
| 209 | + | <DialogDescription>Everywhere in g1t, and in Chat.</DialogDescription> | |
| 210 | + | </DialogHeader> | |
| 211 | + | <dl className="divide-y divide-line rounded-lg border border-line"> | |
| 212 | + | {shortcuts(paletteKeyLabel(platform), sidebarKeyLabel(platform)).map(([key, what]) => ( | |
| 213 | + | <div key={key} className="flex items-center justify-between gap-4 px-3.5 py-2.5 text-sm"> | |
| 214 | + | <dt className="text-fg-soft">{what}</dt> | |
| 215 | + | <dd> | |
| 216 | + | <kbd className="rounded-md border border-line-strong bg-raised px-1.5 py-0.5 font-sans text-xs whitespace-nowrap text-fg">{key}</kbd> | |
| 217 | + | </dd> | |
| 218 | + | </div> | |
| 219 | + | ))} | |
| 220 | + | </dl> | |
| 221 | + | </DialogContent> | |
| 222 | + | </Dialog> | |
| 223 | + | ); | |
| 224 | + | } | |
| 225 | + | ||
| 226 | + | /** The Apps button at the foot of the dock's apps, and the launcher it opens. */ | |
| 227 | + | function AppsButton({ slug, code, pins, onToggle, current }: { slug: string; code: boolean; pins: PinnableApp[]; onToggle: (app: PinnableApp) => void; current: boolean }) { | |
| 228 | + | const [open, setOpen] = useState(false); | |
| 229 | + | return ( | |
| 230 | + | <Popover open={open} onOpenChange={setOpen}> | |
| 231 | + | <PopoverTrigger aria-label="Apps" className={ITEM}> | |
| 232 | + | {current && <CurrentBar />} | |
| 233 | + | <IconSquare current={current || open}> | |
| 234 | + | <LayoutGrid size={19} /> | |
| 235 | + | </IconSquare> | |
| 236 | + | <DockLabel current={current || open}>Apps</DockLabel> | |
| 237 | + | </PopoverTrigger> | |
| 238 | + | <PopoverContent side="right" align="end" sideOffset={14} aria-label="Apps" className="w-[23rem] bg-dock p-2.5"> | |
| 239 | + | <AppsLauncher slug={slug} code={code} pins={pins} onToggle={onToggle} onClose={() => setOpen(false)} /> | |
| 240 | + | </PopoverContent> | |
| 241 | + | </Popover> | |
| 242 | + | ); | |
| 243 | + | } | |
| 244 | + | ||
| 245 | + | /** | |
| 246 | + | * The dock down the left (docs/WORKSPACE.md, "Shell"): g1t's mark, which | |
| 247 | + | * leads to Today; the built-in apps (Today, Chat, Notifications, Agents, | |
| 248 | + | * Code and Artifacts) with their names; the apps you pinned, as icons; | |
| 249 | + | * the Apps launcher; and at its foot People, Workspace and your account. | |
| 250 | + | * What is lit follows the address. A member without Code access has no | |
| 251 | + | * Code. From 768px; a phone has the bottom bar (components/mobile.tsx). | |
| 252 | + | */ | |
| 253 | + | export function Dock({ | |
| 254 | + | workspace, | |
| 255 | + | pins: saved, | |
| 256 | + | unread, | |
| 257 | + | account, | |
| 258 | + | }: { | |
| 259 | + | workspace: Membership; | |
| 260 | + | /** The apps pinned to this person's dock in this workspace, as saved. */ | |
| 261 | + | pins: PinnableApp[]; | |
| 262 | + | unread: { notifications: number; chat: number; mentions: number }; | |
| 263 | + | account: ReactNode; | |
| 264 | + | }) { | |
| 265 | + | const { pathname } = useLocation(); | |
| 266 | + | const going = useNavigation().location?.pathname; | |
| 267 | + | const path = going ?? pathname; | |
| 268 | + | const here = modeOf(path, workspace.slug); | |
| 269 | + | const slug = workspace.slug; | |
| 270 | + | const code = hasCodeAccess(workspace); | |
| 271 | + | const { pins, toggle } = useAppPins(slug, saved); | |
| 272 | + | const builtins: Item[] = [ | |
| 273 | + | { key: "today", label: "Today" }, | |
| 274 | + | { | |
| 275 | + | key: "chat", | |
| 276 | + | label: "Chat", | |
| 277 | + | badge: { count: unread.mentions > 0 ? unread.mentions : unread.chat, loud: unread.mentions > 0 }, | |
| 278 | + | }, | |
| 279 | + | { key: "notifications", label: "Notifications", badge: { count: unread.notifications, loud: true } }, | |
| 280 | + | { key: "agents", label: "Agents" }, | |
| 281 | + | ...(code ? [{ key: "code" as const, label: "Code" }] : []), | |
| 282 | + | { key: "artifacts", label: "Artifacts" }, | |
| 283 | + | ]; | |
| 284 | + | const shown = pins.filter((key) => code || !appOf(key).code); | |
| 285 | + | // A pinned app is current on its own page and the pages under it. | |
| 286 | + | const at = (to: string) => path === to || path.startsWith(`${to}/`); | |
| 287 | + | return ( | |
| 288 | + | <nav | |
| 289 | + | aria-label="Apps" | |
| 290 | + | className="flex h-full flex-col overflow-hidden rounded-2xl border border-line bg-dock shadow-[0_1px_0_rgba(255,255,255,0.03)_inset,0_8px_24px_rgba(0,0,0,0.18)]" | |
| 291 | + | > | |
| 292 | + | <div className="flex h-14 shrink-0 items-center justify-center"> | |
| 293 | + | <Hint label="Today" side="right"> | |
| 294 | + | <Link to={todayPath(slug)} aria-label="g1t: Today" className="flex size-9 items-center justify-center rounded-[11px] transition-colors hover:bg-raised"> | |
| 295 | + | <Mark tight className="h-[1.125rem] w-auto" /> | |
| 296 | + | </Link> | |
| 297 | + | </Hint> | |
| 298 | + | </div> | |
| 299 | + | <div className="flex w-full shrink-0 flex-col items-center gap-1 px-1.5 pb-1.5"> | |
| 300 | + | {builtins.map((item) => ( | |
| 301 | + | <DockLink | |
| 302 | + | key={item.key} | |
| 303 | + | to={modeHome(item.key, slug)} | |
| 304 | + | label={item.label} | |
| 305 | + | icon={appIcon(item.key as AppKey, 19)} | |
| 306 | + | current={here === item.key && !shown.some((key) => at(appOf(key).path(slug)))} | |
| 307 | + | badge={item.badge} | |
| 308 | + | /> | |
| 309 | + | ))} | |
| 310 | + | </div> | |
| 311 | + | {/* Your pinned apps: as many as you like, scrolling under a fade at either end. */} | |
| 312 | + | <div className="flex min-h-0 w-full grow flex-col items-center gap-1 overflow-y-auto border-t border-line px-1.5 py-2 [mask-image:linear-gradient(to_bottom,transparent,#000_10px,#000_calc(100%-14px),transparent)] [scrollbar-width:none]"> | |
| 313 | + | {shown.map((key) => { | |
| 314 | + | const app = appOf(key); | |
| 315 | + | return <DockLink key={key} to={app.path(slug)} label={app.name} icon={appIcon(key, 18)} current={at(app.path(slug))} compact />; | |
| 316 | + | })} | |
| 317 | + | </div> | |
| 318 | + | <div className="flex w-full shrink-0 flex-col items-center px-1.5 pb-1.5"> | |
| 319 | + | <AppsButton slug={slug} code={code} pins={pins} onToggle={toggle} current={here === "apps"} /> | |
| 320 | + | </div> | |
| 321 | + | <div className="flex w-full shrink-0 flex-col items-center gap-1 border-t border-line px-1.5 pt-2 pb-2.5"> | |
| 322 | + | <DockLink to={modeHome("people", slug)} label="People" icon={appIcon("people", 19)} current={here === "people"} /> | |
| 323 | + | <DockLink to={modeHome("workspace", slug)} label="Workspace" icon={appIcon("workspace", 19)} current={here === "workspace"} /> | |
| 324 | + | <div className="mt-1.5">{account}</div> | |
| 325 | + | </div> | |
| 326 | + | </nav> | |
| 327 | + | ); | |
| 328 | + | } |
| 316 | 316 | </div> | |
| 317 | 317 | {showComments && ( | |
| 318 | 318 | <aside ref={comments} aria-label="Comments" className="scroll-mt-20 max-xl:border-t max-xl:border-line max-xl:pt-6"> | |
| 319 | − | <div className="xl:sticky xl:top-20 xl:max-h-[calc(100dvh-6rem)] xl:overflow-y-auto xl:[scrollbar-width:thin]"> | |
| 319 | + | <div className="xl:sticky xl:top-[calc(var(--topbar-h)+1.5rem)] xl:max-h-[calc(100dvh-var(--topbar-h)-2.5rem)] xl:overflow-y-auto xl:[scrollbar-width:thin]"> | |
| 320 | 320 | <ThreadsSidebar filter="all" sort="position" /> | |
| 321 | 321 | </div> | |
| 322 | 322 | </aside> |
| 95 | 95 | <DialogContent className="max-w-xl"> | |
| 96 | 96 | <DialogHeader> | |
| 97 | 97 | <DialogTitle className="truncate pr-6">Share “{folio.title || "Untitled"}”</DialogTitle> | |
| 98 | − | <DialogDescription>{canShare ? "People you add get an inbox item with a link." : `Only people with full access can change who can open it. Ask ${access?.owner.display_name ?? "its owner"} to share it.`}</DialogDescription> | |
| 98 | + | <DialogDescription>{canShare ? "People you add get a notification with a link." : `Only people with full access can change who can open it. Ask ${access?.owner.display_name ?? "its owner"} to share it.`}</DialogDescription> | |
| 99 | 99 | </DialogHeader> | |
| 100 | 100 | ||
| 101 | 101 | {canShare && ( |
| 363 | 363 | }; | |
| 364 | 364 | return ( | |
| 365 | 365 | <div className="flex h-full flex-col"> | |
| 366 | − | <div className="flex h-14 shrink-0 items-center gap-1 border-b border-line pr-2.5 pl-4"> | |
| 367 | − | <h2 className="min-w-0 grow truncate text-[0.9375rem] font-semibold">Artifacts</h2> | |
| 366 | + | <div className="flex h-9 shrink-0 items-center gap-1 pr-1 pl-3"> | |
| 367 | + | <h2 className="min-w-0 grow truncate text-xs font-medium text-faint">Artifacts</h2> | |
| 368 | 368 | <NewMenu onMake={make}> | |
| 369 | 369 | <button type="button" aria-label="New" className="flex h-8 items-center gap-0.5 rounded-md px-1.5 text-faint transition-colors hover:bg-raised hover:text-fg"> | |
| 370 | 370 | <Plus size={16} /> |
| 1 | − | import { Code2, MessagesSquare, Search, Sparkles } from "lucide-react"; | |
| 2 | − | import type { ReactNode } from "react"; | |
| 3 | − | import { NavLink } from "react-router"; | |
| 4 | − | ||
| 5 | − | import type { ChatSidebarEntry } from "@g1t/contracts"; | |
| 6 | − | ||
| 7 | − | import { PaletteKey } from "./command-palette"; | |
| 8 | − | import { StatusDot } from "./chat/marks"; | |
| 9 | − | import { isOrchestrator } from "./orchestrator"; | |
| 10 | − | import type { ShellAgent, ShellData } from "./shell"; | |
| 11 | − | import { channelPath } from "../lib/chat"; | |
| 12 | − | ||
| 13 | − | /** Most of each kind under Recent. */ | |
| 14 | − | const RECENT_CHATS = 5; | |
| 15 | − | const RECENT_PROJECTS = 4; | |
| 16 | − | const RECENT_AGENTS = 3; | |
| 17 | − | ||
| 18 | − | type Item = { key: string; to: string; label: string; mode: "chat" | "code" | "agents"; trailing?: ReactNode }; | |
| 19 | − | ||
| 20 | − | const MODE_ICON: Record<Item["mode"], ReactNode> = { | |
| 21 | − | chat: <MessagesSquare size={15} />, | |
| 22 | − | code: <Code2 size={15} />, | |
| 23 | − | agents: <Sparkles size={15} />, | |
| 24 | − | }; | |
| 25 | − | const MODE_WORD: Record<Item["mode"], string> = { chat: "Chat", code: "Code", agents: "Agents" }; | |
| 26 | − | ||
| 27 | − | function Row({ item }: { item: Item }) { | |
| 28 | − | return ( | |
| 29 | − | <li> | |
| 30 | − | <NavLink | |
| 31 | − | to={item.to} | |
| 32 | − | prefetch="intent" | |
| 33 | − | className={({ isActive }) => | |
| 34 | − | `group flex h-8 items-center gap-2.5 rounded-md px-2 text-[0.8125rem] transition-colors ${ | |
| 35 | − | isActive ? "bg-raised font-medium text-fg" : "text-muted hover:bg-raised/60 hover:text-fg" | |
| 36 | − | }` | |
| 37 | − | } | |
| 38 | − | > | |
| 39 | − | <span className="shrink-0 text-faint group-hover:text-muted" aria-hidden="true"> | |
| 40 | − | {MODE_ICON[item.mode]} | |
| 41 | − | </span> | |
| 42 | − | <span className="sr-only">{MODE_WORD[item.mode]}: </span> | |
| 43 | − | <span className="min-w-0 grow truncate">{item.label}</span> | |
| 44 | − | {item.trailing} | |
| 45 | − | </NavLink> | |
| 46 | − | </li> | |
| 47 | − | ); | |
| 48 | − | } | |
| 49 | − | ||
| 50 | − | function Group({ title, items, empty }: { title: string; items: Item[]; empty: string }) { | |
| 51 | − | return ( | |
| 52 | − | <section className="mt-4"> | |
| 53 | − | <h3 className="mb-1 px-2 text-xs font-medium text-faint">{title}</h3> | |
| 54 | − | {items.length === 0 ? <p className="px-2 py-1 text-xs text-faint">{empty}</p> : <ul className="space-y-px">{items.map((item) => <Row key={item.key} item={item} />)}</ul>} | |
| 55 | − | </section> | |
| 56 | − | ); | |
| 57 | − | } | |
| 58 | − | ||
| 59 | − | function chatItem(slug: string, entry: ChatSidebarEntry): Item { | |
| 60 | − | const count = entry.mentions || entry.unread; | |
| 61 | − | return { | |
| 62 | − | key: `chat:${entry.channel.id}`, | |
| 63 | − | to: channelPath(slug, entry.channel), | |
| 64 | − | label: entry.channel.kind === "channel" ? `#${entry.channel.name}` : entry.title, | |
| 65 | − | mode: "chat", | |
| 66 | − | trailing: | |
| 67 | − | count > 0 && !entry.muted ? ( | |
| 68 | − | <span | |
| 69 | − | className={`rounded-full px-1.5 text-[0.6875rem] leading-[1.125rem] font-semibold tabular-nums ${entry.mentions ? "bg-accent text-bg" : "bg-line-strong text-fg"}`} | |
| 70 | − | > | |
| 71 | − | {count} | |
| 72 | − | </span> | |
| 73 | − | ) : null, | |
| 74 | − | }; | |
| 75 | − | } | |
| 76 | − | ||
| 77 | − | function agentItem(slug: string, agent: ShellAgent): Item { | |
| 78 | − | return { | |
| 79 | − | key: `agent:${agent.id}`, | |
| 80 | − | to: `/${slug}/-/agents/${agent.handle}`, | |
| 81 | − | label: agent.display_name, | |
| 82 | − | mode: "agents", | |
| 83 | − | trailing: isOrchestrator(agent) ? null : <StatusDot status={agent.status} />, | |
| 84 | − | }; | |
| 85 | − | } | |
| 86 | − | ||
| 87 | − | /** | |
| 88 | − | * Home's sidebar: never Code's. A way to search or jump, what you pinned | |
| 89 | − | * across modes (starred channels and conversations, pinned projects, and | |
| 90 | − | * docs once they exist), and what you were in last, each marked with its | |
| 91 | − | * mode. | |
| 92 | − | */ | |
| 93 | − | export function HomeSidebar({ | |
| 94 | − | slug, | |
| 95 | − | shell, | |
| 96 | − | code, | |
| 97 | − | onFind, | |
| 98 | − | header, | |
| 99 | − | }: { | |
| 100 | − | slug: string; | |
| 101 | − | shell: ShellData; | |
| 102 | − | /** Whether the viewer uses Code here: without it, no projects. */ | |
| 103 | − | code: boolean; | |
| 104 | − | onFind: () => void; | |
| 105 | − | header: ReactNode; | |
| 106 | − | }) { | |
| 107 | − | const chat = shell.chat; | |
| 108 | − | const starredChats = chat?.starred ?? []; | |
| 109 | − | const starred: Item[] = [ | |
| 110 | − | ...starredChats.map((entry) => chatItem(slug, entry)), | |
| 111 | − | ...(code ? (shell.pinned ?? []) : []).map((project) => ({ | |
| 112 | − | key: `project:${project.namespace}/${project.name}`, | |
| 113 | − | to: `/${project.namespace}/${project.name}`, | |
| 114 | − | label: project.title ?? project.name, | |
| 115 | − | mode: "code" as const, | |
| 116 | − | })), | |
| 117 | − | ]; | |
| 118 | − | const starredIds = new Set(starredChats.map((entry) => entry.channel.id)); | |
| 119 | − | const pinnedProjects = new Set((shell.pinned ?? []).map((project) => `${project.namespace}/${project.name}`)); | |
| 120 | − | const agents = shell.agents ?? []; | |
| 121 | − | const recent: Item[] = [ | |
| 122 | − | ...(chat?.recent ?? []).filter((entry) => !starredIds.has(entry.channel.id)).slice(0, RECENT_CHATS).map((entry) => chatItem(slug, entry)), | |
| 123 | − | ...(code ? (shell.recent ?? []) : []) | |
| 124 | − | .filter((project) => !pinnedProjects.has(`${project.namespace}/${project.name}`)) | |
| 125 | − | .slice(0, RECENT_PROJECTS) | |
| 126 | − | .map((project) => ({ | |
| 127 | − | key: `project:${project.namespace}/${project.name}`, | |
| 128 | − | to: `/${project.namespace}/${project.name}`, | |
| 129 | − | label: project.title ?? project.name, | |
| 130 | − | mode: "code" as const, | |
| 131 | − | })), | |
| 132 | − | ...[...agents] | |
| 133 | − | .sort((a, b) => Number(isOrchestrator(b)) - Number(isOrchestrator(a)) || Number(b.status !== "idle") - Number(a.status !== "idle")) | |
| 134 | − | .slice(0, RECENT_AGENTS) | |
| 135 | − | .map((agent) => agentItem(slug, agent)), | |
| 136 | − | ]; | |
| 137 | − | return ( | |
| 138 | − | <div className="flex h-full flex-col"> | |
| 139 | − | {header} | |
| 140 | − | <div className="px-2.5 pt-3"> | |
| 141 | − | <button | |
| 142 | − | type="button" | |
| 143 | − | onClick={onFind} | |
| 144 | − | className="flex h-9 w-full items-center gap-2 rounded-md bg-surface px-2.5 text-[0.8125rem] text-faint ring-1 ring-line transition-colors hover:text-muted hover:ring-line-strong" | |
| 145 | − | > | |
| 146 | − | <Search size={14} /> | |
| 147 | − | <span className="grow text-left">Search or jump to…</span> | |
| 148 | − | <PaletteKey className="rounded bg-raised px-1.5 font-mono text-[0.625rem] text-muted ring-1 ring-line" /> | |
| 149 | − | </button> | |
| 150 | − | </div> | |
| 151 | − | <nav aria-label="Home" className="min-h-0 grow overflow-y-auto px-2.5 pb-4 [scrollbar-width:thin]"> | |
| 152 | − | <Group title="Starred" items={starred} empty="Star a channel or pin a project to keep it here." /> | |
| 153 | − | <Group title="Recent" items={recent} empty={chat == null ? "Recent conversations show here once chat answers." : "Nothing yet."} /> | |
| 154 | − | </nav> | |
| 155 | − | </div> | |
| 156 | − | ); | |
| 157 | − | } |
| 1 | − | import { AlarmClock, Bell, Bookmark, Check, CheckCheck, CircleCheck, CircleX, Ellipsis, Hand, Inbox, Info, Mail, MailOpen, Sparkles, Undo2 } from "lucide-react"; | |
| 1 | + | import { AlarmClock, Bell, Bookmark, Check, CheckCheck, CircleCheck, CircleX, Ellipsis, Hand, Info, Mail, MailOpen, Undo2 } from "lucide-react"; | |
| 2 | 2 | import { type ReactNode, useEffect, useState } from "react"; | |
| 3 | 3 | import { Link, useFetcher } from "react-router"; | |
| 4 | 4 | ||
| ⋯ | |||
| 12 | 12 | import { Sheet, SheetClose, SheetContent, SheetDescription, SheetFooter, SheetHeader, SheetTitle, SheetTrigger } from "./ui/sheet"; | |
| 13 | 13 | import { Skeleton } from "./ui/skeleton"; | |
| 14 | 14 | import { Tabs, TabsList, TabsTrigger } from "./ui/tabs"; | |
| 15 | − | import { Tooltip, TooltipContent, TooltipTrigger } from "./ui/tooltip"; | |
| 16 | 15 | import { cn } from "../lib/cn"; | |
| 17 | 16 | import { isPending } from "../lib/pending"; | |
| 18 | 17 | import { INBOX_TABS, type InboxTab, REASON_LABEL, SEVERITY_LABEL, SNOOZES, bellCount, emptyFor, isUnread, tabCount, updatesLabel, whenShort } from "../lib/inbox"; | |
| 19 | 18 | import type { InboxPanelData } from "../routes/inbox-json"; | |
| 20 | 19 | ||
| 21 | − | /** Where every inbox form posts (routes/inbox.tsx). */ | |
| 22 | − | const ACTION = "/inbox"; | |
| 20 | + | /** Where every notifications form posts (routes/notifications.tsx). */ | |
| 21 | + | const ACTION = "/notifications"; | |
| 23 | 22 | /** How often the bell asks for its count while the page is in view. */ | |
| 24 | 23 | const COUNT_EVERY_MS = 60_000; | |
| 25 | 24 | ||
| ⋯ | |||
| 93 | 92 | <Link | |
| 94 | 93 | to={item.url} | |
| 95 | 94 | onClick={open} | |
| 96 | − | className="block truncate pr-14 text-sm font-semibold text-fg outline-none after:absolute after:inset-0 after:rounded-lg focus-visible:after:ring-2 focus-visible:after:ring-accent" | |
| 95 | + | className="block truncate pr-14 text-sm font-semibold text-fg outline-none [@media(hover:none)]:pr-24 after:absolute after:inset-0 after:rounded-lg focus-visible:after:ring-2 focus-visible:after:ring-accent" | |
| 97 | 96 | > | |
| 98 | 97 | {item.title} | |
| 99 | 98 | </Link> | |
| 100 | 99 | {item.body && <p className="mt-0.5 truncate text-xs text-muted">{item.body}</p>} | |
| 101 | − | <div className="mt-2 flex min-w-0 items-center gap-2 text-xs text-faint"> | |
| 100 | + | <div className="mt-2 flex min-w-0 items-center gap-2 text-xs text-faint [@media(hover:none)]:pr-24"> | |
| 102 | 101 | <time dateTime={item.updatedAt} suppressHydrationWarning className="shrink-0 whitespace-nowrap"> | |
| 103 | 102 | {whenShort(item.updatedAt, now)} | |
| 104 | 103 | </time> | |
| ⋯ | |||
| 126 | 125 | {item.doneAt ? ( | |
| 127 | 126 | <fetcher.Form method="post" action={ACTION}> | |
| 128 | 127 | <Fields intent="undone" id={item.id} /> | |
| 129 | − | <Hint label="Move back to the inbox"> | |
| 130 | − | <SubmitButton fetcher={fetcher} icon aria-label="Move back to the inbox" className={ICON_BUTTON}> | |
| 128 | + | <Hint label="Move back to Notifications"> | |
| 129 | + | <SubmitButton fetcher={fetcher} icon aria-label="Move back to Notifications" className={ICON_BUTTON}> | |
| 131 | 130 | <Undo2 size={14} /> | |
| 132 | 131 | </SubmitButton> | |
| 133 | 132 | </Hint> | |
| ⋯ | |||
| 182 | 181 | const { title, detail } = emptyFor(tab, view); | |
| 183 | 182 | return ( | |
| 184 | 183 | <div className="flex flex-col items-center gap-2 rounded-lg border border-dashed border-line px-6 py-12 text-center"> | |
| 185 | − | <Inbox size={20} className="text-faint" aria-hidden="true" /> | |
| 184 | + | <Bell size={20} className="text-faint" aria-hidden="true" /> | |
| 186 | 185 | <p className="text-sm font-medium">{title}</p> | |
| 187 | 186 | <p className="max-w-64 text-xs text-muted">{detail}</p> | |
| 188 | 187 | </div> | |
| ⋯ | |||
| 239 | 238 | ||
| 240 | 239 | /** | |
| 241 | 240 | * The bell in the top bar, and the panel it opens from the right: the | |
| 242 | − | * inbox's tabs and newest items, to work through without leaving the page. | |
| 241 | + | * notifications's tabs and newest items, to work through without leaving the page. | |
| 243 | 242 | * Its count comes with the page (root.tsx), and is asked for again every | |
| 244 | 243 | * minute while the page is in view, and after anything is marked. | |
| 245 | 244 | */ | |
| 246 | − | export function InboxBell({ counts: loaded }: { counts: InboxCounts | null }) { | |
| 245 | + | export function NotificationsBell({ counts: loaded }: { counts: InboxCounts | null }) { | |
| 247 | 246 | const [open, setOpen] = useState(false); | |
| 248 | 247 | const [tab, setTab] = useState<InboxTab>("all"); | |
| 249 | 248 | const list = useFetcher<InboxPanelData>({ key: "inbox-panel" }); | |
| ⋯ | |||
| 274 | 273 | return ( | |
| 275 | 274 | <Sheet open={open} onOpenChange={setOpen}> | |
| 276 | 275 | <SheetTrigger | |
| 277 | − | aria-label={unread ? `Inbox, ${unread} unread` : "Inbox"} | |
| 276 | + | aria-label={unread ? `Notifications, ${unread} unread` : "Notifications"} | |
| 278 | 277 | className="relative flex size-9 items-center justify-center rounded-md text-muted transition-colors hover:bg-raised hover:text-fg" | |
| 279 | 278 | > | |
| 280 | 279 | <Bell size={17} /> | |
| ⋯ | |||
| 298 | 297 | }} | |
| 299 | 298 | > | |
| 300 | 299 | <SheetHeader className="flex-row items-center justify-between gap-3"> | |
| 301 | − | <SheetTitle>Inbox</SheetTitle> | |
| 300 | + | <SheetTitle>Notifications</SheetTitle> | |
| 302 | 301 | <MarkAllRead tab={tab} disabled={tabCount(counts, tab) === 0} /> | |
| 303 | 302 | </SheetHeader> | |
| 304 | 303 | <div className="border-b border-line px-5 py-3"> | |
| ⋯ | |||
| 309 | 308 | <WaitingCards onNavigate={() => setOpen(false)} /> | |
| 310 | 309 | {items == null ? ( | |
| 311 | 310 | list.state === "idle" && list.data?.tab === tab ? ( | |
| 312 | − | <p className="text-sm text-muted">The inbox could not be loaded. Try again in a moment.</p> | |
| 311 | + | <p className="text-sm text-muted">Notifications could not be loaded. Try again in a moment.</p> | |
| 313 | 312 | ) : ( | |
| 314 | 313 | <ul aria-busy="true" className="space-y-2"> | |
| 315 | 314 | {Array.from({ length: 4 }, (_, index) => ( | |
| ⋯ | |||
| 334 | 333 | )} | |
| 335 | 334 | </div> | |
| 336 | 335 | <SheetFooter className="justify-between"> | |
| 337 | − | <SheetDescription className="text-xs">Done, saved and snoozed items are in the full inbox.</SheetDescription> | |
| 336 | + | <SheetDescription className="text-xs">Done, saved and snoozed items are on the Notifications page.</SheetDescription> | |
| 338 | 337 | <SheetClose asChild> | |
| 339 | − | <Link to={tab === "all" ? "/inbox" : `/inbox?tab=${tab}`} className="shrink-0 text-sm font-medium text-accent hover:underline"> | |
| 340 | − | Open inbox | |
| 338 | + | <Link to={tab === "all" ? "/notifications" : `/notifications?tab=${tab}`} className="shrink-0 text-sm font-medium text-accent hover:underline"> | |
| 339 | + | Open Notifications | |
| 341 | 340 | </Link> | |
| 342 | 341 | </SheetClose> | |
| 343 | 342 | </SheetFooter> | |
| ⋯ | |||
| 347 | 346 | } | |
| 348 | 347 | ||
| 349 | 348 | /** | |
| 350 | − | * Agent: g1t's agent to talk to, here to say it is coming, and not yet usable. A disabled button | |
| 351 | − | * gets no pointer events, so the tooltip hangs on a span around it: shown on hover and focus, | |
| 352 | − | * and on a tap, since a touch screen has no hover. | |
| 353 | − | */ | |
| 354 | − | export function AgentButton() { | |
| 355 | − | const [open, setOpen] = useState(false); | |
| 356 | − | return ( | |
| 357 | − | <Tooltip open={open} onOpenChange={setOpen}> | |
| 358 | − | <TooltipTrigger asChild> | |
| 359 | − | <span | |
| 360 | − | tabIndex={0} | |
| 361 | − | // A tap opens it; without preventDefault the trigger's own click would close it again. | |
| 362 | − | onClick={(event) => { | |
| 363 | − | event.preventDefault(); | |
| 364 | − | setOpen(true); | |
| 365 | − | }} | |
| 366 | − | className="inline-flex rounded-md outline-none focus-visible:ring-2 focus-visible:ring-accent" | |
| 367 | − | > | |
| 368 | − | <button | |
| 369 | − | type="button" | |
| 370 | − | disabled | |
| 371 | − | aria-label="Agent" | |
| 372 | − | className="pointer-events-none flex h-9 items-center gap-1.5 rounded-md border border-line px-2.5 text-sm text-muted opacity-60 sm:px-3" | |
| 373 | − | > | |
| 374 | − | <Sparkles size={15} className="text-accent" /> | |
| 375 | − | <span className="hidden sm:inline">Agent</span> | |
| 376 | − | </button> | |
| 377 | − | </span> | |
| 378 | − | </TooltipTrigger> | |
| 379 | − | <TooltipContent>Agent is coming soon</TooltipContent> | |
| 380 | − | </Tooltip> | |
| 381 | − | ); | |
| 382 | − | } | |
| 383 | − | ||
| 384 | − | /** | |
| 385 | − | * Mission control's card of what is unread in the person's inbox and | |
| 349 | + | * Mission control's card of what is unread in the person's notifications and | |
| 386 | 350 | * worth a look: items an agent is waiting on, then failures. Titled for | |
| 387 | − | * the inbox, not "Needs you", which on mission control is the work list's | |
| 351 | + | * notifications, not "Needs you", which on mission control is the work list's | |
| 388 | 352 | * own count. Not shown when there are none. | |
| 389 | 353 | */ | |
| 390 | 354 | export function InboxNeedsCard({ items, total }: { items: InboxItem[]; total: number }) { | |
| ⋯ | |||
| 393 | 357 | <section className="rounded-xl border border-line bg-surface p-5"> | |
| 394 | 358 | <div className="flex items-center justify-between gap-3"> | |
| 395 | 359 | <h2 className="flex items-center gap-2 text-sm font-semibold"> | |
| 396 | − | <Inbox size={14} className="text-warn" aria-hidden="true" /> | |
| 397 | − | From your inbox | |
| 360 | + | <Bell size={14} className="text-warn" aria-hidden="true" /> | |
| 361 | + | From your notifications | |
| 398 | 362 | <span className="rounded-full bg-warn/15 px-1.5 text-[0.6875rem] font-medium tabular-nums text-warn">{total}</span> | |
| 399 | 363 | </h2> | |
| 400 | − | <Link to="/inbox" className="text-xs font-medium text-accent hover:underline"> | |
| 401 | − | Open inbox | |
| 364 | + | <Link to="/notifications" className="text-xs font-medium text-accent hover:underline"> | |
| 365 | + | Open Notifications | |
| 402 | 366 | </Link> | |
| 403 | 367 | </div> | |
| 404 | 368 | <ul className="mt-3 space-y-2"> | |
| 8 | 8 | Eye, | |
| 9 | 9 | FileLock2, | |
| 10 | 10 | Handshake, | |
| 11 | − | Inbox, | |
| 11 | + | Bell, | |
| 12 | 12 | KeyRound, | |
| 13 | 13 | Lock, | |
| 14 | 14 | MessagesSquare, | |
| ⋯ | |||
| 561 | 561 | "Live delivery, typing and read state", | |
| 562 | 562 | { text: "Agents that look up code, issues and checks as they answer", soon: true }, | |
| 563 | 563 | { text: "Pull requests, checks and deploys as cards in the channel", soon: true }, | |
| 564 | − | { text: "Mentions and approvals in your inbox", soon: true }, | |
| 564 | + | { text: "Mentions and approvals in Notifications", soon: true }, | |
| 565 | 565 | { text: "Desktop and mobile apps", soon: true }, | |
| 566 | 566 | ]} | |
| 567 | 567 | /> | |
| ⋯ | |||
| 954 | 954 | <div className="flex flex-col justify-between gap-6"> | |
| 955 | 955 | <ul className="space-y-3 text-sm"> | |
| 956 | 956 | {[ | |
| 957 | − | { icon: <Inbox size={15} />, text: "One inbox for mentions, reviews and approvals" }, | |
| 957 | + | { icon: <Bell size={15} />, text: "One place for mentions, reviews and approvals" }, | |
| 958 | 958 | { icon: <ScrollText size={15} />, text: "One audit log for people and agents" }, | |
| 959 | 959 | { icon: <Wallet size={15} />, text: "One bill, at cost plus 20%" }, | |
| 960 | 960 | ].map((item) => ( | |
| 514 | 514 | > | |
| 515 | 515 | <RadioGroup name="notify" defaultValue={settings.notify} className="gap-3"> | |
| 516 | 516 | <RadioOption value="banner" label="Show it on the repository" description={`A line across ${full}'s pages, and an amber dot beside its name.`} /> | |
| 517 | − | <RadioOption value="inbox" label="Also tell workspace owners in their inbox" description="Once when it stops answering, and once when it answers again." /> | |
| 517 | + | <RadioOption value="inbox" label="Also tell workspace owners in their notifications" description="Once when it stops answering, and once when it answers again." /> | |
| 518 | 518 | </RadioGroup> | |
| 519 | 519 | <div className="rounded-xl border border-line bg-surface p-4"> | |
| 520 | 520 | <div className="flex flex-wrap items-center gap-x-2.5 gap-y-2"> |
| 1 | 1 | import { | |
| 2 | 2 | Activity, | |
| 3 | − | BarChart3, | |
| 4 | 3 | BellOff, | |
| 5 | 4 | BellRing, | |
| 6 | 5 | BookOpen, | |
| 7 | − | Building2, | |
| 8 | − | Check, | |
| 9 | − | ChevronRight, | |
| 10 | 6 | CircleUserRound, | |
| 11 | − | Code2, | |
| 12 | − | House, | |
| 13 | − | Inbox, | |
| 7 | + | Ellipsis, | |
| 8 | + | Keyboard, | |
| 9 | + | LayoutGrid, | |
| 14 | 10 | LifeBuoy, | |
| 15 | 11 | LogOut, | |
| 16 | − | MessagesSquare, | |
| 17 | 12 | Moon, | |
| 18 | − | Plug, | |
| 19 | − | Plus, | |
| 20 | 13 | Settings, | |
| 21 | − | Shapes, | |
| 22 | 14 | Smile, | |
| 23 | − | Sparkles, | |
| 15 | + | Store, | |
| 24 | 16 | Sun, | |
| 25 | − | Users, | |
| 26 | − | UsersRound, | |
| 27 | 17 | } from "lucide-react"; | |
| 28 | 18 | import { Dialog as Primitive } from "radix-ui"; | |
| 29 | 19 | import { type ReactNode, useEffect, useRef, useState } from "react"; | |
| ⋯ | |||
| 31 | 21 | ||
| 32 | 22 | import { type Abilities, type Membership, type User, hasCodeAccess, shownUsername } from "@g1t/contracts"; | |
| 33 | 23 | ||
| 24 | + | import { appIcon } from "./apps"; | |
| 25 | + | import { CountBadge, ShortcutsDialog } from "./dock"; | |
| 26 | + | import { Mark } from "./logo"; | |
| 34 | 27 | import { Avatar } from "./ui"; | |
| 35 | 28 | import { TabStrip } from "./ui/tab-strip"; | |
| 36 | 29 | import { StatusDialog } from "./presence"; | |
| 30 | + | import { type AppKey, type PinnableApp, appOf } from "../lib/apps"; | |
| 37 | 31 | import { setPresence, useOwnPresence } from "../lib/notify-client"; | |
| 38 | 32 | import { dndOn, liveStatus, pauseUntil, untilLabel } from "../lib/presence"; | |
| 39 | 33 | import { STATUS_URL } from "../lib/status"; | |
| 40 | − | import { modeOf } from "../lib/workspace-nav"; | |
| 34 | + | import { type ModeKey, modeHome, modeOf } from "../lib/workspace-nav"; | |
| 41 | 35 | import { PROJECT_PAGE_LINKS, type ProjectPage, projectPageAt, projectPages } from "../lib/chrome"; | |
| 42 | 36 | import { ROADMAP, type RoadmapItem } from "../lib/roadmap"; | |
| 43 | 37 | ||
| 44 | 38 | /** | |
| 45 | − | * The phone's layout (below 768px): a tab bar along the bottom, the | |
| 46 | − | * workspace's avatar opening a sheet for everything else, and each tab a | |
| 47 | − | * list that pushes its detail full screen. Desktop keeps the rail. | |
| 39 | + | * The phone's layout (below 768px): a bottom bar of apps with More for | |
| 40 | + | * the rest, the mode's sidebar as a drawer from the left, and each tab a | |
| 41 | + | * list that pushes its detail full screen. A computer has the dock. | |
| 48 | 42 | */ | |
| 49 | 43 | ||
| 50 | 44 | /** Whether a path is one conversation, which takes the whole screen on a phone. */ | |
| ⋯ | |||
| 121 | 115 | }; | |
| 122 | 116 | } | |
| 123 | 117 | ||
| 124 | − | type Tab = { key: string; label: string; to: string; icon: ReactNode; badge?: { count: number; loud: boolean } }; | |
| 118 | + | type Tab = { key: ModeKey; label: string; badge?: { count: number; loud: boolean } }; | |
| 125 | 119 | ||
| 126 | 120 | /** | |
| 127 | − | * The tab bar along the bottom of a phone: Home, Code (Docs, for a member | |
| 128 | − | * without Code), Chat, Agents and the Inbox, each with what is unread. It | |
| 129 | − | * steps aside while the keyboard is up and inside a conversation. The tab | |
| 130 | − | * you are in, tapped again, opens its mode's menu. | |
| 121 | + | * The bottom bar of a phone, floating 8px from the edges: Today, Chat, | |
| 122 | + | * Notifications, Agents and Code (for a member with Code access), each | |
| 123 | + | * with what is unread, and More. It steps aside while the keyboard is up | |
| 124 | + | * and inside a conversation. The tab you are in, tapped again, opens its | |
| 125 | + | * mode's menu (its sidebar). | |
| 131 | 126 | */ | |
| 132 | − | export function MobileTabBar({ | |
| 127 | + | export function BottomBar({ | |
| 128 | + | user, | |
| 133 | 129 | workspace, | |
| 130 | + | pins, | |
| 134 | 131 | unread, | |
| 135 | 132 | onReselect, | |
| 136 | 133 | }: { | |
| 134 | + | user: User; | |
| 137 | 135 | workspace: Membership; | |
| 138 | − | unread: { inbox: number; chat: number; mentions: number }; | |
| 136 | + | /** The apps pinned to this person's dock here, for the More sheet. */ | |
| 137 | + | pins: PinnableApp[]; | |
| 138 | + | unread: { notifications: number; chat: number; mentions: number }; | |
| 139 | 139 | /** Tapping the tab you are already in: the shell opens that mode's menu (its sidebar). */ | |
| 140 | 140 | onReselect?: () => void; | |
| 141 | 141 | }) { | |
| 142 | 142 | const { pathname } = useLocation(); | |
| 143 | + | const [more, setMore] = useState(false); | |
| 144 | + | useEffect(() => setMore(false), [pathname]); | |
| 143 | 145 | const slug = workspace.slug; | |
| 144 | 146 | const code = hasCodeAccess(workspace); | |
| 145 | 147 | const mode = modeOf(pathname, slug); | |
| 146 | 148 | const tabs: Tab[] = [ | |
| 147 | − | { key: "home", label: "Home", to: `/${slug}/-/home`, icon: <House size={21} /> }, | |
| 148 | − | code | |
| 149 | − | ? { key: "code", label: "Code", to: `/${slug}/-/projects`, icon: <Code2 size={21} /> } | |
| 150 | − | : { key: "artifacts", label: "Artifacts", to: `/${slug}/-/artifacts`, icon: <Shapes size={21} /> }, | |
| 151 | − | { | |
| 152 | − | key: "chat", | |
| 153 | − | label: "Chat", | |
| 154 | − | to: `/${slug}/-/chat`, | |
| 155 | − | icon: <MessagesSquare size={21} />, | |
| 156 | − | badge: { count: unread.mentions > 0 ? unread.mentions : unread.chat, loud: unread.mentions > 0 }, | |
| 157 | − | }, | |
| 158 | − | { key: "agents", label: "Agents", to: `/${slug}/-/agents`, icon: <Sparkles size={21} /> }, | |
| 159 | − | { key: "inbox", label: "Inbox", to: "/inbox", icon: <Inbox size={21} />, badge: { count: unread.inbox, loud: true } }, | |
| 149 | + | { key: "today", label: "Today" }, | |
| 150 | + | { key: "chat", label: "Chat", badge: { count: unread.mentions > 0 ? unread.mentions : unread.chat, loud: unread.mentions > 0 } }, | |
| 151 | + | { key: "notifications", label: "Notifications", badge: { count: unread.notifications, loud: true } }, | |
| 152 | + | { key: "agents", label: "Agents" }, | |
| 153 | + | ...(code ? [{ key: "code" as const, label: "Code" }] : []), | |
| 160 | 154 | ]; | |
| 155 | + | const inTabs = tabs.some((tab) => tab.key === mode); | |
| 161 | 156 | if (isConversation(pathname)) return null; | |
| 162 | 157 | return ( | |
| 163 | − | <nav | |
| 164 | − | aria-label="Tabs" | |
| 165 | − | className="fixed inset-x-0 bottom-0 z-40 border-t border-line bg-[#0b0b0d]/95 pb-[env(safe-area-inset-bottom)] backdrop-blur md:hidden in-data-[keyboard=open]:hidden" | |
| 166 | − | > | |
| 167 | − | <ul className="grid h-14 grid-cols-5"> | |
| 168 | − | {tabs.map((tab) => { | |
| 169 | − | const current = mode === tab.key; | |
| 170 | − | const count = tab.badge?.count ?? 0; | |
| 171 | − | return ( | |
| 172 | − | <li key={tab.key}> | |
| 173 | − | <Link | |
| 174 | − | to={tab.to} | |
| 175 | − | prefetch="intent" | |
| 176 | − | aria-current={current ? "page" : undefined} | |
| 177 | − | aria-label={count > 0 ? `${tab.label}, ${count} unread` : tab.label} | |
| 178 | − | onClick={(event) => { | |
| 179 | − | // The tab you are in, tapped again: its menu, as a phone's own apps do. | |
| 180 | − | // Chat's first page on a phone is its sidebar already. | |
| 181 | − | if (!current || !onReselect || (tab.key === "chat" && pathname.replace(/\/$/, "") === tab.to)) return; | |
| 182 | − | event.preventDefault(); | |
| 183 | − | onReselect(); | |
| 158 | + | <> | |
| 159 | + | <nav | |
| 160 | + | aria-label="Apps" | |
| 161 | + | className="fixed right-2 bottom-[calc(0.5rem+env(safe-area-inset-bottom))] left-2 z-40 h-16 rounded-2xl border border-line bg-dock px-1 shadow-[0_1px_0_rgba(255,255,255,0.03)_inset,0_8px_24px_rgba(0,0,0,0.35)] md:hidden in-data-[keyboard=open]:hidden" | |
| 162 | + | > | |
| 163 | + | <ul className="grid h-full" style={{ gridTemplateColumns: `repeat(${tabs.length + 1}, minmax(0, 1fr))` }}> | |
| 164 | + | {tabs.map((tab) => { | |
| 165 | + | const current = mode === tab.key && !more; | |
| 166 | + | const count = tab.badge?.count ?? 0; | |
| 167 | + | const to = modeHome(tab.key, slug); | |
| 168 | + | return ( | |
| 169 | + | <li key={tab.key} className="min-w-0"> | |
| 170 | + | <Link | |
| 171 | + | to={to} | |
| 172 | + | prefetch="intent" | |
| 173 | + | aria-current={current ? "page" : undefined} | |
| 174 | + | aria-label={count > 0 ? `${tab.label}, ${count} unread` : tab.label} | |
| 175 | + | onClick={(event) => { | |
| 176 | + | // The tab you are in, tapped again: its menu, as a phone's own apps do. | |
| 177 | + | // Chat's first page on a phone is its sidebar already. | |
| 178 | + | if (!current || !onReselect || (tab.key === "chat" && pathname.replace(/\/$/, "") === to)) return; | |
| 179 | + | event.preventDefault(); | |
| 180 | + | onReselect(); | |
| 181 | + | }} | |
| 182 | + | className={BAR_ITEM} | |
| 183 | + | > | |
| 184 | + | {current && <TopBar />} | |
| 185 | + | <BarIcon current={current}> | |
| 186 | + | {appIcon(tab.key as AppKey, 20)} | |
| 187 | + | {tab.badge && <CountBadge count={count} loud={tab.badge.loud} />} | |
| 188 | + | </BarIcon> | |
| 189 | + | <span className={`w-full truncate text-[0.625rem] leading-tight font-medium max-[380px]:text-[0.5625rem] max-[380px]:tracking-[-0.02em] ${current ? "text-fg" : "text-faint"}`}>{tab.label}</span> | |
| 190 | + | </Link> | |
| 191 | + | </li> | |
| 192 | + | ); | |
| 193 | + | })} | |
| 194 | + | <li className="min-w-0"> | |
| 195 | + | <button type="button" aria-expanded={more} aria-haspopup="dialog" onClick={() => setMore(true)} className={BAR_ITEM}> | |
| 196 | + | {(more || !inTabs) && <TopBar />} | |
| 197 | + | <BarIcon current={more || !inTabs}> | |
| 198 | + | <Ellipsis size={20} /> | |
| 199 | + | </BarIcon> | |
| 200 | + | <span className={`w-full truncate text-[0.625rem] leading-tight font-medium ${more || !inTabs ? "text-fg" : "text-faint"}`}>More</span> | |
| 201 | + | </button> | |
| 202 | + | </li> | |
| 203 | + | </ul> | |
| 204 | + | </nav> | |
| 205 | + | <MoreSheet open={more} onOpenChange={setMore} user={user} workspace={workspace} pins={pins} /> | |
| 206 | + | </> | |
| 207 | + | ); | |
| 208 | + | } | |
| 209 | + | ||
| 210 | + | const BAR_ITEM = "relative flex h-full w-full flex-col items-center justify-center gap-0.5 rounded-xl outline-none transition-colors focus-visible:ring-2 focus-visible:ring-accent active:bg-raised/60"; | |
| 211 | + | ||
| 212 | + | /** The bar on the bottom bar's top edge, over the current tab. */ | |
| 213 | + | function TopBar() { | |
| 214 | + | return <span aria-hidden="true" className="absolute -top-px left-1/2 h-[3px] w-[22px] -translate-x-1/2 rounded-b-[3px] bg-fg" />; | |
| 215 | + | } | |
| 216 | + | ||
| 217 | + | function BarIcon({ current, children }: { current: boolean; children: ReactNode }) { | |
| 218 | + | return ( | |
| 219 | + | <span className={`relative flex size-8 items-center justify-center rounded-[10px] transition-colors ${current ? "bg-raised text-fg shadow-[inset_0_0_0_1px_var(--color-line-strong)]" : "text-muted"}`}> | |
| 220 | + | {children} | |
| 221 | + | </span> | |
| 222 | + | ); | |
| 223 | + | } | |
| 224 | + | ||
| 225 | + | /** A big tile in the More sheet. */ | |
| 226 | + | function MoreTile({ to, icon, label, onClick, disabled, note }: { to?: string; icon: ReactNode; label: string; onClick?: () => void; disabled?: boolean; note?: string }) { | |
| 227 | + | const inner = ( | |
| 228 | + | <> | |
| 229 | + | <span className={`flex size-11 items-center justify-center rounded-xl ${disabled ? "bg-raised/60 text-faint" : "bg-raised text-fg"}`}>{icon}</span> | |
| 230 | + | <span className={`line-clamp-2 w-full text-xs leading-tight max-[380px]:text-[0.6875rem] ${disabled ? "text-faint" : "text-fg-soft"}`}>{label}</span> | |
| 231 | + | {note && <span className="-mt-0.5 rounded-full bg-line px-1.5 text-[0.625rem] font-medium text-muted">{note}</span>} | |
| 232 | + | </> | |
| 233 | + | ); | |
| 234 | + | const className = "flex min-w-0 flex-col items-center gap-1.5 rounded-xl px-1 py-2.5 text-center outline-none focus-visible:ring-2 focus-visible:ring-accent active:bg-raised/60"; | |
| 235 | + | if (to) { | |
| 236 | + | return ( | |
| 237 | + | <Link to={to} className={className}> | |
| 238 | + | {inner} | |
| 239 | + | </Link> | |
| 240 | + | ); | |
| 241 | + | } | |
| 242 | + | return ( | |
| 243 | + | <button type="button" onClick={onClick} disabled={disabled} aria-disabled={disabled} className={className}> | |
| 244 | + | {inner} | |
| 245 | + | </button> | |
| 246 | + | ); | |
| 247 | + | } | |
| 248 | + | ||
| 249 | + | /** | |
| 250 | + | * More, from the bottom bar: a rounded panel just above it, with g1t's | |
| 251 | + | * mark at its top and a grid of big tiles: all apps, the Marketplace | |
| 252 | + | * (coming), your pinned apps, Artifacts, People, Workspace, and "You and | |
| 253 | + | * help", which opens your account's sheet. | |
| 254 | + | */ | |
| 255 | + | function MoreSheet({ open, onOpenChange, user, workspace, pins }: { open: boolean; onOpenChange: (open: boolean) => void; user: User; workspace: Membership; pins: PinnableApp[] }) { | |
| 256 | + | const slug = workspace.slug; | |
| 257 | + | const code = hasCodeAccess(workspace); | |
| 258 | + | const [account, setAccount] = useState(false); | |
| 259 | + | const shown = pins.filter((key) => code || !appOf(key).code); | |
| 260 | + | return ( | |
| 261 | + | <> | |
| 262 | + | <Primitive.Root open={open} onOpenChange={onOpenChange}> | |
| 263 | + | <Primitive.Portal> | |
| 264 | + | <Primitive.Overlay className="fixed inset-0 z-50 bg-black/50 data-[state=open]:animate-fade-in data-[state=closed]:animate-fade-out md:hidden" /> | |
| 265 | + | <Primitive.Content | |
| 266 | + | aria-describedby={undefined} | |
| 267 | + | onOpenAutoFocus={(event) => event.preventDefault()} | |
| 268 | + | className="fixed right-2 bottom-[calc(4.875rem+env(safe-area-inset-bottom))] left-2 z-50 max-h-[calc(100dvh-7rem)] overflow-y-auto rounded-[18px] border border-line-strong bg-dock px-3 pt-2 pb-3.5 text-fg shadow-2xl shadow-black/60 outline-none data-[state=open]:animate-pop-in data-[state=closed]:animate-pop-out md:hidden" | |
| 269 | + | > | |
| 270 | + | <div aria-hidden="true" className="mx-auto mt-0.5 mb-2 h-1 w-9 rounded-full bg-line-strong" /> | |
| 271 | + | <div className="mb-2 flex items-center gap-2 px-1"> | |
| 272 | + | <Mark tight className="h-4 w-auto" /> | |
| 273 | + | <Primitive.Title className="text-sm font-semibold">More</Primitive.Title> | |
| 274 | + | <span className="ml-auto truncate font-mono text-xs text-faint">{slug}</span> | |
| 275 | + | </div> | |
| 276 | + | <div className="grid grid-cols-4 gap-1.5"> | |
| 277 | + | <MoreTile to={`/${slug}/-/apps`} icon={<LayoutGrid size={20} />} label="All apps" /> | |
| 278 | + | <MoreTile icon={<Store size={20} />} label="Marketplace" disabled note="Coming" /> | |
| 279 | + | {shown.map((key) => ( | |
| 280 | + | <MoreTile key={key} to={appOf(key).path(slug)} icon={appIcon(key, 20)} label={appOf(key).name} /> | |
| 281 | + | ))} | |
| 282 | + | <MoreTile to={appOf("artifacts").path(slug)} icon={appIcon("artifacts", 20)} label="Artifacts" /> | |
| 283 | + | <MoreTile to={appOf("people").path(slug)} icon={appIcon("people", 20)} label="People" /> | |
| 284 | + | <MoreTile to={appOf("workspace").path(slug)} icon={appIcon("workspace", 20)} label="Workspace" /> | |
| 285 | + | <MoreTile | |
| 286 | + | icon={<Avatar name={user.username} image={user.avatar} size={24} />} | |
| 287 | + | label="You and help" | |
| 288 | + | onClick={() => { | |
| 289 | + | onOpenChange(false); | |
| 290 | + | setAccount(true); | |
| 184 | 291 | }} | |
| 185 | − | className="flex h-full flex-col items-center justify-center gap-0.5 transition-colors active:bg-raised/60" | |
| 186 | − | > | |
| 187 | − | <span className={`relative flex h-7 w-12 items-center justify-center rounded-full transition-colors ${current ? "bg-[#2c2c33] text-fg" : "text-muted"}`}> | |
| 188 | − | {tab.icon} | |
| 189 | − | {count > 0 && ( | |
| 190 | − | <span | |
| 191 | − | className={`absolute -top-1 right-0.5 min-w-[1.125rem] rounded-full px-1 text-center text-[0.625rem] leading-[1.125rem] font-bold tabular-nums ring-2 ring-[#0b0b0d] ${ | |
| 192 | − | tab.badge?.loud ? "bg-accent text-bg" : "bg-[#3d3d45] text-fg" | |
| 193 | − | }`} | |
| 194 | − | > | |
| 195 | − | {count > 99 ? "99+" : count} | |
| 196 | − | </span> | |
| 197 | − | )} | |
| 198 | − | </span> | |
| 199 | − | <span className={`text-[0.6875rem] font-medium ${current ? "text-fg" : "text-faint"}`}>{tab.label}</span> | |
| 200 | − | </Link> | |
| 201 | − | </li> | |
| 202 | − | ); | |
| 203 | − | })} | |
| 204 | − | </ul> | |
| 205 | − | </nav> | |
| 292 | + | /> | |
| 293 | + | </div> | |
| 294 | + | </Primitive.Content> | |
| 295 | + | </Primitive.Portal> | |
| 296 | + | </Primitive.Root> | |
| 297 | + | <AccountSheet open={account} onOpenChange={setAccount} user={user} /> | |
| 298 | + | </> | |
| 206 | 299 | ); | |
| 207 | 300 | } | |
| 208 | − | ||
| 209 | 301 | /** | |
| 210 | 302 | * A sheet that rises from the bottom of a phone, with a handle to drag it | |
| 211 | 303 | * back down. Escape, a tap outside or a drag of more than 90px closes it. | |
| ⋯ | |||
| 300 | 392 | {children} | |
| 301 | 393 | </section> | |
| 302 | 394 | ); | |
| 303 | − | } | |
| 304 | − | ||
| 305 | − | function displayName(membership: Membership): string { | |
| 306 | − | return membership.name?.trim() || membership.slug; | |
| 307 | 395 | } | |
| 308 | 396 | ||
| 309 | 397 | /** Your status, away and pausing notifications, as the account menu has them on a computer. */ | |
| ⋯ | |||
| 345 | 433 | } | |
| 346 | 434 | ||
| 347 | 435 | /** | |
| 348 | − | * The phone's everything-else: the workspace avatar at the top left opens | |
| 349 | − | * it. Docs and the workspace's own pages first, then your workspaces, | |
| 350 | − | * help, and your account. The mode you are in has its own menu (its | |
| 351 | − | * sidebar), behind the button beside the avatar. | |
| 436 | + | * You and help, on a phone: your status, your profile and settings, the | |
| 437 | + | * documentation, support, status and the keyboard's shortcuts, and signing | |
| 438 | + | * out, as the account menu has them on a computer. | |
| 352 | 439 | */ | |
| 353 | − | export function AvatarSheetButton({ user, workspace }: { user: User; workspace: Membership }) { | |
| 354 | − | const [open, setOpen] = useState(false); | |
| 355 | − | const { pathname } = useLocation(); | |
| 440 | + | export function AccountSheet({ open, onOpenChange, user }: { open: boolean; onOpenChange: (open: boolean) => void; user: User }) { | |
| 356 | 441 | const submit = useSubmit(); | |
| 357 | − | useEffect(() => setOpen(false), [pathname]); | |
| 358 | − | const slug = workspace.slug; | |
| 359 | − | const owner = workspace.role === "owner"; | |
| 360 | 442 | const [editing, setEditing] = useState(false); | |
| 443 | + | const [keys, setKeys] = useState(false); | |
| 361 | 444 | return ( | |
| 362 | 445 | <> | |
| 363 | − | <button | |
| 364 | − | type="button" | |
| 365 | − | aria-label={`${displayName(workspace)}: workspaces, settings and account`} | |
| 366 | − | onClick={() => setOpen(true)} | |
| 367 | − | className="-ml-1 flex size-10 shrink-0 items-center justify-center rounded-xl transition-transform active:scale-95 md:hidden" | |
| 368 | − | > | |
| 369 | − | <Avatar name={slug} image={workspace.avatar} size={32} square /> | |
| 370 | − | </button> | |
| 371 | − | <BottomSheet open={open} onOpenChange={setOpen} title="Workspaces, settings and account"> | |
| 446 | + | <BottomSheet open={open} onOpenChange={onOpenChange} title="You and help"> | |
| 372 | 447 | <div className="flex items-center gap-3 px-3 pt-1 pb-3"> | |
| 373 | − | <Avatar name={slug} image={workspace.avatar} size={44} square /> | |
| 448 | + | <Avatar name={user.username} image={user.avatar} size={44} /> | |
| 374 | 449 | <div className="min-w-0"> | |
| 375 | − | <p className="truncate text-base font-semibold">{displayName(workspace)}</p> | |
| 376 | − | <p className="truncate font-mono text-xs text-muted">g1t.sh/{slug}</p> | |
| 450 | + | <p className="truncate text-base font-semibold">@{shownUsername(user)}</p> | |
| 377 | 451 | </div> | |
| 378 | 452 | </div> | |
| 379 | − | <SheetGroup title="Workspace"> | |
| 380 | − | {/* Artifacts has no tab of its own for someone with Code: it leads here. */} | |
| 381 | − | {hasCodeAccess(workspace) && ( | |
| 382 | − | <SheetRow to={`/${slug}/-/artifacts`} icon={<Shapes />} end={<ChevronRight size={16} className="text-faint" />}> | |
| 383 | − | Artifacts | |
| 384 | − | </SheetRow> | |
| 385 | − | )} | |
| 386 | − | <SheetRow to={`/${slug}/-/workspace`} icon={<Building2 />}> | |
| 387 | − | Overview | |
| 388 | − | </SheetRow> | |
| 389 | − | <SheetRow to={`/${slug}/-/people`} icon={<Users />}> | |
| 390 | − | People | |
| 391 | − | </SheetRow> | |
| 392 | − | <SheetRow to={`/${slug}/-/teams`} icon={<UsersRound />}> | |
| 393 | − | Teams | |
| 453 | + | <SheetGroup title="You"> | |
| 454 | + | <OwnPresenceRows | |
| 455 | + | onEdit={() => { | |
| 456 | + | onOpenChange(false); | |
| 457 | + | setEditing(true); | |
| 458 | + | }} | |
| 459 | + | /> | |
| 460 | + | <SheetRow to={`/u/${user.username}`} icon={<CircleUserRound />}> | |
| 461 | + | Your profile | |
| 394 | 462 | </SheetRow> | |
| 395 | − | <SheetRow to={`/${slug}/-/usage`} icon={<BarChart3 />}> | |
| 396 | − | Usage and billing | |
| 397 | − | </SheetRow> | |
| 398 | − | <SheetRow to={`/${slug}/-/integrations`} icon={<Plug />}> | |
| 399 | − | Integrations | |
| 400 | − | </SheetRow> | |
| 401 | − | <SheetRow to={owner ? `/${slug}/-/settings` : `/${slug}/-/repositories`} icon={<Settings />}> | |
| 402 | − | Settings | |
| 463 | + | <SheetRow to="/settings" icon={<Settings />}> | |
| 464 | + | Your settings | |
| 403 | 465 | </SheetRow> | |
| 404 | 466 | </SheetGroup> | |
| 405 | − | <SheetGroup title="Switch workspace"> | |
| 406 | − | {(user.workspaces ?? []).map((membership) => ( | |
| 407 | − | <SheetRow | |
| 408 | − | key={membership.slug} | |
| 409 | − | to={`/${membership.slug}/-/home`} | |
| 410 | − | icon={<Avatar name={membership.slug} image={membership.avatar} size={24} square />} | |
| 411 | − | end={membership.slug === slug ? <Check size={18} className="text-accent" /> : null} | |
| 412 | − | > | |
| 413 | − | {displayName(membership)} | |
| 414 | − | </SheetRow> | |
| 415 | − | ))} | |
| 416 | − | <SheetRow to="/workspaces/new" icon={<Plus />}> | |
| 417 | − | New workspace | |
| 467 | + | <SheetGroup title="Help"> | |
| 468 | + | <SheetRow href="https://docs.g1t.sh/" icon={<BookOpen />}> | |
| 469 | + | Documentation | |
| 418 | 470 | </SheetRow> | |
| 419 | − | </SheetGroup> | |
| 420 | − | <SheetGroup title="Help"> | |
| 421 | 471 | <SheetRow to="/support" icon={<LifeBuoy />}> | |
| 422 | 472 | Support | |
| 423 | 473 | </SheetRow> | |
| 424 | − | <SheetRow href="https://docs.g1t.sh/" icon={<BookOpen />}> | |
| 425 | − | Documentation | |
| 426 | − | </SheetRow> | |
| 427 | 474 | <SheetRow href={STATUS_URL} icon={<Activity />}> | |
| 428 | 475 | Status | |
| 429 | 476 | </SheetRow> | |
| 430 | − | </SheetGroup> | |
| 431 | − | <SheetGroup title={`@${shownUsername(user)}`}> | |
| 432 | − | <OwnPresenceRows | |
| 433 | − | onEdit={() => { | |
| 434 | − | setOpen(false); | |
| 435 | − | setEditing(true); | |
| 477 | + | <SheetRow | |
| 478 | + | icon={<Keyboard />} | |
| 479 | + | onClick={() => { | |
| 480 | + | onOpenChange(false); | |
| 481 | + | setKeys(true); | |
| 436 | 482 | }} | |
| 437 | − | /> | |
| 438 | − | <SheetRow to={`/u/${user.username}`} icon={<CircleUserRound />}> | |
| 439 | − | Your profile | |
| 440 | − | </SheetRow> | |
| 441 | − | <SheetRow to="/settings" icon={<Settings />}> | |
| 442 | − | Your settings | |
| 483 | + | > | |
| 484 | + | Keyboard shortcuts | |
| 443 | 485 | </SheetRow> | |
| 486 | + | </SheetGroup> | |
| 487 | + | <SheetGroup title="Account"> | |
| 444 | 488 | <SheetRow icon={<LogOut />} onClick={() => submit(null, { method: "post", action: "/logout" })}> | |
| 445 | 489 | Sign out | |
| 446 | 490 | </SheetRow> | |
| 447 | 491 | </SheetGroup> | |
| 448 | 492 | </BottomSheet> | |
| 449 | 493 | <StatusDialog open={editing} onOpenChange={setEditing} /> | |
| 494 | + | <ShortcutsDialog open={keys} onOpenChange={setKeys} /> | |
| 450 | 495 | </> | |
| 451 | 496 | ); | |
| 452 | 497 | } | |
| ⋯ | |||
| 477 | 522 | can, | |
| 478 | 523 | settings, | |
| 479 | 524 | counts, | |
| 525 | + | always = false, | |
| 480 | 526 | }: { | |
| 481 | 527 | /** `/<namespace>/<name>`. */ | |
| 482 | 528 | base: string; | |
| ⋯ | |||
| 485 | 531 | /** Whether they see its settings (lib/access.ts `seesSettings`), as the sidebar decides. */ | |
| 486 | 532 | settings?: boolean; | |
| 487 | 533 | counts?: { issues: number; pulls: number }; | |
| 534 | + | /** On a computer too: for a visitor, who has no sidebar to list them. */ | |
| 535 | + | always?: boolean; | |
| 488 | 536 | }) { | |
| 489 | 537 | const { pathname } = useLocation(); | |
| 490 | 538 | const rest = pathname.toLowerCase().startsWith(base.toLowerCase()) ? pathname.slice(base.length) : ""; | |
| ⋯ | |||
| 497 | 545 | current ? "bg-raised font-medium text-fg ring-1 ring-line-strong" : "text-muted active:bg-raised/60" | |
| 498 | 546 | }`; | |
| 499 | 547 | return ( | |
| 500 | − | <TabStrip label="Project" className="-mx-4 gap-1 px-4 md:hidden"> | |
| 548 | + | <TabStrip label="Project" className={`-mx-4 gap-1 px-4 ${always ? "" : "md:hidden"}`}> | |
| 501 | 549 | {pages.map((page) => { | |
| 502 | 550 | const current = at === page; | |
| 503 | 551 | const link = page === "overview" ? { label: "Overview", path: "" } : PROJECT_PAGE_LINKS[page]; | |
| 23 | 23 | } from "../../lib/notify-client"; | |
| 24 | 24 | ||
| 25 | 25 | const LEVELS: { level: NotifyLevel; label: string; detail: string }[] = [ | |
| 26 | − | { level: "all", label: "Everything", detail: "Every message in conversations you're in, and everything in your inbox." }, | |
| 26 | + | { level: "all", label: "Everything", detail: "Every message in conversations you're in, and everything in your notifications." }, | |
| 27 | 27 | { | |
| 28 | 28 | level: "dms_mentions", | |
| 29 | 29 | label: "Direct messages, mentions and replies", |
| 1 | − | import { AtSign, BellRing, CornerDownLeft, GitPullRequest, Hourglass, Inbox, MessageCircle, MessagesSquare, X } from "lucide-react"; | |
| 1 | + | import { AtSign, BellRing, CornerDownLeft, GitPullRequest, Hourglass, Bell, MessageCircle, MessagesSquare, X } from "lucide-react"; | |
| 2 | 2 | import { type FormEvent, type ReactNode, useLayoutEffect, useRef, useState } from "react"; | |
| 3 | 3 | ||
| 4 | 4 | import type { FeedNotification, NotificationKind } from "@g1t/contracts"; | |
| ⋯ | |||
| 13 | 13 | dm: { label: "Direct message", icon: <MessageCircle /> }, | |
| 14 | 14 | mention: { label: "Mentioned you", icon: <AtSign /> }, | |
| 15 | 15 | thread_reply: { label: "Replied in a thread", icon: <MessagesSquare /> }, | |
| 16 | − | inbox: { label: "Inbox", icon: <Inbox /> }, | |
| 16 | + | inbox: { label: "Notifications", icon: <Bell /> }, | |
| 17 | 17 | agent_waiting: { label: "Waiting on you", icon: <Hourglass /> }, | |
| 18 | 18 | approval: { label: "Needs your review", icon: <GitPullRequest /> }, | |
| 19 | 19 | }; | |
| 13 | 13 | * per step, chosen with the pills. | |
| 14 | 14 | * | |
| 15 | 15 | * It is an honest miniature of the app: the same rail (the workspace on | |
| 16 | − | * the top bar's line, then Home, Code, Chat, Docs, Agents and the Inbox, | |
| 16 | + | * the top bar's line, then Today, Code, Chat, Docs, Agents and Notifications, | |
| 17 | 17 | * with names), each mode's sidebar with its real sections and words, the | |
| 18 | 18 | * same top bar (where you are, Explore, Docs, Ask g1t, the inbox and +), | |
| 19 | 19 | * which Chat gives up to the conversation's own header as the app does, | |
| ⋯ | |||
| 44 | 44 | GitMerge, | |
| 45 | 45 | GitPullRequest, | |
| 46 | 46 | Hash, | |
| 47 | − | House, | |
| 48 | − | Inbox, | |
| 47 | + | Sun, | |
| 49 | 48 | MessagesSquare, | |
| 50 | 49 | Network, | |
| 51 | 50 | Paperclip, | |
| ⋯ | |||
| 349 | 348 | ||
| 350 | 349 | /** The rail's modes, in the app's order (components/rail.tsx), with its icons. */ | |
| 351 | 350 | const RAIL: { key: string; icon: ReactNode; label: string; target?: CursorTarget; scene?: Scene; badge?: number }[] = [ | |
| 352 | − | { key: "home", icon: <House size={15} />, label: "Home" }, | |
| 351 | + | { key: "home", icon: <Sun size={15} />, label: "Today" }, | |
| 353 | 352 | { key: "code", icon: <Code2 size={15} />, label: "Code", target: "rail-code", scene: "code" }, | |
| 354 | 353 | { key: "chat", icon: <MessagesSquare size={15} />, label: "Chat", target: "rail-chat", scene: "chat", badge: 3 }, | |
| 355 | 354 | { key: "artifacts", icon: <Shapes size={15} />, label: "Artifacts", target: "rail-docs", scene: "docs" }, | |
| 356 | 355 | { key: "agents", icon: <Sparkles size={15} />, label: "Agents", target: "rail-agents", scene: "agents" }, | |
| 357 | − | { key: "inbox", icon: <Inbox size={15} />, label: "Inbox", badge: 2 }, | |
| 356 | + | { key: "inbox", icon: <Bell size={15} />, label: "Notifications", badge: 2 }, | |
| 358 | 357 | ]; | |
| 359 | 358 | ||
| 360 | 359 | /** One rail item: the square behind its icon, filled for the mode you are in, and its name under it. */ | |
| ⋯ | |||
| 1287 | 1286 | ||
| 1288 | 1287 | /** The phone's tab bar, as the app has it (components/mobile.tsx). */ | |
| 1289 | 1288 | const TABS: { scene: Scene | null; icon: ReactNode; label: string }[] = [ | |
| 1290 | − | { scene: null, icon: <House size={16} />, label: "Home" }, | |
| 1289 | + | { scene: null, icon: <Sun size={16} />, label: "Today" }, | |
| 1291 | 1290 | { scene: "code", icon: <Code2 size={16} />, label: "Code" }, | |
| 1292 | 1291 | { scene: "chat", icon: <MessagesSquare size={16} />, label: "Chat" }, | |
| 1293 | 1292 | { scene: "agents", icon: <Sparkles size={16} />, label: "Agents" }, | |
| 1294 | − | { scene: null, icon: <Inbox size={16} />, label: "Inbox" }, | |
| 1293 | + | { scene: null, icon: <Bell size={16} />, label: "Notifications" }, | |
| 1295 | 1294 | ]; | |
| 1296 | 1295 | ||
| 1297 | 1296 | function Phone({ frame }: { frame: Frame }) { | |
| 1 | + | /** | |
| 2 | + | * The public frame's header (lib/chrome.ts, `public`): for a visitor who is | |
| 3 | + | * not signed in, on every page that is not about signing in. The logo, | |
| 4 | + | * search, Explore and Docs, and signing in or up; on a phone the links fold | |
| 5 | + | * into one menu. Someone still confirming their address gets their | |
| 6 | + | * account's menu in place of signing in. | |
| 7 | + | */ | |
| 8 | + | import { BookOpen, ChevronDown, Compass, CreditCard, LogIn, LogOut, Menu, Plus, Search, Settings } from "lucide-react"; | |
| 9 | + | import { useState } from "react"; | |
| 10 | + | import { Form, Link, NavLink, useParams, useSubmit } from "react-router"; | |
| 11 | + | ||
| 12 | + | import type { User } from "@g1t/contracts"; | |
| 13 | + | ||
| 14 | + | import { CommandPalette, type PaletteCommand, PaletteKey, usePaletteShortcut } from "./command-palette"; | |
| 15 | + | import { Logo } from "./logo"; | |
| 16 | + | import { Avatar, ButtonLink, notACredential } from "./ui"; | |
| 17 | + | import { | |
| 18 | + | DropdownMenu, | |
| 19 | + | DropdownMenuContent, | |
| 20 | + | DropdownMenuItem, | |
| 21 | + | DropdownMenuLabel, | |
| 22 | + | DropdownMenuSeparator, | |
| 23 | + | DropdownMenuTrigger, | |
| 24 | + | } from "./ui/dropdown-menu"; | |
| 25 | + | import { useSignUpCopy } from "../lib/registration"; | |
| 26 | + | ||
| 27 | + | function HeaderLink({ to, children }: { to: string; children: React.ReactNode }) { | |
| 28 | + | return ( | |
| 29 | + | <NavLink | |
| 30 | + | to={to} | |
| 31 | + | className={({ isActive }) => | |
| 32 | + | `rounded-md px-2.5 py-1.5 text-sm transition-colors hover:bg-raised hover:text-fg ${ | |
| 33 | + | isActive ? "text-fg" : "text-muted" | |
| 34 | + | }` | |
| 35 | + | } | |
| 36 | + | > | |
| 37 | + | {children} | |
| 38 | + | </NavLink> | |
| 39 | + | ); | |
| 40 | + | } | |
| 41 | + | ||
| 42 | + | /** What the palette offers someone without the app's sidebar. */ | |
| 43 | + | const PUBLIC_COMMANDS: PaletteCommand[] = [ | |
| 44 | + | { label: "Explore", hint: "Public projects", to: "/explore", icon: <Compass size={15} /> }, | |
| 45 | + | { label: "Search g1t", hint: "Repositories, code, issues, people", to: "/search", icon: <Search size={15} /> }, | |
| 46 | + | { label: "Pricing", to: "/pricing", icon: <CreditCard size={15} /> }, | |
| 47 | + | { label: "Documentation", to: "https://docs.g1t.sh/", icon: <BookOpen size={15} /> }, | |
| 48 | + | { label: "Sign in", to: "/login", icon: <LogIn size={15} /> }, | |
| 49 | + | { label: "Sign up", to: "/register", icon: <Plus size={15} /> }, | |
| 50 | + | ]; | |
| 51 | + | ||
| 52 | + | /** Sign up: the sign-up page says whether it takes an invite. */ | |
| 53 | + | function SignUpButton() { | |
| 54 | + | const copy = useSignUpCopy(); | |
| 55 | + | return <ButtonLink to="/register">{copy.primary}</ButtonLink>; | |
| 56 | + | } | |
| 57 | + | ||
| 58 | + | export function PublicHeader({ user }: { user: User | null | undefined }) { | |
| 59 | + | const submit = useSubmit(); | |
| 60 | + | const signUp = useSignUpCopy(); | |
| 61 | + | const [palette, setPalette] = useState(false); | |
| 62 | + | usePaletteShortcut(() => setPalette((open) => !open)); | |
| 63 | + | const params = useParams(); | |
| 64 | + | const repo = params.owner && params.repo ? `${params.owner}/${params.repo}` : null; | |
| 65 | + | return ( | |
| 66 | + | <header className="sticky top-0 z-40 border-b border-line bg-surface/85 backdrop-blur"> | |
| 67 | + | <div className="mx-auto flex h-14 max-w-6xl items-center gap-2 px-4"> | |
| 68 | + | <Link to="/" aria-label="g1t home" className="mr-2 flex"> | |
| 69 | + | <Logo /> | |
| 70 | + | </Link> | |
| 71 | + | <Form action="/search" role="search" className="relative hidden grow sm:block sm:max-w-xs"> | |
| 72 | + | <Search | |
| 73 | + | size={14} | |
| 74 | + | className="pointer-events-none absolute top-1/2 left-3 -translate-y-1/2 text-faint" | |
| 75 | + | /> | |
| 76 | + | <input | |
| 77 | + | name="q" | |
| 78 | + | {...notACredential()} | |
| 79 | + | placeholder="Search g1t" | |
| 80 | + | aria-label="Search g1t" | |
| 81 | + | className="w-full rounded-md border border-line bg-bg py-1.5 pr-12 pl-8 text-sm outline-none transition-colors placeholder:text-faint hover:border-line-strong focus:border-accent-dim" | |
| 82 | + | /> | |
| 83 | + | <PaletteKey className="pointer-events-none absolute top-1/2 right-2 -translate-y-1/2 rounded bg-raised px-1.5 font-mono text-[0.625rem] text-muted ring-1 ring-line" /> | |
| 84 | + | </Form> | |
| 85 | + | <button | |
| 86 | + | type="button" | |
| 87 | + | aria-label="Search g1t" | |
| 88 | + | onClick={() => setPalette(true)} | |
| 89 | + | className="flex size-10 items-center justify-center rounded-md text-muted transition-colors hover:bg-raised hover:text-fg sm:hidden" | |
| 90 | + | > | |
| 91 | + | <Search size={18} /> | |
| 92 | + | </button> | |
| 93 | + | <CommandPalette | |
| 94 | + | open={palette} | |
| 95 | + | onOpenChange={setPalette} | |
| 96 | + | commands={user ? PUBLIC_COMMANDS.slice(0, 4) : PUBLIC_COMMANDS.map((command) => (command.to === "/register" ? { ...command, label: signUp.primary } : command))} | |
| 97 | + | repo={repo} | |
| 98 | + | /> | |
| 99 | + | <nav aria-label="Main" className="hidden items-center gap-0.5 sm:flex"> | |
| 100 | + | <HeaderLink to="/explore">Explore</HeaderLink> | |
| 101 | + | <HeaderLink to="https://docs.g1t.sh/">Docs</HeaderLink> | |
| 102 | + | </nav> | |
| 103 | + | <div className="ml-auto flex items-center gap-2"> | |
| 104 | + | {/* On a phone the links fold into one menu, so the bar fits. */} | |
| 105 | + | <DropdownMenu> | |
| 106 | + | <DropdownMenuTrigger | |
| 107 | + | aria-label="Menu" | |
| 108 | + | className="flex size-10 items-center justify-center rounded-md text-muted outline-none transition-colors hover:bg-raised hover:text-fg focus-visible:ring-2 focus-visible:ring-accent data-[state=open]:bg-raised data-[state=open]:text-fg sm:hidden" | |
| 109 | + | > | |
| 110 | + | <Menu size={18} /> | |
| 111 | + | </DropdownMenuTrigger> | |
| 112 | + | <DropdownMenuContent align="end" className="w-52"> | |
| 113 | + | <DropdownMenuItem asChild className="min-h-11"> | |
| 114 | + | <Link to="/explore"> | |
| 115 | + | <Compass /> | |
| 116 | + | Explore | |
| 117 | + | </Link> | |
| 118 | + | </DropdownMenuItem> | |
| 119 | + | <DropdownMenuItem asChild className="min-h-11"> | |
| 120 | + | <Link to="https://docs.g1t.sh/"> | |
| 121 | + | <BookOpen /> | |
| 122 | + | Docs | |
| 123 | + | </Link> | |
| 124 | + | </DropdownMenuItem> | |
| 125 | + | {!user && ( | |
| 126 | + | <> | |
| 127 | + | <DropdownMenuSeparator /> | |
| 128 | + | <DropdownMenuItem asChild className="min-h-11"> | |
| 129 | + | <Link to="/login"> | |
| 130 | + | <LogIn /> | |
| 131 | + | Sign in | |
| 132 | + | </Link> | |
| 133 | + | </DropdownMenuItem> | |
| 134 | + | </> | |
| 135 | + | )} | |
| 136 | + | </DropdownMenuContent> | |
| 137 | + | </DropdownMenu> | |
| 138 | + | {user ? ( | |
| 139 | + | <> | |
| 140 | + | <DropdownMenu> | |
| 141 | + | <DropdownMenuTrigger | |
| 142 | + | aria-label="Account menu" | |
| 143 | + | className="flex items-center gap-1.5 rounded-md p-1 outline-none transition-colors hover:bg-raised focus-visible:ring-2 focus-visible:ring-accent data-[state=open]:bg-raised" | |
| 144 | + | > | |
| 145 | + | <Avatar name={user.username} image={user.avatar} size={24} /> | |
| 146 | + | <ChevronDown size={14} className="text-faint" /> | |
| 147 | + | </DropdownMenuTrigger> | |
| 148 | + | <DropdownMenuContent align="end" className="w-56"> | |
| 149 | + | <DropdownMenuLabel> | |
| 150 | + | Signed in as{" "} | |
| 151 | + | <span className="font-mono font-medium text-fg"> | |
| 152 | + | {user.username} | |
| 153 | + | </span> | |
| 154 | + | </DropdownMenuLabel> | |
| 155 | + | <DropdownMenuSeparator /> | |
| 156 | + | <DropdownMenuItem asChild> | |
| 157 | + | <Link to="/settings/profile"> | |
| 158 | + | <Settings /> | |
| 159 | + | Settings | |
| 160 | + | </Link> | |
| 161 | + | </DropdownMenuItem> | |
| 162 | + | <DropdownMenuItem asChild> | |
| 163 | + | <Link to="https://docs.g1t.sh/quickstart/"> | |
| 164 | + | <BookOpen /> | |
| 165 | + | Documentation | |
| 166 | + | </Link> | |
| 167 | + | </DropdownMenuItem> | |
| 168 | + | <DropdownMenuSeparator /> | |
| 169 | + | {/* Submitted from here: the menu closes on select, and a button | |
| 170 | + | that has left the page cannot submit a form. */} | |
| 171 | + | <DropdownMenuItem | |
| 172 | + | onSelect={() => submit(null, { method: "post", action: "/logout" })} | |
| 173 | + | > | |
| 174 | + | <LogOut /> | |
| 175 | + | Sign out | |
| 176 | + | </DropdownMenuItem> | |
| 177 | + | </DropdownMenuContent> | |
| 178 | + | </DropdownMenu> | |
| 179 | + | </> | |
| 180 | + | ) : ( | |
| 181 | + | <> | |
| 182 | + | <span className="hidden sm:contents"> | |
| 183 | + | <HeaderLink to="/login">Sign in</HeaderLink> | |
| 184 | + | </span> | |
| 185 | + | <SignUpButton /> | |
| 186 | + | </> | |
| 187 | + | )} | |
| 188 | + | </div> | |
| 189 | + | </div> | |
| 190 | + | </header> | |
| 191 | + | ); | |
| 192 | + | } | |
| 193 | + |
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
This change is too large to show in full.