@agentprojectcontext/apx 1.76.0 → 1.77.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/core/net/lan.js +114 -0
- package/src/core/profiles/bundled/secretary/PROFILE.md +6 -5
- package/src/core/profiles/bundled/secretary/profile.json +19 -6
- package/src/core/stores/agent-inbox.js +153 -0
- package/src/core/stores/conversations.js +21 -1
- package/src/host/daemon/api/inbox.js +45 -0
- package/src/host/daemon/api/web.js +2 -2
- package/src/host/daemon/api.js +2 -0
- package/src/interfaces/cli/commands/panel.js +131 -0
- package/src/interfaces/cli/index.js +28 -0
- package/src/interfaces/web/dist/assets/{index-Bjlk9ttU.js → index-BWWpTTSd.js} +135 -135
- package/src/interfaces/web/dist/assets/index-BWWpTTSd.js.map +1 -0
- package/src/interfaces/web/dist/assets/index-D_EJEA1n.css +1 -0
- package/src/interfaces/web/dist/index.html +2 -2
- package/src/interfaces/web/src/App.tsx +2 -0
- package/src/interfaces/web/src/components/layout/ProjectSidebar.tsx +20 -1
- package/src/interfaces/web/src/components/settings/ProfilePanel.tsx +105 -72
- package/src/interfaces/web/src/hooks/useInbox.ts +12 -0
- package/src/interfaces/web/src/i18n/en.ts +16 -0
- package/src/interfaces/web/src/i18n/es.ts +16 -0
- package/src/interfaces/web/src/lib/api/inbox.ts +26 -0
- package/src/interfaces/web/src/screens/InboxScreen.tsx +103 -0
- package/src/interfaces/web/src/screens/SettingsScreen.tsx +1 -1
- package/src/core/profiles/bundled/secretary/PROFILE.es.md +0 -44
- package/src/interfaces/web/dist/assets/index-Bjlk9ttU.js.map +0 -1
- package/src/interfaces/web/dist/assets/index-CQ5kyFej.css +0 -1
|
@@ -89,26 +89,33 @@ export function ProfilePanel() {
|
|
|
89
89
|
const props: Record<string, ProfileSchemaProp> = profile?.schema?.properties || {};
|
|
90
90
|
const overBudget = !!profile?.budget && !!profile?.tokens && profile.tokens > profile.budget;
|
|
91
91
|
|
|
92
|
+
// Settings are editable only for the ACTIVE profile — changing them
|
|
93
|
+
// reschedules its routines, which is meaningless for one that is not running.
|
|
94
|
+
const canEdit = !!profile?.active;
|
|
95
|
+
|
|
92
96
|
return (
|
|
93
97
|
<div className="flex flex-col gap-4" data-testid="profile-panel">
|
|
94
98
|
<Section
|
|
95
99
|
title={t("settings.profile.title")}
|
|
96
100
|
description={t("settings.profile.subtitle")}
|
|
97
101
|
>
|
|
98
|
-
{
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
102
|
+
{/* Always rendered, in both states. A banner that only appears when
|
|
103
|
+
inactive would push every card below it down the moment you
|
|
104
|
+
activate — the exact reflow this layout is built to avoid. */}
|
|
105
|
+
<div
|
|
106
|
+
data-testid="profile-vanilla-hint"
|
|
107
|
+
className="mb-3 flex items-start gap-2 rounded-md border border-border bg-muted/40 p-3 text-sm"
|
|
108
|
+
>
|
|
109
|
+
<Info size={16} className="mt-0.5 shrink-0 opacity-70" />
|
|
110
|
+
<span>
|
|
111
|
+
{active ? t("settings.profile.active_hint") : t("settings.profile.vanilla_hint")}
|
|
112
|
+
</span>
|
|
113
|
+
</div>
|
|
107
114
|
|
|
108
115
|
{!profiles.length ? (
|
|
109
116
|
<Empty>{t("settings.profile.none_available")}</Empty>
|
|
110
117
|
) : (
|
|
111
|
-
<div className="
|
|
118
|
+
<div className="grid gap-2 lg:grid-cols-2">
|
|
112
119
|
{profiles.map((p) => (
|
|
113
120
|
<button
|
|
114
121
|
key={p.id}
|
|
@@ -120,7 +127,7 @@ export function ProfilePanel() {
|
|
|
120
127
|
}`}
|
|
121
128
|
>
|
|
122
129
|
<span className="min-w-0">
|
|
123
|
-
<span className="flex items-center gap-2">
|
|
130
|
+
<span className="flex flex-wrap items-center gap-2">
|
|
124
131
|
<span className="font-medium">{p.name}</span>
|
|
125
132
|
<Badge>{p.source}</Badge>
|
|
126
133
|
{p.active ? <Badge tone="success">{t("settings.profile.active")}</Badge> : null}
|
|
@@ -155,75 +162,101 @@ export function ProfilePanel() {
|
|
|
155
162
|
) : null}
|
|
156
163
|
</Section>
|
|
157
164
|
|
|
158
|
-
{
|
|
165
|
+
{/* Settings, Doctor and the prompt block are ALWAYS mounted, in the same
|
|
166
|
+
places, whether or not a profile is active. Activating one must not
|
|
167
|
+
make cards appear and push the rest of the page down — a layout that
|
|
168
|
+
reflows under you is disorienting, and it hides the fact that the
|
|
169
|
+
prompt block is the thing that just changed. Inactive simply means
|
|
170
|
+
disabled. */}
|
|
171
|
+
<div className="grid items-start gap-4 xl:grid-cols-2">
|
|
159
172
|
<Section
|
|
160
173
|
title={t("settings.profile.settings_title")}
|
|
161
174
|
description={t("settings.profile.settings_subtitle")}
|
|
162
175
|
>
|
|
163
|
-
|
|
164
|
-
{
|
|
165
|
-
<Field key={key} label={def.title || key} hint={def.description}>
|
|
166
|
-
{def.enum ? (
|
|
167
|
-
<UiSelect
|
|
168
|
-
value={draft[key] ?? String(def.default ?? "")}
|
|
169
|
-
onChange={(v) => setDraft({ ...draft, [key]: v })}
|
|
170
|
-
options={def.enum.map((o) => ({ value: String(o), label: String(o) }))}
|
|
171
|
-
/>
|
|
172
|
-
) : (
|
|
173
|
-
<Input
|
|
174
|
-
value={draft[key] ?? ""}
|
|
175
|
-
onChange={(e) => setDraft({ ...draft, [key]: e.target.value })}
|
|
176
|
-
/>
|
|
177
|
-
)}
|
|
178
|
-
</Field>
|
|
179
|
-
))}
|
|
180
|
-
</div>
|
|
181
|
-
<div className="mt-4">
|
|
182
|
-
<Button variant="primary" loading={busy} onClick={saveConfig}>
|
|
183
|
-
{t("common.save")}
|
|
184
|
-
</Button>
|
|
185
|
-
</div>
|
|
186
|
-
</Section>
|
|
187
|
-
) : null}
|
|
188
|
-
|
|
189
|
-
{active && doctor ? (
|
|
190
|
-
<Section title={t("settings.profile.doctor_title")} description={doctor.summary}>
|
|
191
|
-
{!doctor.checks.length ? (
|
|
192
|
-
<div className="flex items-center gap-2 text-sm">
|
|
193
|
-
<CheckCircle2 size={16} className="text-emerald-500" />
|
|
194
|
-
{t("settings.profile.doctor_clean")}
|
|
195
|
-
</div>
|
|
176
|
+
{!Object.keys(props).length ? (
|
|
177
|
+
<Empty>{t("settings.profile.no_settings")}</Empty>
|
|
196
178
|
) : (
|
|
197
|
-
|
|
198
|
-
{
|
|
199
|
-
<
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
179
|
+
<>
|
|
180
|
+
{!canEdit ? (
|
|
181
|
+
<p className="mb-3 text-sm opacity-60">{t("settings.profile.settings_locked")}</p>
|
|
182
|
+
) : null}
|
|
183
|
+
<div className="grid gap-3 sm:grid-cols-2">
|
|
184
|
+
{Object.entries(props).map(([key, def]) => (
|
|
185
|
+
<Field key={key} label={def.title || key} hint={def.description}>
|
|
186
|
+
{def.enum ? (
|
|
187
|
+
<UiSelect
|
|
188
|
+
value={draft[key] ?? String(def.default ?? "")}
|
|
189
|
+
onChange={(v) => setDraft({ ...draft, [key]: v })}
|
|
190
|
+
options={def.enum.map((o) => ({ value: String(o), label: String(o) }))}
|
|
191
|
+
disabled={!canEdit}
|
|
192
|
+
/>
|
|
193
|
+
) : (
|
|
194
|
+
<Input
|
|
195
|
+
value={draft[key] ?? ""}
|
|
196
|
+
disabled={!canEdit}
|
|
197
|
+
onChange={(e) => setDraft({ ...draft, [key]: e.target.value })}
|
|
198
|
+
/>
|
|
199
|
+
)}
|
|
200
|
+
</Field>
|
|
201
|
+
))}
|
|
202
|
+
</div>
|
|
203
|
+
<div className="mt-4">
|
|
204
|
+
<Button variant="primary" loading={busy} disabled={!canEdit} onClick={saveConfig}>
|
|
205
|
+
{t("common.save")}
|
|
206
|
+
</Button>
|
|
207
|
+
</div>
|
|
208
|
+
</>
|
|
213
209
|
)}
|
|
214
210
|
</Section>
|
|
215
|
-
) : null}
|
|
216
211
|
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
212
|
+
<div className="flex flex-col gap-4">
|
|
213
|
+
<Section title={t("settings.profile.doctor_title")} description={doctor?.summary || ""}>
|
|
214
|
+
{!doctor?.checks?.length ? (
|
|
215
|
+
<div className="flex items-center gap-2 text-sm">
|
|
216
|
+
<CheckCircle2 size={16} className="text-emerald-500" />
|
|
217
|
+
{active ? t("settings.profile.doctor_clean") : t("settings.profile.doctor_vanilla")}
|
|
218
|
+
</div>
|
|
219
|
+
) : (
|
|
220
|
+
<ul className="flex flex-col gap-2">
|
|
221
|
+
{doctor.checks.map((c, i) => (
|
|
222
|
+
<li key={i} className="flex items-start gap-2 text-sm">
|
|
223
|
+
<AlertTriangle
|
|
224
|
+
size={16}
|
|
225
|
+
className={`mt-0.5 shrink-0 ${c.level === "error" ? "text-red-500" : "text-amber-500"}`}
|
|
226
|
+
/>
|
|
227
|
+
<span className="min-w-0">
|
|
228
|
+
<span className="opacity-60">[{c.label}]</span> {c.detail}
|
|
229
|
+
{c.fix ? (
|
|
230
|
+
<code className="mt-1 block overflow-x-auto rounded bg-muted px-1.5 py-0.5 text-xs">
|
|
231
|
+
{c.fix}
|
|
232
|
+
</code>
|
|
233
|
+
) : null}
|
|
234
|
+
</span>
|
|
235
|
+
</li>
|
|
236
|
+
))}
|
|
237
|
+
</ul>
|
|
238
|
+
)}
|
|
239
|
+
</Section>
|
|
240
|
+
|
|
241
|
+
<Section
|
|
242
|
+
title={t("settings.profile.preview_title")}
|
|
243
|
+
description={
|
|
244
|
+
profile?.active
|
|
245
|
+
? t("settings.profile.preview_subtitle")
|
|
246
|
+
: t("settings.profile.preview_inactive")
|
|
247
|
+
}
|
|
248
|
+
>
|
|
249
|
+
<pre
|
|
250
|
+
data-testid="profile-preview"
|
|
251
|
+
className={`max-h-[32rem] overflow-auto whitespace-pre-wrap rounded-md border border-border bg-muted/30 p-3 text-xs leading-relaxed ${
|
|
252
|
+
profile?.active ? "" : "opacity-60"
|
|
253
|
+
}`}
|
|
254
|
+
>
|
|
255
|
+
{profile?.preview || t("settings.profile.preview_empty")}
|
|
256
|
+
</pre>
|
|
257
|
+
</Section>
|
|
258
|
+
</div>
|
|
259
|
+
</div>
|
|
227
260
|
|
|
228
261
|
<Dialog
|
|
229
262
|
open={confirmOff}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import useSWR from "swr";
|
|
2
|
+
import { Inbox, type InboxRow } from "../lib/api/inbox";
|
|
3
|
+
|
|
4
|
+
/** Every agent as a conversation, most recent first, super-agent pinned. */
|
|
5
|
+
export function useInbox(includeEmpty = false) {
|
|
6
|
+
const { data, error, isLoading, mutate } = useSWR<InboxRow[]>(
|
|
7
|
+
`/inbox?include_empty=${includeEmpty ? 1 : 0}`,
|
|
8
|
+
() => Inbox.list(includeEmpty),
|
|
9
|
+
{ refreshInterval: 15_000 },
|
|
10
|
+
);
|
|
11
|
+
return { rows: data ?? [], error, isLoading, mutate };
|
|
12
|
+
}
|
|
@@ -142,6 +142,16 @@ export const en = {
|
|
|
142
142
|
browser_unavailable: "Browser unavailable until daemon restarts. Paste path manually.",
|
|
143
143
|
no_folders: "No folders.",
|
|
144
144
|
},
|
|
145
|
+
inbox: {
|
|
146
|
+
title: "Agent inbox",
|
|
147
|
+
subtitle:
|
|
148
|
+
"Every agent as a conversation, most recent first.",
|
|
149
|
+
empty: "You have not talked to any agent yet.",
|
|
150
|
+
pinned: "lead",
|
|
151
|
+
show_quiet: "Show quiet agents",
|
|
152
|
+
no_reply_yet: "(no replies yet)",
|
|
153
|
+
},
|
|
154
|
+
|
|
145
155
|
settings: {
|
|
146
156
|
title: "Settings",
|
|
147
157
|
subtitle: "Panel preferences + local daemon diagnostics.",
|
|
@@ -203,6 +213,8 @@ export const en = {
|
|
|
203
213
|
title: "Agent profile",
|
|
204
214
|
subtitle:
|
|
205
215
|
"An installable line of work for the super-agent: what it does with its day and when it speaks to you. Distinct from the agent's name (that lives under Identity).",
|
|
216
|
+
active_hint:
|
|
217
|
+
"A profile is active: its block ships on every turn of every channel. Deactivate to go back to vanilla.",
|
|
206
218
|
vanilla_hint:
|
|
207
219
|
"No profile is active. APX behaves exactly as it always has — the super-agent prompt is identical to a clean install.",
|
|
208
220
|
none_available: "No profiles available yet.",
|
|
@@ -227,6 +239,10 @@ export const en = {
|
|
|
227
239
|
preview_title: "Prompt block",
|
|
228
240
|
preview_subtitle: "Exactly what reaches the model, with your values substituted.",
|
|
229
241
|
preview_empty: "(empty)",
|
|
242
|
+
preview_inactive: "This is what the model would receive if you activated this profile.",
|
|
243
|
+
no_settings: "This profile has nothing to configure.",
|
|
244
|
+
settings_locked: "Activate the profile to change its settings.",
|
|
245
|
+
doctor_vanilla: "No profile active. APX behaves as it always has.",
|
|
230
246
|
},
|
|
231
247
|
|
|
232
248
|
identity: {
|
|
@@ -143,6 +143,16 @@ export const es = {
|
|
|
143
143
|
browser_unavailable: "Explorador no disponible hasta reiniciar daemon. Pegá ruta manual.",
|
|
144
144
|
no_folders: "Sin carpetas.",
|
|
145
145
|
},
|
|
146
|
+
inbox: {
|
|
147
|
+
title: "Bandeja de agentes",
|
|
148
|
+
subtitle:
|
|
149
|
+
"Cada agente como una conversación, lo más reciente primero.",
|
|
150
|
+
empty: "Todavía no hablaste con ningún agente.",
|
|
151
|
+
pinned: "principal",
|
|
152
|
+
show_quiet: "Ver los que no hablaron",
|
|
153
|
+
no_reply_yet: "(sin respuestas todavía)",
|
|
154
|
+
},
|
|
155
|
+
|
|
146
156
|
settings: {
|
|
147
157
|
title: "Settings",
|
|
148
158
|
subtitle: "Preferencias del panel + diagnóstico del daemon local.",
|
|
@@ -204,6 +214,8 @@ export const es = {
|
|
|
204
214
|
title: "Perfil del agente",
|
|
205
215
|
subtitle:
|
|
206
216
|
"Un oficio instalable para el super-agente: qué hace con su día y cuándo te habla. Distinto del nombre del agente (eso está en Identidad).",
|
|
217
|
+
active_hint:
|
|
218
|
+
"Hay un perfil activo: su bloque viaja en cada turno de cada canal. Desactivalo para volver a vanilla.",
|
|
207
219
|
vanilla_hint:
|
|
208
220
|
"No hay ningún perfil activo. APX se comporta exactamente como siempre — el prompt del super-agente es idéntico al de una instalación limpia.",
|
|
209
221
|
none_available: "No hay perfiles disponibles todavía.",
|
|
@@ -228,6 +240,10 @@ export const es = {
|
|
|
228
240
|
preview_title: "Bloque de prompt",
|
|
229
241
|
preview_subtitle: "Exactamente lo que recibe el modelo, con tus valores ya sustituidos.",
|
|
230
242
|
preview_empty: "(vacío)",
|
|
243
|
+
preview_inactive: "Esto es lo que recibiría el modelo si activaras este perfil.",
|
|
244
|
+
no_settings: "Este perfil no tiene nada configurable.",
|
|
245
|
+
settings_locked: "Activá el perfil para poder cambiar su configuración.",
|
|
246
|
+
doctor_vanilla: "Sin perfil activo. APX se comporta como siempre.",
|
|
231
247
|
},
|
|
232
248
|
|
|
233
249
|
identity: {
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { http, unwrapPage } from "../http";
|
|
2
|
+
|
|
3
|
+
/** One row of the agent inbox: an agent, and the last thing it said. */
|
|
4
|
+
export interface InboxRow {
|
|
5
|
+
project_id: number | string | null;
|
|
6
|
+
project_name: string | null;
|
|
7
|
+
project_path: string | null;
|
|
8
|
+
agent_slug: string;
|
|
9
|
+
agent_name: string | null;
|
|
10
|
+
agent_emoji: string | null;
|
|
11
|
+
kind: "agent" | "super_agent";
|
|
12
|
+
pinned: boolean;
|
|
13
|
+
conversation_id: string | null;
|
|
14
|
+
channel: string | null;
|
|
15
|
+
messages: number;
|
|
16
|
+
/** What the AGENT last said — not what the user last asked. */
|
|
17
|
+
preview: string | null;
|
|
18
|
+
last_activity_at: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export const Inbox = {
|
|
22
|
+
list: (includeEmpty = false) =>
|
|
23
|
+
http
|
|
24
|
+
.get<unknown>(`/inbox${includeEmpty ? "?include_empty=1" : ""}`)
|
|
25
|
+
.then((b) => unwrapPage<InboxRow>(b).items),
|
|
26
|
+
};
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { useNavigate } from "react-router-dom";
|
|
2
|
+
import { Section } from "../components/Section";
|
|
3
|
+
import { Badge, Button, Empty, Loading } from "../components/ui";
|
|
4
|
+
import { useInbox } from "../hooks/useInbox";
|
|
5
|
+
import type { InboxRow } from "../lib/api/inbox";
|
|
6
|
+
import { t } from "../i18n";
|
|
7
|
+
import { useState } from "react";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* The agent inbox — every agent as a conversation, most recent first.
|
|
11
|
+
*
|
|
12
|
+
* A SECOND AXIS over the same data, not a replacement for project navigation:
|
|
13
|
+
* this is the conversational way in, the project rail is the structural one.
|
|
14
|
+
* Clicking a row opens that agent's existing chat rather than re-implementing
|
|
15
|
+
* one here — the thread UI already exists and must not be forked.
|
|
16
|
+
*/
|
|
17
|
+
export function InboxScreen() {
|
|
18
|
+
const navigate = useNavigate();
|
|
19
|
+
const [includeEmpty, setIncludeEmpty] = useState(false);
|
|
20
|
+
const { rows, isLoading } = useInbox(includeEmpty);
|
|
21
|
+
|
|
22
|
+
const open = (row: InboxRow) => {
|
|
23
|
+
if (row.kind === "super_agent") {
|
|
24
|
+
navigate("/p/0/chat");
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
navigate(`/p/${row.project_id}/agents/${encodeURIComponent(row.agent_slug)}`);
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
return (
|
|
31
|
+
<Section
|
|
32
|
+
fullHeight
|
|
33
|
+
title={t("inbox.title")}
|
|
34
|
+
description={t("inbox.subtitle")}
|
|
35
|
+
action={
|
|
36
|
+
<Button size="sm" variant={includeEmpty ? "primary" : "ghost"} onClick={() => setIncludeEmpty((v) => !v)}>
|
|
37
|
+
{t("inbox.show_quiet")}
|
|
38
|
+
</Button>
|
|
39
|
+
}
|
|
40
|
+
>
|
|
41
|
+
{isLoading ? <Loading /> : null}
|
|
42
|
+
{!isLoading && rows.length === 0 ? <Empty>{t("inbox.empty")}</Empty> : null}
|
|
43
|
+
|
|
44
|
+
<ul className="space-y-2" data-testid="inbox-list">
|
|
45
|
+
{rows.map((row) => (
|
|
46
|
+
<li key={`${row.project_id ?? "global"}-${row.agent_slug}`}>
|
|
47
|
+
<button
|
|
48
|
+
type="button"
|
|
49
|
+
data-testid={`inbox-row-${row.agent_slug}`}
|
|
50
|
+
onClick={() => open(row)}
|
|
51
|
+
className={`flex w-full items-start gap-3 rounded-md border px-3 py-2 text-left transition ${
|
|
52
|
+
row.pinned
|
|
53
|
+
? "border-primary/60 bg-primary/5 hover:bg-primary/10"
|
|
54
|
+
: "border-border bg-muted/30 hover:bg-muted/50"
|
|
55
|
+
}`}
|
|
56
|
+
>
|
|
57
|
+
<span className="mt-0.5 text-lg leading-none">{row.agent_emoji || "🤖"}</span>
|
|
58
|
+
|
|
59
|
+
<span className="min-w-0 flex-1">
|
|
60
|
+
<span className="flex min-w-0 items-baseline gap-2">
|
|
61
|
+
<span className="truncate font-medium">{row.agent_name || row.agent_slug}</span>
|
|
62
|
+
{row.pinned ? <Badge tone="info">{t("inbox.pinned")}</Badge> : null}
|
|
63
|
+
{/* On a phone the timestamp rides with the name; there is no
|
|
64
|
+
room for a third column. */}
|
|
65
|
+
<span className="ml-auto shrink-0 text-xs opacity-50 sm:hidden">
|
|
66
|
+
{shortTime(row.last_activity_at)}
|
|
67
|
+
</span>
|
|
68
|
+
</span>
|
|
69
|
+
|
|
70
|
+
<span className="mt-0.5 flex flex-wrap items-center gap-x-2 text-xs opacity-55">
|
|
71
|
+
{row.project_name ? <span className="truncate">{row.project_name}</span> : null}
|
|
72
|
+
{row.channel ? <span className="opacity-70">· {row.channel}</span> : null}
|
|
73
|
+
</span>
|
|
74
|
+
|
|
75
|
+
{/* The agent's own last line. Echoing the user's prompt back
|
|
76
|
+
would tell them nothing they do not already know. Two lines
|
|
77
|
+
on a phone, one on a wide screen. */}
|
|
78
|
+
<span className="mt-1 block line-clamp-2 text-sm leading-snug opacity-75 sm:line-clamp-1">
|
|
79
|
+
{row.preview || t("inbox.no_reply_yet")}
|
|
80
|
+
</span>
|
|
81
|
+
</span>
|
|
82
|
+
|
|
83
|
+
<span className="hidden shrink-0 text-xs opacity-50 sm:block">
|
|
84
|
+
{shortTime(row.last_activity_at)}
|
|
85
|
+
</span>
|
|
86
|
+
</button>
|
|
87
|
+
</li>
|
|
88
|
+
))}
|
|
89
|
+
</ul>
|
|
90
|
+
</Section>
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** Today → time of day; older → date. Same idea as a messaging app. */
|
|
95
|
+
function shortTime(iso: string): string {
|
|
96
|
+
if (!iso) return "";
|
|
97
|
+
const d = new Date(iso);
|
|
98
|
+
if (Number.isNaN(d.getTime())) return "";
|
|
99
|
+
const sameDay = new Date().toDateString() === d.toDateString();
|
|
100
|
+
return sameDay
|
|
101
|
+
? d.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" })
|
|
102
|
+
: d.toLocaleDateString();
|
|
103
|
+
}
|
|
@@ -70,7 +70,7 @@ const SECTIONS: TabSection[] = [
|
|
|
70
70
|
// on xl (and so wants full available width). Single-section panels (identity,
|
|
71
71
|
// super agent, devices, advanced) keep a cosier reading width so wide displays
|
|
72
72
|
// don't blow form fields up to absurd widths.
|
|
73
|
-
const WIDE_TABS = new Set<TabKey>(["engines", "telegram", "memory", "skills", "web", "voice"]);
|
|
73
|
+
const WIDE_TABS = new Set<TabKey>(["engines", "telegram", "memory", "skills", "web", "voice", "profile"]);
|
|
74
74
|
|
|
75
75
|
const PANELS: Record<TabKey, () => ReactElement> = {
|
|
76
76
|
identity: () => <IdentityPanel />,
|
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
# Rol: Jefe de Gabinete
|
|
2
|
-
|
|
3
|
-
Sos el jefe de gabinete de {{owner_name}}. No un chatbot, no un asistente de código. Sos
|
|
4
|
-
responsable de una cosa por encima de todas: **que no se caiga nada.**
|
|
5
|
-
|
|
6
|
-
Tu unidad de trabajo es **el proyecto**, no el mensaje suelto. Todo lo que pasa por vos
|
|
7
|
-
queda anclado a un proyecto registrado en APX.
|
|
8
|
-
|
|
9
|
-
## De qué sos responsable, por orden de prioridad
|
|
10
|
-
|
|
11
|
-
1. Mantener vivo el estado de cada proyecto — qué se movió, qué está trabado, qué nadie tocó.
|
|
12
|
-
2. Capturar sin fricción — todo lo que se dice y es una tarea, una decisión o una promesa lo
|
|
13
|
-
registrás vos, en el proyecto que corresponde.
|
|
14
|
-
3. Devolver contexto rápido — al saltar de proyecto, en menos de un minuto se sabe dónde
|
|
15
|
-
quedó la cosa.
|
|
16
|
-
4. Avisar antes de que algo se rompa, no después.
|
|
17
|
-
5. Cuidar los compromisos — sobre todo lo que se le prometió a otra persona.
|
|
18
|
-
6. Coordinar a los especialistas, y hablar con una sola voz.
|
|
19
|
-
|
|
20
|
-
## Cómo trabajás
|
|
21
|
-
|
|
22
|
-
**Capturá por default, preguntá poco.** Cuando aparece una tarea, registrala vos. Inferí el
|
|
23
|
-
proyecto cuando puedas y decí en una línea qué guardaste y dónde. Si genuinamente no podés
|
|
24
|
-
saberlo, preguntá con botones, nunca con una pregunta abierta. El sistema se muere el día en
|
|
25
|
-
que anotar algo cuesta más que no anotarlo.
|
|
26
|
-
|
|
27
|
-
**Tareas y compromisos son cosas distintas.** Una tarea es trabajo por hacer. Un compromiso
|
|
28
|
-
se le prometió a una persona concreta, con fecha; incumplirlo tiene costo relacional. Los
|
|
29
|
-
compromisos le ganan a las tareas y se avisan antes.
|
|
30
|
-
|
|
31
|
-
**Nunca inventes estado.** Si no sabés cómo viene un proyecto, decí "no hay actividad
|
|
32
|
-
registrada en X desde Y". Eso es útil. Un resumen inventado destruye la confianza en todo el
|
|
33
|
-
sistema y no vuelve. Preferí siempre el hueco explícito a la suposición prolija.
|
|
34
|
-
|
|
35
|
-
**Escribí como alguien que sabe del tema.** Frases cortas. Sin títulos decorativos, sin
|
|
36
|
-
rituales de saludo, sin seis bullets donde alcanzan dos frases. Lo que importa va primero.
|
|
37
|
-
|
|
38
|
-
**Delegá el trabajo de dominio.** Vos coordinás; no lo hacés. Todo lo que sea código pasa por
|
|
39
|
-
el agente de desarrollo. Cuando varios especialistas reportan, consolidás vos — nunca pases
|
|
40
|
-
la salida cruda de un subagente.
|
|
41
|
-
|
|
42
|
-
**Permisos.** Registrar, reorganizar, preparar e investigar: adelante. Escribir hacia afuera
|
|
43
|
-
—mandar, publicar, tocar un sistema de terceros—: confirmá primero, pero llegá con todo
|
|
44
|
-
listo para que confirmar sea un botón.
|