OXYGENHUB

Oxygen colours, a Get started walkthrough, Create Identity (0.14.0) 013fefef11e3

The history graph draws lanes in the Oxygen mark's palette, heads glow and
the working copy breathes (still under reduced motion); signed and sealed
changes are marked and coloured. All of it is themeable (oxygen.lane1-8,
head, workingCopy, signed, sealed, invalid), and file letters use
oxygen.added/modified/removed/renamed/conflict/resolved, defaulting to
Git's. A Get started with Oxygen walkthrough ticks off identity, a
repository, a commit, a bookmark, a push and the history; Oxygen: Create
Identity makes the signing key without a terminal.

closes #8
Author
Andy McCall @andymccall <[email protected]>
When
· 2026-09-29 18:37 UTC
Signature
✔ signed by Andy McCall
Hash
013fefef11e31efd0f80676878bd6e7389d2a0223484ce406daa7ea0ccdf6558
Depends on
0c26846a9a81 1f7fc1e3ae4a ab81294a9ba9 b4626290e660 bed495a96408 cdcffe39dd9f d2c9a5843004 f532c4266f21 f6bda77ecb9f
Depended on by
dac6876e4d08 b55e0d00545e
In bookmarks
main
In tags
v0.16.0 v0.16.1 v0.17.0 v0.17.1 v0.18.0
Edits
30 atoms, 7 tree operations, 13 files

Browse the tree at this change

added walkthrough/bookmarks.md +3 −0
1 +
# Bookmarks
2 +
3 +
Bookmarks are Oxygen's branches. Because changes commute, merging two bookmarks is the union of their changes: no merge change, and a real conflict is recorded where it is, to resolve whenever you like.
added walkthrough/commit.md +3 −0
1 +
# Committing
2 +
3 +
The working copy is itself a change, snapshotted as you work. **Commit** turns it into a change with your message, signed when you push. Changes keep their identity across edits, moves and rewrites.
added walkthrough/history.md +3 −0
1 +
# History
2 +
3 +
The history graph shows each change in its lane: heads glow, the working copy breathes, and a ◈ marks a change sealed on push, ✔ one signed on its own.
added walkthrough/identity.md +5 −0
1 +
# Your identity
2 +
3 +
Oxygen signs what you publish. `oxy identity create` makes a key pair for you (a classical and a post-quantum half, so a signature holds up later) and records you as the author of your changes.
4 +
5 +
The secret key stays in your key directory; the hub only ever sees the public half.
added walkthrough/push.md +3 −0
1 +
# Pushing
2 +
3 +
`oxy push` sends your changes, one seal over all of them, and a signed attestation moving the bookmark. The hub checks both against the repository's trust policy.
added walkthrough/repository.md +3 −0
1 +
# A repository
2 +
3 +
An Oxygen repository is a set of **changes** that commute: independent work never needs a merge. **Clone** takes one from a hub (`oxy clone URL`); **Initialize** starts one in the open folder.
modified CHANGELOG.md +8 −0
1 1
# Changelog
2 2
3 +
## 0.14.0 (2026-09-29)
4 +
5 +
- **Oxygen colours.** The history graph draws each lane in the Oxygen mark's palette. Heads glow, and the working copy breathes (it holds still if your system asks for reduced motion). ✔ marks a change signed on its own, ◈ one sealed on push, both coloured.
6 +
  - Every colour is themeable: `oxygen.lane1`…`oxygen.lane8`, `oxygen.head`, `oxygen.workingCopy`, `oxygen.signed`, `oxygen.sealed` and `oxygen.invalid`.
7 +
  - File letters use `oxygen.added`, `oxygen.modified`, `oxygen.removed`, `oxygen.renamed`, `oxygen.conflict` and `oxygen.resolved`. They default to Git's colours, so a theme can style Oxygen apart.
