Merge branch 'feature/miniapp-git-tab'
This commit is contained in:
commit
4666df7402
4 changed files with 70 additions and 46 deletions
|
|
@ -312,7 +312,7 @@ type agentLoopDataProvider struct {
|
|||
loop *agent.AgentLoop
|
||||
workspace string
|
||||
|
||||
gitCache miniapp.GitInfo
|
||||
gitCache []miniapp.GitInfo
|
||||
gitCacheAt time.Time
|
||||
}
|
||||
|
||||
|
|
@ -375,30 +375,47 @@ func (p *agentLoopDataProvider) GetActiveSessions() []miniapp.SessionInfo {
|
|||
return result
|
||||
}
|
||||
|
||||
func (p *agentLoopDataProvider) GetGitInfo() miniapp.GitInfo {
|
||||
func (p *agentLoopDataProvider) GetGitInfo() []miniapp.GitInfo {
|
||||
if time.Since(p.gitCacheAt) < gitCacheTTL {
|
||||
return p.gitCache
|
||||
}
|
||||
|
||||
info := miniapp.GitInfo{}
|
||||
if p.workspace == "" {
|
||||
return info
|
||||
return nil
|
||||
}
|
||||
|
||||
// Detect git root from workspace
|
||||
topOut, err := exec.Command("git", "-C", p.workspace, "rev-parse", "--show-toplevel").Output()
|
||||
if err != nil {
|
||||
return info // not a git repo
|
||||
// Find workspace's own git root to exclude it
|
||||
workspaceGitRoot := ""
|
||||
if out, err := exec.Command("git", "-C", p.workspace, "rev-parse", "--show-toplevel").Output(); err == nil {
|
||||
workspaceGitRoot = strings.TrimSpace(string(out))
|
||||
}
|
||||
gitRoot := strings.TrimSpace(string(topOut))
|
||||
|
||||
// Exclude ~/.picoclaw/workspace* (picoclaw's own managed workspace)
|
||||
home, _ := os.UserHomeDir()
|
||||
picoDir := filepath.Join(home, ".picoclaw")
|
||||
if strings.HasPrefix(gitRoot, picoDir) {
|
||||
return info
|
||||
// Scan for .git dirs up to 2 levels deep under workspace
|
||||
seen := map[string]bool{}
|
||||
var repos []miniapp.GitInfo
|
||||
for _, pattern := range []string{
|
||||
filepath.Join(p.workspace, "*", ".git"),
|
||||
filepath.Join(p.workspace, "*", "*", ".git"),
|
||||
} {
|
||||
matches, _ := filepath.Glob(pattern)
|
||||
for _, m := range matches {
|
||||
repoDir := filepath.Dir(m)
|
||||
if repoDir == workspaceGitRoot || seen[repoDir] {
|
||||
continue
|
||||
}
|
||||
seen[repoDir] = true
|
||||
repos = append(repos, collectGitRepoInfo(repoDir))
|
||||
}
|
||||
}
|
||||
|
||||
p.gitCache = repos
|
||||
p.gitCacheAt = time.Now()
|
||||
return repos
|
||||
}
|
||||
|
||||
func collectGitRepoInfo(gitRoot string) miniapp.GitInfo {
|
||||
info := miniapp.GitInfo{Name: filepath.Base(gitRoot)}
|
||||
|
||||
// Current branch
|
||||
out, err := exec.Command("git", "-C", gitRoot, "rev-parse", "--abbrev-ref", "HEAD").Output()
|
||||
if err == nil {
|
||||
|
|
@ -418,7 +435,7 @@ func (p *agentLoopDataProvider) GetGitInfo() miniapp.GitInfo {
|
|||
}
|
||||
}
|
||||
|
||||
// Modified/untracked files (git status --porcelain)
|
||||
// Modified/untracked files
|
||||
out, err = exec.Command("git", "-C", gitRoot, "status", "--porcelain").Output()
|
||||
if err == nil && len(out) > 0 {
|
||||
for _, line := range strings.Split(strings.TrimRight(string(out), "\n"), "\n") {
|
||||
|
|
@ -432,8 +449,6 @@ func (p *agentLoopDataProvider) GetGitInfo() miniapp.GitInfo {
|
|||
}
|
||||
}
|
||||
|
||||
p.gitCache = info
|
||||
p.gitCacheAt = time.Now()
|
||||
return info
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -60,6 +60,7 @@ type SessionInfo struct {
|
|||
|
||||
// GitInfo represents the git repository state exposed via the API.
|
||||
type GitInfo struct {
|
||||
Name string `json:"name"`
|
||||
Branch string `json:"branch"`
|
||||
Commits []GitCommit `json:"commits"`
|
||||
Modified []GitChange `json:"modified"`
|
||||
|
|
@ -85,7 +86,7 @@ type DataProvider interface {
|
|||
GetPlanInfo() PlanInfo
|
||||
GetSessionStats() *stats.Stats
|
||||
GetActiveSessions() []SessionInfo
|
||||
GetGitInfo() GitInfo
|
||||
GetGitInfo() []GitInfo
|
||||
}
|
||||
|
||||
// CommandSender injects a command into the message bus on behalf of a user.
|
||||
|
|
|
|||
|
|
@ -171,8 +171,8 @@ func (m *mockDataProvider) GetSessionStats() *stats.Stats {
|
|||
func (m *mockDataProvider) GetActiveSessions() []SessionInfo {
|
||||
return []SessionInfo{}
|
||||
}
|
||||
func (m *mockDataProvider) GetGitInfo() GitInfo {
|
||||
return GitInfo{}
|
||||
func (m *mockDataProvider) GetGitInfo() []GitInfo {
|
||||
return nil
|
||||
}
|
||||
|
||||
type mockSender struct{}
|
||||
|
|
@ -448,8 +448,8 @@ func (m *mutatingDataProvider) GetSessionStats() *stats.Stats { return nil }
|
|||
func (m *mutatingDataProvider) GetActiveSessions() []SessionInfo {
|
||||
return []SessionInfo{}
|
||||
}
|
||||
func (m *mutatingDataProvider) GetGitInfo() GitInfo {
|
||||
return GitInfo{}
|
||||
func (m *mutatingDataProvider) GetGitInfo() []GitInfo {
|
||||
return nil
|
||||
}
|
||||
|
||||
// drainEvents reads SSE event lines until it collects `want` distinct event names or times out.
|
||||
|
|
|
|||
|
|
@ -1123,38 +1123,46 @@ function loadGit() {
|
|||
renderGitFromData);
|
||||
}
|
||||
|
||||
function renderGitFromData(data) {
|
||||
function renderGitFromData(repos) {
|
||||
var loading = document.getElementById('git-loading');
|
||||
var el = document.getElementById('git-content');
|
||||
loading.classList.add('hidden');
|
||||
el.classList.remove('hidden');
|
||||
|
||||
var html = '<div class="card glass"><div class="card-title">Branch: ' +
|
||||
escapeHtml(data.branch || 'unknown') + '</div>';
|
||||
|
||||
if (data.commits && data.commits.length > 0) {
|
||||
data.commits.forEach(function(c) {
|
||||
html += '<div class="git-commit">' +
|
||||
'<span class="git-hash">' + escapeHtml(c.hash) + '</span>' +
|
||||
'<span class="git-subject">' + escapeHtml(c.subject) + '</span>' +
|
||||
'<span class="git-meta">' + escapeHtml(c.date) + '</span>' +
|
||||
'</div>';
|
||||
});
|
||||
} else {
|
||||
html += '<div style="padding:12px;color:var(--hint)">No commits found.</div>';
|
||||
if (!repos || repos.length === 0) {
|
||||
el.innerHTML = '<div class="empty-state">No git repositories found.</div>';
|
||||
return;
|
||||
}
|
||||
html += '</div>';
|
||||
|
||||
if (data.modified && data.modified.length > 0) {
|
||||
html += '<div class="card glass" style="margin-top:12px"><div class="card-title">Changes (' + data.modified.length + ')</div>';
|
||||
data.modified.forEach(function(f) {
|
||||
html += '<div class="git-commit">' +
|
||||
'<span class="git-status git-status-' + (f.status === '??' ? 'u' : f.status.toLowerCase()) + '">' + escapeHtml(f.status) + '</span>' +
|
||||
'<span class="git-subject">' + escapeHtml(f.path) + '</span>' +
|
||||
'</div>';
|
||||
});
|
||||
var html = '';
|
||||
repos.forEach(function(repo) {
|
||||
html += '<div class="card glass"><div class="card-title">' +
|
||||
escapeHtml(repo.name) + ' — ' + escapeHtml(repo.branch || '?') + '</div>';
|
||||
|
||||
if (repo.modified && repo.modified.length > 0) {
|
||||
html += '<div style="padding:4px 12px 8px;font-size:12px;color:var(--hint)">Changes (' + repo.modified.length + ')</div>';
|
||||
repo.modified.forEach(function(f) {
|
||||
html += '<div class="git-commit">' +
|
||||
'<span class="git-status git-status-' + (f.status === '??' ? 'u' : f.status.toLowerCase()) + '">' + escapeHtml(f.status) + '</span>' +
|
||||
'<span class="git-subject">' + escapeHtml(f.path) + '</span>' +
|
||||
'</div>';
|
||||
});
|
||||
}
|
||||
|
||||
if (repo.commits && repo.commits.length > 0) {
|
||||
html += '<div style="padding:4px 12px 8px;font-size:12px;color:var(--hint)">Commits</div>';
|
||||
repo.commits.forEach(function(c) {
|
||||
html += '<div class="git-commit">' +
|
||||
'<span class="git-hash">' + escapeHtml(c.hash) + '</span>' +
|
||||
'<span class="git-subject">' + escapeHtml(c.subject) + '</span>' +
|
||||
'<span class="git-meta">' + escapeHtml(c.date) + '</span>' +
|
||||
'</div>';
|
||||
});
|
||||
} else {
|
||||
html += '<div style="padding:12px;color:var(--hint)">No commits found.</div>';
|
||||
}
|
||||
html += '</div>';
|
||||
}
|
||||
});
|
||||
|
||||
el.innerHTML = html;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue