i18n: Englisch als Hauptsprache, Sprachpakete fuer Desktop und WebUI (#661)
Umbau durch Gemini Flash, Durchsicht und Nacharbeit durch Claude. Umfang: - Desktop: LocalizationManager (JSON-Pakete, Rueckfall auf Englisch, Umschalten zur Laufzeit), Localize-Markup-Erweiterung, Sprachwahl in den Einstellungen (AppSettings.Language, Vorgabe "en"); 317 Schluessel je Sprache - WebUI: alle Ansichten auf t()/data-i18n, 656 Schluessel je Sprache - Agenten-Werkzeuge: Beschreibungen, Schemata und Meldungen auf Englisch - Tests an die englischen Meldungen angepasst, keine Pruefung abgeschwaecht Nacharbeit bei der Durchsicht: - ~25 Kommentarbloecke mit Begruendungen waren ersatzlos entfallen, darunter sicherheitsrelevante (NTFS-Datenstrom beim shared:-Praefix, Bremse gegen Agentenschleifen, doppelter POST beim Anlegen einer Instanz). Wieder eingesetzt: auf Englisch, wo die Datei umgestellt ist, sonst im Wortlaut. - Drei WebUI-Schluessel fehlten (logs.paused, instances.instanceOfflineError, instances.noInstanceActiveError). Weil t() bei fehlendem Schluessel den Schluessel selbst liefert, griff der "|| err.message"-Rueckfall nie — die Oberflaeche haette den Schluesselnamen angezeigt. Geprueft: Schluesselgleichheit en/de, Platzhalter je Schluessel, jeder im Code benutzte Schluessel vorhanden, node --check fuer alle Module, 1.220 Tests gruen. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
69718122f3
commit
ad71682c2d
@@ -22,28 +22,28 @@ public sealed class TaskboardTool : IAgentTool
|
||||
public string Name => "Taskboard";
|
||||
|
||||
public string Description => """
|
||||
Das gemeinsame Aufgabenbrett — überdauert Läufe und Neustarts.
|
||||
The shared taskboard — persists across runs and restarts.
|
||||
|
||||
Nutze es, um Arbeit festzuhalten und zu verteilen, statt sie im Kontext zu
|
||||
behalten: eigene offene Punkte, Aufträge an andere Agenten, wiederkehrende
|
||||
Termine. Eine Aufgabe an einen anderen Agenten legst du mit assignee='@<agentId>'
|
||||
an — sie läuft, wenn sie fällig ist, ohne dass du wartest.
|
||||
Use it to track and distribute work instead of keeping it in context:
|
||||
own open items, assignments to other agents, recurring schedules.
|
||||
To assign a task to another agent, create it with assignee='@<agentId>' —
|
||||
it runs when due, without you waiting.
|
||||
|
||||
Aktionen: task_create, task_find, task_list, task_update, task_comment
|
||||
Actions: task_create, task_find, task_list, task_update, task_comment
|
||||
|
||||
Bevor du etwas meldest: task_find mit dem geplanten Titel. Gibt es die Aufgabe
|
||||
schon, hänge deine Beobachtung mit task_comment an, statt sie erneut anzulegen —
|
||||
sonst weiß niemand, ob ein Befund neu ist oder seit Tagen unbeantwortet liegt.
|
||||
Before reporting something: run task_find with the planned title. If the task
|
||||
already exists, append your observation with task_comment instead of recreating it —
|
||||
otherwise nobody knows whether a finding is new or has been sitting unanswered for days.
|
||||
|
||||
Assignee:
|
||||
- '@new' → frischer Lauf ohne Vorgeschichte
|
||||
- '@<agentId>'→ der genannte Agent mit seinem Kontext
|
||||
- '@human' → wartet auf einen Menschen (Standard, wenn unklar)
|
||||
- '@new' → fresh run without history
|
||||
- '@<agentId>'→ the named agent with their context
|
||||
- '@human' → waits for a human (default if unclear)
|
||||
|
||||
Termine (Feld 'when'): einmalig ('at'), wiederkehrend ('every') oder 'cron'.
|
||||
Die Zeitzone 'tz' ist dabei Pflicht (z.B. 'Europe/Berlin').
|
||||
Schedules ('when' field): one-time ('at'), recurring ('every') or 'cron'.
|
||||
The time zone 'tz' is required (e.g. 'Europe/Berlin').
|
||||
|
||||
Fehlt 'when', ist die Aufgabe einmalig und sofort fällig.
|
||||
If 'when' is missing, the task is one-time and due immediately.
|
||||
""";
|
||||
|
||||
public JsonElement InputSchema { get; } = JsonDocument.Parse("""
|
||||
@@ -53,83 +53,83 @@ public sealed class TaskboardTool : IAgentTool
|
||||
"action": {
|
||||
"type": "string",
|
||||
"enum": ["task_create", "task_find", "task_list", "task_update", "task_comment"],
|
||||
"description": "Die auszuführende Aktion. 'task_find' sucht offene Aufgaben mit ähnlichem Titel — immer vor task_create."
|
||||
"description": "The action to execute. 'task_find' searches open tasks with similar titles — always call before task_create."
|
||||
},
|
||||
"id": {
|
||||
"type": "string",
|
||||
"description": "Id der Aufgabe. Pflicht bei task_update und task_comment."
|
||||
"description": "Task ID. Required for task_update and task_comment."
|
||||
},
|
||||
"title": {
|
||||
"type": "string",
|
||||
"description": "Titel der Aufgabe. Pflicht bei task_create und task_find."
|
||||
"description": "Task title. Required for task_create and task_find."
|
||||
},
|
||||
"force": {
|
||||
"type": "boolean",
|
||||
"description": "task_create: trotz gleichlautender offener Aufgabe wirklich eine zweite anlegen. Nur, wenn es ein anderer Sachverhalt ist."
|
||||
"description": "task_create: create a second task even if a matching open task exists. Only use if it is a different issue."
|
||||
},
|
||||
"body": {
|
||||
"type": "string",
|
||||
"description": "Beschreibung/Auftrag — geht als Aufgabenstellung an den ausführenden Agenten."
|
||||
"description": "Description/assignment — passed as task instructions to executing agent."
|
||||
},
|
||||
"assignee": {
|
||||
"type": "string",
|
||||
"description": "@new | @<agentId> | @human. Standard @human."
|
||||
"description": "@new | @<agentId> | @human. Default @human."
|
||||
},
|
||||
"status": {
|
||||
"type": "string",
|
||||
"enum": ["backlog", "todo", "in_progress", "in_review", "done", "canceled", "blocked"],
|
||||
"description": "Neuer Status (task_update). 'blocked' meldet einen Blocker."
|
||||
"description": "New status (task_update). 'blocked' reports a blocker."
|
||||
},
|
||||
"type": {
|
||||
"type": "string",
|
||||
"enum": ["work", "approval", "human_input"],
|
||||
"description": "Art der Aufgabe. Standard 'work'."
|
||||
"description": "Type of task. Default 'work'."
|
||||
},
|
||||
"priority": {
|
||||
"type": "integer",
|
||||
"description": "1 (niedrig) bis 5 (hoch). Standard 3."
|
||||
"description": "1 (low) to 5 (high). Default 3."
|
||||
},
|
||||
"when": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"kind": { "type": "string", "enum": ["at", "every", "cron"] },
|
||||
"value": { "type": "string", "description": "Zeitpunkt (ISO), Intervall (z.B. '30m') oder Cron-Ausdruck." },
|
||||
"tz": { "type": "string", "description": "IANA-Zeitzone, z.B. 'Europe/Berlin'. PFLICHT." }
|
||||
"value": { "type": "string", "description": "Timestamp (ISO), interval (e.g. '30m') or cron expression." },
|
||||
"tz": { "type": "string", "description": "IANA timezone, e.g. 'Europe/Berlin'. REQUIRED." }
|
||||
},
|
||||
"description": "Termin. Fehlt er, ist die Aufgabe einmalig und sofort fällig."
|
||||
"description": "Schedule. If omitted, the task is one-time and due immediately."
|
||||
},
|
||||
"require_approval": {
|
||||
"type": "boolean",
|
||||
"description": "Wenn true, gilt die Aufgabe erst nach Review als erledigt."
|
||||
"description": "If true, the task is only considered done after review."
|
||||
},
|
||||
"acceptance": {
|
||||
"type": "string",
|
||||
"description": "Abnahmekriterien, gegen die das Ergebnis geprüft wird."
|
||||
"description": "Acceptance criteria against which the result is verified."
|
||||
},
|
||||
"blocked_by": {
|
||||
"type": "array",
|
||||
"items": { "type": "string" },
|
||||
"description": "Ids der Aufgaben, die erst erledigt sein müssen."
|
||||
"description": "IDs of tasks that must be completed first."
|
||||
},
|
||||
"onlyWhenMarketOpen": {
|
||||
"type": "boolean",
|
||||
"description": "Termin nur auslösen, wenn der Markt offen ist."
|
||||
"description": "Only trigger schedule when market is open."
|
||||
},
|
||||
"text": {
|
||||
"type": "string",
|
||||
"description": "Kommentartext (task_comment) — Ergebnis oder Kritik."
|
||||
"description": "Comment text (task_comment) — result or feedback."
|
||||
},
|
||||
"reopen": {
|
||||
"type": "boolean",
|
||||
"description": "task_comment: die Aufgabe wieder auf 'todo' setzen, damit sie erneut läuft."
|
||||
"description": "task_comment: set task back to 'todo' so it runs again."
|
||||
},
|
||||
"search": {
|
||||
"type": "string",
|
||||
"description": "Freitextsuche über Titel und Beschreibung (task_list)."
|
||||
"description": "Free-text search across title and description (task_list)."
|
||||
},
|
||||
"limit": {
|
||||
"type": "integer",
|
||||
"description": "Maximale Trefferzahl bei task_list. Standard 50."
|
||||
"description": "Maximum number of results in task_list. Default 50."
|
||||
}
|
||||
},
|
||||
"required": ["action"]
|
||||
@@ -139,17 +139,17 @@ public sealed class TaskboardTool : IAgentTool
|
||||
public async Task<ToolResult> ExecuteAsync(JsonElement input, AgentToolContext context, CancellationToken ct)
|
||||
{
|
||||
if (context.Tasks is not { } repository)
|
||||
return ToolResult.Fail("Das Taskboard ist für diese Instanz nicht verfügbar.");
|
||||
return ToolResult.Fail("The taskboard is not available for this instance.");
|
||||
|
||||
if (string.IsNullOrWhiteSpace(context.SharedWorkspacePath))
|
||||
return ToolResult.Fail("Kein geteilter Arbeitsbereich konfiguriert — das Taskboard braucht ihn.");
|
||||
return ToolResult.Fail("No shared workspace configured — the taskboard requires one.");
|
||||
|
||||
var tasksDir = Path.Combine(context.SharedWorkspacePath, "tasks");
|
||||
var board = new TaskboardService(repository, tasksDir);
|
||||
|
||||
var action = GetString(input, "action");
|
||||
if (string.IsNullOrWhiteSpace(action))
|
||||
return ToolResult.Fail("'action' ist erforderlich.");
|
||||
return ToolResult.Fail("'action' is required.");
|
||||
|
||||
try
|
||||
{
|
||||
@@ -160,49 +160,49 @@ public sealed class TaskboardTool : IAgentTool
|
||||
"task_list" => await ListAsync(input, board, ct),
|
||||
"task_update" => await UpdateAsync(input, board, ct),
|
||||
"task_comment" => await CommentAsync(input, board, context.AgentId, ct),
|
||||
_ => ToolResult.Fail($"Unbekannte Aktion: {action}")
|
||||
_ => ToolResult.Fail($"Unknown action: {action}")
|
||||
};
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
context.Logger.LogError(ex, "Fehler im Taskboard-Tool bei Aktion {Action}", action);
|
||||
return ToolResult.Fail($"Fehler: {ex.Message}");
|
||||
context.Logger.LogError(ex, "Error in taskboard tool for action {Action}", action);
|
||||
return ToolResult.Fail($"Error: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Aktionen ───
|
||||
|
||||
/// <summary>
|
||||
/// #674: Offene Aufgaben mit ähnlichem Titel — die Frage vor dem Anlegen.
|
||||
/// #674: Searches open tasks with similar titles — the question to ask before creating one.
|
||||
///
|
||||
/// <para>Die Freitextsuche in task_list beantwortet sie nicht: sie findet nur, was
|
||||
/// wörtlich übereinstimmt, und liefert eine Liste, die der Agent selbst deuten muss.
|
||||
/// Hier steht die Antwort im ersten Satz.</para>
|
||||
/// <para>The free-text search in task_list does not answer it: it only finds literal
|
||||
/// matches and returns a list the agent has to interpret itself. Here the answer is in
|
||||
/// the first sentence.</para>
|
||||
/// </summary>
|
||||
private static async Task<ToolResult> FindAsync(
|
||||
JsonElement input, TaskboardService board, CancellationToken ct)
|
||||
{
|
||||
var title = GetString(input, "title") ?? GetString(input, "search");
|
||||
if (string.IsNullOrWhiteSpace(title))
|
||||
return ToolResult.Fail("'title' ist erforderlich — wonach soll gesucht werden?");
|
||||
return ToolResult.Fail("'title' is required — what should be searched for?");
|
||||
|
||||
var treffer = await SucheAehnlicheAsync(board, title, null, ct);
|
||||
if (treffer.Count == 0)
|
||||
return ToolResult.Ok($"Keine offene Aufgabe ähnelt \"{title.Trim()}\". Du kannst sie mit task_create anlegen.");
|
||||
return ToolResult.Ok($"No open task resembles \"{title.Trim()}\". You can create it with task_create.");
|
||||
|
||||
var sb = new StringBuilder();
|
||||
sb.AppendLine($"{treffer.Count} offene Aufgabe(n) ähneln \"{title.Trim()}\":");
|
||||
sb.AppendLine($"{treffer.Count} open task(s) resemble \"{title.Trim()}\":");
|
||||
sb.AppendLine();
|
||||
foreach (var (task, match) in treffer.Take(10))
|
||||
{
|
||||
sb.AppendLine($"{task.Id} [{TaskText.Of(task.Status)}] {task.Title}");
|
||||
sb.AppendLine($" {(match == TaskTitleMatch.Duplicate ? "derselbe Sachverhalt" : "verwandt")}" +
|
||||
$" | Assignee: {task.Assignee} | Priorität: {task.Priority}" +
|
||||
$" | offen seit {task.CreatedAt:dd.MM. HH:mm}");
|
||||
sb.AppendLine($" {(match == TaskTitleMatch.Duplicate ? "same issue" : "related")}" +
|
||||
$" | Assignee: {task.Assignee} | Priority: {task.Priority}" +
|
||||
$" | open since {task.CreatedAt:dd.MM. HH:mm}");
|
||||
}
|
||||
|
||||
sb.AppendLine();
|
||||
sb.Append("Hänge deine Beobachtung mit task_comment an die bestehende Aufgabe an, statt eine zweite anzulegen.");
|
||||
sb.Append("Append your observation to the existing task using task_comment instead of creating a second one.");
|
||||
return ToolResult.Ok(sb.ToString());
|
||||
}
|
||||
|
||||
@@ -219,13 +219,13 @@ public sealed class TaskboardTool : IAgentTool
|
||||
{
|
||||
var title = GetString(input, "title");
|
||||
if (string.IsNullOrWhiteSpace(title))
|
||||
return ToolResult.Fail("'title' ist erforderlich.");
|
||||
return ToolResult.Fail("'title' is required.");
|
||||
|
||||
if (!TryReadWhen(input, out var when, out var whenError))
|
||||
return ToolResult.Fail(whenError!);
|
||||
|
||||
// #674: Derselbe Befund zum zweiten Mal ist keine zweite Aufgabe. Er wird an der
|
||||
// bestehenden vermerkt — damit man später sieht, wie oft und wie lange schon.
|
||||
// #674: The same finding a second time is not a second task. It is recorded on the
|
||||
// existing one — so that later one can see how often and for how long.
|
||||
if (GetBool(input, "force") != true)
|
||||
{
|
||||
var dublette = (await SucheAehnlicheAsync(board, title, null, ct))
|
||||
@@ -235,13 +235,13 @@ public sealed class TaskboardTool : IAgentTool
|
||||
{
|
||||
await board.AddCommentAsync(
|
||||
dublette.Id, author,
|
||||
$"Erneut beobachtet — {author} wollte dieselbe Aufgabe anlegen: \"{title.Trim()}\"",
|
||||
$"Observed again — {author} wanted to create the same task: \"{title.Trim()}\"",
|
||||
ct);
|
||||
|
||||
return ToolResult.Fail(
|
||||
$"Nicht angelegt: {dublette.Id} \"{dublette.Title}\" ist seit {dublette.CreatedAt:dd.MM. HH:mm} " +
|
||||
$"offen und meint dasselbe. Dein erneutes Auftreten wurde dort vermerkt.\n" +
|
||||
$"Ergänze sie mit task_comment, oder lege bei wirklich anderem Sachverhalt mit force=true an.");
|
||||
$"Not created: {dublette.Id} \"{dublette.Title}\" has been open since {dublette.CreatedAt:dd.MM. HH:mm} " +
|
||||
$"and refers to the same issue. Your reoccurrence was recorded there.\n" +
|
||||
$"Add details using task_comment, or use force=true if this is truly a different issue.");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -262,9 +262,9 @@ public sealed class TaskboardTool : IAgentTool
|
||||
|
||||
var created = await board.CreateAsync(definition, ct);
|
||||
return ToolResult.Ok(
|
||||
$"Aufgabe angelegt: {created.Id} — {created.Title}\n" +
|
||||
$"Task created: {created.Id} — {created.Title}\n" +
|
||||
$"Status: {TaskText.Of(created.Status)} | Assignee: {created.Assignee} | " +
|
||||
$"Priorität: {created.Priority}{FormatWhen(created.When)}" + Hinweis(created));
|
||||
$"Priority: {created.Priority}{FormatWhen(created.When)}" + Hinweis(created));
|
||||
}
|
||||
|
||||
private static async Task<ToolResult> ListAsync(
|
||||
@@ -280,7 +280,7 @@ public sealed class TaskboardTool : IAgentTool
|
||||
|
||||
var tasks = await board.ListAsync(query, ct);
|
||||
if (tasks.Count == 0)
|
||||
return ToolResult.Ok("Keine passenden Aufgaben.");
|
||||
return ToolResult.Ok("No matching tasks.");
|
||||
|
||||
return ToolResult.Ok(Render(tasks));
|
||||
}
|
||||
@@ -290,7 +290,7 @@ public sealed class TaskboardTool : IAgentTool
|
||||
{
|
||||
var id = GetString(input, "id");
|
||||
if (string.IsNullOrWhiteSpace(id))
|
||||
return ToolResult.Fail("'id' ist erforderlich.");
|
||||
return ToolResult.Fail("'id' is required.");
|
||||
|
||||
if (!TryReadWhen(input, out var when, out var whenError))
|
||||
return ToolResult.Fail(whenError!);
|
||||
@@ -312,11 +312,11 @@ public sealed class TaskboardTool : IAgentTool
|
||||
}, ct);
|
||||
|
||||
if (result is null)
|
||||
return ToolResult.Fail($"Keine Aufgabe mit der Id '{id}' gefunden.");
|
||||
return ToolResult.Fail($"No task found with ID '{id}'.");
|
||||
|
||||
return ToolResult.Ok(
|
||||
$"Aufgabe {result.Id} aktualisiert.\n" +
|
||||
$"Status: {TaskText.Of(result.Status)} | Assignee: {result.Assignee} | Priorität: {result.Priority}" + Hinweis(result));
|
||||
$"Task {result.Id} updated.\n" +
|
||||
$"Status: {TaskText.Of(result.Status)} | Assignee: {result.Assignee} | Priority: {result.Priority}" + Hinweis(result));
|
||||
}
|
||||
|
||||
private static async Task<ToolResult> CommentAsync(
|
||||
@@ -324,23 +324,23 @@ public sealed class TaskboardTool : IAgentTool
|
||||
{
|
||||
var id = GetString(input, "id");
|
||||
if (string.IsNullOrWhiteSpace(id))
|
||||
return ToolResult.Fail("'id' ist erforderlich.");
|
||||
return ToolResult.Fail("'id' is required.");
|
||||
|
||||
var text = GetString(input, "text");
|
||||
if (string.IsNullOrWhiteSpace(text))
|
||||
return ToolResult.Fail("'text' ist erforderlich — was soll kommentiert werden?");
|
||||
return ToolResult.Fail("'text' is required — what should be commented?");
|
||||
|
||||
var result = await board.AddCommentAsync(id.Trim(), author, text.Trim(), ct);
|
||||
if (result is null)
|
||||
return ToolResult.Fail($"Keine Aufgabe mit der Id '{id}' gefunden.");
|
||||
return ToolResult.Fail($"No task found with ID '{id}'.");
|
||||
|
||||
if (GetBool(input, "reopen") == true && result.Status != TaskItemStatus.Todo)
|
||||
{
|
||||
result = await board.UpdateAsync(id.Trim(), c => c with { Status = TaskItemStatus.Todo }, ct);
|
||||
return ToolResult.Ok($"Kommentar angehängt und Aufgabe {id} wieder geöffnet (todo).");
|
||||
return ToolResult.Ok($"Comment appended and task {id} reopened (todo).");
|
||||
}
|
||||
|
||||
return ToolResult.Ok($"Kommentar an Aufgabe {id} angehängt.");
|
||||
return ToolResult.Ok($"Comment appended to task {id}.");
|
||||
}
|
||||
|
||||
// ─── Darstellung ───
|
||||
@@ -348,7 +348,7 @@ public sealed class TaskboardTool : IAgentTool
|
||||
private static string Render(IReadOnlyList<TaskItem> tasks)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
sb.AppendLine($"{tasks.Count} Aufgabe(n), wichtigste zuerst:");
|
||||
sb.AppendLine($"{tasks.Count} task(s), highest priority first:");
|
||||
sb.AppendLine();
|
||||
|
||||
var shown = 0;
|
||||
@@ -357,13 +357,13 @@ public sealed class TaskboardTool : IAgentTool
|
||||
var block = new StringBuilder();
|
||||
block.Append($"{task.Id} [{TaskText.Of(task.Status)}] {task.Title}");
|
||||
block.AppendLine();
|
||||
block.AppendLine($" Assignee: {task.Assignee} | Priorität: {task.Priority}{FormatWhen(task.When)}");
|
||||
block.AppendLine($" Assignee: {task.Assignee} | Priority: {task.Priority}{FormatWhen(task.When)}");
|
||||
if (task.BlockedBy.Count > 0)
|
||||
block.AppendLine($" Wartet auf: {string.Join(", ", task.BlockedBy)}");
|
||||
block.AppendLine($" Waiting for: {string.Join(", ", task.BlockedBy)}");
|
||||
|
||||
if (sb.Length + block.Length > MaxResultChars)
|
||||
{
|
||||
sb.AppendLine($"[… {tasks.Count - shown} weitere ausgelassen. Grenze die Suche ein.]");
|
||||
sb.AppendLine($"[… {tasks.Count - shown} more omitted. Narrow down your search.]");
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -375,7 +375,7 @@ public sealed class TaskboardTool : IAgentTool
|
||||
}
|
||||
|
||||
private static string FormatWhen(TaskWhen? when)
|
||||
=> when is null ? "" : $" | Termin: {TaskText.Of(when.Kind)} {when.Value} ({when.TimeZone})";
|
||||
=> when is null ? "" : $" | Schedule: {TaskText.Of(when.Kind)} {when.Value} ({when.TimeZone})";
|
||||
|
||||
// ─── Eingabe lesen ───
|
||||
|
||||
@@ -391,21 +391,21 @@ public sealed class TaskboardTool : IAgentTool
|
||||
var kind = TaskText.WhenKind(kindText);
|
||||
if (kind is null)
|
||||
{
|
||||
error = "'when.kind' muss 'at', 'every' oder 'cron' sein.";
|
||||
error = "'when.kind' must be 'at', 'every', or 'cron'.";
|
||||
return false;
|
||||
}
|
||||
|
||||
var value = w.TryGetProperty("value", out var v) ? v.GetString()?.Trim() : null;
|
||||
if (string.IsNullOrWhiteSpace(value))
|
||||
{
|
||||
error = "'when.value' ist erforderlich (Zeitpunkt, Intervall oder Cron-Ausdruck).";
|
||||
error = "'when.value' is required (timestamp, interval, or cron expression).";
|
||||
return false;
|
||||
}
|
||||
|
||||
var tz = w.TryGetProperty("tz", out var t) ? t.GetString()?.Trim() : null;
|
||||
if (string.IsNullOrWhiteSpace(tz))
|
||||
{
|
||||
error = "'when.tz' ist erforderlich — ein Termin ohne Zeitzone ist mehrdeutig (z.B. 'Europe/Berlin').";
|
||||
error = "'when.tz' is required — a schedule without a timezone is ambiguous (e.g. 'Europe/Berlin').";
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -422,7 +422,7 @@ public sealed class TaskboardTool : IAgentTool
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>#598: Wiederkehrend mit @agent - einmal beim Anlegen sagen, was das kostet.</summary>
|
||||
/// <summary>#598: Recurring with @agent - notify once on creation about costs.</summary>
|
||||
private static string Hinweis(TaskItem task)
|
||||
=> TaskHinweise.WachsenderKontext(task) is { } hinweis ? "\n" + hinweis : "";
|
||||
|
||||
|
||||
Reference in New Issue
Block a user