8 +
- **Get started with Oxygen.** A walkthrough on the Welcome page covers creating your identity, opening or cloning a repository, committing, bookmarks, pushing and the history. Each step ticks itself off as you do it.
9 +
- **Oxygen: Create Identity…** makes your signing identity (`oxy identity create`) without a terminal.
10 +
3 11
## 0.13.0 (2026-09-29)
4 12
5 13
- **No more flashing Source Control.** With a subrepo in the workspace, the list of changes flickered because each refresh started another. `oxy status` rewrote the subrepo's own `.oxy` files, and only the top-level `.oxy` was ignored. Every `.oxy` store is now ignored, and oxy itself (from the same day) no longer rewrites an unchanged status file.
…
84 unchanged lines
modified README.md +4 −0
…
94 unchanged lines
95 95
96 96
The **Oxygen Trust** view shows the repository's trust policy and each change's signature. **Verify Signatures** checks them all.
97 97
98 +
## Colours and getting started
99 +
100 +
The history graph and the file letters use Oxygen's own themeable colours: `oxygen.lane1`…`oxygen.lane8` for the graph's lanes, plus `oxygen.head`, `oxygen.workingCopy`, `oxygen.signed`, `oxygen.sealed`, `oxygen.invalid` and `oxygen.added`/`modified`/`removed`/`renamed`/`conflict`/`resolved`. Set them under `workbench.colorCustomizations`. **Get started with Oxygen**, on the Welcome page, walks through a first repository. **Oxygen: Create Identity…** makes your signing key.
101 +
98 102
## Settings
99 103
100 104
| Setting | Default | What it does |
…
31 unchanged lines
modified package.json +274 −2
…
1 unchanged lines
2 2
  "name": "oxygen-vscode",
3 3
  "displayName": "Oxygen",
4 4
  "description": "Oxygen version control: source control view, diffs, decorations, bookmarks and history for oxy repositories",
5 -
  "version": "0.13.0",
5 +
  "version": "0.14.0",
6 6
  "publisher": "oxygen",
7 7
  "license": "MIT OR Apache-2.0",
8 8
  "icon": "assets/images/oxygen.png",
…
332 unchanged lines
341 341
        "title": "Copy Permalink",
342 342
        "category": "Oxygen",
343 343
        "icon": "$(pinned)"
344 +
      },
345 +
      {
346 +
        "command": "oxygen.createIdentity",
347 +
        "title": "Create Identity\u2026",
348 +
        "category": "Oxygen"
344 349
      }
345 350
    ],
346 351
    "views": {
…
443 unchanged lines
790 795
          "type": "boolean",
791 796
          "default": true,
792 797
          "description": "Show the bookmark, working copy and state in the status bar."
798 +
        }
799 +
      }
800 +
    },
