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
|
loop *agent.AgentLoop
|
||||||
workspace string
|
workspace string
|
||||||
|
|
||||||
gitCache miniapp.GitInfo
|
gitCache []miniapp.GitInfo
|
||||||
gitCacheAt time.Time
|
gitCacheAt time.Time
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -375,29 +375,46 @@ func (p *agentLoopDataProvider) GetActiveSessions() []miniapp.SessionInfo {
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *agentLoopDataProvider) GetGitInfo() miniapp.GitInfo {
|
func (p *agentLoopDataProvider) GetGitInfo() []miniapp.GitInfo {
|
||||||
if time.Since(p.gitCacheAt) < gitCacheTTL {
|
if time.Since(p.gitCacheAt) < gitCacheTTL {
|
||||||
return p.gitCache
|
return p.gitCache
|
||||||
}
|
}
|
||||||
|
|
||||||
info := miniapp.GitInfo{}
|
|
||||||
if p.workspace == "" {
|
if p.workspace == "" {
|
||||||
return info
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Detect git root from workspace
|
// Find workspace's own git root to exclude it
|
||||||
topOut, err := exec.Command("git", "-C", p.workspace, "rev-parse", "--show-toplevel").Output()
|
workspaceGitRoot := ""
|
||||||
if err != nil {
|
if out, err := exec.Command("git", "-C", p.workspace, "rev-parse", "--show-toplevel").Output(); err == nil {
|
||||||
return info // not a git repo
|
workspaceGitRoot = strings.TrimSpace(string(out))
|
||||||
}
|
}
|
||||||
gitRoot := strings.TrimSpace(string(topOut))
|
|
||||||
|
|
||||||
// Exclude ~/.picoclaw/workspace* (picoclaw's own managed workspace)
|
// Scan for .git dirs up to 2 levels deep under workspace
|
||||||
home, _ := os.UserHomeDir()
|
seen := map[string]bool{}
|
||||||
picoDir := filepath.Join(home, ".picoclaw")
|
var repos []miniapp.GitInfo
|
||||||
if strings.HasPrefix(gitRoot, picoDir) {
|
for _, pattern := range []string{
|
||||||
return info
|
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
|
// Current branch
|
||||||
out, err := exec.Command("git", "-C", gitRoot, "rev-parse", "--abbrev-ref", "HEAD").Output()
|
out, err := exec.Command("git", "-C", gitRoot, "rev-parse", "--abbrev-ref", "HEAD").Output()
|
||||||
|
|
@ -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()
|
out, err = exec.Command("git", "-C", gitRoot, "status", "--porcelain").Output()
|
||||||
if err == nil && len(out) > 0 {
|
if err == nil && len(out) > 0 {
|
||||||
for _, line := range strings.Split(strings.TrimRight(string(out), "\n"), "\n") {
|
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
|
return info
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -60,6 +60,7 @@ type SessionInfo struct {
|
||||||
|
|
||||||
// GitInfo represents the git repository state exposed via the API.
|
// GitInfo represents the git repository state exposed via the API.
|
||||||
type GitInfo struct {
|
type GitInfo struct {
|
||||||
|
Name string `json:"name"`
|
||||||
Branch string `json:"branch"`
|
Branch string `json:"branch"`
|
||||||
Commits []GitCommit `json:"commits"`
|
Commits []GitCommit `json:"commits"`
|
||||||
Modified []GitChange `json:"modified"`
|
Modified []GitChange `json:"modified"`
|
||||||
|
|
@ -85,7 +86,7 @@ type DataProvider interface {
|
||||||
GetPlanInfo() PlanInfo
|
GetPlanInfo() PlanInfo
|
||||||
GetSessionStats() *stats.Stats
|
GetSessionStats() *stats.Stats
|
||||||
GetActiveSessions() []SessionInfo
|
GetActiveSessions() []SessionInfo
|
||||||
GetGitInfo() GitInfo
|
GetGitInfo() []GitInfo
|
||||||
}
|
}
|
||||||
|
|
||||||
// CommandSender injects a command into the message bus on behalf of a user.
|
// 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 {
|
func (m *mockDataProvider) GetActiveSessions() []SessionInfo {
|
||||||
return []SessionInfo{}
|
return []SessionInfo{}
|
||||||
}
|
}
|
||||||
func (m *mockDataProvider) GetGitInfo() GitInfo {
|
func (m *mockDataProvider) GetGitInfo() []GitInfo {
|
||||||
return GitInfo{}
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
type mockSender struct{}
|
type mockSender struct{}
|
||||||
|
|
@ -448,8 +448,8 @@ func (m *mutatingDataProvider) GetSessionStats() *stats.Stats { return nil }
|
||||||
func (m *mutatingDataProvider) GetActiveSessions() []SessionInfo {
|
func (m *mutatingDataProvider) GetActiveSessions() []SessionInfo {
|
||||||
return []SessionInfo{}
|
return []SessionInfo{}
|
||||||
}
|
}
|
||||||
func (m *mutatingDataProvider) GetGitInfo() GitInfo {
|
func (m *mutatingDataProvider) GetGitInfo() []GitInfo {
|
||||||
return GitInfo{}
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// drainEvents reads SSE event lines until it collects `want` distinct event names or times out.
|
// drainEvents reads SSE event lines until it collects `want` distinct event names or times out.
|
||||||
|
|
|
||||||
|
|
@ -1123,17 +1123,35 @@ function loadGit() {
|
||||||
renderGitFromData);
|
renderGitFromData);
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderGitFromData(data) {
|
function renderGitFromData(repos) {
|
||||||
var loading = document.getElementById('git-loading');
|
var loading = document.getElementById('git-loading');
|
||||||
var el = document.getElementById('git-content');
|
var el = document.getElementById('git-content');
|
||||||
loading.classList.add('hidden');
|
loading.classList.add('hidden');
|
||||||
el.classList.remove('hidden');
|
el.classList.remove('hidden');
|
||||||
|
|
||||||
var html = '<div class="card glass"><div class="card-title">Branch: ' +
|
if (!repos || repos.length === 0) {
|
||||||
escapeHtml(data.branch || 'unknown') + '</div>';
|
el.innerHTML = '<div class="empty-state">No git repositories found.</div>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (data.commits && data.commits.length > 0) {
|
var html = '';
|
||||||
data.commits.forEach(function(c) {
|
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">' +
|
html += '<div class="git-commit">' +
|
||||||
'<span class="git-hash">' + escapeHtml(c.hash) + '</span>' +
|
'<span class="git-hash">' + escapeHtml(c.hash) + '</span>' +
|
||||||
'<span class="git-subject">' + escapeHtml(c.subject) + '</span>' +
|
'<span class="git-subject">' + escapeHtml(c.subject) + '</span>' +
|
||||||
|
|
@ -1144,17 +1162,7 @@ function renderGitFromData(data) {
|
||||||
html += '<div style="padding:12px;color:var(--hint)">No commits found.</div>';
|
html += '<div style="padding:12px;color:var(--hint)">No commits found.</div>';
|
||||||
}
|
}
|
||||||
html += '</div>';
|
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>';
|
|
||||||
});
|
});
|
||||||
html += '</div>';
|
|
||||||
}
|
|
||||||
|
|
||||||
el.innerHTML = html;
|
el.innerHTML = html;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue