Compare commits
2 Commits
3641190a77
...
416a994967
| Author | SHA1 | Date | |
|---|---|---|---|
| 416a994967 | |||
| c312b83604 |
22
server/bootstrapPrompt.js
vendored
22
server/bootstrapPrompt.js
vendored
@@ -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 [
|
return [
|
||||||
'Team agent modunu aktif et.',
|
'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).',
|
'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.",
|
"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.",
|
"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:`.",
|
"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.",
|
"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."
|
"Ilk cevap olarak yalnizca takimin hazir oldugunu ve rollerin aktiflestigini bildir. Bu ilk cevap da `Mazlum:` ile baslasin."
|
||||||
].join(" ");
|
].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(" ");
|
||||||
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import path from "path";
|
|||||||
import { fileURLToPath } from "url";
|
import { fileURLToPath } from "url";
|
||||||
import { Server } from "socket.io";
|
import { Server } from "socket.io";
|
||||||
import { getPublicRuntimeConfig, getRuntimeConfig } from "./config.js";
|
import { getPublicRuntimeConfig, getRuntimeConfig } from "./config.js";
|
||||||
|
import { selectProjectFolder } from "./projectPicker.js";
|
||||||
import { SessionManager } from "./sessionManager.js";
|
import { SessionManager } from "./sessionManager.js";
|
||||||
import { registerSocketHandlers } from "./socketHandlers.js";
|
import { registerSocketHandlers } from "./socketHandlers.js";
|
||||||
|
|
||||||
@@ -21,6 +22,8 @@ const __filename = fileURLToPath(import.meta.url);
|
|||||||
const __dirname = path.dirname(__filename);
|
const __dirname = path.dirname(__filename);
|
||||||
const webDistPath = path.resolve(__dirname, "../web/dist");
|
const webDistPath = path.resolve(__dirname, "../web/dist");
|
||||||
|
|
||||||
|
app.use(express.json());
|
||||||
|
|
||||||
app.get("/health", (req, res) => {
|
app.get("/health", (req, res) => {
|
||||||
res.json({
|
res.json({
|
||||||
ok: true,
|
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") {
|
if (config.nodeEnv === "production") {
|
||||||
app.use(express.static(webDistPath));
|
app.use(express.static(webDistPath));
|
||||||
app.get("*", (req, res) => {
|
app.get("*", (req, res) => {
|
||||||
|
|||||||
10
server/projectPicker.js
Normal file
10
server/projectPicker.js
Normal 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();
|
||||||
|
}
|
||||||
@@ -1,5 +1,8 @@
|
|||||||
|
import fs from "fs";
|
||||||
|
import path from "path";
|
||||||
import stripAnsi from "strip-ansi";
|
import stripAnsi from "strip-ansi";
|
||||||
import { buildBootstrapPrompt } from "./bootstrapPrompt.js";
|
import { buildBootstrapPrompt } from "./bootstrapPrompt.js";
|
||||||
|
import { buildProjectSelectionPrompt } from "./bootstrapPrompt.js";
|
||||||
import { LogService } from "./logService.js";
|
import { LogService } from "./logService.js";
|
||||||
import { PtyService } from "./ptyService.js";
|
import { PtyService } from "./ptyService.js";
|
||||||
import { getClaudeEnv, getPublicRuntimeConfig } from "./config.js";
|
import { getClaudeEnv, getPublicRuntimeConfig } from "./config.js";
|
||||||
@@ -26,7 +29,14 @@ function buildRoutedPrompt(prompt, lastDirectedMember = null) {
|
|||||||
if (!targetMember) {
|
if (!targetMember) {
|
||||||
return {
|
return {
|
||||||
targetMember: null,
|
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.ptyService = null;
|
||||||
this.chatOutput = "";
|
this.chatOutput = "";
|
||||||
this.lastDirectedMember = null;
|
this.lastDirectedMember = null;
|
||||||
|
this.currentProjectPath = null;
|
||||||
this.state = {
|
this.state = {
|
||||||
status: "idle",
|
status: "idle",
|
||||||
startedAt: null,
|
startedAt: null,
|
||||||
teamActivated: false,
|
teamActivated: false,
|
||||||
lastError: null,
|
lastError: null,
|
||||||
|
currentProjectPath: null,
|
||||||
runtime: getPublicRuntimeConfig(config)
|
runtime: getPublicRuntimeConfig(config)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -56,6 +68,7 @@ export class SessionManager {
|
|||||||
getState() {
|
getState() {
|
||||||
return {
|
return {
|
||||||
...this.state,
|
...this.state,
|
||||||
|
currentProjectPath: this.currentProjectPath,
|
||||||
runtime: getPublicRuntimeConfig(this.config)
|
runtime: getPublicRuntimeConfig(this.config)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -94,6 +107,39 @@ export class SessionManager {
|
|||||||
this.emitState();
|
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() {
|
async start() {
|
||||||
if (this.ptyService?.isRunning()) {
|
if (this.ptyService?.isRunning()) {
|
||||||
throw new Error("Session is already running");
|
throw new Error("Session is already running");
|
||||||
@@ -105,7 +151,7 @@ export class SessionManager {
|
|||||||
this.io.emit("chat:reset");
|
this.io.emit("chat:reset");
|
||||||
|
|
||||||
this.ptyService = new PtyService({
|
this.ptyService = new PtyService({
|
||||||
cwd: this.config.workspaceDir,
|
cwd: this.getActiveWorkspaceDir(),
|
||||||
env: getClaudeEnv(this.config)
|
env: getClaudeEnv(this.config)
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -116,7 +162,7 @@ export class SessionManager {
|
|||||||
lastError: null
|
lastError: null
|
||||||
});
|
});
|
||||||
|
|
||||||
this.addLog("lifecycle", `Starting Claude session in ${this.config.workspaceDir}`);
|
this.addLog("lifecycle", `Starting Claude session in ${this.getActiveWorkspaceDir()}`);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await this.ptyService.start({
|
await this.ptyService.start({
|
||||||
@@ -174,7 +220,7 @@ export class SessionManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async activateTeam() {
|
async activateTeam() {
|
||||||
const prompt = buildBootstrapPrompt();
|
const prompt = buildBootstrapPrompt(this.currentProjectPath);
|
||||||
this.lastDirectedMember = null;
|
this.lastDirectedMember = null;
|
||||||
await this.sendRawPrompt(prompt, { label: "[bootstrap] Team activation prompt sent" });
|
await this.sendRawPrompt(prompt, { label: "[bootstrap] Team activation prompt sent" });
|
||||||
this.setState({ teamActivated: true });
|
this.setState({ teamActivated: true });
|
||||||
|
|||||||
@@ -52,5 +52,15 @@ export function registerSocketHandlers(io, sessionManager) {
|
|||||||
sessionManager.clearLogs();
|
sessionManager.clearLogs();
|
||||||
callback?.({ ok: true });
|
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 });
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useState } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
import ShellFrame from "./components/ShellFrame.jsx";
|
import ShellFrame from "./components/ShellFrame.jsx";
|
||||||
import SessionToolbar from "./components/SessionToolbar.jsx";
|
import SessionToolbar from "./components/SessionToolbar.jsx";
|
||||||
import ChatStream from "./components/ChatStream.jsx";
|
import ChatStream from "./components/ChatStream.jsx";
|
||||||
@@ -9,8 +9,9 @@ import { useSession } from "./hooks/useSession.js";
|
|||||||
|
|
||||||
export default function App() {
|
export default function App() {
|
||||||
const { socket, connected } = useSocket();
|
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 [busy, setBusy] = useState(false);
|
||||||
|
const autoStartedRef = useRef(false);
|
||||||
|
|
||||||
async function runAction(action) {
|
async function runAction(action) {
|
||||||
setBusy(true);
|
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 (
|
return (
|
||||||
<main className="app-shell">
|
<main className="app-shell">
|
||||||
<div className="app-shell__header">
|
<div className="app-shell__header">
|
||||||
<div>
|
<div className="app-shell__title">
|
||||||
<p className="app-shell__eyebrow">1996 COMMAND CENTER</p>
|
<p className="app-shell__eyebrow">1996 COMMAND CENTER</p>
|
||||||
<h1>Retro Claude Team Console</h1>
|
<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>
|
||||||
<div className="app-shell__meta">
|
<div className="app-shell__meta">
|
||||||
<span>LINK: {connected ? "ONLINE" : "OFFLINE"}</span>
|
<span>LINK: {connected ? "ONLINE" : "OFFLINE"}</span>
|
||||||
@@ -54,14 +71,13 @@ export default function App() {
|
|||||||
<SessionToolbar
|
<SessionToolbar
|
||||||
session={session}
|
session={session}
|
||||||
busy={busy}
|
busy={busy}
|
||||||
onStart={() => runAction(startSession)}
|
|
||||||
onActivate={() => runAction(activateTeam)}
|
|
||||||
onStop={() => runAction(stopSession)}
|
onStop={() => runAction(stopSession)}
|
||||||
|
onSelectProject={() => runAction(selectProject)}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<PromptComposer
|
<PromptComposer
|
||||||
disabled={busy || session.status !== "running"}
|
disabled={busy || session.status !== "running" || !session.teamActivated || !session.currentProjectPath}
|
||||||
onSubmit={(prompt) => runAction(() => sendPrompt(prompt))}
|
onSubmit={(prompt) => runAction(() => sendPrompt(prompt))}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,20 +1,16 @@
|
|||||||
import PixelButton from "./PixelButton.jsx";
|
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 isRunning = session.status === "running";
|
||||||
const isStarting = session.status === "starting";
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="session-toolbar session-toolbar--inline">
|
<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}>
|
<PixelButton tone="red" disabled={busy || (!isRunning && session.status !== "starting")} onClick={onStop}>
|
||||||
Stop Session
|
Stop Session
|
||||||
</PixelButton>
|
</PixelButton>
|
||||||
|
<PixelButton tone="amber" disabled={busy} onClick={onSelectProject}>
|
||||||
|
Select Project
|
||||||
|
</PixelButton>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ function TeamCard({ member }) {
|
|||||||
<span>NO SIGNAL YET</span>
|
<span>NO SIGNAL YET</span>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
member.messages.slice(-4).map((message) => (
|
[...member.messages].slice(-4).reverse().map((message) => (
|
||||||
<article key={message.id} className="team-message">
|
<article key={message.id} className="team-message">
|
||||||
<span className="team-message__speaker">{message.speaker}:</span>
|
<span className="team-message__speaker">{message.speaker}:</span>
|
||||||
<pre>{message.text}</pre>
|
<pre>{message.text}</pre>
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ const initialState = {
|
|||||||
startedAt: null,
|
startedAt: null,
|
||||||
teamActivated: false,
|
teamActivated: false,
|
||||||
lastError: null,
|
lastError: null,
|
||||||
|
currentProjectPath: null,
|
||||||
runtime: {
|
runtime: {
|
||||||
anthropicModel: "",
|
anthropicModel: "",
|
||||||
anthropicBaseUrl: "",
|
anthropicBaseUrl: "",
|
||||||
@@ -64,6 +65,25 @@ export function useSession(socket) {
|
|||||||
stopSession: () => emitWithAck("session:stop"),
|
stopSession: () => emitWithAck("session:stop"),
|
||||||
activateTeam: () => emitWithAck("team:activate"),
|
activateTeam: () => emitWithAck("team:activate"),
|
||||||
sendPrompt: (prompt) => emitWithAck("prompt:send", { prompt }),
|
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 })
|
resizeTerminal: (cols, rows) => socket.emit("terminal:resize", { cols, rows })
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -69,8 +69,7 @@ function shouldBreakCurrentEntry(line) {
|
|||||||
/^[-=]{4,}$/.test(trimmed),
|
/^[-=]{4,}$/.test(trimmed),
|
||||||
/^>/.test(trimmed),
|
/^>/.test(trimmed),
|
||||||
/^Kullanici /i.test(trimmed),
|
/^Kullanici /i.test(trimmed),
|
||||||
/^Mazlum nasilsin\?/i.test(trimmed),
|
/^Mazlum nasilsin\?/i.test(trimmed)
|
||||||
/^[A-Za-zÀ-ÿ]+,/.test(trimmed)
|
|
||||||
].some(Boolean);
|
].some(Boolean);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -94,11 +93,34 @@ function dedupeMessages(messages) {
|
|||||||
const result = [];
|
const result = [];
|
||||||
|
|
||||||
for (const message of messages) {
|
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)) {
|
if (seen.has(key)) {
|
||||||
continue;
|
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);
|
seen.add(key);
|
||||||
result.push(message);
|
result.push(message);
|
||||||
}
|
}
|
||||||
@@ -112,7 +134,7 @@ export function parseTeamFeed(chat) {
|
|||||||
|
|
||||||
for (const rawLine of String(chat ?? "").split("\n")) {
|
for (const rawLine of String(chat ?? "").split("\n")) {
|
||||||
const line = rawLine.trim();
|
const line = rawLine.trim();
|
||||||
const speakerMatch = line.match(/^[•*\-⏺]?\s*([A-Za-zÀ-ÿ]+):\s*(.*)$/);
|
const speakerMatch = line.match(/^(?:[•*⏺]\s*)?([A-Za-zÀ-ÿ]+):\s*(.*)$/);
|
||||||
|
|
||||||
if (speakerMatch) {
|
if (speakerMatch) {
|
||||||
const member = memberMap.get(normalizeSpeaker(speakerMatch[1]));
|
const member = memberMap.get(normalizeSpeaker(speakerMatch[1]));
|
||||||
|
|||||||
@@ -35,6 +35,24 @@
|
|||||||
font-size: clamp(1.3rem, 2vw, 2rem);
|
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 {
|
.app-shell__meta {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
@@ -107,13 +125,14 @@
|
|||||||
|
|
||||||
.session-toolbar {
|
.session-toolbar {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 12px;
|
gap: 8px;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.session-toolbar--inline {
|
.session-toolbar--inline {
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
|
flex-wrap: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.pixel-button {
|
.pixel-button {
|
||||||
@@ -123,17 +142,17 @@
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
min-width: 156px;
|
min-width: 128px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.pixel-button span {
|
.pixel-button span {
|
||||||
display: block;
|
display: block;
|
||||||
padding: 14px 16px;
|
padding: 12px 12px;
|
||||||
border: 3px solid var(--border-dark);
|
border: 3px solid var(--border-dark);
|
||||||
box-shadow: inset 0 0 0 2px rgba(255, 255, 255, 0.06);
|
box-shadow: inset 0 0 0 2px rgba(255, 255, 255, 0.06);
|
||||||
font-family: var(--font-display);
|
font-family: var(--font-display);
|
||||||
font-size: 0.62rem;
|
font-size: 0.56rem;
|
||||||
letter-spacing: 0.14em;
|
letter-spacing: 0.12em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.pixel-button:hover span {
|
.pixel-button:hover span {
|
||||||
@@ -401,6 +420,10 @@
|
|||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.app-shell__project {
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
.shell-frame__screen {
|
.shell-frame__screen {
|
||||||
min-height: auto;
|
min-height: auto;
|
||||||
padding: 12px;
|
padding: 12px;
|
||||||
@@ -413,5 +436,6 @@
|
|||||||
|
|
||||||
.session-toolbar--inline {
|
.session-toolbar--inline {
|
||||||
justify-content: stretch;
|
justify-content: stretch;
|
||||||
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user