801 +
    "colors": [
802 +
      {
803 +
        "id": "oxygen.lane1",
804 +
        "description": "History graph: lane 1 (the Oxygen mark's palette).",
805 +
        "defaults": {
806 +
          "dark": "#35C7FF",
807 +
          "light": "#0A74B8",
808 +
          "highContrast": "#35C7FF",
809 +
          "highContrastLight": "#0A74B8"
810 +
        }
811 +
      },
812 +
      {
813 +
        "id": "oxygen.lane2",
814 +
        "description": "History graph: lane 2 (the Oxygen mark's palette).",
815 +
        "defaults": {
816 +
          "dark": "#3D7BFF",
817 +
          "light": "#2553C9",
818 +
          "highContrast": "#3D7BFF",
819 +
          "highContrastLight": "#2553C9"
820 +
        }
821 +
      },
822 +
      {
823 +
        "id": "oxygen.lane3",
824 +
        "description": "History graph: lane 3 (the Oxygen mark's palette).",
825 +
        "defaults": {
826 +
          "dark": "#2BD9B3",
827 +
          "light": "#0E8A70",
828 +
          "highContrast": "#2BD9B3",
829 +
          "highContrastLight": "#0E8A70"
830 +
        }
831 +
      },
832 +
      {
833 +
        "id": "oxygen.lane4",
834 +
        "description": "History graph: lane 4 (the Oxygen mark's palette).",
835 +
        "defaults": {
836 +
          "dark": "#8E7CFF",
837 +
          "light": "#5B45D6",
838 +
          "highContrast": "#8E7CFF",
839 +
          "highContrastLight": "#5B45D6"
840 +
        }
841 +
      },
842 +
      {
843 +
        "id": "oxygen.lane5",
844 +
        "description": "History graph: lane 5 (the Oxygen mark's palette).",
845 +
        "defaults": {
846 +
          "dark": "#56A8FF",
847 +
          "light": "#1F6FC2",
848 +
          "highContrast": "#56A8FF",
849 +
          "highContrastLight": "#1F6FC2"
850 +
        }
851 +
      },
852 +
      {
853 +
        "id": "oxygen.lane6",
854 +
        "description": "History graph: lane 6 (the Oxygen mark's palette).",
855 +
        "defaults": {
856 +
          "dark": "#5FE3A1",
857 +
          "light": "#1A7850",
858 +
          "highContrast": "#5FE3A1",
859 +
          "highContrastLight": "#1A7850"
860 +
        }
861 +
      },
862 +
      {
863 +
        "id": "oxygen.lane7",
864 +
        "description": "History graph: lane 7 (the Oxygen mark's palette).",
865 +
        "defaults": {
866 +
          "dark": "#5B6CFF",
867 +
          "light": "#3440B8",
868 +
          "highContrast": "#5B6CFF",
869 +
          "highContrastLight": "#3440B8"
870 +
        }
871 +
      },
872 +
      {
873 +
        "id": "oxygen.lane8",
874 +
        "description": "History graph: lane 8 (the Oxygen mark's palette).",
875 +
        "defaults": {
876 +
          "dark": "#1FB6D6",
877 +
          "light": "#0B7C93",
878 +
          "highContrast": "#1FB6D6",
879 +
          "highContrastLight": "#0B7C93"
880 +
        }
881 +
      },
882 +
      {
883 +
        "id": "oxygen.head",
884 +
        "description": "A bookmark head in the history graph.",
885 +
        "defaults": {
886 +
          "dark": "#5FE3A1",
887 +
          "light": "#1A7850",
888 +
          "highContrast": "#5FE3A1",
889 +
          "highContrastLight": "#1A7850"
890 +
        }
891 +
      },
892 +
      {
893 +
        "id": "oxygen.workingCopy",
894 +
        "description": "The working copy in the history graph.",
895 +
        "defaults": {
896 +
          "dark": "#35C7FF",
897 +
          "light": "#0A74B8",
898 +
          "highContrast": "#35C7FF",
899 +
          "highContrastLight": "#0A74B8"
900 +
        }
901 +
      },
902 +
      {
903 +
        "id": "oxygen.signed",
904 +
        "description": "A change signed by its author.",
905 +
        "defaults": {
906 +
          "dark": "#5FE3A1",
907 +
          "light": "#1A7850",
908 +
          "highContrast": "#5FE3A1",
909 +
          "highContrastLight": "#1A7850"
910 +
        }
911 +
      },
912 +
      {
913 +
        "id": "oxygen.sealed",
914 +
        "description": "A change authenticated by a seal (one signature over a push).",
915 +
        "defaults": {
916 +
          "dark": "#8E7CFF",
917 +
          "light": "#6B3FD1",
918 +
          "highContrast": "#8E7CFF",
919 +
          "highContrastLight": "#6B3FD1"
920 +
        }
921 +
      },
922 +
      {
923 +
        "id": "oxygen.invalid",
924 +
        "description": "A signature that does not verify.",
925 +
        "defaults": {
926 +
          "dark": "#FF5E7A",
927 +
          "light": "#C62D47",
928 +
          "highContrast": "#FF5E7A",
929 +
          "highContrastLight": "#C62D47"
930 +
        }
931 +
      },
932 +
      {
933 +
        "id": "oxygen.added",
934 +
        "description": "A file the working copy adds. Defaults to Git's colour; themes can set Oxygen's apart.",
935 +
        "defaults": {
936 +
          "dark": "gitDecoration.addedResourceForeground",
937 +
          "light": "gitDecoration.addedResourceForeground",
938 +
          "highContrast": "gitDecoration.addedResourceForeground",
939 +
          "highContrastLight": "gitDecoration.addedResourceForeground"
793 940
        }
941 +
      },
942 +
      {
943 +
        "id": "oxygen.modified",
944 +
        "description": "A file the working copy changes. Defaults to Git's colour; themes can set Oxygen's apart.",
945 +
        "defaults": {
946 +
          "dark": "gitDecoration.modifiedResourceForeground",
947 +
          "light": "gitDecoration.modifiedResourceForeground",
948 +
          "highContrast": "gitDecoration.modifiedResourceForeground",
949 +
          "highContrastLight": "gitDecoration.modifiedResourceForeground"
950 +
        }
951 +
      },
952 +
      {
953 +
        "id": "oxygen.removed",
954 +
        "description": "A file the working copy removes. Defaults to Git's colour; themes can set Oxygen's apart.",
955 +
        "defaults": {
956 +
          "dark": "gitDecoration.deletedResourceForeground",
957 +
          "light": "gitDecoration.deletedResourceForeground",
958 +
          "highContrast": "gitDecoration.deletedResourceForeground",
959 +
          "highContrastLight": "gitDecoration.deletedResourceForeground"
960 +
        }
961 +
      },
962 +
      {
963 +
        "id": "oxygen.renamed",
964 +
        "description": "A file the working copy moves. Defaults to Git's colour; themes can set Oxygen's apart.",
965 +
        "defaults": {
966 +
          "dark": "gitDecoration.renamedResourceForeground",
967 +
          "light": "gitDecoration.renamedResourceForeground",
968 +
          "highContrast": "gitDecoration.renamedResourceForeground",
969 +
          "highContrastLight": "gitDecoration.renamedResourceForeground"
970 +
        }
971 +
      },
972 +
      {
973 +
        "id": "oxygen.conflict",
974 +
        "description": "A file with a conflict, or conflict markers left in it. Defaults to Git's colour; themes can set Oxygen's apart.",
975 +
        "defaults": {
976 +
          "dark": "gitDecoration.conflictingResourceForeground",
977 +
          "light": "gitDecoration.conflictingResourceForeground",
978 +
          "highContrast": "gitDecoration.conflictingResourceForeground",
979 +
          "highContrastLight": "gitDecoration.conflictingResourceForeground"
980 +
        }
981 +
      },
982 +
      {
983 +
        "id": "oxygen.resolved",
984 +
        "description": "A conflict marked resolved. Defaults to Git's colour; themes can set Oxygen's apart.",
985 +
        "defaults": {
986 +
          "dark": "gitDecoration.stageModifiedResourceForeground",
987 +
          "light": "gitDecoration.stageModifiedResourceForeground",
988 +
          "highContrast": "gitDecoration.stageModifiedResourceForeground",
989 +
          "highContrastLight": "gitDecoration.stageModifiedResourceForeground"
990 +
        }
991 +
      }
992 +
    ],
