Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { useCallback, useEffect, useState } from "react";
import { Welcome } from "./components/Welcome";
import { Workspace } from "./components/Workspace";
import { shouldShowOnboarding } from "./lib/onboarding";
import { loadLastWorkspace, recordRecent, deriveName } from "./lib/recents";
import { api } from "./lib/ipc";
import type { WorkspaceBootstrap } from "./types";
Expand Down Expand Up @@ -30,7 +31,7 @@ export default function App() {
// Try to auto-open last workspace on boot. Silent fallback to the
// welcome screen if the folder no longer exists or fails to open.
useEffect(() => {
if (startsEmpty) return;
if (startsEmpty || shouldShowOnboarding()) return;
const last = loadLastWorkspace();
if (!last) return;
(async () => {
Expand Down
47 changes: 39 additions & 8 deletions src/components/ConversationList.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { useRef, useState } from "react";
import { Icon } from "@iconify/react";
import { useLanguage } from "../lib/i18n";
import type { ConversationSummary } from "../types";

type Props = {
Expand All @@ -21,9 +22,17 @@ export function ConversationList({
onRename,
onDelete,
}: Props) {
const language = useLanguage();
const copy = conversationCopy[language];
const [editingId, setEditingId] = useState<string | null>(null);
const editRef = useRef<HTMLSpanElement | null>(null);

const displayTitle = (title: string) => {
const trimmed = title.trim();
if (!trimmed) return copy.untitled;
return trimmed === "New conversation" ? copy.newConversation : trimmed;
};

const commitRename = (id: string) => {
const value = editRef.current?.textContent?.trim() ?? "";
setEditingId(null);
Expand All @@ -37,20 +46,20 @@ export function ConversationList({
<div className="sidebar__head">
<span className="sidebar__head-title">
<Icon icon="solar:chat-round-dots-bold-duotone" width={16} height={16} />
<span>Conversations</span>
<span>{copy.title}</span>
</span>
<button
className="sidebar__head-btn"
onClick={onCreate}
title="New conversation"
title={copy.newConversation}
>
<Icon icon="solar:add-square-linear" width={15} height={15} />
</button>
</div>
<div className="sidebar__body">
<div className="conv-list">
{conversations.length === 0 && (
<div className="conv-empty">No conversations yet.</div>
<div className="conv-empty">{copy.empty}</div>
)}
{conversations.map((conv) => {
const isEditing = editingId === conv.id;
Expand All @@ -66,7 +75,7 @@ export function ConversationList({
>
<span className="conv-row__icon">
{isStreaming ? (
<span className="conv-row__spinner" aria-label="Streaming" />
<span className="conv-row__spinner" aria-label={copy.streaming} />
) : (
<Icon
icon={
Expand Down Expand Up @@ -97,12 +106,12 @@ export function ConversationList({
if (isEditing) commitRename(conv.id);
}}
>
{conv.title || "Untitled"}
{displayTitle(conv.title)}
</span>
<span className="conv-row__actions">
<button
className="conv-row__btn"
title="Rename"
title={copy.rename}
onClick={(event) => {
event.stopPropagation();
setEditingId(conv.id);
Expand All @@ -123,10 +132,10 @@ export function ConversationList({
</button>
<button
className="conv-row__btn conv-row__btn--danger"
title="Delete"
title={copy.delete}
onClick={(event) => {
event.stopPropagation();
if (confirm("Delete this conversation?")) {
if (confirm(copy.deleteConfirm)) {
onDelete(conv.id);
}
}}
Expand All @@ -142,3 +151,25 @@ export function ConversationList({
</div>
);
}
const conversationCopy = {
en: {
title: "Conversations",
newConversation: "New conversation",
empty: "No conversations yet.",
streaming: "Streaming",
untitled: "Untitled",
rename: "Rename",
delete: "Delete",
deleteConfirm: "Delete this conversation?",
},
fr: {
title: "Conversations",
newConversation: "Nouvelle conversation",
empty: "Aucune conversation pour le moment.",
streaming: "Génération en cours",
untitled: "Sans titre",
rename: "Renommer",
delete: "Supprimer",
deleteConfirm: "Supprimer cette conversation ?",
},
} as const;
51 changes: 40 additions & 11 deletions src/components/EditorPane.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import htmlWorker from "monaco-editor/esm/vs/language/html/html.worker?worker";
import tsWorker from "monaco-editor/esm/vs/language/typescript/ts.worker?worker";
import { useCallback, useEffect, useRef, useState, type ReactNode } from "react";
import { Icon } from "@iconify/react";
import { useLanguage } from "../lib/i18n";
import { languageForPath } from "../lib/language";
import { fileIcon } from "../lib/fileIcon";
import { Markdown } from "./chat/Markdown";
Expand Down Expand Up @@ -72,6 +73,8 @@ export function EditorPane({
onSettingsActivate,
onSettingsClose,
}: Props) {
const language = useLanguage();
const copy = editorCopy[language];
const editorRef = useRef<Monaco.editor.IStandaloneCodeEditor | null>(null);
const searchDecorationsRef =
useRef<Monaco.editor.IEditorDecorationsCollection | null>(null);
Expand Down Expand Up @@ -332,7 +335,7 @@ export function EditorPane({
event.stopPropagation();
onClose(index);
}}
title="Close tab"
title={copy.closeTab}
>
<Icon icon="solar:close-circle-linear" width={13} height={13} />
</button>
Expand All @@ -343,19 +346,19 @@ export function EditorPane({
className="tab"
data-active={settingsActive ? "true" : "false"}
onClick={onSettingsActivate}
title="Settings"
title={copy.settings}
>
<span className="tab__icon">
<Icon icon="solar:settings-linear" width={14} height={14} />
</span>
<span className="tab__name">Settings</span>
<span className="tab__name">{copy.settings}</span>
<button
className="tab__close"
onClick={(event) => {
event.stopPropagation();
onSettingsClose?.();
}}
title="Close tab"
title={copy.closeTab}
>
<Icon icon="solar:close-circle-linear" width={13} height={13} />
</button>
Expand All @@ -370,8 +373,8 @@ export function EditorPane({
onClick={toggleMarkdownPreview}
title={
activePreview
? "Show raw markdown source"
: "Show rendered markdown preview"
? copy.showSource
: copy.showPreview
}
>
<Icon
Expand All @@ -383,7 +386,7 @@ export function EditorPane({
width={13}
height={13}
/>
<span>{activePreview ? "Source" : "Preview"}</span>
<span>{activePreview ? copy.source : copy.preview}</span>
</button>
)}
</div>
Expand All @@ -403,9 +406,9 @@ export function EditorPane({
<span className="editor-empty__mark">
<Icon icon="solar:document-text-linear" width={18} height={18} />
</span>
<span className="editor-empty__title">Nothing open</span>
<span className="editor-empty__title">{copy.nothingOpen}</span>
<span className="editor-empty__sub">
Click a file in the sidebar to get started
{copy.clickFile}
</span>
</div>
) : isPreviewableImagePath(activeTab.relativePath) ? (
Expand Down Expand Up @@ -480,8 +483,8 @@ export function EditorPane({
</div>
) : !activeTab.doc.editable ? (
<div className="editor-noneditable">
<div>This file can&rsquo;t be edited here.</div>
<code>{activeTab.doc.reason ?? "binary or too large"}</code>
<div>{copy.cannotEdit}</div>
<code>{activeTab.doc.reason ?? copy.binaryOrTooLarge}</code>
<small style={{ color: "var(--text-3)" }}>
{activeTab.doc.relativePath} &middot;{" "}
{formatBytes(activeTab.doc.size)}
Expand Down Expand Up @@ -629,3 +632,29 @@ function isMarkdownPath(relativePath: string): boolean {
function isPlanMarkdownPath(relativePath: string): boolean {
return /^\.sinew\/plans\/.+\.md$/i.test(relativePath);
}
const editorCopy = {
en: {
settings: "Settings",
closeTab: "Close tab",
showSource: "Show raw markdown source",
showPreview: "Show rendered markdown preview",
source: "Source",
preview: "Preview",
nothingOpen: "Nothing open",
clickFile: "Click a file in the sidebar to get started",
cannotEdit: "This file can't be edited here.",
binaryOrTooLarge: "binary or too large",
},
fr: {
settings: "Paramètres",
closeTab: "Fermer l'onglet",
showSource: "Afficher la source markdown",
showPreview: "Afficher l'aperçu markdown",
source: "Source",
preview: "Aperçu",
nothingOpen: "Aucun fichier ouvert",
clickFile: "Cliquez sur un fichier dans la barre latérale pour commencer",
cannotEdit: "Ce fichier ne peut pas être modifié ici.",
binaryOrTooLarge: "binaire ou trop volumineux",
},
} as const;
33 changes: 27 additions & 6 deletions src/components/SearchPane.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { useCallback, useEffect, useRef, useState, type ReactNode } from "react";
import { Icon } from "@iconify/react";
import { api } from "../lib/ipc";
import { useLanguage } from "../lib/i18n";
import { fileIcon } from "../lib/fileIcon";
import type {
EditorRevealTarget,
Expand All @@ -21,6 +22,8 @@ type Props = {
const SEARCH_DELAY_MS = 180;

export function SearchPane({ workspacePath, refreshToken, onOpenFile }: Props) {
const language = useLanguage();
const copy = searchPaneCopy[language];
const [query, setQuery] = useState("");
const [result, setResult] = useState<WorkspaceSearchResult | null>(null);
const [loading, setLoading] = useState(false);
Expand Down Expand Up @@ -102,7 +105,7 @@ export function SearchPane({ workspacePath, refreshToken, onOpenFile }: Props) {
<input
ref={inputRef}
value={query}
placeholder="Search"
placeholder={copy.search}
spellCheck={false}
onChange={(event) => setQuery(event.target.value)}
onKeyDown={(event) => {
Expand All @@ -116,7 +119,7 @@ export function SearchPane({ workspacePath, refreshToken, onOpenFile }: Props) {
<button
type="button"
className="search-pane__clear"
title="Clear"
title={copy.clear}
onClick={() => setQuery("")}
>
<Icon icon="solar:close-circle-linear" width={14} height={14} />
Expand All @@ -126,12 +129,12 @@ export function SearchPane({ workspacePath, refreshToken, onOpenFile }: Props) {

<div className="search-pane__meta">
{loading
? "Searching..."
? copy.searching
: error
? error
: hasQuery
? `${files.length} files, ${result?.totalMatches ?? 0} matches`
: "Search in files"}
: copy.searchInFiles}
</div>

<div className="search-pane__results">
Expand Down Expand Up @@ -168,12 +171,12 @@ export function SearchPane({ workspacePath, refreshToken, onOpenFile }: Props) {
))}
</div>
) : (
<div className="search-result__path-match">Path match</div>
<div className="search-result__path-match">{copy.pathMatch}</div>
)}
</div>
))}
{hasQuery && !loading && !error && files.length === 0 && (
<div className="search-pane__empty">No results</div>
<div className="search-pane__empty">{copy.noResults}</div>
)}
</div>
</div>
Expand All @@ -192,3 +195,21 @@ function highlightMatch(match: WorkspaceSearchMatch): ReactNode {
</>
);
}
const searchPaneCopy = {
en: {
search: "Search",
clear: "Clear",
searching: "Searching...",
searchInFiles: "Search in files",
pathMatch: "Path match",
noResults: "No results",
},
fr: {
search: "Rechercher",
clear: "Effacer",
searching: "Recherche...",
searchInFiles: "Rechercher dans les fichiers",
pathMatch: "Chemin correspondant",
noResults: "Aucun résultat",
},
} as const;
Loading
Loading