${branch.name}${statusBadge}
- ${branch.description ? branch.description + " · " : ""}
- ${branch.conflicts ? " · " + branch.conflicts + " conflicts" : ""}
+ ${branch.description ? branch.description : ""}
diff --git a/vscode-extension/client/src/providers/treeviews/workspaceTreeDataProvider.ts b/vscode-extension/client/src/providers/treeviews/workspaceTreeDataProvider.ts
index f4801239c9..1d232ec904 100644
--- a/vscode-extension/client/src/providers/treeviews/workspaceTreeDataProvider.ts
+++ b/vscode-extension/client/src/providers/treeviews/workspaceTreeDataProvider.ts
@@ -1,16 +1,8 @@
import * as vscode from "vscode";
import { LanguageClient } from "vscode-languageclient/node";
-import { BranchNode } from "../../types";
+import { BranchNode, BranchNodeType } from "../../types";
import { BranchStateManager } from "../../data/branchStateManager";
-interface OpsFilterConfig {
- limit: number | null; // null means no limit
- branch: 'current' | 'all' | string; // 'current', 'all', or a specific branch ID
- dateRange: 'all' | 'today' | 'week' | 'month' | 'custom';
- customStartDate?: Date;
- locationFilter?: string;
-}
-
// Interface for pending op response from LSP
interface PendingOpResponse {
op: string;
@@ -26,15 +18,16 @@ interface OpLocation {
// Extended BranchNode for pending ops with additional properties
interface PendingOpNode extends BranchNode {
- type: "pending-op";
+ type: BranchNodeType.PendingOp;
location?: OpLocation;
opData?: string;
}
-// Interface for branch quick pick items with custom properties
-interface BranchQuickPickItem extends vscode.QuickPickItem {
- value?: string;
- branchID?: string;
+// Group node for owner grouping
+interface OwnerGroupNode extends BranchNode {
+ type: BranchNodeType.OwnerGroup;
+ owner: string;
+ children: PendingOpNode[];
}
// Type for package ops (from file system provider)
@@ -43,21 +36,18 @@ interface PackageOp {
[key: string]: unknown;
}
-export class WorkspaceTreeDataProvider implements vscode.TreeDataProvider
{
- private _onDidChangeTreeData: vscode.EventEmitter =
- new vscode.EventEmitter();
- readonly onDidChangeTreeData: vscode.Event =
- this._onDidChangeTreeData.event;
+export class WorkspaceTreeDataProvider
+ implements vscode.TreeDataProvider
+{
+ private _onDidChangeTreeData: vscode.EventEmitter<
+ BranchNode | undefined | null | void
+ > = new vscode.EventEmitter();
+ readonly onDidChangeTreeData: vscode.Event<
+ BranchNode | undefined | null | void
+ > = this._onDidChangeTreeData.event;
private branchStateManager = BranchStateManager.getInstance();
- // Filter configuration for ops display
- private opsFilter: OpsFilterConfig = {
- limit: 50,
- branch: 'current',
- dateRange: 'all',
- };
-
constructor(private client: LanguageClient) {
// Listen for branch changes and refresh the tree
this.branchStateManager.onBranchChanged(() => {
@@ -93,9 +83,11 @@ export class WorkspaceTreeDataProvider implements vscode.TreeDataProvider 0) {
collapsibleState = vscode.TreeItemCollapsibleState.Collapsed;
} else {
@@ -108,28 +100,72 @@ export class WorkspaceTreeDataProvider implements vscode.TreeDataProvider {
- try {
- // Calculate date filter if applicable
- let sinceDate: string | undefined;
- if (this.opsFilter.dateRange !== 'all') {
- const now = new Date();
- const since = new Date();
-
- switch (this.opsFilter.dateRange) {
- case 'today':
- since.setHours(0, 0, 0, 0);
- break;
- case 'week':
- since.setDate(now.getDate() - 7);
- break;
- case 'month':
- since.setMonth(now.getMonth() - 1);
- break;
- case 'custom':
- if (this.opsFilter.customStartDate) {
- since.setTime(this.opsFilter.customStartDate.getTime());
- }
- break;
- }
-
- sinceDate = since.toISOString();
+ private async getBranchChildren(): Promise {
+ const currentBranchId = this.branchStateManager.getCurrentBranchId();
+ const allBranches = this.branchStateManager.getBranches();
+
+ const children: BranchNode[] = [];
+
+ // 1. Current branch (if any)
+ if (currentBranchId) {
+ const currentBranch = allBranches.find(b => b.id === currentBranchId);
+ if (currentBranch) {
+ children.push({
+ id: `branch-${currentBranch.id}`,
+ label: currentBranch.name,
+ type: BranchNodeType.BranchItem,
+ contextValue: "branch-item",
+ branchData: {
+ branchId: currentBranch.id,
+ branchName: currentBranch.name,
+ isCurrent: true,
+ status: undefined,
+ opsCount: undefined,
+ lastSynced: undefined,
+ },
+ });
}
+ }
+
+ // 2. Main/Root branch - show as "main (no branch)" when on a branch
+ // Only add if we're currently on a specific branch
+ if (currentBranchId && currentBranchId !== "") {
+ const mainBranchNode: BranchNode = {
+ id: "branch-main-clear",
+ label: "main (no branch)",
+ type: BranchNodeType.BranchItem,
+ contextValue: "branch-item-main",
+ branchData: {
+ branchId: "",
+ branchName: "main",
+ isMain: true,
+ status: undefined,
+ opsCount: undefined,
+ },
+ };
+ children.push(mainBranchNode);
+ }
+
+ // 3. All other branches (directly, no grouping)
+ const otherBranches = allBranches.filter(b => b.id !== currentBranchId);
+
+ otherBranches.forEach(branch => {
+ children.push({
+ id: `branch-${branch.id}`,
+ label: branch.name,
+ type: BranchNodeType.BranchItem,
+ contextValue: "branch-item",
+ branchData: {
+ branchId: branch.id,
+ branchName: branch.name,
+ isCurrent: false,
+ status: undefined,
+ opsCount: undefined,
+ lastSynced: undefined,
+ },
+ });
+ });
+
+ // 4. Add "Manage All" action node at the bottom
+ children.push({
+ id: "branch-manage-all",
+ label: "Manage All Branches",
+ type: BranchNodeType.BranchAction,
+ contextValue: "branch-manage-all",
+ });
+
+ return children;
+ }
+ private async getPendingChanges(): Promise {
+ try {
const requestParams = {
- limit: this.opsFilter.limit ?? 999999, // Send large number if null
- branchFilter: this.opsFilter.branch,
- sinceDate: sinceDate,
+ limit: 20,
+ branchFilter: "current",
+ sinceDate: undefined,
};
const ops = await this.client.sendRequest(
- 'dark/getPendingOps',
- requestParams
+ "dark/getPendingOps",
+ requestParams,
);
- let nodes: PendingOpNode[] = ops.map((opWithLabel, index) => {
- const location = this.extractLocationFromOp(opWithLabel.op);
- return {
- id: `op-${index}`,
- label: opWithLabel.label, // Use the formatted label from backend
- type: "pending-op",
- contextValue: "pending-op",
- location: location, // Store location for the command
- opData: opWithLabel.op // Store full op for diff view
- };
- });
-
- // Apply client-side location filter if specified
- if (this.opsFilter.locationFilter && this.opsFilter.locationFilter.trim() !== '') {
- const filterLower = this.opsFilter.locationFilter.toLowerCase();
- nodes = nodes.filter(node => {
- // Filter by label
- if (node.label.toLowerCase().includes(filterLower)) {
- return true;
- }
- // Filter by location if available
- if (node.location) {
- const locStr = `${node.location.owner}.${node.location.modules.join('.')}.${node.location.name}`.toLowerCase();
- return locStr.includes(filterLower);
+ let nodes: PendingOpNode[] = ops
+ .filter(opWithLabel => {
+ // Filter out Add* ops completely (AddFn, AddType, AddValue, etc.)
+ try {
+ const parsed = JSON.parse(opWithLabel.op);
+ if (parsed.AddFn || parsed.AddType || parsed.AddValue) {
+ return false; // Skip Add* ops
+ }
+ } catch (e) {
+ // If parsing fails, keep the op
}
- // If no location available, don't match (only label matching works)
- return false;
+ return true;
+ })
+ .map(opWithLabel => {
+ const location = this.extractLocationFromOp(opWithLabel.op);
+ // Use the op data itself as a stable ID (not index-based)
+ // This ensures IDs don't change when ops are reordered
+ const opId = opWithLabel.op;
+
+ // Clean up label - remove "Set*Name →" prefix text, keep only the fqname
+ let cleanLabel = opWithLabel.label;
+ cleanLabel = cleanLabel.replace(/^SetFnName\s+→\s+/, "");
+ cleanLabel = cleanLabel.replace(/^SetTypeName\s+→\s+/, "");
+ cleanLabel = cleanLabel.replace(/^SetValueName\s+→\s+/, "");
+
+ return {
+ id: opId,
+ label: cleanLabel,
+ type: BranchNodeType.PendingOp,
+ contextValue: "pending-op",
+ location: location, // Store location for the command
+ opData: opWithLabel.op, // Store full op for diff view
+ };
});
+
+ // Add "see more" node if we hit the limit
+ const hasMore = ops.length === 20;
+
+ // Only group by owner when on a non-main branch
+ const currentBranchId = this.branchStateManager.getCurrentBranchId();
+ const shouldGroupByOwner =
+ currentBranchId !== null && currentBranchId !== "";
+
+ let result: BranchNode[];
+ if (shouldGroupByOwner) {
+ result = this.groupByOwner(nodes);
+ } else {
+ result = nodes;
+ }
+
+ // Add "see more" node at the end if there are more items
+ if (hasMore) {
+ const seeMoreNode: BranchNode = {
+ id: "see-more-changes",
+ label: "See more...",
+ type: BranchNodeType.SeeMore,
+ contextValue: "see-more",
+ };
+ result.push(seeMoreNode);
}
- return nodes;
+ return result;
} catch (error) {
- console.error('Failed to get pending ops:', error);
+ console.error("Failed to get pending ops:", error);
return [];
}
}
+ private groupByOwner(nodes: PendingOpNode[]): BranchNode[] {
+ // Group nodes by owner
+ const groupMap = new Map();
+
+ for (const node of nodes) {
+ const owner = node.location?.owner || "Unknown";
+ if (!groupMap.has(owner)) {
+ groupMap.set(owner, []);
+ }
+ groupMap.get(owner)!.push(node);
+ }
+
+ // Convert to owner group nodes
+ const ownerGroups: OwnerGroupNode[] = [];
+ for (const [owner, opsInGroup] of groupMap.entries()) {
+ ownerGroups.push({
+ id: `owner-group-${owner}`,
+ label: owner,
+ type: BranchNodeType.OwnerGroup,
+ contextValue: "owner-group",
+ owner: owner,
+ children: opsInGroup,
+ });
+ }
+
+ // Sort by owner name
+ return ownerGroups.sort((a, b) => a.owner.localeCompare(b.owner));
+ }
+
private extractLocationFromOp(op: string): OpLocation | null {
try {
- if (typeof op === 'string') {
+ if (typeof op === "string") {
const parsed = JSON.parse(op);
// SetTypeName has location
if (parsed.SetTypeName && parsed.SetTypeName[1]) {
const location = parsed.SetTypeName[1];
return {
- owner: location.owner || '',
+ owner: location.owner || "",
modules: location.modules || [],
- name: location.name || ''
+ name: location.name || "",
};
}
@@ -295,9 +459,9 @@ export class WorkspaceTreeDataProvider implements vscode.TreeDataProvider {
const branchName = this.branchStateManager.getCurrentBranchName();
const branchID = this.branchStateManager.getCurrentBranchId();
@@ -325,21 +488,20 @@ export class WorkspaceTreeDataProvider implements vscode.TreeDataProvider>(
- "dark/listInstances",
- {}
- );
+ const instances = await this.client.sendRequest<
+ Array<{ id: string; name: string; url: string }>
+ >("dark/listInstances", {});
instanceChildren = instances.map(inst => ({
id: inst.id,
label: inst.name,
- type: "instance-item" as const,
+ type: BranchNodeType.InstanceItem,
contextValue: "remote-instance",
instanceData: {
url: inst.url,
- status: "connected" as const
+ status: "connected" as const,
},
- children: []
+ children: [],
}));
} catch (error) {
console.error("Failed to fetch instances:", error);
@@ -349,246 +511,35 @@ export class WorkspaceTreeDataProvider implements vscode.TreeDataProvider 0) {
- return `Pending Changes (${filters.join(', ')})`;
- }
- return 'Pending Changes';
+ return "Changes";
}
private getOpsFilterTooltip(): string {
- const parts: string[] = [];
-
- const limitText = this.opsFilter.limit === null ? 'all' : `up to ${this.opsFilter.limit}`;
- parts.push(`Showing ${limitText} ops`);
-
- if (this.opsFilter.dateRange !== 'all') {
- const dateLabel = this.opsFilter.dateRange === 'custom'
- ? `since ${this.opsFilter.customStartDate?.toLocaleDateString()}`
- : `from ${this.opsFilter.dateRange}`;
- parts.push(dateLabel);
- }
-
- // Show branch filter information
- if (this.opsFilter.branch === 'current') {
- parts.push('current branch only');
- } else if (this.opsFilter.branch === 'all') {
- parts.push('all branches');
- } else {
- // It's a specific branch
- const branchLabel = this.getBranchFilterLabel();
- parts.push(`branch: ${branchLabel}`);
- }
-
- if (this.opsFilter.locationFilter) {
- parts.push(`filtered by "${this.opsFilter.locationFilter}"`);
- }
-
- return parts.join(', ');
- }
-
- async configureLimitFilter(): Promise {
- const currentLimit = this.opsFilter.limit === null ? 'all' : this.opsFilter.limit.toString();
- const choice = await vscode.window.showQuickPick([
- { label: '10', value: 10 },
- { label: '25', value: 25 },
- { label: '50', value: 50 },
- { label: '100', value: 100 },
- { label: '500', value: 500 },
- { label: '1000', value: 1000 },
- { label: 'All (no limit)', value: null },
- ], {
- placeHolder: `Current limit: ${currentLimit}`
- });
-
- if (choice) {
- this.opsFilter.limit = choice.value;
- vscode.window.showInformationMessage(`Ops limit set to ${choice.label}`);
- this.refresh();
- }
- }
-
- async configureDateFilter(): Promise {
- type DateRangeOption = {
- label: string;
- value: OpsFilterConfig['dateRange'];
- };
-
- const choice = await vscode.window.showQuickPick([
- { label: 'All time', value: 'all' },
- { label: 'Today', value: 'today' },
- { label: 'Last 7 days', value: 'week' },
- { label: 'Last 30 days', value: 'month' },
- { label: 'Custom date...', value: 'custom' },
- ], {
- placeHolder: `Current: ${this.opsFilter.dateRange}`
- });
-
- if (!choice) {
- return;
- }
-
- this.opsFilter.dateRange = choice.value;
-
- if (choice.value === 'custom') {
- const dateStr = await vscode.window.showInputBox({
- prompt: 'Enter start date (YYYY-MM-DD)',
- placeHolder: '2024-01-01'
- });
-
- if (dateStr) {
- const date = new Date(dateStr);
- if (!isNaN(date.getTime())) {
- this.opsFilter.customStartDate = date;
- vscode.window.showInformationMessage(`Filtering ops since ${dateStr}`);
- } else {
- vscode.window.showErrorMessage('Invalid date format');
- return;
- }
- }
- } else {
- vscode.window.showInformationMessage(`Date filter set to: ${choice.label}`);
- }
-
- this.refresh();
- }
-
- private getBranchFilterLabel(): string {
- if (this.opsFilter.branch === 'all') {
- return 'All branches';
- } else if (this.opsFilter.branch === 'current') {
- return 'Current branch';
- } else {
- // It's a specific branch ID - find the branch name
- const branch = this.branchStateManager.getBranches().find(b => b.id === this.opsFilter.branch);
- return branch ? branch.name : 'Unknown branch';
- }
- }
-
- async configureBranchFilter(): Promise {
- const branches = this.branchStateManager.getBranches();
- const currentBranchId = this.branchStateManager.getCurrentBranchId();
-
- // Build the list of branch options
- const branchItems: BranchQuickPickItem[] = [
- {
- label: '$(git-branch) Current branch only',
- value: 'current',
- description: currentBranchId ? this.branchStateManager.getCurrentBranchName() : 'No branch selected'
- },
- { label: '$(layers) All branches', value: 'all', description: 'Show ops from all branches' },
- { label: '', kind: vscode.QuickPickItemKind.Separator }
- ];
-
- // Add individual branches (only show non-merged branches)
- branches
- .filter(b => !b.mergedAt)
- .forEach(b => {
- const isCurrent = b.id === currentBranchId;
- branchItems.push({
- label: b.name,
- description: isCurrent ? '● Current' : undefined,
- detail: `Branch ID: ${b.id}`,
- branchID: b.id
- });
- });
-
- const choice = await vscode.window.showQuickPick(branchItems, {
- placeHolder: `Current filter: ${this.getBranchFilterLabel()}`
- });
-
- if (choice) {
- if (choice.branchID) {
- this.opsFilter.branch = choice.branchID;
- vscode.window.showInformationMessage(`Branch filter set to: ${choice.label}`);
- } else if (choice.value) {
- this.opsFilter.branch = choice.value;
- vscode.window.showInformationMessage(`Branch filter set to: ${choice.label}`);
- }
- this.refresh();
- }
- }
-
- async configureLocationFilter(): Promise {
- const input = await vscode.window.showInputBox({
- prompt: 'Filter by module/function name (case-insensitive)',
- placeHolder: 'e.g., Stdlib.List or MyModule',
- value: this.opsFilter.locationFilter || ''
- });
-
- if (input !== undefined) {
- this.opsFilter.locationFilter = input.trim() || undefined;
- if (this.opsFilter.locationFilter) {
- vscode.window.showInformationMessage(`Filtering by location: ${this.opsFilter.locationFilter}`);
- } else {
- vscode.window.showInformationMessage('Location filter cleared');
- }
- this.refresh();
- }
- }
-
- async clearAllFilters(): Promise {
- this.opsFilter = {
- limit: 50,
- branch: 'current',
- dateRange: 'all',
- };
- vscode.window.showInformationMessage('All filters cleared');
- this.refresh();
+ return "Pending changes from the current branch";
}
}
diff --git a/vscode-extension/client/src/types/index.ts b/vscode-extension/client/src/types/index.ts
index 6ad038ea59..f53919c0e3 100644
--- a/vscode-extension/client/src/types/index.ts
+++ b/vscode-extension/client/src/types/index.ts
@@ -9,25 +9,22 @@ export interface PackageNode {
packagePath?: string;
}
+export enum BranchNodeType {
+ PendingOp = "pending-op",
+ OwnerGroup = "owner-group",
+ BranchItem = "branch-item",
+ BranchAction = "branch-action",
+ InstanceItem = "instance-item",
+ InstanceRoot = "instance-root",
+ BranchRoot = "branch-root",
+ ChangesRoot = "changes-root",
+ SeeMore = "see-more"
+}
+
export interface BranchNode {
id: string;
label: string;
- type:
- "current"
- | "recent"
- | "shared"
- | "actions"
- | "operation"
- | "conflict"
- | "section"
- | "instance-root"
- | "branch-root"
- | "changes-root"
- | "instance-item"
- | "packages"
- | "branches"
- | "category"
- | "pending-op";
+ type: BranchNodeType;
contextValue: string;
children?: BranchNode[];
instanceData?: {
@@ -38,6 +35,15 @@ export interface BranchNode {
packageCount?: number;
branchCount?: number;
};
+ branchData?: {
+ branchId: string;
+ branchName: string;
+ isCurrent?: boolean;
+ isMain?: boolean;
+ status?: "up-to-date" | "ahead" | "behind";
+ opsCount?: number;
+ lastSynced?: Date;
+ };
}
diff --git a/vscode-extension/package.json b/vscode-extension/package.json
index 42b410fcbf..ebf2242e37 100644
--- a/vscode-extension/package.json
+++ b/vscode-extension/package.json
@@ -128,53 +128,44 @@
"command": "darklang.branch.clear",
"title": "Clear Branch"
},
+ {
+ "command": "darklang.branch.showMenu",
+ "title": "Branch Menu",
+ "icon": "$(ellipsis)"
+ },
{
"command": "darklang.branches.manageAll",
"title": "Manage All Branches",
"icon": "$(list-flat)"
},
{
- "command": "darklang.sync.execute",
- "title": "Darklang: Sync with Remote Instance",
- "icon": "$(sync)"
- },
- {
- "command": "darklang.sync.quickSync",
- "title": "Darklang: Quick Sync (instance2)",
- "icon": "$(sync)"
- },
- {
- "command": "darklang.instance.sync",
- "title": "Sync with Instance",
- "icon": "$(sync)"
- },
- {
- "command": "darklang.ops.setLimit",
- "title": "Set Ops Limit",
- "icon": "$(symbol-number)"
- },
- {
- "command": "darklang.ops.setDateRange",
- "title": "Filter by Date Range",
- "icon": "$(calendar)"
+ "command": "darklang.openHomepage",
+ "title": "Darklang: Open Homepage",
+ "icon": "./static/logo-dark-transparent.svg"
},
{
- "command": "darklang.ops.setBranch",
- "title": "Filter by Branch",
- "icon": "$(git-branch)"
+ "command": "darklang.packages.search",
+ "title": "Search Packages",
+ "icon": "$(search)"
},
{
- "command": "darklang.ops.setLocation",
- "title": "Filter by Location",
- "icon": "$(location)"
+ "command": "darklang.packages.clearSearch",
+ "title": "Clear Search",
+ "icon": "$(clear-all)"
},
{
- "command": "darklang.ops.clearFilters",
- "title": "Clear All Filters",
- "icon": "$(clear-all)"
+ "command": "darklang.changes.review",
+ "title": "Review Changes",
+ "icon": "$(preview)"
}
],
"menus": {
+ "editor/title": [
+ {
+ "command": "darklang.openHomepage",
+ "group": "navigation"
+ }
+ ],
"view/item/context": [
{
"command": "darklang.openFullModule",
@@ -202,34 +193,21 @@
"group": "basic@1"
},
{
- "command": "darklang.instance.sync",
- "when": "view == darklangWorkspace && viewItem == remote-instance",
- "group": "inline@1"
- },
- {
- "command": "darklang.ops.setLimit",
- "when": "view == darklangWorkspace && viewItem == workspace-changes-root",
- "group": "inline@1"
- },
- {
- "command": "darklang.ops.setDateRange",
- "when": "view == darklangWorkspace && viewItem == workspace-changes-root",
- "group": "inline@2"
- },
- {
- "command": "darklang.ops.setBranch",
- "when": "view == darklangWorkspace && viewItem == workspace-changes-root",
- "group": "inline@3"
- },
+ "command": "darklang.branch.rename",
+ "when": "view == darklangWorkspace && viewItem == branch-item",
+ "group": "context@1"
+ }
+ ],
+ "view/title": [
{
- "command": "darklang.ops.setLocation",
- "when": "view == darklangWorkspace && viewItem == workspace-changes-root",
- "group": "inline@4"
+ "command": "darklang.packages.search",
+ "when": "view == darklangPackages",
+ "group": "navigation@1"
},
{
- "command": "darklang.ops.clearFilters",
- "when": "view == darklangWorkspace && viewItem == workspace-changes-root",
- "group": "inline@5"
+ "command": "darklang.packages.clearSearch",
+ "when": "view == darklangPackages",
+ "group": "navigation@2"
}
]
},