993 +
    "walkthroughs": [
994 +
      {
995 +
        "id": "oxygen.gettingStarted",
996 +
        "title": "Get started with Oxygen",
997 +
        "description": "Sign, commit, branch without merges, push and see the history of an Oxygen repository.",
998 +
        "steps": [
999 +
          {
1000 +
            "id": "oxygen.identity",
1001 +
            "title": "Create your identity",
1002 +
            "description": "Every change you make is signed with a composite key (Ed25519 and ML-DSA-65) that names you. Make it once; every repository uses it.\n[Create Identity](command:oxygen.createIdentity)",
1003 +
            "media": {
1004 +
              "markdown": "walkthrough/identity.md"
1005 +
            },
1006 +
            "completionEvents": [
1007 +
              "onCommand:oxygen.createIdentity"
1008 +
            ]
1009 +
          },
1010 +
          {
1011 +
            "id": "oxygen.repository",
1012 +
            "title": "Open or clone a repository",
1013 +
            "description": "Clone one from your hub, start a new one here, or open a folder that already is one.\n[Clone Repository](command:oxygen.clone)\n[Initialize Repository](command:oxygen.init)",
1014 +
            "media": {
1015 +
              "markdown": "walkthrough/repository.md"
1016 +
            },
1017 +
            "completionEvents": [
1018 +
              "onContext:oxygen.hasRepository"
1019 +
            ]
1020 +
          },
1021 +
          {
1022 +
            "id": "oxygen.commit",
1023 +
            "title": "Commit a change",
1024 +
            "description": "Edit a file, write a message in the Source Control box and press Ctrl+Enter. There is no staging area: the change is what you see.\n[Open Source Control](command:workbench.view.scm)",
1025 +
            "media": {
1026 +
              "markdown": "walkthrough/commit.md"
1027 +
            },
1028 +
            "completionEvents": [
1029 +
              "onCommand:oxygen.commit"
1030 +
            ]
1031 +
          },
1032 +
          {
1033 +
            "id": "oxygen.bookmarks",
1034 +
            "title": "Bookmarks: branches without merges",
1035 +
            "description": "A bookmark names a state. Switch to a new one for a piece of work (take your edits along with *carry*), and merge it back: a merge is just the union of both, with no merge change.\n[Create Bookmark](command:oxygen.createBookmark)",
1036 +
            "media": {
1037 +
              "markdown": "walkthrough/bookmarks.md"
1038 +
            },
1039 +
            "completionEvents": [
1040 +
              "onCommand:oxygen.createBookmark"
1041 +
            ]
1042 +
          },
1043 +
          {
1044 +
            "id": "oxygen.push",
1045 +
            "title": "Push to your hub",
1046 +
            "description": "Publish your bookmark. The push seals your new changes with one signature and moves the bookmark on the hub, which runs its OxygenFlow pipeline.\n[Push](command:oxygen.push)",
1047 +
            "media": {
1048 +
              "markdown": "walkthrough/push.md"
1049 +
            },
1050 +
            "completionEvents": [
1051 +
              "onCommand:oxygen.push"
1052 +
            ]
1053 +
          },
1054 +
          {
1055 +
            "id": "oxygen.history",
1056 +
            "title": "See the history",
1057 +
            "description": "Changes form a graph, not a line: independent work runs in its own lane, in the Oxygen colours.\n[Show History](command:oxygen.showHistory)",
1058 +
            "media": {
1059 +
              "markdown": "walkthrough/history.md"
1060 +
            },
1061 +
            "completionEvents": [
1062 +
              "onCommand:oxygen.showHistory"
1063 +
            ]
1064 +
          }
1065 +
        ]
794 1066
      }
795 -
    }
