Skip to content
Merged
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
7 changes: 4 additions & 3 deletions .cursor/rules/project.mdc
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,10 @@ API keys live in **`.env.project`** (gitignored). Typical keys: `JIRA_API_TOKEN`
If the user describes work and does **not** give a `CLEAN-<n>` id, that description *is* the ticket. Do not start code, SpecKit, Brainstorm, or a git branch first.

1. Create a Jira issue in CLEAN from their description (title + short summary). Use `.env.project`.
2. Reply with the issue key and URL.
3. Move it to In Progress.
4. Then the usual flow: branch → work → comments → PR.
2. Mark surface: title prefix `[ADMIN]` / `[APP]` / `[ADMIN][APP]`, plus Jira labels `admin` / `app`. Infer from `admin/` vs `app/`; if unclear, ask. API-only → `[API]`.
3. Reply with the issue key and URL.
4. Move it to In Progress.
5. Then the usual flow: branch → work → comments → PR.

## OpenAPI

Expand Down
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ At the start of every task, read `.cursor/rules/project.mdc` (project card) and
- Git identity: folder `E:/code/dream/` → Maksym Hryzodub [DREAM] / `github-dream`
- Keys: `JIRA_API_TOKEN`, `JIRA_DOMAIN`, `JIRA_EMAIL`, `GITHUB_TOKEN`, and any other tokens in `.env.project` only

**No `CLEAN-<n>` in the request:** the user's description *is* the ticket. Create the Jira issue first (keys in `.env.project`), send back the key/URL, move to In Progress — then branch/code. Do not start SpecKit or implementation without that id.
**No `CLEAN-<n>` in the request:** the user's description *is* the ticket. Create the Jira issue first (keys in `.env.project`), mark `[ADMIN]` / `[APP]` in the title and labels, send back the key/URL, move to In Progress — then branch/code. Do not start SpecKit or implementation without that id.

**OpenAPI:** if SDK/types or `api/swagger-spec.json` are missing, regenerate first (`cd api && bun run generate:swagger` or `bun run build`, then `cd admin && bun run build:api` / `cd app && bun run build:api`). Ask where the schema is only after that fails.

