Nouvelle commande WS de haut niveau `answer {channel, action, optionN?}`
traduite côté serveur en keystrokes (chiffre+Entrée / Entrée / Esc) et
validée contre le dialogue courant du tracker (anti-frappe fantôme mobile).
- protocol.ts : message `answer` + validation parseClientMessage + ErrorCode INVALID_ANSWER
- pty-manager.answer() : mappe l'intention en write PTY, réutilise le modèle mono-utilisateur
- gateway : case `answer` (NOT_CONTROLLING / SESSION_EXITED / INVALID_ANSWER)
- web : Attachment.answer(), DialogPrompt.vue (attache interactive légère sink no-op),
intégré dans SessionView, SessionsListView, WorktreeCard + i18n en/fr
- tests : parseClientMessage (answer valide/malformé + fuzz), pty-manager.answer (163 verts)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
49 lines
2.3 KiB
Vue
49 lines
2.3 KiB
Vue
<template>
|
|
<div class="flex min-h-0 flex-col">
|
|
<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') }}
|
|
</RouterLink>
|
|
<span v-if="session" class="badge bg-zinc-800 font-mono text-zinc-300">{{ session.command }}</span>
|
|
<span class="truncate font-mono text-xs text-zinc-500" :title="session?.cwd ?? sessionId">
|
|
{{ session?.cwd ?? sessionId }}
|
|
</span>
|
|
<div class="ml-auto">
|
|
<LanguageSwitcher />
|
|
</div>
|
|
</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" />
|
|
<!-- session externe non détenue par Arboretum : pas de terminal interactif (cf. fork/resume). -->
|
|
<div v-else class="flex flex-1 items-center justify-center p-6 text-center">
|
|
<p class="max-w-md text-sm text-zinc-400">{{ t('terminal.notAttachable') }}</p>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { computed, onMounted } from 'vue';
|
|
import { useRoute } from 'vue-router';
|
|
import { useI18n } from 'vue-i18n';
|
|
import { useSessionsStore } from '../stores/sessions';
|
|
import TerminalView from '../components/TerminalView.vue';
|
|
import DialogPrompt from '../components/DialogPrompt.vue';
|
|
import LanguageSwitcher from '../components/LanguageSwitcher.vue';
|
|
|
|
const { t } = useI18n();
|
|
const route = useRoute();
|
|
const store = useSessionsStore();
|
|
|
|
const sessionId = computed(() => String(route.params.id));
|
|
const mode = computed<'interactive' | 'observer'>(() => (route.query.mode === 'observer' ? 'observer' : 'interactive'));
|
|
const session = computed(() => store.sessions.find((s) => s.id === sessionId.value) ?? null);
|
|
// 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);
|
|
|
|
onMounted(() => {
|
|
// navigation directe : la liste n'est pas encore chargée (en-tête cwd/commande)
|
|
if (store.sessions.length === 0) void store.fetchSessions();
|
|
store.startRealtime();
|
|
});
|
|
</script>
|