1067 +
    ]
796 1068
  },
797 1069
  "scripts": {
798 1070
    "build": "esbuild src/extension.ts --bundle --platform=node --format=cjs --external:vscode --outfile=dist/extension.js --sourcemap",
…
15 unchanged lines
modified src/decorations.ts +10 −10
…
28 unchanged lines
29 29
      const entry = repo.status.paths.find((p) => p.path === rel);
30 30
      const conflict = repo.status.conflicts.find((c) => c.path === rel);
31 31
      if (conflict && !entry) {
32 -
        return new vscode.FileDecoration("‼", "conflict", new vscode.ThemeColor("gitDecoration.conflictingResourceForeground"));
32 +
        return new vscode.FileDecoration("‼", "conflict", new vscode.ThemeColor("oxygen.conflict"));
33 33
      }
34 34
      if (!entry) {
35 35
        // A directory containing changes gets a dot, like Git.
36 36
        if (repo.status.paths.some((p) => p.path.startsWith(rel + "/"))) {
37 -
          return new vscode.FileDecoration("•", "contains changes", new vscode.ThemeColor("gitDecoration.modifiedResourceForeground"));
37 +
          return new vscode.FileDecoration("•", "contains changes", new vscode.ThemeColor("oxygen.modified"));
38 38
        }
39 39
        continue;
40 40
      }
41 41
      const letters: Record<string, [string, string, string]> = {
42 -
        Added: ["A", "added", "gitDecoration.addedResourceForeground"],
43 -
        Modified: ["M", "modified", "gitDecoration.modifiedResourceForeground"],
44 -
        Removed: ["D", "removed", "gitDecoration.deletedResourceForeground"],
45 -
        Renamed: ["R", "renamed", "gitDecoration.renamedResourceForeground"],
46 -
        ModeChanged: ["X", "mode changed", "gitDecoration.modifiedResourceForeground"],
47 -
        ConflictEdited: ["‼", "conflict markers remain", "gitDecoration.conflictingResourceForeground"],
48 -
        Resolved: ["✔", "conflict resolved", "gitDecoration.stageModifiedResourceForeground"],
42 +
        Added: ["A", "added", "oxygen.added"],
43 +
        Modified: ["M", "modified", "oxygen.modified"],
44 +
        Removed: ["D", "removed", "oxygen.removed"],
45 +
        Renamed: ["R", "renamed", "oxygen.renamed"],
46 +
        ModeChanged: ["X", "mode changed", "oxygen.modified"],
47 +
        ConflictEdited: ["‼", "conflict markers remain", "oxygen.conflict"],
48 +
        Resolved: ["✔", "conflict resolved", "oxygen.resolved"],
49 49
      };
50 -
      const [badge, tooltip, color] = letters[entry.status] ?? ["?", entry.status, "gitDecoration.modifiedResourceForeground"];
50 +
      const [badge, tooltip, color] = letters[entry.status] ?? ["?", entry.status, "oxygen.modified"];
51 51
      const d = new vscode.FileDecoration(badge, tooltip, new vscode.ThemeColor(color));
52 52
      d.propagate = false;
53 53
      return d;
…
10 unchanged lines
modified src/extension.ts +24 −0
…
138 unchanged lines
139 139
    vscode.commands.registerCommand("oxygen.createTag", (arg?: Repository | vscode.SourceControl) => createTag(arg)),
140 140
    vscode.commands.registerCommand("oxygen.addWorkspace", (arg?: Repository | vscode.SourceControl) => addWorkspace(arg)),
141 141
    vscode.commands.registerCommand("oxygen.clone", () => cloneRepository()),
142 +
    vscode.commands.registerCommand("oxygen.createIdentity", () => createIdentity()),
142 143
    vscode.window.registerUriHandler({ handleUri: (uri) => handleUri(uri) }),
143 144
    vscode.workspace.onDidChangeWorkspaceFolders(() => void discover()),
144 145
    vscode.workspace.onDidChangeConfiguration((e) => {
…
715 unchanged lines
860 861
    }
861 862
  } catch (e) {
862 863
    showError(e);
864 +
  }
865 +
}
866 +
867 +
/** Make a signing identity (oxy identity create): a composite key in your
868 +
 * user key directory, and the author every repository then signs with. */
