diff --git a/UI/git_sidebar/gitman.js b/UI/git_sidebar/gitman.js index e85cea7..dcfabc7 100644 --- a/UI/git_sidebar/gitman.js +++ b/UI/git_sidebar/gitman.js @@ -1,94 +1,47 @@ -const { exec, execFile } = require("child_process"); - class GitManager { constructor(options = {}) { - // Directory where Git commands will run - this.cwd = options.cwd || process.cwd(); - - // Optional: enable verbose logging - this.verbose = options.verbose || false; - } - - run(cmd) { - return new Promise((resolve, reject) => { - exec(cmd, { cwd: this.cwd }, (err, stdout, stderr) => { - if (this.verbose) { - console.log("[GitManager] CMD: ", cmd); - console.log("[GitManager] OUT: ", stdout); - console.log("[GitManager] ERR: ", stderr); - } - if (err) { - const msg = stdout.trim() || stderr.trim(); - reject(msg); - return; - } - resolve(stdout.trim()); - }); - }); - } - - runGit(operation, name) { - return new Promise((resolve, reject) => { - execFile("git", [operation, name], { cwd: this.cwd }, (err, stdout, stderr) => { - if (this.verbose) { - console.log("[GitManager] CMD: ", "git", operation, name); - console.log("[GitManager] OUT: ", stdout); - console.log("[GitManager] ERR: ", stderr); - } - if (err) { - const msg = stdout.trim() || stderr.trim(); - reject(msg); - return; - } - resolve(stdout.trim()); - }); - }); + this.api = options.api; + this.workspace = options.workspace || null; } - async init() { // Function to handle git init - return this.run("git init"); + setWorkspace(workspace) { + this.workspace = workspace || null; } - async addAll() { // Function to handle git add . - return this.run("git add ."); + async run(operation, ...args) { + if (!this.workspace) throw new Error('Open a folder to use Git.'); + if (!this.api || typeof this.api.run !== 'function') throw new Error('Git API is unavailable.'); + const result = await this.api.run(operation, this.workspace, args); + if (!result.ok) { + const error = new Error(result.stderr || result.stdout || 'Git command failed.'); + error.code = result.code; + throw error; + } + return result.stdout; } - async commit(message) { // Function to handle git commit -m "" - return new Promise((resolve, reject) => { - execFile("git", ["commit", "-m", message], { cwd: this.cwd }, (err, stdout, stderr) => { - if (this.verbose) { - console.log("[GitManager] CMD: ", "git commit -m", message); - console.log("[GitManager] OUT: ", stdout); - console.log("[GitManager] ERR: ", stderr); - } - if (err) { - const msg = stdout.trim() || stderr.trim(); - reject(msg); - return; - } - resolve(stdout.trim()); - }); - }); + async init() { + return this.run('init'); } - async status() { // Function to handle git status - return this.run("git status --short"); + async addAll() { + return this.run('addAll'); } - async push() { // Function to handle git push - return this.run("git push"); + async commit(message) { + return this.run('commit', message); } - async pull() { // Function to handle git pull - return this.run("git pull"); + async status() { + return this.run('status'); } - async branch(name) { // Function to handle git branch - return this.runGit("branch", name); + async push() { + return this.run('push'); } - async switch(name) { // Function to handle git switch branch - return this.runGit("switch", name); + async pull() { + return this.run('pull'); } } diff --git a/UI/git_sidebar/gitman.test.js b/UI/git_sidebar/gitman.test.js new file mode 100644 index 0000000..8c82933 --- /dev/null +++ b/UI/git_sidebar/gitman.test.js @@ -0,0 +1,16 @@ +const assert = require('node:assert/strict'); +const test = require('node:test'); +const GitManager = require('./gitman'); + +test('passes the selected workspace and arguments to the Git bridge', async () => { + let request; + const manager = new GitManager({ api: { run: async (...args) => { request = args; return { ok: true, stdout: 'done' }; } } }); + manager.setWorkspace('/selected/workspace'); + assert.equal(await manager.commit('message with spaces'), 'done'); + assert.deepEqual(request, ['commit', '/selected/workspace', ['message with spaces']]); +}); + +test('rejects structured bridge errors', async () => { + const manager = new GitManager({ workspace: '/workspace', api: { run: async () => ({ ok: false, stderr: 'failed', code: 1 }) } }); + await assert.rejects(manager.status(), /failed/); +}); diff --git a/UI/git_sidebar/gitstub.js b/UI/git_sidebar/gitstub.js index 1a0d3c1..2db84e7 100644 --- a/UI/git_sidebar/gitstub.js +++ b/UI/git_sidebar/gitstub.js @@ -1,10 +1,10 @@ class StatusStub { static parse(raw) { - if (!raw.trim()) { + if (!raw || !raw.trim()) { return []; } - return raw.split("\n").map(line => { + return raw.split(/\r?\n/).filter(Boolean).map(line => { const code = line.slice(0, 2); const file = line.slice(2).trim(); return { code, file }; @@ -47,4 +47,4 @@ module.exports = { StatusStub, BranchStub, LogStub -} +}; diff --git a/UI/git_sidebar/gitstub.test.js b/UI/git_sidebar/gitstub.test.js new file mode 100644 index 0000000..17f1999 --- /dev/null +++ b/UI/git_sidebar/gitstub.test.js @@ -0,0 +1,10 @@ +const assert = require('node:assert/strict'); +const test = require('node:test'); +const { StatusStub } = require('./gitstub'); + +test('parses modified, added, deleted, renamed, and untracked files', () => { + assert.deepEqual(StatusStub.parse(' M modified.js\nA added.js\n D deleted.js\nR old.js -> new.js\n?? untracked.js'), [ + { code: ' M', file: 'modified.js' }, { code: 'A ', file: 'added.js' }, { code: ' D', file: 'deleted.js' }, + { code: 'R ', file: 'old.js -> new.js' }, { code: '??', file: 'untracked.js' } + ]); +}); diff --git a/UI/git_sidebar/gitui.js b/UI/git_sidebar/gitui.js index 156eb31..5b0bc14 100644 --- a/UI/git_sidebar/gitui.js +++ b/UI/git_sidebar/gitui.js @@ -1,60 +1,135 @@ const GitManager = require('./gitman'); -const { generateUnifiedDiff } = require('../../patchgen'); +const { StatusStub } = require('./gitstub'); class GitUi { - constructor(containerEl, gitBar) { - this.gitman = new GitManager(); - this.containerEl = containerEl; - this.patch = generateUnifiedDiff; - this.gitBar = gitBar; + constructor(containerEl, options = {}) { + this.containerEl = containerEl; + this.gitman = options.gitman || new GitManager({ api: options.gitApi }); + this.workspace = null; + this.state = { mode: 'empty', entries: [], message: 'Open a folder to use Git.', messageType: 'info' }; + this.listening = false; + } + + async setWorkspace(workspace) { + this.workspace = workspace || null; + this.gitman.setWorkspace(this.workspace); + if (!this.workspace) { + this.state = { mode: 'empty', entries: [], message: 'Open a folder to use Git.', messageType: 'info' }; + this.render(); + return; + } + await this.refresh(); + } + + initListeners() { + if (!this.containerEl || this.listening) return; + this.listening = true; + this.containerEl.addEventListener('click', (event) => this.handleClick(event)); + this.containerEl.addEventListener('input', (event) => { + if (event.target.id === 'git-commit-message') this.renderActionState(); + }); + } + + init_listners() { this.initListeners(); } + + async handleClick(event) { + const action = event.target.dataset?.gitAction; + if (!action) return; + if (action === 'refresh') return this.refresh(); + const operations = { init: 'init', stage: 'addAll', commit: 'commit', pull: 'pull', push: 'push' }; + const method = operations[action]; + if (!method) return; + const message = action === 'commit' ? this.containerEl.querySelector('#git-commit-message')?.value.trim() : undefined; + if (action === 'commit' && !message) return; + await this.runAction(method, message); + } + + async runAction(method, argument) { + this.setMessage(`${this.actionLabel(method)}…`, 'loading'); + try { + const output = await this.gitman[method](argument); + await this.refresh(`${this.actionLabel(method)} completed${output ? `: ${output}` : '.'}`); + } catch (error) { + this.setMessage(error.message || String(error), 'error'); } + } - init_listners() { - this.containerEl.addEventListener('click', (e) => { - if (e.target.id === 'git-init-btn') { - this.gitman.init().then((output) => { - console.log("[GitUi MSG] : ", output); - }); - } - }); - - this.containerEl.addEventListener('click', (e) => { - if (e.target.id === 'git-add-btn') { - this.gitman.addAll().then((output) => { - console.log("[GitUi MSG] : ", output); - }); - } - }); - - this.containerEl.addEventListener('click', (e) => { - if (e.target.id === 'git-patch-btn') { - const oldFile = this.containerEl.querySelector('#git-old-file').files[0]; - const newFile = this.containerEl.querySelector('#git-new-file').files[0]; - if (!oldFile || !newFile) return; - - const oldFilePath = this.gitBar.getPathForFile(oldFile); - const newFilePath = this.gitBar.getPathForFile(newFile); - const output = this.patch(oldFilePath, newFilePath); - console.log("[GitUi MSG] : ", output); - } - }); + actionLabel(method) { + return { init: 'Initializing repository', addAll: 'Staging files', commit: 'Committing changes', pull: 'Pulling changes', push: 'Pushing changes' }[method]; + } + + async refresh(successMessage = '') { + if (!this.workspace) return; + this.state = { ...this.state, mode: 'loading', message: 'Loading repository status…', messageType: 'loading' }; + this.render(); + try { + const raw = await this.gitman.status(); + this.state = { mode: 'repository', entries: StatusStub.parse(raw), message: successMessage || 'Repository status is up to date.', messageType: successMessage ? 'success' : 'info' }; + } catch (error) { + if (/not a git repository/i.test(error.message || '')) { + this.state = { mode: 'non-repository', entries: [], message: 'This folder is not a Git repository.', messageType: 'info' }; + } else { + this.state = { mode: 'error', entries: [], message: error.message || String(error), messageType: 'error' }; + } } + this.render(); + } + + setMessage(message, messageType) { + this.state.message = message; + this.state.messageType = messageType; + this.render(); + } + + create(tagName, properties = {}, text = '') { + const element = this.containerEl.ownerDocument.createElement(tagName); + Object.assign(element, properties); + if (text) element.textContent = text; + return element; + } - render() { - if (!this.containerEl) return; - - this.containerEl.innerHTML = ` -
- - - - - - - -
- `; + button(label, action, disabled = false) { + const button = this.create('button', { type: 'button', disabled }, label); + button.dataset.gitAction = action; + return button; + } + + render() { + if (!this.containerEl || !this.containerEl.ownerDocument) return; + const panel = this.create('section', { className: 'git-panel' }); + panel.append(this.create('h2', { className: 'git-title' }, 'Source Control')); + const status = this.create('p', { className: `git-message git-message--${this.state.messageType}`, role: this.state.messageType === 'error' ? 'alert' : 'status' }, this.state.message); + panel.append(status); + + if (this.state.mode === 'non-repository') { + panel.append(this.button('Init repository', 'init')); + } else if (this.state.mode === 'repository' || this.state.mode === 'loading' || this.state.mode === 'error') { + const busy = this.state.mode === 'loading' || this.state.messageType === 'loading'; + const toolbar = this.create('div', { className: 'git-actions' }); + toolbar.append(this.button('Refresh', 'refresh', busy), this.button('Stage all', 'stage', busy || !this.state.entries.length), this.button('Pull', 'pull', busy), this.button('Push', 'push', busy)); + panel.append(toolbar); + const input = this.create('input', { id: 'git-commit-message', className: 'git-commit-input', type: 'text', placeholder: 'Commit message', disabled: busy || !this.state.entries.length }); + input.setAttribute('aria-label', 'Commit message'); + panel.append(input, this.button('Commit', 'commit', true)); + const list = this.create('ul', { className: 'git-status-list' }); + list.setAttribute('aria-label', 'Changed files'); + if (!this.state.entries.length) list.append(this.create('li', { className: 'git-clean' }, 'No changes')); + for (const entry of this.state.entries) { + const item = this.create('li', { className: 'git-status-entry' }); + item.append(this.create('span', { className: 'git-status-code' }, entry.code), this.create('span', { className: 'git-status-file', title: entry.file }, entry.file)); + list.append(item); + } + panel.append(list); } + this.containerEl.replaceChildren(panel); + this.renderActionState(); + } + + renderActionState() { + const input = this.containerEl?.querySelector?.('#git-commit-message'); + const commit = this.containerEl?.querySelector?.('[data-git-action="commit"]'); + if (input && commit) commit.disabled = input.disabled || !input.value.trim(); + } } module.exports = { GitUi }; diff --git a/UI/git_sidebar/gitui.test.js b/UI/git_sidebar/gitui.test.js index 6976e9c..dab0f42 100644 --- a/UI/git_sidebar/gitui.test.js +++ b/UI/git_sidebar/gitui.test.js @@ -1,71 +1,71 @@ const assert = require('node:assert/strict'); const test = require('node:test'); const { GitUi } = require('./gitui'); -const { generateUnifiedDiff } = require('../../patchgen'); -test('exports GitUi and stores the diff generator function', () => { - const gitUi = new GitUi({}, {}); +class FakeElement { + constructor(tagName, document) { this.tagName = tagName; this.ownerDocument = document; this.children = []; this.dataset = {}; this.disabled = false; this.value = ''; } + append(...children) { this.children.push(...children); } + replaceChildren(...children) { this.children = children; } + setAttribute(name, value) { this[name] = value; } + addEventListener() {} + querySelector(selector) { + const matches = (element) => selector.startsWith('#') ? element.id === selector.slice(1) : selector === '[data-git-action="commit"]' && element.dataset.gitAction === 'commit'; + const visit = (element) => matches(element) ? element : element.children.map(visit).find(Boolean); + return visit(this); + } + get textContent() { return this._text || this.children.map((child) => child.textContent).join(' '); } + set textContent(value) { this._text = value; } +} - assert.equal(gitUi.patch, generateUnifiedDiff); -}); - -test('renders the Git controls into its mount element', () => { - const container = { innerHTML: '' }; - const gitUi = new GitUi(container, {}); +function createContainer() { + const document = { createElement: (tagName) => new FakeElement(tagName, document) }; + return new FakeElement('div', document); +} - gitUi.render(); +function deferred() { + let resolve; + const promise = new Promise((done) => { resolve = done; }); + return { promise, resolve }; +} - assert.match(container.innerHTML, /id="git-add-btn"/); - assert.match(container.innerHTML, /id="git-old-file"/); - assert.match(container.innerHTML, /id="git-new-file"/); - assert.match(container.innerHTML, /id="git-patch-btn"/); +test('renders empty, loading, repository, success, non-repository, and error states', async () => { + const container = createContainer(); + const pending = deferred(); + const gitman = { setWorkspace() {}, status: () => pending.promise }; + const ui = new GitUi(container, { gitman }); + ui.render(); + assert.match(container.textContent, /Open a folder/); + const refresh = ui.setWorkspace('/workspace'); + assert.match(container.textContent, /Loading repository status/); + pending.resolve(' M changed.js\n?? new.js'); + await refresh; + assert.match(container.textContent, /changed\.js/); + ui.setMessage('Staging files completed.', 'success'); + assert.match(container.textContent, /completed/); + gitman.status = async () => { throw new Error('fatal: not a git repository'); }; + await ui.refresh(); + assert.match(container.textContent, /Init repository/); + gitman.status = async () => { throw new Error('permission denied'); }; + await ui.refresh(); + assert.match(container.textContent, /permission denied/); }); -test('the Add control stages all files', () => { - const listeners = []; - const container = { - addEventListener: (_type, listener) => listeners.push(listener) - }; - const gitUi = new GitUi(container, {}); - let addAllCalls = 0; - gitUi.gitman = { - init: () => Promise.resolve(''), - addAll: () => { - addAllCalls++; - return Promise.resolve(''); - } - }; - - gitUi.init_listners(); - listeners.forEach((listener) => listener({ target: { id: 'git-add-btn' } })); - - assert.equal(addAllCalls, 1); +test('actions invoke the expected operation and refresh status', async () => { + let stageCalls = 0; + let statusCalls = 0; + const gitman = { setWorkspace() {}, status: async () => { statusCalls++; return ' M file.js'; }, addAll: async () => { stageCalls++; return ''; } }; + const ui = new GitUi(createContainer(), { gitman }); + await ui.setWorkspace('/workspace'); + await ui.runAction('addAll'); + assert.equal(stageCalls, 1); + assert.equal(statusCalls, 2); + assert.equal(ui.state.messageType, 'success'); }); -test('generates a patch synchronously from the selected file paths', () => { - const listeners = []; - const oldFile = { name: 'old.txt' }; - const newFile = { name: 'new.txt' }; - const inputs = { - '#git-old-file': { files: [oldFile] }, - '#git-new-file': { files: [newFile] } - }; - const container = { - addEventListener: (_type, listener) => listeners.push(listener), - querySelector: (selector) => inputs[selector] - }; - const gitBar = { - getPathForFile: (file) => `/selected/${file.name}` - }; - const gitUi = new GitUi(container, gitBar); - let selectedPaths; - gitUi.patch = (oldFilePath, newFilePath) => { - selectedPaths = [oldFilePath, newFilePath]; - return 'patch text'; - }; - - gitUi.init_listners(); - listeners.forEach((listener) => listener({ target: { id: 'git-patch-btn' } })); - - assert.deepEqual(selectedPaths, ['/selected/old.txt', '/selected/new.txt']); +test('clearing the workspace clears repository state', async () => { + const ui = new GitUi(createContainer(), { gitman: { setWorkspace() {}, status: async () => '' } }); + await ui.setWorkspace('/workspace'); + await ui.setWorkspace(null); + assert.equal(ui.state.mode, 'empty'); + assert.deepEqual(ui.state.entries, []); }); diff --git a/eslint.config.mjs b/eslint.config.mjs index 13159d0..7c88e86 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -13,8 +13,7 @@ export default defineConfig([ "backend/**/*.js", "git/**/*.js", "UI/git_sidebar/**.js", - "UI/codebase_indexer/**.js", - "UI/tab_manager/**.js", + "UI/codebase_indexer/cb_index.js", "patchgen.js" ], extends: [js.configs.recommended], @@ -34,6 +33,24 @@ export default defineConfig([ } }, + { + files: ["UI/codebase_indexer/editor_integration.js"], + extends: [js.configs.recommended], + languageOptions: { + sourceType: "commonjs", + globals: { ...globals.node, ...globals.browser } + } + }, + + { + files: ["UI/tab_manager/**/*.js"], + extends: [js.configs.recommended], + languageOptions: { + sourceType: "module", + globals: { ...globals.browser } + } + }, + // Renderer: Browser + AMD (Monaco) { files: ["renderer/**/*.js"], diff --git a/index.html b/index.html index a3e6a89..9cdf270 100644 --- a/index.html +++ b/index.html @@ -456,8 +456,13 @@ tabManager.onNewTabRequest = () => createNewTab(); async function refreshFileTree(directoryPath) { - if (!directoryPath) return; + if (!directoryPath) { + currentDirectory = null; + await window.api.setGitWorkspace(null); + return; + } currentDirectory = directoryPath; + await window.api.setGitWorkspace(directoryPath); await renderDirectory(directoryPath, document.getElementById('tree-root')); } diff --git a/main.js b/main.js index 2ff1eca..75ec086 100644 --- a/main.js +++ b/main.js @@ -3,6 +3,7 @@ const { app, BrowserWindow, ipcMain, dialog } = require('electron'); const path = require('path'); const fs = require('fs'); const { spawn } = require('child_process'); +const { runGitRequest } = require('./main/git-service'); let clangdProcess = null; let clangdBuffer = Buffer.alloc(0); @@ -321,6 +322,8 @@ ipcMain.handle('directory:list', async (_event, directoryPath) => { .sort((left, right) => Number(right.isDirectory) - Number(left.isDirectory) || left.name.localeCompare(right.name)); }); +ipcMain.handle('git:run', (_event, request) => runGitRequest(request)); + ipcMain.handle('clangd:start', (event, rootPath) => startClangd(event, rootPath)); ipcMain.on('clangd:message', (_event, message) => sendToClangd(message)); ipcMain.on('clangd:stop', stopClangd); @@ -342,4 +345,4 @@ app.on('before-quit', (event) => { app.on('will-quit', () => { stopClangd(); stopPyright(); -}); \ No newline at end of file +}); diff --git a/main/git-service.js b/main/git-service.js new file mode 100644 index 0000000..fa7b527 --- /dev/null +++ b/main/git-service.js @@ -0,0 +1,37 @@ +const path = require('node:path'); +const { execFile } = require('node:child_process'); + +const OPERATIONS = Object.freeze({ + init: ['init'], status: ['status', '--short'], addAll: ['add', '--all'], pull: ['pull'], push: ['push'] +}); + +function validateGitRequest(request) { + if (!request || typeof request !== 'object') throw new Error('Invalid Git request.'); + const { operation, workspace, args = [] } = request; + if (!path.isAbsolute(workspace || '')) throw new Error('A valid workspace path is required.'); + if (!Array.isArray(args) || args.some((argument) => typeof argument !== 'string')) throw new Error('Invalid Git arguments.'); + if (operation === 'commit') { + if (args.length !== 1 || !args[0].trim() || args[0].length > 1000) throw new Error('A commit message is required.'); + return { workspace: path.resolve(workspace), gitArgs: ['commit', '-m', args[0]] }; + } + if (!Object.hasOwn(OPERATIONS, operation) || args.length) throw new Error('Unsupported Git operation.'); + return { workspace: path.resolve(workspace), gitArgs: OPERATIONS[operation] }; +} + +function runGitRequest(request, executor = execFile) { + let command; + try { command = validateGitRequest(request); } catch (error) { + return Promise.resolve({ ok: false, stdout: '', stderr: error.message, code: 'INVALID_REQUEST' }); + } + return new Promise((resolve) => { + executor('git', command.gitArgs, { cwd: command.workspace }, (error, stdout = '', stderr = '') => { + if (error) { + resolve({ ok: false, stdout: stdout.trim(), stderr: stderr.trim() || error.message, code: typeof error.code === 'number' ? error.code : 'GIT_ERROR' }); + return; + } + resolve({ ok: true, stdout: stdout.trim(), stderr: stderr.trim(), code: 0 }); + }); + }); +} + +module.exports = { OPERATIONS, validateGitRequest, runGitRequest }; diff --git a/main/git-service.test.js b/main/git-service.test.js new file mode 100644 index 0000000..7337759 --- /dev/null +++ b/main/git-service.test.js @@ -0,0 +1,18 @@ +const assert = require('node:assert/strict'); +const test = require('node:test'); +const { runGitRequest, validateGitRequest } = require('./git-service'); + +test('accepts allowlisted operations and rejects unsupported operations', async () => { + assert.deepEqual(validateGitRequest({ operation: 'status', workspace: '/workspace' }).gitArgs, ['status', '--short']); + const result = await runGitRequest({ operation: 'shell', workspace: '/workspace' }); + assert.equal(result.ok, false); + assert.equal(result.code, 'INVALID_REQUEST'); +}); + +test('uses the selected workspace as cwd and keeps commit arguments separate', async () => { + let invocation; + const executor = (...args) => { invocation = args.slice(0, 3); args[3](null, 'committed\n', ''); }; + const result = await runGitRequest({ operation: 'commit', workspace: '/selected/project', args: ['fix; echo unsafe'] }, executor); + assert.equal(result.ok, true); + assert.deepEqual(invocation, ['git', ['commit', '-m', 'fix; echo unsafe'], { cwd: '/selected/project' }]); +}); diff --git a/preload.js b/preload.js index a12ee75..f3ac7e2 100644 --- a/preload.js +++ b/preload.js @@ -1,16 +1,15 @@ -const { contextBridge, ipcRenderer, webUtils } = require('electron'); +const { contextBridge, ipcRenderer } = require('electron'); const { indexer } = require('./UI/codebase_indexer/cb_index'); const { GitUi } = require('./UI/git_sidebar/gitui'); +let gitUi = null; +const gitApi = { run: (operation, workspace, args = []) => ipcRenderer.invoke('git:run', { operation, workspace, args }) }; + globalThis.addEventListener('DOMContentLoaded', () => { const container = globalThis.document.getElementById('git-sidebar-mount'); - const gitBar = { - getPathForFile: (file) => webUtils.getPathForFile(file) - }; - - const gitUi = new GitUi(container, gitBar); + gitUi = new GitUi(container, { gitApi }); gitUi.render(); - gitUi.init_listners(); + gitUi.initListeners(); }); contextBridge.exposeInMainWorld('api', { @@ -20,6 +19,13 @@ contextBridge.exposeInMainWorld('api', { saveFile: (data) => ipcRenderer.invoke('file:save', data), listDirectory: (directoryPath) => ipcRenderer.invoke('directory:list', directoryPath), openFolder: () => ipcRenderer.invoke('dialog:openFolder'), + setGitWorkspace: (workspace) => gitUi?.setWorkspace(workspace), + gitStatus: (workspace) => gitApi.run('status', workspace), + gitInit: (workspace) => gitApi.run('init', workspace), + gitAddAll: (workspace) => gitApi.run('addAll', workspace), + gitCommit: (workspace, message) => gitApi.run('commit', workspace, [message]), + gitPull: (workspace) => gitApi.run('pull', workspace), + gitPush: (workspace) => gitApi.run('push', workspace), updateCodebaseIndex: (documentId, text) => indexer.updateActiveDocument(documentId, text), queryCodebaseGhostText: (prefix) => indexer.queryGhostText(prefix), diff --git a/styles/style1.css b/styles/style1.css index 2414d4f..0e1b697 100644 --- a/styles/style1.css +++ b/styles/style1.css @@ -196,22 +196,14 @@ body { } #git-sidebar { - width: 220px; + width: min(260px, 28vw); + min-width: 190px; padding: 10px; background-color: var(--vscode-sidebar-bg); border-right: 1px solid var(--vscode-border); overflow-y: auto; } -#git-sidebar .git_bar { - display: flex; - flex-direction: column; - align-items: stretch; - gap: 8px; - height: auto; - padding: 0; -} - #git-sidebar input, #git-sidebar button { width: 100%; @@ -601,36 +593,35 @@ body { outline-offset: -2px; } -.git_bar { +.git-panel { display: flex; - align-items: center; - gap: 6px; - height: 30px; - padding: 0 10px; - background-color: var(--vscode-toolbar-bg); - border-top: 1px solid var(--vscode-border); -} - -.init_btn { - background-color: transparent; + flex-direction: column; + gap: 8px; + min-width: 0; +} + +.git-title { margin: 0; font-size: 12px; text-transform: uppercase; letter-spacing: 0.5px; } +.git-message { margin: 0; padding: 7px; border-radius: 3px; color: var(--vscode-text-muted); overflow-wrap: anywhere; } +.git-message--success { color: #89d185; } +.git-message--error { color: #f48771; } +.git-message--loading { color: var(--vscode-text-main); } +.git-actions { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 6px; } +.git-panel button, .git-commit-input { box-sizing: border-box; min-height: 30px; border: 1px solid var(--vscode-border); border-radius: 4px; padding: 5px 8px; font: 12px var(--font-ui); } +.git-panel button { background-color: var(--vscode-toolbar-bg); color: var(--vscode-text-main); cursor: pointer; } +.git-panel button:hover:not(:disabled) { background-color: var(--vscode-hover-item); } +.git-panel button:disabled { cursor: default; opacity: 0.5; } +.git-panel button:focus-visible, .git-commit-input:focus-visible { outline: 2px solid var(--vscode-focus-border); outline-offset: 1px; } +.git-commit-input { + background: var(--vscode-editor-bg); color: var(--vscode-text-main); - border: 1px solid transparent; - border-radius: 4px; - padding: 4px 10px; - font-size: 12px; - font-family: var(--font-ui); - cursor: pointer; - transition: background-color 0.15s ease; } +.git-status-list { margin: 4px 0 0; padding: 0; list-style: none; overflow-y: auto; } +.git-status-entry { display: flex; gap: 8px; padding: 5px 2px; min-width: 0; } +.git-status-code { flex: 0 0 22px; color: #cca700; font-family: monospace; } +.git-status-file { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } +.git-clean { color: var(--vscode-text-muted); padding: 6px 2px; } -.add_btn { - background-color: transparent; - color: var(--vscode-text-main); - border: 1px solid transparent; - border-radius: 4px; - padding: 4px 10px; - font-size: 12px; - font-family: var(--font-ui); - cursor: pointer; - transition: background-color 0.15s ease; +@media (max-width: 760px) { + #git-sidebar { width: 190px; min-width: 160px; padding: 7px; } + .git-actions { grid-template-columns: 1fr; } }