feat(sessions): masquer les sessions externes + terminal plein écran
Masquage persistant des sessions Claude découvertes (lancées hors Arboretum) qui polluent la liste — calqué sur repos.hidden : - table hidden_sessions (#9) clé claudeSessionId ; helpers DB - DiscoveryService pose le champ additif SessionSummary.hidden (resume/fork restent possibles, survit à un re-scan) - routes POST/DELETE /sessions/:id/hide, POST /sessions/hide-discovered (masse, un clic), GET /sessions?includeHidden=true - UI : bouton Masquer par ligne, « Masquer l'historique externe », toggle « Afficher les masquées », badge masquée ; store + i18n EN/FR + audit Terminal plein écran (corrige le sizing) : - SessionView root s'étire (flex-1) → xterm calcule les bonnes dimensions - refit robuste (requestAnimationFrame + après resync), h-full sur le wrapper - bouton plein écran (API Fullscreen), tabbar mobile masquée en fullbleed DialogPrompt : options en liste verticale pleine largeur (plus de troncature ni de chevauchement sur les libellés longs / multi-lignes). Doc : centre d'aide (sessions), README EN/FR, site (showcase), CLAUDE.md. Tests : discovery (hidden) + route hide/unhide/hide-discovered.
This commit is contained in:
@@ -4,18 +4,21 @@
|
||||
<span class="badge bg-amber-900 text-amber-200">{{ t(`sessions.dialog.kind.${dialog.kind}`) }}</span>
|
||||
<span class="text-sm text-amber-100">{{ dialog.waitingFor ?? t('sessions.dialog.waiting') }}</span>
|
||||
</div>
|
||||
<div class="mt-2 flex flex-wrap gap-2">
|
||||
<!-- Options empilées verticalement et pleine largeur : un libellé long (ex. AskUserQuestion) wrappe
|
||||
proprement au lieu d'être tronqué/collé au suivant comme avec un flex-wrap horizontal. -->
|
||||
<div class="mt-2 flex flex-col gap-1.5">
|
||||
<button
|
||||
v-for="opt in dialog.options"
|
||||
:key="opt.n"
|
||||
class="btn text-xs"
|
||||
class="btn w-full items-start justify-start whitespace-normal break-words text-left text-xs"
|
||||
:class="opt.selected ? 'border-amber-400 text-amber-200' : ''"
|
||||
:disabled="busy"
|
||||
@click="respond('select', opt.n)"
|
||||
>
|
||||
<span class="font-mono text-amber-400">{{ opt.n }}</span> {{ opt.label }}
|
||||
<span class="font-mono text-amber-400">{{ opt.n }}</span>
|
||||
<span class="min-w-0 flex-1">{{ opt.label }}</span>
|
||||
</button>
|
||||
<button class="btn-danger text-xs" :disabled="busy" @click="respond('deny')">
|
||||
<button class="btn-danger w-full justify-start text-left text-xs" :disabled="busy" @click="respond('deny')">
|
||||
{{ t('sessions.dialog.deny') }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="relative min-h-0 bg-[#09090b]">
|
||||
<div class="relative h-full min-h-0 bg-[#09090b]">
|
||||
<div ref="container" class="h-full w-full" />
|
||||
<div
|
||||
v-if="!controlling && !ended"
|
||||
@@ -71,6 +71,19 @@ onMounted(async () => {
|
||||
fit.fit();
|
||||
const activeTerm = term;
|
||||
|
||||
// Re-mesure après stabilisation du layout flex : au `onMounted`, le conteneur peut ne pas avoir sa
|
||||
// taille finale (la hauteur flex se résout après le 1er paint) → un seul `fit()` donne trop peu de
|
||||
// lignes et déforme le TUI. On refait donc le calcul en `requestAnimationFrame`, puis on resynchronise
|
||||
// les dimensions PTY. Sert aussi après un resync (reconnexion) où l'écran est remis à plat.
|
||||
const refit = (): void => {
|
||||
try {
|
||||
fit.fit();
|
||||
} catch {
|
||||
/* conteneur sans dimension (démontage en cours) : ignoré */
|
||||
}
|
||||
attachment?.resize(activeTerm.cols, activeTerm.rows);
|
||||
};
|
||||
|
||||
try {
|
||||
attachment = await wsClient.attach({
|
||||
sessionId: props.sessionId,
|
||||
@@ -79,7 +92,10 @@ onMounted(async () => {
|
||||
rows: activeTerm.rows,
|
||||
sink: {
|
||||
write: (data, callback) => activeTerm.write(data, callback),
|
||||
reset: () => activeTerm.reset(),
|
||||
reset: () => {
|
||||
activeTerm.reset();
|
||||
requestAnimationFrame(refit);
|
||||
},
|
||||
onDetached: (reason) => {
|
||||
if (reason === 'session_exit' || reason === 'replaced') ended.value = true;
|
||||
},
|
||||
@@ -99,12 +115,10 @@ onMounted(async () => {
|
||||
return;
|
||||
}
|
||||
controlling.value = attachment.controlling;
|
||||
requestAnimationFrame(refit); // recale la taille une fois le layout fullbleed stabilisé
|
||||
|
||||
activeTerm.onData((data) => attachment?.sendStdin(data));
|
||||
resizeObserver = new ResizeObserver(() => {
|
||||
fit.fit();
|
||||
attachment?.resize(activeTerm.cols, activeTerm.rows);
|
||||
});
|
||||
resizeObserver = new ResizeObserver(refit);
|
||||
resizeObserver.observe(container.value);
|
||||
if (props.mode === 'interactive') activeTerm.focus();
|
||||
});
|
||||
|
||||
@@ -2,7 +2,9 @@
|
||||
<div class="flex min-h-0 w-full flex-1 overflow-hidden">
|
||||
<AppSidebar />
|
||||
<div class="flex min-h-0 min-w-0 flex-1 flex-col">
|
||||
<main v-if="fullbleed" class="flex min-h-0 flex-1 flex-col pb-14 md:pb-0">
|
||||
<!-- fullbleed (terminal) : pas de tabbar mobile ni de padding bas → terminal réellement plein écran
|
||||
(la navigation reste accessible via le lien « ← retour » du header de la vue). -->
|
||||
<main v-if="fullbleed" class="flex min-h-0 flex-1 flex-col">
|
||||
<slot />
|
||||
</main>
|
||||
<main v-else class="flex-1 overflow-y-auto pb-14 md:pb-0">
|
||||
@@ -10,7 +12,7 @@
|
||||
<slot />
|
||||
</div>
|
||||
</main>
|
||||
<MobileTabBar />
|
||||
<MobileTabBar v-if="!fullbleed" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -41,6 +41,12 @@ export default {
|
||||
live: 'live',
|
||||
resumable: 'resumable',
|
||||
sourceDiscovered: 'discovered',
|
||||
hide: 'Hide',
|
||||
unhide: 'Unhide',
|
||||
hiddenBadge: 'hidden',
|
||||
hideDiscovered: 'Hide external history',
|
||||
showHidden: 'Show hidden',
|
||||
hideHidden: 'Hide hidden',
|
||||
clients: 'no client | 1 client | {n} clients',
|
||||
exitCode: 'exit code {code}',
|
||||
statusLabel: {
|
||||
@@ -168,6 +174,8 @@ export default {
|
||||
sessionEnded: 'Session ended',
|
||||
back: 'Sessions',
|
||||
notAttachable: 'This session runs outside Arboretum — fork it to interact, or resume it once it has stopped.',
|
||||
fullscreen: 'Fullscreen',
|
||||
exitFullscreen: 'Exit fullscreen',
|
||||
},
|
||||
push: {
|
||||
enable: 'Enable notifications',
|
||||
@@ -298,6 +306,9 @@ export default {
|
||||
sessionKilled: 'Session killed',
|
||||
sessionResumed: 'Session resumed',
|
||||
sessionForked: 'Session forked',
|
||||
sessionHidden: 'Session hidden',
|
||||
sessionUnhidden: 'Session shown',
|
||||
sessionsHidden: 'no session hidden | 1 session hidden | {n} sessions hidden',
|
||||
repoAdded: 'Repository added',
|
||||
repoRemoved: 'Repository removed',
|
||||
worktreeCreated: 'Worktree created',
|
||||
|
||||
@@ -43,6 +43,12 @@ const fr: typeof en = {
|
||||
live: 'en cours',
|
||||
resumable: 'reprenable',
|
||||
sourceDiscovered: 'découverte',
|
||||
hide: 'Masquer',
|
||||
unhide: 'Réafficher',
|
||||
hiddenBadge: 'masquée',
|
||||
hideDiscovered: 'Masquer l’historique externe',
|
||||
showHidden: 'Afficher les masquées',
|
||||
hideHidden: 'Masquer les masquées',
|
||||
clients: 'aucun client | 1 client | {n} clients',
|
||||
exitCode: 'code de sortie {code}',
|
||||
statusLabel: {
|
||||
@@ -171,6 +177,8 @@ const fr: typeof en = {
|
||||
back: 'Sessions',
|
||||
notAttachable:
|
||||
'Cette session tourne hors d’Arboretum — dupliquez-la pour interagir, ou reprenez-la une fois arrêtée.',
|
||||
fullscreen: 'Plein écran',
|
||||
exitFullscreen: 'Quitter le plein écran',
|
||||
},
|
||||
push: {
|
||||
enable: 'Activer les notifications',
|
||||
@@ -301,6 +309,9 @@ const fr: typeof en = {
|
||||
sessionKilled: 'Session tuée',
|
||||
sessionResumed: 'Session reprise',
|
||||
sessionForked: 'Session dupliquée',
|
||||
sessionHidden: 'Session masquée',
|
||||
sessionUnhidden: 'Session réaffichée',
|
||||
sessionsHidden: 'aucune session masquée | 1 session masquée | {n} sessions masquées',
|
||||
repoAdded: 'Dépôt ajouté',
|
||||
repoRemoved: 'Dépôt retiré',
|
||||
worktreeCreated: 'Worktree créé',
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { defineStore } from 'pinia';
|
||||
import { ref } from 'vue';
|
||||
import type { CreateSessionRequest, SessionResponse, SessionSummary, SessionsListResponse } from '@arboretum/shared';
|
||||
import type { CreateSessionRequest, HideDiscoveredResponse, SessionResponse, SessionSummary, SessionsListResponse } from '@arboretum/shared';
|
||||
import { api } from '../lib/api';
|
||||
import { wsClient, type SessionEvent } from '../lib/ws-client';
|
||||
|
||||
@@ -8,6 +8,8 @@ export const useSessionsStore = defineStore('sessions', () => {
|
||||
const sessions = ref<SessionSummary[]>([]);
|
||||
const loading = ref(false);
|
||||
const loadError = ref<string | null>(null);
|
||||
// false (défaut) : les sessions masquées sont exclues ; true : on les inclut (mode « afficher les masquées »).
|
||||
const showHidden = ref(false);
|
||||
let unsubscribe: (() => void) | null = null;
|
||||
|
||||
// vivantes d'abord, puis par date de création décroissante (même ordre que le serveur)
|
||||
@@ -15,7 +17,17 @@ export const useSessionsStore = defineStore('sessions', () => {
|
||||
return [...list].sort((a, b) => Number(b.live) - Number(a.live) || b.createdAt.localeCompare(a.createdAt));
|
||||
}
|
||||
|
||||
function remove(id: string): void {
|
||||
const idx = sessions.value.findIndex((s) => s.id === id);
|
||||
if (idx >= 0) sessions.value.splice(idx, 1);
|
||||
}
|
||||
|
||||
function upsert(session: SessionSummary): void {
|
||||
// session masquée et mode « afficher les masquées » inactif : on la retire de la liste.
|
||||
if (session.hidden && !showHidden.value) {
|
||||
remove(session.id);
|
||||
return;
|
||||
}
|
||||
const idx = sessions.value.findIndex((s) => s.id === session.id);
|
||||
if (idx >= 0) sessions.value.splice(idx, 1, session);
|
||||
else sessions.value.push(session);
|
||||
@@ -43,7 +55,8 @@ export const useSessionsStore = defineStore('sessions', () => {
|
||||
loading.value = true;
|
||||
loadError.value = null;
|
||||
try {
|
||||
const res = await api.get<SessionsListResponse>('/api/v1/sessions');
|
||||
const url = showHidden.value ? '/api/v1/sessions?includeHidden=true' : '/api/v1/sessions';
|
||||
const res = await api.get<SessionsListResponse>(url);
|
||||
sessions.value = sorted(res.sessions);
|
||||
} catch (err) {
|
||||
loadError.value = err instanceof Error ? err.message : String(err);
|
||||
@@ -52,6 +65,31 @@ export const useSessionsStore = defineStore('sessions', () => {
|
||||
}
|
||||
}
|
||||
|
||||
async function setShowHidden(value: boolean): Promise<void> {
|
||||
showHidden.value = value;
|
||||
await fetchSessions();
|
||||
}
|
||||
|
||||
// Masque une session découverte (retrait optimiste si on n'affiche pas les masquées).
|
||||
async function hideSession(id: string): Promise<void> {
|
||||
await api.post<{ ok: true }>(`/api/v1/sessions/${id}/hide`);
|
||||
if (!showHidden.value) remove(id);
|
||||
}
|
||||
|
||||
// Ré-affiche une session masquée.
|
||||
async function unhideSession(id: string): Promise<void> {
|
||||
await api.delete<{ ok: true }>(`/api/v1/sessions/${id}/hide`);
|
||||
}
|
||||
|
||||
// Masque tout l'historique de sessions externes visible. Renvoie le nombre masqué.
|
||||
async function hideDiscovered(): Promise<number> {
|
||||
const res = await api.post<HideDiscoveredResponse>('/api/v1/sessions/hide-discovered');
|
||||
if (!showHidden.value) {
|
||||
sessions.value = sessions.value.filter((s) => !(s.source === 'discovered' && !s.hidden));
|
||||
}
|
||||
return res.hidden;
|
||||
}
|
||||
|
||||
function startRealtime(): void {
|
||||
unsubscribe ??= wsClient.subscribeSessions(onEvent);
|
||||
}
|
||||
@@ -90,12 +128,17 @@ export const useSessionsStore = defineStore('sessions', () => {
|
||||
sessions,
|
||||
loading,
|
||||
loadError,
|
||||
showHidden,
|
||||
fetchSessions,
|
||||
setShowHidden,
|
||||
startRealtime,
|
||||
stopRealtime,
|
||||
createSession,
|
||||
killSession,
|
||||
resumeSession,
|
||||
forkSession,
|
||||
hideSession,
|
||||
unhideSession,
|
||||
hideDiscovered,
|
||||
};
|
||||
});
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="flex min-h-0 flex-col">
|
||||
<div ref="root" class="flex min-h-0 flex-1 flex-col bg-[#09090b]">
|
||||
<header class="flex items-center gap-3 border-b border-zinc-800 bg-zinc-900/60 px-3 py-1.5">
|
||||
<RouterLink :to="{ name: 'sessions' }" class="text-sm text-zinc-400 transition-colors hover:text-zinc-200">
|
||||
← {{ t('terminal.back') }}
|
||||
@@ -8,6 +8,15 @@
|
||||
<span class="truncate font-mono text-xs text-zinc-500" :title="session?.cwd ?? sessionId">
|
||||
{{ session?.cwd ?? sessionId }}
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
class="ml-auto shrink-0 rounded p-1 text-zinc-400 transition-colors hover:bg-zinc-800 hover:text-zinc-200"
|
||||
:title="t(isFullscreen ? 'terminal.exitFullscreen' : 'terminal.fullscreen')"
|
||||
:aria-label="t(isFullscreen ? 'terminal.exitFullscreen' : 'terminal.fullscreen')"
|
||||
@click="toggleFullscreen"
|
||||
>
|
||||
<component :is="isFullscreen ? Minimize : Maximize" class="h-4 w-4" />
|
||||
</button>
|
||||
</header>
|
||||
<DialogPrompt v-if="session && session.activity === 'waiting'" :session="session" class="m-2" />
|
||||
<TerminalView v-if="attachable" :key="`${sessionId}:${mode}`" :session-id="sessionId" :mode="mode" class="flex-1" />
|
||||
@@ -19,9 +28,10 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted } from 'vue';
|
||||
import { computed, onBeforeUnmount, onMounted, ref, useTemplateRef } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { Maximize, Minimize } from '@lucide/vue';
|
||||
import { useSessionsStore } from '../stores/sessions';
|
||||
import TerminalView from '../components/TerminalView.vue';
|
||||
import DialogPrompt from '../components/DialogPrompt.vue';
|
||||
@@ -36,8 +46,25 @@ const session = computed(() => store.sessions.find((s) => s.id === sessionId.val
|
||||
// défaut true : une navigation directe (liste pas encore chargée) tente l'attache d'une session managée.
|
||||
const attachable = computed(() => session.value?.attachable ?? true);
|
||||
|
||||
// Vrai plein écran (API Fullscreen) — surtout utile sur mobile pour récupérer la barre d'URL.
|
||||
// On synchronise `isFullscreen` sur l'événement natif : l'utilisateur peut sortir via Échap.
|
||||
const root = useTemplateRef<HTMLDivElement>('root');
|
||||
const isFullscreen = ref(false);
|
||||
function syncFullscreen(): void {
|
||||
isFullscreen.value = document.fullscreenElement === root.value;
|
||||
}
|
||||
function toggleFullscreen(): void {
|
||||
if (document.fullscreenElement) void document.exitFullscreen().catch(() => {});
|
||||
else void root.value?.requestFullscreen().catch(() => {});
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
// navigation directe : garantit l'en-tête (cwd/commande) même si l'AppShell n'a pas encore chargé.
|
||||
if (store.sessions.length === 0) void store.fetchSessions();
|
||||
document.addEventListener('fullscreenchange', syncFullscreen);
|
||||
});
|
||||
onBeforeUnmount(() => {
|
||||
document.removeEventListener('fullscreenchange', syncFullscreen);
|
||||
if (document.fullscreenElement === root.value) void document.exitFullscreen().catch(() => {});
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
<template>
|
||||
<PageHeader :title="t('nav.sessions')">
|
||||
<BaseButton v-if="hasDiscovered" variant="ghost" size="sm" :icon="EyeOff" :loading="hidingAll" @click="onHideDiscovered">
|
||||
{{ t('sessions.hideDiscovered') }}
|
||||
</BaseButton>
|
||||
<BaseButton variant="ghost" size="sm" :icon="store.showHidden ? EyeOff : Eye" @click="onToggleHidden">
|
||||
{{ t(store.showHidden ? 'sessions.hideHidden' : 'sessions.showHidden') }}
|
||||
</BaseButton>
|
||||
<BaseButton variant="ghost" size="sm" :icon="RefreshCw" :loading="store.loading" @click="store.fetchSessions()">
|
||||
{{ t('common.refresh') }}
|
||||
</BaseButton>
|
||||
@@ -43,6 +49,7 @@
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="badge bg-zinc-800 font-mono text-zinc-300">{{ s.command }}</span>
|
||||
<BaseBadge v-if="s.source === 'discovered'" tone="sky">{{ t('sessions.sourceDiscovered') }}</BaseBadge>
|
||||
<BaseBadge v-if="s.hidden" tone="zinc">{{ t('sessions.hiddenBadge') }}</BaseBadge>
|
||||
<SessionStateBadge :session="s" />
|
||||
<span class="ml-auto text-xs text-zinc-500">{{ formatDate(s.createdAt) }}</span>
|
||||
</div>
|
||||
@@ -73,6 +80,9 @@
|
||||
<BaseButton variant="primary" size="sm" :icon="Play" :loading="actingId === s.id" @click="onResume(s.id)">{{ t('sessions.resume') }}</BaseButton>
|
||||
<BaseButton size="sm" :icon="GitFork" :loading="actingId === s.id" @click="onFork(s.id)">{{ t('sessions.fork') }}</BaseButton>
|
||||
</template>
|
||||
<!-- masquage : réafficher une session masquée, ou masquer une session découverte qui pollue la liste -->
|
||||
<BaseButton v-if="s.hidden" variant="ghost" size="sm" :icon="Eye" :loading="hidingId === s.id" @click="onUnhide(s.id)">{{ t('sessions.unhide') }}</BaseButton>
|
||||
<BaseButton v-else-if="s.source === 'discovered'" variant="ghost" size="sm" :icon="EyeOff" :loading="hidingId === s.id" @click="onHide(s.id)">{{ t('sessions.hide') }}</BaseButton>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
@@ -88,11 +98,11 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted, ref } from 'vue';
|
||||
import { computed, onMounted, ref } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import type { SessionSummary } from '@arboretum/shared';
|
||||
import { RefreshCw, FolderOpen, Plus, TerminalSquare, SquareTerminal, Eye, Trash2, GitFork, Play, Search } from '@lucide/vue';
|
||||
import { RefreshCw, FolderOpen, Plus, TerminalSquare, SquareTerminal, Eye, EyeOff, Trash2, GitFork, Play, Search } from '@lucide/vue';
|
||||
import { useSessionsStore } from '../stores/sessions';
|
||||
import { useWorktreesStore } from '../stores/worktrees';
|
||||
import { useToastsStore } from '../stores/toasts';
|
||||
@@ -130,6 +140,10 @@ const creating = ref(false);
|
||||
const killArmedId = ref<string | null>(null);
|
||||
const killing = ref(false);
|
||||
const actingId = ref<string | null>(null);
|
||||
const hidingId = ref<string | null>(null);
|
||||
const hidingAll = ref(false);
|
||||
// au moins une session externe visible → propose le masquage de masse
|
||||
const hasDiscovered = computed(() => store.sessions.some((s) => s.source === 'discovered' && !s.hidden));
|
||||
|
||||
onMounted(() => {
|
||||
// navigation directe : garantit la liste même si l'AppShell n'a pas encore chargé.
|
||||
@@ -197,4 +211,48 @@ async function onConfirmKill(id: string): Promise<void> {
|
||||
killArmedId.value = null;
|
||||
}
|
||||
}
|
||||
|
||||
async function onHide(id: string): Promise<void> {
|
||||
hidingId.value = id;
|
||||
try {
|
||||
await store.hideSession(id);
|
||||
toasts.success(t('toast.sessionHidden'));
|
||||
} catch (err) {
|
||||
toasts.error(err);
|
||||
} finally {
|
||||
hidingId.value = null;
|
||||
}
|
||||
}
|
||||
|
||||
async function onUnhide(id: string): Promise<void> {
|
||||
hidingId.value = id;
|
||||
try {
|
||||
await store.unhideSession(id);
|
||||
toasts.success(t('toast.sessionUnhidden'));
|
||||
} catch (err) {
|
||||
toasts.error(err);
|
||||
} finally {
|
||||
hidingId.value = null;
|
||||
}
|
||||
}
|
||||
|
||||
async function onHideDiscovered(): Promise<void> {
|
||||
hidingAll.value = true;
|
||||
try {
|
||||
const n = await store.hideDiscovered();
|
||||
toasts.success(t('toast.sessionsHidden', n));
|
||||
} catch (err) {
|
||||
toasts.error(err);
|
||||
} finally {
|
||||
hidingAll.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function onToggleHidden(): Promise<void> {
|
||||
try {
|
||||
await store.setShowHidden(!store.showHidden);
|
||||
} catch (err) {
|
||||
toasts.error(err);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -70,6 +70,14 @@ const en: HelpSection[] = [
|
||||
title: 'Discovered sessions',
|
||||
body: 'Sessions you start in your own terminal are detected automatically and tagged as discovered. Observe them read-only, or resume/fork them once stopped.',
|
||||
},
|
||||
{
|
||||
title: 'Hide noisy history',
|
||||
body: 'Old sessions started outside Arboretum can pile up. Hide one from its row, or use “Hide external history” to clear them all in one click. Hidden sessions stay resumable and survive a re-scan — toggle “Show hidden” to bring them back.',
|
||||
},
|
||||
{
|
||||
title: 'Fullscreen terminal',
|
||||
body: 'The web terminal fills the whole screen, with the prompt pinned to the bottom and full scrollback above. Tap the fullscreen button in the header to go edge-to-edge — handy on phones.',
|
||||
},
|
||||
{
|
||||
title: 'Resume & fork',
|
||||
body: 'Resume restarts a stopped session in its original working directory, with full scrollback replayed. Fork creates a fresh independent copy with the same command and cwd.',
|
||||
@@ -238,6 +246,14 @@ const fr: HelpSection[] = [
|
||||
title: 'Sessions découvertes',
|
||||
body: 'Les sessions lancées dans votre propre terminal sont détectées automatiquement et marquées « découvertes ». Observez-les en lecture seule, ou reprenez/forkez-les une fois arrêtées.',
|
||||
},
|
||||
{
|
||||
title: 'Masquer l’historique',
|
||||
body: 'Les anciennes sessions lancées hors d’Arboretum peuvent s’accumuler. Masquez-en une depuis sa ligne, ou utilisez « Masquer l’historique externe » pour toutes les retirer en un clic. Les sessions masquées restent reprenables et survivent à un nouveau scan — activez « Afficher les masquées » pour les retrouver.',
|
||||
},
|
||||
{
|
||||
title: 'Terminal plein écran',
|
||||
body: 'Le terminal web occupe tout l’écran, avec l’invite ancrée en bas et tout l’historique défilable au-dessus. Touchez le bouton plein écran dans l’en-tête pour passer bord à bord — pratique sur téléphone.',
|
||||
},
|
||||
{
|
||||
title: 'Reprendre & forker',
|
||||
body: 'Reprendre relance une session arrêtée dans son répertoire d’origine, avec tout l’historique rejoué. Forker crée une copie indépendante neuve avec la même commande et le même cwd.',
|
||||
|
||||
Reference in New Issue
Block a user