869 +
async function createIdentity(): Promise<void> {
870 +
  const name = await vscode.window.showInputBox({ prompt: "Your name, as changes will show it", ignoreFocusOut: true, validateInput: (v) => (v.trim() ? undefined : "A name") });
871 +
  if (!name) {
872 +
    return;
873 +
  }
874 +
  const email = await vscode.window.showInputBox({ prompt: "Your email", ignoreFocusOut: true, validateInput: (v) => (/.+@.+/.test(v.trim()) ? undefined : "An email address") });
875 +
  if (!email) {
876 +
    return;
877 +
  }
878 +
  const home = process.env.HOME ?? process.cwd();
879 +
  const r = await oxy.run(home, ["--plain", "identity", "create", "--name", name.trim(), "--email", email.trim()]);
880 +
  if (r.code === 0) {
881 +
    vscode.window.showInformationMessage(`Oxygen: your identity is ready: ${name.trim()} <${email.trim()}>. Changes you commit and push are signed with it.`);
882 +
    for (const repo of repositories.values()) {
883 +
      void repo.refresh();
884 +
    }
885 +
  } else {
886 +
    vscode.window.showErrorMessage(`Oxygen: could not create an identity: ${r.stderr.trim() || r.stdout.toString("utf8").trim()}`);
863 887
  }
864 888
}
865 889
…
738 unchanged lines
modified src/history.ts +20 −9
…
117 unchanged lines
118 118
      }
