diff --git a/openspec/changes/agent-codex-add-openspec-and-provider-icons-2026-04-23-14-02/proposal.md b/openspec/changes/agent-codex-add-openspec-and-provider-icons-2026-04-23-14-02/proposal.md
new file mode 100644
index 0000000..5fc5a10
--- /dev/null
+++ b/openspec/changes/agent-codex-add-openspec-and-provider-icons-2026-04-23-14-02/proposal.md
@@ -0,0 +1,16 @@
+## Why
+
+- The repo now ships a VS Code Source Control companion for Active Agents, but the Explorer still lacks repo-specific visual cues for OpenSpec folders, agent surfaces, and hook files.
+- Active Agents rows also do not distinguish Codex/OpenAI versus Claude sessions, snapshot identity, or branch-group semantics in the place operators watch most closely: the working row beside the loader.
+
+## What Changes
+
+- Add a bundled `GitGuardex File Icons` theme to the shipped VS Code companion with distinct icons for OpenSpec `changes`, `plan`, `specs`, agent surfaces, hook paths, and related config/context files.
+- Surface provider-aware Active Agents row copy and badges so Codex/OpenAI sessions read as `OpenAI`, Claude sessions read as `Claude`, snapshot-backed rows show the snapshot name and initial badge, and raw agent branch groups use a branch icon plus `working: agent` state copy.
+- Keep the live extension, template extension, packaging metadata, docs, and focused tests aligned.
+
+## Impact
+
+- Affected surfaces: `vscode/guardex-active-agents/*`, `templates/vscode/guardex-active-agents/*`, `src/context.js`, `test/vscode-active-agents-session-state.test.js`, `test/metadata.test.js`, and `test/setup.test.js`.
+- Risk stays narrow: this is presentation-only work inside the VS Code companion bundle and its packaging metadata.
+- Operator caveat: Explorer icons require selecting the bundled file icon theme inside VS Code; the Active Agents row/provider badges work immediately once the updated extension is installed.
diff --git a/openspec/changes/agent-codex-add-openspec-and-provider-icons-2026-04-23-14-02/specs/vscode-active-agents-provider-icons/spec.md b/openspec/changes/agent-codex-add-openspec-and-provider-icons-2026-04-23-14-02/specs/vscode-active-agents-provider-icons/spec.md
new file mode 100644
index 0000000..0f5ffbe
--- /dev/null
+++ b/openspec/changes/agent-codex-add-openspec-and-provider-icons-2026-04-23-14-02/specs/vscode-active-agents-provider-icons/spec.md
@@ -0,0 +1,55 @@
+## ADDED Requirements
+
+### Requirement: Active Agents rows show provider-aware working state
+
+Active Agents session rows SHALL surface provider identity for Codex/OpenAI and Claude sessions without sacrificing higher-priority warning badges.
+
+#### Scenario: Codex session shows OpenAI branding in the row
+
+- **GIVEN** an active session record whose CLI or agent identity resolves to Codex/OpenAI
+- **WHEN** the Active Agents tree renders the session row
+- **THEN** the row description includes `OpenAI`
+- **AND** the session decoration exposes an `AI` badge whenever no blocked/dead/stalled/idle-threshold badge overrides it
+
+#### Scenario: Snapshot session shows the snapshot name and badge
+
+- **GIVEN** an active session or managed worktree telemetry record carries snapshot identity such as `nagyviktor@edixa.com`
+- **WHEN** the Active Agents tree renders the session row
+- **THEN** the row description includes the snapshot name
+- **AND** the session decoration exposes the first alphanumeric snapshot initial, such as `N`, ahead of provider-only badges
+
+#### Scenario: Claude session shows Claude branding in the row
+
+- **GIVEN** an active session record whose CLI or agent identity resolves to Claude
+- **WHEN** the Active Agents tree renders the session row
+- **THEN** the row description includes `Claude`
+- **AND** the session decoration exposes a `CL` badge whenever no blocked/dead/stalled/idle-threshold badge overrides it
+
+#### Scenario: Raw agent branch groups use branch presentation
+
+- **GIVEN** the Active Agents raw tree groups sessions by worktree branch
+- **WHEN** a worktree group is rendered
+- **THEN** the row uses the VS Code `git-branch` icon instead of the generic folder icon
+- **AND** the row description includes the current state plus agent name, such as `working: codex`
+
+### Requirement: Bundled Explorer file icon theme highlights repo workflow surfaces
+
+The shipped VS Code companion SHALL bundle an optional file icon theme that gives workflow-critical repo paths distinct Explorer icons.
+
+#### Scenario: OpenSpec and workflow folders receive semantic icons
+
+- **GIVEN** the bundled `GitGuardex File Icons` theme is selected in VS Code
+- **WHEN** the Explorer renders folders named `changes`, `plan`, `specs`, `.agents`, `agent-worktrees`, `.githooks`, or `rules`
+- **THEN** each folder uses a bundled semantic icon instead of the generic default
+
+#### Scenario: Key workflow files receive semantic icons
+
+- **GIVEN** the bundled `GitGuardex File Icons` theme is selected in VS Code
+- **WHEN** the Explorer renders workflow files such as `AGENTS.md`, `CLAUDE.md`, `proposal.md`, `tasks.md`, `plan.md`, `spec.md`, `config.yaml`, `.openspec.yaml`, `context-docs-cue.md`, `pre-commit`, `pre-push`, or `post-checkout`
+- **THEN** each file uses the corresponding bundled semantic icon
+
+#### Scenario: Install bundle ships the icon theme assets
+
+- **GIVEN** maintainers install the workspace extension bundle through `scripts/install-vscode-active-agents-extension.js`
+- **WHEN** the extension payload is copied into the VS Code extensions directory
+- **THEN** the installed bundle contains the icon-theme manifest plus the SVG assets referenced by it
diff --git a/openspec/changes/agent-codex-add-openspec-and-provider-icons-2026-04-23-14-02/tasks.md b/openspec/changes/agent-codex-add-openspec-and-provider-icons-2026-04-23-14-02/tasks.md
new file mode 100644
index 0000000..6ae5b9a
--- /dev/null
+++ b/openspec/changes/agent-codex-add-openspec-and-provider-icons-2026-04-23-14-02/tasks.md
@@ -0,0 +1,34 @@
+## Definition of Done
+
+This change is complete only when all of the following are true:
+
+- Every checkbox below is checked.
+- Focused extension/package tests pass.
+- Cleanup records the final PR URL plus `MERGED` evidence, or a `BLOCKED:` line explains why finish could not complete.
+
+Handoff: 2026-04-23 codex owns branch `agent/codex/add-openspec-and-provider-icons-2026-04-23-14-02`, the Active Agents extension bundle, mirrored template files, focused tests, and this OpenSpec change to ship Explorer file icons plus provider-aware Active Agents rows.
+
+## 1. Specification
+
+- [x] 1.1 Finalize proposal scope for Explorer file icons plus provider-aware Active Agents rows.
+- [x] 1.2 Define normative requirements in `specs/vscode-active-agents-provider-icons/spec.md`.
+
+## 2. Implementation
+
+- [x] 2.1 Add bundled file icon theme assets and manifest wiring for OpenSpec, agent, and hook surfaces.
+- [x] 2.2 Add provider/snapshot-aware Active Agents row labels/badges and branch-icon worktree groups without overriding higher-priority warning/idle decorations.
+- [x] 2.3 Keep live/template extension sources, docs, and packaging metadata aligned.
+
+## 3. Verification
+
+- [x] 3.1 Run focused extension/install/package coverage. Result: `node --test test/vscode-active-agents-session-state.test.js test/metadata.test.js test/setup.test.js` passed `102/102`.
+- [x] 3.2 Run `openspec validate agent-codex-add-openspec-and-provider-icons-2026-04-23-14-02 --type change --strict`. Result: passed.
+- [x] 3.3 Run `openspec validate --specs`. Result: `No items found to validate.`
+
+## 4. Cleanup (mandatory; run before claiming completion)
+
+- [ ] 4.1 Run `gx branch finish --branch "agent/codex/add-openspec-and-provider-icons-2026-04-23-14-02" --base main --via-pr --wait-for-merge --cleanup`.
+- [ ] 4.2 Record the PR URL and final `MERGED` state in the completion handoff.
+- [ ] 4.3 Confirm the sandbox worktree and branch refs are gone after cleanup.
+
+BLOCKED:
diff --git a/src/context.js b/src/context.js
index 2c931fe..6fa31a4 100644
--- a/src/context.js
+++ b/src/context.js
@@ -129,6 +129,14 @@ const TEMPLATE_FILES = [
'vscode/guardex-active-agents/session-schema.js',
'vscode/guardex-active-agents/README.md',
'vscode/guardex-active-agents/icon.png',
+ 'vscode/guardex-active-agents/fileicons/gitguardex-fileicons.json',
+ 'vscode/guardex-active-agents/fileicons/icons/agent.svg',
+ 'vscode/guardex-active-agents/fileicons/icons/branch.svg',
+ 'vscode/guardex-active-agents/fileicons/icons/config.svg',
+ 'vscode/guardex-active-agents/fileicons/icons/hook.svg',
+ 'vscode/guardex-active-agents/fileicons/icons/openspec.svg',
+ 'vscode/guardex-active-agents/fileicons/icons/plan.svg',
+ 'vscode/guardex-active-agents/fileicons/icons/spec.svg',
];
const PACKAGE_ROOT_SOURCE_OVERRIDES = new Set([
@@ -139,6 +147,14 @@ const PACKAGE_ROOT_SOURCE_OVERRIDES = new Set([
'vscode/guardex-active-agents/session-schema.js',
'vscode/guardex-active-agents/README.md',
'vscode/guardex-active-agents/icon.png',
+ 'vscode/guardex-active-agents/fileicons/gitguardex-fileicons.json',
+ 'vscode/guardex-active-agents/fileicons/icons/agent.svg',
+ 'vscode/guardex-active-agents/fileicons/icons/branch.svg',
+ 'vscode/guardex-active-agents/fileicons/icons/config.svg',
+ 'vscode/guardex-active-agents/fileicons/icons/hook.svg',
+ 'vscode/guardex-active-agents/fileicons/icons/openspec.svg',
+ 'vscode/guardex-active-agents/fileicons/icons/plan.svg',
+ 'vscode/guardex-active-agents/fileicons/icons/spec.svg',
]);
const LEGACY_WORKFLOW_SHIM_SPECS = [
diff --git a/templates/vscode/guardex-active-agents/README.md b/templates/vscode/guardex-active-agents/README.md
index 6878a1c..9b0c580 100644
--- a/templates/vscode/guardex-active-agents/README.md
+++ b/templates/vscode/guardex-active-agents/README.md
@@ -18,11 +18,14 @@ node scripts/install-vscode-active-agents-extension.js
What it does:
- Bundles a local GitGuardex icon so repo installs show branded extension metadata inside VS Code.
+- Bundles the optional `GitGuardex File Icons` theme for OpenSpec, agent worktree, and hook files in Explorer.
- Adds an `Active Agents` view to the Source Control container.
- Renders one repo node per live Guardex workspace with grouped `ACTIVE AGENTS` and `CHANGES` sections.
- Splits live sessions inside `ACTIVE AGENTS` into `BLOCKED`, `WORKING NOW`, `THINKING`, `STALLED`, and `DEAD` groups so stuck, active, and inactive lanes stand out immediately.
- Mirrors the same live state in the VS Code status bar so the selected session or active-agent count stays visible outside the tree.
- Shows one row per live Guardex sandbox session inside those activity groups, with changed-file rows nested under sessions that are touching files.
+- Labels session rows with provider identity and snapshot context; snapshot-backed rows use a one-letter snapshot badge such as `N` for `nagyviktor@edixa.com`.
+- Shows raw agent branch groups with the `git-branch` icon instead of the generic folder icon.
- Shows repo-root git changes in a sibling `CHANGES` section when the guarded repo itself is dirty.
- Derives session state from dirty worktree status, git conflict markers, heartbeat freshness, PID liveness, and recent file mtimes, surfaces working/dead/conflict counts in the repo/header summary, and shows changed-file counts for active edits.
- Uses distinct VS Code codicons for each session state, including animated `loading~spin` for `WORKING NOW`.
diff --git a/templates/vscode/guardex-active-agents/extension.js b/templates/vscode/guardex-active-agents/extension.js
index 255344c..0f9a096 100644
--- a/templates/vscode/guardex-active-agents/extension.js
+++ b/templates/vscode/guardex-active-agents/extension.js
@@ -56,6 +56,18 @@ const SESSION_ACTIVITY_ICON_IDS = {
stalled: 'clock',
dead: 'error',
};
+const SESSION_PROVIDER_BRANDS = {
+ openai: {
+ id: 'openai',
+ label: 'OpenAI',
+ badge: 'AI',
+ },
+ claude: {
+ id: 'claude',
+ label: 'Claude',
+ badge: 'CL',
+ },
+};
function sessionDecorationUri(branch) {
return vscode.Uri.parse(`${SESSION_DECORATION_SCHEME}://${sanitizeBranchForFile(branch)}`);
@@ -134,6 +146,84 @@ function uniqueStringList(values) {
return result;
}
+function normalizeSessionProviderToken(value) {
+ return typeof value === 'string' ? value.trim().toLowerCase() : '';
+}
+
+function resolveSessionProvider(session) {
+ const signals = [
+ session?.cliName,
+ session?.agentName,
+ session?.branch,
+ ]
+ .map(normalizeSessionProviderToken)
+ .filter(Boolean);
+
+ if (signals.some((value) => value.includes('claude'))) {
+ return {
+ ...SESSION_PROVIDER_BRANDS.claude,
+ cliName: typeof session?.cliName === 'string' ? session.cliName.trim() : '',
+ };
+ }
+ if (signals.some((value) => value.includes('codex') || value.includes('openai'))) {
+ return {
+ ...SESSION_PROVIDER_BRANDS.openai,
+ cliName: typeof session?.cliName === 'string' ? session.cliName.trim() : '',
+ };
+ }
+ return null;
+}
+
+function sessionProviderDecoration(session) {
+ const provider = resolveSessionProvider(session);
+ if (!provider) {
+ return undefined;
+ }
+
+ const cliName = provider.cliName || provider.id;
+ return {
+ badge: provider.badge,
+ tooltip: `${provider.label} session via ${cliName}`,
+ };
+}
+
+function normalizeSnapshotIdentityValue(value) {
+ return typeof value === 'string' ? value.trim() : '';
+}
+
+function sessionSnapshotDisplayName(session) {
+ return normalizeSnapshotIdentityValue(session?.snapshotName)
+ || normalizeSnapshotIdentityValue(session?.snapshotEmail);
+}
+
+function sessionSnapshotBadge(session) {
+ const displayName = sessionSnapshotDisplayName(session);
+ const match = displayName.match(/[a-z0-9]/i);
+ return match ? match[0].toUpperCase() : '';
+}
+
+function sessionSnapshotDescription(session) {
+ const displayName = sessionSnapshotDisplayName(session);
+ return displayName ? `snapshot ${displayName}` : '';
+}
+
+function sessionSnapshotDecoration(session) {
+ const badge = sessionSnapshotBadge(session);
+ const displayName = sessionSnapshotDisplayName(session);
+ if (!badge || !displayName) {
+ return undefined;
+ }
+
+ return {
+ badge,
+ tooltip: `Snapshot ${displayName}`,
+ };
+}
+
+function sessionIdentityDecoration(session) {
+ return sessionSnapshotDecoration(session) || sessionProviderDecoration(session);
+}
+
function stringListsEqual(left, right) {
if (!Array.isArray(left) || !Array.isArray(right) || left.length !== right.length) {
return false;
@@ -408,9 +498,12 @@ function changeRiskBadges(change) {
}
function buildSessionCardDescription(session) {
+ const provider = resolveSessionProvider(session);
+ const statusAgentLabel = `${sessionStatusLabel(session)}: ${session.agentName || 'agent'}`;
const descriptionParts = [
- session.agentName || 'agent',
- sessionStatusLabel(session),
+ statusAgentLabel,
+ provider?.label ? `via ${provider.label}` : '',
+ sessionSnapshotDescription(session),
session.deltaLabel || '',
session.changeCount > 0 ? formatCountLabel(session.changeCount, 'changed file') : '',
session.lockCount > 0 ? formatCountLabel(session.lockCount, 'lock') : '',
@@ -421,7 +514,16 @@ function buildSessionCardDescription(session) {
}
function buildRawSessionDescription(session) {
- const descriptionParts = [session.activityLabel || 'thinking'];
+ const provider = resolveSessionProvider(session);
+ const status = sessionStatusLabel(session).toLowerCase();
+ const descriptionParts = [`${status}: ${session.agentName || 'agent'}`];
+ if (provider?.label) {
+ descriptionParts.push(provider.label);
+ }
+ const snapshot = sessionSnapshotDescription(session);
+ if (snapshot) {
+ descriptionParts.push(snapshot);
+ }
if (session.activityCountLabel) {
descriptionParts.push(session.activityCountLabel);
}
@@ -433,6 +535,7 @@ function buildRawSessionDescription(session) {
}
function buildSessionTooltip(session, description) {
+ const provider = resolveSessionProvider(session);
const riskSummary = uniqueStringList([
...(session?.riskBadges || []),
session?.deltaLabel || '',
@@ -440,6 +543,10 @@ function buildSessionTooltip(session, description) {
const topFiles = session?.topChangedFilesLabel || summarizeCompactPaths(session?.worktreeChangedPaths || []);
return [
session.branch,
+ provider?.label
+ ? `Provider ${provider.label}${provider.cliName ? ` (${provider.cliName})` : ''}`
+ : '',
+ sessionSnapshotDisplayName(session) ? `Snapshot ${sessionSnapshotDisplayName(session)}` : '',
`${session.agentName} · ${session.taskName}`,
`Status ${description}`,
session.recentChangeSummary ? `Recent ${session.recentChangeSummary}` : '',
@@ -461,6 +568,23 @@ function buildUnassignedChangeDescription(change) {
].filter(Boolean).join(' · ');
}
+function buildWorktreeBranchDescription(sessions) {
+ const sessionList = Array.isArray(sessions) ? sessions : [];
+ const primarySession = sessionList[0] || null;
+ if (!primarySession) {
+ return '';
+ }
+
+ const descriptionParts = [
+ `${sessionStatusLabel(primarySession).toLowerCase()}: ${primarySession.agentName || 'agent'}`,
+ sessionSnapshotDescription(primarySession),
+ ];
+ if (sessionList.length > 1) {
+ descriptionParts.push(formatCountLabel(sessionList.length, 'agent'));
+ }
+ return descriptionParts.filter(Boolean).join(' · ');
+}
+
function buildOverviewDescription(summary) {
return [
formatCountLabel(summary?.workingCount || 0, 'working agent'),
@@ -812,7 +936,12 @@ class SessionDecorationProvider {
};
}
- return sessionIdleDecoration(this.sessionsByUri.get(uri.toString()), this.nowProvider());
+ const session = this.sessionsByUri.get(uri.toString());
+ const idleDecoration = sessionIdleDecoration(session, this.nowProvider());
+ if (idleDecoration) {
+ return idleDecoration;
+ }
+ return sessionIdentityDecoration(session);
}
}
@@ -869,6 +998,7 @@ class WorktreeItem extends vscode.TreeItem {
constructor(worktreePath, sessions, items = [], options = {}) {
const normalizedWorktreePath = typeof worktreePath === 'string' ? worktreePath.trim() : '';
const sessionList = Array.isArray(sessions) ? sessions : [];
+ const primarySession = options.resourceSession || sessionList[0] || null;
const changedCount = Number.isInteger(options.changedCount)
? options.changedCount
: sessionList.reduce((total, session) => total + (session.changeCount || 0), 0);
@@ -877,7 +1007,7 @@ class WorktreeItem extends vscode.TreeItem {
descriptionParts.push(`${changedCount} changed`);
}
super(
- path.basename(normalizedWorktreePath || '') || 'worktree',
+ options.label || path.basename(normalizedWorktreePath || '') || 'worktree',
items.length > 0 ? vscode.TreeItemCollapsibleState.Expanded : vscode.TreeItemCollapsibleState.None,
);
this.worktreePath = normalizedWorktreePath;
@@ -888,13 +1018,16 @@ class WorktreeItem extends vscode.TreeItem {
normalizedWorktreePath,
...sessionList.map((session) => session.branch).filter(Boolean),
].filter(Boolean).join('\n');
- this.iconPath = new vscode.ThemeIcon('folder');
+ this.iconPath = new vscode.ThemeIcon(options.iconId || 'folder');
+ if (options.useSessionDecoration && primarySession?.branch) {
+ this.resourceUri = sessionDecorationUri(primarySession.branch);
+ }
this.contextValue = 'gitguardex.worktree';
- if (sessionList[0]?.worktreePath) {
+ if (primarySession?.worktreePath) {
this.command = {
command: 'gitguardex.activeAgents.openWorktree',
title: 'Open Agent Worktree',
- arguments: [sessionList[0]],
+ arguments: [primarySession],
};
}
}
@@ -1856,6 +1989,8 @@ function commitWorktree(worktreePath, message) {
}
function buildSessionDetailItems(session) {
+ const provider = resolveSessionProvider(session);
+ const snapshot = sessionSnapshotDisplayName(session);
const items = [
new DetailItem('Recent change', session.recentChangeSummary || 'No recent change summary.', {
iconId: 'history',
@@ -1871,6 +2006,16 @@ function buildSessionDetailItems(session) {
tooltip: session.worktreePath,
}),
];
+ if (snapshot) {
+ items.splice(3, 0, new DetailItem('Snapshot', snapshot, {
+ iconId: 'account',
+ }));
+ }
+ if (provider?.label) {
+ items.splice(3, 0, new DetailItem('Provider', provider.label, {
+ iconId: 'sparkle',
+ }));
+ }
const badgeSummary = uniqueStringList([
...(session.riskBadges || []),
session.deltaLabel || '',
@@ -1923,6 +2068,12 @@ function buildRawActiveAgentGroupNodes(sessions) {
variant: 'raw',
},
)),
+ {
+ description: buildWorktreeBranchDescription(worktreeSessions),
+ iconId: 'git-branch',
+ resourceSession: worktreeSessions[0],
+ useSessionDecoration: true,
+ },
)
));
if (worktreeItems.length > 0) {
diff --git a/templates/vscode/guardex-active-agents/fileicons/gitguardex-fileicons.json b/templates/vscode/guardex-active-agents/fileicons/gitguardex-fileicons.json
new file mode 100644
index 0000000..e8e5968
--- /dev/null
+++ b/templates/vscode/guardex-active-agents/fileicons/gitguardex-fileicons.json
@@ -0,0 +1,54 @@
+{
+ "iconDefinitions": {
+ "_gitguardex_agent": {
+ "iconPath": "./icons/agent.svg"
+ },
+ "_gitguardex_branch": {
+ "iconPath": "./icons/branch.svg"
+ },
+ "_gitguardex_config": {
+ "iconPath": "./icons/config.svg"
+ },
+ "_gitguardex_hook": {
+ "iconPath": "./icons/hook.svg"
+ },
+ "_gitguardex_openspec": {
+ "iconPath": "./icons/openspec.svg"
+ },
+ "_gitguardex_plan": {
+ "iconPath": "./icons/plan.svg"
+ },
+ "_gitguardex_spec": {
+ "iconPath": "./icons/spec.svg"
+ }
+ },
+ "folderNames": {
+ ".agents": "_gitguardex_agent",
+ ".githooks": "_gitguardex_hook",
+ ".omc": "_gitguardex_agent",
+ ".omx": "_gitguardex_agent",
+ "agent-worktrees": "_gitguardex_branch",
+ "changes": "_gitguardex_openspec",
+ "plan": "_gitguardex_plan",
+ "rules": "_gitguardex_spec",
+ "specs": "_gitguardex_spec"
+ },
+ "fileNames": {
+ ".openspec.yaml": "_gitguardex_config",
+ "AGENT.lock": "_gitguardex_agent",
+ "AGENTS.md": "_gitguardex_agent",
+ "CLAUDE.md": "_gitguardex_agent",
+ "config.yaml": "_gitguardex_config",
+ "context-docs-cue.md": "_gitguardex_spec",
+ "post-checkout": "_gitguardex_hook",
+ "pre-commit": "_gitguardex_hook",
+ "pre-push": "_gitguardex_hook",
+ "proposal.md": "_gitguardex_openspec",
+ "spec.md": "_gitguardex_spec",
+ "tasks.md": "_gitguardex_plan",
+ "plan.md": "_gitguardex_plan"
+ },
+ "fileExtensions": {
+ "openspec.yaml": "_gitguardex_config"
+ }
+}
diff --git a/templates/vscode/guardex-active-agents/fileicons/icons/agent.svg b/templates/vscode/guardex-active-agents/fileicons/icons/agent.svg
new file mode 100644
index 0000000..7a71d75
--- /dev/null
+++ b/templates/vscode/guardex-active-agents/fileicons/icons/agent.svg
@@ -0,0 +1,4 @@
+
diff --git a/templates/vscode/guardex-active-agents/fileicons/icons/branch.svg b/templates/vscode/guardex-active-agents/fileicons/icons/branch.svg
new file mode 100644
index 0000000..f55fed0
--- /dev/null
+++ b/templates/vscode/guardex-active-agents/fileicons/icons/branch.svg
@@ -0,0 +1,4 @@
+
diff --git a/templates/vscode/guardex-active-agents/fileicons/icons/config.svg b/templates/vscode/guardex-active-agents/fileicons/icons/config.svg
new file mode 100644
index 0000000..d6d45ee
--- /dev/null
+++ b/templates/vscode/guardex-active-agents/fileicons/icons/config.svg
@@ -0,0 +1,4 @@
+
diff --git a/templates/vscode/guardex-active-agents/fileicons/icons/hook.svg b/templates/vscode/guardex-active-agents/fileicons/icons/hook.svg
new file mode 100644
index 0000000..3478b55
--- /dev/null
+++ b/templates/vscode/guardex-active-agents/fileicons/icons/hook.svg
@@ -0,0 +1,3 @@
+
diff --git a/templates/vscode/guardex-active-agents/fileicons/icons/openspec.svg b/templates/vscode/guardex-active-agents/fileicons/icons/openspec.svg
new file mode 100644
index 0000000..84314d6
--- /dev/null
+++ b/templates/vscode/guardex-active-agents/fileicons/icons/openspec.svg
@@ -0,0 +1,5 @@
+
diff --git a/templates/vscode/guardex-active-agents/fileicons/icons/plan.svg b/templates/vscode/guardex-active-agents/fileicons/icons/plan.svg
new file mode 100644
index 0000000..c4f65a6
--- /dev/null
+++ b/templates/vscode/guardex-active-agents/fileicons/icons/plan.svg
@@ -0,0 +1,4 @@
+
diff --git a/templates/vscode/guardex-active-agents/fileicons/icons/spec.svg b/templates/vscode/guardex-active-agents/fileicons/icons/spec.svg
new file mode 100644
index 0000000..9eb1fa5
--- /dev/null
+++ b/templates/vscode/guardex-active-agents/fileicons/icons/spec.svg
@@ -0,0 +1,4 @@
+
diff --git a/templates/vscode/guardex-active-agents/package.json b/templates/vscode/guardex-active-agents/package.json
index 05ce3e8..f9fad4f 100644
--- a/templates/vscode/guardex-active-agents/package.json
+++ b/templates/vscode/guardex-active-agents/package.json
@@ -3,7 +3,7 @@
"displayName": "GitGuardex Active Agents",
"description": "Shows live Guardex sandbox sessions and repo changes inside VS Code Source Control.",
"publisher": "recodeee",
- "version": "0.0.9",
+ "version": "0.0.10",
"license": "MIT",
"icon": "icon.png",
"engines": {
@@ -22,6 +22,13 @@
],
"main": "./extension.js",
"contributes": {
+ "iconThemes": [
+ {
+ "id": "gitguardex-file-icons",
+ "label": "GitGuardex File Icons",
+ "path": "./fileicons/gitguardex-fileicons.json"
+ }
+ ],
"commands": [
{
"command": "gitguardex.activeAgents.startAgent",
diff --git a/templates/vscode/guardex-active-agents/session-schema.js b/templates/vscode/guardex-active-agents/session-schema.js
index d2a71c8..e561987 100644
--- a/templates/vscode/guardex-active-agents/session-schema.js
+++ b/templates/vscode/guardex-active-agents/session-schema.js
@@ -744,6 +744,8 @@ function buildSessionRecord(input) {
taskName: toNonEmptyString(input.taskName, 'task'),
latestTaskPreview: '',
agentName: toNonEmptyString(input.agentName, 'agent'),
+ snapshotName: toNonEmptyString(input.snapshotName),
+ snapshotEmail: toNonEmptyString(input.snapshotEmail || input.email),
worktreePath,
pid,
cliName: toNonEmptyString(input.cliName, 'codex'),
@@ -794,6 +796,8 @@ function normalizeSessionRecord(input, options = {}) {
taskName: toNonEmptyString(input.taskName, 'task'),
latestTaskPreview: '',
agentName: toNonEmptyString(input.agentName, 'agent'),
+ snapshotName: toNonEmptyString(input.snapshotName),
+ snapshotEmail: toNonEmptyString(input.snapshotEmail || input.email),
worktreePath: path.resolve(worktreePath),
pid,
cliName: toNonEmptyString(input.cliName, 'codex'),
@@ -915,7 +919,18 @@ function sortSessionsByTimestamp(sessions) {
}
function deriveLockTaskAnchor(entries, fallbackTaskName, fallbackTimestamp) {
- const sortedEntries = [...entries].sort((left, right) => {
+ const sortedEntries = sortTelemetryEntriesForAnchor(entries);
+
+ const latestEntry = sortedEntries[0] || null;
+ return {
+ taskName: latestEntry?.taskPreview || fallbackTaskName || 'task',
+ latestTaskPreview: latestEntry?.taskPreview || '',
+ timestamp: latestEntry?.taskUpdatedAt || fallbackTimestamp || '',
+ };
+}
+
+function sortTelemetryEntriesForAnchor(entries) {
+ return [...entries].sort((left, right) => {
const timeDelta = Date.parse(right.taskUpdatedAt || '') - Date.parse(left.taskUpdatedAt || '');
if (timeDelta !== 0) {
return timeDelta;
@@ -925,12 +940,14 @@ function deriveLockTaskAnchor(entries, fallbackTaskName, fallbackTimestamp) {
}
return (right.projectPath || '').localeCompare(left.projectPath || '');
});
+}
- const latestEntry = sortedEntries[0] || null;
+function deriveLockSnapshotIdentity(entries) {
+ const latestEntry = sortTelemetryEntriesForAnchor(entries)
+ .find((entry) => entry?.snapshotName || entry?.email) || null;
return {
- taskName: latestEntry?.taskPreview || fallbackTaskName || 'task',
- latestTaskPreview: latestEntry?.taskPreview || '',
- timestamp: latestEntry?.taskUpdatedAt || fallbackTimestamp || '',
+ snapshotName: toNonEmptyString(latestEntry?.snapshotName),
+ snapshotEmail: toNonEmptyString(latestEntry?.email),
};
}
@@ -944,6 +961,7 @@ function buildWorktreeLockSession(repoRoot, worktreePath, lockPayload, options =
: `agent/telemetry/${path.basename(worktreePath)}`;
const label = deriveSessionLabel(effectiveBranch, worktreePath);
const taskAnchor = deriveLockTaskAnchor(telemetryEntries, label, telemetryUpdatedAt);
+ const snapshotIdentity = deriveLockSnapshotIdentity(telemetryEntries);
const startedAt = taskAnchor.timestamp || telemetryUpdatedAt || new Date(now).toISOString();
const session = {
@@ -953,6 +971,8 @@ function buildWorktreeLockSession(repoRoot, worktreePath, lockPayload, options =
taskName: taskAnchor.taskName,
latestTaskPreview: taskAnchor.latestTaskPreview,
agentName: deriveAgentNameFromBranch(effectiveBranch),
+ snapshotName: snapshotIdentity.snapshotName,
+ snapshotEmail: snapshotIdentity.snapshotEmail,
worktreePath: path.resolve(worktreePath),
pid: null,
cliName: 'codex',
@@ -995,6 +1015,8 @@ function buildManagedWorktreeSession(repoRoot, worktreePath, options = {}) {
taskName: label,
latestTaskPreview: '',
agentName: deriveAgentNameFromBranch(branch),
+ snapshotName: '',
+ snapshotEmail: '',
worktreePath: path.resolve(worktreePath),
pid: null,
cliName: 'gx',
diff --git a/test/metadata.test.js b/test/metadata.test.js
index 2ae25b3..96df253 100644
--- a/test/metadata.test.js
+++ b/test/metadata.test.js
@@ -138,6 +138,14 @@ test('critical runtime helper scripts and active-agents sources stay in sync wit
['templates/vscode/guardex-active-agents/extension.js', 'vscode/guardex-active-agents/extension.js'],
['templates/vscode/guardex-active-agents/session-schema.js', 'vscode/guardex-active-agents/session-schema.js'],
['templates/vscode/guardex-active-agents/icon.png', 'vscode/guardex-active-agents/icon.png'],
+ ['templates/vscode/guardex-active-agents/fileicons/gitguardex-fileicons.json', 'vscode/guardex-active-agents/fileicons/gitguardex-fileicons.json'],
+ ['templates/vscode/guardex-active-agents/fileicons/icons/agent.svg', 'vscode/guardex-active-agents/fileicons/icons/agent.svg'],
+ ['templates/vscode/guardex-active-agents/fileicons/icons/branch.svg', 'vscode/guardex-active-agents/fileicons/icons/branch.svg'],
+ ['templates/vscode/guardex-active-agents/fileicons/icons/config.svg', 'vscode/guardex-active-agents/fileicons/icons/config.svg'],
+ ['templates/vscode/guardex-active-agents/fileicons/icons/hook.svg', 'vscode/guardex-active-agents/fileicons/icons/hook.svg'],
+ ['templates/vscode/guardex-active-agents/fileicons/icons/openspec.svg', 'vscode/guardex-active-agents/fileicons/icons/openspec.svg'],
+ ['templates/vscode/guardex-active-agents/fileicons/icons/plan.svg', 'vscode/guardex-active-agents/fileicons/icons/plan.svg'],
+ ['templates/vscode/guardex-active-agents/fileicons/icons/spec.svg', 'vscode/guardex-active-agents/fileicons/icons/spec.svg'],
];
for (const [templatePath, runtimePath] of pairs) {
diff --git a/test/setup.test.js b/test/setup.test.js
index e3759bd..632c643 100644
--- a/test/setup.test.js
+++ b/test/setup.test.js
@@ -180,6 +180,14 @@ test('setup provisions workflow files and repo config', () => {
'vscode/guardex-active-agents/extension.js',
'vscode/guardex-active-agents/session-schema.js',
'vscode/guardex-active-agents/icon.png',
+ 'vscode/guardex-active-agents/fileicons/gitguardex-fileicons.json',
+ 'vscode/guardex-active-agents/fileicons/icons/agent.svg',
+ 'vscode/guardex-active-agents/fileicons/icons/branch.svg',
+ 'vscode/guardex-active-agents/fileicons/icons/config.svg',
+ 'vscode/guardex-active-agents/fileicons/icons/hook.svg',
+ 'vscode/guardex-active-agents/fileicons/icons/openspec.svg',
+ 'vscode/guardex-active-agents/fileicons/icons/plan.svg',
+ 'vscode/guardex-active-agents/fileicons/icons/spec.svg',
];
for (const relativePath of canonicalBundleFiles) {
const installedPath = path.join(repoDir, relativePath);
diff --git a/test/vscode-active-agents-session-state.test.js b/test/vscode-active-agents-session-state.test.js
index 1a43482..49185fd 100644
--- a/test/vscode-active-agents-session-state.test.js
+++ b/test/vscode-active-agents-session-state.test.js
@@ -1214,8 +1214,11 @@ test('install-vscode-active-agents-extension installs the current extension into
assert.equal(installedManifest.icon, 'icon.png');
assert.equal(installedManifest.version, manifest.version);
assert.deepEqual(installedManifest.activationEvents, manifest.activationEvents);
+ assert.deepEqual(installedManifest.contributes.iconThemes, manifest.contributes.iconThemes);
assert.equal(installedManifest.activationEvents.includes('onStartupFinished'), true);
assert.equal(fs.existsSync(path.join(canonicalDir, 'icon.png')), true);
+ assert.equal(fs.existsSync(path.join(canonicalDir, 'fileicons', 'gitguardex-fileicons.json')), true);
+ assert.equal(fs.existsSync(path.join(canonicalDir, 'fileicons', 'icons', 'openspec.svg')), true);
assert.equal(fs.existsSync(currentVersionDir), true);
assert.equal(fs.existsSync(path.join(recentCompatDir, 'package.json')), true);
assert.equal(fs.existsSync(path.join(recentCompatDir, 'stale.txt')), false);
@@ -1247,6 +1250,11 @@ test('active-agents extension edits require a higher manifest version than the b
templateManifest.activationEvents,
'Live and template Active Agents activation events must stay in sync.',
);
+ assert.deepEqual(
+ liveManifest.contributes.iconThemes,
+ templateManifest.contributes.iconThemes,
+ 'Live and template Active Agents icon theme contributions must stay in sync.',
+ );
assert.equal(
liveManifest.activationEvents.includes('onStartupFinished'),
true,
@@ -1261,6 +1269,31 @@ test('active-agents extension edits require a higher manifest version than the b
);
});
+test('active-agents file icon theme maps Guardex workflow paths and ships referenced assets', () => {
+ const manifest = readExtensionManifest();
+ const themeContribution = manifest.contributes.iconThemes.find((entry) => entry.id === 'gitguardex-file-icons');
+ assert.ok(themeContribution, 'Expected the GitGuardex file icon theme contribution.');
+ assert.equal(themeContribution.path, './fileicons/gitguardex-fileicons.json');
+
+ const themePath = path.join(path.dirname(extensionManifestPath), themeContribution.path);
+ const theme = readJson(themePath);
+ assert.equal(theme.folderNames.changes, '_gitguardex_openspec');
+ assert.equal(theme.folderNames.plan, '_gitguardex_plan');
+ assert.equal(theme.folderNames.specs, '_gitguardex_spec');
+ assert.equal(theme.folderNames['agent-worktrees'], '_gitguardex_branch');
+ assert.equal(theme.folderNames['.githooks'], '_gitguardex_hook');
+ assert.equal(theme.fileNames['AGENTS.md'], '_gitguardex_agent');
+ assert.equal(theme.fileNames['proposal.md'], '_gitguardex_openspec');
+ assert.equal(theme.fileNames['tasks.md'], '_gitguardex_plan');
+ assert.equal(theme.fileNames['spec.md'], '_gitguardex_spec');
+ assert.equal(theme.fileNames['pre-commit'], '_gitguardex_hook');
+
+ for (const definition of Object.values(theme.iconDefinitions)) {
+ assert.equal(typeof definition.iconPath, 'string');
+ assert.equal(fs.existsSync(path.join(path.dirname(themePath), definition.iconPath)), true);
+ }
+});
+
test('active-agents extension auto-installs a newer workspace build and offers reload', async () => {
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'guardex-vscode-autoupdate-'));
const repoManifest = {
@@ -1580,7 +1613,7 @@ test('active-agents extension groups live sessions under a repo node', async ()
assert.equal(worktreeItem, null);
assert.equal(sessionItem.label, 'live-task');
assert.equal(sessionItem.session.branch, 'agent/codex/live-task');
- assert.match(sessionItem.description, /^codex · Idle/);
+ assert.match(sessionItem.description, /^Idle: codex · via OpenAI/);
assert.equal(sessionItem.iconPath.id, 'comment-discussion');
assert.equal(sessionItem.resourceUri.scheme, 'gitguardex-agent');
assert.equal(
@@ -1606,6 +1639,69 @@ test('active-agents extension groups live sessions under a repo node', async ()
}
});
+test('active-agents extension shows provider and snapshot identity badges', async () => {
+ const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'guardex-vscode-provider-badges-'));
+ const codexWorktreePath = fs.mkdtempSync(path.join(os.tmpdir(), 'guardex-vscode-provider-codex-'));
+ const claudeWorktreePath = fs.mkdtempSync(path.join(os.tmpdir(), 'guardex-vscode-provider-claude-'));
+ initGitRepo(codexWorktreePath);
+ initGitRepo(claudeWorktreePath);
+
+ const codexSessionPath = writeSessionRecord(tempRoot, sessionSchema.buildSessionRecord({
+ repoRoot: tempRoot,
+ branch: 'agent/codex/provider-task',
+ taskName: 'provider-task',
+ agentName: 'codex',
+ snapshotName: 'nagyviktor@edixa.com',
+ worktreePath: codexWorktreePath,
+ pid: process.pid,
+ cliName: 'codex',
+ }));
+ const claudeSessionPath = writeSessionRecord(tempRoot, sessionSchema.buildSessionRecord({
+ repoRoot: tempRoot,
+ branch: 'agent/claude/provider-task',
+ taskName: 'provider-task',
+ agentName: 'claude',
+ worktreePath: claudeWorktreePath,
+ pid: process.pid,
+ cliName: 'claude',
+ }));
+
+ const { registrations, vscode } = createMockVscode(tempRoot);
+ vscode.workspace.findFiles = async () => [
+ { fsPath: codexSessionPath },
+ { fsPath: claudeSessionPath },
+ ];
+ const extension = loadExtensionWithMockVscode(vscode);
+ const context = { subscriptions: [] };
+
+ extension.activate(context);
+ await flushAsyncWork();
+
+ const provider = registrations.providers[0].provider;
+ const [repoItem] = await provider.getChildren();
+ const idleSection = await getSectionByLabel(provider, repoItem, 'Idle / thinking');
+ const codexItem = await getSessionByBranch(provider, idleSection, 'agent/codex/provider-task');
+ const claudeItem = await getSessionByBranch(provider, idleSection, 'agent/claude/provider-task');
+ assert.match(codexItem.description, /^Idle: codex · via OpenAI · snapshot nagyviktor@edixa\.com/);
+ assert.match(claudeItem.description, /^Idle: claude · via Claude/);
+
+ const decorationProvider = registrations.decorationProviders[0];
+ const codexDecoration = decorationProvider.provideFileDecoration(vscode.Uri.parse(
+ `gitguardex-agent://${sessionSchema.sanitizeBranchForFile('agent/codex/provider-task')}`,
+ ));
+ const claudeDecoration = decorationProvider.provideFileDecoration(vscode.Uri.parse(
+ `gitguardex-agent://${sessionSchema.sanitizeBranchForFile('agent/claude/provider-task')}`,
+ ));
+ assert.equal(codexDecoration.badge, 'N');
+ assert.equal(codexDecoration.tooltip, 'Snapshot nagyviktor@edixa.com');
+ assert.equal(claudeDecoration.badge, 'CL');
+ assert.equal(claudeDecoration.tooltip, 'Claude session via claude');
+
+ for (const subscription of context.subscriptions) {
+ subscription.dispose?.();
+ }
+});
+
test('active-agents extension decorates idle clean sessions without overriding working rows', async () => {
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'guardex-vscode-idle-decorations-'));
@@ -1696,7 +1792,8 @@ test('active-agents extension decorates idle clean sessions without overriding w
const workingDecoration = decorationProvider.provideFileDecoration(vscode.Uri.parse(
`gitguardex-agent://${sessionSchema.sanitizeBranchForFile('agent/codex/working-now')}`,
));
- assert.equal(workingDecoration, undefined);
+ assert.equal(workingDecoration.badge, 'AI');
+ assert.equal(workingDecoration.tooltip, 'OpenAI session via codex');
for (const subscription of context.subscriptions) {
subscription.dispose?.();
@@ -1831,7 +1928,7 @@ test('active-agents extension shows grouped repo changes beside active agents',
assert.equal(worktreeItem, null);
assert.equal(sessionItem.label, 'live-task');
assert.equal(sessionItem.session.branch, 'agent/codex/live-task');
- assert.match(sessionItem.description, /^codex · Working · 2 changed files/);
+ assert.match(sessionItem.description, /^Working: codex · via OpenAI · 2 changed files/);
assert.match(sessionItem.tooltip, /Recent Changed src\/nested\.js, tracked\.txt/);
assert.equal(sessionItem.iconPath.id, 'loading~spin');
const sessionDetails = await provider.getChildren(sessionItem);
@@ -1891,6 +1988,25 @@ test('active-agents extension surfaces live managed worktrees from AGENT.lock fa
fs.writeFileSync(path.join(worktreePath, 'src', 'live.js'), 'base\nchanged\n', 'utf8');
const lockPath = writeWorktreeLock(worktreePath, {
updatedAt: '2026-04-22T09:01:00.000Z',
+ snapshots: [
+ {
+ snapshotName: 'nagyviktor@edixa.com',
+ accountId: 'acct-1',
+ email: 'nagyviktor@edixa.com',
+ liveSessionCount: 1,
+ trackedSessionCount: 1,
+ compatSessionCount: 1,
+ sessions: [
+ {
+ sessionKey: 'pid:101',
+ taskPreview: 'Implement live worktree telemetry',
+ taskUpdatedAt: '2026-04-22T08:55:00.000Z',
+ projectName: 'gitguardex',
+ projectPath: worktreePath,
+ },
+ ],
+ },
+ ],
});
const { registrations, vscode } = createMockVscode(tempRoot);
@@ -1922,8 +2038,15 @@ test('active-agents extension surfaces live managed worktrees from AGENT.lock fa
const { worktreeItem, sessionItem } = await getOnlyWorktreeAndSession(provider, workingSection);
assert.equal(worktreeItem, null);
assert.equal(sessionItem.session.branch, 'agent/codex/lock-visible-task');
- assert.match(sessionItem.description, /^codex · Working · 1 changed file/);
+ assert.match(sessionItem.description, /^Working: codex · via OpenAI · snapshot nagyviktor@edixa\.com · 1 changed file/);
+ assert.equal(sessionItem.session.snapshotName, 'nagyviktor@edixa.com');
assert.match(sessionItem.tooltip, /Telemetry updated 2026-04-22T09:01:00.000Z/);
+ assert.match(sessionItem.tooltip, /Snapshot nagyviktor@edixa\.com/);
+ const snapshotDecoration = registrations.decorationProviders[0].provideFileDecoration(vscode.Uri.parse(
+ `gitguardex-agent://${sessionSchema.sanitizeBranchForFile('agent/codex/lock-visible-task')}`,
+ ));
+ assert.equal(snapshotDecoration.badge, 'N');
+ assert.equal(snapshotDecoration.tooltip, 'Snapshot nagyviktor@edixa.com');
for (const subscription of context.subscriptions) {
subscription.dispose?.();
@@ -1964,7 +2087,7 @@ test('active-agents extension surfaces plain managed worktrees from workspace fa
const { worktreeItem, sessionItem } = await getOnlyWorktreeAndSession(provider, workingSection);
assert.equal(worktreeItem, null);
assert.equal(sessionItem.session.branch, 'agent/codex/plain-visible-task');
- assert.match(sessionItem.description, /^codex · Working · 1 changed file/);
+ assert.match(sessionItem.description, /^Working: codex · via OpenAI · 1 changed file/);
assert.match(sessionItem.tooltip, /Started /);
for (const subscription of context.subscriptions) {
@@ -2050,6 +2173,9 @@ test('active-agents extension decorates sessions and repo changes from the lock
const activeAgentTree = await getSectionByLabel(provider, advancedSection, 'Active agent tree');
const rawWorkingSection = await getSectionByLabel(provider, activeAgentTree, 'WORKING NOW');
const worktreeGroup = await getChildByLabel(provider, rawWorkingSection, path.basename(worktreePath));
+ assert.equal(worktreeGroup.iconPath.id, 'git-branch');
+ assert.equal(worktreeGroup.description, 'working: codex');
+ assert.equal(worktreeGroup.resourceUri.toString(), `gitguardex-agent://${sessionSchema.sanitizeBranchForFile(branch)}`);
const [sessionGroup] = await provider.getChildren(worktreeGroup);
const [sessionChangeItem] = await provider.getChildren(sessionGroup);
assert.equal(sessionChangeItem.label, 'tracked.txt');
@@ -2300,15 +2426,15 @@ test('active-agents extension groups blocked, working, idle, stalled, and dead s
const idleItem = await getSessionByBranch(provider, idleThinkingSection, 'agent/codex/idle-task');
const stalledItem = await getSessionByBranch(provider, idleThinkingSection, 'agent/codex/stalled-task');
const deadItem = await getSessionByBranch(provider, idleThinkingSection, 'agent/codex/dead-task');
- assert.match(blockedItem.description, /^codex · Blocked/);
+ assert.match(blockedItem.description, /^Blocked: codex · via OpenAI/);
assert.equal(blockedItem.iconPath.id, 'warning');
- assert.match(workingItem.description, /^codex · Working · 1 changed file/);
+ assert.match(workingItem.description, /^Working: codex · via OpenAI · 1 changed file/);
assert.equal(workingItem.iconPath.id, 'loading~spin');
- assert.match(idleItem.description, /^codex · Idle/);
+ assert.match(idleItem.description, /^Idle: codex · via OpenAI/);
assert.equal(idleItem.iconPath.id, 'comment-discussion');
- assert.match(stalledItem.description, /^codex · Stale/);
+ assert.match(stalledItem.description, /^Stale: codex · via OpenAI/);
assert.equal(stalledItem.iconPath.id, 'clock');
- assert.match(deadItem.description, /^codex · Dead/);
+ assert.match(deadItem.description, /^Dead: codex · via OpenAI/);
assert.equal(deadItem.iconPath.id, 'error');
assert.deepEqual(registrations.treeViews[0].badge, {
value: 5,
diff --git a/vscode/guardex-active-agents/README.md b/vscode/guardex-active-agents/README.md
index 6878a1c..9b0c580 100644
--- a/vscode/guardex-active-agents/README.md
+++ b/vscode/guardex-active-agents/README.md
@@ -18,11 +18,14 @@ node scripts/install-vscode-active-agents-extension.js
What it does:
- Bundles a local GitGuardex icon so repo installs show branded extension metadata inside VS Code.
+- Bundles the optional `GitGuardex File Icons` theme for OpenSpec, agent worktree, and hook files in Explorer.
- Adds an `Active Agents` view to the Source Control container.
- Renders one repo node per live Guardex workspace with grouped `ACTIVE AGENTS` and `CHANGES` sections.
- Splits live sessions inside `ACTIVE AGENTS` into `BLOCKED`, `WORKING NOW`, `THINKING`, `STALLED`, and `DEAD` groups so stuck, active, and inactive lanes stand out immediately.
- Mirrors the same live state in the VS Code status bar so the selected session or active-agent count stays visible outside the tree.
- Shows one row per live Guardex sandbox session inside those activity groups, with changed-file rows nested under sessions that are touching files.
+- Labels session rows with provider identity and snapshot context; snapshot-backed rows use a one-letter snapshot badge such as `N` for `nagyviktor@edixa.com`.
+- Shows raw agent branch groups with the `git-branch` icon instead of the generic folder icon.
- Shows repo-root git changes in a sibling `CHANGES` section when the guarded repo itself is dirty.
- Derives session state from dirty worktree status, git conflict markers, heartbeat freshness, PID liveness, and recent file mtimes, surfaces working/dead/conflict counts in the repo/header summary, and shows changed-file counts for active edits.
- Uses distinct VS Code codicons for each session state, including animated `loading~spin` for `WORKING NOW`.
diff --git a/vscode/guardex-active-agents/extension.js b/vscode/guardex-active-agents/extension.js
index 255344c..0f9a096 100644
--- a/vscode/guardex-active-agents/extension.js
+++ b/vscode/guardex-active-agents/extension.js
@@ -56,6 +56,18 @@ const SESSION_ACTIVITY_ICON_IDS = {
stalled: 'clock',
dead: 'error',
};
+const SESSION_PROVIDER_BRANDS = {
+ openai: {
+ id: 'openai',
+ label: 'OpenAI',
+ badge: 'AI',
+ },
+ claude: {
+ id: 'claude',
+ label: 'Claude',
+ badge: 'CL',
+ },
+};
function sessionDecorationUri(branch) {
return vscode.Uri.parse(`${SESSION_DECORATION_SCHEME}://${sanitizeBranchForFile(branch)}`);
@@ -134,6 +146,84 @@ function uniqueStringList(values) {
return result;
}
+function normalizeSessionProviderToken(value) {
+ return typeof value === 'string' ? value.trim().toLowerCase() : '';
+}
+
+function resolveSessionProvider(session) {
+ const signals = [
+ session?.cliName,
+ session?.agentName,
+ session?.branch,
+ ]
+ .map(normalizeSessionProviderToken)
+ .filter(Boolean);
+
+ if (signals.some((value) => value.includes('claude'))) {
+ return {
+ ...SESSION_PROVIDER_BRANDS.claude,
+ cliName: typeof session?.cliName === 'string' ? session.cliName.trim() : '',
+ };
+ }
+ if (signals.some((value) => value.includes('codex') || value.includes('openai'))) {
+ return {
+ ...SESSION_PROVIDER_BRANDS.openai,
+ cliName: typeof session?.cliName === 'string' ? session.cliName.trim() : '',
+ };
+ }
+ return null;
+}
+
+function sessionProviderDecoration(session) {
+ const provider = resolveSessionProvider(session);
+ if (!provider) {
+ return undefined;
+ }
+
+ const cliName = provider.cliName || provider.id;
+ return {
+ badge: provider.badge,
+ tooltip: `${provider.label} session via ${cliName}`,
+ };
+}
+
+function normalizeSnapshotIdentityValue(value) {
+ return typeof value === 'string' ? value.trim() : '';
+}
+
+function sessionSnapshotDisplayName(session) {
+ return normalizeSnapshotIdentityValue(session?.snapshotName)
+ || normalizeSnapshotIdentityValue(session?.snapshotEmail);
+}
+
+function sessionSnapshotBadge(session) {
+ const displayName = sessionSnapshotDisplayName(session);
+ const match = displayName.match(/[a-z0-9]/i);
+ return match ? match[0].toUpperCase() : '';
+}
+
+function sessionSnapshotDescription(session) {
+ const displayName = sessionSnapshotDisplayName(session);
+ return displayName ? `snapshot ${displayName}` : '';
+}
+
+function sessionSnapshotDecoration(session) {
+ const badge = sessionSnapshotBadge(session);
+ const displayName = sessionSnapshotDisplayName(session);
+ if (!badge || !displayName) {
+ return undefined;
+ }
+
+ return {
+ badge,
+ tooltip: `Snapshot ${displayName}`,
+ };
+}
+
+function sessionIdentityDecoration(session) {
+ return sessionSnapshotDecoration(session) || sessionProviderDecoration(session);
+}
+
function stringListsEqual(left, right) {
if (!Array.isArray(left) || !Array.isArray(right) || left.length !== right.length) {
return false;
@@ -408,9 +498,12 @@ function changeRiskBadges(change) {
}
function buildSessionCardDescription(session) {
+ const provider = resolveSessionProvider(session);
+ const statusAgentLabel = `${sessionStatusLabel(session)}: ${session.agentName || 'agent'}`;
const descriptionParts = [
- session.agentName || 'agent',
- sessionStatusLabel(session),
+ statusAgentLabel,
+ provider?.label ? `via ${provider.label}` : '',
+ sessionSnapshotDescription(session),
session.deltaLabel || '',
session.changeCount > 0 ? formatCountLabel(session.changeCount, 'changed file') : '',
session.lockCount > 0 ? formatCountLabel(session.lockCount, 'lock') : '',
@@ -421,7 +514,16 @@ function buildSessionCardDescription(session) {
}
function buildRawSessionDescription(session) {
- const descriptionParts = [session.activityLabel || 'thinking'];
+ const provider = resolveSessionProvider(session);
+ const status = sessionStatusLabel(session).toLowerCase();
+ const descriptionParts = [`${status}: ${session.agentName || 'agent'}`];
+ if (provider?.label) {
+ descriptionParts.push(provider.label);
+ }
+ const snapshot = sessionSnapshotDescription(session);
+ if (snapshot) {
+ descriptionParts.push(snapshot);
+ }
if (session.activityCountLabel) {
descriptionParts.push(session.activityCountLabel);
}
@@ -433,6 +535,7 @@ function buildRawSessionDescription(session) {
}
function buildSessionTooltip(session, description) {
+ const provider = resolveSessionProvider(session);
const riskSummary = uniqueStringList([
...(session?.riskBadges || []),
session?.deltaLabel || '',
@@ -440,6 +543,10 @@ function buildSessionTooltip(session, description) {
const topFiles = session?.topChangedFilesLabel || summarizeCompactPaths(session?.worktreeChangedPaths || []);
return [
session.branch,
+ provider?.label
+ ? `Provider ${provider.label}${provider.cliName ? ` (${provider.cliName})` : ''}`
+ : '',
+ sessionSnapshotDisplayName(session) ? `Snapshot ${sessionSnapshotDisplayName(session)}` : '',
`${session.agentName} · ${session.taskName}`,
`Status ${description}`,
session.recentChangeSummary ? `Recent ${session.recentChangeSummary}` : '',
@@ -461,6 +568,23 @@ function buildUnassignedChangeDescription(change) {
].filter(Boolean).join(' · ');
}
+function buildWorktreeBranchDescription(sessions) {
+ const sessionList = Array.isArray(sessions) ? sessions : [];
+ const primarySession = sessionList[0] || null;
+ if (!primarySession) {
+ return '';
+ }
+
+ const descriptionParts = [
+ `${sessionStatusLabel(primarySession).toLowerCase()}: ${primarySession.agentName || 'agent'}`,
+ sessionSnapshotDescription(primarySession),
+ ];
+ if (sessionList.length > 1) {
+ descriptionParts.push(formatCountLabel(sessionList.length, 'agent'));
+ }
+ return descriptionParts.filter(Boolean).join(' · ');
+}
+
function buildOverviewDescription(summary) {
return [
formatCountLabel(summary?.workingCount || 0, 'working agent'),
@@ -812,7 +936,12 @@ class SessionDecorationProvider {
};
}
- return sessionIdleDecoration(this.sessionsByUri.get(uri.toString()), this.nowProvider());
+ const session = this.sessionsByUri.get(uri.toString());
+ const idleDecoration = sessionIdleDecoration(session, this.nowProvider());
+ if (idleDecoration) {
+ return idleDecoration;
+ }
+ return sessionIdentityDecoration(session);
}
}
@@ -869,6 +998,7 @@ class WorktreeItem extends vscode.TreeItem {
constructor(worktreePath, sessions, items = [], options = {}) {
const normalizedWorktreePath = typeof worktreePath === 'string' ? worktreePath.trim() : '';
const sessionList = Array.isArray(sessions) ? sessions : [];
+ const primarySession = options.resourceSession || sessionList[0] || null;
const changedCount = Number.isInteger(options.changedCount)
? options.changedCount
: sessionList.reduce((total, session) => total + (session.changeCount || 0), 0);
@@ -877,7 +1007,7 @@ class WorktreeItem extends vscode.TreeItem {
descriptionParts.push(`${changedCount} changed`);
}
super(
- path.basename(normalizedWorktreePath || '') || 'worktree',
+ options.label || path.basename(normalizedWorktreePath || '') || 'worktree',
items.length > 0 ? vscode.TreeItemCollapsibleState.Expanded : vscode.TreeItemCollapsibleState.None,
);
this.worktreePath = normalizedWorktreePath;
@@ -888,13 +1018,16 @@ class WorktreeItem extends vscode.TreeItem {
normalizedWorktreePath,
...sessionList.map((session) => session.branch).filter(Boolean),
].filter(Boolean).join('\n');
- this.iconPath = new vscode.ThemeIcon('folder');
+ this.iconPath = new vscode.ThemeIcon(options.iconId || 'folder');
+ if (options.useSessionDecoration && primarySession?.branch) {
+ this.resourceUri = sessionDecorationUri(primarySession.branch);
+ }
this.contextValue = 'gitguardex.worktree';
- if (sessionList[0]?.worktreePath) {
+ if (primarySession?.worktreePath) {
this.command = {
command: 'gitguardex.activeAgents.openWorktree',
title: 'Open Agent Worktree',
- arguments: [sessionList[0]],
+ arguments: [primarySession],
};
}
}
@@ -1856,6 +1989,8 @@ function commitWorktree(worktreePath, message) {
}
function buildSessionDetailItems(session) {
+ const provider = resolveSessionProvider(session);
+ const snapshot = sessionSnapshotDisplayName(session);
const items = [
new DetailItem('Recent change', session.recentChangeSummary || 'No recent change summary.', {
iconId: 'history',
@@ -1871,6 +2006,16 @@ function buildSessionDetailItems(session) {
tooltip: session.worktreePath,
}),
];
+ if (snapshot) {
+ items.splice(3, 0, new DetailItem('Snapshot', snapshot, {
+ iconId: 'account',
+ }));
+ }
+ if (provider?.label) {
+ items.splice(3, 0, new DetailItem('Provider', provider.label, {
+ iconId: 'sparkle',
+ }));
+ }
const badgeSummary = uniqueStringList([
...(session.riskBadges || []),
session.deltaLabel || '',
@@ -1923,6 +2068,12 @@ function buildRawActiveAgentGroupNodes(sessions) {
variant: 'raw',
},
)),
+ {
+ description: buildWorktreeBranchDescription(worktreeSessions),
+ iconId: 'git-branch',
+ resourceSession: worktreeSessions[0],
+ useSessionDecoration: true,
+ },
)
));
if (worktreeItems.length > 0) {
diff --git a/vscode/guardex-active-agents/fileicons/gitguardex-fileicons.json b/vscode/guardex-active-agents/fileicons/gitguardex-fileicons.json
new file mode 100644
index 0000000..e8e5968
--- /dev/null
+++ b/vscode/guardex-active-agents/fileicons/gitguardex-fileicons.json
@@ -0,0 +1,54 @@
+{
+ "iconDefinitions": {
+ "_gitguardex_agent": {
+ "iconPath": "./icons/agent.svg"
+ },
+ "_gitguardex_branch": {
+ "iconPath": "./icons/branch.svg"
+ },
+ "_gitguardex_config": {
+ "iconPath": "./icons/config.svg"
+ },
+ "_gitguardex_hook": {
+ "iconPath": "./icons/hook.svg"
+ },
+ "_gitguardex_openspec": {
+ "iconPath": "./icons/openspec.svg"
+ },
+ "_gitguardex_plan": {
+ "iconPath": "./icons/plan.svg"
+ },
+ "_gitguardex_spec": {
+ "iconPath": "./icons/spec.svg"
+ }
+ },
+ "folderNames": {
+ ".agents": "_gitguardex_agent",
+ ".githooks": "_gitguardex_hook",
+ ".omc": "_gitguardex_agent",
+ ".omx": "_gitguardex_agent",
+ "agent-worktrees": "_gitguardex_branch",
+ "changes": "_gitguardex_openspec",
+ "plan": "_gitguardex_plan",
+ "rules": "_gitguardex_spec",
+ "specs": "_gitguardex_spec"
+ },
+ "fileNames": {
+ ".openspec.yaml": "_gitguardex_config",
+ "AGENT.lock": "_gitguardex_agent",
+ "AGENTS.md": "_gitguardex_agent",
+ "CLAUDE.md": "_gitguardex_agent",
+ "config.yaml": "_gitguardex_config",
+ "context-docs-cue.md": "_gitguardex_spec",
+ "post-checkout": "_gitguardex_hook",
+ "pre-commit": "_gitguardex_hook",
+ "pre-push": "_gitguardex_hook",
+ "proposal.md": "_gitguardex_openspec",
+ "spec.md": "_gitguardex_spec",
+ "tasks.md": "_gitguardex_plan",
+ "plan.md": "_gitguardex_plan"
+ },
+ "fileExtensions": {
+ "openspec.yaml": "_gitguardex_config"
+ }
+}
diff --git a/vscode/guardex-active-agents/fileicons/icons/agent.svg b/vscode/guardex-active-agents/fileicons/icons/agent.svg
new file mode 100644
index 0000000..7a71d75
--- /dev/null
+++ b/vscode/guardex-active-agents/fileicons/icons/agent.svg
@@ -0,0 +1,4 @@
+
diff --git a/vscode/guardex-active-agents/fileicons/icons/branch.svg b/vscode/guardex-active-agents/fileicons/icons/branch.svg
new file mode 100644
index 0000000..f55fed0
--- /dev/null
+++ b/vscode/guardex-active-agents/fileicons/icons/branch.svg
@@ -0,0 +1,4 @@
+
diff --git a/vscode/guardex-active-agents/fileicons/icons/config.svg b/vscode/guardex-active-agents/fileicons/icons/config.svg
new file mode 100644
index 0000000..d6d45ee
--- /dev/null
+++ b/vscode/guardex-active-agents/fileicons/icons/config.svg
@@ -0,0 +1,4 @@
+
diff --git a/vscode/guardex-active-agents/fileicons/icons/hook.svg b/vscode/guardex-active-agents/fileicons/icons/hook.svg
new file mode 100644
index 0000000..3478b55
--- /dev/null
+++ b/vscode/guardex-active-agents/fileicons/icons/hook.svg
@@ -0,0 +1,3 @@
+
diff --git a/vscode/guardex-active-agents/fileicons/icons/openspec.svg b/vscode/guardex-active-agents/fileicons/icons/openspec.svg
new file mode 100644
index 0000000..84314d6
--- /dev/null
+++ b/vscode/guardex-active-agents/fileicons/icons/openspec.svg
@@ -0,0 +1,5 @@
+
diff --git a/vscode/guardex-active-agents/fileicons/icons/plan.svg b/vscode/guardex-active-agents/fileicons/icons/plan.svg
new file mode 100644
index 0000000..c4f65a6
--- /dev/null
+++ b/vscode/guardex-active-agents/fileicons/icons/plan.svg
@@ -0,0 +1,4 @@
+
diff --git a/vscode/guardex-active-agents/fileicons/icons/spec.svg b/vscode/guardex-active-agents/fileicons/icons/spec.svg
new file mode 100644
index 0000000..9eb1fa5
--- /dev/null
+++ b/vscode/guardex-active-agents/fileicons/icons/spec.svg
@@ -0,0 +1,4 @@
+
diff --git a/vscode/guardex-active-agents/package.json b/vscode/guardex-active-agents/package.json
index 05ce3e8..f9fad4f 100644
--- a/vscode/guardex-active-agents/package.json
+++ b/vscode/guardex-active-agents/package.json
@@ -3,7 +3,7 @@
"displayName": "GitGuardex Active Agents",
"description": "Shows live Guardex sandbox sessions and repo changes inside VS Code Source Control.",
"publisher": "recodeee",
- "version": "0.0.9",
+ "version": "0.0.10",
"license": "MIT",
"icon": "icon.png",
"engines": {
@@ -22,6 +22,13 @@
],
"main": "./extension.js",
"contributes": {
+ "iconThemes": [
+ {
+ "id": "gitguardex-file-icons",
+ "label": "GitGuardex File Icons",
+ "path": "./fileicons/gitguardex-fileicons.json"
+ }
+ ],
"commands": [
{
"command": "gitguardex.activeAgents.startAgent",
diff --git a/vscode/guardex-active-agents/session-schema.js b/vscode/guardex-active-agents/session-schema.js
index d2a71c8..e561987 100644
--- a/vscode/guardex-active-agents/session-schema.js
+++ b/vscode/guardex-active-agents/session-schema.js
@@ -744,6 +744,8 @@ function buildSessionRecord(input) {
taskName: toNonEmptyString(input.taskName, 'task'),
latestTaskPreview: '',
agentName: toNonEmptyString(input.agentName, 'agent'),
+ snapshotName: toNonEmptyString(input.snapshotName),
+ snapshotEmail: toNonEmptyString(input.snapshotEmail || input.email),
worktreePath,
pid,
cliName: toNonEmptyString(input.cliName, 'codex'),
@@ -794,6 +796,8 @@ function normalizeSessionRecord(input, options = {}) {
taskName: toNonEmptyString(input.taskName, 'task'),
latestTaskPreview: '',
agentName: toNonEmptyString(input.agentName, 'agent'),
+ snapshotName: toNonEmptyString(input.snapshotName),
+ snapshotEmail: toNonEmptyString(input.snapshotEmail || input.email),
worktreePath: path.resolve(worktreePath),
pid,
cliName: toNonEmptyString(input.cliName, 'codex'),
@@ -915,7 +919,18 @@ function sortSessionsByTimestamp(sessions) {
}
function deriveLockTaskAnchor(entries, fallbackTaskName, fallbackTimestamp) {
- const sortedEntries = [...entries].sort((left, right) => {
+ const sortedEntries = sortTelemetryEntriesForAnchor(entries);
+
+ const latestEntry = sortedEntries[0] || null;
+ return {
+ taskName: latestEntry?.taskPreview || fallbackTaskName || 'task',
+ latestTaskPreview: latestEntry?.taskPreview || '',
+ timestamp: latestEntry?.taskUpdatedAt || fallbackTimestamp || '',
+ };
+}
+
+function sortTelemetryEntriesForAnchor(entries) {
+ return [...entries].sort((left, right) => {
const timeDelta = Date.parse(right.taskUpdatedAt || '') - Date.parse(left.taskUpdatedAt || '');
if (timeDelta !== 0) {
return timeDelta;
@@ -925,12 +940,14 @@ function deriveLockTaskAnchor(entries, fallbackTaskName, fallbackTimestamp) {
}
return (right.projectPath || '').localeCompare(left.projectPath || '');
});
+}
- const latestEntry = sortedEntries[0] || null;
+function deriveLockSnapshotIdentity(entries) {
+ const latestEntry = sortTelemetryEntriesForAnchor(entries)
+ .find((entry) => entry?.snapshotName || entry?.email) || null;
return {
- taskName: latestEntry?.taskPreview || fallbackTaskName || 'task',
- latestTaskPreview: latestEntry?.taskPreview || '',
- timestamp: latestEntry?.taskUpdatedAt || fallbackTimestamp || '',
+ snapshotName: toNonEmptyString(latestEntry?.snapshotName),
+ snapshotEmail: toNonEmptyString(latestEntry?.email),
};
}
@@ -944,6 +961,7 @@ function buildWorktreeLockSession(repoRoot, worktreePath, lockPayload, options =
: `agent/telemetry/${path.basename(worktreePath)}`;
const label = deriveSessionLabel(effectiveBranch, worktreePath);
const taskAnchor = deriveLockTaskAnchor(telemetryEntries, label, telemetryUpdatedAt);
+ const snapshotIdentity = deriveLockSnapshotIdentity(telemetryEntries);
const startedAt = taskAnchor.timestamp || telemetryUpdatedAt || new Date(now).toISOString();
const session = {
@@ -953,6 +971,8 @@ function buildWorktreeLockSession(repoRoot, worktreePath, lockPayload, options =
taskName: taskAnchor.taskName,
latestTaskPreview: taskAnchor.latestTaskPreview,
agentName: deriveAgentNameFromBranch(effectiveBranch),
+ snapshotName: snapshotIdentity.snapshotName,
+ snapshotEmail: snapshotIdentity.snapshotEmail,
worktreePath: path.resolve(worktreePath),
pid: null,
cliName: 'codex',
@@ -995,6 +1015,8 @@ function buildManagedWorktreeSession(repoRoot, worktreePath, options = {}) {
taskName: label,
latestTaskPreview: '',
agentName: deriveAgentNameFromBranch(branch),
+ snapshotName: '',
+ snapshotEmail: '',
worktreePath: path.resolve(worktreePath),
pid: null,
cliName: 'gx',