Expand Down
50 changes: 44 additions & 6 deletions app/i18n.sync.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,50 @@
"version": 1,
"hashes": {
"agent:ru": {
"agent_name": "28145084c725",
"agent_resources": "e89b30aa1dc3",
"agent_status": "920e413c7d41",
"agent_template": "0575f29df888",
"agents": "279b44d2ab4b",
"create_agent": "5a614e73e098"
"card.open_chat": "0600175af842",
"card.updated": "f0a6d28abcbc",
"chat.back_to_agents": "7da2164b2679",
"chat.elapsed": "e7a16571c3e4",
"chat.loading": "ba3bbbe10d8b",
"chat.not_found": "a50ba166a23a",
"chat.restart": "6b983a81e5e8",
"chat.restart_failed": "b8864cf013b4",
"chat.restarting": "75d0f1469d16",
"chat.unavailable_hint": "b14ea709f7e0",
"chat.unavailable_title": "d6bf283bee98",
"item.load_error": "af04f385814b",
"item.loading": "7e1e624a3210",
"item.resources": "e89b30aa1dc3",
"item.resources_value": "d4c134c14b82",
"item.template": "0575f29df888",
"list.cluster_full": "129b2952a7dd",
"list.cluster_no_nodes": "4b5656d4657f",
"list.create": "5a614e73e098",
"list.empty_cta": "88dcddea044f",
"list.empty_hint": "3b00ddd1d6b2",
"list.empty_no_permission": "11c1123faf1d",
"list.empty_title": "971b61f8cb4d",
"list.lede": "1a2a5c6eb9e9",
"list.running_of": "dafe09b6ae07",
"list.slots_free": "aa856cbf3070",
"list.title": "279b44d2ab4b",
"relative_time.days": "a41234397f3c",
"relative_time.hours": "520c737ea624",
"relative_time.just_now": "7ddb44d8a533",
"relative_time.minutes": "0a03167c208c",
"restart_stage.almost_body": "cc88c514c77b",
"restart_stage.almost_title": "0ceb8f97a4cc",
"restart_stage.deploying_body": "4cc2d4111a84",
"restart_stage.deploying_title": "dfdc8e60ff35",
"restart_stage.starting_body": "f048e1dcc8ca",
"restart_stage.starting_title": "a1661573d16c",
"restart_stage.stopping_body": "4d3eb76d014c",
"restart_stage.stopping_title": "93f2bb08afc2",
"status.deploying": "4338bdb0808b",
"status.failed": "031a8f0f659d",
"status.pending": "331551b0de41",
"status.running": "f4ccae29e1bb",
"status.stopped": "1a4f630ac1b6"
},
"bridle:ru": {
"chat.agent": "11b39c93777e",
Expand Down
13 changes: 10 additions & 3 deletions app/slices/agent/components/agent/Item.vue
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,19 @@
</div>
<div class="grid grid-cols-2 gap-4">
<div>
<p class="text-sm text-muted-foreground">Template</p>
<p class="text-sm text-muted-foreground">{{ $t('item.template') }}</p>
<p>{{ agent.templateId }}</p>
</div>
<div>
<p class="text-sm text-muted-foreground">Resources</p>
<p>CPU: {{ agent.resources.cpu }} / Memory: {{ agent.resources.memory }}</p>
<p class="text-sm text-muted-foreground">{{ $t('item.resources') }}</p>
<p>
{{
$t('item.resources_value', {
cpu: agent.resources.cpu,
memory: agent.resources.memory,
})
}}
</p>
</div>
</div>
</div>
Expand Down
4 changes: 2 additions & 2 deletions app/slices/agent/components/agent/Provider.vue
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<template>
<div v-if="pending">Loading agent...</div>
<div v-else-if="error">Error loading agent</div>
<div v-if="pending">{{ $t('item.loading') }}</div>
<div v-else-if="error">{{ $t('item.load_error') }}</div>
<div v-else-if="agent">
<AgentItem :agent="agent" />
</div>
Expand Down
62 changes: 33 additions & 29 deletions app/slices/agent/components/agentChat/Provider.vue
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ const canManage = computed(() =>

const restarting = ref(false);
const restartError = ref<string | null>(null);
const restartFailed = ref(false);
/** Wallclock the user clicked Restart — used to compute progress / time. */
const restartStartedAt = ref<number | null>(null);

Expand All @@ -29,6 +30,7 @@ async function onRestart() {
if (!agent.value || restarting.value) return;
restarting.value = true;
restartError.value = null;
restartFailed.value = false;
restartStartedAt.value = Date.now();
const previous = agent.value.status;
// Optimistic flip — overlay appears immediately, status pill animates.
Expand All @@ -38,7 +40,8 @@ async function onRestart() {
await refresh();
} catch (err) {
if (agent.value) agent.value = { ...agent.value, status: previous };
restartError.value = (err as Error).message || 'Restart failed';
restartFailed.value = true;
restartError.value = (err as Error).message || null;
restartStartedAt.value = null;
} finally {
restarting.value = false;
Expand Down Expand Up @@ -76,17 +79,18 @@ const statusMeta = computed(() => {
const status = agent.value?.status;
switch (status) {
case 'running':
return { label: 'Running', dot: 'bg-emerald-500', pulse: true };
return { labelKey: 'status.running', label: status, dot: 'bg-emerald-500', pulse: true };
case 'deploying':
return { label: 'Deploying', dot: 'bg-amber-500', pulse: true };
return { labelKey: 'status.deploying', label: status, dot: 'bg-amber-500', pulse: true };
case 'pending':
return { label: 'Pending', dot: 'bg-amber-500', pulse: true };
return { labelKey: 'status.pending', label: status, dot: 'bg-amber-500', pulse: true };
case 'failed':
return { label: 'Failed', dot: 'bg-rose-500', pulse: false };
return { labelKey: 'status.failed', label: status, dot: 'bg-rose-500', pulse: false };
case 'stopped':
return { label: 'Stopped', dot: 'bg-muted-foreground', pulse: false };
return { labelKey: 'status.stopped', label: status, dot: 'bg-muted-foreground', pulse: false };
default:
return { label: status ?? 'Unknown', dot: 'bg-muted-foreground', pulse: false };
// No wording for a status the runtime invented — show it verbatim.
return { labelKey: null, label: status ?? '', dot: 'bg-muted-foreground', pulse: false };
}
});

Expand Down Expand Up @@ -116,30 +120,30 @@ onBeforeUnmount(() => {
if (elapsedTimer) clearInterval(elapsedTimer);
});

const overlayStage = computed<{ title: string; subtitle: string }>(() => {
const overlayStage = computed<{ titleKey: string; bodyKey: string }>(() => {
const sec = elapsedSec.value;
// Stages chosen by feel — a typical k8s pod restart is 10–25s.
if (sec < 3) {
return {
title: 'Stopping current pod…',
subtitle: 'Cancelling the running workflow.',
titleKey: 'restart_stage.stopping_title',
bodyKey: 'restart_stage.stopping_body',
};
}
if (sec < 10) {
return {
title: 'Deploying new pod…',
subtitle: 'Scheduling on the cluster.',
titleKey: 'restart_stage.deploying_title',
bodyKey: 'restart_stage.deploying_body',
};
}
if (sec < 20) {
return {
title: 'Starting up…',
subtitle: 'The container is pulling and initialising.',
titleKey: 'restart_stage.starting_title',
bodyKey: 'restart_stage.starting_body',
};
}
return {
title: 'Almost ready…',
subtitle: 'Waiting for the agent to come online.',
titleKey: 'restart_stage.almost_title',
bodyKey: 'restart_stage.almost_body',
};
});

Expand All @@ -164,7 +168,7 @@ const initials = computed(() => {
<NuxtLink
to="/agents"
class="inline-flex items-center justify-center rounded-md p-2 text-muted-foreground hover:text-foreground hover:bg-muted transition"
aria-label="Back to agents"
:aria-label="$t('chat.back_to_agents')"
>
<Icon name="arrow-left" :size="16" />
</NuxtLink>
Expand All @@ -180,8 +184,8 @@ const initials = computed(() => {
<div class="flex items-center gap-2">
<h1 class="truncate text-sm font-semibold">
<template v-if="agent">{{ agent.name }}</template>
<template v-else-if="pending">Loading…</template>
<template v-else>Agent not found</template>
<template v-else-if="pending">{{ $t('chat.loading') }}</template>
<template v-else>{{ $t('chat.not_found') }}</template>
</h1>
<span
v-if="agent"
Expand All @@ -198,7 +202,7 @@ const initials = computed(() => {
:class="statusMeta.dot"
/>
</span>
{{ statusMeta.label }}
{{ statusMeta.labelKey ? $t(statusMeta.labelKey) : statusMeta.label }}
</span>
</div>
<p
Expand All @@ -222,19 +226,19 @@ const initials = computed(() => {
:size="13"
:class="isTransitioning ? 'animate-spin' : undefined"
/>
{{ isTransitioning ? 'Restarting…' : 'Restart' }}
{{ $t(isTransitioning ? 'chat.restarting' : 'chat.restart') }}
</button>
</div>
</header>

<div
v-if="restartError"
v-if="restartFailed"
class="shrink-0 border-b bg-rose-500/10"
>
<p
class="mx-auto w-full max-w-3xl px-4 py-2 text-xs text-rose-700 dark:text-rose-400"
>
{{ restartError }}
{{ restartError ?? $t('chat.restart_failed') }}
</p>
</div>

Expand All @@ -251,16 +255,16 @@ const initials = computed(() => {
<Icon name="bot-off" :size="22" />
</div>
<div>
<p class="text-sm font-medium">Agent unavailable</p>
<p class="text-sm font-medium">{{ $t('chat.unavailable_title') }}</p>
<p class="mt-1 text-xs text-muted-foreground">
This agent may have been removed or the runtime is offline.
{{ $t('chat.unavailable_hint') }}
</p>
</div>
<NuxtLink
to="/agents"
class="text-xs font-medium text-primary hover:underline"
>
Back to agents
{{ $t('chat.back_to_agents') }}
</NuxtLink>
</div>
<BridleChatProvider
Expand Down Expand Up @@ -305,9 +309,9 @@ const initials = computed(() => {
</span>
</div>

<h3 class="text-sm font-semibold">{{ overlayStage.title }}</h3>
<h3 class="text-sm font-semibold">{{ $t(overlayStage.titleKey) }}</h3>
<p class="mt-1.5 text-xs text-muted-foreground">
{{ overlayStage.subtitle }}
{{ $t(overlayStage.bodyKey) }}
</p>

<!-- Indeterminate progress bar — visual only, since k8s doesn't
Expand All @@ -317,7 +321,7 @@ const initials = computed(() => {
</div>

<p class="mt-3 text-[11px] text-muted-foreground/70">
Elapsed {{ elapsedSec }}s · agent will reload memory from S3
{{ $t('chat.elapsed', { seconds: elapsedSec }) }}
</p>
</div>
</div>
Expand Down
Loading
Loading