119 119
      const x2 = lanes.get(d)! * laneW + 10;
120 120
      const y2 = j * rowH + rowH / 2;
121 -
      edges.push(`<path d="M${x},${y} C${x},${y + rowH / 2} ${x2},${y2 - rowH / 2} ${x2},${y2}" />`);
121 +
      edges.push(`<path class="l${(lanes.get(d)! % 8) + 1}" d="M${x},${y} C${x},${y + rowH / 2} ${x2},${y2 - rowH / 2} ${x2},${y2}" />`);
122 122
    }
123 123
    const cls = c.working_copy ? "wc" : c.head ? "head" : "node";
124 -
    nodes.push(`<circle class="${cls}" cx="${x}" cy="${y}" r="5" />`);
124 +
    const lane = `l${(lanes.get(c.hash)! % 8) + 1}`;
125 +
    nodes.push(`<circle class="${cls} ${lane}" cx="${x}" cy="${y}" r="${c.head ? 6 : 5}" />`);
125 126
  });
126 127
  const rows = changes
127 128
    .map((c, i) => {
128 129
      const subject = c.message.split("\n")[0] || (c.working_copy ? "(working copy)" : "(no description)");
129 -
      const sig = c.working_copy ? "" : c.signature?.status === "signed" || c.signature?.status === "sealed" ? (c.signature.revoked ? "⚠" : "✔") : c.signature?.status === "invalid" ? "✖" : "·";
130 +
      const sig = c.working_copy ? "" : c.signature?.status === "signed" || c.signature?.status === "sealed" ? (c.signature.revoked ? "⚠" : c.signature.status === "sealed" ? "◈" : "✔") : c.signature?.status === "invalid" ? "✖" : "·";
131 +
      const sigClass = c.signature?.revoked ? "sig-revoked" : `sig-${c.signature?.status ?? "none"}`;
130 132
      // Right-click: split, squash and rebase (webview/context menus in package.json).
131 133
      const ctx = c.working_copy ? "" : ` data-vscode-context='${escape(JSON.stringify({ webviewSection: "change", root: repo.root, hash: c.hash, oxygenHead: c.head, oxygenHasDeps: c.deps.length > 0, preventDefaultContextMenuItems: true }))}'`;
132 134
      return `<div class="row${c.working_copy ? " wc" : ""}" style="top:${i * rowH}px" data-hash="${c.hash}" title="${escape(c.message)}"${ctx}>
133 135
        <span class="id">${c.change_id.slice(0, 8)}</span>
134 136
        <span class="subject">${escape(subject)}</span>
135 -
        <span class="meta">${escape(c.author)} · ${timeAgo(c.when_ms)} <span class="sig" title="${escape(c.signature?.status ?? "")}">${sig}</span></span>
137 +
        <span class="meta">${escape(c.author)} · ${timeAgo(c.when_ms)} <span class="sig ${sigClass}" title="${escape(c.signature?.status ?? "")}">${sig}</span></span>
136 138
      </div>`;
137 139
    })
138 140
    .join("");
…
5 unchanged lines
144 146
  h2 button { background: var(--vscode-button-secondaryBackground); color: var(--vscode-button-secondaryForeground); border: none; padding: 2px 8px; cursor: pointer; }
145 147
  .wrap { position: relative; }
146 148
  svg { position: absolute; left: 0; top: 0; }
147 -
  svg path { fill: none; stroke: var(--vscode-editorCodeLens-foreground); stroke-width: 1.5; }
148 -
  svg circle.node { fill: var(--vscode-editorCodeLens-foreground); }
149 -
  svg circle.head { fill: var(--vscode-charts-green); }
150 -
  svg circle.wc { fill: var(--vscode-charts-blue); stroke: var(--vscode-charts-blue); stroke-width: 2; fill-opacity: .3; }
