Compare commits

...

2 Commits

Author SHA1 Message Date
416a994967 fix: ekip panosu ve ust arayuzu rafine et 2026-03-17 00:41:07 +03:00
c312b83604 feat: proje secimi ve otomatik ekip akisini ekle 2026-03-17 00:40:50 +03:00
11 changed files with 212 additions and 29 deletions

View File

@@ -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(" ");
}

View File

@@ -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) => {

10
server/projectPicker.js Normal file
View File

@@ -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();
}

View File

@@ -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 });

View File

@@ -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 });
}
});
});
}

View File

@@ -1,4 +1,4 @@
import { useState } from "react";
import { useEffect, useRef, useState } from "react";
import ShellFrame from "./components/ShellFrame.jsx";
import SessionToolbar from "./components/SessionToolbar.jsx";
import ChatStream from "./components/ChatStream.jsx";
@@ -9,8 +9,9 @@ import { useSession } from "./hooks/useSession.js";
export default function App() {
const { socket, connected } = useSocket();
const { session, chat, error, startSession, stopSession, activateTeam, sendPrompt, clearError } = useSession(socket);
const { session, chat, error, startSession, stopSession, sendPrompt, selectProject, clearError } = useSession(socket);
const [busy, setBusy] = useState(false);
const autoStartedRef = useRef(false);
async function runAction(action) {
setBusy(true);
@@ -23,12 +24,28 @@ export default function App() {
}
}
useEffect(() => {
if (!connected || autoStartedRef.current) {
return;
}
if (session.status === "idle") {
autoStartedRef.current = true;
runAction(startSession).catch(() => {
autoStartedRef.current = false;
});
}
}, [connected, session.status]);
return (
<main className="app-shell">
<div className="app-shell__header">
<div>
<div className="app-shell__title">
<p className="app-shell__eyebrow">1996 COMMAND CENTER</p>
<h1>Retro Claude Team Console</h1>
<p className="app-shell__project" title={session.currentProjectPath ?? "None"}>
<span>Current Project:</span> {session.currentProjectPath ?? "None"}
</p>
</div>
<div className="app-shell__meta">
<span>LINK: {connected ? "ONLINE" : "OFFLINE"}</span>
@@ -54,14 +71,13 @@ export default function App() {
<SessionToolbar
session={session}
busy={busy}
onStart={() => runAction(startSession)}
onActivate={() => runAction(activateTeam)}
onStop={() => runAction(stopSession)}
onSelectProject={() => runAction(selectProject)}
/>
}
/>
<PromptComposer
disabled={busy || session.status !== "running"}
disabled={busy || session.status !== "running" || !session.teamActivated || !session.currentProjectPath}
onSubmit={(prompt) => runAction(() => sendPrompt(prompt))}
/>
</div>

View File

@@ -1,20 +1,16 @@
import PixelButton from "./PixelButton.jsx";
export default function SessionToolbar({ session, busy, onStart, onActivate, onStop }) {
export default function SessionToolbar({ session, busy, onStop, onSelectProject }) {
const isRunning = session.status === "running";
const isStarting = session.status === "starting";
return (
<div className="session-toolbar session-toolbar--inline">
<PixelButton tone="green" disabled={busy || isRunning || isStarting} onClick={onStart}>
Start Session
</PixelButton>
<PixelButton tone="cyan" disabled={busy || !isRunning} onClick={onActivate}>
Activate Team
</PixelButton>
<PixelButton tone="red" disabled={busy || (!isRunning && session.status !== "starting")} onClick={onStop}>
Stop Session
</PixelButton>
<PixelButton tone="amber" disabled={busy} onClick={onSelectProject}>
Select Project
</PixelButton>
</div>
);
}

View File

@@ -21,7 +21,7 @@ function TeamCard({ member }) {
<span>NO SIGNAL YET</span>
</div>
) : (
member.messages.slice(-4).map((message) => (
[...member.messages].slice(-4).reverse().map((message) => (
<article key={message.id} className="team-message">
<span className="team-message__speaker">{message.speaker}:</span>
<pre>{message.text}</pre>

View File

@@ -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 })
};
}

View File

@@ -69,8 +69,7 @@ function shouldBreakCurrentEntry(line) {
/^[-=]{4,}$/.test(trimmed),
/^>/.test(trimmed),
/^Kullanici /i.test(trimmed),
/^Mazlum nasilsin\?/i.test(trimmed),
/^[A-Za-zÀ-ÿ]+,/.test(trimmed)
/^Mazlum nasilsin\?/i.test(trimmed)
].some(Boolean);
}
@@ -94,11 +93,34 @@ function dedupeMessages(messages) {
const result = [];
for (const message of messages) {
const key = `${message.speaker}::${message.text}`;
const normalizedText = String(message.text ?? "").replace(/\s+/g, " ").trim();
const firstLine = normalizedText.split("\n")[0]?.trim() ?? "";
const key = `${message.speaker}::${normalizedText}`;
if (seen.has(key)) {
continue;
}
const lastMessage = result[result.length - 1];
if (lastMessage) {
const lastNormalized = String(lastMessage.text ?? "").replace(/\s+/g, " ").trim();
const lastFirstLine = lastNormalized.split("\n")[0]?.trim() ?? "";
if (
lastMessage.speaker === message.speaker &&
firstLine &&
lastFirstLine === firstLine
) {
const mergedText = lastNormalized.length >= normalizedText.length ? lastMessage.text : message.text;
result[result.length - 1] = {
...lastMessage,
text: mergedText
};
seen.add(key);
continue;
}
}
seen.add(key);
result.push(message);
}
@@ -112,7 +134,7 @@ export function parseTeamFeed(chat) {
for (const rawLine of String(chat ?? "").split("\n")) {
const line = rawLine.trim();
const speakerMatch = line.match(/^[•*\-⏺]?\s*([A-Za-zÀ-ÿ]+):\s*(.*)$/);
const speakerMatch = line.match(/^(?:[•*⏺]\s*)?([A-Za-zÀ-ÿ]+):\s*(.*)$/);
if (speakerMatch) {
const member = memberMap.get(normalizeSpeaker(speakerMatch[1]));

View File

@@ -35,6 +35,24 @@
font-size: clamp(1.3rem, 2vw, 2rem);
}
.app-shell__title {
min-width: 0;
}
.app-shell__project {
margin: 10px 0 0;
max-width: min(70vw, 760px);
color: var(--text-dim);
font-size: 0.78rem;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.app-shell__project span {
color: var(--accent-amber);
}
.app-shell__meta {
display: flex;
gap: 10px;
@@ -107,13 +125,14 @@
.session-toolbar {
display: flex;
gap: 12px;
gap: 8px;
flex-wrap: wrap;
}
.session-toolbar--inline {
justify-content: flex-end;
margin-bottom: 0;
flex-wrap: nowrap;
}
.pixel-button {
@@ -123,17 +142,17 @@
cursor: pointer;
text-transform: uppercase;
background: transparent;
min-width: 156px;
min-width: 128px;
}
.pixel-button span {
display: block;
padding: 14px 16px;
padding: 12px 12px;
border: 3px solid var(--border-dark);
box-shadow: inset 0 0 0 2px rgba(255, 255, 255, 0.06);
font-family: var(--font-display);
font-size: 0.62rem;
letter-spacing: 0.14em;
font-size: 0.56rem;
letter-spacing: 0.12em;
}
.pixel-button:hover span {
@@ -401,6 +420,10 @@
align-items: stretch;
}
.app-shell__project {
max-width: 100%;
}
.shell-frame__screen {
min-height: auto;
padding: 12px;
@@ -413,5 +436,6 @@
.session-toolbar--inline {
justify-content: stretch;
flex-wrap: wrap;
}
}