From c312b83604f5a6c6e82d7af3ef593bc270302bec Mon Sep 17 00:00:00 2001 From: wisecolt Date: Tue, 17 Mar 2026 00:40:50 +0300 Subject: [PATCH] feat: proje secimi ve otomatik ekip akisini ekle --- server/bootstrapPrompt.js | 22 ++++++++++++++- server/index.js | 19 +++++++++++++ server/projectPicker.js | 10 +++++++ server/sessionManager.js | 54 ++++++++++++++++++++++++++++++++++--- server/socketHandlers.js | 10 +++++++ web/src/hooks/useSession.js | 20 ++++++++++++++ 6 files changed, 130 insertions(+), 5 deletions(-) create mode 100644 server/projectPicker.js diff --git a/server/bootstrapPrompt.js b/server/bootstrapPrompt.js index 40dca04..ce30be9 100644 --- a/server/bootstrapPrompt.js +++ b/server/bootstrapPrompt.js @@ -1,8 +1,16 @@ -export function buildBootstrapPrompt() { +export function buildBootstrapPrompt(projectPath = null) { + const projectContext = projectPath + ? `Aktif proje kok dizini: ${projectPath}. Bundan sonra tum analiz, yorum, gorev parcasi ve dosya referanslarini yalnizca bu proje uzerinden yap. Bu proje disina tasma.` + : "Aktif proje henuz secilmedi. Kullanici proje secene kadar dosya baglaminda varsayim yapma."; + return [ 'Team agent modunu aktif et.', 'Team icerisindeki roller -> Team Lead: "Mazlum" (erkek), Frontend Developer: "Berkecan" (erkek), Backend Developer: "Simsar" (erkek), UI/UX Designer: "Aybuke" (disi), iOS Developer: "Ive" (erkek) ve takim uyelerine kahveleri getirmesi icin Trainee: "Irgatov" (erkek).', "Bu takim yapisini aynen koru.", + projectContext, + "Irgatov teknik ekip uyesi degildir; yalnizca kahve, icecek, ofis ici lojistik ve basit yardim isleriyle ilgilenir.", + "Irgatov kod mimarisi, dosya yapisi, planlama, bug analizi, teknoloji secimi, UI/UX karari, backend karari veya iOS karari vermez.", + "Teknik gorev dagitimi yaparken Irgatov'a teknik is yazma. Irgatov sadece kahve ve lojistik destek icin konussun.", "Takim ici tum mesajlarda konusan kisi zorunlu olarak ad etiketiyle baslasin.", "Her cevap yalnizca su formatla baslasin: `Mazlum:` veya `Berkecan:` veya `Simsar:` veya `Aybuke:` veya `Ive:` veya `Irgatov:`.", "Etiketsiz cevap verme. `Ben`, `Team Lead`, `Frontend Developer`, `UI/UX Designer`, `biz`, `takim olarak` gibi baslangiclar kullanma.", @@ -13,3 +21,15 @@ export function buildBootstrapPrompt() { "Ilk cevap olarak yalnizca takimin hazir oldugunu ve rollerin aktiflestigini bildir. Bu ilk cevap da `Mazlum:` ile baslasin." ].join(" "); } + +export function buildProjectSelectionPrompt(projectPath) { + return [ + "Proje baglami guncellendi.", + `Yeni aktif proje kok dizini: ${projectPath}.`, + "Bu andan itibaren tum yorum, plan, gorev ve kod onerilerini yalnizca bu proje uzerinden yap.", + "Bu proje disinda dosya, klasor veya kod tabani varsayimi yapma.", + "Irgatov bu proje baglaminda da sadece kahve ve lojistik destek verir; teknik gorev almaz.", + "Kullanici yeni bir proje secene kadar bu proje varsayilan tek calisma alanidir.", + "Bu bildirimi tekrar etme; sadece yeni proje baglamina gore calismaya devam et." + ].join(" "); +} diff --git a/server/index.js b/server/index.js index 38b77b6..36f6695 100644 --- a/server/index.js +++ b/server/index.js @@ -4,6 +4,7 @@ import path from "path"; import { fileURLToPath } from "url"; import { Server } from "socket.io"; import { getPublicRuntimeConfig, getRuntimeConfig } from "./config.js"; +import { selectProjectFolder } from "./projectPicker.js"; import { SessionManager } from "./sessionManager.js"; import { registerSocketHandlers } from "./socketHandlers.js"; @@ -21,6 +22,8 @@ const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); const webDistPath = path.resolve(__dirname, "../web/dist"); +app.use(express.json()); + app.get("/health", (req, res) => { res.json({ ok: true, @@ -37,6 +40,22 @@ app.get("/api/session/state", (req, res) => { }); }); +app.post("/api/project/select", async (req, res) => { + try { + const selectedPath = req.body?.projectPath ? String(req.body.projectPath) : await selectProjectFolder(); + await sessionManager.setProjectPath(selectedPath); + res.json({ + ok: true, + projectPath: sessionManager.getState().currentProjectPath + }); + } catch (error) { + res.status(500).json({ + ok: false, + error: error.message + }); + } +}); + if (config.nodeEnv === "production") { app.use(express.static(webDistPath)); app.get("*", (req, res) => { diff --git a/server/projectPicker.js b/server/projectPicker.js new file mode 100644 index 0000000..c2f74d6 --- /dev/null +++ b/server/projectPicker.js @@ -0,0 +1,10 @@ +import { execFile } from "node:child_process"; +import { promisify } from "node:util"; + +const execFileAsync = promisify(execFile); + +export async function selectProjectFolder() { + const script = 'POSIX path of (choose folder with prompt "Select project folder")'; + const { stdout } = await execFileAsync("/usr/bin/osascript", ["-e", script]); + return String(stdout ?? "").trim(); +} diff --git a/server/sessionManager.js b/server/sessionManager.js index a08b84c..feb1637 100644 --- a/server/sessionManager.js +++ b/server/sessionManager.js @@ -1,5 +1,8 @@ +import fs from "fs"; +import path from "path"; import stripAnsi from "strip-ansi"; import { buildBootstrapPrompt } from "./bootstrapPrompt.js"; +import { buildProjectSelectionPrompt } from "./bootstrapPrompt.js"; import { LogService } from "./logService.js"; import { PtyService } from "./ptyService.js"; import { getClaudeEnv, getPublicRuntimeConfig } from "./config.js"; @@ -26,7 +29,14 @@ function buildRoutedPrompt(prompt, lastDirectedMember = null) { if (!targetMember) { return { targetMember: null, - routedPrompt: `Not: Bu genel mesajdir. Once Mazlum cevap versin ve konusan herkes ad etiketi kullansin. Kullanici mesaji: ${prompt}` + routedPrompt: `Not: Bu genel mesajdir. Once Mazlum cevap versin ve konusan herkes ad etiketi kullansin. Irgatov teknik gorev almaz; sadece kahve ve lojistik destek verir. Kullanici mesaji: ${prompt}` + }; + } + + if (targetMember.name === "Irgatov") { + return { + targetMember, + routedPrompt: `Not: Bu mesaj Irgatov icindir. Irgatov sadece kahve, icecek, servis ve basit lojistik destek konularinda cevap versin. Teknik plan, kod, mimari veya dosya yapisi onermesin. Cevap \`Irgatov:\` ile baslasin. Kullanici mesaji: ${prompt}` }; } @@ -44,11 +54,13 @@ export class SessionManager { this.ptyService = null; this.chatOutput = ""; this.lastDirectedMember = null; + this.currentProjectPath = null; this.state = { status: "idle", startedAt: null, teamActivated: false, lastError: null, + currentProjectPath: null, runtime: getPublicRuntimeConfig(config) }; } @@ -56,6 +68,7 @@ export class SessionManager { getState() { return { ...this.state, + currentProjectPath: this.currentProjectPath, runtime: getPublicRuntimeConfig(this.config) }; } @@ -94,6 +107,39 @@ export class SessionManager { this.emitState(); } + getActiveWorkspaceDir() { + return this.currentProjectPath ?? this.config.workspaceDir; + } + + async setProjectPath(projectPath) { + const resolved = projectPath ? path.resolve(projectPath) : null; + const wasRunning = this.ptyService?.isRunning() ?? false; + + if (resolved && (!fs.existsSync(resolved) || !fs.statSync(resolved).isDirectory())) { + throw new Error(`Selected project path is invalid: ${resolved}`); + } + + if (wasRunning) { + await this.stop(); + } + + this.currentProjectPath = resolved; + this.lastDirectedMember = null; + this.setState({ currentProjectPath: resolved }); + this.addLog("system", `Current project set to ${resolved ?? "None"}`); + + if (wasRunning) { + await this.start(); + + if (resolved) { + await this.activateTeam(); + } else { + const prompt = buildProjectSelectionPrompt(this.getActiveWorkspaceDir()); + await this.sendRawPrompt(prompt, { label: `[project] Switched active project to ${this.getActiveWorkspaceDir()}` }); + } + } + } + async start() { if (this.ptyService?.isRunning()) { throw new Error("Session is already running"); @@ -105,7 +151,7 @@ export class SessionManager { this.io.emit("chat:reset"); this.ptyService = new PtyService({ - cwd: this.config.workspaceDir, + cwd: this.getActiveWorkspaceDir(), env: getClaudeEnv(this.config) }); @@ -116,7 +162,7 @@ export class SessionManager { lastError: null }); - this.addLog("lifecycle", `Starting Claude session in ${this.config.workspaceDir}`); + this.addLog("lifecycle", `Starting Claude session in ${this.getActiveWorkspaceDir()}`); try { await this.ptyService.start({ @@ -174,7 +220,7 @@ export class SessionManager { } async activateTeam() { - const prompt = buildBootstrapPrompt(); + const prompt = buildBootstrapPrompt(this.currentProjectPath); this.lastDirectedMember = null; await this.sendRawPrompt(prompt, { label: "[bootstrap] Team activation prompt sent" }); this.setState({ teamActivated: true }); diff --git a/server/socketHandlers.js b/server/socketHandlers.js index 0d30655..3e423d1 100644 --- a/server/socketHandlers.js +++ b/server/socketHandlers.js @@ -52,5 +52,15 @@ export function registerSocketHandlers(io, sessionManager) { sessionManager.clearLogs(); callback?.({ ok: true }); }); + + socket.on("project:select", async ({ projectPath }, callback) => { + try { + await sessionManager.setProjectPath(projectPath); + callback?.({ ok: true, projectPath: sessionManager.getState().currentProjectPath }); + } catch (error) { + socket.emit("session:error", { message: error.message }); + callback?.({ ok: false, error: error.message }); + } + }); }); } diff --git a/web/src/hooks/useSession.js b/web/src/hooks/useSession.js index 7daed10..fde055e 100644 --- a/web/src/hooks/useSession.js +++ b/web/src/hooks/useSession.js @@ -5,6 +5,7 @@ const initialState = { startedAt: null, teamActivated: false, lastError: null, + currentProjectPath: null, runtime: { anthropicModel: "", anthropicBaseUrl: "", @@ -64,6 +65,25 @@ export function useSession(socket) { stopSession: () => emitWithAck("session:stop"), activateTeam: () => emitWithAck("team:activate"), sendPrompt: (prompt) => emitWithAck("prompt:send", { prompt }), + selectProject: async () => { + const response = await fetch("/api/project/select", { + method: "POST", + headers: { + "Content-Type": "application/json" + }, + body: JSON.stringify({}) + }); + + const payload = await response.json(); + if (!response.ok || !payload.ok) { + const message = payload.error ?? "Project selection failed"; + setError(message); + throw new Error(message); + } + + setError(""); + return payload; + }, resizeTerminal: (cols, rows) => socket.emit("terminal:resize", { cols, rows }) }; }