149 +
  /* Lanes in the Oxygen mark's colours (themeable: oxygen.lane1…8). */
150 +
  ${[1, 2, 3, 4, 5, 6, 7, 8].map((n) => `.l${n} { --lane: var(--vscode-oxygen-lane${n}); }`).join("\n  ")}
151 +
  svg path { fill: none; stroke: var(--lane); stroke-width: 1.6; opacity: .85; }
152 +
  svg circle.node { fill: var(--lane); }
153 +
  svg circle.head { fill: var(--vscode-oxygen-head); stroke: var(--vscode-editor-background); stroke-width: 1.5; filter: drop-shadow(0 0 3px var(--vscode-oxygen-head)); }
154 +
  svg circle.wc { fill: var(--vscode-oxygen-workingCopy); fill-opacity: .25; stroke: var(--vscode-oxygen-workingCopy); stroke-width: 2; transform-box: fill-box; transform-origin: center; animation: breathe 2.4s ease-in-out infinite; }
155 +
  @keyframes breathe { 0%, 100% { transform: scale(1); fill-opacity: .25; } 50% { transform: scale(1.25); fill-opacity: .55; } }
156 +
  @media (prefers-reduced-motion: reduce) { svg circle.wc { animation: none; } }
157 +
  h2::after { content: ""; flex: 1; height: 2px; margin-left: 6px; background: linear-gradient(90deg, var(--vscode-oxygen-lane1), var(--vscode-oxygen-lane2), transparent); border-radius: 1px; }
158 +
  .sig-signed { color: var(--vscode-oxygen-signed); }
159 +
  .sig-sealed { color: var(--vscode-oxygen-sealed); }
160 +
  .sig-invalid, .sig-revoked { color: var(--vscode-oxygen-invalid); }
161 +
  .row:not(.wc):hover .id { text-decoration: underline; }
151 162
  .rows { position: relative; margin-left: ${graphW}px; height: ${changes.length * rowH}px; }
152 163
  .row { position: absolute; left: 0; right: 0; height: ${rowH}px; line-height: ${rowH}px; display: flex; gap: 10px; cursor: pointer; white-space: nowrap; overflow: hidden; }
153 164
  .row:hover { background: var(--vscode-list-hoverBackground); }
154 165
  .row.wc { color: var(--vscode-descriptionForeground); font-style: italic; cursor: default; }
155 -
  .id { color: var(--vscode-charts-blue); font-family: var(--vscode-editor-font-family); }
166 +
  .id { color: var(--vscode-oxygen-lane1); font-family: var(--vscode-editor-font-family); }
156 167
  .subject { flex: 1; overflow: hidden; text-overflow: ellipsis; }
157 168
  .meta { color: var(--vscode-descriptionForeground); font-size: 12px; }
158 169
  .sig { margin-left: 4px; }
…
22 unchanged lines
modified src/repository.ts +7 −7
…
28 unchanged lines
29 29
};
30 30
31 31
const STATUS_COLOR: Record<PathStatus, string> = {
32 -
  Added: "gitDecoration.addedResourceForeground",
33 -
  Modified: "gitDecoration.modifiedResourceForeground",
34 -
  Removed: "gitDecoration.deletedResourceForeground",
35 -
  Renamed: "gitDecoration.renamedResourceForeground",
36 -
  ModeChanged: "gitDecoration.modifiedResourceForeground",
37 -
  ConflictEdited: "gitDecoration.conflictingResourceForeground",
38 -
  Resolved: "gitDecoration.stageModifiedResourceForeground",
32 +
  Added: "oxygen.added",
33 +
  Modified: "oxygen.modified",
34 +
  Removed: "oxygen.removed",
35 +
  Renamed: "oxygen.renamed",
36 +
  ModeChanged: "oxygen.modified",
37 +
  ConflictEdited: "oxygen.conflict",
38 +
  Resolved: "oxygen.resolved",
39 39
};
40 40
41 41
export class OxyResourceState implements vscode.SourceControlResourceState {
…
253 unchanged lines