🎉 Initial commit: PiAgentUI — AI Agent Desktop & Web UI
Build & Push Frontend Docker Image / build-and-push (push) Canceled after 0s
Build Validation / validate (push) Canceled after 6s
Build & Push Backend Docker Image / build-and-push (push) Failing after 10m10s
Build & Push Gateway Docker Image / build-and-push (push) Failing after 10m9s
Build & Push Frontend Docker Image / build-and-push (push) Canceled after 0s
Build Validation / validate (push) Canceled after 6s
Build & Push Backend Docker Image / build-and-push (push) Failing after 10m10s
Build & Push Gateway Docker Image / build-and-push (push) Failing after 10m9s
A professional workspace for AI coding agents with: - Real-time streaming chat with SSE - Session management (fork, clone, tree navigation) - MCP tools browser & metadata panel - Skills browser (global, project, package sources) - Voice input & STT provider configuration - Integrated terminal & file explorer - Model selector with capability metadata - Keyboard-first UI with command palette - 8 languages (en, pt-BR, es, ja, zh-CN, ar, bn, hi) - Tauri 2 desktop app - React 19 + TypeScript + Vite 8 + Tailwind CSS 4 - Pi Agent extension runtime Originally forked from OpenCodeUI, now an independent project. License: GPL-3.0-only
This commit is contained in:
commit
b3489e1ede
983 files changed
+158475
No files matched your search
@@ -0,0 +1,259 @@
|
||||
import { spawn } from 'node:child_process'
|
||||
|
||||
export interface GitCommandResult {
|
||||
stdout: string
|
||||
stderr: string
|
||||
}
|
||||
|
||||
export type GitCommandRunner = (args: string[], options: { cwd: string }) => Promise<GitCommandResult>
|
||||
|
||||
export interface GitBranchInfo {
|
||||
name: string
|
||||
current: boolean
|
||||
remote: boolean
|
||||
upstream?: string
|
||||
commit?: string
|
||||
}
|
||||
|
||||
export interface GitBranchesResult {
|
||||
current?: string
|
||||
branches: GitBranchInfo[]
|
||||
}
|
||||
|
||||
export interface GitStatusFile {
|
||||
path: string
|
||||
index: string
|
||||
workingTree: string
|
||||
staged: boolean
|
||||
unstaged: boolean
|
||||
untracked: boolean
|
||||
}
|
||||
|
||||
export interface GitStatusResult {
|
||||
branch?: string
|
||||
upstream?: string
|
||||
ahead: number
|
||||
behind: number
|
||||
dirty: boolean
|
||||
files: GitStatusFile[]
|
||||
}
|
||||
|
||||
export interface GitActionResult {
|
||||
ok: true
|
||||
stdout?: string
|
||||
stderr?: string
|
||||
}
|
||||
|
||||
export interface CheckoutGitBranchRequest {
|
||||
branch: string
|
||||
}
|
||||
|
||||
export interface CreateGitBranchRequest {
|
||||
name: string
|
||||
}
|
||||
|
||||
export interface GitPathsRequest {
|
||||
paths: string[]
|
||||
}
|
||||
|
||||
export interface CommitGitChangesRequest {
|
||||
message: string
|
||||
}
|
||||
|
||||
export const runGit: GitCommandRunner = (args, options) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const child = spawn('git', args, { cwd: options.cwd, shell: false })
|
||||
let stdout = ''
|
||||
let stderr = ''
|
||||
|
||||
child.stdout.setEncoding('utf8')
|
||||
child.stderr.setEncoding('utf8')
|
||||
child.stdout.on('data', chunk => {
|
||||
stdout += chunk
|
||||
})
|
||||
child.stderr.on('data', chunk => {
|
||||
stderr += chunk
|
||||
})
|
||||
child.on('error', reject)
|
||||
child.on('close', code => {
|
||||
if (code === 0) {
|
||||
resolve({ stdout, stderr })
|
||||
return
|
||||
}
|
||||
reject(new Error((stderr || stdout || `git ${args.join(' ')} failed with exit code ${code}`).trim()))
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
function assertSafeRefName(value: string, label: string): string {
|
||||
const name = value.trim()
|
||||
if (!name) throw new Error(`${label} is required`)
|
||||
if (name.startsWith('-')) throw new Error(`Invalid ${label}`)
|
||||
if (/\s/.test(name) || /[\0~^:?*[\]\\]/.test(name) || name.includes('..') || name.includes('@{')) {
|
||||
throw new Error(`Invalid ${label}`)
|
||||
}
|
||||
return name
|
||||
}
|
||||
|
||||
function assertSafePath(value: string): string {
|
||||
const filePath = value.trim()
|
||||
if (!filePath) throw new Error('Git path is required')
|
||||
if (filePath.includes('\0')) throw new Error('Invalid Git path')
|
||||
return filePath
|
||||
}
|
||||
|
||||
function normalizeRemoteName(name: string): string {
|
||||
return name.replace(/^remotes\//, '')
|
||||
}
|
||||
|
||||
export async function listGitBranches(cwd: string, runner: GitCommandRunner = runGit): Promise<GitBranchesResult> {
|
||||
const { stdout } = await runner(
|
||||
['branch', '--format=%(refname:short)|%(upstream:short)|%(objectname:short)|%(HEAD)', '--all'],
|
||||
{
|
||||
cwd,
|
||||
},
|
||||
)
|
||||
|
||||
const branches = stdout
|
||||
.split(/\r?\n/)
|
||||
.map(line => line.trim())
|
||||
.filter(Boolean)
|
||||
.map(line => {
|
||||
const [rawName, upstream, commit, head] = line.split('|')
|
||||
const name = normalizeRemoteName(rawName)
|
||||
return {
|
||||
name,
|
||||
current: head === '*',
|
||||
remote: rawName.startsWith('remotes/') || name.startsWith('origin/'),
|
||||
upstream: upstream || undefined,
|
||||
commit: commit || undefined,
|
||||
}
|
||||
})
|
||||
.filter(branch => branch.name !== 'origin/HEAD')
|
||||
|
||||
return { current: branches.find(branch => branch.current)?.name, branches }
|
||||
}
|
||||
|
||||
function parseStatusPath(line: string): string {
|
||||
if (line.includes('\t')) return line.split('\t').pop() || ''
|
||||
const parts = line.split(' ')
|
||||
return parts[parts.length - 1] || ''
|
||||
}
|
||||
|
||||
export async function getGitStatus(cwd: string, runner: GitCommandRunner = runGit): Promise<GitStatusResult> {
|
||||
const { stdout } = await runner(['status', '--porcelain=v2', '--branch'], { cwd })
|
||||
const status: GitStatusResult = { ahead: 0, behind: 0, dirty: false, files: [] }
|
||||
|
||||
for (const line of stdout.split(/\r?\n/)) {
|
||||
if (!line) continue
|
||||
if (line.startsWith('# branch.head ')) {
|
||||
const branch = line.slice('# branch.head '.length).trim()
|
||||
if (branch && branch !== '(detached)') status.branch = branch
|
||||
continue
|
||||
}
|
||||
if (line.startsWith('# branch.upstream ')) {
|
||||
status.upstream = line.slice('# branch.upstream '.length).trim()
|
||||
continue
|
||||
}
|
||||
if (line.startsWith('# branch.ab ')) {
|
||||
const match = line.match(/\+(\d+)\s+-(\d+)/)
|
||||
if (match) {
|
||||
status.ahead = Number(match[1])
|
||||
status.behind = Number(match[2])
|
||||
}
|
||||
continue
|
||||
}
|
||||
if (line.startsWith('1 ') || line.startsWith('2 ')) {
|
||||
const xy = line.slice(2, 4)
|
||||
const file = {
|
||||
path: parseStatusPath(line),
|
||||
index: xy[0],
|
||||
workingTree: xy[1],
|
||||
staged: xy[0] !== '.',
|
||||
unstaged: xy[1] !== '.',
|
||||
untracked: false,
|
||||
}
|
||||
status.files.push(file)
|
||||
continue
|
||||
}
|
||||
if (line.startsWith('? ')) {
|
||||
status.files.push({
|
||||
path: line.slice(2).trim(),
|
||||
index: '?',
|
||||
workingTree: '?',
|
||||
staged: false,
|
||||
unstaged: false,
|
||||
untracked: true,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
status.dirty = status.files.length > 0
|
||||
return status
|
||||
}
|
||||
|
||||
export async function checkoutGitBranch(
|
||||
cwd: string,
|
||||
request: CheckoutGitBranchRequest,
|
||||
runner: GitCommandRunner = runGit,
|
||||
): Promise<GitActionResult> {
|
||||
const branch = assertSafeRefName(request.branch, 'Branch')
|
||||
const result = await runner(['checkout', branch], { cwd })
|
||||
return { ok: true, ...result }
|
||||
}
|
||||
|
||||
export async function createGitBranch(
|
||||
cwd: string,
|
||||
request: CreateGitBranchRequest,
|
||||
runner: GitCommandRunner = runGit,
|
||||
): Promise<GitActionResult> {
|
||||
const name = assertSafeRefName(request.name, 'Branch name')
|
||||
const result = await runner(['checkout', '-b', name], { cwd })
|
||||
return { ok: true, ...result }
|
||||
}
|
||||
|
||||
function normalizePaths(request: GitPathsRequest): string[] {
|
||||
if (!Array.isArray(request.paths) || request.paths.length === 0) throw new Error('At least one path is required')
|
||||
return request.paths.map(assertSafePath)
|
||||
}
|
||||
|
||||
export async function stageGitPaths(
|
||||
cwd: string,
|
||||
request: GitPathsRequest,
|
||||
runner: GitCommandRunner = runGit,
|
||||
): Promise<GitActionResult> {
|
||||
const paths = normalizePaths(request)
|
||||
const result = await runner(['add', '--', ...paths], { cwd })
|
||||
return { ok: true, ...result }
|
||||
}
|
||||
|
||||
export async function unstageGitPaths(
|
||||
cwd: string,
|
||||
request: GitPathsRequest,
|
||||
runner: GitCommandRunner = runGit,
|
||||
): Promise<GitActionResult> {
|
||||
const paths = normalizePaths(request)
|
||||
const result = await runner(['restore', '--staged', '--', ...paths], { cwd })
|
||||
return { ok: true, ...result }
|
||||
}
|
||||
|
||||
export async function commitGitChanges(
|
||||
cwd: string,
|
||||
request: CommitGitChangesRequest,
|
||||
runner: GitCommandRunner = runGit,
|
||||
): Promise<GitActionResult> {
|
||||
const message = request.message.trim()
|
||||
if (!message) throw new Error('Commit message is required')
|
||||
const result = await runner(['commit', '-m', message], { cwd })
|
||||
return { ok: true, ...result }
|
||||
}
|
||||
|
||||
export async function pullGitBranch(cwd: string, runner: GitCommandRunner = runGit): Promise<GitActionResult> {
|
||||
const result = await runner(['pull', '--ff-only'], { cwd })
|
||||
return { ok: true, ...result }
|
||||
}
|
||||
|
||||
export async function pushGitBranch(cwd: string, runner: GitCommandRunner = runGit): Promise<GitActionResult> {
|
||||
const result = await runner(['push'], { cwd })
|
||||
return { ok: true, ...result }
|
||||
}
|
||||
Reference in new issue
Block a user